@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/README.md +15 -10
- package/dist/index.d.mts +123 -58
- package/dist/index.d.ts +123 -58
- package/dist/index.js +329 -157
- package/dist/index.mjs +318 -155
- package/package.json +1 -1
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
|
|
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 (
|
|
103
|
-
|
|
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
|
-
|
|
126
|
+
serviceProductId ?? null,
|
|
108
127
|
roles ?? [],
|
|
109
|
-
|
|
128
|
+
subscriptionStatus ?? null,
|
|
110
129
|
billingModelType ?? null,
|
|
111
130
|
measurementType ?? null,
|
|
112
131
|
unitLabel ?? null
|
|
113
|
-
)
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
if (signingVersion === "2") {
|
|
135
|
+
return buildGatewayUserContextStringV2(
|
|
114
136
|
userId,
|
|
115
|
-
plan,
|
|
137
|
+
plan ?? null,
|
|
116
138
|
roles ?? [],
|
|
117
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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:
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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
|
-
|
|
239
|
-
|
|
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())
|
|
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,
|
|
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
|
|
253
|
-
return
|
|
390
|
+
} catch {
|
|
391
|
+
return { ok: false, code: "NETWORK" };
|
|
254
392
|
}
|
|
255
|
-
|
|
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)
|
|
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))
|
|
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
|
|
416
|
+
return { ok: false, code: "PARSE_ERROR", httpStatus };
|
|
268
417
|
}
|
|
269
|
-
if (!data.valid)
|
|
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
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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
|
|
449
|
-
const {
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
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
|
-
|
|
471
|
-
|
|
472
|
-
|
|
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
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
|
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,
|