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.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}`);
@@ -624,12 +748,37 @@ var EntityQueryBuilder = class {
624
748
  }
625
749
  /**
626
750
  * Link this record to another entity record
751
+ * Supports:
752
+ * .link(id, 'in_category', categoryId)
753
+ * .link(id, 'in_category', 'category', categoryId)
754
+ * .link(id, { relationName: 'in_category', targetId: categoryId, targetTypeName: 'category' })
627
755
  */
628
- async link(id, payload) {
756
+ async link(id, relationOrPayload, targetTypeNameOrId, maybeTargetId) {
757
+ let payload;
758
+ if (typeof relationOrPayload === "string") {
759
+ if (maybeTargetId) {
760
+ payload = {
761
+ relationName: relationOrPayload,
762
+ targetTypeName: targetTypeNameOrId,
763
+ targetId: maybeTargetId
764
+ };
765
+ } else {
766
+ payload = {
767
+ relationName: relationOrPayload,
768
+ targetId: targetTypeNameOrId
769
+ };
770
+ }
771
+ } else {
772
+ const p = relationOrPayload;
773
+ payload = {
774
+ relationName: p.relationName || p.relation_name || "",
775
+ targetId: p.targetId || p.target_id || p.child_id || p.childId || "",
776
+ targetTypeName: p.targetTypeName || p.target_type_name
777
+ };
778
+ }
629
779
  const url = `${this.gatewayUrl}/generic/${this.tenantId}/${this.typeName}/${id}/relations`;
630
- const res = await this.fetchFn(url, {
780
+ const res = await this.executeRequest(url, {
631
781
  method: "POST",
632
- headers: this.getHeaders(),
633
782
  body: JSON.stringify(payload)
634
783
  });
635
784
  if (!res.ok) {
@@ -643,9 +792,8 @@ var EntityQueryBuilder = class {
643
792
  */
644
793
  async setLinks(id, payload) {
645
794
  const url = `${this.gatewayUrl}/generic/${this.tenantId}/${this.typeName}/${id}/relations`;
646
- const res = await this.fetchFn(url, {
795
+ const res = await this.executeRequest(url, {
647
796
  method: "PUT",
648
- headers: this.getHeaders(),
649
797
  body: JSON.stringify(payload)
650
798
  });
651
799
  if (!res.ok) {
@@ -659,9 +807,8 @@ var EntityQueryBuilder = class {
659
807
  */
660
808
  async unlink(id, relationName, targetId, targetTypeName) {
661
809
  const url = `${this.gatewayUrl}/generic/${this.tenantId}/${this.typeName}/${id}/relations/${encodeURIComponent(relationName)}/${encodeURIComponent(targetId)}?targetTypeName=${encodeURIComponent(targetTypeName)}`;
662
- const res = await this.fetchFn(url, {
663
- method: "DELETE",
664
- headers: this.getHeaders()
810
+ const res = await this.executeRequest(url, {
811
+ method: "DELETE"
665
812
  });
666
813
  if (!res.ok) {
667
814
  const err = await res.json().catch(() => ({ message: res.statusText }));
@@ -674,9 +821,8 @@ var EntityQueryBuilder = class {
674
821
  */
675
822
  async action(actionName, payload) {
676
823
  const url = `${this.gatewayUrl}/generic/${this.tenantId}/${this.typeName}/actions/${actionName}`;
677
- const res = await this.fetchFn(url, {
824
+ const res = await this.executeRequest(url, {
678
825
  method: "POST",
679
- headers: this.getHeaders(),
680
826
  body: payload ? JSON.stringify(payload) : void 0
681
827
  });
682
828
  if (!res.ok) {
@@ -690,9 +836,8 @@ var EntityQueryBuilder = class {
690
836
  */
691
837
  async instanceAction(id, actionName, payload) {
692
838
  const url = `${this.gatewayUrl}/generic/${this.tenantId}/${this.typeName}/${id}/actions/${actionName}`;
693
- const res = await this.fetchFn(url, {
839
+ const res = await this.executeRequest(url, {
694
840
  method: "POST",
695
- headers: this.getHeaders(),
696
841
  body: payload ? JSON.stringify(payload) : void 0
697
842
  });
698
843
  if (!res.ok) {
@@ -707,9 +852,8 @@ var EntityQueryBuilder = class {
707
852
  async exportCsv() {
708
853
  const qs = this.queryParams.toString();
709
854
  const url = `${this.gatewayUrl}/generic/${this.tenantId}/${this.typeName}/export/csv${qs ? `?${qs}` : ""}`;
710
- const res = await this.fetchFn(url, {
855
+ const res = await this.executeRequest(url, {
711
856
  headers: {
712
- ...this.getHeaders(),
713
857
  "Accept": "text/csv"
714
858
  }
715
859
  });
@@ -727,11 +871,13 @@ var SchemaClient = class {
727
871
  tenantId;
728
872
  getToken;
729
873
  fetchFn;
874
+ fetchWithAuth;
730
875
  constructor(options) {
731
876
  this.gatewayUrl = options.gatewayUrl.replace(/\/+$/, "");
732
877
  this.tenantId = options.tenantId;
733
- this.getToken = options.getToken;
878
+ this.getToken = options.getToken || (() => null);
734
879
  this.fetchFn = options.fetchFn || (typeof fetch !== "undefined" ? fetch : globalThis.fetch);
880
+ this.fetchWithAuth = options.fetchWithAuth;
735
881
  }
736
882
  getHeaders() {
737
883
  const headers = {
@@ -749,11 +895,11 @@ var SchemaClient = class {
749
895
  */
750
896
  async define(definition) {
751
897
  const url = `${this.gatewayUrl}/generic/${this.tenantId}/definition`;
752
- const res = await this.fetchFn(url, {
898
+ const init = {
753
899
  method: "POST",
754
- headers: this.getHeaders(),
755
900
  body: JSON.stringify(definition)
756
- });
901
+ };
902
+ const res = this.fetchWithAuth ? await this.fetchWithAuth(url, init) : await this.fetchFn(url, { ...init, headers: this.getHeaders() });
757
903
  if (!res.ok) {
758
904
  const err = await res.json().catch(() => ({ message: res.statusText }));
759
905
  throw new Error(err.message || `Failed to define schema for ${definition.typeName}`);
@@ -807,13 +953,17 @@ var Zone4CodeClient = class {
807
953
  storage: this.storage,
808
954
  storageKey: config.storageKey,
809
955
  initialToken: config.token,
810
- fetchFn: this.fetchFn
956
+ fetchFn: this.fetchFn,
957
+ autoRefresh: config.autoRefresh,
958
+ tokenExpiryBuffer: config.tokenExpiryBuffer,
959
+ onSessionExpired: config.onSessionExpired
811
960
  });
812
961
  this.schema = new SchemaClient({
813
962
  gatewayUrl: this.gatewayUrl,
814
963
  tenantId: this.tenantId,
815
964
  getToken: () => this.auth.getToken(),
816
- fetchFn: this.fetchFn
965
+ fetchFn: this.fetchFn,
966
+ fetchWithAuth: (url, init) => this.auth.fetchWithAuth(url, init)
817
967
  });
818
968
  }
819
969
  /**
@@ -825,7 +975,8 @@ var Zone4CodeClient = class {
825
975
  tenantId: this.tenantId,
826
976
  typeName,
827
977
  getToken: () => this.auth.getToken(),
828
- fetchFn: this.fetchFn
978
+ fetchFn: this.fetchFn,
979
+ fetchWithAuth: (url, init) => this.auth.fetchWithAuth(url, init)
829
980
  });
830
981
  }
831
982
  /**
@@ -834,21 +985,17 @@ var Zone4CodeClient = class {
834
985
  entities(typeName) {
835
986
  return this.from(typeName);
836
987
  }
988
+ /**
989
+ * Register a listener called whenever the session expires completely
990
+ */
991
+ onSessionExpired(callback) {
992
+ return this.auth.onSessionExpired(callback);
993
+ }
837
994
  /**
838
995
  * Get current authenticated user profile and platform wallet
839
996
  */
840
997
  async getMe() {
841
- const token = this.auth.getToken();
842
- if (!token) {
843
- throw new Error("Not authenticated: please call login() or setToken() first");
844
- }
845
- const res = await this.fetchFn(`${this.gatewayUrl}/generic/${this.tenantId}/me`, {
846
- headers: {
847
- "Content-Type": "application/json",
848
- "Authorization": `Bearer ${token}`,
849
- "x-tenant-id": this.tenantId
850
- }
851
- });
998
+ const res = await this.auth.fetchWithAuth(`${this.gatewayUrl}/generic/${this.tenantId}/me`);
852
999
  if (!res.ok) {
853
1000
  const err = await res.json().catch(() => ({ message: res.statusText }));
854
1001
  throw new Error(err.message || `Failed to fetch profile & wallet (HTTP ${res.status})`);