@bulutklinik/sdk 0.1.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/dist/index.cjs ADDED
@@ -0,0 +1,647 @@
1
+ 'use strict';
2
+
3
+ // src/token-store.ts
4
+ var MemoryTokenStore = class {
5
+ #access;
6
+ #refresh;
7
+ constructor(seed) {
8
+ this.#access = seed?.accessToken ?? null;
9
+ this.#refresh = seed?.refreshToken ?? null;
10
+ }
11
+ getAccessToken() {
12
+ return this.#access;
13
+ }
14
+ getRefreshToken() {
15
+ return this.#refresh;
16
+ }
17
+ setTokens(accessToken, refreshToken) {
18
+ this.#access = accessToken;
19
+ this.#refresh = refreshToken;
20
+ }
21
+ clear() {
22
+ this.#access = null;
23
+ this.#refresh = null;
24
+ }
25
+ };
26
+
27
+ // src/config.ts
28
+ var ENVIRONMENT_BASE_URLS = {
29
+ production: "https://api.bulutklinik.com/api/v3",
30
+ test: "https://apitest.bulutklinik.com/api/v3",
31
+ local: "https://api-bulutklinik.test/api/v3"
32
+ };
33
+ function resolveConfig(options = {}) {
34
+ const base = options.baseUrl ?? ENVIRONMENT_BASE_URLS[options.environment ?? "production"];
35
+ const fetchImpl = options.fetch ?? (typeof globalThis.fetch === "function" ? globalThis.fetch.bind(globalThis) : void 0);
36
+ if (typeof fetchImpl !== "function") {
37
+ throw new Error(
38
+ "No fetch implementation available. Provide options.fetch or run on Node >= 18."
39
+ );
40
+ }
41
+ return {
42
+ baseUrl: base.replace(/\/+$/, ""),
43
+ lang: options.lang ?? "tr",
44
+ clientId: options.clientId,
45
+ clientSecret: options.clientSecret,
46
+ partnerToken: options.partnerToken,
47
+ tokenStore: options.tokenStore ?? new MemoryTokenStore(),
48
+ timeoutMs: options.timeoutMs ?? 3e4,
49
+ fetchImpl
50
+ };
51
+ }
52
+
53
+ // src/errors.ts
54
+ var BulutklinikError = class extends Error {
55
+ constructor(message, options) {
56
+ super(message, options);
57
+ this.name = new.target.name;
58
+ }
59
+ };
60
+ var TransportError = class extends BulutklinikError {
61
+ constructor(message, cause) {
62
+ super(message, { cause });
63
+ }
64
+ };
65
+ var ApiError = class extends BulutklinikError {
66
+ httpStatus;
67
+ resultType;
68
+ errorType;
69
+ data;
70
+ method;
71
+ path;
72
+ constructor(message, ctx) {
73
+ super(message);
74
+ this.httpStatus = ctx.httpStatus;
75
+ this.resultType = ctx.resultType;
76
+ this.errorType = ctx.errorType;
77
+ this.data = ctx.data;
78
+ this.method = ctx.method;
79
+ this.path = ctx.path;
80
+ }
81
+ };
82
+ var ValidationError = class extends ApiError {
83
+ };
84
+ var AuthenticationError = class extends ApiError {
85
+ };
86
+ var AuthorizationError = class extends ApiError {
87
+ };
88
+ var NotFoundError = class extends ApiError {
89
+ };
90
+ var RateLimitError = class extends ApiError {
91
+ retryAfter;
92
+ constructor(message, ctx) {
93
+ super(message, ctx);
94
+ this.retryAfter = ctx.retryAfter;
95
+ }
96
+ };
97
+ function createApiError(ctx, message) {
98
+ if (ctx.resultType === 2) return new AuthenticationError(message, ctx);
99
+ const type = typeof ctx.errorType === "string" ? ctx.errorType.toLowerCase() : void 0;
100
+ if (type === "validation" || ctx.httpStatus === 422) {
101
+ return new ValidationError(message, ctx);
102
+ }
103
+ switch (ctx.httpStatus) {
104
+ case 401:
105
+ return new AuthenticationError(message, ctx);
106
+ case 403:
107
+ return new AuthorizationError(message, ctx);
108
+ case 404:
109
+ return new NotFoundError(message, ctx);
110
+ case 429:
111
+ return new RateLimitError(message, ctx);
112
+ default:
113
+ return new ApiError(message, ctx);
114
+ }
115
+ }
116
+
117
+ // src/types.ts
118
+ var ResultType = {
119
+ Success: 0,
120
+ Error: 1,
121
+ Logout: 2,
122
+ Update: 3,
123
+ Refresh: 4
124
+ };
125
+
126
+ // src/http.ts
127
+ var HttpClient = class {
128
+ tokenStore;
129
+ clientId;
130
+ clientSecret;
131
+ baseUrl;
132
+ lang;
133
+ partnerToken;
134
+ timeoutMs;
135
+ fetchImpl;
136
+ refreshInFlight = null;
137
+ constructor(config) {
138
+ this.baseUrl = config.baseUrl;
139
+ this.lang = config.lang;
140
+ this.clientId = config.clientId;
141
+ this.clientSecret = config.clientSecret;
142
+ this.partnerToken = config.partnerToken;
143
+ this.tokenStore = config.tokenStore;
144
+ this.timeoutMs = config.timeoutMs;
145
+ this.fetchImpl = config.fetchImpl;
146
+ }
147
+ request(spec) {
148
+ return this.send(spec, false);
149
+ }
150
+ /** Force a token refresh using the stored refresh token. Throws on failure. */
151
+ async refresh() {
152
+ await this.performRefresh();
153
+ }
154
+ async send(spec, isRetry) {
155
+ const response = await this.dispatch(spec);
156
+ const envelope = await this.readEnvelope(response);
157
+ if (response.ok && envelope.resultType === ResultType.Success) {
158
+ return envelope.data;
159
+ }
160
+ const expired = response.status === 401 || envelope.resultType === ResultType.Refresh;
161
+ if (spec.auth === "bearer" && expired && !isRetry) {
162
+ const refreshed = await this.ensureRefreshed();
163
+ if (refreshed) {
164
+ return this.send(spec, true);
165
+ }
166
+ }
167
+ if (envelope.resultType === ResultType.Logout) {
168
+ await this.tokenStore.clear();
169
+ }
170
+ throw this.toError(spec, response, envelope);
171
+ }
172
+ async dispatch(spec) {
173
+ const url = this.baseUrl + spec.path;
174
+ const headers = {
175
+ Accept: "application/json",
176
+ lang: spec.lang ?? this.lang
177
+ };
178
+ const hasBody = spec.body !== void 0 && spec.method !== "GET";
179
+ if (hasBody) headers["Content-Type"] = "application/json";
180
+ if (spec.auth === "bearer") {
181
+ const token = await this.tokenStore.getAccessToken();
182
+ if (token) headers.Authorization = `Bearer ${token}`;
183
+ } else if (spec.auth === "partner") {
184
+ if (this.partnerToken) headers.Authorization = `Bearer ${this.partnerToken}`;
185
+ }
186
+ const controller = new AbortController();
187
+ const timer = setTimeout(() => controller.abort(), this.timeoutMs);
188
+ try {
189
+ return await this.fetchImpl(url, {
190
+ method: spec.method,
191
+ headers,
192
+ body: hasBody ? JSON.stringify(spec.body) : void 0,
193
+ signal: controller.signal
194
+ });
195
+ } catch (error) {
196
+ if (controller.signal.aborted) {
197
+ throw new TransportError(
198
+ `Request ${spec.method} ${spec.path} timed out after ${this.timeoutMs}ms`,
199
+ error
200
+ );
201
+ }
202
+ throw new TransportError(`Network error on ${spec.method} ${spec.path}`, error);
203
+ } finally {
204
+ clearTimeout(timer);
205
+ }
206
+ }
207
+ async readEnvelope(response) {
208
+ const text = await response.text();
209
+ if (!text) return {};
210
+ try {
211
+ return JSON.parse(text);
212
+ } catch {
213
+ return { errorMessage: text };
214
+ }
215
+ }
216
+ async ensureRefreshed() {
217
+ if (!this.refreshInFlight) {
218
+ this.refreshInFlight = this.performRefresh().finally(() => {
219
+ this.refreshInFlight = null;
220
+ });
221
+ }
222
+ try {
223
+ await this.refreshInFlight;
224
+ return true;
225
+ } catch {
226
+ return false;
227
+ }
228
+ }
229
+ async performRefresh() {
230
+ const refreshToken = await this.tokenStore.getRefreshToken();
231
+ if (!refreshToken) throw new Error("No refresh token available");
232
+ if (!this.clientId || !this.clientSecret) {
233
+ throw new Error("clientId and clientSecret are required to refresh tokens");
234
+ }
235
+ const response = await this.dispatch({
236
+ method: "POST",
237
+ path: "/general/refreshApi",
238
+ auth: "public",
239
+ body: { refreshToken, clientId: this.clientId, clientSecretKey: this.clientSecret }
240
+ });
241
+ const envelope = await this.readEnvelope(response);
242
+ const data = envelope.data;
243
+ if (!response.ok || envelope.resultType !== ResultType.Success || !data?.access_token) {
244
+ await this.tokenStore.clear();
245
+ throw new Error("Token refresh failed");
246
+ }
247
+ await this.tokenStore.setTokens(data.access_token, data.refresh_token ?? refreshToken);
248
+ }
249
+ toError(spec, response, envelope) {
250
+ const retryAfterHeader = response.headers.get("retry-after");
251
+ const ctx = {
252
+ httpStatus: response.status,
253
+ resultType: envelope.resultType,
254
+ errorType: envelope.errorType,
255
+ data: envelope.data,
256
+ method: spec.method,
257
+ path: spec.path,
258
+ retryAfter: retryAfterHeader ? Number(retryAfterHeader) : void 0
259
+ };
260
+ const message = envelope.errorMessage ?? `Bulutklinik API request failed: ${spec.method} ${spec.path} (HTTP ${response.status})`;
261
+ return createApiError(ctx, message);
262
+ }
263
+ };
264
+
265
+ // src/resources/appointments.ts
266
+ var AppointmentsResource = class {
267
+ constructor(http) {
268
+ this.http = http;
269
+ }
270
+ http;
271
+ /** Reserve an online (interview) slot. Resolves to `null` on success. */
272
+ reserveInterview(input) {
273
+ return this.http.request({
274
+ method: "POST",
275
+ path: "/patients/addInterviewDateReservation",
276
+ auth: "bearer",
277
+ body: {
278
+ doctorId: input.doctorId,
279
+ appointmentDate: input.appointmentDate,
280
+ appointmentType: input.appointmentType ?? "interview"
281
+ }
282
+ });
283
+ }
284
+ /** Create a physical appointment. */
285
+ addPhysical(input) {
286
+ return this.http.request({
287
+ method: "POST",
288
+ path: "/patients/addNewAppointment",
289
+ auth: "bearer",
290
+ body: { doctorId: input.doctorId, appointmentDate: input.appointmentDate }
291
+ });
292
+ }
293
+ /** Cancel an appointment by event id (`cln_events.id`). */
294
+ cancel(eventId) {
295
+ return this.http.request({
296
+ method: "DELETE",
297
+ path: `/patients/deleteUserAppointment/${eventId}`,
298
+ auth: "bearer"
299
+ });
300
+ }
301
+ };
302
+
303
+ // src/resources/auth.ts
304
+ var AuthResource = class {
305
+ constructor(http) {
306
+ this.http = http;
307
+ }
308
+ http;
309
+ /**
310
+ * Log in. On success tokens are stored automatically and
311
+ * `{ twoFactorRequired: false }` is returned. If 2FA is enabled the result is
312
+ * `{ twoFactorRequired: true, twoFactorResponse }` — pass that blob to
313
+ * {@link connectWithTwoFactor} together with the SMS code.
314
+ */
315
+ async connect(input) {
316
+ const data = await this.http.request({
317
+ method: "POST",
318
+ path: "/general/connectApi",
319
+ auth: "public",
320
+ body: {
321
+ apiUserName: input.apiUserName,
322
+ apiUserPassword: input.apiUserPassword,
323
+ apiClientId: input.clientId ?? this.http.clientId,
324
+ apiSecretKey: input.clientSecret ?? this.http.clientSecret,
325
+ loginMode: input.loginMode,
326
+ ...input.withPhoneNumber !== void 0 ? { withPhoneNumber: input.withPhoneNumber } : {}
327
+ }
328
+ });
329
+ if (data.access_token) {
330
+ await this.storeTokens(data);
331
+ return { twoFactorRequired: false, passwordPolicy: data.password_policy };
332
+ }
333
+ if (data.response) {
334
+ return { twoFactorRequired: true, twoFactorResponse: data.response };
335
+ }
336
+ return { twoFactorRequired: false };
337
+ }
338
+ /** Complete a 2FA login with the SMS code and the challenge blob. */
339
+ async connectWithTwoFactor(input) {
340
+ const data = await this.http.request({
341
+ method: "POST",
342
+ path: "/general/connectApiWithTwoFactor",
343
+ auth: "public",
344
+ body: { smsVerificationCode: input.smsVerificationCode, response: input.response }
345
+ });
346
+ await this.storeTokens(data);
347
+ }
348
+ /** Register a new patient (afterRegister auto-login). Stores tokens on success. */
349
+ async register(input) {
350
+ const data = await this.http.request({
351
+ method: "POST",
352
+ path: "/patients/addNewPatient",
353
+ auth: "public",
354
+ body: {
355
+ name: input.name,
356
+ surname: input.surname,
357
+ apiUserName: input.apiUserName,
358
+ phoneNumber: input.phoneNumber,
359
+ password: input.password,
360
+ smsVerificationCode: input.smsVerificationCode,
361
+ response: input.response,
362
+ acceptUserAgreement: input.acceptUserAgreement ?? 1,
363
+ apiClientId: input.clientId ?? this.http.clientId,
364
+ apiSecretKey: input.clientSecret ?? this.http.clientSecret
365
+ }
366
+ });
367
+ await this.storeTokens(data);
368
+ }
369
+ /** Manually refresh the access token using the stored refresh token. */
370
+ refresh() {
371
+ return this.http.refresh();
372
+ }
373
+ /** Revoke the current tokens server-side and clear the local token store. */
374
+ async disconnect() {
375
+ try {
376
+ await this.http.request({
377
+ method: "POST",
378
+ path: "/general/disconnectApi",
379
+ auth: "bearer",
380
+ body: {}
381
+ });
382
+ } finally {
383
+ await this.http.tokenStore.clear();
384
+ }
385
+ }
386
+ async storeTokens(data) {
387
+ if (!data.access_token) {
388
+ throw new Error("Login response did not contain an access token");
389
+ }
390
+ await this.http.tokenStore.setTokens(data.access_token, data.refresh_token ?? null);
391
+ }
392
+ };
393
+
394
+ // src/resources/doctors.ts
395
+ var DoctorsResource = class {
396
+ constructor(http) {
397
+ this.http = http;
398
+ }
399
+ http;
400
+ branches() {
401
+ return this.http.request({
402
+ method: "GET",
403
+ path: "/patients/allBranches",
404
+ auth: "bearer"
405
+ });
406
+ }
407
+ locations() {
408
+ return this.http.request({
409
+ method: "GET",
410
+ path: "/patients/allLocations",
411
+ auth: "bearer"
412
+ });
413
+ }
414
+ quickSearch(input) {
415
+ return this.http.request({
416
+ method: "POST",
417
+ path: "/patients/quickSearch",
418
+ auth: "bearer",
419
+ body: {
420
+ searchText: input.searchText,
421
+ listType: input.listType ?? null,
422
+ location: input.location ?? null
423
+ }
424
+ });
425
+ }
426
+ search(input) {
427
+ return this.http.request({
428
+ method: "POST",
429
+ path: "/patients/filteredSearch",
430
+ auth: "bearer",
431
+ body: {
432
+ searchParams: input.searchParams ?? {},
433
+ orderParams: input.orderParams ?? [],
434
+ otherParams: input.otherParams ?? [],
435
+ currentPage: input.currentPage,
436
+ perPageLimit: input.perPageLimit ?? 20
437
+ }
438
+ });
439
+ }
440
+ detail(id, corporate) {
441
+ const path = corporate !== void 0 ? `/patients/doctorDetail/${id}/${corporate}` : `/patients/doctorDetail/${id}`;
442
+ return this.http.request({ method: "GET", path, auth: "bearer" });
443
+ }
444
+ };
445
+
446
+ // src/resources/measures.ts
447
+ var MeasuresResource = class {
448
+ constructor(http) {
449
+ this.http = http;
450
+ }
451
+ http;
452
+ /** Submit multiple measurements of any types in one call (primary entrypoint). */
453
+ addList(records) {
454
+ return this.http.request({
455
+ method: "POST",
456
+ path: "/patients/addNewUserMeasures",
457
+ auth: "bearer",
458
+ body: { data: records }
459
+ });
460
+ }
461
+ /** Submit a single measurement of one type. */
462
+ add(type, fields) {
463
+ return this.http.request({
464
+ method: "POST",
465
+ path: `/patients/addNewUserMeasures/${type}`,
466
+ auth: "bearer",
467
+ body: fields
468
+ });
469
+ }
470
+ update(type, input) {
471
+ return this.http.request({
472
+ method: "PUT",
473
+ path: `/patients/updateUserMeasures/${type}`,
474
+ auth: "bearer",
475
+ body: input
476
+ });
477
+ }
478
+ delete(type, id) {
479
+ return this.http.request({
480
+ method: "DELETE",
481
+ path: `/patients/deleteUserMeasures/${type}`,
482
+ auth: "bearer",
483
+ body: { id }
484
+ });
485
+ }
486
+ /** Latest value of each measurement type. */
487
+ last() {
488
+ return this.http.request({
489
+ method: "GET",
490
+ path: "/patients/measuresList",
491
+ auth: "bearer"
492
+ });
493
+ }
494
+ /** Paginated history for one type. `glucoseType` (0/1) applies only to glucose. */
495
+ list(type, page, glucoseType) {
496
+ const path = glucoseType !== void 0 ? `/patients/userMeasuresList/${type}/${page}/${glucoseType}` : `/patients/userMeasuresList/${type}/${page}`;
497
+ return this.http.request({ method: "GET", path, auth: "bearer" });
498
+ }
499
+ /** Grouped graph data. `period`: 1=day, 2=week, 3=month, 4=year. */
500
+ graph(type, period, page, glucoseType) {
501
+ const path = glucoseType !== void 0 ? `/patients/userMeasuresGraph/${type}/${period}/${page}/${glucoseType}` : `/patients/userMeasuresGraph/${type}/${period}/${page}`;
502
+ return this.http.request({ method: "GET", path, auth: "bearer" });
503
+ }
504
+ /** Partner (teusan) submission — uses the configured partner token. */
505
+ partnerHealthInformation(input) {
506
+ return this.http.request({
507
+ method: "POST",
508
+ path: "/outher/healthInformation",
509
+ auth: "partner",
510
+ body: { identity: input.identity, phoneNumber: input.phoneNumber, data: input.data }
511
+ });
512
+ }
513
+ };
514
+
515
+ // src/resources/payments.ts
516
+ var PaymentsResource = class {
517
+ constructor(http) {
518
+ this.http = http;
519
+ }
520
+ http;
521
+ /** Validate a discount code. Note: this endpoint lives under the `patients` prefix. */
522
+ checkDiscountCode(input) {
523
+ return this.http.request({
524
+ method: "POST",
525
+ path: "/patients/checkDiscountCode",
526
+ auth: "bearer",
527
+ body: { ...input }
528
+ });
529
+ }
530
+ getCards() {
531
+ return this.http.request({
532
+ method: "GET",
533
+ path: "/payments/getCards",
534
+ auth: "bearer"
535
+ });
536
+ }
537
+ saveCard(card) {
538
+ return this.http.request({
539
+ method: "POST",
540
+ path: "/payments/saveCard",
541
+ auth: "bearer",
542
+ body: { ...card }
543
+ });
544
+ }
545
+ /**
546
+ * Start an appointment payment. The amount is computed server-side. On a 3DS
547
+ * flow the result carries `payment3DUrl` — a browser URL to open; the SDK does
548
+ * not follow it. 3DS capture happens via the bank → server callback.
549
+ */
550
+ pay(input) {
551
+ return this.http.request({
552
+ method: "POST",
553
+ path: "/payments/interviewPayment",
554
+ auth: "bearer",
555
+ body: {
556
+ doctorId: input.doctorId,
557
+ appointmentDate: input.appointmentDate,
558
+ appointmentType: input.appointmentType ?? "interview",
559
+ is3D: input.is3D,
560
+ termsAccept: input.termsAccept,
561
+ saveCard: input.saveCard ?? 0,
562
+ discountCode: input.discountCode ?? "",
563
+ ...input.cardId !== void 0 ? { cardId: input.cardId } : {},
564
+ ...input.cardInfo ? { cardInfo: input.cardInfo } : {},
565
+ ...input.caseDetail ? { caseDetail: input.caseDetail } : {}
566
+ }
567
+ });
568
+ }
569
+ deleteCard(cardId) {
570
+ return this.http.request({
571
+ method: "DELETE",
572
+ path: `/payments/deleteCard/${cardId}`,
573
+ auth: "bearer"
574
+ });
575
+ }
576
+ };
577
+
578
+ // src/resources/slots.ts
579
+ var SlotsResource = class {
580
+ constructor(http) {
581
+ this.http = http;
582
+ }
583
+ http;
584
+ /**
585
+ * Fetch a doctor's free slots. Returns a date-keyed map of slots. Build the
586
+ * next step's `appointmentDate` as `"<date> <slotStart>"` (drop the seconds).
587
+ */
588
+ schedule(input) {
589
+ return this.http.request({
590
+ method: "POST",
591
+ path: "/patients/doctorScheduler",
592
+ auth: "bearer",
593
+ body: {
594
+ doctorId: input.doctorId,
595
+ scheduleDate: input.scheduleDate ?? null,
596
+ scheduleStep: input.scheduleStep ?? "7",
597
+ schedulePage: input.schedulePage ?? "1",
598
+ listType: input.listType
599
+ }
600
+ });
601
+ }
602
+ };
603
+
604
+ // src/client.ts
605
+ var BulutklinikClient = class {
606
+ auth;
607
+ doctors;
608
+ slots;
609
+ appointments;
610
+ payments;
611
+ measures;
612
+ /** The active token store (also accepts a custom one via options). */
613
+ tokenStore;
614
+ http;
615
+ constructor(options) {
616
+ const config = resolveConfig(options);
617
+ this.http = new HttpClient(config);
618
+ this.tokenStore = config.tokenStore;
619
+ this.auth = new AuthResource(this.http);
620
+ this.doctors = new DoctorsResource(this.http);
621
+ this.slots = new SlotsResource(this.http);
622
+ this.appointments = new AppointmentsResource(this.http);
623
+ this.payments = new PaymentsResource(this.http);
624
+ this.measures = new MeasuresResource(this.http);
625
+ }
626
+ };
627
+
628
+ exports.ApiError = ApiError;
629
+ exports.AppointmentsResource = AppointmentsResource;
630
+ exports.AuthResource = AuthResource;
631
+ exports.AuthenticationError = AuthenticationError;
632
+ exports.AuthorizationError = AuthorizationError;
633
+ exports.BulutklinikClient = BulutklinikClient;
634
+ exports.BulutklinikError = BulutklinikError;
635
+ exports.DoctorsResource = DoctorsResource;
636
+ exports.ENVIRONMENT_BASE_URLS = ENVIRONMENT_BASE_URLS;
637
+ exports.MeasuresResource = MeasuresResource;
638
+ exports.MemoryTokenStore = MemoryTokenStore;
639
+ exports.NotFoundError = NotFoundError;
640
+ exports.PaymentsResource = PaymentsResource;
641
+ exports.RateLimitError = RateLimitError;
642
+ exports.ResultType = ResultType;
643
+ exports.SlotsResource = SlotsResource;
644
+ exports.TransportError = TransportError;
645
+ exports.ValidationError = ValidationError;
646
+ //# sourceMappingURL=index.cjs.map
647
+ //# sourceMappingURL=index.cjs.map