zone4code-sdk 1.0.4 → 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}`);
@@ -657,12 +781,37 @@ var EntityQueryBuilder = class {
657
781
  }
658
782
  /**
659
783
  * Link this record to another entity record
784
+ * Supports:
785
+ * .link(id, 'in_category', categoryId)
786
+ * .link(id, 'in_category', 'category', categoryId)
787
+ * .link(id, { relationName: 'in_category', targetId: categoryId, targetTypeName: 'category' })
660
788
  */
661
- async link(id, payload) {
789
+ async link(id, relationOrPayload, targetTypeNameOrId, maybeTargetId) {
790
+ let payload;
791
+ if (typeof relationOrPayload === "string") {
792
+ if (maybeTargetId) {
793
+ payload = {
794
+ relationName: relationOrPayload,
795
+ targetTypeName: targetTypeNameOrId,
796
+ targetId: maybeTargetId
797
+ };
798
+ } else {
799
+ payload = {
800
+ relationName: relationOrPayload,
801
+ targetId: targetTypeNameOrId
802
+ };
803
+ }
804
+ } else {
805
+ const p = relationOrPayload;
806
+ payload = {
807
+ relationName: p.relationName || p.relation_name || "",
808
+ targetId: p.targetId || p.target_id || p.child_id || p.childId || "",
809
+ targetTypeName: p.targetTypeName || p.target_type_name
810
+ };
811
+ }
662
812
  const url = `${this.gatewayUrl}/generic/${this.tenantId}/${this.typeName}/${id}/relations`;
663
- const res = await this.fetchFn(url, {
813
+ const res = await this.executeRequest(url, {
664
814
  method: "POST",
665
- headers: this.getHeaders(),
666
815
  body: JSON.stringify(payload)
667
816
  });
668
817
  if (!res.ok) {
@@ -676,9 +825,8 @@ var EntityQueryBuilder = class {
676
825
  */
677
826
  async setLinks(id, payload) {
678
827
  const url = `${this.gatewayUrl}/generic/${this.tenantId}/${this.typeName}/${id}/relations`;
679
- const res = await this.fetchFn(url, {
828
+ const res = await this.executeRequest(url, {
680
829
  method: "PUT",
681
- headers: this.getHeaders(),
682
830
  body: JSON.stringify(payload)
683
831
  });
684
832
  if (!res.ok) {
@@ -692,9 +840,8 @@ var EntityQueryBuilder = class {
692
840
  */
693
841
  async unlink(id, relationName, targetId, targetTypeName) {
694
842
  const url = `${this.gatewayUrl}/generic/${this.tenantId}/${this.typeName}/${id}/relations/${encodeURIComponent(relationName)}/${encodeURIComponent(targetId)}?targetTypeName=${encodeURIComponent(targetTypeName)}`;
695
- const res = await this.fetchFn(url, {
696
- method: "DELETE",
697
- headers: this.getHeaders()
843
+ const res = await this.executeRequest(url, {
844
+ method: "DELETE"
698
845
  });
699
846
  if (!res.ok) {
700
847
  const err = await res.json().catch(() => ({ message: res.statusText }));
@@ -707,9 +854,8 @@ var EntityQueryBuilder = class {
707
854
  */
708
855
  async action(actionName, payload) {
709
856
  const url = `${this.gatewayUrl}/generic/${this.tenantId}/${this.typeName}/actions/${actionName}`;
710
- const res = await this.fetchFn(url, {
857
+ const res = await this.executeRequest(url, {
711
858
  method: "POST",
712
- headers: this.getHeaders(),
713
859
  body: payload ? JSON.stringify(payload) : void 0
714
860
  });
715
861
  if (!res.ok) {
@@ -723,9 +869,8 @@ var EntityQueryBuilder = class {
723
869
  */
724
870
  async instanceAction(id, actionName, payload) {
725
871
  const url = `${this.gatewayUrl}/generic/${this.tenantId}/${this.typeName}/${id}/actions/${actionName}`;
726
- const res = await this.fetchFn(url, {
872
+ const res = await this.executeRequest(url, {
727
873
  method: "POST",
728
- headers: this.getHeaders(),
729
874
  body: payload ? JSON.stringify(payload) : void 0
730
875
  });
731
876
  if (!res.ok) {
@@ -740,9 +885,8 @@ var EntityQueryBuilder = class {
740
885
  async exportCsv() {
741
886
  const qs = this.queryParams.toString();
742
887
  const url = `${this.gatewayUrl}/generic/${this.tenantId}/${this.typeName}/export/csv${qs ? `?${qs}` : ""}`;
743
- const res = await this.fetchFn(url, {
888
+ const res = await this.executeRequest(url, {
744
889
  headers: {
745
- ...this.getHeaders(),
746
890
  "Accept": "text/csv"
747
891
  }
748
892
  });
@@ -760,11 +904,13 @@ var SchemaClient = class {
760
904
  tenantId;
761
905
  getToken;
762
906
  fetchFn;
907
+ fetchWithAuth;
763
908
  constructor(options) {
764
909
  this.gatewayUrl = options.gatewayUrl.replace(/\/+$/, "");
765
910
  this.tenantId = options.tenantId;
766
- this.getToken = options.getToken;
911
+ this.getToken = options.getToken || (() => null);
767
912
  this.fetchFn = options.fetchFn || (typeof fetch !== "undefined" ? fetch : globalThis.fetch);
913
+ this.fetchWithAuth = options.fetchWithAuth;
768
914
  }
769
915
  getHeaders() {
770
916
  const headers = {
@@ -782,11 +928,11 @@ var SchemaClient = class {
782
928
  */
783
929
  async define(definition) {
784
930
  const url = `${this.gatewayUrl}/generic/${this.tenantId}/definition`;
785
- const res = await this.fetchFn(url, {
931
+ const init = {
786
932
  method: "POST",
787
- headers: this.getHeaders(),
788
933
  body: JSON.stringify(definition)
789
- });
934
+ };
935
+ const res = this.fetchWithAuth ? await this.fetchWithAuth(url, init) : await this.fetchFn(url, { ...init, headers: this.getHeaders() });
790
936
  if (!res.ok) {
791
937
  const err = await res.json().catch(() => ({ message: res.statusText }));
792
938
  throw new Error(err.message || `Failed to define schema for ${definition.typeName}`);
@@ -840,13 +986,17 @@ var Zone4CodeClient = class {
840
986
  storage: this.storage,
841
987
  storageKey: config.storageKey,
842
988
  initialToken: config.token,
843
- fetchFn: this.fetchFn
989
+ fetchFn: this.fetchFn,
990
+ autoRefresh: config.autoRefresh,
991
+ tokenExpiryBuffer: config.tokenExpiryBuffer,
992
+ onSessionExpired: config.onSessionExpired
844
993
  });
845
994
  this.schema = new SchemaClient({
846
995
  gatewayUrl: this.gatewayUrl,
847
996
  tenantId: this.tenantId,
848
997
  getToken: () => this.auth.getToken(),
849
- fetchFn: this.fetchFn
998
+ fetchFn: this.fetchFn,
999
+ fetchWithAuth: (url, init) => this.auth.fetchWithAuth(url, init)
850
1000
  });
851
1001
  }
852
1002
  /**
@@ -858,7 +1008,8 @@ var Zone4CodeClient = class {
858
1008
  tenantId: this.tenantId,
859
1009
  typeName,
860
1010
  getToken: () => this.auth.getToken(),
861
- fetchFn: this.fetchFn
1011
+ fetchFn: this.fetchFn,
1012
+ fetchWithAuth: (url, init) => this.auth.fetchWithAuth(url, init)
862
1013
  });
863
1014
  }
864
1015
  /**
@@ -867,21 +1018,17 @@ var Zone4CodeClient = class {
867
1018
  entities(typeName) {
868
1019
  return this.from(typeName);
869
1020
  }
1021
+ /**
1022
+ * Register a listener called whenever the session expires completely
1023
+ */
1024
+ onSessionExpired(callback) {
1025
+ return this.auth.onSessionExpired(callback);
1026
+ }
870
1027
  /**
871
1028
  * Get current authenticated user profile and platform wallet
872
1029
  */
873
1030
  async getMe() {
874
- const token = this.auth.getToken();
875
- if (!token) {
876
- throw new Error("Not authenticated: please call login() or setToken() first");
877
- }
878
- const res = await this.fetchFn(`${this.gatewayUrl}/generic/${this.tenantId}/me`, {
879
- headers: {
880
- "Content-Type": "application/json",
881
- "Authorization": `Bearer ${token}`,
882
- "x-tenant-id": this.tenantId
883
- }
884
- });
1031
+ const res = await this.auth.fetchWithAuth(`${this.gatewayUrl}/generic/${this.tenantId}/me`);
885
1032
  if (!res.ok) {
886
1033
  const err = await res.json().catch(() => ({ message: res.statusText }));
887
1034
  throw new Error(err.message || `Failed to fetch profile & wallet (HTTP ${res.status})`);