@oapiex/sdk-kit 0.1.2 → 0.1.4

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 CHANGED
@@ -90,15 +90,53 @@ const buildUrl = (baseUrl, ...endpoint) => {
90
90
  return (baseUrl + path.default.normalize(path.default.join(...normalizeValue(endpoint)))).replace(/([^:]\/)\/+/g, "$1");
91
91
  };
92
92
 
93
+ //#endregion
94
+ //#region src/utilities/Manager.ts
95
+ const defaultConfig = {
96
+ environment: "sandbox",
97
+ urls: {
98
+ live: "",
99
+ sandbox: ""
100
+ },
101
+ headers: {}
102
+ };
103
+ let globalConfig = defaultConfig;
104
+ const defineConfig = (config) => {
105
+ const baseConfig = globalConfig ?? defaultConfig;
106
+ const userConfig = {
107
+ ...baseConfig,
108
+ ...config,
109
+ urls: config.urls ? {
110
+ ...baseConfig.urls ?? defaultConfig.urls,
111
+ ...config.urls
112
+ } : baseConfig.urls,
113
+ headers: config.headers ? {
114
+ ...baseConfig.headers ?? defaultConfig.headers,
115
+ ...config.headers
116
+ } : baseConfig.headers
117
+ };
118
+ globalConfig = userConfig;
119
+ return userConfig;
120
+ };
121
+ const getConfig = () => globalConfig;
122
+ const resetConfig = () => {
123
+ globalConfig = {
124
+ ...defaultConfig,
125
+ urls: { ...defaultConfig.urls ?? {} },
126
+ headers: { ...defaultConfig.headers ?? {} }
127
+ };
128
+ return globalConfig;
129
+ };
130
+
93
131
  //#endregion
94
132
  //#region src/Builder.ts
95
133
  var Builder = class {
96
134
  static baseUrls = {
97
- live: "https://api.flutterwave.com/v4/",
98
- sandbox: "https://developersandbox-api.flutterwave.com/"
135
+ live: "",
136
+ sandbox: ""
99
137
  };
100
138
  /**
101
- * Flutterwave Environment
139
+ * API Environment
102
140
  */
103
141
  static environment;
104
142
  constructor() {}
@@ -116,8 +154,11 @@ var Builder = class {
116
154
  * @returns
117
155
  */
118
156
  static baseUrl() {
119
- if ((process.env.ENVIRONMENT || this.environment || "sandbox") === "live") return this.baseUrls.live;
120
- return this.baseUrls.sandbox;
157
+ const config = getConfig();
158
+ const env = this.environment ?? config.environment ?? this.normalizeEnvironment(process.env.ENVIRONMENT) ?? "sandbox";
159
+ const configuredUrl = config.urls?.[env];
160
+ if (configuredUrl) return this.normalizeBaseUrl(configuredUrl);
161
+ return this.normalizeBaseUrl(this.baseUrls[env]);
121
162
  }
122
163
  /**
123
164
  * Builds a full url based on endpoint provided
@@ -192,10 +233,12 @@ var Builder = class {
192
233
  * @returns A new object with the specified keys encrypted.
193
234
  */
194
235
  static async encryptDetails(input, keysToEncrypt = [], outputMapping = {}) {
236
+ const encryptionKey = getConfig().encryptionKey ?? process.env.ENCRYPTION_KEY;
237
+ if (!encryptionKey) throw new Error("Encryption key is required to encrypt details");
195
238
  const nonce = crypto.default.randomBytes(12).toString("base64").slice(0, 12);
196
239
  const encryptableKeys = keysToEncrypt.length > 0 ? keysToEncrypt : Object.keys(input);
197
240
  const encrypted = Object.fromEntries(Object.entries(input).map(([key, value]) => {
198
- if (encryptableKeys.includes(key) && typeof value === "string") return [outputMapping?.[key] || key, this.encryptAES(value, process.env.ENCRYPTION_KEY, nonce)];
241
+ if (encryptableKeys.includes(key) && typeof value === "string") return [outputMapping?.[key] || key, this.encryptAES(value, encryptionKey, nonce)];
199
242
  return [key, value];
200
243
  }));
201
244
  for (const key of encryptableKeys) delete input[key];
@@ -221,6 +264,10 @@ var Builder = class {
221
264
  }, key, new TextEncoder().encode(data));
222
265
  return btoa(String.fromCharCode(...new Uint8Array(encryptedData)));
223
266
  }
267
+ static normalizeBaseUrl = (url) => url.endsWith("/") ? url : `${url}/`;
268
+ static normalizeEnvironment = (value) => {
269
+ if (value === "live" || value === "sandbox") return value;
270
+ };
224
271
  };
225
272
 
226
273
  //#endregion
@@ -368,13 +415,41 @@ var Http = class Http {
368
415
  */
369
416
  static setBearerToken(token) {
370
417
  this.bearerToken = token;
418
+ defineConfig({ auth: {
419
+ type: "bearer",
420
+ token
421
+ } });
422
+ }
423
+ static setAuth(auth) {
424
+ defineConfig({ auth });
425
+ }
426
+ static setBasicAuth(username, password) {
427
+ this.setAuth({
428
+ type: "basic",
429
+ username,
430
+ password
431
+ });
432
+ }
433
+ static setApiKey(name, value, location = "header", prefix) {
434
+ this.setAuth({
435
+ type: "apiKey",
436
+ name,
437
+ value,
438
+ in: location,
439
+ prefix
440
+ });
441
+ }
442
+ static clearAuth() {
443
+ this.bearerToken = void 0;
444
+ defineConfig({ auth: void 0 });
371
445
  }
372
446
  setDefaultHeaders(defaults) {
447
+ const config = getConfig();
373
448
  this.headers = {
374
449
  ...defaults,
450
+ ...config.headers ?? {},
375
451
  ...this.headers
376
452
  };
377
- if (Http.bearerToken) this.headers.Authorization = `Bearer ${Http.bearerToken}`;
378
453
  }
379
454
  getHeaders() {
380
455
  return this.headers;
@@ -383,13 +458,15 @@ var Http = class Http {
383
458
  return this.body;
384
459
  }
385
460
  axiosApi() {
461
+ const config = getConfig();
386
462
  this.setDefaultHeaders({
387
463
  "Accept": "application/json",
388
464
  "Content-Type": "application/json"
389
465
  });
390
466
  const instance = axios.default.create({
391
467
  baseURL: Builder.baseUrl(),
392
- headers: this.getHeaders()
468
+ headers: this.getHeaders(),
469
+ timeout: config.timeout
393
470
  });
394
471
  if (Http.debugLevel > 0) {
395
472
  instance.interceptors.request.use((request) => {
@@ -442,11 +519,18 @@ var Http = class Http {
442
519
  */
443
520
  static async send(url, method, body, headers = {}, params) {
444
521
  try {
445
- const { data } = await new Http(headers).axiosApi()({
522
+ const request = await this.prepareRequest({
446
523
  url,
447
524
  method,
448
- data: body,
449
- params
525
+ body,
526
+ headers: this.toHeaderRecord(headers),
527
+ params: { ...params ?? {} }
528
+ });
529
+ const { data } = await new Http(request.headers, request.body).axiosApi()({
530
+ url: request.url,
531
+ method: request.method,
532
+ data: request.body,
533
+ params: request.params
450
534
  });
451
535
  return {
452
536
  success: true,
@@ -459,6 +543,71 @@ var Http = class Http {
459
543
  throw this.exception(e.response?.status ?? 500, error || e, e);
460
544
  }
461
545
  }
546
+ static async prepareRequest(request) {
547
+ let prepared = {
548
+ ...request,
549
+ headers: { ...request.headers },
550
+ params: { ...request.params ?? {} }
551
+ };
552
+ for (const auth of this.getConfiguredAuth()) prepared = await this.applyAuth(prepared, auth);
553
+ return prepared;
554
+ }
555
+ static getConfiguredAuth() {
556
+ const configuredAuth = getConfig().auth;
557
+ if (configuredAuth) return Array.isArray(configuredAuth) ? configuredAuth : [configuredAuth];
558
+ if (this.bearerToken) return [{
559
+ type: "bearer",
560
+ token: this.bearerToken
561
+ }];
562
+ return [];
563
+ }
564
+ static async applyAuth(request, auth) {
565
+ switch (auth.type) {
566
+ case "bearer":
567
+ this.setHeaderIfMissing(request.headers, "Authorization", `${auth.prefix ?? "Bearer"} ${auth.token}`.trim());
568
+ return request;
569
+ case "oauth2":
570
+ this.setHeaderIfMissing(request.headers, "Authorization", `${auth.tokenType ?? "Bearer"} ${auth.accessToken}`.trim());
571
+ return request;
572
+ case "basic": {
573
+ const encoded = Buffer.from(`${auth.username}:${auth.password}`).toString("base64");
574
+ this.setHeaderIfMissing(request.headers, "Authorization", `Basic ${encoded}`);
575
+ return request;
576
+ }
577
+ case "apiKey": {
578
+ const value = auth.prefix ? `${auth.prefix} ${auth.value}`.trim() : auth.value;
579
+ const location = auth.in ?? "header";
580
+ if (location === "query") {
581
+ if (request.params[auth.name] === void 0) request.params[auth.name] = value;
582
+ return request;
583
+ }
584
+ if (location === "cookie") {
585
+ this.appendCookie(request.headers, auth.name, value);
586
+ return request;
587
+ }
588
+ this.setHeaderIfMissing(request.headers, auth.name, value);
589
+ return request;
590
+ }
591
+ case "custom": return await auth.apply({
592
+ ...request,
593
+ headers: { ...request.headers },
594
+ params: { ...request.params }
595
+ });
596
+ }
597
+ }
598
+ static setHeaderIfMissing(headers, name, value) {
599
+ if (!Object.keys(headers).find((header) => header.toLowerCase() === name.toLowerCase())) headers[name] = value;
600
+ }
601
+ static appendCookie(headers, name, value) {
602
+ const headerName = Object.keys(headers).find((header) => header.toLowerCase() === "cookie") ?? "Cookie";
603
+ const existingCookie = headers[headerName];
604
+ const cookieEntry = `${encodeURIComponent(name)}=${encodeURIComponent(value)}`;
605
+ if (!existingCookie) {
606
+ headers[headerName] = cookieEntry;
607
+ return;
608
+ }
609
+ if (!existingCookie.split(";").map((part) => part.trim()).some((part) => part.startsWith(`${encodeURIComponent(name)}=`))) headers[headerName] = `${existingCookie}; ${cookieEntry}`;
610
+ }
462
611
  /**
463
612
  * Create an HttpException from status and error
464
613
  *
@@ -483,13 +632,16 @@ var Http = class Http {
483
632
  if (this.apiInstance) this.apiInstance.setLastException(exception);
484
633
  return exception;
485
634
  }
635
+ static toHeaderRecord = (headers) => {
636
+ return Object.fromEntries(Object.entries(headers).map(([key, value]) => [key, String(value)]));
637
+ };
486
638
  };
487
639
 
488
640
  //#endregion
489
641
  //#region src/Apis/BaseApi.ts
490
642
  var BaseApi = class {
491
643
  /**
492
- * Flutterwave instance
644
+ * API Core instance for API bootstrapping and shared utilities
493
645
  */
494
646
  #core;
495
647
  lastException;
@@ -595,7 +747,7 @@ const toRecord = (value) => {
595
747
 
596
748
  //#endregion
597
749
  //#region src/Core.ts
598
- var Core = class {
750
+ var Core = class Core {
599
751
  static apiClass = BaseApi;
600
752
  debugLevel = 0;
601
753
  /**
@@ -607,7 +759,7 @@ var Core = class {
607
759
  */
608
760
  clientSecret;
609
761
  /**
610
- * Flutterwave Environment
762
+ * API Environment
611
763
  */
612
764
  environment = "live";
613
765
  accessValidator;
@@ -619,25 +771,46 @@ var Core = class {
619
771
  * Builder Instance
620
772
  */
621
773
  builder = Builder;
622
- constructor(clientId, clientSecret, encryptionKey, env) {
774
+ constructor(clientId, clientSecret, encryptionKey, env, config) {
775
+ const currentConfig = getConfig();
623
776
  if (typeof clientId === "object") {
624
777
  this.clientId = clientId.clientId;
625
778
  this.clientSecret = clientId.clientSecret;
626
- this.environment = clientId.environment ?? "live";
779
+ this.environment = clientId.environment ?? currentConfig.environment ?? Core.normalizeEnvironment(process.env.ENVIRONMENT) ?? "live";
780
+ this.configure({
781
+ environment: this.environment,
782
+ urls: clientId.urls,
783
+ headers: clientId.headers,
784
+ timeout: clientId.timeout,
785
+ encryptionKey: clientId.encryptionKey,
786
+ auth: clientId.auth
787
+ });
627
788
  } else {
628
789
  this.clientId = clientId ?? process.env.CLIENT_ID ?? "";
629
790
  this.clientSecret = clientSecret ?? process.env.CLIENT_SECRET ?? "";
630
- this.environment = env ?? process.env.ENVIRONMENT ?? "live";
791
+ this.environment = env ?? currentConfig.environment ?? Core.normalizeEnvironment(process.env.ENVIRONMENT) ?? "live";
792
+ this.configure({
793
+ ...config ?? {},
794
+ environment: this.environment,
795
+ encryptionKey: encryptionKey ?? config?.encryptionKey
796
+ });
631
797
  }
632
- if (!this.clientId || !this.clientSecret) throw new Error("Client ID and Client Secret are required to initialize Flutterwave instance");
633
- this.builder.setEnvironment(this.environment);
798
+ if (!this.clientId || !this.clientSecret) throw new Error("Client ID and Client Secret are required to initialize API instance");
634
799
  this.api = this.createApi();
635
800
  }
636
801
  createApi() {
637
802
  return (this.constructor.apiClass ?? BaseApi).initialize(this);
638
803
  }
639
- init(clientId, clientSecret, encryptionKey, env) {
640
- return new this.constructor(clientId, clientSecret, encryptionKey, env);
804
+ init(clientId, clientSecret, encryptionKey, env, config) {
805
+ return new this.constructor(clientId, clientSecret, encryptionKey, env, config);
806
+ }
807
+ configure(config) {
808
+ const nextConfig = defineConfig(config);
809
+ if (nextConfig.environment) {
810
+ this.environment = nextConfig.environment;
811
+ this.builder.setEnvironment(nextConfig.environment);
812
+ }
813
+ return this;
641
814
  }
642
815
  /**
643
816
  * Set the debug level
@@ -659,12 +832,44 @@ var Core = class {
659
832
  return this.environment;
660
833
  }
661
834
  /**
835
+ * Get the configured client identifier.
836
+ */
837
+ getClientId() {
838
+ return this.clientId;
839
+ }
840
+ /**
841
+ * Get the configured client secret.
842
+ */
843
+ getClientSecret() {
844
+ return this.clientSecret;
845
+ }
846
+ /**
847
+ * Get the current shared SDK config.
848
+ */
849
+ getConfig() {
850
+ return getConfig();
851
+ }
852
+ /**
853
+ * Replace the active auth strategy.
854
+ */
855
+ setAuth(auth) {
856
+ return this.configure({ auth });
857
+ }
858
+ /**
859
+ * Clear any configured auth strategy.
860
+ */
861
+ clearAuth() {
862
+ Http.clearAuth();
863
+ return this;
864
+ }
865
+ /**
662
866
  * Set access validator function
663
867
  *
664
868
  * @param validator Function to validate access
665
869
  */
666
870
  setAccessValidator(validator) {
667
871
  this.accessValidator = validator;
872
+ return this;
668
873
  }
669
874
  /**
670
875
  * Validates access using the provided access validator function
@@ -673,7 +878,16 @@ var Core = class {
673
878
  */
674
879
  async validateAccess() {
675
880
  const check = this.accessValidator ? await this.accessValidator(this) : true;
676
- if (check !== true) throw new Error(typeof check === "string" ? check : "Access validation failed");
881
+ if (check === true || check == null) return;
882
+ if (this.isAuthConfigOrArray(check)) {
883
+ this.setAuth(check);
884
+ return;
885
+ }
886
+ if (this.isConfigUpdate(check)) {
887
+ this.configure(check);
888
+ return;
889
+ }
890
+ throw new Error(typeof check === "string" ? check : "Access validation failed");
677
891
  }
678
892
  /**
679
893
  * Use a manifest bundle to create the API instance
@@ -694,29 +908,61 @@ var Core = class {
694
908
  useSdk(bundle) {
695
909
  return this.useDocument(bundle);
696
910
  }
911
+ static normalizeEnvironment = (value) => {
912
+ if (value === "live" || value === "sandbox") return value;
913
+ };
914
+ isAuthConfigOrArray(value) {
915
+ if (Array.isArray(value)) return value.every((entry) => this.isAuthConfig(entry));
916
+ return this.isAuthConfig(value);
917
+ }
918
+ isAuthConfig(value) {
919
+ return typeof value === "object" && value !== null && "type" in value && typeof value.type === "string";
920
+ }
921
+ isConfigUpdate(value) {
922
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
923
+ return [
924
+ "auth",
925
+ "environment",
926
+ "headers",
927
+ "timeout",
928
+ "urls",
929
+ "encryptionKey"
930
+ ].some((key) => key in value);
931
+ }
697
932
  };
698
933
 
699
934
  //#endregion
700
- //#region src/utilities/Manager.ts
701
- const defaultConfig = {
702
- environment: "sandbox",
703
- urls: {
704
- live: "",
705
- sandbox: ""
706
- }
707
- };
708
- let globalConfig = defaultConfig;
709
- const defineConfig = (config) => {
710
- const userConfig = {
711
- ...defaultConfig,
712
- ...config,
713
- urls: {
714
- ...defaultConfig.urls,
715
- ...config.urls
716
- }
935
+ //#region src/utilities/AuthCache.ts
936
+ /**
937
+ * Cache any auth payload returned from a loader until it expires.
938
+ */
939
+ const createAuthCache = (loader) => {
940
+ let cached;
941
+ return async (core) => {
942
+ if (cached && !isExpired(cached.expiresAt)) return cached.auth;
943
+ cached = await loader(core);
944
+ return cached.auth;
717
945
  };
718
- globalConfig = userConfig;
719
- return userConfig;
946
+ };
947
+ /**
948
+ * Cache bearer or oauth-style access tokens returned from a loader until expiry.
949
+ */
950
+ const createAccessTokenCache = (loader) => {
951
+ return createAuthCache(async (core) => {
952
+ const token = await loader(core);
953
+ const expiresAt = token.expiresAt ?? (typeof token.expiresInMs === "number" ? Date.now() + token.expiresInMs : typeof token.expiresInSeconds === "number" ? Date.now() + token.expiresInSeconds * 1e3 : void 0);
954
+ return {
955
+ auth: {
956
+ type: "oauth2",
957
+ accessToken: token.token,
958
+ tokenType: token.tokenType ?? "Bearer"
959
+ },
960
+ expiresAt
961
+ };
962
+ });
963
+ };
964
+ const isExpired = (expiresAt) => {
965
+ return typeof expiresAt === "number" && expiresAt <= Date.now();
720
966
  };
721
967
 
722
968
  //#endregion
@@ -789,14 +1035,18 @@ exports.HttpException = HttpException;
789
1035
  exports.UnauthorizedRequestException = UnauthorizedRequestException;
790
1036
  exports.WebhookValidator = WebhookValidator;
791
1037
  exports.buildUrl = buildUrl;
1038
+ exports.createAccessTokenCache = createAccessTokenCache;
1039
+ exports.createAuthCache = createAuthCache;
792
1040
  exports.createRuntimeApi = createRuntimeApi;
793
1041
  exports.createSdk = createSdk;
794
1042
  exports.defaultConfig = defaultConfig;
795
1043
  exports.defineConfig = defineConfig;
1044
+ exports.getConfig = getConfig;
796
1045
  Object.defineProperty(exports, 'globalConfig', {
797
1046
  enumerable: true,
798
1047
  get: function () {
799
1048
  return globalConfig;
800
1049
  }
801
1050
  });
802
- exports.normalizeValue = normalizeValue;
1051
+ exports.normalizeValue = normalizeValue;
1052
+ exports.resetConfig = resetConfig;