zone4code-sdk 1.0.5 → 1.0.6

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
@@ -50,6 +50,22 @@ function decodeJwtPayload(token) {
50
50
  return null;
51
51
  }
52
52
  }
53
+ function normalizeHeaders(headers) {
54
+ const result = {};
55
+ if (!headers) return result;
56
+ if (typeof Headers !== "undefined" && headers instanceof Headers) {
57
+ headers.forEach((value, key) => {
58
+ result[key] = value;
59
+ });
60
+ } else if (Array.isArray(headers)) {
61
+ for (const [key, value] of headers) {
62
+ result[key] = value;
63
+ }
64
+ } else {
65
+ Object.assign(result, headers);
66
+ }
67
+ return result;
68
+ }
53
69
  var AuthClient = class _AuthClient {
54
70
  gatewayUrl;
55
71
  tenantId;
@@ -59,26 +75,39 @@ var AuthClient = class _AuthClient {
59
75
  token = null;
60
76
  refreshTokenValue = null;
61
77
  fetchFn;
78
+ autoRefresh;
79
+ tokenExpiryBuffer;
80
+ onSessionExpiredCallback;
81
+ sessionExpiredListeners = [];
82
+ activeRefreshPromise = null;
83
+ storageLoadedPromise;
62
84
  static TOKEN_KEY_PREFIX = "z4c_token_";
63
85
  constructor(options) {
86
+ const storage = options.storage;
87
+ const storageKey = options.storageKey || _AuthClient.TOKEN_KEY_PREFIX + options.tenantId;
88
+ const refreshStorageKey = storageKey + "_refresh";
64
89
  this.gatewayUrl = options.gatewayUrl.replace(/\/+$/, "");
65
90
  this.tenantId = options.tenantId;
66
- this.storage = options.storage;
67
- this.storageKey = options.storageKey || _AuthClient.TOKEN_KEY_PREFIX + this.tenantId;
68
- this.refreshStorageKey = this.storageKey + "_refresh";
91
+ this.storage = storage;
92
+ this.storageKey = storageKey;
93
+ this.refreshStorageKey = refreshStorageKey;
69
94
  this.fetchFn = options.fetchFn || (typeof fetch !== "undefined" ? fetch : globalThis.fetch);
70
- if (options.initialToken) {
71
- this.setToken(options.initialToken);
72
- } else {
73
- Promise.resolve(this.storage.getItem(this.storageKey)).then((stored) => {
74
- if (stored) this.token = stored;
75
- }).catch(() => {
76
- });
77
- Promise.resolve(this.storage.getItem(this.refreshStorageKey)).then((stored) => {
78
- if (stored) this.refreshTokenValue = stored;
79
- }).catch(() => {
80
- });
81
- }
95
+ this.autoRefresh = options.autoRefresh ?? true;
96
+ this.tokenExpiryBuffer = options.tokenExpiryBuffer ?? 30;
97
+ this.onSessionExpiredCallback = options.onSessionExpired;
98
+ this.storageLoadedPromise = (async () => {
99
+ if (options.initialToken) {
100
+ this.setToken(options.initialToken);
101
+ } else {
102
+ try {
103
+ const storedToken = await storage.getItem(storageKey);
104
+ if (storedToken) this.token = storedToken;
105
+ const storedRefresh = await storage.getItem(refreshStorageKey);
106
+ if (storedRefresh) this.refreshTokenValue = storedRefresh;
107
+ } catch {
108
+ }
109
+ }
110
+ })();
82
111
  }
83
112
  getToken() {
84
113
  return this.token;
@@ -105,6 +134,91 @@ var AuthClient = class _AuthClient {
105
134
  isAuthenticated() {
106
135
  return !!this.token;
107
136
  }
137
+ /**
138
+ * Check whether the current access token is expired or expiring within bufferSeconds
139
+ */
140
+ isTokenExpired(bufferSeconds = this.tokenExpiryBuffer) {
141
+ if (!this.token) return true;
142
+ const payload = decodeJwtPayload(this.token);
143
+ if (!payload || typeof payload.exp !== "number") return false;
144
+ const nowSeconds = Math.floor(Date.now() / 1e3);
145
+ return nowSeconds >= payload.exp - bufferSeconds;
146
+ }
147
+ /**
148
+ * Returns a valid access token, proactively refreshing it if expired and autoRefresh is enabled
149
+ */
150
+ async getValidToken() {
151
+ await this.storageLoadedPromise;
152
+ if (!this.token) return null;
153
+ if (!this.autoRefresh) return this.token;
154
+ if (this.isTokenExpired()) {
155
+ const refreshToken = this.getRefreshToken();
156
+ if (refreshToken) {
157
+ try {
158
+ const res = await this.refreshToken(refreshToken);
159
+ return res.token || this.token;
160
+ } catch {
161
+ return null;
162
+ }
163
+ } else {
164
+ this.notifySessionExpired();
165
+ return null;
166
+ }
167
+ }
168
+ return this.token;
169
+ }
170
+ /**
171
+ * Register a callback triggered when session has expired completely
172
+ */
173
+ onSessionExpired(listener) {
174
+ this.sessionExpiredListeners.push(listener);
175
+ return () => {
176
+ this.sessionExpiredListeners = this.sessionExpiredListeners.filter((l) => l !== listener);
177
+ };
178
+ }
179
+ notifySessionExpired() {
180
+ this.logout();
181
+ for (const listener of this.sessionExpiredListeners) {
182
+ try {
183
+ listener();
184
+ } catch (e) {
185
+ console.error("Error in onSessionExpired listener:", e);
186
+ }
187
+ }
188
+ if (this.onSessionExpiredCallback) {
189
+ try {
190
+ this.onSessionExpiredCallback();
191
+ } catch (e) {
192
+ console.error("Error in onSessionExpired callback:", e);
193
+ }
194
+ }
195
+ }
196
+ /**
197
+ * Perform an HTTP request with automatic Authorization injection, proactive refresh, and reactive 401 retry
198
+ */
199
+ async fetchWithAuth(url, init = {}) {
200
+ await this.storageLoadedPromise;
201
+ const token = await this.getValidToken();
202
+ const headers = normalizeHeaders(init.headers);
203
+ if (!headers["x-tenant-id"] && !headers["X-Tenant-Id"]) {
204
+ headers["x-tenant-id"] = this.tenantId;
205
+ }
206
+ if (token && !headers["Authorization"] && !headers["authorization"]) {
207
+ headers["Authorization"] = `Bearer ${token}`;
208
+ }
209
+ let res = await this.fetchFn(url, { ...init, headers });
210
+ if (res.status === 401 && this.autoRefresh && this.getRefreshToken()) {
211
+ try {
212
+ const refreshed = await this.refreshToken();
213
+ if (refreshed?.token) {
214
+ headers["Authorization"] = `Bearer ${refreshed.token}`;
215
+ res = await this.fetchFn(url, { ...init, headers });
216
+ }
217
+ } catch {
218
+ }
219
+ }
220
+ return res;
221
+ }
108
222
  /**
109
223
  * Synchronously parse and return current authenticated user from JWT token
110
224
  */
@@ -260,36 +374,48 @@ var AuthClient = class _AuthClient {
260
374
  async refreshToken(refreshToken) {
261
375
  const tokenToUse = refreshToken || this.getRefreshToken();
262
376
  if (!tokenToUse) {
377
+ this.notifySessionExpired();
263
378
  throw new Error("No refresh token available");
264
379
  }
265
- const url = `${this.gatewayUrl}/auth/${this.tenantId}/refresh`;
266
- const res = await this.fetchFn(url, {
267
- method: "POST",
268
- headers: {
269
- "Content-Type": "application/json",
270
- "x-tenant-id": this.tenantId
271
- },
272
- body: JSON.stringify({ refresh_token: tokenToUse })
273
- });
274
- if (!res.ok) {
275
- const err = await res.json().catch(() => ({ message: res.statusText }));
276
- throw new Error(err.message || err.error || `Token refresh failed with HTTP ${res.status}`);
277
- }
278
- const json = await res.json();
279
- const token = json.token || json.access_token;
280
- if (token) {
281
- this.setToken(token);
282
- }
283
- if (json.refresh_token) {
284
- this.setRefreshToken(json.refresh_token);
380
+ if (this.activeRefreshPromise) {
381
+ return this.activeRefreshPromise;
285
382
  }
286
- const user = this.getUser() || json.user;
287
- return {
288
- token: token || "",
289
- refreshToken: json.refresh_token,
290
- user,
291
- ...json
292
- };
383
+ this.activeRefreshPromise = (async () => {
384
+ try {
385
+ const url = `${this.gatewayUrl}/auth/${this.tenantId}/refresh`;
386
+ const res = await this.fetchFn(url, {
387
+ method: "POST",
388
+ headers: {
389
+ "Content-Type": "application/json",
390
+ "x-tenant-id": this.tenantId
391
+ },
392
+ body: JSON.stringify({ refresh_token: tokenToUse })
393
+ });
394
+ if (!res.ok) {
395
+ const err = await res.json().catch(() => ({ message: res.statusText }));
396
+ this.notifySessionExpired();
397
+ throw new Error(err.message || err.error || `Token refresh failed with HTTP ${res.status}`);
398
+ }
399
+ const json = await res.json();
400
+ const token = json.token || json.access_token;
401
+ if (token) {
402
+ this.setToken(token);
403
+ }
404
+ if (json.refresh_token) {
405
+ this.setRefreshToken(json.refresh_token);
406
+ }
407
+ const user = this.getUser() || json.user;
408
+ return {
409
+ token: token || "",
410
+ refreshToken: json.refresh_token,
411
+ user,
412
+ ...json
413
+ };
414
+ } finally {
415
+ this.activeRefreshPromise = null;
416
+ }
417
+ })();
418
+ return this.activeRefreshPromise;
293
419
  }
294
420
  /**
295
421
  * Request password reset email
@@ -418,13 +544,15 @@ var EntityQueryBuilder = class {
418
544
  typeName;
419
545
  getToken;
420
546
  fetchFn;
547
+ fetchWithAuth;
421
548
  queryParams = new URLSearchParams();
422
549
  constructor(options) {
423
550
  this.gatewayUrl = options.gatewayUrl.replace(/\/+$/, "");
424
551
  this.tenantId = options.tenantId;
425
552
  this.typeName = options.typeName;
426
- this.getToken = options.getToken;
553
+ this.getToken = options.getToken || (() => null);
427
554
  this.fetchFn = options.fetchFn || (typeof fetch !== "undefined" ? fetch : globalThis.fetch);
555
+ this.fetchWithAuth = options.fetchWithAuth;
428
556
  }
429
557
  getHeaders() {
430
558
  const headers = {
@@ -437,6 +565,13 @@ var EntityQueryBuilder = class {
437
565
  }
438
566
  return headers;
439
567
  }
568
+ async executeRequest(url, init = {}) {
569
+ if (this.fetchWithAuth) {
570
+ return this.fetchWithAuth(url, init);
571
+ }
572
+ const headers = { ...this.getHeaders(), ...init.headers || {} };
573
+ return this.fetchFn(url, { ...init, headers });
574
+ }
440
575
  // ----------------------------------------------------
441
576
  // Filter Operators
442
577
  // ----------------------------------------------------
@@ -545,9 +680,7 @@ var EntityQueryBuilder = class {
545
680
  async list() {
546
681
  const qs = this.queryParams.toString();
547
682
  const url = `${this.gatewayUrl}/generic/${this.tenantId}/${this.typeName}${qs ? `?${qs}` : ""}`;
548
- const res = await this.fetchFn(url, {
549
- headers: this.getHeaders()
550
- });
683
+ const res = await this.executeRequest(url);
551
684
  if (!res.ok) {
552
685
  const err = await res.json().catch(() => ({ message: res.statusText }));
553
686
  throw new Error(err.message || `Query failed with HTTP ${res.status}`);
@@ -570,9 +703,7 @@ var EntityQueryBuilder = class {
570
703
  }
571
704
  const qs = params.toString();
572
705
  const url = `${this.gatewayUrl}/generic/${this.tenantId}/${this.typeName}/${id}${qs ? `?${qs}` : ""}`;
573
- const res = await this.fetchFn(url, {
574
- headers: this.getHeaders()
575
- });
706
+ const res = await this.executeRequest(url);
576
707
  if (!res.ok) {
577
708
  const err = await res.json().catch(() => ({ message: res.statusText }));
578
709
  throw new Error(err.message || `Failed to fetch ${this.typeName}/${id}`);
@@ -584,9 +715,8 @@ var EntityQueryBuilder = class {
584
715
  */
585
716
  async create(data) {
586
717
  const url = `${this.gatewayUrl}/generic/${this.tenantId}/${this.typeName}`;
587
- const res = await this.fetchFn(url, {
718
+ const res = await this.executeRequest(url, {
588
719
  method: "POST",
589
- headers: this.getHeaders(),
590
720
  body: JSON.stringify(data)
591
721
  });
592
722
  if (!res.ok) {
@@ -600,9 +730,8 @@ var EntityQueryBuilder = class {
600
730
  */
601
731
  async update(id, partialData) {
602
732
  const url = `${this.gatewayUrl}/generic/${this.tenantId}/${this.typeName}/${id}`;
603
- const res = await this.fetchFn(url, {
733
+ const res = await this.executeRequest(url, {
604
734
  method: "PATCH",
605
- headers: this.getHeaders(),
606
735
  body: JSON.stringify(partialData)
607
736
  });
608
737
  if (!res.ok) {
@@ -616,9 +745,8 @@ var EntityQueryBuilder = class {
616
745
  */
617
746
  async delete(id) {
618
747
  const url = `${this.gatewayUrl}/generic/${this.tenantId}/${this.typeName}/${id}`;
619
- const res = await this.fetchFn(url, {
620
- method: "DELETE",
621
- headers: this.getHeaders()
748
+ const res = await this.executeRequest(url, {
749
+ method: "DELETE"
622
750
  });
623
751
  if (!res.ok) {
624
752
  const err = await res.json().catch(() => ({ message: res.statusText }));
@@ -631,9 +759,7 @@ var EntityQueryBuilder = class {
631
759
  */
632
760
  async revisions(id) {
633
761
  const url = `${this.gatewayUrl}/generic/${this.tenantId}/${this.typeName}/${id}/revisions`;
634
- const res = await this.fetchFn(url, {
635
- headers: this.getHeaders()
636
- });
762
+ const res = await this.executeRequest(url);
637
763
  if (!res.ok) {
638
764
  const err = await res.json().catch(() => ({ message: res.statusText }));
639
765
  throw new Error(err.message || `Failed to fetch revisions for ${this.typeName}/${id}`);
@@ -646,9 +772,7 @@ var EntityQueryBuilder = class {
646
772
  async getRelations(id, lang) {
647
773
  const qs = lang ? `?lang=${encodeURIComponent(lang)}` : "";
648
774
  const url = `${this.gatewayUrl}/generic/${this.tenantId}/${this.typeName}/${id}/relations${qs}`;
649
- const res = await this.fetchFn(url, {
650
- headers: this.getHeaders()
651
- });
775
+ const res = await this.executeRequest(url);
652
776
  if (!res.ok) {
653
777
  const err = await res.json().catch(() => ({ message: res.statusText }));
654
778
  throw new Error(err.message || `Failed to fetch relations for ${this.typeName}/${id}`);
@@ -686,9 +810,8 @@ var EntityQueryBuilder = class {
686
810
  };
687
811
  }
688
812
  const url = `${this.gatewayUrl}/generic/${this.tenantId}/${this.typeName}/${id}/relations`;
689
- const res = await this.fetchFn(url, {
813
+ const res = await this.executeRequest(url, {
690
814
  method: "POST",
691
- headers: this.getHeaders(),
692
815
  body: JSON.stringify(payload)
693
816
  });
694
817
  if (!res.ok) {
@@ -702,9 +825,8 @@ var EntityQueryBuilder = class {
702
825
  */
703
826
  async setLinks(id, payload) {
704
827
  const url = `${this.gatewayUrl}/generic/${this.tenantId}/${this.typeName}/${id}/relations`;
705
- const res = await this.fetchFn(url, {
828
+ const res = await this.executeRequest(url, {
706
829
  method: "PUT",
707
- headers: this.getHeaders(),
708
830
  body: JSON.stringify(payload)
709
831
  });
710
832
  if (!res.ok) {
@@ -718,9 +840,8 @@ var EntityQueryBuilder = class {
718
840
  */
719
841
  async unlink(id, relationName, targetId, targetTypeName) {
720
842
  const url = `${this.gatewayUrl}/generic/${this.tenantId}/${this.typeName}/${id}/relations/${encodeURIComponent(relationName)}/${encodeURIComponent(targetId)}?targetTypeName=${encodeURIComponent(targetTypeName)}`;
721
- const res = await this.fetchFn(url, {
722
- method: "DELETE",
723
- headers: this.getHeaders()
843
+ const res = await this.executeRequest(url, {
844
+ method: "DELETE"
724
845
  });
725
846
  if (!res.ok) {
726
847
  const err = await res.json().catch(() => ({ message: res.statusText }));
@@ -733,9 +854,8 @@ var EntityQueryBuilder = class {
733
854
  */
734
855
  async action(actionName, payload) {
735
856
  const url = `${this.gatewayUrl}/generic/${this.tenantId}/${this.typeName}/actions/${actionName}`;
736
- const res = await this.fetchFn(url, {
857
+ const res = await this.executeRequest(url, {
737
858
  method: "POST",
738
- headers: this.getHeaders(),
739
859
  body: payload ? JSON.stringify(payload) : void 0
740
860
  });
741
861
  if (!res.ok) {
@@ -749,9 +869,8 @@ var EntityQueryBuilder = class {
749
869
  */
750
870
  async instanceAction(id, actionName, payload) {
751
871
  const url = `${this.gatewayUrl}/generic/${this.tenantId}/${this.typeName}/${id}/actions/${actionName}`;
752
- const res = await this.fetchFn(url, {
872
+ const res = await this.executeRequest(url, {
753
873
  method: "POST",
754
- headers: this.getHeaders(),
755
874
  body: payload ? JSON.stringify(payload) : void 0
756
875
  });
757
876
  if (!res.ok) {
@@ -766,9 +885,8 @@ var EntityQueryBuilder = class {
766
885
  async exportCsv() {
767
886
  const qs = this.queryParams.toString();
768
887
  const url = `${this.gatewayUrl}/generic/${this.tenantId}/${this.typeName}/export/csv${qs ? `?${qs}` : ""}`;
769
- const res = await this.fetchFn(url, {
888
+ const res = await this.executeRequest(url, {
770
889
  headers: {
771
- ...this.getHeaders(),
772
890
  "Accept": "text/csv"
773
891
  }
774
892
  });
@@ -786,11 +904,13 @@ var SchemaClient = class {
786
904
  tenantId;
787
905
  getToken;
788
906
  fetchFn;
907
+ fetchWithAuth;
789
908
  constructor(options) {
790
909
  this.gatewayUrl = options.gatewayUrl.replace(/\/+$/, "");
791
910
  this.tenantId = options.tenantId;
792
- this.getToken = options.getToken;
911
+ this.getToken = options.getToken || (() => null);
793
912
  this.fetchFn = options.fetchFn || (typeof fetch !== "undefined" ? fetch : globalThis.fetch);
913
+ this.fetchWithAuth = options.fetchWithAuth;
794
914
  }
795
915
  getHeaders() {
796
916
  const headers = {
@@ -808,11 +928,11 @@ var SchemaClient = class {
808
928
  */
809
929
  async define(definition) {
810
930
  const url = `${this.gatewayUrl}/generic/${this.tenantId}/definition`;
811
- const res = await this.fetchFn(url, {
931
+ const init = {
812
932
  method: "POST",
813
- headers: this.getHeaders(),
814
933
  body: JSON.stringify(definition)
815
- });
934
+ };
935
+ const res = this.fetchWithAuth ? await this.fetchWithAuth(url, init) : await this.fetchFn(url, { ...init, headers: this.getHeaders() });
816
936
  if (!res.ok) {
817
937
  const err = await res.json().catch(() => ({ message: res.statusText }));
818
938
  throw new Error(err.message || `Failed to define schema for ${definition.typeName}`);
@@ -866,13 +986,17 @@ var Zone4CodeClient = class {
866
986
  storage: this.storage,
867
987
  storageKey: config.storageKey,
868
988
  initialToken: config.token,
869
- fetchFn: this.fetchFn
989
+ fetchFn: this.fetchFn,
990
+ autoRefresh: config.autoRefresh,
991
+ tokenExpiryBuffer: config.tokenExpiryBuffer,
992
+ onSessionExpired: config.onSessionExpired
870
993
  });
871
994
  this.schema = new SchemaClient({
872
995
  gatewayUrl: this.gatewayUrl,
873
996
  tenantId: this.tenantId,
874
997
  getToken: () => this.auth.getToken(),
875
- fetchFn: this.fetchFn
998
+ fetchFn: this.fetchFn,
999
+ fetchWithAuth: (url, init) => this.auth.fetchWithAuth(url, init)
876
1000
  });
877
1001
  }
878
1002
  /**
@@ -884,7 +1008,8 @@ var Zone4CodeClient = class {
884
1008
  tenantId: this.tenantId,
885
1009
  typeName,
886
1010
  getToken: () => this.auth.getToken(),
887
- fetchFn: this.fetchFn
1011
+ fetchFn: this.fetchFn,
1012
+ fetchWithAuth: (url, init) => this.auth.fetchWithAuth(url, init)
888
1013
  });
889
1014
  }
890
1015
  /**
@@ -893,21 +1018,17 @@ var Zone4CodeClient = class {
893
1018
  entities(typeName) {
894
1019
  return this.from(typeName);
895
1020
  }
1021
+ /**
1022
+ * Register a listener called whenever the session expires completely
1023
+ */
1024
+ onSessionExpired(callback) {
1025
+ return this.auth.onSessionExpired(callback);
1026
+ }
896
1027
  /**
897
1028
  * Get current authenticated user profile and platform wallet
898
1029
  */
899
1030
  async getMe() {
900
- const token = this.auth.getToken();
901
- if (!token) {
902
- throw new Error("Not authenticated: please call login() or setToken() first");
903
- }
904
- const res = await this.fetchFn(`${this.gatewayUrl}/generic/${this.tenantId}/me`, {
905
- headers: {
906
- "Content-Type": "application/json",
907
- "Authorization": `Bearer ${token}`,
908
- "x-tenant-id": this.tenantId
909
- }
910
- });
1031
+ const res = await this.auth.fetchWithAuth(`${this.gatewayUrl}/generic/${this.tenantId}/me`);
911
1032
  if (!res.ok) {
912
1033
  const err = await res.json().catch(() => ({ message: res.statusText }));
913
1034
  throw new Error(err.message || `Failed to fetch profile & wallet (HTTP ${res.status})`);