@h-ai/api-client 0.1.0-alpha.72 → 0.1.0-alpha.74

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
@@ -1,4 +1,4 @@
1
- import { apiContract } from '@h-ai/api-contract';
1
+ import { apiContract, IamSessionErrorCodeSchema } from '@h-ai/api-contract';
2
2
  import { core, err, ok } from '@h-ai/core';
3
3
  import { TransportTimeError, TransportBodyTooLargeError } from '@h-ai/crypto';
4
4
  import { createORPCClient, ORPCError } from '@orpc/client';
@@ -63,6 +63,7 @@ function createTokenManager(storage, refreshEndpointUrl, fetchFn, onRefreshFaile
63
63
  let refreshPromise = null;
64
64
  let refreshController = null;
65
65
  let generation = 0;
66
+ let sessionInvalid = false;
66
67
  let storageMutation = Promise.resolve();
67
68
  function mutateStorage(operation) {
68
69
  const pending = storageMutation.then(operation);
@@ -83,8 +84,10 @@ function createTokenManager(storage, refreshEndpointUrl, fetchFn, onRefreshFaile
83
84
  if (refreshGeneration !== generation)
84
85
  return;
85
86
  await storage.clear();
86
- if (refreshGeneration === generation)
87
+ if (refreshGeneration === generation) {
88
+ sessionInvalid = true;
87
89
  onRefreshFailed?.();
90
+ }
88
91
  });
89
92
  }
90
93
  async function refresh() {
@@ -111,7 +114,8 @@ function createTokenManager(storage, refreshEndpointUrl, fetchFn, onRefreshFaile
111
114
  if (refreshGeneration !== generation)
112
115
  return null;
113
116
  if (!refreshToken) {
114
- logger.warn("Token refresh skipped, no refresh token available");
117
+ sessionInvalid = true;
118
+ logger.debug("Token refresh skipped, no refresh token available");
115
119
  onRefreshFailed?.();
116
120
  return null;
117
121
  }
@@ -171,7 +175,8 @@ function createTokenManager(storage, refreshEndpointUrl, fetchFn, onRefreshFaile
171
175
  });
172
176
  if (!committed || refreshGeneration !== generation)
173
177
  return null;
174
- logger.info("Token refreshed successfully");
178
+ sessionInvalid = false;
179
+ logger.debug("Token refreshed successfully");
175
180
  for (const cb of callbacks) {
176
181
  if (refreshGeneration !== generation)
177
182
  return null;
@@ -189,9 +194,16 @@ function createTokenManager(storage, refreshEndpointUrl, fetchFn, onRefreshFaile
189
194
  }
190
195
  return {
191
196
  storage,
197
+ get generation() {
198
+ return generation;
199
+ },
200
+ get sessionInvalid() {
201
+ return sessionInvalid;
202
+ },
192
203
  refresh,
193
204
  async setTokens(tokens) {
194
205
  const previousController = invalidateRefresh();
206
+ sessionInvalid = false;
195
207
  const mutationGeneration = generation;
196
208
  const pending = mutateStorage(async () => {
197
209
  if (mutationGeneration !== generation)
@@ -286,8 +298,12 @@ var apiClientM = core.i18n.createMessageGetter({
286
298
  "en-US": en_US_default,
287
299
  "ja-JP": ja_JP_default
288
300
  });
289
-
290
- // src/api-client-iam.ts
301
+ var pageExitBindings = /* @__PURE__ */ new WeakMap();
302
+ function clearIamPageExitBindings(auth) {
303
+ for (const stop of pageExitBindings.get(auth) ?? [])
304
+ stop();
305
+ pageExitBindings.delete(auth);
306
+ }
291
307
  function createIamAuthActions(client) {
292
308
  async function save(result) {
293
309
  if (result.success)
@@ -295,15 +311,45 @@ function createIamAuthActions(client) {
295
311
  return result;
296
312
  }
297
313
  return {
314
+ bindPageExit(options = {}) {
315
+ if (!options.enabled || typeof window === "undefined")
316
+ return () => {
317
+ };
318
+ const target = window;
319
+ let sent = false;
320
+ const onPageHide = (event) => {
321
+ if (event.persisted || sent)
322
+ return;
323
+ sent = true;
324
+ void client.iam.auth.closeSession({}, { context: { keepalive: true, autoRefresh: false } }).then((result) => {
325
+ if (!result.success)
326
+ core.logger.warn("Page exit session revocation failed", { errorCode: result.error.code });
327
+ }).catch(() => core.logger.warn("Page exit session request did not complete"));
328
+ };
329
+ target.addEventListener("pagehide", onPageHide);
330
+ const bindings = pageExitBindings.get(client.auth) ?? /* @__PURE__ */ new Set();
331
+ const stop = () => {
332
+ target.removeEventListener("pagehide", onPageHide);
333
+ bindings.delete(stop);
334
+ if (!bindings.size)
335
+ pageExitBindings.delete(client.auth);
336
+ };
337
+ bindings.add(stop);
338
+ pageExitBindings.set(client.auth, bindings);
339
+ return stop;
340
+ },
298
341
  publicConfig: () => client.iam.auth.publicConfig(),
342
+ createCaptcha: (input) => client.iam.auth.createCaptcha(input),
299
343
  login: async (input) => {
300
- const result = input.method === "ldap" ? await client.iam.auth.loginWithLdap({ username: input.identifier, password: input.password }) : input.method === "email" || input.method === "sms" ? await client.iam.auth.loginWithOtp({ identifier: input.identifier, code: input.code ?? "" }) : await client.iam.auth.login({ identifier: input.identifier, password: input.password });
344
+ const result = input.method === "ldap" ? await client.iam.auth.loginWithLdap({ username: input.identifier, password: input.password, captcha: input.captcha, expectedUserId: input.expectedUserId }) : input.method === "email" || input.method === "sms" ? await client.iam.auth.loginWithOtp({ identifier: input.identifier, code: input.code ?? "", captcha: input.captcha, expectedUserId: input.expectedUserId }) : await client.iam.auth.login({ identifier: input.identifier, password: input.password, captcha: input.captcha, expectedUserId: input.expectedUserId });
301
345
  return save(result);
302
346
  },
303
347
  register: async (input) => save(await client.iam.auth.register(input)),
304
- sendOtp: (identifier) => client.iam.auth.sendOtp({ identifier }),
305
- requestPasswordReset: (identifier) => client.iam.auth.requestPasswordReset({ identifier }),
306
- confirmPasswordReset: (token, newPassword) => client.iam.auth.confirmPasswordReset({ token, newPassword })
348
+ sendOtp: (identifier, options) => client.iam.auth.sendOtp({ identifier, ...options }),
349
+ requestPasswordReset: (identifier, options) => client.iam.auth.requestPasswordReset({ identifier, ...options }),
350
+ confirmPasswordReset: (token, newPassword) => client.iam.auth.confirmPasswordReset({ token, newPassword }),
351
+ renewPassword: (input) => client.iam.auth.renewPassword(input),
352
+ reauthenticate: (password, operation) => client.iam.auth.reauthenticate({ password, operation })
307
353
  };
308
354
  }
309
355
  var ApiClientErrorInfo = {
@@ -404,6 +450,7 @@ function createApiClient(contract) {
404
450
  }
405
451
  },
406
452
  async close() {
453
+ clearIamPageExitBindings(auth);
407
454
  state.observers.clear();
408
455
  state.transport?.destroy();
409
456
  state.config = null;
@@ -541,7 +588,7 @@ function buildTokenManager(config, transport) {
541
588
  new Headers(init?.headers).forEach((value, name) => headers.set(name, value));
542
589
  return fetchFn(input, { ...init, headers });
543
590
  };
544
- const manager = createTokenManager(storage, refreshUrl, refreshFetch, config.auth.onRefreshFailed, config.auth.refreshTimeoutMs);
591
+ const manager = createTokenManager(storage, refreshUrl, refreshFetch, config.auth.recoverSession ? void 0 : config.auth.onRefreshFailed, config.auth.refreshTimeoutMs);
545
592
  if (config.auth.onTokenRefreshed) {
546
593
  manager.onTokenRefreshed(config.auth.onTokenRefreshed);
547
594
  }
@@ -559,7 +606,12 @@ function buildTransportSession(config) {
559
606
  function buildRawClient(contract, config, tokenManager, transport) {
560
607
  const link = new OpenAPILink(contract, {
561
608
  url: config.baseUrl,
562
- headers: async (options) => buildHeaders(config, tokenManager, getObservation(options.context)?.requestId),
609
+ headers: async (options) => {
610
+ const headers = await buildHeaders(config, tokenManager, getObservation(options.context)?.requestId);
611
+ if (typeof options.context.reauthentication === "string")
612
+ headers["x-hai-reauthentication"] = options.context.reauthentication;
613
+ return headers;
614
+ },
563
615
  fetch: async (request, init, options) => {
564
616
  const record = getObservation(options.context);
565
617
  if (record) {
@@ -569,7 +621,8 @@ function buildRawClient(contract, config, tokenManager, transport) {
569
621
  url.hash = "";
570
622
  record.url = core.sanitize.sanitizeUrl(url.toString());
571
623
  }
572
- const response = await fetchWithRefresh(request, init, config, tokenManager, transport, record);
624
+ const requestInit = options.context.keepalive === true ? { ...init, keepalive: true } : init;
625
+ const response = await fetchWithRefresh(request, requestInit, config, options.context.autoRefresh === false ? void 0 : tokenManager, transport, record);
573
626
  if (record) {
574
627
  record.status = response.status;
575
628
  const traceId = response.headers.get("x-trace-id") ?? response.headers.get("x-request-id");
@@ -588,6 +641,7 @@ function getObservation(context) {
588
641
  async function buildHeaders(config, tokenManager, requestId) {
589
642
  const configured = typeof config.headers === "function" ? await config.headers() : config.headers ?? {};
590
643
  const headers = new Headers(configured);
644
+ headers.set("x-hai-terminal", config.auth?.terminalType ?? "web");
591
645
  if (!headers.has("accept-language"))
592
646
  headers.set("accept-language", core.i18n.getGlobalLocale());
593
647
  const accessToken = await tokenManager?.storage.getAccessToken();
@@ -600,6 +654,7 @@ async function buildHeaders(config, tokenManager, requestId) {
600
654
  }
601
655
  async function fetchWithRefresh(request, init, config, tokenManager, transport, observation) {
602
656
  const retryRequest = tokenManager ? request.clone() : void 0;
657
+ const identityGeneration = tokenManager?.generation;
603
658
  if (observation)
604
659
  observation.attempts++;
605
660
  const response = await fetchWithTimeout(request, init, config, transport);
@@ -612,11 +667,28 @@ async function fetchWithRefresh(request, init, config, tokenManager, transport,
612
667
  return response;
613
668
  if (response.status !== 401 && !await isUnauthorizedResult(response))
614
669
  return response;
670
+ const failure = await response.clone().json().catch(() => void 0);
671
+ if (failure && typeof failure === "object" && "error" in failure && failure.error && typeof failure.error === "object" && "code" in failure.error && typeof failure.error.code === "string" && failure.error.code.startsWith("hai:iam:") && !IamSessionErrorCodeSchema.safeParse(failure.error.code).success) {
672
+ return response;
673
+ }
674
+ if (tokenManager.generation !== identityGeneration)
675
+ return response;
615
676
  const tokens = await tokenManager.refresh();
616
- if (!tokens)
677
+ if (tokenManager.generation !== identityGeneration)
678
+ return response;
679
+ let accessToken = tokens?.accessToken;
680
+ if (!accessToken && config.auth?.recoverSession && tokenManager.sessionInvalid) {
681
+ if (await config.auth.recoverSession()) {
682
+ const restoredGeneration = tokenManager.generation;
683
+ accessToken = await tokenManager.storage.getAccessToken() ?? void 0;
684
+ if (tokenManager.generation !== restoredGeneration)
685
+ return response;
686
+ }
687
+ }
688
+ if (!accessToken)
617
689
  return response;
618
690
  const headers = new Headers(retryRequest.headers);
619
- headers.set("authorization", `Bearer ${tokens.accessToken}`);
691
+ headers.set("authorization", `Bearer ${accessToken}`);
620
692
  if (observation)
621
693
  observation.attempts++;
622
694
  return fetchWithTimeout(new Request(retryRequest, { headers }), init, config, transport);