authtara-sdk 1.0.0
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 +420 -0
- package/dist/index.d.mts +450 -0
- package/dist/index.d.ts +450 -0
- package/dist/index.js +372 -0
- package/dist/index.mjs +340 -0
- package/package.json +72 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
ApiError: () => ApiError,
|
|
24
|
+
Authtara: () => Authtara,
|
|
25
|
+
AuthtaraError: () => AuthtaraError,
|
|
26
|
+
ConfigurationError: () => ConfigurationError,
|
|
27
|
+
EntitlementDeniedError: () => EntitlementDeniedError,
|
|
28
|
+
InvalidTokenError: () => InvalidTokenError,
|
|
29
|
+
default: () => index_default
|
|
30
|
+
});
|
|
31
|
+
module.exports = __toCommonJS(index_exports);
|
|
32
|
+
|
|
33
|
+
// src/common/errors.ts
|
|
34
|
+
var AuthtaraError = class _AuthtaraError extends Error {
|
|
35
|
+
constructor(message, code, statusCode) {
|
|
36
|
+
super(message);
|
|
37
|
+
this.name = "AuthtaraError";
|
|
38
|
+
this.code = code;
|
|
39
|
+
this.statusCode = statusCode;
|
|
40
|
+
Object.setPrototypeOf(this, _AuthtaraError.prototype);
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
var InvalidTokenError = class _InvalidTokenError extends AuthtaraError {
|
|
44
|
+
constructor(message = "Invalid or expired token") {
|
|
45
|
+
super(message, "INVALID_TOKEN", 401);
|
|
46
|
+
this.name = "InvalidTokenError";
|
|
47
|
+
Object.setPrototypeOf(this, _InvalidTokenError.prototype);
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
var EntitlementDeniedError = class _EntitlementDeniedError extends AuthtaraError {
|
|
51
|
+
constructor(message = "Access denied", reason) {
|
|
52
|
+
super(message, "ENTITLEMENT_DENIED", 403);
|
|
53
|
+
this.name = "EntitlementDeniedError";
|
|
54
|
+
this.reason = reason;
|
|
55
|
+
Object.setPrototypeOf(this, _EntitlementDeniedError.prototype);
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
var ApiError = class _ApiError extends AuthtaraError {
|
|
59
|
+
constructor(message, statusCode, response) {
|
|
60
|
+
super(message, "API_ERROR", statusCode);
|
|
61
|
+
this.name = "ApiError";
|
|
62
|
+
this.response = response;
|
|
63
|
+
Object.setPrototypeOf(this, _ApiError.prototype);
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
var ConfigurationError = class _ConfigurationError extends AuthtaraError {
|
|
67
|
+
constructor(message) {
|
|
68
|
+
super(message, "CONFIG_ERROR");
|
|
69
|
+
this.name = "ConfigurationError";
|
|
70
|
+
Object.setPrototypeOf(this, _ConfigurationError.prototype);
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
// src/common/http-client.ts
|
|
75
|
+
function extractErrorMessage(error, fallback) {
|
|
76
|
+
if (!error) return fallback;
|
|
77
|
+
if (typeof error === "string") return error;
|
|
78
|
+
if (typeof error === "object" && error.message) return error.message;
|
|
79
|
+
return fallback;
|
|
80
|
+
}
|
|
81
|
+
var HttpClient = class {
|
|
82
|
+
constructor(config) {
|
|
83
|
+
if (!config.baseUrl) {
|
|
84
|
+
throw new ConfigurationError("baseUrl is required");
|
|
85
|
+
}
|
|
86
|
+
if (!config.apiKey) {
|
|
87
|
+
throw new ConfigurationError("apiKey is required");
|
|
88
|
+
}
|
|
89
|
+
this.baseUrl = config.baseUrl.replace(/\/$/, "");
|
|
90
|
+
this.apiKey = config.apiKey;
|
|
91
|
+
this.timeout = config.timeout ?? 3e4;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Make HTTP request to Platform API
|
|
95
|
+
*/
|
|
96
|
+
async request(options) {
|
|
97
|
+
const url = `${this.baseUrl}${options.path}`;
|
|
98
|
+
const headers = {
|
|
99
|
+
"Content-Type": "application/json",
|
|
100
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
101
|
+
...options.headers
|
|
102
|
+
};
|
|
103
|
+
const controller = new AbortController();
|
|
104
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
105
|
+
try {
|
|
106
|
+
const response = await fetch(url, {
|
|
107
|
+
method: options.method,
|
|
108
|
+
headers,
|
|
109
|
+
body: options.body ? JSON.stringify(options.body) : void 0,
|
|
110
|
+
signal: controller.signal
|
|
111
|
+
});
|
|
112
|
+
clearTimeout(timeoutId);
|
|
113
|
+
const data = await response.json();
|
|
114
|
+
if (!response.ok) {
|
|
115
|
+
throw new ApiError(
|
|
116
|
+
extractErrorMessage(data.error, `HTTP ${response.status}: ${response.statusText}`),
|
|
117
|
+
response.status,
|
|
118
|
+
data
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
if (!data.success) {
|
|
122
|
+
throw new ApiError(extractErrorMessage(data.error, "API request failed"), 400, data);
|
|
123
|
+
}
|
|
124
|
+
return data.data;
|
|
125
|
+
} catch (error) {
|
|
126
|
+
clearTimeout(timeoutId);
|
|
127
|
+
if (error instanceof ApiError) {
|
|
128
|
+
throw error;
|
|
129
|
+
}
|
|
130
|
+
if (error instanceof Error) {
|
|
131
|
+
if (error.name === "AbortError") {
|
|
132
|
+
throw new ApiError(`Request timeout after ${this.timeout}ms`, 408);
|
|
133
|
+
}
|
|
134
|
+
throw new ApiError(error.message, 500);
|
|
135
|
+
}
|
|
136
|
+
throw new ApiError("Unknown error occurred", 500);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* POST request shorthand
|
|
141
|
+
*/
|
|
142
|
+
async post(path, body) {
|
|
143
|
+
return this.request({ method: "POST", path, body });
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* GET request shorthand
|
|
147
|
+
*/
|
|
148
|
+
async get(path) {
|
|
149
|
+
return this.request({ method: "GET", path });
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
// src/auth/index.ts
|
|
154
|
+
var import_jose = require("jose");
|
|
155
|
+
var AuthModule = class {
|
|
156
|
+
constructor(apiKey, httpClient, issuer = "platform.digitalsolution.com") {
|
|
157
|
+
if (!apiKey) {
|
|
158
|
+
throw new ConfigurationError("apiKey is required for AuthModule");
|
|
159
|
+
}
|
|
160
|
+
this.apiKey = apiKey;
|
|
161
|
+
this.httpClient = httpClient;
|
|
162
|
+
this.issuer = issuer;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Verifikasi SSO JWT token secara offline
|
|
166
|
+
*
|
|
167
|
+
* Token diverifikasi menggunakan App Secret (HS256).
|
|
168
|
+
* Ini lebih cepat daripada memanggil API karena tidak memerlukan network request.
|
|
169
|
+
*
|
|
170
|
+
* @param token - JWT token dari SSO callback
|
|
171
|
+
* @returns SessionVerifyResult dengan data user, tenant, dan subscription
|
|
172
|
+
* @throws InvalidTokenError jika token tidak valid atau expired
|
|
173
|
+
*
|
|
174
|
+
* @example
|
|
175
|
+
* ```typescript
|
|
176
|
+
* const session = await ds.auth.verifySession(token);
|
|
177
|
+
* if (session.isValid) {
|
|
178
|
+
* console.log('User:', session.user);
|
|
179
|
+
* console.log('Tenant:', session.tenant);
|
|
180
|
+
* }
|
|
181
|
+
* ```
|
|
182
|
+
*/
|
|
183
|
+
async verifySession(token) {
|
|
184
|
+
if (!token) {
|
|
185
|
+
throw new InvalidTokenError("Token is required");
|
|
186
|
+
}
|
|
187
|
+
try {
|
|
188
|
+
const secretKey = new TextEncoder().encode(this.apiKey);
|
|
189
|
+
const { payload } = await (0, import_jose.jwtVerify)(token, secretKey, {
|
|
190
|
+
algorithms: ["HS256"],
|
|
191
|
+
issuer: this.issuer
|
|
192
|
+
});
|
|
193
|
+
const ssoPayload = payload;
|
|
194
|
+
return {
|
|
195
|
+
isValid: true,
|
|
196
|
+
user: {
|
|
197
|
+
id: ssoPayload.sub ?? "",
|
|
198
|
+
email: ssoPayload.email ?? "",
|
|
199
|
+
name: ssoPayload.name ?? null
|
|
200
|
+
},
|
|
201
|
+
tenant: ssoPayload.tenant,
|
|
202
|
+
subscription: ssoPayload.subscription
|
|
203
|
+
};
|
|
204
|
+
} catch (error) {
|
|
205
|
+
const message = error instanceof Error ? error.message : "Token verification failed";
|
|
206
|
+
throw new InvalidTokenError(message);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Exchange authorization code untuk JWT token (server-to-server)
|
|
211
|
+
*
|
|
212
|
+
* Gunakan ini di callback endpoint untuk menukar authorization code
|
|
213
|
+
* menjadi JWT token yang bisa diverifikasi.
|
|
214
|
+
*
|
|
215
|
+
* @param code - Authorization code dari callback URL
|
|
216
|
+
* @returns ExchangeResult dengan token dan data context
|
|
217
|
+
*
|
|
218
|
+
* @example
|
|
219
|
+
* ```typescript
|
|
220
|
+
* // Di endpoint /api/sso/callback
|
|
221
|
+
* const code = searchParams.get('code');
|
|
222
|
+
* const result = await ds.auth.exchangeCode(code);
|
|
223
|
+
* // Simpan result.token ke session
|
|
224
|
+
* ```
|
|
225
|
+
*/
|
|
226
|
+
async exchangeCode(code) {
|
|
227
|
+
if (!code) {
|
|
228
|
+
throw new ConfigurationError("Authorization code is required");
|
|
229
|
+
}
|
|
230
|
+
return this.httpClient.post("/api/sso/exchange", {
|
|
231
|
+
code,
|
|
232
|
+
appSecret: this.apiKey
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
// src/billing/index.ts
|
|
238
|
+
var BillingModule = class {
|
|
239
|
+
constructor(httpClient) {
|
|
240
|
+
if (!httpClient) {
|
|
241
|
+
throw new ConfigurationError("httpClient is required for BillingModule");
|
|
242
|
+
}
|
|
243
|
+
this.httpClient = httpClient;
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Cek apakah tenant memiliki akses ke aplikasi Anda
|
|
247
|
+
*
|
|
248
|
+
* @param params - Parameter pengecekan (tenantId wajib, featureKey opsional)
|
|
249
|
+
* @returns EntitlementResult dengan status akses
|
|
250
|
+
*
|
|
251
|
+
* @example
|
|
252
|
+
* ```typescript
|
|
253
|
+
* // Cek akses dasar
|
|
254
|
+
* const hasAccess = await ds.billing.checkEntitlement({
|
|
255
|
+
* tenantId: tenant.id
|
|
256
|
+
* });
|
|
257
|
+
*
|
|
258
|
+
* // Cek fitur spesifik
|
|
259
|
+
* const canUseAI = await ds.billing.checkEntitlement({
|
|
260
|
+
* tenantId: tenant.id,
|
|
261
|
+
* featureKey: 'ai_generator'
|
|
262
|
+
* });
|
|
263
|
+
*
|
|
264
|
+
* if (!canUseAI.granted) {
|
|
265
|
+
* throw new Error(canUseAI.reason);
|
|
266
|
+
* }
|
|
267
|
+
* ```
|
|
268
|
+
*/
|
|
269
|
+
async checkEntitlement(params) {
|
|
270
|
+
if (!params.tenantId) {
|
|
271
|
+
throw new ConfigurationError("tenantId is required");
|
|
272
|
+
}
|
|
273
|
+
return this.httpClient.post(
|
|
274
|
+
"/api/v1/dev/entitlements/check",
|
|
275
|
+
{
|
|
276
|
+
tenantId: params.tenantId,
|
|
277
|
+
featureKey: params.featureKey
|
|
278
|
+
}
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
// src/metering/index.ts
|
|
284
|
+
var MeteringModule = class {
|
|
285
|
+
constructor(httpClient) {
|
|
286
|
+
if (!httpClient) {
|
|
287
|
+
throw new ConfigurationError(
|
|
288
|
+
"httpClient is required for MeteringModule"
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
this.httpClient = httpClient;
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Catat penggunaan untuk usage-based billing
|
|
295
|
+
*
|
|
296
|
+
* @param params - Parameter pencatatan
|
|
297
|
+
* @returns RecordUsageResult
|
|
298
|
+
*
|
|
299
|
+
* @example
|
|
300
|
+
* ```typescript
|
|
301
|
+
* // Setelah user mengirim pesan
|
|
302
|
+
* await ds.metering.recordUsage({
|
|
303
|
+
* tenantId: currentTenant.id,
|
|
304
|
+
* metricSlug: 'message_sent',
|
|
305
|
+
* amount: 1
|
|
306
|
+
* });
|
|
307
|
+
*
|
|
308
|
+
* // Batch recording untuk efisiensi
|
|
309
|
+
* await ds.metering.recordUsage({
|
|
310
|
+
* tenantId: currentTenant.id,
|
|
311
|
+
* metricSlug: 'api_call',
|
|
312
|
+
* amount: 100 // Batch 100 API calls
|
|
313
|
+
* });
|
|
314
|
+
* ```
|
|
315
|
+
*/
|
|
316
|
+
async recordUsage(params) {
|
|
317
|
+
if (!params.tenantId) {
|
|
318
|
+
throw new ConfigurationError("tenantId is required");
|
|
319
|
+
}
|
|
320
|
+
if (!params.metricSlug) {
|
|
321
|
+
throw new ConfigurationError("metricSlug is required");
|
|
322
|
+
}
|
|
323
|
+
if (!params.amount || params.amount < 1) {
|
|
324
|
+
throw new ConfigurationError("amount must be a positive number");
|
|
325
|
+
}
|
|
326
|
+
return this.httpClient.post(
|
|
327
|
+
"/api/v1/dev/metering/record",
|
|
328
|
+
{
|
|
329
|
+
tenantId: params.tenantId,
|
|
330
|
+
metricSlug: params.metricSlug,
|
|
331
|
+
amount: params.amount,
|
|
332
|
+
timestamp: params.timestamp
|
|
333
|
+
}
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
// src/index.ts
|
|
339
|
+
var Authtara = class {
|
|
340
|
+
/**
|
|
341
|
+
* Create new Authtara SDK instance
|
|
342
|
+
*
|
|
343
|
+
* @param config - Konfigurasi SDK
|
|
344
|
+
* @throws ConfigurationError jika apiKey tidak disediakan
|
|
345
|
+
*/
|
|
346
|
+
constructor(config) {
|
|
347
|
+
if (!config.apiKey) {
|
|
348
|
+
throw new ConfigurationError(
|
|
349
|
+
"apiKey is required. Get it from your Developer Dashboard."
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
const endpoint = config.endpoint ?? "https://api.digitalsolution.com";
|
|
353
|
+
const httpClient = new HttpClient({
|
|
354
|
+
baseUrl: endpoint,
|
|
355
|
+
apiKey: config.apiKey,
|
|
356
|
+
timeout: config.timeout
|
|
357
|
+
});
|
|
358
|
+
this.auth = new AuthModule(config.apiKey, httpClient);
|
|
359
|
+
this.billing = new BillingModule(httpClient);
|
|
360
|
+
this.metering = new MeteringModule(httpClient);
|
|
361
|
+
}
|
|
362
|
+
};
|
|
363
|
+
var index_default = Authtara;
|
|
364
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
365
|
+
0 && (module.exports = {
|
|
366
|
+
ApiError,
|
|
367
|
+
Authtara,
|
|
368
|
+
AuthtaraError,
|
|
369
|
+
ConfigurationError,
|
|
370
|
+
EntitlementDeniedError,
|
|
371
|
+
InvalidTokenError
|
|
372
|
+
});
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
// src/common/errors.ts
|
|
2
|
+
var AuthtaraError = class _AuthtaraError extends Error {
|
|
3
|
+
constructor(message, code, statusCode) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.name = "AuthtaraError";
|
|
6
|
+
this.code = code;
|
|
7
|
+
this.statusCode = statusCode;
|
|
8
|
+
Object.setPrototypeOf(this, _AuthtaraError.prototype);
|
|
9
|
+
}
|
|
10
|
+
};
|
|
11
|
+
var InvalidTokenError = class _InvalidTokenError extends AuthtaraError {
|
|
12
|
+
constructor(message = "Invalid or expired token") {
|
|
13
|
+
super(message, "INVALID_TOKEN", 401);
|
|
14
|
+
this.name = "InvalidTokenError";
|
|
15
|
+
Object.setPrototypeOf(this, _InvalidTokenError.prototype);
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
var EntitlementDeniedError = class _EntitlementDeniedError extends AuthtaraError {
|
|
19
|
+
constructor(message = "Access denied", reason) {
|
|
20
|
+
super(message, "ENTITLEMENT_DENIED", 403);
|
|
21
|
+
this.name = "EntitlementDeniedError";
|
|
22
|
+
this.reason = reason;
|
|
23
|
+
Object.setPrototypeOf(this, _EntitlementDeniedError.prototype);
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
var ApiError = class _ApiError extends AuthtaraError {
|
|
27
|
+
constructor(message, statusCode, response) {
|
|
28
|
+
super(message, "API_ERROR", statusCode);
|
|
29
|
+
this.name = "ApiError";
|
|
30
|
+
this.response = response;
|
|
31
|
+
Object.setPrototypeOf(this, _ApiError.prototype);
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
var ConfigurationError = class _ConfigurationError extends AuthtaraError {
|
|
35
|
+
constructor(message) {
|
|
36
|
+
super(message, "CONFIG_ERROR");
|
|
37
|
+
this.name = "ConfigurationError";
|
|
38
|
+
Object.setPrototypeOf(this, _ConfigurationError.prototype);
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// src/common/http-client.ts
|
|
43
|
+
function extractErrorMessage(error, fallback) {
|
|
44
|
+
if (!error) return fallback;
|
|
45
|
+
if (typeof error === "string") return error;
|
|
46
|
+
if (typeof error === "object" && error.message) return error.message;
|
|
47
|
+
return fallback;
|
|
48
|
+
}
|
|
49
|
+
var HttpClient = class {
|
|
50
|
+
constructor(config) {
|
|
51
|
+
if (!config.baseUrl) {
|
|
52
|
+
throw new ConfigurationError("baseUrl is required");
|
|
53
|
+
}
|
|
54
|
+
if (!config.apiKey) {
|
|
55
|
+
throw new ConfigurationError("apiKey is required");
|
|
56
|
+
}
|
|
57
|
+
this.baseUrl = config.baseUrl.replace(/\/$/, "");
|
|
58
|
+
this.apiKey = config.apiKey;
|
|
59
|
+
this.timeout = config.timeout ?? 3e4;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Make HTTP request to Platform API
|
|
63
|
+
*/
|
|
64
|
+
async request(options) {
|
|
65
|
+
const url = `${this.baseUrl}${options.path}`;
|
|
66
|
+
const headers = {
|
|
67
|
+
"Content-Type": "application/json",
|
|
68
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
69
|
+
...options.headers
|
|
70
|
+
};
|
|
71
|
+
const controller = new AbortController();
|
|
72
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
73
|
+
try {
|
|
74
|
+
const response = await fetch(url, {
|
|
75
|
+
method: options.method,
|
|
76
|
+
headers,
|
|
77
|
+
body: options.body ? JSON.stringify(options.body) : void 0,
|
|
78
|
+
signal: controller.signal
|
|
79
|
+
});
|
|
80
|
+
clearTimeout(timeoutId);
|
|
81
|
+
const data = await response.json();
|
|
82
|
+
if (!response.ok) {
|
|
83
|
+
throw new ApiError(
|
|
84
|
+
extractErrorMessage(data.error, `HTTP ${response.status}: ${response.statusText}`),
|
|
85
|
+
response.status,
|
|
86
|
+
data
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
if (!data.success) {
|
|
90
|
+
throw new ApiError(extractErrorMessage(data.error, "API request failed"), 400, data);
|
|
91
|
+
}
|
|
92
|
+
return data.data;
|
|
93
|
+
} catch (error) {
|
|
94
|
+
clearTimeout(timeoutId);
|
|
95
|
+
if (error instanceof ApiError) {
|
|
96
|
+
throw error;
|
|
97
|
+
}
|
|
98
|
+
if (error instanceof Error) {
|
|
99
|
+
if (error.name === "AbortError") {
|
|
100
|
+
throw new ApiError(`Request timeout after ${this.timeout}ms`, 408);
|
|
101
|
+
}
|
|
102
|
+
throw new ApiError(error.message, 500);
|
|
103
|
+
}
|
|
104
|
+
throw new ApiError("Unknown error occurred", 500);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* POST request shorthand
|
|
109
|
+
*/
|
|
110
|
+
async post(path, body) {
|
|
111
|
+
return this.request({ method: "POST", path, body });
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* GET request shorthand
|
|
115
|
+
*/
|
|
116
|
+
async get(path) {
|
|
117
|
+
return this.request({ method: "GET", path });
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
// src/auth/index.ts
|
|
122
|
+
import { jwtVerify } from "jose";
|
|
123
|
+
var AuthModule = class {
|
|
124
|
+
constructor(apiKey, httpClient, issuer = "platform.digitalsolution.com") {
|
|
125
|
+
if (!apiKey) {
|
|
126
|
+
throw new ConfigurationError("apiKey is required for AuthModule");
|
|
127
|
+
}
|
|
128
|
+
this.apiKey = apiKey;
|
|
129
|
+
this.httpClient = httpClient;
|
|
130
|
+
this.issuer = issuer;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Verifikasi SSO JWT token secara offline
|
|
134
|
+
*
|
|
135
|
+
* Token diverifikasi menggunakan App Secret (HS256).
|
|
136
|
+
* Ini lebih cepat daripada memanggil API karena tidak memerlukan network request.
|
|
137
|
+
*
|
|
138
|
+
* @param token - JWT token dari SSO callback
|
|
139
|
+
* @returns SessionVerifyResult dengan data user, tenant, dan subscription
|
|
140
|
+
* @throws InvalidTokenError jika token tidak valid atau expired
|
|
141
|
+
*
|
|
142
|
+
* @example
|
|
143
|
+
* ```typescript
|
|
144
|
+
* const session = await ds.auth.verifySession(token);
|
|
145
|
+
* if (session.isValid) {
|
|
146
|
+
* console.log('User:', session.user);
|
|
147
|
+
* console.log('Tenant:', session.tenant);
|
|
148
|
+
* }
|
|
149
|
+
* ```
|
|
150
|
+
*/
|
|
151
|
+
async verifySession(token) {
|
|
152
|
+
if (!token) {
|
|
153
|
+
throw new InvalidTokenError("Token is required");
|
|
154
|
+
}
|
|
155
|
+
try {
|
|
156
|
+
const secretKey = new TextEncoder().encode(this.apiKey);
|
|
157
|
+
const { payload } = await jwtVerify(token, secretKey, {
|
|
158
|
+
algorithms: ["HS256"],
|
|
159
|
+
issuer: this.issuer
|
|
160
|
+
});
|
|
161
|
+
const ssoPayload = payload;
|
|
162
|
+
return {
|
|
163
|
+
isValid: true,
|
|
164
|
+
user: {
|
|
165
|
+
id: ssoPayload.sub ?? "",
|
|
166
|
+
email: ssoPayload.email ?? "",
|
|
167
|
+
name: ssoPayload.name ?? null
|
|
168
|
+
},
|
|
169
|
+
tenant: ssoPayload.tenant,
|
|
170
|
+
subscription: ssoPayload.subscription
|
|
171
|
+
};
|
|
172
|
+
} catch (error) {
|
|
173
|
+
const message = error instanceof Error ? error.message : "Token verification failed";
|
|
174
|
+
throw new InvalidTokenError(message);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Exchange authorization code untuk JWT token (server-to-server)
|
|
179
|
+
*
|
|
180
|
+
* Gunakan ini di callback endpoint untuk menukar authorization code
|
|
181
|
+
* menjadi JWT token yang bisa diverifikasi.
|
|
182
|
+
*
|
|
183
|
+
* @param code - Authorization code dari callback URL
|
|
184
|
+
* @returns ExchangeResult dengan token dan data context
|
|
185
|
+
*
|
|
186
|
+
* @example
|
|
187
|
+
* ```typescript
|
|
188
|
+
* // Di endpoint /api/sso/callback
|
|
189
|
+
* const code = searchParams.get('code');
|
|
190
|
+
* const result = await ds.auth.exchangeCode(code);
|
|
191
|
+
* // Simpan result.token ke session
|
|
192
|
+
* ```
|
|
193
|
+
*/
|
|
194
|
+
async exchangeCode(code) {
|
|
195
|
+
if (!code) {
|
|
196
|
+
throw new ConfigurationError("Authorization code is required");
|
|
197
|
+
}
|
|
198
|
+
return this.httpClient.post("/api/sso/exchange", {
|
|
199
|
+
code,
|
|
200
|
+
appSecret: this.apiKey
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
// src/billing/index.ts
|
|
206
|
+
var BillingModule = class {
|
|
207
|
+
constructor(httpClient) {
|
|
208
|
+
if (!httpClient) {
|
|
209
|
+
throw new ConfigurationError("httpClient is required for BillingModule");
|
|
210
|
+
}
|
|
211
|
+
this.httpClient = httpClient;
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Cek apakah tenant memiliki akses ke aplikasi Anda
|
|
215
|
+
*
|
|
216
|
+
* @param params - Parameter pengecekan (tenantId wajib, featureKey opsional)
|
|
217
|
+
* @returns EntitlementResult dengan status akses
|
|
218
|
+
*
|
|
219
|
+
* @example
|
|
220
|
+
* ```typescript
|
|
221
|
+
* // Cek akses dasar
|
|
222
|
+
* const hasAccess = await ds.billing.checkEntitlement({
|
|
223
|
+
* tenantId: tenant.id
|
|
224
|
+
* });
|
|
225
|
+
*
|
|
226
|
+
* // Cek fitur spesifik
|
|
227
|
+
* const canUseAI = await ds.billing.checkEntitlement({
|
|
228
|
+
* tenantId: tenant.id,
|
|
229
|
+
* featureKey: 'ai_generator'
|
|
230
|
+
* });
|
|
231
|
+
*
|
|
232
|
+
* if (!canUseAI.granted) {
|
|
233
|
+
* throw new Error(canUseAI.reason);
|
|
234
|
+
* }
|
|
235
|
+
* ```
|
|
236
|
+
*/
|
|
237
|
+
async checkEntitlement(params) {
|
|
238
|
+
if (!params.tenantId) {
|
|
239
|
+
throw new ConfigurationError("tenantId is required");
|
|
240
|
+
}
|
|
241
|
+
return this.httpClient.post(
|
|
242
|
+
"/api/v1/dev/entitlements/check",
|
|
243
|
+
{
|
|
244
|
+
tenantId: params.tenantId,
|
|
245
|
+
featureKey: params.featureKey
|
|
246
|
+
}
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
// src/metering/index.ts
|
|
252
|
+
var MeteringModule = class {
|
|
253
|
+
constructor(httpClient) {
|
|
254
|
+
if (!httpClient) {
|
|
255
|
+
throw new ConfigurationError(
|
|
256
|
+
"httpClient is required for MeteringModule"
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
this.httpClient = httpClient;
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Catat penggunaan untuk usage-based billing
|
|
263
|
+
*
|
|
264
|
+
* @param params - Parameter pencatatan
|
|
265
|
+
* @returns RecordUsageResult
|
|
266
|
+
*
|
|
267
|
+
* @example
|
|
268
|
+
* ```typescript
|
|
269
|
+
* // Setelah user mengirim pesan
|
|
270
|
+
* await ds.metering.recordUsage({
|
|
271
|
+
* tenantId: currentTenant.id,
|
|
272
|
+
* metricSlug: 'message_sent',
|
|
273
|
+
* amount: 1
|
|
274
|
+
* });
|
|
275
|
+
*
|
|
276
|
+
* // Batch recording untuk efisiensi
|
|
277
|
+
* await ds.metering.recordUsage({
|
|
278
|
+
* tenantId: currentTenant.id,
|
|
279
|
+
* metricSlug: 'api_call',
|
|
280
|
+
* amount: 100 // Batch 100 API calls
|
|
281
|
+
* });
|
|
282
|
+
* ```
|
|
283
|
+
*/
|
|
284
|
+
async recordUsage(params) {
|
|
285
|
+
if (!params.tenantId) {
|
|
286
|
+
throw new ConfigurationError("tenantId is required");
|
|
287
|
+
}
|
|
288
|
+
if (!params.metricSlug) {
|
|
289
|
+
throw new ConfigurationError("metricSlug is required");
|
|
290
|
+
}
|
|
291
|
+
if (!params.amount || params.amount < 1) {
|
|
292
|
+
throw new ConfigurationError("amount must be a positive number");
|
|
293
|
+
}
|
|
294
|
+
return this.httpClient.post(
|
|
295
|
+
"/api/v1/dev/metering/record",
|
|
296
|
+
{
|
|
297
|
+
tenantId: params.tenantId,
|
|
298
|
+
metricSlug: params.metricSlug,
|
|
299
|
+
amount: params.amount,
|
|
300
|
+
timestamp: params.timestamp
|
|
301
|
+
}
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
// src/index.ts
|
|
307
|
+
var Authtara = class {
|
|
308
|
+
/**
|
|
309
|
+
* Create new Authtara SDK instance
|
|
310
|
+
*
|
|
311
|
+
* @param config - Konfigurasi SDK
|
|
312
|
+
* @throws ConfigurationError jika apiKey tidak disediakan
|
|
313
|
+
*/
|
|
314
|
+
constructor(config) {
|
|
315
|
+
if (!config.apiKey) {
|
|
316
|
+
throw new ConfigurationError(
|
|
317
|
+
"apiKey is required. Get it from your Developer Dashboard."
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
const endpoint = config.endpoint ?? "https://api.digitalsolution.com";
|
|
321
|
+
const httpClient = new HttpClient({
|
|
322
|
+
baseUrl: endpoint,
|
|
323
|
+
apiKey: config.apiKey,
|
|
324
|
+
timeout: config.timeout
|
|
325
|
+
});
|
|
326
|
+
this.auth = new AuthModule(config.apiKey, httpClient);
|
|
327
|
+
this.billing = new BillingModule(httpClient);
|
|
328
|
+
this.metering = new MeteringModule(httpClient);
|
|
329
|
+
}
|
|
330
|
+
};
|
|
331
|
+
var index_default = Authtara;
|
|
332
|
+
export {
|
|
333
|
+
ApiError,
|
|
334
|
+
Authtara,
|
|
335
|
+
AuthtaraError,
|
|
336
|
+
ConfigurationError,
|
|
337
|
+
EntitlementDeniedError,
|
|
338
|
+
InvalidTokenError,
|
|
339
|
+
index_default as default
|
|
340
|
+
};
|