@iblai/data-layer 1.13.0 → 1.13.2

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
@@ -39144,6 +39144,12 @@ Config.axdUrl = 'https://base.manager.iblai.app';
39144
39144
  Config.mentorIframeUrl = 'https://mentor.iblai.tech';
39145
39145
  Config.studioUrl = 'https://studio.iblai.app';
39146
39146
  Config.httpErrorHandlers = {};
39147
+ /**
39148
+ * Optional override for how expired auth tokens are re-minted after a 401.
39149
+ * Resolve `true` once fresh tokens are in storage. Left unset, the data
39150
+ * layer mints them itself from the edX session.
39151
+ */
39152
+ Config.refreshAuthToken = undefined;
39147
39153
 
39148
39154
  class IblDataLayer {
39149
39155
  }
@@ -39198,6 +39204,133 @@ const iblFakeBaseQuery = react.retry(fakeBaseQueryWithLogging, {
39198
39204
  },
39199
39205
  });
39200
39206
 
39207
+ const AUTH_ENDPOINTS = {
39208
+ GET_MFE_CONTEXT: {
39209
+ service: exports.SERVICES.LMS,
39210
+ path: () => `/api/mfe_context`,
39211
+ },
39212
+ APPLE_EXCHANGE_TOKEN: {
39213
+ service: exports.SERVICES.LMS,
39214
+ path: () => `/oauth2/exchange_access_token/apple-id/`,
39215
+ },
39216
+ GET_APP_TOKENS: {
39217
+ service: exports.SERVICES.LMS,
39218
+ path: () => '/api/ibl/manager/consolidated-token/proxy/',
39219
+ },
39220
+ REFRESH_JWT_TOKEN: {
39221
+ service: exports.SERVICES.LEGACY_LMS,
39222
+ path: () => '/ibl-auth/request-jwt/',
39223
+ },
39224
+ };
39225
+ const AUTH_REDUCER_PATH = 'authApiSlice';
39226
+
39227
+ /**
39228
+ * Expiry keys are written alongside the tokens so `hasNonExpiredAuthToken()`
39229
+ * and friends don't treat a freshly minted token as stale.
39230
+ */
39231
+ const AXD_TOKEN_EXPIRES_KEY = 'axd_token_expires';
39232
+ const DM_TOKEN_EXPIRES_KEY = 'dm_token_expires';
39233
+ /** Reads the platform key the tokens should be minted for. */
39234
+ const readPlatformKey = async () => {
39235
+ var _a;
39236
+ const storage = IblDataLayer.storage;
39237
+ if (!storage)
39238
+ return null;
39239
+ const raw = await storage.getItem(STORAGE_KEYS.CURRENT_TENANT);
39240
+ if (raw) {
39241
+ try {
39242
+ const key = (_a = JSON.parse(raw)) === null || _a === void 0 ? void 0 : _a.key;
39243
+ if (key)
39244
+ return key;
39245
+ }
39246
+ catch (_b) {
39247
+ // `current_tenant` is occasionally stored as a bare slug rather than the
39248
+ // serialised tenant object; use it as-is.
39249
+ if (raw.trim())
39250
+ return raw;
39251
+ }
39252
+ }
39253
+ return (await storage.getItem(STORAGE_KEYS.TENANT)) || null;
39254
+ };
39255
+ /**
39256
+ * Mints a fresh set of app tokens from the edX session and writes them to
39257
+ * storage. Deliberately uses `fetch` rather than the data layer's own base
39258
+ * query: routing it through `iblFetchBaseQuery` would re-enter the 401
39259
+ * handling this function exists to serve.
39260
+ */
39261
+ const defaultRefreshAuthToken = async () => {
39262
+ var _a, _b;
39263
+ const storage = IblDataLayer.storage;
39264
+ if (!storage)
39265
+ return false;
39266
+ const platformKey = await readPlatformKey();
39267
+ if (!platformKey) {
39268
+ console.warn('[token-refresh] No platform key in storage, cannot refresh');
39269
+ return false;
39270
+ }
39271
+ // The consolidated-token endpoint authenticates with the edX JWT, not with
39272
+ // the axd/dm tokens that just expired.
39273
+ const authHeaders = await getHeaders(AUTH_ENDPOINTS.GET_APP_TOKENS.service);
39274
+ if (!(authHeaders === null || authHeaders === void 0 ? void 0 : authHeaders.Authorization)) {
39275
+ console.warn('[token-refresh] No edX token available, cannot refresh');
39276
+ return false;
39277
+ }
39278
+ const formData = new FormData();
39279
+ formData.append('platform_key', platformKey);
39280
+ const url = `${getServiceUrl(AUTH_ENDPOINTS.GET_APP_TOKENS.service)}${AUTH_ENDPOINTS.GET_APP_TOKENS.path()}`;
39281
+ const response = await fetch(url, {
39282
+ method: 'POST',
39283
+ headers: { Authorization: authHeaders.Authorization },
39284
+ body: formData,
39285
+ });
39286
+ if (!response.ok) {
39287
+ console.warn(`[token-refresh] Mint failed with ${response.status}`);
39288
+ return false;
39289
+ }
39290
+ const body = (await response.json());
39291
+ const axdToken = (_a = body === null || body === void 0 ? void 0 : body.data) === null || _a === void 0 ? void 0 : _a.axd_token;
39292
+ const dmToken = (_b = body === null || body === void 0 ? void 0 : body.data) === null || _b === void 0 ? void 0 : _b.dm_token;
39293
+ if (!(axdToken === null || axdToken === void 0 ? void 0 : axdToken.token) || !(dmToken === null || dmToken === void 0 ? void 0 : dmToken.token)) {
39294
+ console.warn('[token-refresh] Mint returned no tokens');
39295
+ return false;
39296
+ }
39297
+ await storage.setItem(STORAGE_KEYS.AXD_TOKEN_KEY, axdToken.token);
39298
+ await storage.setItem(STORAGE_KEYS.DM_TOKEN_KEY, dmToken.token);
39299
+ if (axdToken.expires) {
39300
+ await storage.setItem(AXD_TOKEN_EXPIRES_KEY, axdToken.expires);
39301
+ }
39302
+ if (dmToken.expires) {
39303
+ await storage.setItem(DM_TOKEN_EXPIRES_KEY, dmToken.expires);
39304
+ }
39305
+ return true;
39306
+ };
39307
+ /** The refresh currently in flight, if any. */
39308
+ let refreshInFlight = null;
39309
+ /**
39310
+ * Refreshes the auth tokens, collapsing concurrent callers onto one request.
39311
+ * A page load that fires ten requests in parallel produces ten 401s; without
39312
+ * this they would mint ten token sets and race each other into storage.
39313
+ *
39314
+ * Never rejects — a thrown refresh resolves `false`, which callers read as
39315
+ * "give up and run the registered 401 handler" (i.e. log out).
39316
+ */
39317
+ const refreshAuthTokenOnce = async () => {
39318
+ var _a;
39319
+ if (!refreshInFlight) {
39320
+ const refresh = (_a = Config.refreshAuthToken) !== null && _a !== void 0 ? _a : defaultRefreshAuthToken;
39321
+ refreshInFlight = Promise.resolve()
39322
+ .then(() => refresh())
39323
+ .catch((error) => {
39324
+ console.error('[token-refresh] Refresh threw:', error);
39325
+ return false;
39326
+ })
39327
+ .finally(() => {
39328
+ refreshInFlight = null;
39329
+ });
39330
+ }
39331
+ return refreshInFlight;
39332
+ };
39333
+
39201
39334
  /**
39202
39335
  * Get the service URL based on the provided service type.
39203
39336
  */
@@ -39292,6 +39425,7 @@ const buildEndpointFromService = (service, serviceFn, options) => {
39292
39425
  async queryFn(args) {
39293
39426
  var _a;
39294
39427
  let lastError;
39428
+ let refreshAttempted = false;
39295
39429
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
39296
39430
  try {
39297
39431
  OpenAPI.BASE = getServiceUrl(service);
@@ -39303,6 +39437,17 @@ const buildEndpointFromService = (service, serviceFn, options) => {
39303
39437
  catch (err) {
39304
39438
  lastError = err;
39305
39439
  const status = err === null || err === void 0 ? void 0 : err.status;
39440
+ // An expired token is recoverable: mint a new one and replay the
39441
+ // call (headers are rebuilt at the top of the loop) rather than
39442
+ // dropping straight to the 401 handler, which logs the user out.
39443
+ // One attempt per call, so a token the server keeps rejecting can't
39444
+ // loop.
39445
+ if (status === 401 && !refreshAttempted) {
39446
+ refreshAttempted = true;
39447
+ if (await refreshAuthTokenOnce()) {
39448
+ continue;
39449
+ }
39450
+ }
39306
39451
  // Don't retry auth errors, statuses the endpoint expects as an
39307
39452
  // empty state, or a spend-cap block (fail fast — no retry storm).
39308
39453
  if (QUERYFN_NON_RETRYABLE_STATUSES.includes(status) ||
@@ -39457,10 +39602,23 @@ const withNonRetryableErrors = (query$1) => async (args, api, extraOptions) => {
39457
39602
  const iblFetchBaseQuery = async (args, api, extraOptions) => {
39458
39603
  var _a, _b, _c;
39459
39604
  try {
39605
+ const runQuery = () => {
39606
+ var _a;
39607
+ return query.retry(withNonRetryableErrors(baseQuery(args.service, (_a = args === null || args === void 0 ? void 0 : args.isJson) !== null && _a !== void 0 ? _a : true, args === null || args === void 0 ? void 0 : args.contentType, args === null || args === void 0 ? void 0 : args.skipAuth)), {
39608
+ maxRetries: 3,
39609
+ })(args, api, extraOptions);
39610
+ };
39460
39611
  // Executing base query
39461
- const result = await query.retry(withNonRetryableErrors(baseQuery(args.service, (_a = args === null || args === void 0 ? void 0 : args.isJson) !== null && _a !== void 0 ? _a : true, args === null || args === void 0 ? void 0 : args.contentType, args === null || args === void 0 ? void 0 : args.skipAuth)), {
39462
- maxRetries: 3,
39463
- })(args, api, extraOptions);
39612
+ let result = await runQuery();
39613
+ // An expired token is recoverable: mint a new one and replay the request
39614
+ // (`prepareHeaders` re-reads storage per call) rather than dropping
39615
+ // straight to the 401 handler, which logs the user out. One attempt per
39616
+ // request, so a token the server keeps rejecting can't loop.
39617
+ if (((_a = result.error) === null || _a === void 0 ? void 0 : _a.status) === 401 && !(args === null || args === void 0 ? void 0 : args.skipAuth)) {
39618
+ if (await refreshAuthTokenOnce()) {
39619
+ result = await runQuery();
39620
+ }
39621
+ }
39464
39622
  // Query completed
39465
39623
  if (result.error) {
39466
39624
  console.log('[MONETIZATION ERROR HANDLER]: ', { result });
@@ -39534,6 +39692,7 @@ const buildEndpointFromServiceLegacy = (service, serviceFn, options) => {
39534
39692
  return {
39535
39693
  async queryFn(args) {
39536
39694
  let lastError;
39695
+ let refreshAttempted = false;
39537
39696
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
39538
39697
  try {
39539
39698
  OpenAPI.BASE = getServiceUrl(service);
@@ -39545,6 +39704,14 @@ const buildEndpointFromServiceLegacy = (service, serviceFn, options) => {
39545
39704
  catch (err) {
39546
39705
  lastError = err;
39547
39706
  const status = err === null || err === void 0 ? void 0 : err.status;
39707
+ // See the note in `buildEndpointFromService`: refresh once and
39708
+ // replay before letting a 401 reach the logout handler.
39709
+ if (status === 401 && !refreshAttempted) {
39710
+ refreshAttempted = true;
39711
+ if (await refreshAuthTokenOnce()) {
39712
+ continue;
39713
+ }
39714
+ }
39548
39715
  if (QUERYFN_NON_RETRYABLE_STATUSES.includes(status) ||
39549
39716
  expectedErrorStatuses.includes(status)) {
39550
39717
  break;
@@ -40229,26 +40396,6 @@ const promptsApiSlice = react.createApi({
40229
40396
  });
40230
40397
  const { useGetPromptCategoriesQuery, useLazyGetPromptCategoriesQuery, useGetPromptsQuery, useLazyGetPromptsQuery, useGetPromptsSearchQuery, useLazyGetPromptsSearchQuery, useCreatePromptMutation, useUpdatePromptMutation, useDeletePromptMutation, useGetGuidedPromptsQuery, useLazyGetGuidedPromptsQuery, } = promptsApiSlice;
40231
40398
 
40232
- const AUTH_ENDPOINTS = {
40233
- GET_MFE_CONTEXT: {
40234
- service: exports.SERVICES.LMS,
40235
- path: () => `/api/mfe_context`,
40236
- },
40237
- APPLE_EXCHANGE_TOKEN: {
40238
- service: exports.SERVICES.LMS,
40239
- path: () => `/oauth2/exchange_access_token/apple-id/`,
40240
- },
40241
- GET_APP_TOKENS: {
40242
- service: exports.SERVICES.LMS,
40243
- path: () => '/api/ibl/manager/consolidated-token/proxy/',
40244
- },
40245
- REFRESH_JWT_TOKEN: {
40246
- service: exports.SERVICES.LEGACY_LMS,
40247
- path: () => '/ibl-auth/request-jwt/',
40248
- },
40249
- };
40250
- const AUTH_REDUCER_PATH = 'authApiSlice';
40251
-
40252
40399
  const authApiSlice = react.createApi({
40253
40400
  reducerPath: AUTH_REDUCER_PATH,
40254
40401
  baseQuery: iblFetchBaseQuery,
@@ -48208,7 +48355,7 @@ const skillsMiddleware = [
48208
48355
  userChatHistoryApiSlice.middleware,
48209
48356
  ];
48210
48357
 
48211
- const initializeDataLayer = (dmUrl, lmsUrl, legacyLmsUrl, storageService, httpErrorHandler = {}, studioUrl) => {
48358
+ const initializeDataLayer = (dmUrl, lmsUrl, legacyLmsUrl, storageService, httpErrorHandler = {}, studioUrl, refreshAuthToken) => {
48212
48359
  IblDataLayer.storage = storageService;
48213
48360
  Config.dmUrl = dmUrl;
48214
48361
  Config.lmsUrl = lmsUrl;
@@ -48217,6 +48364,7 @@ const initializeDataLayer = (dmUrl, lmsUrl, legacyLmsUrl, storageService, httpEr
48217
48364
  if (studioUrl) {
48218
48365
  Config.studioUrl = studioUrl;
48219
48366
  }
48367
+ Config.refreshAuthToken = refreshAuthToken;
48220
48368
  };
48221
48369
 
48222
48370
  exports.AGENT_SKILLS_ENDPOINTS = AGENT_SKILLS_ENDPOINTS;