@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.js CHANGED
@@ -64,15 +64,53 @@ const buildUrl = (baseUrl, ...endpoint) => {
64
64
  return (baseUrl + path.normalize(path.join(...normalizeValue(endpoint)))).replace(/([^:]\/)\/+/g, "$1");
65
65
  };
66
66
 
67
+ //#endregion
68
+ //#region src/utilities/Manager.ts
69
+ const defaultConfig = {
70
+ environment: "sandbox",
71
+ urls: {
72
+ live: "",
73
+ sandbox: ""
74
+ },
75
+ headers: {}
76
+ };
77
+ let globalConfig = defaultConfig;
78
+ const defineConfig = (config) => {
79
+ const baseConfig = globalConfig ?? defaultConfig;
80
+ const userConfig = {
81
+ ...baseConfig,
82
+ ...config,
83
+ urls: config.urls ? {
84
+ ...baseConfig.urls ?? defaultConfig.urls,
85
+ ...config.urls
86
+ } : baseConfig.urls,
87
+ headers: config.headers ? {
88
+ ...baseConfig.headers ?? defaultConfig.headers,
89
+ ...config.headers
90
+ } : baseConfig.headers
91
+ };
92
+ globalConfig = userConfig;
93
+ return userConfig;
94
+ };
95
+ const getConfig = () => globalConfig;
96
+ const resetConfig = () => {
97
+ globalConfig = {
98
+ ...defaultConfig,
99
+ urls: { ...defaultConfig.urls ?? {} },
100
+ headers: { ...defaultConfig.headers ?? {} }
101
+ };
102
+ return globalConfig;
103
+ };
104
+
67
105
  //#endregion
68
106
  //#region src/Builder.ts
69
107
  var Builder = class {
70
108
  static baseUrls = {
71
- live: "https://api.flutterwave.com/v4/",
72
- sandbox: "https://developersandbox-api.flutterwave.com/"
109
+ live: "",
110
+ sandbox: ""
73
111
  };
74
112
  /**
75
- * Flutterwave Environment
113
+ * API Environment
76
114
  */
77
115
  static environment;
78
116
  constructor() {}
@@ -90,8 +128,11 @@ var Builder = class {
90
128
  * @returns
91
129
  */
92
130
  static baseUrl() {
93
- if ((process.env.ENVIRONMENT || this.environment || "sandbox") === "live") return this.baseUrls.live;
94
- return this.baseUrls.sandbox;
131
+ const config = getConfig();
132
+ const env = this.environment ?? config.environment ?? this.normalizeEnvironment(process.env.ENVIRONMENT) ?? "sandbox";
133
+ const configuredUrl = config.urls?.[env];
134
+ if (configuredUrl) return this.normalizeBaseUrl(configuredUrl);
135
+ return this.normalizeBaseUrl(this.baseUrls[env]);
95
136
  }
96
137
  /**
97
138
  * Builds a full url based on endpoint provided
@@ -166,10 +207,12 @@ var Builder = class {
166
207
  * @returns A new object with the specified keys encrypted.
167
208
  */
168
209
  static async encryptDetails(input, keysToEncrypt = [], outputMapping = {}) {
210
+ const encryptionKey = getConfig().encryptionKey ?? process.env.ENCRYPTION_KEY;
211
+ if (!encryptionKey) throw new Error("Encryption key is required to encrypt details");
169
212
  const nonce = crypto.randomBytes(12).toString("base64").slice(0, 12);
170
213
  const encryptableKeys = keysToEncrypt.length > 0 ? keysToEncrypt : Object.keys(input);
171
214
  const encrypted = Object.fromEntries(Object.entries(input).map(([key, value]) => {
172
- if (encryptableKeys.includes(key) && typeof value === "string") return [outputMapping?.[key] || key, this.encryptAES(value, process.env.ENCRYPTION_KEY, nonce)];
215
+ if (encryptableKeys.includes(key) && typeof value === "string") return [outputMapping?.[key] || key, this.encryptAES(value, encryptionKey, nonce)];
173
216
  return [key, value];
174
217
  }));
175
218
  for (const key of encryptableKeys) delete input[key];
@@ -195,6 +238,10 @@ var Builder = class {
195
238
  }, key, new TextEncoder().encode(data));
196
239
  return btoa(String.fromCharCode(...new Uint8Array(encryptedData)));
197
240
  }
241
+ static normalizeBaseUrl = (url) => url.endsWith("/") ? url : `${url}/`;
242
+ static normalizeEnvironment = (value) => {
243
+ if (value === "live" || value === "sandbox") return value;
244
+ };
198
245
  };
199
246
 
200
247
  //#endregion
@@ -342,13 +389,41 @@ var Http = class Http {
342
389
  */
343
390
  static setBearerToken(token) {
344
391
  this.bearerToken = token;
392
+ defineConfig({ auth: {
393
+ type: "bearer",
394
+ token
395
+ } });
396
+ }
397
+ static setAuth(auth) {
398
+ defineConfig({ auth });
399
+ }
400
+ static setBasicAuth(username, password) {
401
+ this.setAuth({
402
+ type: "basic",
403
+ username,
404
+ password
405
+ });
406
+ }
407
+ static setApiKey(name, value, location = "header", prefix) {
408
+ this.setAuth({
409
+ type: "apiKey",
410
+ name,
411
+ value,
412
+ in: location,
413
+ prefix
414
+ });
415
+ }
416
+ static clearAuth() {
417
+ this.bearerToken = void 0;
418
+ defineConfig({ auth: void 0 });
345
419
  }
346
420
  setDefaultHeaders(defaults) {
421
+ const config = getConfig();
347
422
  this.headers = {
348
423
  ...defaults,
424
+ ...config.headers ?? {},
349
425
  ...this.headers
350
426
  };
351
- if (Http.bearerToken) this.headers.Authorization = `Bearer ${Http.bearerToken}`;
352
427
  }
353
428
  getHeaders() {
354
429
  return this.headers;
@@ -357,13 +432,15 @@ var Http = class Http {
357
432
  return this.body;
358
433
  }
359
434
  axiosApi() {
435
+ const config = getConfig();
360
436
  this.setDefaultHeaders({
361
437
  "Accept": "application/json",
362
438
  "Content-Type": "application/json"
363
439
  });
364
440
  const instance = axios.create({
365
441
  baseURL: Builder.baseUrl(),
366
- headers: this.getHeaders()
442
+ headers: this.getHeaders(),
443
+ timeout: config.timeout
367
444
  });
368
445
  if (Http.debugLevel > 0) {
369
446
  instance.interceptors.request.use((request) => {
@@ -416,11 +493,18 @@ var Http = class Http {
416
493
  */
417
494
  static async send(url, method, body, headers = {}, params) {
418
495
  try {
419
- const { data } = await new Http(headers).axiosApi()({
496
+ const request = await this.prepareRequest({
420
497
  url,
421
498
  method,
422
- data: body,
423
- params
499
+ body,
500
+ headers: this.toHeaderRecord(headers),
501
+ params: { ...params ?? {} }
502
+ });
503
+ const { data } = await new Http(request.headers, request.body).axiosApi()({
504
+ url: request.url,
505
+ method: request.method,
506
+ data: request.body,
507
+ params: request.params
424
508
  });
425
509
  return {
426
510
  success: true,
@@ -433,6 +517,71 @@ var Http = class Http {
433
517
  throw this.exception(e.response?.status ?? 500, error || e, e);
434
518
  }
435
519
  }
520
+ static async prepareRequest(request) {
521
+ let prepared = {
522
+ ...request,
523
+ headers: { ...request.headers },
524
+ params: { ...request.params ?? {} }
525
+ };
526
+ for (const auth of this.getConfiguredAuth()) prepared = await this.applyAuth(prepared, auth);
527
+ return prepared;
528
+ }
529
+ static getConfiguredAuth() {
530
+ const configuredAuth = getConfig().auth;
531
+ if (configuredAuth) return Array.isArray(configuredAuth) ? configuredAuth : [configuredAuth];
532
+ if (this.bearerToken) return [{
533
+ type: "bearer",
534
+ token: this.bearerToken
535
+ }];
536
+ return [];
537
+ }
538
+ static async applyAuth(request, auth) {
539
+ switch (auth.type) {
540
+ case "bearer":
541
+ this.setHeaderIfMissing(request.headers, "Authorization", `${auth.prefix ?? "Bearer"} ${auth.token}`.trim());
542
+ return request;
543
+ case "oauth2":
544
+ this.setHeaderIfMissing(request.headers, "Authorization", `${auth.tokenType ?? "Bearer"} ${auth.accessToken}`.trim());
545
+ return request;
546
+ case "basic": {
547
+ const encoded = Buffer.from(`${auth.username}:${auth.password}`).toString("base64");
548
+ this.setHeaderIfMissing(request.headers, "Authorization", `Basic ${encoded}`);
549
+ return request;
550
+ }
551
+ case "apiKey": {
552
+ const value = auth.prefix ? `${auth.prefix} ${auth.value}`.trim() : auth.value;
553
+ const location = auth.in ?? "header";
554
+ if (location === "query") {
555
+ if (request.params[auth.name] === void 0) request.params[auth.name] = value;
556
+ return request;
557
+ }
558
+ if (location === "cookie") {
559
+ this.appendCookie(request.headers, auth.name, value);
560
+ return request;
561
+ }
562
+ this.setHeaderIfMissing(request.headers, auth.name, value);
563
+ return request;
564
+ }
565
+ case "custom": return await auth.apply({
566
+ ...request,
567
+ headers: { ...request.headers },
568
+ params: { ...request.params }
569
+ });
570
+ }
571
+ }
572
+ static setHeaderIfMissing(headers, name, value) {
573
+ if (!Object.keys(headers).find((header) => header.toLowerCase() === name.toLowerCase())) headers[name] = value;
574
+ }
575
+ static appendCookie(headers, name, value) {
576
+ const headerName = Object.keys(headers).find((header) => header.toLowerCase() === "cookie") ?? "Cookie";
577
+ const existingCookie = headers[headerName];
578
+ const cookieEntry = `${encodeURIComponent(name)}=${encodeURIComponent(value)}`;
579
+ if (!existingCookie) {
580
+ headers[headerName] = cookieEntry;
581
+ return;
582
+ }
583
+ if (!existingCookie.split(";").map((part) => part.trim()).some((part) => part.startsWith(`${encodeURIComponent(name)}=`))) headers[headerName] = `${existingCookie}; ${cookieEntry}`;
584
+ }
436
585
  /**
437
586
  * Create an HttpException from status and error
438
587
  *
@@ -457,13 +606,16 @@ var Http = class Http {
457
606
  if (this.apiInstance) this.apiInstance.setLastException(exception);
458
607
  return exception;
459
608
  }
609
+ static toHeaderRecord = (headers) => {
610
+ return Object.fromEntries(Object.entries(headers).map(([key, value]) => [key, String(value)]));
611
+ };
460
612
  };
461
613
 
462
614
  //#endregion
463
615
  //#region src/Apis/BaseApi.ts
464
616
  var BaseApi = class {
465
617
  /**
466
- * Flutterwave instance
618
+ * API Core instance for API bootstrapping and shared utilities
467
619
  */
468
620
  #core;
469
621
  lastException;
@@ -569,7 +721,7 @@ const toRecord = (value) => {
569
721
 
570
722
  //#endregion
571
723
  //#region src/Core.ts
572
- var Core = class {
724
+ var Core = class Core {
573
725
  static apiClass = BaseApi;
574
726
  debugLevel = 0;
575
727
  /**
@@ -581,7 +733,7 @@ var Core = class {
581
733
  */
582
734
  clientSecret;
583
735
  /**
584
- * Flutterwave Environment
736
+ * API Environment
585
737
  */
586
738
  environment = "live";
587
739
  accessValidator;
@@ -593,25 +745,46 @@ var Core = class {
593
745
  * Builder Instance
594
746
  */
595
747
  builder = Builder;
596
- constructor(clientId, clientSecret, encryptionKey, env) {
748
+ constructor(clientId, clientSecret, encryptionKey, env, config) {
749
+ const currentConfig = getConfig();
597
750
  if (typeof clientId === "object") {
598
751
  this.clientId = clientId.clientId;
599
752
  this.clientSecret = clientId.clientSecret;
600
- this.environment = clientId.environment ?? "live";
753
+ this.environment = clientId.environment ?? currentConfig.environment ?? Core.normalizeEnvironment(process.env.ENVIRONMENT) ?? "live";
754
+ this.configure({
755
+ environment: this.environment,
756
+ urls: clientId.urls,
757
+ headers: clientId.headers,
758
+ timeout: clientId.timeout,
759
+ encryptionKey: clientId.encryptionKey,
760
+ auth: clientId.auth
761
+ });
601
762
  } else {
602
763
  this.clientId = clientId ?? process.env.CLIENT_ID ?? "";
603
764
  this.clientSecret = clientSecret ?? process.env.CLIENT_SECRET ?? "";
604
- this.environment = env ?? process.env.ENVIRONMENT ?? "live";
765
+ this.environment = env ?? currentConfig.environment ?? Core.normalizeEnvironment(process.env.ENVIRONMENT) ?? "live";
766
+ this.configure({
767
+ ...config ?? {},
768
+ environment: this.environment,
769
+ encryptionKey: encryptionKey ?? config?.encryptionKey
770
+ });
605
771
  }
606
- if (!this.clientId || !this.clientSecret) throw new Error("Client ID and Client Secret are required to initialize Flutterwave instance");
607
- this.builder.setEnvironment(this.environment);
772
+ if (!this.clientId || !this.clientSecret) throw new Error("Client ID and Client Secret are required to initialize API instance");
608
773
  this.api = this.createApi();
609
774
  }
610
775
  createApi() {
611
776
  return (this.constructor.apiClass ?? BaseApi).initialize(this);
612
777
  }
613
- init(clientId, clientSecret, encryptionKey, env) {
614
- return new this.constructor(clientId, clientSecret, encryptionKey, env);
778
+ init(clientId, clientSecret, encryptionKey, env, config) {
779
+ return new this.constructor(clientId, clientSecret, encryptionKey, env, config);
780
+ }
781
+ configure(config) {
782
+ const nextConfig = defineConfig(config);
783
+ if (nextConfig.environment) {
784
+ this.environment = nextConfig.environment;
785
+ this.builder.setEnvironment(nextConfig.environment);
786
+ }
787
+ return this;
615
788
  }
616
789
  /**
617
790
  * Set the debug level
@@ -633,12 +806,44 @@ var Core = class {
633
806
  return this.environment;
634
807
  }
635
808
  /**
809
+ * Get the configured client identifier.
810
+ */
811
+ getClientId() {
812
+ return this.clientId;
813
+ }
814
+ /**
815
+ * Get the configured client secret.
816
+ */
817
+ getClientSecret() {
818
+ return this.clientSecret;
819
+ }
820
+ /**
821
+ * Get the current shared SDK config.
822
+ */
823
+ getConfig() {
824
+ return getConfig();
825
+ }
826
+ /**
827
+ * Replace the active auth strategy.
828
+ */
829
+ setAuth(auth) {
830
+ return this.configure({ auth });
831
+ }
832
+ /**
833
+ * Clear any configured auth strategy.
834
+ */
835
+ clearAuth() {
836
+ Http.clearAuth();
837
+ return this;
838
+ }
839
+ /**
636
840
  * Set access validator function
637
841
  *
638
842
  * @param validator Function to validate access
639
843
  */
640
844
  setAccessValidator(validator) {
641
845
  this.accessValidator = validator;
846
+ return this;
642
847
  }
643
848
  /**
644
849
  * Validates access using the provided access validator function
@@ -647,7 +852,16 @@ var Core = class {
647
852
  */
648
853
  async validateAccess() {
649
854
  const check = this.accessValidator ? await this.accessValidator(this) : true;
650
- if (check !== true) throw new Error(typeof check === "string" ? check : "Access validation failed");
855
+ if (check === true || check == null) return;
856
+ if (this.isAuthConfigOrArray(check)) {
857
+ this.setAuth(check);
858
+ return;
859
+ }
860
+ if (this.isConfigUpdate(check)) {
861
+ this.configure(check);
862
+ return;
863
+ }
864
+ throw new Error(typeof check === "string" ? check : "Access validation failed");
651
865
  }
652
866
  /**
653
867
  * Use a manifest bundle to create the API instance
@@ -668,29 +882,61 @@ var Core = class {
668
882
  useSdk(bundle) {
669
883
  return this.useDocument(bundle);
670
884
  }
885
+ static normalizeEnvironment = (value) => {
886
+ if (value === "live" || value === "sandbox") return value;
887
+ };
888
+ isAuthConfigOrArray(value) {
889
+ if (Array.isArray(value)) return value.every((entry) => this.isAuthConfig(entry));
890
+ return this.isAuthConfig(value);
891
+ }
892
+ isAuthConfig(value) {
893
+ return typeof value === "object" && value !== null && "type" in value && typeof value.type === "string";
894
+ }
895
+ isConfigUpdate(value) {
896
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
897
+ return [
898
+ "auth",
899
+ "environment",
900
+ "headers",
901
+ "timeout",
902
+ "urls",
903
+ "encryptionKey"
904
+ ].some((key) => key in value);
905
+ }
671
906
  };
672
907
 
673
908
  //#endregion
674
- //#region src/utilities/Manager.ts
675
- const defaultConfig = {
676
- environment: "sandbox",
677
- urls: {
678
- live: "",
679
- sandbox: ""
680
- }
681
- };
682
- let globalConfig = defaultConfig;
683
- const defineConfig = (config) => {
684
- const userConfig = {
685
- ...defaultConfig,
686
- ...config,
687
- urls: {
688
- ...defaultConfig.urls,
689
- ...config.urls
690
- }
909
+ //#region src/utilities/AuthCache.ts
910
+ /**
911
+ * Cache any auth payload returned from a loader until it expires.
912
+ */
913
+ const createAuthCache = (loader) => {
914
+ let cached;
915
+ return async (core) => {
916
+ if (cached && !isExpired(cached.expiresAt)) return cached.auth;
917
+ cached = await loader(core);
918
+ return cached.auth;
691
919
  };
692
- globalConfig = userConfig;
693
- return userConfig;
920
+ };
921
+ /**
922
+ * Cache bearer or oauth-style access tokens returned from a loader until expiry.
923
+ */
924
+ const createAccessTokenCache = (loader) => {
925
+ return createAuthCache(async (core) => {
926
+ const token = await loader(core);
927
+ const expiresAt = token.expiresAt ?? (typeof token.expiresInMs === "number" ? Date.now() + token.expiresInMs : typeof token.expiresInSeconds === "number" ? Date.now() + token.expiresInSeconds * 1e3 : void 0);
928
+ return {
929
+ auth: {
930
+ type: "oauth2",
931
+ accessToken: token.token,
932
+ tokenType: token.tokenType ?? "Bearer"
933
+ },
934
+ expiresAt
935
+ };
936
+ });
937
+ };
938
+ const isExpired = (expiresAt) => {
939
+ return typeof expiresAt === "number" && expiresAt <= Date.now();
694
940
  };
695
941
 
696
942
  //#endregion
@@ -753,4 +999,4 @@ var WebhookValidator = class {
753
999
  };
754
1000
 
755
1001
  //#endregion
756
- export { BadRequestException, BaseApi, Builder, Core, ForbiddenRequestException, Http, HttpException, UnauthorizedRequestException, WebhookValidator, buildUrl, createRuntimeApi, createSdk, defaultConfig, defineConfig, globalConfig, normalizeValue };
1002
+ export { BadRequestException, BaseApi, Builder, Core, ForbiddenRequestException, Http, HttpException, UnauthorizedRequestException, WebhookValidator, buildUrl, createAccessTokenCache, createAuthCache, createRuntimeApi, createSdk, defaultConfig, defineConfig, getConfig, globalConfig, normalizeValue, resetConfig };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@oapiex/sdk-kit",
3
3
  "type": "module",
4
- "version": "0.1.2",
4
+ "version": "0.1.4",
5
5
  "description": "A TypeScript Base for building SDKs using OpenAPI specifications extracted by OAPIEX.",
6
6
  "main": "./dist/index.cjs",
7
7
  "private": false,