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.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,91 @@ 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 (token && !headers["Authorization"] && !headers["authorization"]) {
174
+ headers["Authorization"] = `Bearer ${token}`;
175
+ }
176
+ let res = await this.fetchFn(url, { ...init, headers });
177
+ if (res.status === 401 && this.autoRefresh && this.getRefreshToken()) {
178
+ try {
179
+ const refreshed = await this.refreshToken();
180
+ if (refreshed?.token) {
181
+ headers["Authorization"] = `Bearer ${refreshed.token}`;
182
+ res = await this.fetchFn(url, { ...init, headers });
183
+ }
184
+ } catch {
185
+ }
186
+ }
187
+ return res;
188
+ }
75
189
  /**
76
190
  * Synchronously parse and return current authenticated user from JWT token
77
191
  */
@@ -227,36 +341,48 @@ var AuthClient = class _AuthClient {
227
341
  async refreshToken(refreshToken) {
228
342
  const tokenToUse = refreshToken || this.getRefreshToken();
229
343
  if (!tokenToUse) {
344
+ this.notifySessionExpired();
230
345
  throw new Error("No refresh token available");
231
346
  }
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}`);
244
- }
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);
347
+ if (this.activeRefreshPromise) {
348
+ return this.activeRefreshPromise;
252
349
  }
253
- const user = this.getUser() || json.user;
254
- return {
255
- token: token || "",
256
- refreshToken: json.refresh_token,
257
- user,
258
- ...json
259
- };
350
+ this.activeRefreshPromise = (async () => {
351
+ try {
352
+ const url = `${this.gatewayUrl}/auth/${this.tenantId}/refresh`;
353
+ const res = await this.fetchFn(url, {
354
+ method: "POST",
355
+ headers: {
356
+ "Content-Type": "application/json",
357
+ "x-tenant-id": this.tenantId
358
+ },
359
+ body: JSON.stringify({ refresh_token: tokenToUse })
360
+ });
361
+ if (!res.ok) {
362
+ const err = await res.json().catch(() => ({ message: res.statusText }));
363
+ this.notifySessionExpired();
364
+ throw new Error(err.message || err.error || `Token refresh failed with HTTP ${res.status}`);
365
+ }
366
+ const json = await res.json();
367
+ const token = json.token || json.access_token;
368
+ if (token) {
369
+ this.setToken(token);
370
+ }
371
+ if (json.refresh_token) {
372
+ this.setRefreshToken(json.refresh_token);
373
+ }
374
+ const user = this.getUser() || json.user;
375
+ return {
376
+ token: token || "",
377
+ refreshToken: json.refresh_token,
378
+ user,
379
+ ...json
380
+ };
381
+ } finally {
382
+ this.activeRefreshPromise = null;
383
+ }
384
+ })();
385
+ return this.activeRefreshPromise;
260
386
  }
261
387
  /**
262
388
  * Request password reset email
@@ -385,13 +511,15 @@ var EntityQueryBuilder = class {
385
511
  typeName;
386
512
  getToken;
387
513
  fetchFn;
514
+ fetchWithAuth;
388
515
  queryParams = new URLSearchParams();
389
516
  constructor(options) {
390
517
  this.gatewayUrl = options.gatewayUrl.replace(/\/+$/, "");
391
518
  this.tenantId = options.tenantId;
392
519
  this.typeName = options.typeName;
393
- this.getToken = options.getToken;
520
+ this.getToken = options.getToken || (() => null);
394
521
  this.fetchFn = options.fetchFn || (typeof fetch !== "undefined" ? fetch : globalThis.fetch);
522
+ this.fetchWithAuth = options.fetchWithAuth;
395
523
  }
396
524
  getHeaders() {
397
525
  const headers = {
@@ -404,6 +532,13 @@ var EntityQueryBuilder = class {
404
532
  }
405
533
  return headers;
406
534
  }
535
+ async executeRequest(url, init = {}) {
536
+ if (this.fetchWithAuth) {
537
+ return this.fetchWithAuth(url, init);
538
+ }
539
+ const headers = { ...this.getHeaders(), ...init.headers || {} };
540
+ return this.fetchFn(url, { ...init, headers });
541
+ }
407
542
  // ----------------------------------------------------
408
543
  // Filter Operators
409
544
  // ----------------------------------------------------
@@ -512,9 +647,7 @@ var EntityQueryBuilder = class {
512
647
  async list() {
513
648
  const qs = this.queryParams.toString();
514
649
  const url = `${this.gatewayUrl}/generic/${this.tenantId}/${this.typeName}${qs ? `?${qs}` : ""}`;
515
- const res = await this.fetchFn(url, {
516
- headers: this.getHeaders()
517
- });
650
+ const res = await this.executeRequest(url);
518
651
  if (!res.ok) {
519
652
  const err = await res.json().catch(() => ({ message: res.statusText }));
520
653
  throw new Error(err.message || `Query failed with HTTP ${res.status}`);
@@ -537,9 +670,7 @@ var EntityQueryBuilder = class {
537
670
  }
538
671
  const qs = params.toString();
539
672
  const url = `${this.gatewayUrl}/generic/${this.tenantId}/${this.typeName}/${id}${qs ? `?${qs}` : ""}`;
540
- const res = await this.fetchFn(url, {
541
- headers: this.getHeaders()
542
- });
673
+ const res = await this.executeRequest(url);
543
674
  if (!res.ok) {
544
675
  const err = await res.json().catch(() => ({ message: res.statusText }));
545
676
  throw new Error(err.message || `Failed to fetch ${this.typeName}/${id}`);
@@ -551,9 +682,8 @@ var EntityQueryBuilder = class {
551
682
  */
552
683
  async create(data) {
553
684
  const url = `${this.gatewayUrl}/generic/${this.tenantId}/${this.typeName}`;
554
- const res = await this.fetchFn(url, {
685
+ const res = await this.executeRequest(url, {
555
686
  method: "POST",
556
- headers: this.getHeaders(),
557
687
  body: JSON.stringify(data)
558
688
  });
559
689
  if (!res.ok) {
@@ -567,9 +697,8 @@ var EntityQueryBuilder = class {
567
697
  */
568
698
  async update(id, partialData) {
569
699
  const url = `${this.gatewayUrl}/generic/${this.tenantId}/${this.typeName}/${id}`;
570
- const res = await this.fetchFn(url, {
700
+ const res = await this.executeRequest(url, {
571
701
  method: "PATCH",
572
- headers: this.getHeaders(),
573
702
  body: JSON.stringify(partialData)
574
703
  });
575
704
  if (!res.ok) {
@@ -583,9 +712,8 @@ var EntityQueryBuilder = class {
583
712
  */
584
713
  async delete(id) {
585
714
  const url = `${this.gatewayUrl}/generic/${this.tenantId}/${this.typeName}/${id}`;
586
- const res = await this.fetchFn(url, {
587
- method: "DELETE",
588
- headers: this.getHeaders()
715
+ const res = await this.executeRequest(url, {
716
+ method: "DELETE"
589
717
  });
590
718
  if (!res.ok) {
591
719
  const err = await res.json().catch(() => ({ message: res.statusText }));
@@ -598,9 +726,7 @@ var EntityQueryBuilder = class {
598
726
  */
599
727
  async revisions(id) {
600
728
  const url = `${this.gatewayUrl}/generic/${this.tenantId}/${this.typeName}/${id}/revisions`;
601
- const res = await this.fetchFn(url, {
602
- headers: this.getHeaders()
603
- });
729
+ const res = await this.executeRequest(url);
604
730
  if (!res.ok) {
605
731
  const err = await res.json().catch(() => ({ message: res.statusText }));
606
732
  throw new Error(err.message || `Failed to fetch revisions for ${this.typeName}/${id}`);
@@ -613,9 +739,7 @@ var EntityQueryBuilder = class {
613
739
  async getRelations(id, lang) {
614
740
  const qs = lang ? `?lang=${encodeURIComponent(lang)}` : "";
615
741
  const url = `${this.gatewayUrl}/generic/${this.tenantId}/${this.typeName}/${id}/relations${qs}`;
616
- const res = await this.fetchFn(url, {
617
- headers: this.getHeaders()
618
- });
742
+ const res = await this.executeRequest(url);
619
743
  if (!res.ok) {
620
744
  const err = await res.json().catch(() => ({ message: res.statusText }));
621
745
  throw new Error(err.message || `Failed to fetch relations for ${this.typeName}/${id}`);
@@ -653,9 +777,8 @@ var EntityQueryBuilder = class {
653
777
  };
654
778
  }
655
779
  const url = `${this.gatewayUrl}/generic/${this.tenantId}/${this.typeName}/${id}/relations`;
656
- const res = await this.fetchFn(url, {
780
+ const res = await this.executeRequest(url, {
657
781
  method: "POST",
658
- headers: this.getHeaders(),
659
782
  body: JSON.stringify(payload)
660
783
  });
661
784
  if (!res.ok) {
@@ -669,9 +792,8 @@ var EntityQueryBuilder = class {
669
792
  */
670
793
  async setLinks(id, payload) {
671
794
  const url = `${this.gatewayUrl}/generic/${this.tenantId}/${this.typeName}/${id}/relations`;
672
- const res = await this.fetchFn(url, {
795
+ const res = await this.executeRequest(url, {
673
796
  method: "PUT",
674
- headers: this.getHeaders(),
675
797
  body: JSON.stringify(payload)
676
798
  });
677
799
  if (!res.ok) {
@@ -685,9 +807,8 @@ var EntityQueryBuilder = class {
685
807
  */
686
808
  async unlink(id, relationName, targetId, targetTypeName) {
687
809
  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()
810
+ const res = await this.executeRequest(url, {
811
+ method: "DELETE"
691
812
  });
692
813
  if (!res.ok) {
693
814
  const err = await res.json().catch(() => ({ message: res.statusText }));
@@ -700,9 +821,8 @@ var EntityQueryBuilder = class {
700
821
  */
701
822
  async action(actionName, payload) {
702
823
  const url = `${this.gatewayUrl}/generic/${this.tenantId}/${this.typeName}/actions/${actionName}`;
703
- const res = await this.fetchFn(url, {
824
+ const res = await this.executeRequest(url, {
704
825
  method: "POST",
705
- headers: this.getHeaders(),
706
826
  body: payload ? JSON.stringify(payload) : void 0
707
827
  });
708
828
  if (!res.ok) {
@@ -716,9 +836,8 @@ var EntityQueryBuilder = class {
716
836
  */
717
837
  async instanceAction(id, actionName, payload) {
718
838
  const url = `${this.gatewayUrl}/generic/${this.tenantId}/${this.typeName}/${id}/actions/${actionName}`;
719
- const res = await this.fetchFn(url, {
839
+ const res = await this.executeRequest(url, {
720
840
  method: "POST",
721
- headers: this.getHeaders(),
722
841
  body: payload ? JSON.stringify(payload) : void 0
723
842
  });
724
843
  if (!res.ok) {
@@ -733,9 +852,8 @@ var EntityQueryBuilder = class {
733
852
  async exportCsv() {
734
853
  const qs = this.queryParams.toString();
735
854
  const url = `${this.gatewayUrl}/generic/${this.tenantId}/${this.typeName}/export/csv${qs ? `?${qs}` : ""}`;
736
- const res = await this.fetchFn(url, {
855
+ const res = await this.executeRequest(url, {
737
856
  headers: {
738
- ...this.getHeaders(),
739
857
  "Accept": "text/csv"
740
858
  }
741
859
  });
@@ -753,11 +871,13 @@ var SchemaClient = class {
753
871
  tenantId;
754
872
  getToken;
755
873
  fetchFn;
874
+ fetchWithAuth;
756
875
  constructor(options) {
757
876
  this.gatewayUrl = options.gatewayUrl.replace(/\/+$/, "");
758
877
  this.tenantId = options.tenantId;
759
- this.getToken = options.getToken;
878
+ this.getToken = options.getToken || (() => null);
760
879
  this.fetchFn = options.fetchFn || (typeof fetch !== "undefined" ? fetch : globalThis.fetch);
880
+ this.fetchWithAuth = options.fetchWithAuth;
761
881
  }
762
882
  getHeaders() {
763
883
  const headers = {
@@ -775,11 +895,11 @@ var SchemaClient = class {
775
895
  */
776
896
  async define(definition) {
777
897
  const url = `${this.gatewayUrl}/generic/${this.tenantId}/definition`;
778
- const res = await this.fetchFn(url, {
898
+ const init = {
779
899
  method: "POST",
780
- headers: this.getHeaders(),
781
900
  body: JSON.stringify(definition)
782
- });
901
+ };
902
+ const res = this.fetchWithAuth ? await this.fetchWithAuth(url, init) : await this.fetchFn(url, { ...init, headers: this.getHeaders() });
783
903
  if (!res.ok) {
784
904
  const err = await res.json().catch(() => ({ message: res.statusText }));
785
905
  throw new Error(err.message || `Failed to define schema for ${definition.typeName}`);
@@ -833,13 +953,17 @@ var Zone4CodeClient = class {
833
953
  storage: this.storage,
834
954
  storageKey: config.storageKey,
835
955
  initialToken: config.token,
836
- fetchFn: this.fetchFn
956
+ fetchFn: this.fetchFn,
957
+ autoRefresh: config.autoRefresh,
958
+ tokenExpiryBuffer: config.tokenExpiryBuffer,
959
+ onSessionExpired: config.onSessionExpired
837
960
  });
838
961
  this.schema = new SchemaClient({
839
962
  gatewayUrl: this.gatewayUrl,
840
963
  tenantId: this.tenantId,
841
964
  getToken: () => this.auth.getToken(),
842
- fetchFn: this.fetchFn
965
+ fetchFn: this.fetchFn,
966
+ fetchWithAuth: (url, init) => this.auth.fetchWithAuth(url, init)
843
967
  });
844
968
  }
845
969
  /**
@@ -851,7 +975,8 @@ var Zone4CodeClient = class {
851
975
  tenantId: this.tenantId,
852
976
  typeName,
853
977
  getToken: () => this.auth.getToken(),
854
- fetchFn: this.fetchFn
978
+ fetchFn: this.fetchFn,
979
+ fetchWithAuth: (url, init) => this.auth.fetchWithAuth(url, init)
855
980
  });
856
981
  }
857
982
  /**
@@ -860,21 +985,17 @@ var Zone4CodeClient = class {
860
985
  entities(typeName) {
861
986
  return this.from(typeName);
862
987
  }
988
+ /**
989
+ * Register a listener called whenever the session expires completely
990
+ */
991
+ onSessionExpired(callback) {
992
+ return this.auth.onSessionExpired(callback);
993
+ }
863
994
  /**
864
995
  * Get current authenticated user profile and platform wallet
865
996
  */
866
997
  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
- });
998
+ const res = await this.auth.fetchWithAuth(`${this.gatewayUrl}/generic/${this.tenantId}/me`);
878
999
  if (!res.ok) {
879
1000
  const err = await res.json().catch(() => ({ message: res.statusText }));
880
1001
  throw new Error(err.message || `Failed to fetch profile & wallet (HTTP ${res.status})`);