@tollara/service-sdk 0.0.1 → 0.0.2

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 CHANGED
@@ -25,6 +25,9 @@ __export(index_exports, {
25
25
  DEFAULT_CORE_PATH_PREFIX: () => DEFAULT_CORE_PATH_PREFIX,
26
26
  DEFAULT_GATEWAY_PATH_PREFIX: () => DEFAULT_GATEWAY_PATH_PREFIX,
27
27
  DEFAULT_USAGE_PATH_PREFIX: () => DEFAULT_USAGE_PATH_PREFIX,
28
+ ECS_CORE_PATH_PREFIX: () => ECS_CORE_PATH_PREFIX,
29
+ ECS_GATEWAY_PATH_PREFIX: () => ECS_GATEWAY_PATH_PREFIX,
30
+ ECS_USAGE_PATH_PREFIX: () => ECS_USAGE_PATH_PREFIX,
28
31
  ENV_API_URL: () => ENV_API_URL,
29
32
  ENV_SERVICE_ID: () => ENV_SERVICE_ID,
30
33
  ENV_SERVICE_SECRET: () => ENV_SERVICE_SECRET,
@@ -32,6 +35,7 @@ __export(index_exports, {
32
35
  TollaraHeaders: () => TollaraHeaders,
33
36
  buildGatewayUserContextString: () => buildGatewayUserContextString,
34
37
  buildGatewayUserContextStringV2: () => buildGatewayUserContextStringV2,
38
+ buildGatewayUserContextStringV3: () => buildGatewayUserContextStringV3,
35
39
  buildUsageReportUrl: () => buildUsageReportUrl,
36
40
  calculateHmac: () => calculateHmac,
37
41
  calculateHmacWithTimestamp: () => calculateHmacWithTimestamp,
@@ -42,14 +46,19 @@ __export(index_exports, {
42
46
  getRequestResult: () => getRequestResult,
43
47
  getRequestStatus: () => getRequestStatus,
44
48
  getUserContext: () => getUserContext,
49
+ grantAccess: () => grantAccess,
45
50
  invokeService: () => invokeService,
51
+ isHostedTollaraApiOrigin: () => isHostedTollaraApiOrigin,
52
+ parseUsageBreakdown: () => parseUsageBreakdown,
46
53
  reportCompletion: () => reportCompletion,
47
- reportCompletionFull: () => reportCompletionFull,
48
- reportCompletionWithResult: () => reportCompletionWithResult,
49
54
  reportProgress: () => reportProgress,
50
55
  reportUsage: () => reportUsage,
56
+ resolveCorePathPrefix: () => resolveCorePathPrefix,
57
+ resolveGatewayPathPrefix: () => resolveGatewayPathPrefix,
58
+ resolveUsagePathPrefix: () => resolveUsagePathPrefix,
51
59
  validateHmacSignature: () => validateHmacSignature,
52
60
  validateServiceKey: () => validateServiceKey,
61
+ validateServiceKeyWithOutcome: () => validateServiceKeyWithOutcome,
53
62
  verifyInboundHmac: () => verifyInboundHmac,
54
63
  verifySignature: () => verifySignature,
55
64
  verifySignatureFromHeaders: () => verifySignatureFromHeaders,
@@ -62,10 +71,15 @@ var TollaraHeaders = {
62
71
  SIGNATURE: "X-Tollara-Signature",
63
72
  TIMESTAMP: "X-Tollara-Timestamp",
64
73
  USER_ID: "X-Tollara-User-ID",
74
+ /** @deprecated v1/v2 only; v3 uses SERVICE_PRODUCT_ID */
65
75
  PLAN: "X-Tollara-Plan",
76
+ SERVICE_PRODUCT_ID: "X-Tollara-Service-Product-ID",
66
77
  ROLES: "X-Tollara-Roles",
78
+ /** @deprecated v1 only */
67
79
  QUOTA_REMAINING: "X-Tollara-Quota-Remaining",
80
+ /** @deprecated v1/v2 only; v3 uses SUBSCRIPTION_STATUS */
68
81
  SUBSCRIPTION_ACTIVE: "X-Tollara-Subscription-Active",
82
+ SUBSCRIPTION_STATUS: "X-Tollara-Subscription-Status",
69
83
  BILLING_MODEL: "X-Tollara-Billing-Model",
70
84
  MEASUREMENT_TYPE: "X-Tollara-Measurement-Type",
71
85
  UNIT_LABEL: "X-Tollara-Unit-Label",
@@ -106,6 +120,13 @@ function validateHmacSignature(signature, payloadString, key) {
106
120
  }
107
121
  }
108
122
 
123
+ // src/grantAccess.ts
124
+ var ACCESS_STATUSES = /* @__PURE__ */ new Set(["ACTIVE", "TRIAL", "CANCELLING", "CANCELLING_PENDING"]);
125
+ function grantAccess(subscriptionStatus) {
126
+ if (subscriptionStatus == null || subscriptionStatus === "") return false;
127
+ return ACCESS_STATUSES.has(subscriptionStatus.trim().toUpperCase());
128
+ }
129
+
109
130
  // src/verifier.ts
110
131
  function headerGet(headers, canonicalName) {
111
132
  const target = canonicalName.toLowerCase();
@@ -143,42 +164,69 @@ function buildGatewayUserContextStringV2(userId, plan, roles, subscriptionActive
143
164
  const ul = unitLabel ?? "";
144
165
  return "2" + u + p + r + sub + b + m + ul;
145
166
  }
146
- function verifySignature(serviceSecret, input) {
167
+ function buildGatewayUserContextStringV3(userId, serviceProductId, roles, subscriptionStatus, billingModelType, measurementType, unitLabel) {
168
+ const u = userId ?? "";
169
+ const sp = serviceProductId ?? "";
170
+ const r = roles?.length ? roles.join(",") : "";
171
+ const st = subscriptionStatus ?? "";
172
+ const b = billingModelType ?? "";
173
+ const m = measurementType ?? "";
174
+ const ul = unitLabel ?? "";
175
+ return "3" + u + sp + r + st + b + m + ul;
176
+ }
177
+ function buildUserContextString(input) {
147
178
  const {
148
- signature,
149
- timestamp,
150
- payload,
151
179
  userId,
180
+ serviceProductId,
152
181
  plan,
153
182
  roles,
154
183
  quotaRemaining,
184
+ subscriptionStatus,
155
185
  subscriptionActive,
156
186
  billingModelType,
157
187
  measurementType,
158
188
  unitLabel,
159
189
  signingVersion
160
190
  } = 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(
191
+ if (signingVersion === "3") {
192
+ return buildGatewayUserContextStringV3(
165
193
  userId,
166
- plan,
194
+ serviceProductId ?? null,
167
195
  roles ?? [],
168
- subscriptionActive,
196
+ subscriptionStatus ?? null,
169
197
  billingModelType ?? null,
170
198
  measurementType ?? null,
171
199
  unitLabel ?? null
172
- ) : buildGatewayUserContextString(
200
+ );
201
+ }
202
+ if (signingVersion === "2") {
203
+ return buildGatewayUserContextStringV2(
173
204
  userId,
174
- plan,
205
+ plan ?? null,
175
206
  roles ?? [],
176
- quotaRemaining,
177
- subscriptionActive,
207
+ subscriptionActive ?? false,
178
208
  billingModelType ?? null,
179
209
  measurementType ?? null,
180
210
  unitLabel ?? null
181
211
  );
212
+ }
213
+ return buildGatewayUserContextString(
214
+ userId,
215
+ plan ?? null,
216
+ roles ?? [],
217
+ quotaRemaining ?? null,
218
+ subscriptionActive ?? false,
219
+ billingModelType ?? null,
220
+ measurementType ?? null,
221
+ unitLabel ?? null
222
+ );
223
+ }
224
+ function verifySignature(serviceSecret, input) {
225
+ const { signature, timestamp, payload } = input;
226
+ if (!signature || !timestamp || !serviceSecret) return false;
227
+ try {
228
+ const payloadString = payload == null ? "" : typeof payload === "string" ? payload : JSON.stringify(payload);
229
+ const userContextString = buildUserContextString(input);
182
230
  const dataToSign = payloadString + timestamp + userContextString;
183
231
  const expectedSignature = calculateHmac(dataToSign, serviceSecret);
184
232
  return constantTimeEquals(expectedSignature, signature);
@@ -193,9 +241,11 @@ function verifyInboundHmac(serviceSecret, request) {
193
241
  timestamp: request.timestamp,
194
242
  payload: request.payload,
195
243
  userId: s.userId,
244
+ serviceProductId: s.serviceProductId,
196
245
  plan: s.plan,
197
246
  roles: s.roles ?? [],
198
247
  quotaRemaining: s.quotaRemaining,
248
+ subscriptionStatus: s.subscriptionStatus,
199
249
  subscriptionActive: s.subscriptionActive,
200
250
  billingModelType: s.billingModelType,
201
251
  measurementType: s.measurementType,
@@ -203,10 +253,7 @@ function verifyInboundHmac(serviceSecret, request) {
203
253
  signingVersion: request.signingVersion
204
254
  });
205
255
  }
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;
256
+ function parseSignedUserContextFromHeaders(headers) {
210
257
  const rolesHeader = headerGet(headers, TollaraHeaders.ROLES);
211
258
  const roles = rolesHeader ? rolesHeader.split(",").map((x) => x.trim()).filter(Boolean) : [];
212
259
  let quotaRemaining = headerGet(headers, TollaraHeaders.QUOTA_REMAINING);
@@ -221,16 +268,24 @@ function verifySignatureFromHeaders(serviceSecret, headers, payload) {
221
268
  const billing = headerGet(headers, TollaraHeaders.BILLING_MODEL);
222
269
  const measurement = headerGet(headers, TollaraHeaders.MEASUREMENT_TYPE);
223
270
  const unit = headerGet(headers, TollaraHeaders.UNIT_LABEL);
224
- const signedUserContext = {
271
+ return {
225
272
  userId: headerGet(headers, TollaraHeaders.USER_ID),
273
+ serviceProductId: headerGet(headers, TollaraHeaders.SERVICE_PRODUCT_ID),
226
274
  plan: headerGet(headers, TollaraHeaders.PLAN),
227
275
  roles,
228
276
  quotaRemaining,
277
+ subscriptionStatus: headerGet(headers, TollaraHeaders.SUBSCRIPTION_STATUS),
229
278
  subscriptionActive,
230
279
  billingModelType: billing && billing !== "" ? billing : null,
231
280
  measurementType: measurement && measurement !== "" ? measurement : null,
232
281
  unitLabel: unit && unit !== "" ? unit : null
233
282
  };
283
+ }
284
+ function verifySignatureFromHeaders(serviceSecret, headers, payload) {
285
+ const signature = headerGet(headers, TollaraHeaders.SIGNATURE);
286
+ const timestamp = headerGet(headers, TollaraHeaders.TIMESTAMP);
287
+ if (!signature || !timestamp) return false;
288
+ const signedUserContext = parseSignedUserContextFromHeaders(headers);
234
289
  const signingVersion = headerGet(headers, TollaraHeaders.SIGNING_VERSION);
235
290
  return verifyInboundHmac(serviceSecret, {
236
291
  signature,
@@ -245,28 +300,42 @@ function verifySignatureFromHeadersAndGetUserContext(serviceSecret, headers, pay
245
300
  return getUserContext(headers);
246
301
  }
247
302
  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);
303
+ const s = parseSignedUserContextFromHeaders(headers);
261
304
  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
305
+ userId: s.userId,
306
+ serviceProductId: s.serviceProductId ?? null,
307
+ plan: s.plan ?? null,
308
+ roles: s.roles ?? [],
309
+ quotaRemaining: typeof s.quotaRemaining === "number" ? s.quotaRemaining : s.quotaRemaining != null && s.quotaRemaining !== "" ? Number(s.quotaRemaining) : null,
310
+ subscriptionStatus: s.subscriptionStatus ?? null,
311
+ subscriptionActive: s.subscriptionActive ?? false,
312
+ billingModelType: s.billingModelType ?? null,
313
+ measurementType: s.measurementType ?? null,
314
+ unitLabel: s.unitLabel ?? null
315
+ };
316
+ }
317
+
318
+ // src/usageBreakdown.ts
319
+ function parseUsageBreakdown(raw) {
320
+ if (raw == null || typeof raw !== "object" || Array.isArray(raw)) return null;
321
+ const o = raw;
322
+ const num = (k) => typeof o[k] === "number" ? o[k] : void 0;
323
+ const bool = (k) => typeof o[k] === "boolean" ? o[k] : void 0;
324
+ return {
325
+ unitsUsed: num("unitsUsed"),
326
+ baseUnitsUsed: num("baseUnitsUsed"),
327
+ overageUnits: num("overageUnits"),
328
+ chargeableOverageUnits: num("chargeableOverageUnits"),
329
+ surplusOverageUnits: num("surplusOverageUnits"),
330
+ overageCost: num("overageCost"),
331
+ totalOverageCost: num("totalOverageCost"),
332
+ unitsRemaining: num("unitsRemaining"),
333
+ remainingCredits: num("remainingCredits"),
334
+ remainingSpendingCap: num("remainingSpendingCap"),
335
+ totalUnitsUsedThisCycle: num("totalUnitsUsedThisCycle"),
336
+ isOverLimit: bool("isOverLimit"),
337
+ isOverage: bool("isOverage"),
338
+ isOverageAllowed: bool("isOverageAllowed")
270
339
  };
271
340
  }
272
341
 
@@ -275,6 +344,9 @@ var DEFAULT_API_URL = "https://api.tollara.ai";
275
344
  var DEFAULT_CORE_PATH_PREFIX = "/api/v1";
276
345
  var DEFAULT_GATEWAY_PATH_PREFIX = "/api";
277
346
  var DEFAULT_USAGE_PATH_PREFIX = "/api/usage";
347
+ var ECS_CORE_PATH_PREFIX = "/core/api/v1";
348
+ var ECS_GATEWAY_PATH_PREFIX = "/gateway/api/v1";
349
+ var ECS_USAGE_PATH_PREFIX = "/usage/api/v1";
278
350
 
279
351
  // src/urls.ts
280
352
  function trimTrailingSlashes(s) {
@@ -293,13 +365,88 @@ function resolveBaseUrl(override, fallback) {
293
365
  return trimTrailingSlashes(t || fallback);
294
366
  }
295
367
 
368
+ // src/pathPrefixes.ts
369
+ function isHostedTollaraApiOrigin(origin) {
370
+ try {
371
+ const host = new URL(origin).hostname.toLowerCase();
372
+ if (host === "api.tollara.ai" || host.endsWith(".api.tollara.ai")) {
373
+ return true;
374
+ }
375
+ if (host === "api.ppe.tollara.ai" || host.endsWith(".api.ppe.tollara.ai")) {
376
+ return true;
377
+ }
378
+ return false;
379
+ } catch {
380
+ return false;
381
+ }
382
+ }
383
+ function resolveGatewayPathPrefix(baseUrl, override) {
384
+ const explicit = override?.trim();
385
+ if (explicit) {
386
+ return explicit;
387
+ }
388
+ const origin = resolveBaseUrl(baseUrl, DEFAULT_API_URL);
389
+ return isHostedTollaraApiOrigin(origin) ? ECS_GATEWAY_PATH_PREFIX : DEFAULT_GATEWAY_PATH_PREFIX;
390
+ }
391
+ function resolveCorePathPrefix(baseUrl, override) {
392
+ const explicit = override?.trim();
393
+ if (explicit) {
394
+ return explicit;
395
+ }
396
+ const origin = resolveBaseUrl(baseUrl, DEFAULT_API_URL);
397
+ return isHostedTollaraApiOrigin(origin) ? ECS_CORE_PATH_PREFIX : DEFAULT_CORE_PATH_PREFIX;
398
+ }
399
+ function resolveUsagePathPrefix(baseUrl, override) {
400
+ const explicit = override?.trim();
401
+ if (explicit) {
402
+ return explicit;
403
+ }
404
+ const origin = resolveBaseUrl(baseUrl, DEFAULT_API_URL);
405
+ return isHostedTollaraApiOrigin(origin) ? ECS_USAGE_PATH_PREFIX : DEFAULT_USAGE_PATH_PREFIX;
406
+ }
407
+
296
408
  // src/validationClient.ts
297
- var CACHE_TTL_MS = 6e4;
298
- async function validateServiceKey(params) {
409
+ function invalidKeyFromUnsignedErrorBody(responseText, httpStatus) {
410
+ if (httpStatus !== 401 && httpStatus !== 403) {
411
+ return null;
412
+ }
413
+ try {
414
+ const data = JSON.parse(responseText);
415
+ if (data.valid === false) {
416
+ return {
417
+ ok: false,
418
+ code: "INVALID_KEY",
419
+ message: typeof data.error === "string" ? data.error : void 0,
420
+ httpStatus
421
+ };
422
+ }
423
+ } catch {
424
+ }
425
+ return null;
426
+ }
427
+ function parseValidationResult(data, serviceId) {
428
+ const subscriptionStatus = typeof data.subscriptionStatus === "string" ? data.subscriptionStatus : null;
429
+ return {
430
+ userId: data.userId ?? null,
431
+ serviceId: data.serviceId ?? serviceId ?? null,
432
+ serviceKeyId: typeof data.serviceKeyId === "string" && data.serviceKeyId.length > 0 ? data.serviceKeyId : null,
433
+ serviceProductId: typeof data.serviceProductId === "string" ? data.serviceProductId : null,
434
+ roles: Array.isArray(data.roles) ? data.roles : [],
435
+ subscriptionStatus,
436
+ validationSchemaVersion: typeof data.validationSchemaVersion === "number" ? data.validationSchemaVersion : 0,
437
+ billingModelType: data.billingModelType ?? null,
438
+ measurementType: data.measurementType ?? null,
439
+ unitLabel: data.unitLabel ?? null,
440
+ grantAccess: grantAccess(subscriptionStatus)
441
+ };
442
+ }
443
+ async function validateServiceKeyWithOutcome(params) {
299
444
  const { baseUrl, serviceKey, serviceId, serviceSecret, fetch: fetchFn = fetch } = params;
300
- if (!serviceKey?.trim()) return null;
445
+ if (!serviceKey?.trim()) {
446
+ return { ok: false, code: "MISSING_KEY" };
447
+ }
301
448
  const origin = resolveBaseUrl(baseUrl, DEFAULT_API_URL);
302
- const url = `${joinUrl(origin, DEFAULT_CORE_PATH_PREFIX)}/service-keys/validate`;
449
+ const url = `${joinUrl(origin, resolveCorePathPrefix(baseUrl))}/service-keys/validate`;
303
450
  const body = JSON.stringify({ serviceKey, serviceId, serviceSecret });
304
451
  let res;
305
452
  try {
@@ -308,35 +455,61 @@ async function validateServiceKey(params) {
308
455
  headers: { "Content-Type": "application/json" },
309
456
  body
310
457
  });
311
- } catch (e) {
312
- return null;
458
+ } catch {
459
+ return { ok: false, code: "NETWORK" };
313
460
  }
314
- if (!res.ok) return null;
461
+ const httpStatus = res.status;
315
462
  const responseText = await res.text();
463
+ if (!res.ok) {
464
+ const unsignedInvalid = invalidKeyFromUnsignedErrorBody(responseText, httpStatus);
465
+ if (unsignedInvalid) {
466
+ return unsignedInvalid;
467
+ }
468
+ return { ok: false, code: "HTTP_ERROR", httpStatus };
469
+ }
316
470
  const signature = res.headers.get(TollaraHeaders.SIGNATURE);
317
471
  const timestamp = res.headers.get(TollaraHeaders.TIMESTAMP);
318
- if (!signature || !timestamp) return null;
472
+ if (!signature || !timestamp) {
473
+ return { ok: false, code: "MISSING_SIGNATURE_HEADERS", httpStatus };
474
+ }
319
475
  const dataToVerify = responseText + timestamp;
320
476
  const expectedSig = calculateHmac(dataToVerify, serviceSecret);
321
- if (!constantTimeEquals(expectedSig, signature)) return null;
477
+ if (!constantTimeEquals(expectedSig, signature)) {
478
+ return { ok: false, code: "HMAC_MISMATCH", httpStatus };
479
+ }
322
480
  let data;
323
481
  try {
324
482
  data = JSON.parse(responseText);
325
483
  } catch {
326
- return null;
484
+ return { ok: false, code: "PARSE_ERROR", httpStatus };
327
485
  }
328
- if (!data.valid) return null;
486
+ if (!data.valid) {
487
+ return {
488
+ ok: false,
489
+ code: "INVALID_KEY",
490
+ message: typeof data.error === "string" ? data.error : void 0,
491
+ httpStatus
492
+ };
493
+ }
494
+ return { ok: true, result: parseValidationResult(data, serviceId) };
495
+ }
496
+ async function validateServiceKey(params) {
497
+ const outcome = await validateServiceKeyWithOutcome(params);
498
+ return outcome.ok ? outcome.result : null;
499
+ }
500
+ function parseEstimateResult(data, httpStatus) {
329
501
  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
502
+ sufficientCredits: Boolean(data.sufficientCredits),
503
+ wouldExceedCap: Boolean(data.wouldExceedCap),
504
+ wouldAllow: Boolean(data.wouldAllow),
505
+ estimatedCost: typeof data.estimatedCost === "number" ? data.estimatedCost : null,
506
+ billingModelType: typeof data.billingModelType === "string" ? data.billingModelType : null,
507
+ measurementType: typeof data.measurementType === "string" ? data.measurementType : null,
508
+ unitLabel: typeof data.unitLabel === "string" ? data.unitLabel : null,
509
+ breakdown: parseUsageBreakdown(data.breakdown),
510
+ estimateSchemaVersion: typeof data.estimateSchemaVersion === "number" ? data.estimateSchemaVersion : 0,
511
+ timestamp: typeof data.timestamp === "number" ? data.timestamp : 0,
512
+ httpStatus
340
513
  };
341
514
  }
342
515
  async function estimateUsage(params) {
@@ -344,7 +517,7 @@ async function estimateUsage(params) {
344
517
  if (!serviceKey?.trim()) return null;
345
518
  if (estimatedUnits == null || !Number.isFinite(estimatedUnits) || estimatedUnits <= 0) return null;
346
519
  const origin = resolveBaseUrl(baseUrl, DEFAULT_API_URL);
347
- const url = `${joinUrl(origin, DEFAULT_CORE_PATH_PREFIX)}/service-keys/estimate-usage`;
520
+ const url = `${joinUrl(origin, resolveCorePathPrefix(baseUrl))}/service-keys/estimate-usage`;
348
521
  const body = JSON.stringify({ serviceKey, serviceId, serviceSecret, estimatedUnits });
349
522
  let res;
350
523
  try {
@@ -370,23 +543,7 @@ async function estimateUsage(params) {
370
543
  } catch {
371
544
  return null;
372
545
  }
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
- };
546
+ return parseEstimateResult(data, code);
390
547
  }
391
548
  async function estimateUsageWithJwt(params) {
392
549
  const {
@@ -401,7 +558,7 @@ async function estimateUsageWithJwt(params) {
401
558
  if (!bearerToken?.trim() || !userId?.trim() || !serviceId?.trim()) return null;
402
559
  if (estimatedUnits == null || !Number.isFinite(estimatedUnits) || estimatedUnits <= 0) return null;
403
560
  const origin = resolveBaseUrl(baseUrl, DEFAULT_API_URL);
404
- const prefix = (corePathPrefix ?? DEFAULT_CORE_PATH_PREFIX).trim();
561
+ const prefix = resolveCorePathPrefix(baseUrl, corePathPrefix);
405
562
  const url = `${joinUrl(origin, prefix)}/billing/usage/estimate`;
406
563
  const body = JSON.stringify({ userId, serviceId, estimatedUnits });
407
564
  let res;
@@ -427,24 +584,9 @@ async function estimateUsageWithJwt(params) {
427
584
  } catch {
428
585
  return null;
429
586
  }
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
- };
587
+ return parseEstimateResult(data, code);
447
588
  }
589
+ var CACHE_TTL_MS = 6e4;
448
590
  function createValidationCache() {
449
591
  const cache = /* @__PURE__ */ new Map();
450
592
  return {
@@ -486,12 +628,15 @@ function usageReportInstantAndEpochSeconds(timestamp) {
486
628
  }
487
629
  function buildUsageReportUrl(baseUrl) {
488
630
  const base = resolveBaseUrl(baseUrl, DEFAULT_API_URL);
489
- let p = DEFAULT_USAGE_PATH_PREFIX.trim();
631
+ let p = resolveUsagePathPrefix(baseUrl).trim();
490
632
  if (!p.startsWith("/")) p = `/${p}`;
491
633
  p = p.replace(/\/$/, "");
492
634
  return `${base}${p}/report`;
493
635
  }
494
636
  function parseUrlParams(url) {
637
+ if (url == null || typeof url !== "string") {
638
+ return { baseUrl: "", signature: null, timestamp: null };
639
+ }
495
640
  let baseUrl = url;
496
641
  let signature = null;
497
642
  let timestamp = null;
@@ -504,17 +649,16 @@ function parseUrlParams(url) {
504
649
  }
505
650
  return { baseUrl, signature, timestamp };
506
651
  }
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);
652
+ async function postSignedUsageCallback(urlWithQuery, bodyString, serviceSecret, fetchFn) {
653
+ const { baseUrl, timestamp } = parseUrlParams(urlWithQuery);
654
+ if (!timestamp) {
655
+ return {
656
+ success: false,
657
+ httpStatus: 0,
658
+ httpStatusText: urlWithQuery ? "Missing timestamp query parameter in URL" : "Missing or invalid callback/progress URL",
659
+ requestUrl: baseUrl
660
+ };
661
+ }
518
662
  const signature = calculateHmacWithTimestamp(bodyString, timestamp, serviceSecret);
519
663
  try {
520
664
  const res = await fetchFn(baseUrl, {
@@ -526,21 +670,37 @@ async function reportProgress(params) {
526
670
  },
527
671
  body: bodyString
528
672
  });
529
- return res.ok;
530
- } catch {
531
- return false;
673
+ const responseBody = await res.text();
674
+ return {
675
+ success: res.ok,
676
+ httpStatus: res.status,
677
+ httpStatusText: res.statusText,
678
+ requestUrl: baseUrl,
679
+ responseBody: responseBody || void 0
680
+ };
681
+ } catch (err) {
682
+ const message = err instanceof Error ? err.message : String(err);
683
+ return {
684
+ success: false,
685
+ httpStatus: 0,
686
+ httpStatusText: "Network error",
687
+ requestUrl: baseUrl,
688
+ networkError: message
689
+ };
532
690
  }
533
691
  }
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 });
692
+ async function reportProgress(params) {
693
+ const { progressUrl, stage, percentageComplete, errorMessage, serviceSecret, fetch: fetchFn = fetch } = params;
694
+ const body = {
695
+ stage,
696
+ percentageComplete,
697
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
698
+ };
699
+ if (errorMessage != null) body.errorMessage = errorMessage;
700
+ return postSignedUsageCallback(progressUrl, JSON.stringify(body), serviceSecret, fetchFn);
539
701
  }
540
- async function reportCompletionFull(params) {
702
+ async function reportCompletion(params) {
541
703
  const { callbackUrl, status, result, resultUrl, contentType, units, serviceSecret, fetch: fetchFn = fetch } = params;
542
- const { baseUrl, timestamp } = parseUrlParams(callbackUrl);
543
- if (!timestamp) return false;
544
704
  const body = {
545
705
  status,
546
706
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
@@ -549,22 +709,7 @@ async function reportCompletionFull(params) {
549
709
  if (result != null) body.result = result;
550
710
  if (resultUrl != null) body.resultUrl = resultUrl;
551
711
  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
- }
712
+ return postSignedUsageCallback(callbackUrl, JSON.stringify(body), serviceSecret, fetchFn);
568
713
  }
569
714
  async function reportUsage(params) {
570
715
  const {
@@ -593,7 +738,18 @@ async function reportUsage(params) {
593
738
  if (!res.ok) {
594
739
  throw new Error(`Usage report failed: ${res.status} ${res.statusText}`);
595
740
  }
596
- return await res.json();
741
+ const json = await res.json();
742
+ return {
743
+ reportSchemaVersion: typeof json.reportSchemaVersion === "number" ? json.reportSchemaVersion : void 0,
744
+ status: typeof json.status === "string" ? json.status : void 0,
745
+ warning: typeof json.warning === "string" ? json.warning : json.warning === null ? null : void 0,
746
+ userId: typeof json.userId === "string" ? json.userId : void 0,
747
+ serviceId: typeof json.serviceId === "string" ? json.serviceId : void 0,
748
+ billingModelType: typeof json.billingModelType === "string" ? json.billingModelType : null,
749
+ measurementType: typeof json.measurementType === "string" ? json.measurementType : null,
750
+ unitLabel: typeof json.unitLabel === "string" ? json.unitLabel : null,
751
+ breakdown: parseUsageBreakdown(json.breakdown)
752
+ };
597
753
  }
598
754
 
599
755
  // src/gatewayClient.ts
@@ -608,7 +764,11 @@ function buildUrl(origin, gatewayPathPrefix, suffix) {
608
764
  }
609
765
  async function getRequestStatus(params) {
610
766
  const { baseUrl, requestId, serviceKey, fetch: fetchFn = fetch } = params;
611
- const url = buildUrl(baseUrl ?? DEFAULT_API_URL, DEFAULT_GATEWAY_PATH_PREFIX, `/requests/${requestId}/status`);
767
+ const url = buildUrl(
768
+ baseUrl ?? DEFAULT_API_URL,
769
+ resolveGatewayPathPrefix(baseUrl),
770
+ `/requests/${requestId}/status`
771
+ );
612
772
  try {
613
773
  const res = await fetchFn(url, {
614
774
  method: "GET",
@@ -622,7 +782,11 @@ async function getRequestStatus(params) {
622
782
  }
623
783
  async function getRequestResult(params) {
624
784
  const { baseUrl, requestId, serviceKey, fetch: fetchFn = fetch } = params;
625
- const url = buildUrl(baseUrl ?? DEFAULT_API_URL, DEFAULT_GATEWAY_PATH_PREFIX, `/requests/${requestId}/result`);
785
+ const url = buildUrl(
786
+ baseUrl ?? DEFAULT_API_URL,
787
+ resolveGatewayPathPrefix(baseUrl),
788
+ `/requests/${requestId}/result`
789
+ );
626
790
  try {
627
791
  const res = await fetchFn(url, {
628
792
  method: "GET",
@@ -654,7 +818,7 @@ async function invokeService(params) {
654
818
  fetch: fetchFn = fetch
655
819
  } = params;
656
820
  const origin = resolveBaseUrl(baseUrl, DEFAULT_API_URL);
657
- const prefix = normalizePrefix2((gatewayPathPrefix ?? DEFAULT_GATEWAY_PATH_PREFIX).trim());
821
+ const prefix = normalizePrefix2(resolveGatewayPathPrefix(baseUrl, gatewayPathPrefix));
658
822
  const path = `${prefix}/service/${serviceId}/endpoint/${endpointId}/invoke${isAsync ? "/async" : ""}`;
659
823
  const url = `${origin}${path}`;
660
824
  const m = method.toUpperCase();
@@ -731,6 +895,15 @@ var TollaraClient = class {
731
895
  fetch: this.fetchFn
732
896
  });
733
897
  }
898
+ async validateServiceKeyWithOutcome(serviceKey) {
899
+ return validateServiceKeyWithOutcome({
900
+ baseUrl: this.apiOrigin,
901
+ serviceKey,
902
+ serviceId: this.serviceId,
903
+ serviceSecret: this.serviceSecret,
904
+ fetch: this.fetchFn
905
+ });
906
+ }
734
907
  async estimateUsage(serviceKey, estimatedUnits) {
735
908
  return estimateUsage({
736
909
  baseUrl: this.apiOrigin,
@@ -792,23 +965,13 @@ var TollaraClient = class {
792
965
  }
793
966
  async sendCompletion(callbackUrl, requestId, status, units, options) {
794
967
  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
968
  return reportCompletion({
809
969
  callbackUrl,
810
970
  requestId,
811
971
  status,
972
+ result,
973
+ resultUrl,
974
+ contentType,
812
975
  units,
813
976
  serviceSecret: this.serviceSecret,
814
977
  fetch: this.fetchFn
@@ -845,6 +1008,9 @@ TollaraClient.DEFAULT_USAGE_PATH_PREFIX = DEFAULT_USAGE_PATH_PREFIX;
845
1008
  DEFAULT_CORE_PATH_PREFIX,
846
1009
  DEFAULT_GATEWAY_PATH_PREFIX,
847
1010
  DEFAULT_USAGE_PATH_PREFIX,
1011
+ ECS_CORE_PATH_PREFIX,
1012
+ ECS_GATEWAY_PATH_PREFIX,
1013
+ ECS_USAGE_PATH_PREFIX,
848
1014
  ENV_API_URL,
849
1015
  ENV_SERVICE_ID,
850
1016
  ENV_SERVICE_SECRET,
@@ -852,6 +1018,7 @@ TollaraClient.DEFAULT_USAGE_PATH_PREFIX = DEFAULT_USAGE_PATH_PREFIX;
852
1018
  TollaraHeaders,
853
1019
  buildGatewayUserContextString,
854
1020
  buildGatewayUserContextStringV2,
1021
+ buildGatewayUserContextStringV3,
855
1022
  buildUsageReportUrl,
856
1023
  calculateHmac,
857
1024
  calculateHmacWithTimestamp,
@@ -862,14 +1029,19 @@ TollaraClient.DEFAULT_USAGE_PATH_PREFIX = DEFAULT_USAGE_PATH_PREFIX;
862
1029
  getRequestResult,
863
1030
  getRequestStatus,
864
1031
  getUserContext,
1032
+ grantAccess,
865
1033
  invokeService,
1034
+ isHostedTollaraApiOrigin,
1035
+ parseUsageBreakdown,
866
1036
  reportCompletion,
867
- reportCompletionFull,
868
- reportCompletionWithResult,
869
1037
  reportProgress,
870
1038
  reportUsage,
1039
+ resolveCorePathPrefix,
1040
+ resolveGatewayPathPrefix,
1041
+ resolveUsagePathPrefix,
871
1042
  validateHmacSignature,
872
1043
  validateServiceKey,
1044
+ validateServiceKeyWithOutcome,
873
1045
  verifyInboundHmac,
874
1046
  verifySignature,
875
1047
  verifySignatureFromHeaders,