@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.mjs CHANGED
@@ -3,10 +3,15 @@ var TollaraHeaders = {
3
3
  SIGNATURE: "X-Tollara-Signature",
4
4
  TIMESTAMP: "X-Tollara-Timestamp",
5
5
  USER_ID: "X-Tollara-User-ID",
6
+ /** @deprecated v1/v2 only; v3 uses SERVICE_PRODUCT_ID */
6
7
  PLAN: "X-Tollara-Plan",
8
+ SERVICE_PRODUCT_ID: "X-Tollara-Service-Product-ID",
7
9
  ROLES: "X-Tollara-Roles",
10
+ /** @deprecated v1 only */
8
11
  QUOTA_REMAINING: "X-Tollara-Quota-Remaining",
12
+ /** @deprecated v1/v2 only; v3 uses SUBSCRIPTION_STATUS */
9
13
  SUBSCRIPTION_ACTIVE: "X-Tollara-Subscription-Active",
14
+ SUBSCRIPTION_STATUS: "X-Tollara-Subscription-Status",
10
15
  BILLING_MODEL: "X-Tollara-Billing-Model",
11
16
  MEASUREMENT_TYPE: "X-Tollara-Measurement-Type",
12
17
  UNIT_LABEL: "X-Tollara-Unit-Label",
@@ -47,6 +52,13 @@ function validateHmacSignature(signature, payloadString, key) {
47
52
  }
48
53
  }
49
54
 
55
+ // src/grantAccess.ts
56
+ var ACCESS_STATUSES = /* @__PURE__ */ new Set(["ACTIVE", "TRIAL", "CANCELLING", "CANCELLING_PENDING"]);
57
+ function grantAccess(subscriptionStatus) {
58
+ if (subscriptionStatus == null || subscriptionStatus === "") return false;
59
+ return ACCESS_STATUSES.has(subscriptionStatus.trim().toUpperCase());
60
+ }
61
+
50
62
  // src/verifier.ts
51
63
  function headerGet(headers, canonicalName) {
52
64
  const target = canonicalName.toLowerCase();
@@ -84,42 +96,69 @@ function buildGatewayUserContextStringV2(userId, plan, roles, subscriptionActive
84
96
  const ul = unitLabel ?? "";
85
97
  return "2" + u + p + r + sub + b + m + ul;
86
98
  }
87
- function verifySignature(serviceSecret, input) {
99
+ function buildGatewayUserContextStringV3(userId, serviceProductId, roles, subscriptionStatus, billingModelType, measurementType, unitLabel) {
100
+ const u = userId ?? "";
101
+ const sp = serviceProductId ?? "";
102
+ const r = roles?.length ? roles.join(",") : "";
103
+ const st = subscriptionStatus ?? "";
104
+ const b = billingModelType ?? "";
105
+ const m = measurementType ?? "";
106
+ const ul = unitLabel ?? "";
107
+ return "3" + u + sp + r + st + b + m + ul;
108
+ }
109
+ function buildUserContextString(input) {
88
110
  const {
89
- signature,
90
- timestamp,
91
- payload,
92
111
  userId,
112
+ serviceProductId,
93
113
  plan,
94
114
  roles,
95
115
  quotaRemaining,
116
+ subscriptionStatus,
96
117
  subscriptionActive,
97
118
  billingModelType,
98
119
  measurementType,
99
120
  unitLabel,
100
121
  signingVersion
101
122
  } = 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(
123
+ if (signingVersion === "3") {
124
+ return buildGatewayUserContextStringV3(
106
125
  userId,
107
- plan,
126
+ serviceProductId ?? null,
108
127
  roles ?? [],
109
- subscriptionActive,
128
+ subscriptionStatus ?? null,
110
129
  billingModelType ?? null,
111
130
  measurementType ?? null,
112
131
  unitLabel ?? null
113
- ) : buildGatewayUserContextString(
132
+ );
133
+ }
134
+ if (signingVersion === "2") {
135
+ return buildGatewayUserContextStringV2(
114
136
  userId,
115
- plan,
137
+ plan ?? null,
116
138
  roles ?? [],
117
- quotaRemaining,
118
- subscriptionActive,
139
+ subscriptionActive ?? false,
119
140
  billingModelType ?? null,
120
141
  measurementType ?? null,
121
142
  unitLabel ?? null
122
143
  );
144
+ }
145
+ return buildGatewayUserContextString(
146
+ userId,
147
+ plan ?? null,
148
+ roles ?? [],
149
+ quotaRemaining ?? null,
150
+ subscriptionActive ?? false,
151
+ billingModelType ?? null,
152
+ measurementType ?? null,
153
+ unitLabel ?? null
154
+ );
155
+ }
156
+ function verifySignature(serviceSecret, input) {
157
+ const { signature, timestamp, payload } = input;
158
+ if (!signature || !timestamp || !serviceSecret) return false;
159
+ try {
160
+ const payloadString = payload == null ? "" : typeof payload === "string" ? payload : JSON.stringify(payload);
161
+ const userContextString = buildUserContextString(input);
123
162
  const dataToSign = payloadString + timestamp + userContextString;
124
163
  const expectedSignature = calculateHmac(dataToSign, serviceSecret);
125
164
  return constantTimeEquals(expectedSignature, signature);
@@ -134,9 +173,11 @@ function verifyInboundHmac(serviceSecret, request) {
134
173
  timestamp: request.timestamp,
135
174
  payload: request.payload,
136
175
  userId: s.userId,
176
+ serviceProductId: s.serviceProductId,
137
177
  plan: s.plan,
138
178
  roles: s.roles ?? [],
139
179
  quotaRemaining: s.quotaRemaining,
180
+ subscriptionStatus: s.subscriptionStatus,
140
181
  subscriptionActive: s.subscriptionActive,
141
182
  billingModelType: s.billingModelType,
142
183
  measurementType: s.measurementType,
@@ -144,10 +185,7 @@ function verifyInboundHmac(serviceSecret, request) {
144
185
  signingVersion: request.signingVersion
145
186
  });
146
187
  }
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;
188
+ function parseSignedUserContextFromHeaders(headers) {
151
189
  const rolesHeader = headerGet(headers, TollaraHeaders.ROLES);
152
190
  const roles = rolesHeader ? rolesHeader.split(",").map((x) => x.trim()).filter(Boolean) : [];
153
191
  let quotaRemaining = headerGet(headers, TollaraHeaders.QUOTA_REMAINING);
@@ -162,16 +200,24 @@ function verifySignatureFromHeaders(serviceSecret, headers, payload) {
162
200
  const billing = headerGet(headers, TollaraHeaders.BILLING_MODEL);
163
201
  const measurement = headerGet(headers, TollaraHeaders.MEASUREMENT_TYPE);
164
202
  const unit = headerGet(headers, TollaraHeaders.UNIT_LABEL);
165
- const signedUserContext = {
203
+ return {
166
204
  userId: headerGet(headers, TollaraHeaders.USER_ID),
205
+ serviceProductId: headerGet(headers, TollaraHeaders.SERVICE_PRODUCT_ID),
167
206
  plan: headerGet(headers, TollaraHeaders.PLAN),
168
207
  roles,
169
208
  quotaRemaining,
209
+ subscriptionStatus: headerGet(headers, TollaraHeaders.SUBSCRIPTION_STATUS),
170
210
  subscriptionActive,
171
211
  billingModelType: billing && billing !== "" ? billing : null,
172
212
  measurementType: measurement && measurement !== "" ? measurement : null,
173
213
  unitLabel: unit && unit !== "" ? unit : null
174
214
  };
215
+ }
216
+ function verifySignatureFromHeaders(serviceSecret, headers, payload) {
217
+ const signature = headerGet(headers, TollaraHeaders.SIGNATURE);
218
+ const timestamp = headerGet(headers, TollaraHeaders.TIMESTAMP);
219
+ if (!signature || !timestamp) return false;
220
+ const signedUserContext = parseSignedUserContextFromHeaders(headers);
175
221
  const signingVersion = headerGet(headers, TollaraHeaders.SIGNING_VERSION);
176
222
  return verifyInboundHmac(serviceSecret, {
177
223
  signature,
@@ -186,28 +232,42 @@ function verifySignatureFromHeadersAndGetUserContext(serviceSecret, headers, pay
186
232
  return getUserContext(headers);
187
233
  }
188
234
  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);
235
+ const s = parseSignedUserContextFromHeaders(headers);
202
236
  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
237
+ userId: s.userId,
238
+ serviceProductId: s.serviceProductId ?? null,
239
+ plan: s.plan ?? null,
240
+ roles: s.roles ?? [],
241
+ quotaRemaining: typeof s.quotaRemaining === "number" ? s.quotaRemaining : s.quotaRemaining != null && s.quotaRemaining !== "" ? Number(s.quotaRemaining) : null,
242
+ subscriptionStatus: s.subscriptionStatus ?? null,
243
+ subscriptionActive: s.subscriptionActive ?? false,
244
+ billingModelType: s.billingModelType ?? null,
245
+ measurementType: s.measurementType ?? null,
246
+ unitLabel: s.unitLabel ?? null
247
+ };
248
+ }
249
+
250
+ // src/usageBreakdown.ts
251
+ function parseUsageBreakdown(raw) {
252
+ if (raw == null || typeof raw !== "object" || Array.isArray(raw)) return null;
253
+ const o = raw;
254
+ const num = (k) => typeof o[k] === "number" ? o[k] : void 0;
255
+ const bool = (k) => typeof o[k] === "boolean" ? o[k] : void 0;
256
+ return {
257
+ unitsUsed: num("unitsUsed"),
258
+ baseUnitsUsed: num("baseUnitsUsed"),
259
+ overageUnits: num("overageUnits"),
260
+ chargeableOverageUnits: num("chargeableOverageUnits"),
261
+ surplusOverageUnits: num("surplusOverageUnits"),
262
+ overageCost: num("overageCost"),
263
+ totalOverageCost: num("totalOverageCost"),
264
+ unitsRemaining: num("unitsRemaining"),
265
+ remainingCredits: num("remainingCredits"),
266
+ remainingSpendingCap: num("remainingSpendingCap"),
267
+ totalUnitsUsedThisCycle: num("totalUnitsUsedThisCycle"),
268
+ isOverLimit: bool("isOverLimit"),
269
+ isOverage: bool("isOverage"),
270
+ isOverageAllowed: bool("isOverageAllowed")
211
271
  };
212
272
  }
213
273
 
@@ -216,6 +276,9 @@ var DEFAULT_API_URL = "https://api.tollara.ai";
216
276
  var DEFAULT_CORE_PATH_PREFIX = "/api/v1";
217
277
  var DEFAULT_GATEWAY_PATH_PREFIX = "/api";
218
278
  var DEFAULT_USAGE_PATH_PREFIX = "/api/usage";
279
+ var ECS_CORE_PATH_PREFIX = "/core/api/v1";
280
+ var ECS_GATEWAY_PATH_PREFIX = "/gateway/api/v1";
281
+ var ECS_USAGE_PATH_PREFIX = "/usage/api/v1";
219
282
 
220
283
  // src/urls.ts
221
284
  function trimTrailingSlashes(s) {
@@ -234,13 +297,88 @@ function resolveBaseUrl(override, fallback) {
234
297
  return trimTrailingSlashes(t || fallback);
235
298
  }
236
299
 
300
+ // src/pathPrefixes.ts
301
+ function isHostedTollaraApiOrigin(origin) {
302
+ try {
303
+ const host = new URL(origin).hostname.toLowerCase();
304
+ if (host === "api.tollara.ai" || host.endsWith(".api.tollara.ai")) {
305
+ return true;
306
+ }
307
+ if (host === "api.ppe.tollara.ai" || host.endsWith(".api.ppe.tollara.ai")) {
308
+ return true;
309
+ }
310
+ return false;
311
+ } catch {
312
+ return false;
313
+ }
314
+ }
315
+ function resolveGatewayPathPrefix(baseUrl, override) {
316
+ const explicit = override?.trim();
317
+ if (explicit) {
318
+ return explicit;
319
+ }
320
+ const origin = resolveBaseUrl(baseUrl, DEFAULT_API_URL);
321
+ return isHostedTollaraApiOrigin(origin) ? ECS_GATEWAY_PATH_PREFIX : DEFAULT_GATEWAY_PATH_PREFIX;
322
+ }
323
+ function resolveCorePathPrefix(baseUrl, override) {
324
+ const explicit = override?.trim();
325
+ if (explicit) {
326
+ return explicit;
327
+ }
328
+ const origin = resolveBaseUrl(baseUrl, DEFAULT_API_URL);
329
+ return isHostedTollaraApiOrigin(origin) ? ECS_CORE_PATH_PREFIX : DEFAULT_CORE_PATH_PREFIX;
330
+ }
331
+ function resolveUsagePathPrefix(baseUrl, override) {
332
+ const explicit = override?.trim();
333
+ if (explicit) {
334
+ return explicit;
335
+ }
336
+ const origin = resolveBaseUrl(baseUrl, DEFAULT_API_URL);
337
+ return isHostedTollaraApiOrigin(origin) ? ECS_USAGE_PATH_PREFIX : DEFAULT_USAGE_PATH_PREFIX;
338
+ }
339
+
237
340
  // src/validationClient.ts
238
- var CACHE_TTL_MS = 6e4;
239
- async function validateServiceKey(params) {
341
+ function invalidKeyFromUnsignedErrorBody(responseText, httpStatus) {
342
+ if (httpStatus !== 401 && httpStatus !== 403) {
343
+ return null;
344
+ }
345
+ try {
346
+ const data = JSON.parse(responseText);
347
+ if (data.valid === false) {
348
+ return {
349
+ ok: false,
350
+ code: "INVALID_KEY",
351
+ message: typeof data.error === "string" ? data.error : void 0,
352
+ httpStatus
353
+ };
354
+ }
355
+ } catch {
356
+ }
357
+ return null;
358
+ }
359
+ function parseValidationResult(data, serviceId) {
360
+ const subscriptionStatus = typeof data.subscriptionStatus === "string" ? data.subscriptionStatus : null;
361
+ return {
362
+ userId: data.userId ?? null,
363
+ serviceId: data.serviceId ?? serviceId ?? null,
364
+ serviceKeyId: typeof data.serviceKeyId === "string" && data.serviceKeyId.length > 0 ? data.serviceKeyId : null,
365
+ serviceProductId: typeof data.serviceProductId === "string" ? data.serviceProductId : null,
366
+ roles: Array.isArray(data.roles) ? data.roles : [],
367
+ subscriptionStatus,
368
+ validationSchemaVersion: typeof data.validationSchemaVersion === "number" ? data.validationSchemaVersion : 0,
369
+ billingModelType: data.billingModelType ?? null,
370
+ measurementType: data.measurementType ?? null,
371
+ unitLabel: data.unitLabel ?? null,
372
+ grantAccess: grantAccess(subscriptionStatus)
373
+ };
374
+ }
375
+ async function validateServiceKeyWithOutcome(params) {
240
376
  const { baseUrl, serviceKey, serviceId, serviceSecret, fetch: fetchFn = fetch } = params;
241
- if (!serviceKey?.trim()) return null;
377
+ if (!serviceKey?.trim()) {
378
+ return { ok: false, code: "MISSING_KEY" };
379
+ }
242
380
  const origin = resolveBaseUrl(baseUrl, DEFAULT_API_URL);
243
- const url = `${joinUrl(origin, DEFAULT_CORE_PATH_PREFIX)}/service-keys/validate`;
381
+ const url = `${joinUrl(origin, resolveCorePathPrefix(baseUrl))}/service-keys/validate`;
244
382
  const body = JSON.stringify({ serviceKey, serviceId, serviceSecret });
245
383
  let res;
246
384
  try {
@@ -249,35 +387,61 @@ async function validateServiceKey(params) {
249
387
  headers: { "Content-Type": "application/json" },
250
388
  body
251
389
  });
252
- } catch (e) {
253
- return null;
390
+ } catch {
391
+ return { ok: false, code: "NETWORK" };
254
392
  }
255
- if (!res.ok) return null;
393
+ const httpStatus = res.status;
256
394
  const responseText = await res.text();
395
+ if (!res.ok) {
396
+ const unsignedInvalid = invalidKeyFromUnsignedErrorBody(responseText, httpStatus);
397
+ if (unsignedInvalid) {
398
+ return unsignedInvalid;
399
+ }
400
+ return { ok: false, code: "HTTP_ERROR", httpStatus };
401
+ }
257
402
  const signature = res.headers.get(TollaraHeaders.SIGNATURE);
258
403
  const timestamp = res.headers.get(TollaraHeaders.TIMESTAMP);
259
- if (!signature || !timestamp) return null;
404
+ if (!signature || !timestamp) {
405
+ return { ok: false, code: "MISSING_SIGNATURE_HEADERS", httpStatus };
406
+ }
260
407
  const dataToVerify = responseText + timestamp;
261
408
  const expectedSig = calculateHmac(dataToVerify, serviceSecret);
262
- if (!constantTimeEquals(expectedSig, signature)) return null;
409
+ if (!constantTimeEquals(expectedSig, signature)) {
410
+ return { ok: false, code: "HMAC_MISMATCH", httpStatus };
411
+ }
263
412
  let data;
264
413
  try {
265
414
  data = JSON.parse(responseText);
266
415
  } catch {
267
- return null;
416
+ return { ok: false, code: "PARSE_ERROR", httpStatus };
268
417
  }
269
- if (!data.valid) return null;
418
+ if (!data.valid) {
419
+ return {
420
+ ok: false,
421
+ code: "INVALID_KEY",
422
+ message: typeof data.error === "string" ? data.error : void 0,
423
+ httpStatus
424
+ };
425
+ }
426
+ return { ok: true, result: parseValidationResult(data, serviceId) };
427
+ }
428
+ async function validateServiceKey(params) {
429
+ const outcome = await validateServiceKeyWithOutcome(params);
430
+ return outcome.ok ? outcome.result : null;
431
+ }
432
+ function parseEstimateResult(data, httpStatus) {
270
433
  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
434
+ sufficientCredits: Boolean(data.sufficientCredits),
435
+ wouldExceedCap: Boolean(data.wouldExceedCap),
436
+ wouldAllow: Boolean(data.wouldAllow),
437
+ estimatedCost: typeof data.estimatedCost === "number" ? data.estimatedCost : null,
438
+ billingModelType: typeof data.billingModelType === "string" ? data.billingModelType : null,
439
+ measurementType: typeof data.measurementType === "string" ? data.measurementType : null,
440
+ unitLabel: typeof data.unitLabel === "string" ? data.unitLabel : null,
441
+ breakdown: parseUsageBreakdown(data.breakdown),
442
+ estimateSchemaVersion: typeof data.estimateSchemaVersion === "number" ? data.estimateSchemaVersion : 0,
443
+ timestamp: typeof data.timestamp === "number" ? data.timestamp : 0,
444
+ httpStatus
281
445
  };
282
446
  }
283
447
  async function estimateUsage(params) {
@@ -285,7 +449,7 @@ async function estimateUsage(params) {
285
449
  if (!serviceKey?.trim()) return null;
286
450
  if (estimatedUnits == null || !Number.isFinite(estimatedUnits) || estimatedUnits <= 0) return null;
287
451
  const origin = resolveBaseUrl(baseUrl, DEFAULT_API_URL);
288
- const url = `${joinUrl(origin, DEFAULT_CORE_PATH_PREFIX)}/service-keys/estimate-usage`;
452
+ const url = `${joinUrl(origin, resolveCorePathPrefix(baseUrl))}/service-keys/estimate-usage`;
289
453
  const body = JSON.stringify({ serviceKey, serviceId, serviceSecret, estimatedUnits });
290
454
  let res;
291
455
  try {
@@ -311,23 +475,7 @@ async function estimateUsage(params) {
311
475
  } catch {
312
476
  return null;
313
477
  }
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
- };
478
+ return parseEstimateResult(data, code);
331
479
  }
332
480
  async function estimateUsageWithJwt(params) {
333
481
  const {
@@ -342,7 +490,7 @@ async function estimateUsageWithJwt(params) {
342
490
  if (!bearerToken?.trim() || !userId?.trim() || !serviceId?.trim()) return null;
343
491
  if (estimatedUnits == null || !Number.isFinite(estimatedUnits) || estimatedUnits <= 0) return null;
344
492
  const origin = resolveBaseUrl(baseUrl, DEFAULT_API_URL);
345
- const prefix = (corePathPrefix ?? DEFAULT_CORE_PATH_PREFIX).trim();
493
+ const prefix = resolveCorePathPrefix(baseUrl, corePathPrefix);
346
494
  const url = `${joinUrl(origin, prefix)}/billing/usage/estimate`;
347
495
  const body = JSON.stringify({ userId, serviceId, estimatedUnits });
348
496
  let res;
@@ -368,24 +516,9 @@ async function estimateUsageWithJwt(params) {
368
516
  } catch {
369
517
  return null;
370
518
  }
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
- };
519
+ return parseEstimateResult(data, code);
388
520
  }
521
+ var CACHE_TTL_MS = 6e4;
389
522
  function createValidationCache() {
390
523
  const cache = /* @__PURE__ */ new Map();
391
524
  return {
@@ -427,12 +560,15 @@ function usageReportInstantAndEpochSeconds(timestamp) {
427
560
  }
428
561
  function buildUsageReportUrl(baseUrl) {
429
562
  const base = resolveBaseUrl(baseUrl, DEFAULT_API_URL);
430
- let p = DEFAULT_USAGE_PATH_PREFIX.trim();
563
+ let p = resolveUsagePathPrefix(baseUrl).trim();
431
564
  if (!p.startsWith("/")) p = `/${p}`;
432
565
  p = p.replace(/\/$/, "");
433
566
  return `${base}${p}/report`;
434
567
  }
435
568
  function parseUrlParams(url) {
569
+ if (url == null || typeof url !== "string") {
570
+ return { baseUrl: "", signature: null, timestamp: null };
571
+ }
436
572
  let baseUrl = url;
437
573
  let signature = null;
438
574
  let timestamp = null;
@@ -445,17 +581,16 @@ function parseUrlParams(url) {
445
581
  }
446
582
  return { baseUrl, signature, timestamp };
447
583
  }
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);
584
+ async function postSignedUsageCallback(urlWithQuery, bodyString, serviceSecret, fetchFn) {
585
+ const { baseUrl, timestamp } = parseUrlParams(urlWithQuery);
586
+ if (!timestamp) {
587
+ return {
588
+ success: false,
589
+ httpStatus: 0,
590
+ httpStatusText: urlWithQuery ? "Missing timestamp query parameter in URL" : "Missing or invalid callback/progress URL",
591
+ requestUrl: baseUrl
592
+ };
593
+ }
459
594
  const signature = calculateHmacWithTimestamp(bodyString, timestamp, serviceSecret);
460
595
  try {
461
596
  const res = await fetchFn(baseUrl, {
@@ -467,21 +602,37 @@ async function reportProgress(params) {
467
602
  },
468
603
  body: bodyString
469
604
  });
470
- return res.ok;
471
- } catch {
472
- return false;
605
+ const responseBody = await res.text();
606
+ return {
607
+ success: res.ok,
608
+ httpStatus: res.status,
609
+ httpStatusText: res.statusText,
610
+ requestUrl: baseUrl,
611
+ responseBody: responseBody || void 0
612
+ };
613
+ } catch (err) {
614
+ const message = err instanceof Error ? err.message : String(err);
615
+ return {
616
+ success: false,
617
+ httpStatus: 0,
618
+ httpStatusText: "Network error",
619
+ requestUrl: baseUrl,
620
+ networkError: message
621
+ };
473
622
  }
474
623
  }
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 });
624
+ async function reportProgress(params) {
625
+ const { progressUrl, stage, percentageComplete, errorMessage, serviceSecret, fetch: fetchFn = fetch } = params;
626
+ const body = {
627
+ stage,
628
+ percentageComplete,
629
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
630
+ };
631
+ if (errorMessage != null) body.errorMessage = errorMessage;
632
+ return postSignedUsageCallback(progressUrl, JSON.stringify(body), serviceSecret, fetchFn);
480
633
  }
481
- async function reportCompletionFull(params) {
634
+ async function reportCompletion(params) {
482
635
  const { callbackUrl, status, result, resultUrl, contentType, units, serviceSecret, fetch: fetchFn = fetch } = params;
483
- const { baseUrl, timestamp } = parseUrlParams(callbackUrl);
484
- if (!timestamp) return false;
485
636
  const body = {
486
637
  status,
487
638
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
@@ -490,22 +641,7 @@ async function reportCompletionFull(params) {
490
641
  if (result != null) body.result = result;
491
642
  if (resultUrl != null) body.resultUrl = resultUrl;
492
643
  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
- }
644
+ return postSignedUsageCallback(callbackUrl, JSON.stringify(body), serviceSecret, fetchFn);
509
645
  }
510
646
  async function reportUsage(params) {
511
647
  const {
@@ -534,7 +670,18 @@ async function reportUsage(params) {
534
670
  if (!res.ok) {
535
671
  throw new Error(`Usage report failed: ${res.status} ${res.statusText}`);
536
672
  }
537
- return await res.json();
673
+ const json = await res.json();
674
+ return {
675
+ reportSchemaVersion: typeof json.reportSchemaVersion === "number" ? json.reportSchemaVersion : void 0,
676
+ status: typeof json.status === "string" ? json.status : void 0,
677
+ warning: typeof json.warning === "string" ? json.warning : json.warning === null ? null : void 0,
678
+ userId: typeof json.userId === "string" ? json.userId : void 0,
679
+ serviceId: typeof json.serviceId === "string" ? json.serviceId : void 0,
680
+ billingModelType: typeof json.billingModelType === "string" ? json.billingModelType : null,
681
+ measurementType: typeof json.measurementType === "string" ? json.measurementType : null,
682
+ unitLabel: typeof json.unitLabel === "string" ? json.unitLabel : null,
683
+ breakdown: parseUsageBreakdown(json.breakdown)
684
+ };
538
685
  }
539
686
 
540
687
  // src/gatewayClient.ts
@@ -549,7 +696,11 @@ function buildUrl(origin, gatewayPathPrefix, suffix) {
549
696
  }
550
697
  async function getRequestStatus(params) {
551
698
  const { baseUrl, requestId, serviceKey, fetch: fetchFn = fetch } = params;
552
- const url = buildUrl(baseUrl ?? DEFAULT_API_URL, DEFAULT_GATEWAY_PATH_PREFIX, `/requests/${requestId}/status`);
699
+ const url = buildUrl(
700
+ baseUrl ?? DEFAULT_API_URL,
701
+ resolveGatewayPathPrefix(baseUrl),
702
+ `/requests/${requestId}/status`
703
+ );
553
704
  try {
554
705
  const res = await fetchFn(url, {
555
706
  method: "GET",
@@ -563,7 +714,11 @@ async function getRequestStatus(params) {
563
714
  }
564
715
  async function getRequestResult(params) {
565
716
  const { baseUrl, requestId, serviceKey, fetch: fetchFn = fetch } = params;
566
- const url = buildUrl(baseUrl ?? DEFAULT_API_URL, DEFAULT_GATEWAY_PATH_PREFIX, `/requests/${requestId}/result`);
717
+ const url = buildUrl(
718
+ baseUrl ?? DEFAULT_API_URL,
719
+ resolveGatewayPathPrefix(baseUrl),
720
+ `/requests/${requestId}/result`
721
+ );
567
722
  try {
568
723
  const res = await fetchFn(url, {
569
724
  method: "GET",
@@ -595,7 +750,7 @@ async function invokeService(params) {
595
750
  fetch: fetchFn = fetch
596
751
  } = params;
597
752
  const origin = resolveBaseUrl(baseUrl, DEFAULT_API_URL);
598
- const prefix = normalizePrefix2((gatewayPathPrefix ?? DEFAULT_GATEWAY_PATH_PREFIX).trim());
753
+ const prefix = normalizePrefix2(resolveGatewayPathPrefix(baseUrl, gatewayPathPrefix));
599
754
  const path = `${prefix}/service/${serviceId}/endpoint/${endpointId}/invoke${isAsync ? "/async" : ""}`;
600
755
  const url = `${origin}${path}`;
601
756
  const m = method.toUpperCase();
@@ -672,6 +827,15 @@ var TollaraClient = class {
672
827
  fetch: this.fetchFn
673
828
  });
674
829
  }
830
+ async validateServiceKeyWithOutcome(serviceKey) {
831
+ return validateServiceKeyWithOutcome({
832
+ baseUrl: this.apiOrigin,
833
+ serviceKey,
834
+ serviceId: this.serviceId,
835
+ serviceSecret: this.serviceSecret,
836
+ fetch: this.fetchFn
837
+ });
838
+ }
675
839
  async estimateUsage(serviceKey, estimatedUnits) {
676
840
  return estimateUsage({
677
841
  baseUrl: this.apiOrigin,
@@ -733,23 +897,13 @@ var TollaraClient = class {
733
897
  }
734
898
  async sendCompletion(callbackUrl, requestId, status, units, options) {
735
899
  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
900
  return reportCompletion({
750
901
  callbackUrl,
751
902
  requestId,
752
903
  status,
904
+ result,
905
+ resultUrl,
906
+ contentType,
753
907
  units,
754
908
  serviceSecret: this.serviceSecret,
755
909
  fetch: this.fetchFn
@@ -785,6 +939,9 @@ export {
785
939
  DEFAULT_CORE_PATH_PREFIX,
786
940
  DEFAULT_GATEWAY_PATH_PREFIX,
787
941
  DEFAULT_USAGE_PATH_PREFIX,
942
+ ECS_CORE_PATH_PREFIX,
943
+ ECS_GATEWAY_PATH_PREFIX,
944
+ ECS_USAGE_PATH_PREFIX,
788
945
  ENV_API_URL,
789
946
  ENV_SERVICE_ID,
790
947
  ENV_SERVICE_SECRET,
@@ -792,6 +949,7 @@ export {
792
949
  TollaraHeaders,
793
950
  buildGatewayUserContextString,
794
951
  buildGatewayUserContextStringV2,
952
+ buildGatewayUserContextStringV3,
795
953
  buildUsageReportUrl,
796
954
  calculateHmac,
797
955
  calculateHmacWithTimestamp,
@@ -802,14 +960,19 @@ export {
802
960
  getRequestResult,
803
961
  getRequestStatus,
804
962
  getUserContext,
963
+ grantAccess,
805
964
  invokeService,
965
+ isHostedTollaraApiOrigin,
966
+ parseUsageBreakdown,
806
967
  reportCompletion,
807
- reportCompletionFull,
808
- reportCompletionWithResult,
809
968
  reportProgress,
810
969
  reportUsage,
970
+ resolveCorePathPrefix,
971
+ resolveGatewayPathPrefix,
972
+ resolveUsagePathPrefix,
811
973
  validateHmacSignature,
812
974
  validateServiceKey,
975
+ validateServiceKeyWithOutcome,
813
976
  verifyInboundHmac,
814
977
  verifySignature,
815
978
  verifySignatureFromHeaders,