zone4code-sdk 1.0.5 → 1.0.7

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,94 @@ 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 (init.body && !headers["Content-Type"] && !headers["content-type"]) {
207
+ headers["Content-Type"] = "application/json";
208
+ }
209
+ if (token && !headers["Authorization"] && !headers["authorization"]) {
210
+ headers["Authorization"] = `Bearer ${token}`;
211
+ }
212
+ let res = await this.fetchFn(url, { ...init, headers });
213
+ if (res.status === 401 && this.autoRefresh && this.getRefreshToken()) {
214
+ try {
215
+ const refreshed = await this.refreshToken();
216
+ if (refreshed?.token) {
217
+ headers["Authorization"] = `Bearer ${refreshed.token}`;
218
+ res = await this.fetchFn(url, { ...init, headers });
219
+ }
220
+ } catch {
221
+ }
222
+ }
223
+ return res;
224
+ }
108
225
  /**
109
226
  * Synchronously parse and return current authenticated user from JWT token
110
227
  */
@@ -260,36 +377,48 @@ var AuthClient = class _AuthClient {
260
377
  async refreshToken(refreshToken) {
261
378
  const tokenToUse = refreshToken || this.getRefreshToken();
262
379
  if (!tokenToUse) {
380
+ this.notifySessionExpired();
263
381
  throw new Error("No refresh token available");
264
382
  }
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}`);
383
+ if (this.activeRefreshPromise) {
384
+ return this.activeRefreshPromise;
277
385
  }
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);
285
- }
286
- const user = this.getUser() || json.user;
287
- return {
288
- token: token || "",
289
- refreshToken: json.refresh_token,
290
- user,
291
- ...json
292
- };
386
+ this.activeRefreshPromise = (async () => {
387
+ try {
388
+ const url = `${this.gatewayUrl}/auth/${this.tenantId}/refresh`;
389
+ const res = await this.fetchFn(url, {
390
+ method: "POST",
391
+ headers: {
392
+ "Content-Type": "application/json",
393
+ "x-tenant-id": this.tenantId
394
+ },
395
+ body: JSON.stringify({ refresh_token: tokenToUse })
396
+ });
397
+ if (!res.ok) {
398
+ const err = await res.json().catch(() => ({ message: res.statusText }));
399
+ this.notifySessionExpired();
400
+ throw new Error(err.message || err.error || `Token refresh failed with HTTP ${res.status}`);
401
+ }
402
+ const json = await res.json();
403
+ const token = json.token || json.access_token;
404
+ if (token) {
405
+ this.setToken(token);
406
+ }
407
+ if (json.refresh_token) {
408
+ this.setRefreshToken(json.refresh_token);
409
+ }
410
+ const user = this.getUser() || json.user;
411
+ return {
412
+ token: token || "",
413
+ refreshToken: json.refresh_token,
414
+ user,
415
+ ...json
416
+ };
417
+ } finally {
418
+ this.activeRefreshPromise = null;
419
+ }
420
+ })();
421
+ return this.activeRefreshPromise;
293
422
  }
294
423
  /**
295
424
  * Request password reset email
@@ -418,13 +547,15 @@ var EntityQueryBuilder = class {
418
547
  typeName;
419
548
  getToken;
420
549
  fetchFn;
550
+ fetchWithAuth;
421
551
  queryParams = new URLSearchParams();
422
552
  constructor(options) {
423
553
  this.gatewayUrl = options.gatewayUrl.replace(/\/+$/, "");
424
554
  this.tenantId = options.tenantId;
425
555
  this.typeName = options.typeName;
426
- this.getToken = options.getToken;
556
+ this.getToken = options.getToken || (() => null);
427
557
  this.fetchFn = options.fetchFn || (typeof fetch !== "undefined" ? fetch : globalThis.fetch);
558
+ this.fetchWithAuth = options.fetchWithAuth;
428
559
  }
429
560
  getHeaders() {
430
561
  const headers = {
@@ -437,6 +568,13 @@ var EntityQueryBuilder = class {
437
568
  }
438
569
  return headers;
439
570
  }
571
+ async executeRequest(url, init = {}) {
572
+ if (this.fetchWithAuth) {
573
+ return this.fetchWithAuth(url, init);
574
+ }
575
+ const headers = { ...this.getHeaders(), ...init.headers || {} };
576
+ return this.fetchFn(url, { ...init, headers });
577
+ }
440
578
  // ----------------------------------------------------
441
579
  // Filter Operators
442
580
  // ----------------------------------------------------
@@ -545,9 +683,7 @@ var EntityQueryBuilder = class {
545
683
  async list() {
546
684
  const qs = this.queryParams.toString();
547
685
  const url = `${this.gatewayUrl}/generic/${this.tenantId}/${this.typeName}${qs ? `?${qs}` : ""}`;
548
- const res = await this.fetchFn(url, {
549
- headers: this.getHeaders()
550
- });
686
+ const res = await this.executeRequest(url);
551
687
  if (!res.ok) {
552
688
  const err = await res.json().catch(() => ({ message: res.statusText }));
553
689
  throw new Error(err.message || `Query failed with HTTP ${res.status}`);
@@ -570,9 +706,7 @@ var EntityQueryBuilder = class {
570
706
  }
571
707
  const qs = params.toString();
572
708
  const url = `${this.gatewayUrl}/generic/${this.tenantId}/${this.typeName}/${id}${qs ? `?${qs}` : ""}`;
573
- const res = await this.fetchFn(url, {
574
- headers: this.getHeaders()
575
- });
709
+ const res = await this.executeRequest(url);
576
710
  if (!res.ok) {
577
711
  const err = await res.json().catch(() => ({ message: res.statusText }));
578
712
  throw new Error(err.message || `Failed to fetch ${this.typeName}/${id}`);
@@ -584,9 +718,8 @@ var EntityQueryBuilder = class {
584
718
  */
585
719
  async create(data) {
586
720
  const url = `${this.gatewayUrl}/generic/${this.tenantId}/${this.typeName}`;
587
- const res = await this.fetchFn(url, {
721
+ const res = await this.executeRequest(url, {
588
722
  method: "POST",
589
- headers: this.getHeaders(),
590
723
  body: JSON.stringify(data)
591
724
  });
592
725
  if (!res.ok) {
@@ -600,9 +733,8 @@ var EntityQueryBuilder = class {
600
733
  */
601
734
  async update(id, partialData) {
602
735
  const url = `${this.gatewayUrl}/generic/${this.tenantId}/${this.typeName}/${id}`;
603
- const res = await this.fetchFn(url, {
736
+ const res = await this.executeRequest(url, {
604
737
  method: "PATCH",
605
- headers: this.getHeaders(),
606
738
  body: JSON.stringify(partialData)
607
739
  });
608
740
  if (!res.ok) {
@@ -616,9 +748,8 @@ var EntityQueryBuilder = class {
616
748
  */
617
749
  async delete(id) {
618
750
  const url = `${this.gatewayUrl}/generic/${this.tenantId}/${this.typeName}/${id}`;
619
- const res = await this.fetchFn(url, {
620
- method: "DELETE",
621
- headers: this.getHeaders()
751
+ const res = await this.executeRequest(url, {
752
+ method: "DELETE"
622
753
  });
623
754
  if (!res.ok) {
624
755
  const err = await res.json().catch(() => ({ message: res.statusText }));
@@ -631,9 +762,7 @@ var EntityQueryBuilder = class {
631
762
  */
632
763
  async revisions(id) {
633
764
  const url = `${this.gatewayUrl}/generic/${this.tenantId}/${this.typeName}/${id}/revisions`;
634
- const res = await this.fetchFn(url, {
635
- headers: this.getHeaders()
636
- });
765
+ const res = await this.executeRequest(url);
637
766
  if (!res.ok) {
638
767
  const err = await res.json().catch(() => ({ message: res.statusText }));
639
768
  throw new Error(err.message || `Failed to fetch revisions for ${this.typeName}/${id}`);
@@ -646,9 +775,7 @@ var EntityQueryBuilder = class {
646
775
  async getRelations(id, lang) {
647
776
  const qs = lang ? `?lang=${encodeURIComponent(lang)}` : "";
648
777
  const url = `${this.gatewayUrl}/generic/${this.tenantId}/${this.typeName}/${id}/relations${qs}`;
649
- const res = await this.fetchFn(url, {
650
- headers: this.getHeaders()
651
- });
778
+ const res = await this.executeRequest(url);
652
779
  if (!res.ok) {
653
780
  const err = await res.json().catch(() => ({ message: res.statusText }));
654
781
  throw new Error(err.message || `Failed to fetch relations for ${this.typeName}/${id}`);
@@ -686,9 +813,8 @@ var EntityQueryBuilder = class {
686
813
  };
687
814
  }
688
815
  const url = `${this.gatewayUrl}/generic/${this.tenantId}/${this.typeName}/${id}/relations`;
689
- const res = await this.fetchFn(url, {
816
+ const res = await this.executeRequest(url, {
690
817
  method: "POST",
691
- headers: this.getHeaders(),
692
818
  body: JSON.stringify(payload)
693
819
  });
694
820
  if (!res.ok) {
@@ -702,9 +828,8 @@ var EntityQueryBuilder = class {
702
828
  */
703
829
  async setLinks(id, payload) {
704
830
  const url = `${this.gatewayUrl}/generic/${this.tenantId}/${this.typeName}/${id}/relations`;
705
- const res = await this.fetchFn(url, {
831
+ const res = await this.executeRequest(url, {
706
832
  method: "PUT",
707
- headers: this.getHeaders(),
708
833
  body: JSON.stringify(payload)
709
834
  });
710
835
  if (!res.ok) {
@@ -718,9 +843,8 @@ var EntityQueryBuilder = class {
718
843
  */
719
844
  async unlink(id, relationName, targetId, targetTypeName) {
720
845
  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()
846
+ const res = await this.executeRequest(url, {
847
+ method: "DELETE"
724
848
  });
725
849
  if (!res.ok) {
726
850
  const err = await res.json().catch(() => ({ message: res.statusText }));
@@ -733,9 +857,8 @@ var EntityQueryBuilder = class {
733
857
  */
734
858
  async action(actionName, payload) {
735
859
  const url = `${this.gatewayUrl}/generic/${this.tenantId}/${this.typeName}/actions/${actionName}`;
736
- const res = await this.fetchFn(url, {
860
+ const res = await this.executeRequest(url, {
737
861
  method: "POST",
738
- headers: this.getHeaders(),
739
862
  body: payload ? JSON.stringify(payload) : void 0
740
863
  });
741
864
  if (!res.ok) {
@@ -749,9 +872,8 @@ var EntityQueryBuilder = class {
749
872
  */
750
873
  async instanceAction(id, actionName, payload) {
751
874
  const url = `${this.gatewayUrl}/generic/${this.tenantId}/${this.typeName}/${id}/actions/${actionName}`;
752
- const res = await this.fetchFn(url, {
875
+ const res = await this.executeRequest(url, {
753
876
  method: "POST",
754
- headers: this.getHeaders(),
755
877
  body: payload ? JSON.stringify(payload) : void 0
756
878
  });
757
879
  if (!res.ok) {
@@ -766,9 +888,8 @@ var EntityQueryBuilder = class {
766
888
  async exportCsv() {
767
889
  const qs = this.queryParams.toString();
768
890
  const url = `${this.gatewayUrl}/generic/${this.tenantId}/${this.typeName}/export/csv${qs ? `?${qs}` : ""}`;
769
- const res = await this.fetchFn(url, {
891
+ const res = await this.executeRequest(url, {
770
892
  headers: {
771
- ...this.getHeaders(),
772
893
  "Accept": "text/csv"
773
894
  }
774
895
  });
@@ -786,11 +907,13 @@ var SchemaClient = class {
786
907
  tenantId;
787
908
  getToken;
788
909
  fetchFn;
910
+ fetchWithAuth;
789
911
  constructor(options) {
790
912
  this.gatewayUrl = options.gatewayUrl.replace(/\/+$/, "");
791
913
  this.tenantId = options.tenantId;
792
- this.getToken = options.getToken;
914
+ this.getToken = options.getToken || (() => null);
793
915
  this.fetchFn = options.fetchFn || (typeof fetch !== "undefined" ? fetch : globalThis.fetch);
916
+ this.fetchWithAuth = options.fetchWithAuth;
794
917
  }
795
918
  getHeaders() {
796
919
  const headers = {
@@ -808,11 +931,11 @@ var SchemaClient = class {
808
931
  */
809
932
  async define(definition) {
810
933
  const url = `${this.gatewayUrl}/generic/${this.tenantId}/definition`;
811
- const res = await this.fetchFn(url, {
934
+ const init = {
812
935
  method: "POST",
813
- headers: this.getHeaders(),
814
936
  body: JSON.stringify(definition)
815
- });
937
+ };
938
+ const res = this.fetchWithAuth ? await this.fetchWithAuth(url, init) : await this.fetchFn(url, { ...init, headers: this.getHeaders() });
816
939
  if (!res.ok) {
817
940
  const err = await res.json().catch(() => ({ message: res.statusText }));
818
941
  throw new Error(err.message || `Failed to define schema for ${definition.typeName}`);
@@ -866,13 +989,17 @@ var Zone4CodeClient = class {
866
989
  storage: this.storage,
867
990
  storageKey: config.storageKey,
868
991
  initialToken: config.token,
869
- fetchFn: this.fetchFn
992
+ fetchFn: this.fetchFn,
993
+ autoRefresh: config.autoRefresh,
994
+ tokenExpiryBuffer: config.tokenExpiryBuffer,
995
+ onSessionExpired: config.onSessionExpired
870
996
  });
871
997
  this.schema = new SchemaClient({
872
998
  gatewayUrl: this.gatewayUrl,
873
999
  tenantId: this.tenantId,
874
1000
  getToken: () => this.auth.getToken(),
875
- fetchFn: this.fetchFn
1001
+ fetchFn: this.fetchFn,
1002
+ fetchWithAuth: (url, init) => this.auth.fetchWithAuth(url, init)
876
1003
  });
877
1004
  }
878
1005
  /**
@@ -884,7 +1011,8 @@ var Zone4CodeClient = class {
884
1011
  tenantId: this.tenantId,
885
1012
  typeName,
886
1013
  getToken: () => this.auth.getToken(),
887
- fetchFn: this.fetchFn
1014
+ fetchFn: this.fetchFn,
1015
+ fetchWithAuth: (url, init) => this.auth.fetchWithAuth(url, init)
888
1016
  });
889
1017
  }
890
1018
  /**
@@ -893,21 +1021,17 @@ var Zone4CodeClient = class {
893
1021
  entities(typeName) {
894
1022
  return this.from(typeName);
895
1023
  }
1024
+ /**
1025
+ * Register a listener called whenever the session expires completely
1026
+ */
1027
+ onSessionExpired(callback) {
1028
+ return this.auth.onSessionExpired(callback);
1029
+ }
896
1030
  /**
897
1031
  * Get current authenticated user profile and platform wallet
898
1032
  */
899
1033
  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
- });
1034
+ const res = await this.auth.fetchWithAuth(`${this.gatewayUrl}/generic/${this.tenantId}/me`);
911
1035
  if (!res.ok) {
912
1036
  const err = await res.json().catch(() => ({ message: res.statusText }));
913
1037
  throw new Error(err.message || `Failed to fetch profile & wallet (HTTP ${res.status})`);