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