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.js CHANGED
@@ -17,6 +17,22 @@ function decodeJwtPayload(token) {
17
17
  return null;
18
18
  }
19
19
  }
20
+ function normalizeHeaders(headers) {
21
+ const result = {};
22
+ if (!headers) return result;
23
+ if (typeof Headers !== "undefined" && headers instanceof Headers) {
24
+ headers.forEach((value, key) => {
25
+ result[key] = value;
26
+ });
27
+ } else if (Array.isArray(headers)) {
28
+ for (const [key, value] of headers) {
29
+ result[key] = value;
30
+ }
31
+ } else {
32
+ Object.assign(result, headers);
33
+ }
34
+ return result;
35
+ }
20
36
  var AuthClient = class _AuthClient {
21
37
  gatewayUrl;
22
38
  tenantId;
@@ -26,26 +42,39 @@ var AuthClient = class _AuthClient {
26
42
  token = null;
27
43
  refreshTokenValue = null;
28
44
  fetchFn;
45
+ autoRefresh;
46
+ tokenExpiryBuffer;
47
+ onSessionExpiredCallback;
48
+ sessionExpiredListeners = [];
49
+ activeRefreshPromise = null;
50
+ storageLoadedPromise;
29
51
  static TOKEN_KEY_PREFIX = "z4c_token_";
30
52
  constructor(options) {
53
+ const storage = options.storage;
54
+ const storageKey = options.storageKey || _AuthClient.TOKEN_KEY_PREFIX + options.tenantId;
55
+ const refreshStorageKey = storageKey + "_refresh";
31
56
  this.gatewayUrl = options.gatewayUrl.replace(/\/+$/, "");
32
57
  this.tenantId = options.tenantId;
33
- this.storage = options.storage;
34
- this.storageKey = options.storageKey || _AuthClient.TOKEN_KEY_PREFIX + this.tenantId;
35
- this.refreshStorageKey = this.storageKey + "_refresh";
58
+ this.storage = storage;
59
+ this.storageKey = storageKey;
60
+ this.refreshStorageKey = refreshStorageKey;
36
61
  this.fetchFn = options.fetchFn || (typeof fetch !== "undefined" ? fetch : globalThis.fetch);
37
- if (options.initialToken) {
38
- this.setToken(options.initialToken);
39
- } else {
40
- Promise.resolve(this.storage.getItem(this.storageKey)).then((stored) => {
41
- if (stored) this.token = stored;
42
- }).catch(() => {
43
- });
44
- Promise.resolve(this.storage.getItem(this.refreshStorageKey)).then((stored) => {
45
- if (stored) this.refreshTokenValue = stored;
46
- }).catch(() => {
47
- });
48
- }
62
+ this.autoRefresh = options.autoRefresh ?? true;
63
+ this.tokenExpiryBuffer = options.tokenExpiryBuffer ?? 30;
64
+ this.onSessionExpiredCallback = options.onSessionExpired;
65
+ this.storageLoadedPromise = (async () => {
66
+ if (options.initialToken) {
67
+ this.setToken(options.initialToken);
68
+ } else {
69
+ try {
70
+ const storedToken = await storage.getItem(storageKey);
71
+ if (storedToken) this.token = storedToken;
72
+ const storedRefresh = await storage.getItem(refreshStorageKey);
73
+ if (storedRefresh) this.refreshTokenValue = storedRefresh;
74
+ } catch {
75
+ }
76
+ }
77
+ })();
49
78
  }
50
79
  getToken() {
51
80
  return this.token;
@@ -72,6 +101,94 @@ var AuthClient = class _AuthClient {
72
101
  isAuthenticated() {
73
102
  return !!this.token;
74
103
  }
104
+ /**
105
+ * Check whether the current access token is expired or expiring within bufferSeconds
106
+ */
107
+ isTokenExpired(bufferSeconds = this.tokenExpiryBuffer) {
108
+ if (!this.token) return true;
109
+ const payload = decodeJwtPayload(this.token);
110
+ if (!payload || typeof payload.exp !== "number") return false;
111
+ const nowSeconds = Math.floor(Date.now() / 1e3);
112
+ return nowSeconds >= payload.exp - bufferSeconds;
113
+ }
114
+ /**
115
+ * Returns a valid access token, proactively refreshing it if expired and autoRefresh is enabled
116
+ */
117
+ async getValidToken() {
118
+ await this.storageLoadedPromise;
119
+ if (!this.token) return null;
120
+ if (!this.autoRefresh) return this.token;
121
+ if (this.isTokenExpired()) {
122
+ const refreshToken = this.getRefreshToken();
123
+ if (refreshToken) {
124
+ try {
125
+ const res = await this.refreshToken(refreshToken);
126
+ return res.token || this.token;
127
+ } catch {
128
+ return null;
129
+ }
130
+ } else {
131
+ this.notifySessionExpired();
132
+ return null;
133
+ }
134
+ }
135
+ return this.token;
136
+ }
137
+ /**
138
+ * Register a callback triggered when session has expired completely
139
+ */
140
+ onSessionExpired(listener) {
141
+ this.sessionExpiredListeners.push(listener);
142
+ return () => {
143
+ this.sessionExpiredListeners = this.sessionExpiredListeners.filter((l) => l !== listener);
144
+ };
145
+ }
146
+ notifySessionExpired() {
147
+ this.logout();
148
+ for (const listener of this.sessionExpiredListeners) {
149
+ try {
150
+ listener();
151
+ } catch (e) {
152
+ console.error("Error in onSessionExpired listener:", e);
153
+ }
154
+ }
155
+ if (this.onSessionExpiredCallback) {
156
+ try {
157
+ this.onSessionExpiredCallback();
158
+ } catch (e) {
159
+ console.error("Error in onSessionExpired callback:", e);
160
+ }
161
+ }
162
+ }
163
+ /**
164
+ * Perform an HTTP request with automatic Authorization injection, proactive refresh, and reactive 401 retry
165
+ */
166
+ async fetchWithAuth(url, init = {}) {
167
+ await this.storageLoadedPromise;
168
+ const token = await this.getValidToken();
169
+ const headers = normalizeHeaders(init.headers);
170
+ if (!headers["x-tenant-id"] && !headers["X-Tenant-Id"]) {
171
+ headers["x-tenant-id"] = this.tenantId;
172
+ }
173
+ if (init.body && !headers["Content-Type"] && !headers["content-type"]) {
174
+ headers["Content-Type"] = "application/json";
175
+ }
176
+ if (token && !headers["Authorization"] && !headers["authorization"]) {
177
+ headers["Authorization"] = `Bearer ${token}`;
178
+ }
179
+ let res = await this.fetchFn(url, { ...init, headers });
180
+ if (res.status === 401 && this.autoRefresh && this.getRefreshToken()) {
181
+ try {
182
+ const refreshed = await this.refreshToken();
183
+ if (refreshed?.token) {
184
+ headers["Authorization"] = `Bearer ${refreshed.token}`;
185
+ res = await this.fetchFn(url, { ...init, headers });
186
+ }
187
+ } catch {
188
+ }
189
+ }
190
+ return res;
191
+ }
75
192
  /**
76
193
  * Synchronously parse and return current authenticated user from JWT token
77
194
  */
@@ -227,36 +344,48 @@ var AuthClient = class _AuthClient {
227
344
  async refreshToken(refreshToken) {
228
345
  const tokenToUse = refreshToken || this.getRefreshToken();
229
346
  if (!tokenToUse) {
347
+ this.notifySessionExpired();
230
348
  throw new Error("No refresh token available");
231
349
  }
232
- const url = `${this.gatewayUrl}/auth/${this.tenantId}/refresh`;
233
- const res = await this.fetchFn(url, {
234
- method: "POST",
235
- headers: {
236
- "Content-Type": "application/json",
237
- "x-tenant-id": this.tenantId
238
- },
239
- body: JSON.stringify({ refresh_token: tokenToUse })
240
- });
241
- if (!res.ok) {
242
- const err = await res.json().catch(() => ({ message: res.statusText }));
243
- throw new Error(err.message || err.error || `Token refresh failed with HTTP ${res.status}`);
350
+ if (this.activeRefreshPromise) {
351
+ return this.activeRefreshPromise;
244
352
  }
245
- const json = await res.json();
246
- const token = json.token || json.access_token;
247
- if (token) {
248
- this.setToken(token);
249
- }
250
- if (json.refresh_token) {
251
- this.setRefreshToken(json.refresh_token);
252
- }
253
- const user = this.getUser() || json.user;
254
- return {
255
- token: token || "",
256
- refreshToken: json.refresh_token,
257
- user,
258
- ...json
259
- };
353
+ this.activeRefreshPromise = (async () => {
354
+ try {
355
+ const url = `${this.gatewayUrl}/auth/${this.tenantId}/refresh`;
356
+ const res = await this.fetchFn(url, {
357
+ method: "POST",
358
+ headers: {
359
+ "Content-Type": "application/json",
360
+ "x-tenant-id": this.tenantId
361
+ },
362
+ body: JSON.stringify({ refresh_token: tokenToUse })
363
+ });
364
+ if (!res.ok) {
365
+ const err = await res.json().catch(() => ({ message: res.statusText }));
366
+ this.notifySessionExpired();
367
+ throw new Error(err.message || err.error || `Token refresh failed with HTTP ${res.status}`);
368
+ }
369
+ const json = await res.json();
370
+ const token = json.token || json.access_token;
371
+ if (token) {
372
+ this.setToken(token);
373
+ }
374
+ if (json.refresh_token) {
375
+ this.setRefreshToken(json.refresh_token);
376
+ }
377
+ const user = this.getUser() || json.user;
378
+ return {
379
+ token: token || "",
380
+ refreshToken: json.refresh_token,
381
+ user,
382
+ ...json
383
+ };
384
+ } finally {
385
+ this.activeRefreshPromise = null;
386
+ }
387
+ })();
388
+ return this.activeRefreshPromise;
260
389
  }
261
390
  /**
262
391
  * Request password reset email
@@ -385,13 +514,15 @@ var EntityQueryBuilder = class {
385
514
  typeName;
386
515
  getToken;
387
516
  fetchFn;
517
+ fetchWithAuth;
388
518
  queryParams = new URLSearchParams();
389
519
  constructor(options) {
390
520
  this.gatewayUrl = options.gatewayUrl.replace(/\/+$/, "");
391
521
  this.tenantId = options.tenantId;
392
522
  this.typeName = options.typeName;
393
- this.getToken = options.getToken;
523
+ this.getToken = options.getToken || (() => null);
394
524
  this.fetchFn = options.fetchFn || (typeof fetch !== "undefined" ? fetch : globalThis.fetch);
525
+ this.fetchWithAuth = options.fetchWithAuth;
395
526
  }
396
527
  getHeaders() {
397
528
  const headers = {
@@ -404,6 +535,13 @@ var EntityQueryBuilder = class {
404
535
  }
405
536
  return headers;
406
537
  }
538
+ async executeRequest(url, init = {}) {
539
+ if (this.fetchWithAuth) {
540
+ return this.fetchWithAuth(url, init);
541
+ }
542
+ const headers = { ...this.getHeaders(), ...init.headers || {} };
543
+ return this.fetchFn(url, { ...init, headers });
544
+ }
407
545
  // ----------------------------------------------------
408
546
  // Filter Operators
409
547
  // ----------------------------------------------------
@@ -512,9 +650,7 @@ var EntityQueryBuilder = class {
512
650
  async list() {
513
651
  const qs = this.queryParams.toString();
514
652
  const url = `${this.gatewayUrl}/generic/${this.tenantId}/${this.typeName}${qs ? `?${qs}` : ""}`;
515
- const res = await this.fetchFn(url, {
516
- headers: this.getHeaders()
517
- });
653
+ const res = await this.executeRequest(url);
518
654
  if (!res.ok) {
519
655
  const err = await res.json().catch(() => ({ message: res.statusText }));
520
656
  throw new Error(err.message || `Query failed with HTTP ${res.status}`);
@@ -537,9 +673,7 @@ var EntityQueryBuilder = class {
537
673
  }
538
674
  const qs = params.toString();
539
675
  const url = `${this.gatewayUrl}/generic/${this.tenantId}/${this.typeName}/${id}${qs ? `?${qs}` : ""}`;
540
- const res = await this.fetchFn(url, {
541
- headers: this.getHeaders()
542
- });
676
+ const res = await this.executeRequest(url);
543
677
  if (!res.ok) {
544
678
  const err = await res.json().catch(() => ({ message: res.statusText }));
545
679
  throw new Error(err.message || `Failed to fetch ${this.typeName}/${id}`);
@@ -551,9 +685,8 @@ var EntityQueryBuilder = class {
551
685
  */
552
686
  async create(data) {
553
687
  const url = `${this.gatewayUrl}/generic/${this.tenantId}/${this.typeName}`;
554
- const res = await this.fetchFn(url, {
688
+ const res = await this.executeRequest(url, {
555
689
  method: "POST",
556
- headers: this.getHeaders(),
557
690
  body: JSON.stringify(data)
558
691
  });
559
692
  if (!res.ok) {
@@ -567,9 +700,8 @@ var EntityQueryBuilder = class {
567
700
  */
568
701
  async update(id, partialData) {
569
702
  const url = `${this.gatewayUrl}/generic/${this.tenantId}/${this.typeName}/${id}`;
570
- const res = await this.fetchFn(url, {
703
+ const res = await this.executeRequest(url, {
571
704
  method: "PATCH",
572
- headers: this.getHeaders(),
573
705
  body: JSON.stringify(partialData)
574
706
  });
575
707
  if (!res.ok) {
@@ -583,9 +715,8 @@ var EntityQueryBuilder = class {
583
715
  */
584
716
  async delete(id) {
585
717
  const url = `${this.gatewayUrl}/generic/${this.tenantId}/${this.typeName}/${id}`;
586
- const res = await this.fetchFn(url, {
587
- method: "DELETE",
588
- headers: this.getHeaders()
718
+ const res = await this.executeRequest(url, {
719
+ method: "DELETE"
589
720
  });
590
721
  if (!res.ok) {
591
722
  const err = await res.json().catch(() => ({ message: res.statusText }));
@@ -598,9 +729,7 @@ var EntityQueryBuilder = class {
598
729
  */
599
730
  async revisions(id) {
600
731
  const url = `${this.gatewayUrl}/generic/${this.tenantId}/${this.typeName}/${id}/revisions`;
601
- const res = await this.fetchFn(url, {
602
- headers: this.getHeaders()
603
- });
732
+ const res = await this.executeRequest(url);
604
733
  if (!res.ok) {
605
734
  const err = await res.json().catch(() => ({ message: res.statusText }));
606
735
  throw new Error(err.message || `Failed to fetch revisions for ${this.typeName}/${id}`);
@@ -613,9 +742,7 @@ var EntityQueryBuilder = class {
613
742
  async getRelations(id, lang) {
614
743
  const qs = lang ? `?lang=${encodeURIComponent(lang)}` : "";
615
744
  const url = `${this.gatewayUrl}/generic/${this.tenantId}/${this.typeName}/${id}/relations${qs}`;
616
- const res = await this.fetchFn(url, {
617
- headers: this.getHeaders()
618
- });
745
+ const res = await this.executeRequest(url);
619
746
  if (!res.ok) {
620
747
  const err = await res.json().catch(() => ({ message: res.statusText }));
621
748
  throw new Error(err.message || `Failed to fetch relations for ${this.typeName}/${id}`);
@@ -653,9 +780,8 @@ var EntityQueryBuilder = class {
653
780
  };
654
781
  }
655
782
  const url = `${this.gatewayUrl}/generic/${this.tenantId}/${this.typeName}/${id}/relations`;
656
- const res = await this.fetchFn(url, {
783
+ const res = await this.executeRequest(url, {
657
784
  method: "POST",
658
- headers: this.getHeaders(),
659
785
  body: JSON.stringify(payload)
660
786
  });
661
787
  if (!res.ok) {
@@ -669,9 +795,8 @@ var EntityQueryBuilder = class {
669
795
  */
670
796
  async setLinks(id, payload) {
671
797
  const url = `${this.gatewayUrl}/generic/${this.tenantId}/${this.typeName}/${id}/relations`;
672
- const res = await this.fetchFn(url, {
798
+ const res = await this.executeRequest(url, {
673
799
  method: "PUT",
674
- headers: this.getHeaders(),
675
800
  body: JSON.stringify(payload)
676
801
  });
677
802
  if (!res.ok) {
@@ -685,9 +810,8 @@ var EntityQueryBuilder = class {
685
810
  */
686
811
  async unlink(id, relationName, targetId, targetTypeName) {
687
812
  const url = `${this.gatewayUrl}/generic/${this.tenantId}/${this.typeName}/${id}/relations/${encodeURIComponent(relationName)}/${encodeURIComponent(targetId)}?targetTypeName=${encodeURIComponent(targetTypeName)}`;
688
- const res = await this.fetchFn(url, {
689
- method: "DELETE",
690
- headers: this.getHeaders()
813
+ const res = await this.executeRequest(url, {
814
+ method: "DELETE"
691
815
  });
692
816
  if (!res.ok) {
693
817
  const err = await res.json().catch(() => ({ message: res.statusText }));
@@ -700,9 +824,8 @@ var EntityQueryBuilder = class {
700
824
  */
701
825
  async action(actionName, payload) {
702
826
  const url = `${this.gatewayUrl}/generic/${this.tenantId}/${this.typeName}/actions/${actionName}`;
703
- const res = await this.fetchFn(url, {
827
+ const res = await this.executeRequest(url, {
704
828
  method: "POST",
705
- headers: this.getHeaders(),
706
829
  body: payload ? JSON.stringify(payload) : void 0
707
830
  });
708
831
  if (!res.ok) {
@@ -716,9 +839,8 @@ var EntityQueryBuilder = class {
716
839
  */
717
840
  async instanceAction(id, actionName, payload) {
718
841
  const url = `${this.gatewayUrl}/generic/${this.tenantId}/${this.typeName}/${id}/actions/${actionName}`;
719
- const res = await this.fetchFn(url, {
842
+ const res = await this.executeRequest(url, {
720
843
  method: "POST",
721
- headers: this.getHeaders(),
722
844
  body: payload ? JSON.stringify(payload) : void 0
723
845
  });
724
846
  if (!res.ok) {
@@ -733,9 +855,8 @@ var EntityQueryBuilder = class {
733
855
  async exportCsv() {
734
856
  const qs = this.queryParams.toString();
735
857
  const url = `${this.gatewayUrl}/generic/${this.tenantId}/${this.typeName}/export/csv${qs ? `?${qs}` : ""}`;
736
- const res = await this.fetchFn(url, {
858
+ const res = await this.executeRequest(url, {
737
859
  headers: {
738
- ...this.getHeaders(),
739
860
  "Accept": "text/csv"
740
861
  }
741
862
  });
@@ -753,11 +874,13 @@ var SchemaClient = class {
753
874
  tenantId;
754
875
  getToken;
755
876
  fetchFn;
877
+ fetchWithAuth;
756
878
  constructor(options) {
757
879
  this.gatewayUrl = options.gatewayUrl.replace(/\/+$/, "");
758
880
  this.tenantId = options.tenantId;
759
- this.getToken = options.getToken;
881
+ this.getToken = options.getToken || (() => null);
760
882
  this.fetchFn = options.fetchFn || (typeof fetch !== "undefined" ? fetch : globalThis.fetch);
883
+ this.fetchWithAuth = options.fetchWithAuth;
761
884
  }
762
885
  getHeaders() {
763
886
  const headers = {
@@ -775,11 +898,11 @@ var SchemaClient = class {
775
898
  */
776
899
  async define(definition) {
777
900
  const url = `${this.gatewayUrl}/generic/${this.tenantId}/definition`;
778
- const res = await this.fetchFn(url, {
901
+ const init = {
779
902
  method: "POST",
780
- headers: this.getHeaders(),
781
903
  body: JSON.stringify(definition)
782
- });
904
+ };
905
+ const res = this.fetchWithAuth ? await this.fetchWithAuth(url, init) : await this.fetchFn(url, { ...init, headers: this.getHeaders() });
783
906
  if (!res.ok) {
784
907
  const err = await res.json().catch(() => ({ message: res.statusText }));
785
908
  throw new Error(err.message || `Failed to define schema for ${definition.typeName}`);
@@ -833,13 +956,17 @@ var Zone4CodeClient = class {
833
956
  storage: this.storage,
834
957
  storageKey: config.storageKey,
835
958
  initialToken: config.token,
836
- fetchFn: this.fetchFn
959
+ fetchFn: this.fetchFn,
960
+ autoRefresh: config.autoRefresh,
961
+ tokenExpiryBuffer: config.tokenExpiryBuffer,
962
+ onSessionExpired: config.onSessionExpired
837
963
  });
838
964
  this.schema = new SchemaClient({
839
965
  gatewayUrl: this.gatewayUrl,
840
966
  tenantId: this.tenantId,
841
967
  getToken: () => this.auth.getToken(),
842
- fetchFn: this.fetchFn
968
+ fetchFn: this.fetchFn,
969
+ fetchWithAuth: (url, init) => this.auth.fetchWithAuth(url, init)
843
970
  });
844
971
  }
845
972
  /**
@@ -851,7 +978,8 @@ var Zone4CodeClient = class {
851
978
  tenantId: this.tenantId,
852
979
  typeName,
853
980
  getToken: () => this.auth.getToken(),
854
- fetchFn: this.fetchFn
981
+ fetchFn: this.fetchFn,
982
+ fetchWithAuth: (url, init) => this.auth.fetchWithAuth(url, init)
855
983
  });
856
984
  }
857
985
  /**
@@ -860,21 +988,17 @@ var Zone4CodeClient = class {
860
988
  entities(typeName) {
861
989
  return this.from(typeName);
862
990
  }
991
+ /**
992
+ * Register a listener called whenever the session expires completely
993
+ */
994
+ onSessionExpired(callback) {
995
+ return this.auth.onSessionExpired(callback);
996
+ }
863
997
  /**
864
998
  * Get current authenticated user profile and platform wallet
865
999
  */
866
1000
  async getMe() {
867
- const token = this.auth.getToken();
868
- if (!token) {
869
- throw new Error("Not authenticated: please call login() or setToken() first");
870
- }
871
- const res = await this.fetchFn(`${this.gatewayUrl}/generic/${this.tenantId}/me`, {
872
- headers: {
873
- "Content-Type": "application/json",
874
- "Authorization": `Bearer ${token}`,
875
- "x-tenant-id": this.tenantId
876
- }
877
- });
1001
+ const res = await this.auth.fetchWithAuth(`${this.gatewayUrl}/generic/${this.tenantId}/me`);
878
1002
  if (!res.ok) {
879
1003
  const err = await res.json().catch(() => ({ message: res.statusText }));
880
1004
  throw new Error(err.message || `Failed to fetch profile & wallet (HTTP ${res.status})`);