@nurama/sdk 0.0.0-stage → 1.4.1

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.
Files changed (225) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +5 -0
  3. package/README.md +1080 -2
  4. package/dist/BotClient.d.ts +66 -0
  5. package/dist/BotClient.d.ts.map +1 -0
  6. package/dist/BotClient.js +68 -0
  7. package/dist/BotClient.js.map +1 -0
  8. package/dist/NuramaClient.d.ts +480 -0
  9. package/dist/NuramaClient.d.ts.map +1 -0
  10. package/dist/NuramaClient.js +902 -0
  11. package/dist/NuramaClient.js.map +1 -0
  12. package/dist/browser/nurama-bot-sdk.js +12051 -0
  13. package/dist/browser/nurama-bot-sdk.min.js +1 -0
  14. package/dist/browser/nurama-sdk.js +12003 -0
  15. package/dist/browser/nurama-sdk.min.js +1 -0
  16. package/dist/routes/ai.d.ts +280 -0
  17. package/dist/routes/ai.d.ts.map +1 -0
  18. package/dist/routes/ai.js +173 -0
  19. package/dist/routes/ai.js.map +1 -0
  20. package/dist/routes/asset.d.ts +493 -0
  21. package/dist/routes/asset.d.ts.map +1 -0
  22. package/dist/routes/asset.js +848 -0
  23. package/dist/routes/asset.js.map +1 -0
  24. package/dist/routes/auth.d.ts +218 -0
  25. package/dist/routes/auth.d.ts.map +1 -0
  26. package/dist/routes/auth.js +454 -0
  27. package/dist/routes/auth.js.map +1 -0
  28. package/dist/routes/blogPosts.d.ts +17 -0
  29. package/dist/routes/blogPosts.d.ts.map +1 -0
  30. package/dist/routes/blogPosts.js +29 -0
  31. package/dist/routes/blogPosts.js.map +1 -0
  32. package/dist/routes/board.d.ts +187 -0
  33. package/dist/routes/board.d.ts.map +1 -0
  34. package/dist/routes/board.js +270 -0
  35. package/dist/routes/board.js.map +1 -0
  36. package/dist/routes/bot.d.ts +202 -0
  37. package/dist/routes/bot.d.ts.map +1 -0
  38. package/dist/routes/bot.js +229 -0
  39. package/dist/routes/bot.js.map +1 -0
  40. package/dist/routes/chat.d.ts +842 -0
  41. package/dist/routes/chat.d.ts.map +1 -0
  42. package/dist/routes/chat.js +863 -0
  43. package/dist/routes/chat.js.map +1 -0
  44. package/dist/routes/chatAi.d.ts +51 -0
  45. package/dist/routes/chatAi.d.ts.map +1 -0
  46. package/dist/routes/chatAi.js +109 -0
  47. package/dist/routes/chatAi.js.map +1 -0
  48. package/dist/routes/config.d.ts +11 -0
  49. package/dist/routes/config.d.ts.map +1 -0
  50. package/dist/routes/config.js +24 -0
  51. package/dist/routes/config.js.map +1 -0
  52. package/dist/routes/convo.d.ts +169 -0
  53. package/dist/routes/convo.d.ts.map +1 -0
  54. package/dist/routes/convo.js +284 -0
  55. package/dist/routes/convo.js.map +1 -0
  56. package/dist/routes/credits.d.ts +82 -0
  57. package/dist/routes/credits.d.ts.map +1 -0
  58. package/dist/routes/credits.js +49 -0
  59. package/dist/routes/credits.js.map +1 -0
  60. package/dist/routes/device.d.ts +74 -0
  61. package/dist/routes/device.d.ts.map +1 -0
  62. package/dist/routes/device.js +122 -0
  63. package/dist/routes/device.js.map +1 -0
  64. package/dist/routes/folder.d.ts +75 -0
  65. package/dist/routes/folder.d.ts.map +1 -0
  66. package/dist/routes/folder.js +99 -0
  67. package/dist/routes/folder.js.map +1 -0
  68. package/dist/routes/invite.d.ts +61 -0
  69. package/dist/routes/invite.d.ts.map +1 -0
  70. package/dist/routes/invite.js +86 -0
  71. package/dist/routes/invite.js.map +1 -0
  72. package/dist/routes/joinLink.d.ts +88 -0
  73. package/dist/routes/joinLink.d.ts.map +1 -0
  74. package/dist/routes/joinLink.js +205 -0
  75. package/dist/routes/joinLink.js.map +1 -0
  76. package/dist/routes/membership.d.ts +116 -0
  77. package/dist/routes/membership.d.ts.map +1 -0
  78. package/dist/routes/membership.js +183 -0
  79. package/dist/routes/membership.js.map +1 -0
  80. package/dist/routes/notification.d.ts +103 -0
  81. package/dist/routes/notification.d.ts.map +1 -0
  82. package/dist/routes/notification.js +89 -0
  83. package/dist/routes/notification.js.map +1 -0
  84. package/dist/routes/oauthGrant.d.ts +45 -0
  85. package/dist/routes/oauthGrant.d.ts.map +1 -0
  86. package/dist/routes/oauthGrant.js +32 -0
  87. package/dist/routes/oauthGrant.js.map +1 -0
  88. package/dist/routes/payment.d.ts +56 -0
  89. package/dist/routes/payment.d.ts.map +1 -0
  90. package/dist/routes/payment.js +78 -0
  91. package/dist/routes/payment.js.map +1 -0
  92. package/dist/routes/product.d.ts +43 -0
  93. package/dist/routes/product.d.ts.map +1 -0
  94. package/dist/routes/product.js +53 -0
  95. package/dist/routes/product.js.map +1 -0
  96. package/dist/routes/project.d.ts +821 -0
  97. package/dist/routes/project.d.ts.map +1 -0
  98. package/dist/routes/project.js +1153 -0
  99. package/dist/routes/project.js.map +1 -0
  100. package/dist/routes/public.d.ts +269 -0
  101. package/dist/routes/public.d.ts.map +1 -0
  102. package/dist/routes/public.js +412 -0
  103. package/dist/routes/public.js.map +1 -0
  104. package/dist/routes/scratch.d.ts +70 -0
  105. package/dist/routes/scratch.d.ts.map +1 -0
  106. package/dist/routes/scratch.js +67 -0
  107. package/dist/routes/scratch.js.map +1 -0
  108. package/dist/routes/settings.d.ts +102 -0
  109. package/dist/routes/settings.d.ts.map +1 -0
  110. package/dist/routes/settings.js +94 -0
  111. package/dist/routes/settings.js.map +1 -0
  112. package/dist/routes/shortlink.d.ts +79 -0
  113. package/dist/routes/shortlink.d.ts.map +1 -0
  114. package/dist/routes/shortlink.js +25 -0
  115. package/dist/routes/shortlink.js.map +1 -0
  116. package/dist/routes/socket.d.ts +108 -0
  117. package/dist/routes/socket.d.ts.map +1 -0
  118. package/dist/routes/socket.js +573 -0
  119. package/dist/routes/socket.js.map +1 -0
  120. package/dist/routes/storage.d.ts +44 -0
  121. package/dist/routes/storage.d.ts.map +1 -0
  122. package/dist/routes/storage.js +49 -0
  123. package/dist/routes/storage.js.map +1 -0
  124. package/dist/routes/subscription.d.ts +184 -0
  125. package/dist/routes/subscription.d.ts.map +1 -0
  126. package/dist/routes/subscription.js +219 -0
  127. package/dist/routes/subscription.js.map +1 -0
  128. package/dist/routes/supportChat.d.ts +40 -0
  129. package/dist/routes/supportChat.d.ts.map +1 -0
  130. package/dist/routes/supportChat.js +53 -0
  131. package/dist/routes/supportChat.js.map +1 -0
  132. package/dist/routes/supportTicket.d.ts +89 -0
  133. package/dist/routes/supportTicket.d.ts.map +1 -0
  134. package/dist/routes/supportTicket.js +54 -0
  135. package/dist/routes/supportTicket.js.map +1 -0
  136. package/dist/routes/tag.d.ts +72 -0
  137. package/dist/routes/tag.d.ts.map +1 -0
  138. package/dist/routes/tag.js +81 -0
  139. package/dist/routes/tag.js.map +1 -0
  140. package/dist/routes/task.d.ts +252 -0
  141. package/dist/routes/task.d.ts.map +1 -0
  142. package/dist/routes/task.js +284 -0
  143. package/dist/routes/task.js.map +1 -0
  144. package/dist/routes/taskRelation.d.ts +80 -0
  145. package/dist/routes/taskRelation.d.ts.map +1 -0
  146. package/dist/routes/taskRelation.js +71 -0
  147. package/dist/routes/taskRelation.js.map +1 -0
  148. package/dist/routes/token.d.ts +97 -0
  149. package/dist/routes/token.d.ts.map +1 -0
  150. package/dist/routes/token.js +73 -0
  151. package/dist/routes/token.js.map +1 -0
  152. package/dist/routes/user.d.ts +112 -0
  153. package/dist/routes/user.d.ts.map +1 -0
  154. package/dist/routes/user.js +151 -0
  155. package/dist/routes/user.js.map +1 -0
  156. package/dist/routes/version.d.ts +42 -0
  157. package/dist/routes/version.d.ts.map +1 -0
  158. package/dist/routes/version.js +38 -0
  159. package/dist/routes/version.js.map +1 -0
  160. package/dist/routes/webhook.d.ts +170 -0
  161. package/dist/routes/webhook.d.ts.map +1 -0
  162. package/dist/routes/webhook.js +173 -0
  163. package/dist/routes/webhook.js.map +1 -0
  164. package/dist/routes/workspace.d.ts +120 -0
  165. package/dist/routes/workspace.d.ts.map +1 -0
  166. package/dist/routes/workspace.js +199 -0
  167. package/dist/routes/workspace.js.map +1 -0
  168. package/dist/utils/uploadSessionManager.d.ts +133 -0
  169. package/dist/utils/uploadSessionManager.d.ts.map +1 -0
  170. package/dist/utils/uploadSessionManager.js +321 -0
  171. package/dist/utils/uploadSessionManager.js.map +1 -0
  172. package/dist/utils/urlParams.d.ts +35 -0
  173. package/dist/utils/urlParams.d.ts.map +1 -0
  174. package/dist/utils/urlParams.js +146 -0
  175. package/dist/utils/urlParams.js.map +1 -0
  176. package/dist/version.d.ts +15 -0
  177. package/dist/version.d.ts.map +1 -0
  178. package/dist/version.js +12 -0
  179. package/dist/version.js.map +1 -0
  180. package/package.json +87 -3
  181. package/src/BotClient.ts +113 -0
  182. package/src/NuramaClient.ts +1253 -0
  183. package/src/bot-browser-entry.js +15 -0
  184. package/src/browser-entry.js +20 -0
  185. package/src/routes/ai.ts +378 -0
  186. package/src/routes/asset.ts +1104 -0
  187. package/src/routes/auth.ts +587 -0
  188. package/src/routes/blogPosts.ts +29 -0
  189. package/src/routes/board.ts +403 -0
  190. package/src/routes/bot.ts +356 -0
  191. package/src/routes/chat.ts +1292 -0
  192. package/src/routes/chatAi.ts +125 -0
  193. package/src/routes/config.ts +31 -0
  194. package/src/routes/convo.ts +321 -0
  195. package/src/routes/credits.ts +112 -0
  196. package/src/routes/device.ts +133 -0
  197. package/src/routes/folder.ts +154 -0
  198. package/src/routes/invite.ts +133 -0
  199. package/src/routes/joinLink.ts +233 -0
  200. package/src/routes/membership.ts +237 -0
  201. package/src/routes/notification.ts +166 -0
  202. package/src/routes/oauthGrant.ts +64 -0
  203. package/src/routes/payment.ts +104 -0
  204. package/src/routes/product.ts +67 -0
  205. package/src/routes/project.ts +1528 -0
  206. package/src/routes/public.ts +496 -0
  207. package/src/routes/scratch.ts +94 -0
  208. package/src/routes/settings.ts +152 -0
  209. package/src/routes/shortlink.ts +90 -0
  210. package/src/routes/socket.ts +757 -0
  211. package/src/routes/storage.ts +83 -0
  212. package/src/routes/subscription.ts +307 -0
  213. package/src/routes/supportChat.ts +62 -0
  214. package/src/routes/supportTicket.ts +114 -0
  215. package/src/routes/tag.ts +131 -0
  216. package/src/routes/task.ts +431 -0
  217. package/src/routes/taskRelation.ts +125 -0
  218. package/src/routes/token.ts +152 -0
  219. package/src/routes/user.ts +214 -0
  220. package/src/routes/version.ts +62 -0
  221. package/src/routes/webhook.ts +295 -0
  222. package/src/routes/workspace.ts +223 -0
  223. package/src/utils/uploadSessionManager.ts +407 -0
  224. package/src/utils/urlParams.ts +181 -0
  225. package/src/version.ts +22 -0
@@ -0,0 +1,902 @@
1
+ import { jwtDecode } from 'jwt-decode';
2
+ import { paramsToUrlSearchParams } from './utils/urlParams.js';
3
+ import createAuthMethods from './routes/auth.js';
4
+ import createSubscriptionMethods from './routes/subscription.js';
5
+ import createUserMethods from './routes/user.js';
6
+ import createWorkspaceMethods from './routes/workspace.js';
7
+ import createProjectMethods from './routes/project.js';
8
+ import createAssetMethods from './routes/asset.js';
9
+ import createChatMethods from './routes/chat.js';
10
+ import createFolderMethods from './routes/folder.js';
11
+ import createInviteMethods from './routes/invite.js';
12
+ import createJoinLinkMethods from './routes/joinLink.js';
13
+ import createMembershipMethods from './routes/membership.js';
14
+ import createNotificationMethods from './routes/notification.js';
15
+ import createPaymentMethods from './routes/payment.js';
16
+ import createProductMethods from './routes/product.js';
17
+ import createSocketMethods from './routes/socket.js';
18
+ import createStorageMethods from './routes/storage.js';
19
+ import createTaskMethods from './routes/task.js';
20
+ import createVersionMethods from './routes/version.js';
21
+ import createConfigMethods from './routes/config.js';
22
+ import createTagMethods from './routes/tag.js';
23
+ import createPublicMethods from './routes/public.js';
24
+ import createSettingsMethods from './routes/settings.js';
25
+ import createDeviceMethods from './routes/device.js';
26
+ import createShortLinkMethods from './routes/shortlink.js';
27
+ import createConvoMethods from './routes/convo.js';
28
+ import createBoardMethods from './routes/board.js';
29
+ import createTaskRelationMethods from './routes/taskRelation.js';
30
+ import createBotMethods from './routes/bot.js';
31
+ import createAiMethods from './routes/ai.js';
32
+ import createAiChatMethods from './routes/chatAi.js';
33
+ import createSupportChatMethods from './routes/supportChat.js';
34
+ import createBlogPostsMethods from './routes/blogPosts.js';
35
+ import createCreditsMethods from './routes/credits.js';
36
+ import createScratchMethods from './routes/scratch.js';
37
+ import createTokenMethods from './routes/token.js';
38
+ import createOAuthGrantMethods from './routes/oauthGrant.js';
39
+ import createWebhookMethods from './routes/webhook.js';
40
+ import createSupportTicketMethods from './routes/supportTicket.js';
41
+ import { SDK_VERSION } from './version.js';
42
+ /** Narrow an unknown thrown value to an API error carrying an HTTP status. */
43
+ export const isApiError = (error) => error instanceof Error && typeof error.status === 'number';
44
+ /**
45
+ * Client interface for interacting with the Nurama REST API.
46
+ */
47
+ class NuramaClient {
48
+ /**
49
+ * Creates an instance of NuramaClient.
50
+ * @param baseURL - The base URL for the Nurama API.
51
+ * @param options - Configuration options.
52
+ */
53
+ constructor(baseURL, options = {}) {
54
+ this.browserMode = false;
55
+ this._accessToken = null;
56
+ this._refreshTokenValue = null;
57
+ this._accessTokenExpiry = null;
58
+ this._refreshTokenExpiry = null;
59
+ this._verifyMfaToken = null;
60
+ this._isRefreshing = false;
61
+ this._lastRefreshAttempt = 0;
62
+ this._cache = new Map();
63
+ // Bumped on any cache clear/invalidation so a GET already in flight won't
64
+ // re-cache a response body that predates the clear.
65
+ this._cacheGeneration = 0;
66
+ this._apiKey = null;
67
+ if (!baseURL) {
68
+ throw new Error('baseURL is required.');
69
+ }
70
+ // Use global fetch if available and no fetch provided
71
+ const defaultFetch = typeof fetch !== 'undefined' ? fetch : undefined;
72
+ this.baseURL = baseURL.replace(/\/$/, '');
73
+ // Determine if we're in browser mode
74
+ this.browserMode = options.browserMode || false;
75
+ // Handle fetch implementation with proper binding in browser mode
76
+ let effectiveFetch;
77
+ if (options.fetch) {
78
+ // User provided custom fetch implementation
79
+ effectiveFetch = options.fetch;
80
+ }
81
+ else if (this.browserMode && typeof window !== 'undefined' && window.fetch) {
82
+ // In browser mode, wrap fetch to ensure proper binding to window
83
+ effectiveFetch = (url, init) => window.fetch(url, init);
84
+ }
85
+ else {
86
+ // Fallback to default fetch
87
+ effectiveFetch = defaultFetch;
88
+ }
89
+ // Validate fetch availability
90
+ if (!effectiveFetch) {
91
+ throw new Error('Fetch implementation not found. Please provide one in options or ensure global fetch is available.');
92
+ }
93
+ this.fetch = effectiveFetch;
94
+ this.tokenStorageKey = options.tokenStorageKey || 'nurama_access_token';
95
+ this.refreshTokenStorageKey = options.refreshTokenStorageKey || 'nurama_refresh_token';
96
+ this.tokenExpiryBufferSeconds = options.tokenExpiryBufferSeconds === undefined ? 60 : options.tokenExpiryBufferSeconds;
97
+ this.refreshLockTimeoutMs = options.refreshLockTimeoutMs === undefined ? 1000 : options.refreshLockTimeoutMs;
98
+ this.tokenRefreshRetryDelayMs = options.tokenRefreshRetryDelayMs === undefined ? 100 : options.tokenRefreshRetryDelayMs;
99
+ this.tokenRefreshMaxWaitMs = options.tokenRefreshMaxWaitMs === undefined ? 1000 : options.tokenRefreshMaxWaitMs;
100
+ this.debug = options.debug || false;
101
+ this.websocketURL = options.websocketURL;
102
+ this.enableCache = options.enableCache === undefined ? true : options.enableCache;
103
+ this.cacheDurationSeconds = options.cacheDurationSeconds === undefined ? 5 : options.cacheDurationSeconds;
104
+ this.invalidateCacheOnMutation = options.invalidateCacheOnMutation === undefined ? true : options.invalidateCacheOnMutation;
105
+ this._onUnauthorized = options.onUnauthorized;
106
+ this._onMaintenance = options.onMaintenance;
107
+ this._onTokensChanged = options.onTokensChanged;
108
+ this._apiKey = options.apiKey || null;
109
+ if (this.browserMode && !this._apiKey) {
110
+ this._loadTokens();
111
+ }
112
+ // --- Create API namespaces ---
113
+ this.auth = createAuthMethods(this);
114
+ this.subscription = createSubscriptionMethods(this);
115
+ this.user = createUserMethods(this);
116
+ this.workspace = createWorkspaceMethods(this);
117
+ this.project = createProjectMethods(this);
118
+ this.asset = createAssetMethods(this);
119
+ this.chat = createChatMethods(this);
120
+ this.folder = createFolderMethods(this);
121
+ this.invite = createInviteMethods(this);
122
+ this.joinLink = createJoinLinkMethods(this);
123
+ this.membership = createMembershipMethods(this);
124
+ this.notification = createNotificationMethods(this);
125
+ this.payment = createPaymentMethods(this);
126
+ this.product = createProductMethods(this);
127
+ this.socket = createSocketMethods(this);
128
+ this.storage = createStorageMethods(this);
129
+ this.task = createTaskMethods(this);
130
+ this.version = createVersionMethods(this);
131
+ this.config = createConfigMethods(this);
132
+ this.tag = createTagMethods(this);
133
+ this.public = createPublicMethods(this);
134
+ this.settings = createSettingsMethods(this);
135
+ this.device = createDeviceMethods(this);
136
+ this.shortlink = createShortLinkMethods(this);
137
+ this.convo = createConvoMethods(this);
138
+ this.board = createBoardMethods(this);
139
+ this.taskRelation = createTaskRelationMethods(this);
140
+ this.bot = createBotMethods(this);
141
+ this.ai = createAiMethods(this);
142
+ this.aiChat = createAiChatMethods(this);
143
+ this.supportChat = createSupportChatMethods(this);
144
+ this.blogPosts = createBlogPostsMethods(this);
145
+ this.credits = createCreditsMethods(this);
146
+ this.scratch = createScratchMethods(this);
147
+ this.token = createTokenMethods(this);
148
+ this.oauthGrant = createOAuthGrantMethods(this);
149
+ this.webhook = createWebhookMethods(this);
150
+ this.supportTicket = createSupportTicketMethods(this);
151
+ }
152
+ /**
153
+ * Internal helper for conditional logging. Underscore-prefixed to flag
154
+ * "internal contract" — accessible from sibling route modules but not
155
+ * part of the documented public API.
156
+ *
157
+ * @param args - Arguments to pass to console.log.
158
+ */
159
+ _log(...args) {
160
+ if (this.debug) {
161
+ console.log(...args); // eslint-disable-line no-console
162
+ }
163
+ }
164
+ /**
165
+ * Checks if a POST endpoint is safe to cache (read-only query operations).
166
+ * @param endpoint - The API endpoint to check.
167
+ * @returns True if the POST endpoint is cacheable, false otherwise.
168
+ * @private
169
+ */
170
+ _isCacheablePostEndpoint(endpoint) {
171
+ // List of POST endpoints that are safe to cache (read-only query operations)
172
+ const cacheablePostEndpoints = [
173
+ '/v1/notifications/count',
174
+ '/v1/notifications/count/bulk',
175
+ '/v1/notifications',
176
+ '/v1/notifications/new',
177
+ '/v1/notifications/last-seen'
178
+ ];
179
+ return cacheablePostEndpoints.some(cacheableEndpoint => endpoint === cacheableEndpoint);
180
+ }
181
+ /**
182
+ * Generates a cache key for a request.
183
+ * @param options - Request configuration.
184
+ * @returns A unique cache key string.
185
+ * @private
186
+ */
187
+ _generateCacheKey(options) {
188
+ const { endpoint, method = 'GET', params = {}, body = null } = options;
189
+ const sortedParams = Object.keys(params).sort().reduce((acc, key) => {
190
+ acc[key] = params[key];
191
+ return acc;
192
+ }, {});
193
+ return JSON.stringify({
194
+ endpoint,
195
+ method,
196
+ params: sortedParams,
197
+ body
198
+ });
199
+ }
200
+ /**
201
+ * Checks if a cache entry is still valid.
202
+ * @param entry - The cache entry to check.
203
+ * @returns True if the entry is still valid, false otherwise.
204
+ * @private
205
+ */
206
+ _isCacheEntryValid(entry) {
207
+ const now = Date.now();
208
+ const maxAge = this.cacheDurationSeconds * 1000;
209
+ return (now - entry.timestamp) < maxAge;
210
+ }
211
+ /**
212
+ * Gets a cached response if available and valid.
213
+ * @param cacheKey - The cache key to look up.
214
+ * @returns The cached data or null if not found/expired.
215
+ * @private
216
+ */
217
+ _getCachedResponse(cacheKey) {
218
+ const entry = this._cache.get(cacheKey);
219
+ if (!entry)
220
+ return null;
221
+ if (this._isCacheEntryValid(entry)) {
222
+ this._log('[DEBUG] Cache hit for key:', cacheKey);
223
+ return entry.data;
224
+ }
225
+ else {
226
+ // Remove expired entry
227
+ this._cache.delete(cacheKey);
228
+ this._log('[DEBUG] Cache entry expired and removed for key:', cacheKey);
229
+ return null;
230
+ }
231
+ }
232
+ /**
233
+ * Stores a response in the cache.
234
+ * @param cacheKey - The cache key.
235
+ * @param data - The data to cache.
236
+ * @private
237
+ */
238
+ _setCachedResponse(cacheKey, data) {
239
+ this._cache.set(cacheKey, {
240
+ data,
241
+ timestamp: Date.now()
242
+ });
243
+ this._log('[DEBUG] Cached response for key:', cacheKey);
244
+ }
245
+ /**
246
+ * Gets or sets a pending promise for concurrent requests.
247
+ * @param cacheKey - The cache key.
248
+ * @param promise - The promise to store (optional).
249
+ * @returns The existing or new promise.
250
+ * @private
251
+ */
252
+ _getPendingPromise(cacheKey, promise) {
253
+ const entry = this._cache.get(cacheKey);
254
+ if (promise) {
255
+ // Store the promise
256
+ if (entry) {
257
+ entry.promise = promise;
258
+ }
259
+ else {
260
+ this._cache.set(cacheKey, {
261
+ data: null,
262
+ timestamp: Date.now(),
263
+ promise
264
+ });
265
+ }
266
+ return promise;
267
+ }
268
+ // Return existing promise if valid
269
+ if (entry?.promise && this._isCacheEntryValid(entry)) {
270
+ this._log('[DEBUG] Returning pending promise for concurrent request:', cacheKey);
271
+ return entry.promise;
272
+ }
273
+ return null;
274
+ }
275
+ /**
276
+ * Clears the pending promise from a cache entry.
277
+ * @param cacheKey - The cache key.
278
+ * @private
279
+ */
280
+ _clearPendingPromise(cacheKey) {
281
+ const entry = this._cache.get(cacheKey);
282
+ if (entry) {
283
+ delete entry.promise;
284
+ }
285
+ }
286
+ /**
287
+ * True if a request is a write that should invalidate cached reads for its
288
+ * resource. Cacheable "read" POSTs (query endpoints) are excluded.
289
+ * @private
290
+ */
291
+ _isMutation(method, endpoint) {
292
+ const m = (method || 'GET').toUpperCase();
293
+ if (m === 'PUT' || m === 'PATCH' || m === 'DELETE')
294
+ return true;
295
+ if (m === 'POST')
296
+ return !this._isCacheablePostEndpoint(endpoint);
297
+ return false;
298
+ }
299
+ /**
300
+ * True if a path segment looks like a resource id (UUID or numeric) rather
301
+ * than a collection name.
302
+ * @private
303
+ */
304
+ _looksLikeSegmentId(segment) {
305
+ return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(segment)
306
+ || /^\d+$/.test(segment);
307
+ }
308
+ /**
309
+ * The resource "collection" a mutation acts on — the deepest non-id,
310
+ * non-version path segment (e.g. `/v1/workspaces/:id/projects` -> `projects`,
311
+ * `DELETE /v1/projects/:id` -> `projects`).
312
+ * @private
313
+ */
314
+ _resourceToken(endpoint) {
315
+ const path = (endpoint || '').split('?')[0];
316
+ const segments = path.split('/').filter(Boolean);
317
+ for (let i = segments.length - 1; i >= 0; i -= 1) {
318
+ const seg = segments[i];
319
+ if (seg === 'v1' || this._looksLikeSegmentId(seg))
320
+ continue;
321
+ return seg;
322
+ }
323
+ return null;
324
+ }
325
+ /**
326
+ * Evict cached GET responses that belong to the same resource collection as a
327
+ * just-completed write, so the next read is fresh. Matches the parent-nested
328
+ * list form too — a `POST /v1/projects` also evicts a cached
329
+ * `/v1/workspaces/:id/projects` because both paths contain the `projects`
330
+ * segment. Cross-resource relationships that don't share a path segment
331
+ * (e.g. a payment write vs the subscription read) are NOT covered here and
332
+ * are handled at their call sites.
333
+ * @private
334
+ */
335
+ _invalidateForMutation(endpoint) {
336
+ // Bump first so any GET already in flight won't re-cache a pre-write body.
337
+ this._cacheGeneration++;
338
+ const token = this._resourceToken(endpoint);
339
+ if (!token) {
340
+ // Couldn't identify the resource — clear everything to stay correct.
341
+ this._cache.clear();
342
+ return;
343
+ }
344
+ for (const key of Array.from(this._cache.keys())) {
345
+ let cachedEndpoint = '';
346
+ try {
347
+ cachedEndpoint = JSON.parse(key).endpoint || '';
348
+ }
349
+ catch {
350
+ continue;
351
+ }
352
+ const segments = cachedEndpoint.split('?')[0].split('/').filter(Boolean);
353
+ if (segments.includes(token)) {
354
+ this._cache.delete(key);
355
+ }
356
+ }
357
+ this._log('[DEBUG] Invalidated cached reads for resource:', token);
358
+ }
359
+ /**
360
+ * Sets the access and refresh tokens with optional expiration timestamps.
361
+ * @param accessToken - The JWT access token or null to clear.
362
+ * @param refreshToken - The refresh token or null to clear.
363
+ * @param accessTokenExpiry - ISO timestamp when access token expires (optional).
364
+ * @param refreshTokenExpiry - ISO timestamp when refresh token expires (optional).
365
+ * @private
366
+ */
367
+ _setTokens(accessToken, refreshToken, accessTokenExpiry, refreshTokenExpiry) {
368
+ this._log('[DEBUG] Setting tokens. Access:', accessToken ? 'Yes' : 'No', 'Refresh:', refreshToken ? 'Yes' : 'No');
369
+ this._accessToken = accessToken;
370
+ this._refreshTokenValue = refreshToken;
371
+ this._accessTokenExpiry = accessTokenExpiry || null;
372
+ this._refreshTokenExpiry = refreshTokenExpiry || null;
373
+ // Clear verifyMfa token when setting regular tokens (or clearing all)
374
+ if (!accessToken && !refreshToken) {
375
+ this._verifyMfaToken = null;
376
+ }
377
+ if (this.browserMode) {
378
+ try {
379
+ if (accessToken) {
380
+ localStorage.setItem(this.tokenStorageKey, accessToken);
381
+ }
382
+ else {
383
+ localStorage.removeItem(this.tokenStorageKey);
384
+ }
385
+ if (refreshToken) {
386
+ localStorage.setItem(this.refreshTokenStorageKey, refreshToken);
387
+ }
388
+ else {
389
+ localStorage.removeItem(this.refreshTokenStorageKey);
390
+ }
391
+ // Store expiration timestamps
392
+ if (accessTokenExpiry) {
393
+ localStorage.setItem(`${this.tokenStorageKey}_expiry`, accessTokenExpiry);
394
+ }
395
+ else {
396
+ localStorage.removeItem(`${this.tokenStorageKey}_expiry`);
397
+ }
398
+ if (refreshTokenExpiry) {
399
+ localStorage.setItem(`${this.refreshTokenStorageKey}_expiry`, refreshTokenExpiry);
400
+ }
401
+ else {
402
+ localStorage.removeItem(`${this.refreshTokenStorageKey}_expiry`);
403
+ }
404
+ }
405
+ catch (e) {
406
+ console.error('Failed to save tokens to localStorage:', e);
407
+ }
408
+ }
409
+ if (this._onTokensChanged) {
410
+ try {
411
+ this._onTokensChanged(accessToken || refreshToken ? this.getSession() : null);
412
+ }
413
+ catch (e) {
414
+ console.error('onTokensChanged handler threw:', e);
415
+ }
416
+ }
417
+ }
418
+ /**
419
+ * Returns the current session (tokens and expiries), or null when signed out.
420
+ * @public
421
+ */
422
+ getSession() {
423
+ if (!this._accessToken && !this._refreshTokenValue)
424
+ return null;
425
+ return {
426
+ accessToken: this._accessToken,
427
+ refreshToken: this._refreshTokenValue,
428
+ accessTokenExpiry: this._accessTokenExpiry,
429
+ refreshTokenExpiry: this._refreshTokenExpiry,
430
+ };
431
+ }
432
+ /**
433
+ * Restores a session previously captured through `onTokensChanged` /
434
+ * `getSession()` — for hosts that persist tokens outside localStorage.
435
+ * Does not fire `onTokensChanged`. A missing or expired access token is
436
+ * refreshed on the next authenticated request.
437
+ * @public
438
+ */
439
+ restoreSession(session) {
440
+ this._accessToken = session?.accessToken ?? null;
441
+ this._refreshTokenValue = session?.refreshToken ?? null;
442
+ this._accessTokenExpiry = session?.accessTokenExpiry ?? null;
443
+ this._refreshTokenExpiry = session?.refreshTokenExpiry ?? null;
444
+ }
445
+ /**
446
+ * Retrieves the current access token.
447
+ * @returns The access token or null.
448
+ * @private
449
+ */
450
+ _getAccessToken() {
451
+ return this._accessToken;
452
+ }
453
+ /**
454
+ * Retrieves the current refresh token.
455
+ * @returns The refresh token or null.
456
+ * @private
457
+ */
458
+ _getRefreshToken() {
459
+ return this._refreshTokenValue;
460
+ }
461
+ /**
462
+ * Sets the MFA verification token.
463
+ * @param verifyMfaToken - The verify MFA token or null to clear.
464
+ * @internal
465
+ */
466
+ _setVerifyMfaToken(verifyMfaToken) {
467
+ this._log('[DEBUG] Setting verifyMfa token:', verifyMfaToken ? 'Yes' : 'No');
468
+ this._verifyMfaToken = verifyMfaToken;
469
+ }
470
+ /**
471
+ * Retrieves the current verify MFA token.
472
+ * @returns The verify MFA token or null.
473
+ * @internal
474
+ */
475
+ _getVerifyMfaToken() {
476
+ return this._verifyMfaToken;
477
+ }
478
+ /**
479
+ * Loads tokens and their expiration timestamps from storage (browser mode only).
480
+ * @private
481
+ */
482
+ _loadTokens() {
483
+ if (!this.browserMode)
484
+ return;
485
+ try {
486
+ this._accessToken = localStorage.getItem(this.tokenStorageKey);
487
+ this._refreshTokenValue = localStorage.getItem(this.refreshTokenStorageKey);
488
+ this._accessTokenExpiry = localStorage.getItem(`${this.tokenStorageKey}_expiry`);
489
+ this._refreshTokenExpiry = localStorage.getItem(`${this.refreshTokenStorageKey}_expiry`);
490
+ this._log('[DEBUG] Loaded tokens from localStorage. Access:', this._accessToken ? 'Yes' : 'No', 'Refresh:', this._refreshTokenValue ? 'Yes' : 'No');
491
+ this._log('[DEBUG] Loaded token expiries. Access expiry:', this._accessTokenExpiry || 'None', 'Refresh expiry:', this._refreshTokenExpiry || 'None');
492
+ }
493
+ catch (e) {
494
+ console.error('Failed to load tokens from localStorage:', e);
495
+ this._accessToken = null;
496
+ this._refreshTokenValue = null;
497
+ this._accessTokenExpiry = null;
498
+ this._refreshTokenExpiry = null;
499
+ }
500
+ }
501
+ /**
502
+ * Checks if the current access token is expired or close to expiring.
503
+ * @returns True if the token is missing, expired, or needs refresh based on buffer.
504
+ * @private
505
+ */
506
+ _isTokenExpired() {
507
+ // API keys never expire client-side; the server is the source of truth.
508
+ if (this._apiKey)
509
+ return false;
510
+ const token = this._getAccessToken();
511
+ if (!token)
512
+ return true;
513
+ try {
514
+ const decoded = jwtDecode(token);
515
+ if (!decoded || typeof decoded.exp !== 'number') {
516
+ console.error('Invalid token or missing exp claim.');
517
+ return true;
518
+ }
519
+ const expirationTimeMs = decoded.exp * 1000;
520
+ const currentTimeMs = Date.now();
521
+ const bufferMs = this.tokenExpiryBufferSeconds * 1000;
522
+ const needsRefresh = currentTimeMs >= expirationTimeMs - bufferMs;
523
+ if (needsRefresh && this.debug) {
524
+ this._log(`[DEBUG] Token needs refresh. Current: ${new Date(currentTimeMs).toISOString()}, Expires: ${new Date(expirationTimeMs).toISOString()}, Buffer: ${this.tokenExpiryBufferSeconds}s`);
525
+ }
526
+ return needsRefresh;
527
+ }
528
+ catch (e) {
529
+ console.error('Failed to decode or check token expiry:', e);
530
+ return true;
531
+ }
532
+ }
533
+ /**
534
+ * Checks if the current JWT token exists and is valid.
535
+ * @returns {boolean} True if the token exists and is valid, false otherwise.
536
+ */
537
+ tokenValid() {
538
+ return !this._isTokenExpired();
539
+ }
540
+ /**
541
+ * Gets the token expiration timestamps.
542
+ * @returns Object containing access and refresh token expiry ISO timestamps.
543
+ * @public
544
+ */
545
+ getTokenExpiry() {
546
+ return {
547
+ access: this._accessTokenExpiry,
548
+ refresh: this._refreshTokenExpiry
549
+ };
550
+ }
551
+ /**
552
+ * Refreshes the access token using the stored refresh token.
553
+ * Includes a time lock to prevent rapid concurrent refresh attempts.
554
+ * @returns Promise resolving when refresh attempt is complete (or bypassed).
555
+ * @private
556
+ */
557
+ async _refreshToken() {
558
+ const now = Date.now();
559
+ if (this._isRefreshing) {
560
+ this._log('[DEBUG] Token refresh already in progress, waiting...');
561
+ const waitStartTime = Date.now();
562
+ while (this._isRefreshing) {
563
+ if (Date.now() - waitStartTime > this.tokenRefreshMaxWaitMs) {
564
+ console.error('Timed out waiting for token refresh to complete.');
565
+ throw new Error('Timeout waiting for token refresh.');
566
+ }
567
+ await new Promise(resolve => setTimeout(resolve, this.tokenRefreshRetryDelayMs));
568
+ }
569
+ this._log('[DEBUG] Other refresh process finished.');
570
+ if (!this._isTokenExpired()) {
571
+ this._log('[DEBUG] Token is now valid after waiting.');
572
+ return;
573
+ }
574
+ if (now - this._lastRefreshAttempt < this.refreshLockTimeoutMs) {
575
+ this._log(`[DEBUG] Refresh attempt too soon after waiting (last attempt: ${new Date(this._lastRefreshAttempt).toISOString()}, now: ${new Date(now).toISOString()})`);
576
+ throw new Error('Token still requires refresh after wait, but locked due to recent attempt.');
577
+ }
578
+ }
579
+ else if (now - this._lastRefreshAttempt < this.refreshLockTimeoutMs) {
580
+ this._log(`[DEBUG] Refresh attempt too soon (last attempt: ${new Date(this._lastRefreshAttempt).toISOString()}, now: ${new Date(now).toISOString()})`);
581
+ if (this._isTokenExpired()) {
582
+ throw new Error('Token requires refresh, but locked due to recent attempt.');
583
+ }
584
+ return;
585
+ }
586
+ const refreshToken = this._getRefreshToken();
587
+ if (!refreshToken) {
588
+ this._log('[DEBUG] No refresh token available for _refreshToken.');
589
+ this._setTokens(null, null);
590
+ throw new Error('Refresh token not found.');
591
+ }
592
+ this._isRefreshing = true;
593
+ this._lastRefreshAttempt = now;
594
+ try {
595
+ this._log('[DEBUG] Attempting token refresh...');
596
+ const response = await this._request({
597
+ endpoint: '/v1/auth/refresh-tokens',
598
+ method: 'POST',
599
+ body: { refreshToken },
600
+ sendJWT: false,
601
+ _isInternalRefresh: true
602
+ });
603
+ if (response && response.access && response.access.token && response.refresh && response.refresh.token) {
604
+ this._log('[DEBUG] Token refresh successful.');
605
+ this._setTokens(response.access.token, response.refresh.token, response.access.expires, response.refresh.expires);
606
+ }
607
+ else {
608
+ console.error('Invalid token refresh response:', response);
609
+ // Clear both tokens since response structure is invalid
610
+ this._setTokens(null, null);
611
+ throw new Error('Invalid token refresh response structure.');
612
+ }
613
+ }
614
+ catch (error) {
615
+ console.error('Token refresh API call failed:', error);
616
+ // Check if the error is due to invalid refresh token (401/403)
617
+ // In these cases, the refresh token itself is invalid and should be cleared
618
+ const isInvalidRefreshToken = error?.status === 401 || error?.status === 403;
619
+ if (isInvalidRefreshToken) {
620
+ this._log('[DEBUG] Refresh token is invalid (401/403), clearing all tokens');
621
+ // Clear both access and refresh tokens
622
+ this._setTokens(null, null);
623
+ }
624
+ else {
625
+ this._log('[DEBUG] Refresh failed due to network/server error, keeping refresh token for retry');
626
+ // For other errors (network issues, 500 errors, etc.), only clear access token
627
+ // Keep refresh token so it can be retried later
628
+ this._setTokens(null, refreshToken, null, this._refreshTokenExpiry);
629
+ }
630
+ // Preserve the backend error type (e.g. `emailVerificationRequired`)
631
+ // so the request-level handler can pass it to onUnauthorized and the
632
+ // app can prompt verification rather than a generic logout.
633
+ const wrapped = new Error('Token refresh failed.');
634
+ wrapped.reasonType = error?.data?.type;
635
+ throw wrapped;
636
+ }
637
+ finally {
638
+ this._isRefreshing = false;
639
+ }
640
+ }
641
+ /**
642
+ * Makes an HTTP request to the API.
643
+ * Handles token attachment and potential refresh.
644
+ * @param options - Request configuration.
645
+ * @returns The response data.
646
+ * @private
647
+ */
648
+ async _request(options) {
649
+ const { endpoint, method = 'GET', params = {}, body = null, sendJWT = true, customHeaders = {}, _isInternalRefresh = false, bypassCache = false } = options;
650
+ // Generate cache key for this request
651
+ const cacheKey = this._generateCacheKey(options);
652
+ // Check if this is a cacheable POST request (read-only query operations)
653
+ const isCacheablePost = method === 'POST' && this._isCacheablePostEndpoint(endpoint);
654
+ const shouldUseCache = this.enableCache && !bypassCache && !_isInternalRefresh &&
655
+ (method === 'GET' || method === 'HEAD' || isCacheablePost);
656
+ if (isCacheablePost) {
657
+ this._log('[DEBUG] POST request eligible for caching:', endpoint);
658
+ }
659
+ // Check cache first (for GET/HEAD requests and cacheable POST requests when cache is enabled)
660
+ if (shouldUseCache) {
661
+ // Check for cached response
662
+ const cachedResponse = this._getCachedResponse(cacheKey);
663
+ if (cachedResponse !== null) {
664
+ return cachedResponse;
665
+ }
666
+ // Check for pending promise (concurrent request handling)
667
+ const pendingPromise = this._getPendingPromise(cacheKey);
668
+ if (pendingPromise) {
669
+ return pendingPromise;
670
+ }
671
+ }
672
+ let token = this._apiKey || this._getAccessToken();
673
+ // Only check for token expiry and refresh if we need to send JWT (skip for API key auth)
674
+ if (!_isInternalRefresh && sendJWT === true && !this._apiKey) {
675
+ if (this._isTokenExpired()) {
676
+ try {
677
+ await this._refreshToken();
678
+ token = this._getAccessToken();
679
+ }
680
+ catch (refreshError) {
681
+ console.error('Token refresh failed during request:', refreshError);
682
+ this._setTokens(null, null);
683
+ if (this._onUnauthorized) {
684
+ this._onUnauthorized({ type: refreshError?.reasonType });
685
+ }
686
+ throw new Error('Authentication required due to refresh failure.');
687
+ }
688
+ // If token is still null after refresh attempt, something went wrong
689
+ if (!token) {
690
+ if (this._onUnauthorized) {
691
+ this._onUnauthorized();
692
+ }
693
+ throw new Error('Authentication required, token unavailable after refresh attempt.');
694
+ }
695
+ }
696
+ }
697
+ else if (!_isInternalRefresh && sendJWT === 'optional' && !this._apiKey) {
698
+ // Optional auth: send a valid token if we have one so the caller keeps
699
+ // their identity, but proceed anonymously (no token) rather than throwing
700
+ // when there's no session or a refresh fails. Used by public routes that
701
+ // accept both authenticated and anonymous callers.
702
+ if (token && this._isTokenExpired()) {
703
+ try {
704
+ await this._refreshToken();
705
+ token = this._getAccessToken();
706
+ }
707
+ catch {
708
+ token = null; // proceed without auth
709
+ }
710
+ }
711
+ }
712
+ const headers = {
713
+ 'Content-Type': 'application/json',
714
+ ...customHeaders,
715
+ };
716
+ if (sendJWT && token) {
717
+ headers['Authorization'] = `Bearer ${token}`;
718
+ }
719
+ const config = {
720
+ method,
721
+ headers,
722
+ };
723
+ if (body && (method === 'POST' || method === 'PUT' || method === 'PATCH' || method === 'DELETE')) {
724
+ if (headers['Content-Type'] === 'application/json') {
725
+ config.body = JSON.stringify(body);
726
+ }
727
+ else {
728
+ // For non-JSON, pass body directly (e.g., FormData)
729
+ config.body = body;
730
+ // Let fetch handle Content-Type for FormData, etc.
731
+ delete headers['Content-Type'];
732
+ }
733
+ }
734
+ // Ensure params is a proper object to prevent string iteration
735
+ const safeParams = (params && typeof params === 'object' && !Array.isArray(params)) ? params : {};
736
+ // Filter undefined/null params and convert to URL-safe format using enhanced parseUrlParams
737
+ const filteredParams = Object.entries(safeParams)
738
+ .filter(([, value]) => value !== undefined && value !== null)
739
+ .reduce((acc, [key, value]) => {
740
+ acc[key] = value;
741
+ return acc;
742
+ }, {});
743
+ // Use enhanced parseUrlParams utility with 'repeat' format for arrays
744
+ const searchParams = paramsToUrlSearchParams(filteredParams, { arrayFormat: 'repeat' });
745
+ const url = `${this.baseURL}${endpoint}${searchParams.toString() ? `?${searchParams.toString()}` : ''}`;
746
+ this._log(`[DEBUG] API Request: ${method} ${url}`, body ? { Body: body } : '');
747
+ // Snapshot the cache generation so a clear/invalidation that lands while
748
+ // this request is in flight prevents it from caching a now-stale body.
749
+ const cacheGenerationAtStart = this._cacheGeneration;
750
+ // Create the actual request promise
751
+ const requestPromise = this._executeRequest(url, config);
752
+ // Store pending promise for concurrent request handling
753
+ if (shouldUseCache) {
754
+ this._getPendingPromise(cacheKey, requestPromise);
755
+ }
756
+ try {
757
+ const result = await requestPromise;
758
+ // Cache the successful response (unless it was invalidated mid-flight).
759
+ if (shouldUseCache && result !== null && this._cacheGeneration === cacheGenerationAtStart) {
760
+ this._setCachedResponse(cacheKey, result);
761
+ }
762
+ // A successful write evicts cached reads for the same resource so the
763
+ // next GET returns fresh data (read-after-write consistency).
764
+ if (this.enableCache && this.invalidateCacheOnMutation && !_isInternalRefresh && this._isMutation(method, endpoint)) {
765
+ this._invalidateForMutation(endpoint);
766
+ }
767
+ return result;
768
+ }
769
+ finally {
770
+ // Clear pending promise
771
+ if (shouldUseCache) {
772
+ this._clearPendingPromise(cacheKey);
773
+ }
774
+ }
775
+ }
776
+ /**
777
+ * Executes the actual HTTP request.
778
+ * @param url - The complete URL to request.
779
+ * @param config - The fetch configuration.
780
+ * @returns The response data.
781
+ * @private
782
+ */
783
+ async _executeRequest(url, config) {
784
+ try {
785
+ const response = await this.fetch(url, config);
786
+ if (!response.ok) {
787
+ let errorData;
788
+ try {
789
+ errorData = await response.json();
790
+ this._log('[DEBUG] API Error Response Body:', errorData);
791
+ }
792
+ catch (error) {
793
+ errorData = { message: response.statusText };
794
+ this._log(`[DEBUG] API Error Response Text: ${response.statusText}`);
795
+ }
796
+ // A `{ error: 'maintenance' }` body on a 418/503 → the site is in a
797
+ // maintenance window (the edge rule returns exactly this to
798
+ // non-allowlisted clients). Notify the host so an already-loaded /
799
+ // cached SPA can show a maintenance message. Gating on the BODY — not
800
+ // just the status — avoids mislabeling a transient/real outage (which
801
+ // returns a different body) as planned maintenance.
802
+ //
803
+ // 418 and 503 are accepted: a Cloudflare WAF "Block" custom response can
804
+ // only be a 4xx, and 418 ("I'm a teapot") is a guaranteed-unused
805
+ // sentinel no real API path emits — so it can never collide. A Worker /
806
+ // real origin can instead return the semantically-correct 503.
807
+ if ((response.status === 418 || response.status === 503) &&
808
+ errorData?.error === 'maintenance' &&
809
+ this._onMaintenance) {
810
+ try {
811
+ this._onMaintenance();
812
+ }
813
+ catch { /* handler must never break the request path */ }
814
+ }
815
+ // Create a more informative error object
816
+ const error = new Error(errorData.message || errorData.type || `HTTP error! status: ${response.status}`);
817
+ error.status = response.status;
818
+ error.data = errorData;
819
+ throw error;
820
+ }
821
+ if (response.status === 204) {
822
+ this._log(`[DEBUG] API Response: ${response.status} No Content`);
823
+ return null; // Return null for 204 No Content
824
+ }
825
+ try {
826
+ const responseData = await response.json();
827
+ this._log(`[DEBUG] API Response: ${response.status}`, responseData);
828
+ return responseData;
829
+ }
830
+ catch (error) {
831
+ this._log(`[DEBUG] API Response OK but failed to parse as JSON: ${response.status}`, response);
832
+ return null;
833
+ }
834
+ }
835
+ catch (error) {
836
+ const apiError = error; // Assert the error type
837
+ if (!(error instanceof Error && apiError.status)) {
838
+ // Only log generic network/fetch errors here
839
+ // Specific API errors (response.ok === false) are logged above
840
+ console.error(`[ERROR] API request failed: ${config.method} ${url}`, error);
841
+ }
842
+ else {
843
+ this._log(`[DEBUG] API Error Caught: ${config.method} ${url}`, error);
844
+ }
845
+ throw error; // Re-throw the error for handling by the caller
846
+ }
847
+ }
848
+ /**
849
+ * Gets the current user's ID from the JWT token.
850
+ * @returns {string | null} The user ID if available, null otherwise.
851
+ */
852
+ getUserId() {
853
+ const token = this._getAccessToken();
854
+ if (!token)
855
+ return null;
856
+ try {
857
+ const decoded = jwtDecode(token);
858
+ return decoded?.sub || null;
859
+ }
860
+ catch (e) {
861
+ console.error('Failed to decode token:', e);
862
+ return null;
863
+ }
864
+ }
865
+ /**
866
+ * Gets the SDK version information including build timestamp and hash.
867
+ * Useful for debugging and verifying which SDK version is in use.
868
+ * @returns {SDKVersionInfo} SDK version, build timestamp, build hash, and git commit
869
+ */
870
+ getVersion() {
871
+ return SDK_VERSION;
872
+ }
873
+ /**
874
+ * Clears the response cache.
875
+ * @public
876
+ */
877
+ clearCache() {
878
+ this._cache.clear();
879
+ this._cacheGeneration++;
880
+ this._log('[DEBUG] Cache cleared');
881
+ }
882
+ /**
883
+ * Gets cache statistics for debugging.
884
+ * @returns Object containing cache statistics.
885
+ * @public
886
+ */
887
+ getCacheStats() {
888
+ const entries = Array.from(this._cache.entries()).map(([key, entry]) => ({
889
+ key,
890
+ timestamp: entry.timestamp,
891
+ hasData: entry.data !== null && entry.data !== undefined,
892
+ hasPendingPromise: !!entry.promise
893
+ }));
894
+ return {
895
+ size: this._cache.size,
896
+ entries
897
+ };
898
+ }
899
+ }
900
+ export default NuramaClient;
901
+ // Removed redundant export: export type { UserMethods, AssetMethods };
902
+ //# sourceMappingURL=NuramaClient.js.map