@intlayer/api 9.5.10 → 9.5.12

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.
@@ -6,6 +6,40 @@ let defu = require("defu");
6
6
 
7
7
  //#region src/cms/createAuthManager.ts
8
8
  const ONE_MINUTE_MS = 6e4;
9
+ /**
10
+ * Process-wide token states, keyed by backend URL and credentials. Build
11
+ * steps and watch rebuilds each create their own manager; sharing the state
12
+ * lets them reuse one token instead of awaiting a new exchange every time.
13
+ */
14
+ const accessTokenStates = /* @__PURE__ */ new Map();
15
+ /** Clears every cached access token, forcing the next call to re-authenticate. */
16
+ const clearAccessTokenCache = () => {
17
+ accessTokenStates.clear();
18
+ };
19
+ /** Returns the shared token state for the given credentials, creating it if missing. */
20
+ const getAccessTokenState = (editorConfig) => {
21
+ const stateKey = [
22
+ editorConfig?.backendURL,
23
+ editorConfig?.clientId,
24
+ editorConfig?.clientSecret
25
+ ].join("\0");
26
+ let accessTokenState = accessTokenStates.get(stateKey);
27
+ if (!accessTokenState) {
28
+ accessTokenState = {};
29
+ accessTokenStates.set(stateKey, accessTokenState);
30
+ }
31
+ return accessTokenState;
32
+ };
33
+ /** HTTP statuses meaning the access token was rejected. */
34
+ const AUTHENTICATION_ERROR_STATUSES = /* @__PURE__ */ new Set([401, 403]);
35
+ /**
36
+ * Whether a failed call may succeed with a fresh token: an auth rejection, or
37
+ * an error without HTTP status. A `404` or `500` would fail again regardless.
38
+ */
39
+ const isAuthenticationError = (error) => {
40
+ const status = error?.status;
41
+ return typeof status !== "number" || AUTHENTICATION_ERROR_STATUSES.has(status);
42
+ };
9
43
  /** Whether the current runtime is a browser rather than a server. */
10
44
  const isBrowser = () => typeof window !== "undefined";
11
45
  /**
@@ -26,8 +60,8 @@ const getExpiryTimestamp = (token) => {
26
60
  * Creates an instance-scoped {@link AuthManager} handling the OAuth2
27
61
  * `client_credentials` flow (or a pre-issued CLI session token).
28
62
  *
29
- * Unlike a global token cache, each manager keeps its own token state, so
30
- * independent SDK instances never interfere with one another.
63
+ * Managers built with the same backend URL and credentials share one token
64
+ * state, so a process never exchanges credentials more than needed.
31
65
  *
32
66
  * @param intlayerConfig - Intlayer configuration carrying the `editor`
33
67
  * credentials. Defaults to the build-time configuration.
@@ -48,10 +82,9 @@ const createAuthManager = (intlayerConfig = { editor: _intlayer_config_built.edi
48
82
  ...fetcherOptions.headers ?? {},
49
83
  Authorization: `Bearer ${sessionToken}`
50
84
  };
51
- let accessToken;
52
- let expiryTimestamp;
53
- let pendingRefresh;
85
+ const accessTokenState = getAccessTokenState(resolvedConfig?.editor);
54
86
  const needsRefresh = () => {
87
+ const { accessToken, expiryTimestamp } = accessTokenState;
55
88
  if (!accessToken) return true;
56
89
  if (!expiryTimestamp) return false;
57
90
  return Date.now() + ONE_MINUTE_MS >= expiryTimestamp;
@@ -59,19 +92,19 @@ const createAuthManager = (intlayerConfig = { editor: _intlayer_config_built.edi
59
92
  const refreshToken = async () => {
60
93
  const doRefresh = async () => {
61
94
  const tokenData = (await require_getIntlayerAPI_oAuth.getOAuthAPI({}, resolvedConfig).getOAuth2AccessToken())?.data;
62
- accessToken = tokenData?.accessToken;
63
- expiryTimestamp = getExpiryTimestamp(tokenData);
95
+ accessTokenState.accessToken = tokenData?.accessToken;
96
+ accessTokenState.expiryTimestamp = getExpiryTimestamp(tokenData);
64
97
  };
65
- pendingRefresh ??= doRefresh().finally(() => {
66
- pendingRefresh = void 0;
98
+ accessTokenState.pendingRefresh ??= doRefresh().finally(() => {
99
+ accessTokenState.pendingRefresh = void 0;
67
100
  });
68
- await pendingRefresh;
101
+ await accessTokenState.pendingRefresh;
69
102
  };
70
103
  const applyAuthHeader = () => {
71
- if (!accessToken) return;
104
+ if (!accessTokenState.accessToken) return;
72
105
  fetcherOptions.headers = {
73
106
  ...fetcherOptions.headers ?? {},
74
- Authorization: `Bearer ${accessToken}`
107
+ Authorization: `Bearer ${accessTokenState.accessToken}`
75
108
  };
76
109
  };
77
110
  const ensureAuth = async () => {
@@ -80,8 +113,8 @@ const createAuthManager = (intlayerConfig = { editor: _intlayer_config_built.edi
80
113
  applyAuthHeader();
81
114
  };
82
115
  const reset = () => {
83
- accessToken = void 0;
84
- expiryTimestamp = void 0;
116
+ accessTokenState.accessToken = void 0;
117
+ accessTokenState.expiryTimestamp = void 0;
85
118
  };
86
119
  const withAuth = (method, skipAuth = !hasCredentials) => {
87
120
  if (skipAuth && !sessionToken && isBrowser()) return async () => {
@@ -93,7 +126,7 @@ const createAuthManager = (intlayerConfig = { editor: _intlayer_config_built.edi
93
126
  try {
94
127
  return await method(...args);
95
128
  } catch (error) {
96
- if (sessionToken) throw error;
129
+ if (sessionToken || !isAuthenticationError(error)) throw error;
97
130
  await refreshToken();
98
131
  applyAuthHeader();
99
132
  return await method(...args);
@@ -121,5 +154,6 @@ const createAuthManager = (intlayerConfig = { editor: _intlayer_config_built.edi
121
154
  };
122
155
 
123
156
  //#endregion
157
+ exports.clearAccessTokenCache = clearAccessTokenCache;
124
158
  exports.createAuthManager = createAuthManager;
125
159
  //# sourceMappingURL=createAuthManager.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"createAuthManager.cjs","names":["editor","getEditorClientSecret","getOAuthAPI"],"sources":["../../../src/cms/createAuthManager.ts"],"sourcesContent":["import { editor } from '@intlayer/config/built';\nimport { getEditorClientSecret } from '@intlayer/config/secrets';\nimport type { IntlayerConfig } from '@intlayer/types/config';\nimport { defu } from 'defu';\nimport type { FetcherOptions } from '../fetcher';\nimport { getOAuthAPI } from '../getIntlayerAPI/oAuth';\n\n/**\n * Shape of the various OAuth2 token payloads the backend may return. Only the\n * fields used to derive the expiration timestamp are described here.\n */\ntype OAuthTokenLike = {\n accessToken?: string;\n accessTokenExpiresAt?: string | Date;\n expires_in?: number;\n expiresIn?: number;\n expiresAt?: string | Date;\n};\n\n/**\n * Any asynchronous API method exposed by a domain section\n * (e.g. `getDictionaries`, `pushDictionaries`).\n */\ntype AsyncMethod<Arguments extends unknown[], Result> = (\n ...args: Arguments\n) => Promise<Result>;\n\n/**\n * A domain section factory: builds a plain object of async methods from shared\n * fetcher options and the Intlayer configuration.\n */\ntype SectionFactory<Section extends Record<string, unknown>> = (\n fetcherOptions: FetcherOptions,\n intlayerConfig: IntlayerConfig\n) => Section;\n\nconst ONE_MINUTE_MS = 60_000;\n\n/** Whether the current runtime is a browser rather than a server. */\nconst isBrowser = (): boolean => typeof window !== 'undefined';\n\n/**\n * Resolves the expiration timestamp (in ms since epoch) from an OAuth2\n * token-like payload, supporting both absolute dates and relative durations.\n */\nconst getExpiryTimestamp = (\n token: OAuthTokenLike | undefined\n): number | undefined => {\n if (!token) return undefined;\n\n const dateLike = (token.accessTokenExpiresAt ?? token.expiresAt) as\n | string\n | Date\n | undefined;\n\n if (dateLike) {\n const timestamp =\n typeof dateLike === 'string'\n ? Date.parse(dateLike)\n : dateLike.getTime?.();\n if (typeof timestamp === 'number' && Number.isFinite(timestamp)) {\n return timestamp;\n }\n }\n\n const seconds = token.expires_in ?? token.expiresIn;\n if (typeof seconds === 'number' && Number.isFinite(seconds)) {\n return Date.now() + seconds * 1000;\n }\n\n return undefined;\n};\n\n/**\n * Options accepted when creating an {@link AuthManager}.\n */\nexport type AuthManagerOptions = {\n /**\n * Base fetcher options merged into every request (e.g. custom headers).\n */\n baseFetcherOptions?: FetcherOptions;\n /**\n * A pre-issued CLI session token (prefixed with `clisession_`). When provided,\n * it is used directly as the Bearer token and the OAuth2 `client_credentials`\n * exchange is skipped entirely.\n */\n sessionToken?: string;\n};\n\n/**\n * Options accepted when binding a domain section to an {@link AuthManager}.\n */\nexport type BindSectionOptions = {\n /**\n * Forces auth injection on (`false`) or off (`true`). Defaults to enabled\n * whenever credentials are available.\n */\n skipAuth?: boolean;\n};\n\n/**\n * Manages CMS authentication for a set of API calls.\n *\n * The manager owns a single mutable {@link FetcherOptions} object\n * (`fetcherOptions`). Domain factories bound through it share the same\n * reference, so refreshing the token in place transparently authenticates every\n * call — without rebuilding the API clients.\n */\nexport type AuthManager = {\n /**\n * Mutable fetcher options carrying the `Authorization` header. Bound sections\n * share this reference so the injected token is reused everywhere.\n */\n fetcherOptions: FetcherOptions;\n /**\n * The resolved Intlayer configuration (merged with the build-time defaults).\n */\n intlayerConfig: IntlayerConfig;\n /**\n * `true` when credentials are available — either a session token or a\n * `clientId`/`clientSecret` pair in the configuration.\n */\n hasCredentials: boolean;\n /**\n * Ensures a valid access token is available and applies it to\n * `fetcherOptions`. Called automatically by {@link withAuth}.\n */\n ensureAuth: () => Promise<void>;\n /**\n * Forces a token refresh on the next call by clearing the cached token.\n */\n reset: () => void;\n /**\n * Wraps a single API method so it injects (and, on failure, refreshes) the\n * access token on every invocation while preserving the method's signature.\n */\n withAuth: <Arguments extends unknown[], Result>(\n method: AsyncMethod<Arguments, Result>,\n skipAuth?: boolean\n ) => AsyncMethod<Arguments, Result>;\n /**\n * Instantiates a domain factory with the managed fetcher options and config,\n * returning a section whose every method authenticates automatically.\n *\n * @example\n * ```ts\n * const dictionary = authManager.bindSection(getDictionaryAPI);\n * await dictionary.pushDictionaries(dictionaries);\n * ```\n */\n bindSection: <Section extends Record<string, unknown>>(\n factory: SectionFactory<Section>,\n options?: BindSectionOptions\n ) => Section;\n};\n\n/**\n * Creates an instance-scoped {@link AuthManager} handling the OAuth2\n * `client_credentials` flow (or a pre-issued CLI session token).\n *\n * Unlike a global token cache, each manager keeps its own token state, so\n * independent SDK instances never interfere with one another.\n *\n * @param intlayerConfig - Intlayer configuration carrying the `editor`\n * credentials. Defaults to the build-time configuration.\n * @param options - Optional session token and base fetcher options.\n */\nexport const createAuthManager = (\n intlayerConfig: Pick<IntlayerConfig, 'editor'> = { editor },\n options: AuthManagerOptions = {}\n): AuthManager => {\n const { baseFetcherOptions = {}, sessionToken } = options;\n\n // `editor` comes from the generated build-time configuration, which never\n // carries `clientSecret` — it would be inlined into client bundles. The\n // secret is supplied server-side by `@intlayer/config/secrets`, which\n // resolves to an inert stub in the browser; `hasCredentials` then stays false\n // there and the OAuth2 exchange never runs.\n //\n // Only consulted when the configuration declares a `clientId`, so a project\n // that comments its credentials out stays uncredentialed even with the\n // environment variables still defined.\n const resolvedConfig = defu(intlayerConfig ?? {}, {\n editor: {\n ...editor,\n clientSecret: editor?.clientId ? getEditorClientSecret() : undefined,\n },\n }) as IntlayerConfig;\n\n // `credentials: 'omit'` prevents the browser from attaching session cookies;\n // authentication is handled exclusively through the injected Bearer token.\n // This is required because the backend only allows credentialed requests for\n // whitelisted first-party origins.\n const fetcherOptions: FetcherOptions = {\n ...baseFetcherOptions,\n credentials: 'omit',\n };\n\n const hasCredentials =\n Boolean(sessionToken) ||\n Boolean(\n resolvedConfig?.editor?.clientId && resolvedConfig?.editor?.clientSecret\n );\n\n // A session token is used as-is and never expires within the SDK's concern,\n // so inject it once up-front and skip the OAuth2 exchange.\n if (sessionToken) {\n fetcherOptions.headers = {\n ...(fetcherOptions.headers ?? {}),\n Authorization: `Bearer ${sessionToken}`,\n } as HeadersInit;\n }\n\n let accessToken: string | undefined;\n let expiryTimestamp: number | undefined;\n let pendingRefresh: Promise<void> | undefined;\n\n const needsRefresh = (): boolean => {\n if (!accessToken) return true;\n if (!expiryTimestamp) return false; // Unknown expiry: assume usable until failure.\n\n // Refresh one minute before the actual expiry to avoid edge-of-expiry races.\n return Date.now() + ONE_MINUTE_MS >= expiryTimestamp;\n };\n\n const refreshToken = async (): Promise<void> => {\n const doRefresh = async (): Promise<void> => {\n const oAuthAPI = getOAuthAPI({}, resolvedConfig);\n const response = await oAuthAPI.getOAuth2AccessToken();\n const tokenData = response?.data as OAuthTokenLike | undefined;\n\n accessToken = tokenData?.accessToken;\n expiryTimestamp = getExpiryTimestamp(tokenData);\n };\n\n // De-duplicate concurrent refreshes so a burst of calls triggers one fetch.\n pendingRefresh ??= doRefresh().finally(() => {\n pendingRefresh = undefined;\n });\n\n await pendingRefresh;\n };\n\n const applyAuthHeader = (): void => {\n if (!accessToken) return;\n fetcherOptions.headers = {\n ...(fetcherOptions.headers ?? {}),\n Authorization: `Bearer ${accessToken}`,\n } as HeadersInit;\n };\n\n const ensureAuth = async (): Promise<void> => {\n if (sessionToken) return; // Already injected; nothing to refresh.\n if (needsRefresh()) {\n await refreshToken();\n }\n applyAuthHeader();\n };\n\n const reset = (): void => {\n accessToken = undefined;\n expiryTimestamp = undefined;\n };\n\n const withAuth = <Arguments extends unknown[], Result>(\n method: AsyncMethod<Arguments, Result>,\n skipAuth = !hasCredentials\n ): AsyncMethod<Arguments, Result> => {\n // In a browser the project credentials are structurally unavailable, so an\n // authenticated call would silently go out unauthenticated and come back\n // 401. Fail with an actionable message instead: a browser must authenticate\n // as the signed-in *user* (a session token, or the session cookie on a\n // first-party origin), never as the project.\n if (skipAuth && !sessionToken && isBrowser()) {\n return async () => {\n throw new Error(\n 'Intlayer CMS credentials are not available in the browser. ' +\n '`clientSecret` is a server-side credential and is never bundled. ' +\n 'Call this from a server route, or pass a user session token: ' +\n 'createIntlayerCMS(config, { sessionToken }).'\n );\n };\n }\n\n if (skipAuth) return method;\n\n return async (...args: Arguments): Promise<Result> => {\n await ensureAuth();\n\n try {\n return await method(...args);\n } catch (error) {\n // The token may have been revoked or expired early: refresh once and\n // retry before surfacing the error. A session token cannot be\n // refreshed, so re-throw immediately in that case.\n if (sessionToken) throw error;\n\n await refreshToken();\n applyAuthHeader();\n return await method(...args);\n }\n };\n };\n\n const bindSection = <Section extends Record<string, unknown>>(\n factory: SectionFactory<Section>,\n bindOptions: BindSectionOptions = {}\n ): Section => {\n const section = factory(fetcherOptions, resolvedConfig);\n const skipAuth = bindOptions.skipAuth ?? !hasCredentials;\n\n return new Proxy(section, {\n get(target, property, receiver) {\n const value = Reflect.get(target, property, receiver);\n\n if (typeof value !== 'function') return value;\n\n return withAuth(\n (value as AsyncMethod<unknown[], unknown>).bind(target),\n skipAuth\n );\n },\n });\n };\n\n return {\n fetcherOptions,\n intlayerConfig: resolvedConfig,\n hasCredentials,\n ensureAuth,\n reset,\n withAuth,\n bindSection,\n };\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"createAuthManager.cjs","names":["editor","getEditorClientSecret","getOAuthAPI"],"sources":["../../../src/cms/createAuthManager.ts"],"sourcesContent":["import { editor } from '@intlayer/config/built';\nimport { getEditorClientSecret } from '@intlayer/config/secrets';\nimport type { IntlayerConfig } from '@intlayer/types/config';\nimport { defu } from 'defu';\nimport type { FetcherOptions } from '../fetcher';\nimport { getOAuthAPI } from '../getIntlayerAPI/oAuth';\n\n/**\n * Shape of the various OAuth2 token payloads the backend may return. Only the\n * fields used to derive the expiration timestamp are described here.\n */\ntype OAuthTokenLike = {\n accessToken?: string;\n accessTokenExpiresAt?: string | Date;\n expires_in?: number;\n expiresIn?: number;\n expiresAt?: string | Date;\n};\n\n/**\n * Any asynchronous API method exposed by a domain section\n * (e.g. `getDictionaries`, `pushDictionaries`).\n */\ntype AsyncMethod<Arguments extends unknown[], Result> = (\n ...args: Arguments\n) => Promise<Result>;\n\n/**\n * A domain section factory: builds a plain object of async methods from shared\n * fetcher options and the Intlayer configuration.\n */\ntype SectionFactory<Section extends Record<string, unknown>> = (\n fetcherOptions: FetcherOptions,\n intlayerConfig: IntlayerConfig\n) => Section;\n\nconst ONE_MINUTE_MS = 60_000;\n\n/**\n * OAuth2 token state for one set of credentials, shared by every manager\n * built with them.\n */\ntype AccessTokenState = {\n accessToken?: string;\n expiryTimestamp?: number;\n pendingRefresh?: Promise<void>;\n};\n\n/**\n * Process-wide token states, keyed by backend URL and credentials. Build\n * steps and watch rebuilds each create their own manager; sharing the state\n * lets them reuse one token instead of awaiting a new exchange every time.\n */\nconst accessTokenStates = new Map<string, AccessTokenState>();\n\n/** Clears every cached access token, forcing the next call to re-authenticate. */\nexport const clearAccessTokenCache = (): void => {\n accessTokenStates.clear();\n};\n\n/** Returns the shared token state for the given credentials, creating it if missing. */\nconst getAccessTokenState = (\n editorConfig: IntlayerConfig['editor'] | undefined\n): AccessTokenState => {\n const stateKey = [\n editorConfig?.backendURL,\n editorConfig?.clientId,\n editorConfig?.clientSecret,\n ].join('\\0');\n\n let accessTokenState = accessTokenStates.get(stateKey);\n\n if (!accessTokenState) {\n accessTokenState = {};\n accessTokenStates.set(stateKey, accessTokenState);\n }\n\n return accessTokenState;\n};\n\n/** HTTP statuses meaning the access token was rejected. */\nconst AUTHENTICATION_ERROR_STATUSES = new Set([401, 403]);\n\n/**\n * Whether a failed call may succeed with a fresh token: an auth rejection, or\n * an error without HTTP status. A `404` or `500` would fail again regardless.\n */\nconst isAuthenticationError = (error: unknown): boolean => {\n const status = (error as { status?: unknown } | undefined)?.status;\n\n return (\n typeof status !== 'number' || AUTHENTICATION_ERROR_STATUSES.has(status)\n );\n};\n\n/** Whether the current runtime is a browser rather than a server. */\nconst isBrowser = (): boolean => typeof window !== 'undefined';\n\n/**\n * Resolves the expiration timestamp (in ms since epoch) from an OAuth2\n * token-like payload, supporting both absolute dates and relative durations.\n */\nconst getExpiryTimestamp = (\n token: OAuthTokenLike | undefined\n): number | undefined => {\n if (!token) return undefined;\n\n const dateLike = (token.accessTokenExpiresAt ?? token.expiresAt) as\n | string\n | Date\n | undefined;\n\n if (dateLike) {\n const timestamp =\n typeof dateLike === 'string'\n ? Date.parse(dateLike)\n : dateLike.getTime?.();\n if (typeof timestamp === 'number' && Number.isFinite(timestamp)) {\n return timestamp;\n }\n }\n\n const seconds = token.expires_in ?? token.expiresIn;\n if (typeof seconds === 'number' && Number.isFinite(seconds)) {\n return Date.now() + seconds * 1000;\n }\n\n return undefined;\n};\n\n/**\n * Options accepted when creating an {@link AuthManager}.\n */\nexport type AuthManagerOptions = {\n /**\n * Base fetcher options merged into every request (e.g. custom headers).\n */\n baseFetcherOptions?: FetcherOptions;\n /**\n * A pre-issued CLI session token (prefixed with `clisession_`). When provided,\n * it is used directly as the Bearer token and the OAuth2 `client_credentials`\n * exchange is skipped entirely.\n */\n sessionToken?: string;\n};\n\n/**\n * Options accepted when binding a domain section to an {@link AuthManager}.\n */\nexport type BindSectionOptions = {\n /**\n * Forces auth injection on (`false`) or off (`true`). Defaults to enabled\n * whenever credentials are available.\n */\n skipAuth?: boolean;\n};\n\n/**\n * Manages CMS authentication for a set of API calls.\n *\n * The manager owns a single mutable {@link FetcherOptions} object\n * (`fetcherOptions`). Domain factories bound through it share the same\n * reference, so refreshing the token in place transparently authenticates every\n * call — without rebuilding the API clients.\n */\nexport type AuthManager = {\n /**\n * Mutable fetcher options carrying the `Authorization` header. Bound sections\n * share this reference so the injected token is reused everywhere.\n */\n fetcherOptions: FetcherOptions;\n /**\n * The resolved Intlayer configuration (merged with the build-time defaults).\n */\n intlayerConfig: IntlayerConfig;\n /**\n * `true` when credentials are available — either a session token or a\n * `clientId`/`clientSecret` pair in the configuration.\n */\n hasCredentials: boolean;\n /**\n * Ensures a valid access token is available and applies it to\n * `fetcherOptions`. Called automatically by {@link withAuth}.\n */\n ensureAuth: () => Promise<void>;\n /**\n * Forces a token refresh on the next call by clearing the cached token.\n */\n reset: () => void;\n /**\n * Wraps a single API method so it injects (and, on failure, refreshes) the\n * access token on every invocation while preserving the method's signature.\n */\n withAuth: <Arguments extends unknown[], Result>(\n method: AsyncMethod<Arguments, Result>,\n skipAuth?: boolean\n ) => AsyncMethod<Arguments, Result>;\n /**\n * Instantiates a domain factory with the managed fetcher options and config,\n * returning a section whose every method authenticates automatically.\n *\n * @example\n * ```ts\n * const dictionary = authManager.bindSection(getDictionaryAPI);\n * await dictionary.pushDictionaries(dictionaries);\n * ```\n */\n bindSection: <Section extends Record<string, unknown>>(\n factory: SectionFactory<Section>,\n options?: BindSectionOptions\n ) => Section;\n};\n\n/**\n * Creates an instance-scoped {@link AuthManager} handling the OAuth2\n * `client_credentials` flow (or a pre-issued CLI session token).\n *\n * Managers built with the same backend URL and credentials share one token\n * state, so a process never exchanges credentials more than needed.\n *\n * @param intlayerConfig - Intlayer configuration carrying the `editor`\n * credentials. Defaults to the build-time configuration.\n * @param options - Optional session token and base fetcher options.\n */\nexport const createAuthManager = (\n intlayerConfig: Pick<IntlayerConfig, 'editor'> = { editor },\n options: AuthManagerOptions = {}\n): AuthManager => {\n const { baseFetcherOptions = {}, sessionToken } = options;\n\n // `editor` comes from the generated build-time configuration, which never\n // carries `clientSecret` — it would be inlined into client bundles. The\n // secret is supplied server-side by `@intlayer/config/secrets`, which\n // resolves to an inert stub in the browser; `hasCredentials` then stays false\n // there and the OAuth2 exchange never runs.\n //\n // Only consulted when the configuration declares a `clientId`, so a project\n // that comments its credentials out stays uncredentialed even with the\n // environment variables still defined.\n const resolvedConfig = defu(intlayerConfig ?? {}, {\n editor: {\n ...editor,\n clientSecret: editor?.clientId ? getEditorClientSecret() : undefined,\n },\n }) as IntlayerConfig;\n\n // `credentials: 'omit'` prevents the browser from attaching session cookies;\n // authentication is handled exclusively through the injected Bearer token.\n // This is required because the backend only allows credentialed requests for\n // whitelisted first-party origins.\n const fetcherOptions: FetcherOptions = {\n ...baseFetcherOptions,\n credentials: 'omit',\n };\n\n const hasCredentials =\n Boolean(sessionToken) ||\n Boolean(\n resolvedConfig?.editor?.clientId && resolvedConfig?.editor?.clientSecret\n );\n\n // A session token is used as-is and never expires within the SDK's concern,\n // so inject it once up-front and skip the OAuth2 exchange.\n if (sessionToken) {\n fetcherOptions.headers = {\n ...(fetcherOptions.headers ?? {}),\n Authorization: `Bearer ${sessionToken}`,\n } as HeadersInit;\n }\n\n const accessTokenState = getAccessTokenState(resolvedConfig?.editor);\n\n const needsRefresh = (): boolean => {\n const { accessToken, expiryTimestamp } = accessTokenState;\n\n if (!accessToken) return true;\n if (!expiryTimestamp) return false; // Unknown expiry: assume usable until failure.\n\n // Refresh one minute before the actual expiry to avoid edge-of-expiry races.\n return Date.now() + ONE_MINUTE_MS >= expiryTimestamp;\n };\n\n const refreshToken = async (): Promise<void> => {\n const doRefresh = async (): Promise<void> => {\n const oAuthAPI = getOAuthAPI({}, resolvedConfig);\n const response = await oAuthAPI.getOAuth2AccessToken();\n const tokenData = response?.data as OAuthTokenLike | undefined;\n\n accessTokenState.accessToken = tokenData?.accessToken;\n accessTokenState.expiryTimestamp = getExpiryTimestamp(tokenData);\n };\n\n // De-duplicate concurrent refreshes so a burst of calls triggers one fetch.\n accessTokenState.pendingRefresh ??= doRefresh().finally(() => {\n accessTokenState.pendingRefresh = undefined;\n });\n\n await accessTokenState.pendingRefresh;\n };\n\n const applyAuthHeader = (): void => {\n if (!accessTokenState.accessToken) return;\n fetcherOptions.headers = {\n ...(fetcherOptions.headers ?? {}),\n Authorization: `Bearer ${accessTokenState.accessToken}`,\n } as HeadersInit;\n };\n\n const ensureAuth = async (): Promise<void> => {\n if (sessionToken) return; // Already injected; nothing to refresh.\n if (needsRefresh()) {\n await refreshToken();\n }\n applyAuthHeader();\n };\n\n const reset = (): void => {\n accessTokenState.accessToken = undefined;\n accessTokenState.expiryTimestamp = undefined;\n };\n\n const withAuth = <Arguments extends unknown[], Result>(\n method: AsyncMethod<Arguments, Result>,\n skipAuth = !hasCredentials\n ): AsyncMethod<Arguments, Result> => {\n // In a browser the project credentials are structurally unavailable, so an\n // authenticated call would silently go out unauthenticated and come back\n // 401. Fail with an actionable message instead: a browser must authenticate\n // as the signed-in *user* (a session token, or the session cookie on a\n // first-party origin), never as the project.\n if (skipAuth && !sessionToken && isBrowser()) {\n return async () => {\n throw new Error(\n 'Intlayer CMS credentials are not available in the browser. ' +\n '`clientSecret` is a server-side credential and is never bundled. ' +\n 'Call this from a server route, or pass a user session token: ' +\n 'createIntlayerCMS(config, { sessionToken }).'\n );\n };\n }\n\n if (skipAuth) return method;\n\n return async (...args: Arguments): Promise<Result> => {\n await ensureAuth();\n\n try {\n return await method(...args);\n } catch (error) {\n // The token may have been revoked or expired early: refresh once and\n // retry before surfacing the error. A session token cannot be\n // refreshed, so re-throw immediately in that case.\n if (sessionToken || !isAuthenticationError(error)) throw error;\n\n await refreshToken();\n applyAuthHeader();\n return await method(...args);\n }\n };\n };\n\n const bindSection = <Section extends Record<string, unknown>>(\n factory: SectionFactory<Section>,\n bindOptions: BindSectionOptions = {}\n ): Section => {\n const section = factory(fetcherOptions, resolvedConfig);\n const skipAuth = bindOptions.skipAuth ?? !hasCredentials;\n\n return new Proxy(section, {\n get(target, property, receiver) {\n const value = Reflect.get(target, property, receiver);\n\n if (typeof value !== 'function') return value;\n\n return withAuth(\n (value as AsyncMethod<unknown[], unknown>).bind(target),\n skipAuth\n );\n },\n });\n };\n\n return {\n fetcherOptions,\n intlayerConfig: resolvedConfig,\n hasCredentials,\n ensureAuth,\n reset,\n withAuth,\n bindSection,\n };\n};\n"],"mappings":"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"}
@@ -2,6 +2,7 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
2
2
  const require_cms_createAuthManager = require('./createAuthManager.cjs');
3
3
  const require_cms_createIntlayerCMS = require('./createIntlayerCMS.cjs');
4
4
 
5
+ exports.clearAccessTokenCache = require_cms_createAuthManager.clearAccessTokenCache;
5
6
  exports.createAuthManager = require_cms_createAuthManager.createAuthManager;
6
7
  exports.createEndpoint = require_cms_createIntlayerCMS.createEndpoint;
7
8
  exports.createIntlayerCMS = require_cms_createIntlayerCMS.createIntlayerCMS;
@@ -43,12 +43,22 @@ const getAnalyticsAPI = (authAPIOptions = {}, intlayerConfig) => {
43
43
  * @returns The experiment result set.
44
44
  */
45
45
  const getExperimentResults = async (experimentKey, otherOptions = {}) => await require_fetcher.fetcher(`${ANALYTICS_API_ROUTE}/experiments/${experimentKey}`, authAPIOptions, otherOptions, { method: "GET" });
46
+ /**
47
+ * Page metadata (title and meta description) for a given url.
48
+ * @param url - The page url to inspect.
49
+ * @returns Title and description of the page.
50
+ */
51
+ const getPageMetadata = async (url, otherOptions = {}) => await require_fetcher.fetcher(`${ANALYTICS_API_ROUTE}/page-metadata`, authAPIOptions, otherOptions, {
52
+ method: "GET",
53
+ params: { url }
54
+ });
46
55
  return {
47
56
  sendEvents,
48
57
  getOverview,
49
58
  getAudience,
50
59
  getContentStats,
51
- getExperimentResults
60
+ getExperimentResults,
61
+ getPageMetadata
52
62
  };
53
63
  };
54
64
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"analytics.cjs","names":["editor","fetcher","createEndpoint"],"sources":["../../../src/getIntlayerAPI/analytics.ts"],"sourcesContent":["import type {\n AudienceRange,\n GetAnalyticsOverviewResult,\n GetAudienceResult,\n GetContentStatsResult,\n GetExperimentResultsResult,\n IngestAnalyticsBody,\n IngestAnalyticsResult,\n} from '@intlayer/backend';\nimport { editor } from '@intlayer/config/built';\nimport type { IntlayerConfig } from '@intlayer/types/config';\nimport { createEndpoint } from '../cms/createIntlayerCMS';\nimport { type FetcherOptions, fetcher } from '../fetcher';\n\nexport const getAnalyticsAPI = (\n authAPIOptions: FetcherOptions = {},\n intlayerConfig?: IntlayerConfig\n) => {\n const backendURL = intlayerConfig?.editor?.backendURL ?? editor.backendURL;\n\n const ANALYTICS_API_ROUTE = `${backendURL}/api/analytics`;\n\n /**\n * Ingest a batch of analytics events. Public — attribution is by the\n * project's `clientId` in the body.\n * @param body - Session id, sdk version, and the collected events.\n * @returns The number of accepted events.\n */\n const sendEvents = async (\n body: IngestAnalyticsBody,\n otherOptions: FetcherOptions = {}\n ) =>\n await fetcher<IngestAnalyticsResult>(\n `${ANALYTICS_API_ROUTE}/events`,\n authAPIOptions,\n otherOptions,\n {\n method: 'POST',\n body,\n }\n );\n\n /**\n * Page/locale totals for the authenticated project.\n * @returns Views grouped by url and locale.\n */\n const getOverview = async (otherOptions: FetcherOptions = {}) =>\n await fetcher<GetAnalyticsOverviewResult>(\n `${ANALYTICS_API_ROUTE}/overview`,\n authAPIOptions,\n otherOptions,\n { method: 'GET' }\n );\n\n /**\n * Audience report: distinct visitors (today / 7d / window), page views, the\n * evolution series, and locale + country breakdowns.\n * @param range - Rolling window, either a named range (`1h`, `24h`, `7d`,\n * `30d`, `90d`, `6mo`, `1y`, `3y`) or a number of days (default 30).\n * @returns The audience statistics.\n */\n const getAudience = async (\n range: AudienceRange | number = 30,\n otherOptions: FetcherOptions = {}\n ) =>\n await fetcher<GetAudienceResult>(\n `${ANALYTICS_API_ROUTE}/audience`,\n authAPIOptions,\n otherOptions,\n {\n method: 'GET',\n params: typeof range === 'number' ? { days: String(range) } : { range },\n }\n );\n\n /**\n * Per-content exposure totals — \"which content is actually shown\".\n * @returns Exposures grouped by dictionary key, key path, and locale.\n */\n const getContentStats = async (otherOptions: FetcherOptions = {}) =>\n await fetcher<GetContentStatsResult>(\n `${ANALYTICS_API_ROUTE}/content-stats`,\n authAPIOptions,\n otherOptions,\n { method: 'GET' }\n );\n\n /**\n * A/B experiment results with per-variant conversion rates and significance.\n * @param experimentKey - The experiment to evaluate.\n * @returns The experiment result set.\n */\n const getExperimentResults = async (\n experimentKey: string,\n otherOptions: FetcherOptions = {}\n ) =>\n await fetcher<GetExperimentResultsResult>(\n `${ANALYTICS_API_ROUTE}/experiments/${experimentKey}`,\n authAPIOptions,\n otherOptions,\n { method: 'GET' }\n );\n\n return {\n sendEvents,\n getOverview,\n getAudience,\n getContentStats,\n getExperimentResults,\n };\n};\n\n/**\n * Authenticated `analytics` endpoint bound to an Intlayer CMS authenticator.\n *\n * Pass an authenticator created with `createIntlayerCMS`, or omit it to use\n * the build-time configuration (`@intlayer/config/built`).\n */\nexport const analyticsEndpoint = createEndpoint(getAnalyticsAPI);\n"],"mappings":";;;;;;AAcA,MAAa,mBACX,iBAAiC,CAAC,GAClC,mBACG;CAGH,MAAM,sBAAsB,GAFT,gBAAgB,QAAQ,cAAcA,8BAAO,WAEtB;;;;;;;CAQ1C,MAAM,aAAa,OACjB,MACA,eAA+B,CAAC,MAEhC,MAAMC,wBACJ,GAAG,oBAAoB,UACvB,gBACA,cACA;EACE,QAAQ;EACR;CACF,CACF;;;;;CAMF,MAAM,cAAc,OAAO,eAA+B,CAAC,MACzD,MAAMA,wBACJ,GAAG,oBAAoB,YACvB,gBACA,cACA,EAAE,QAAQ,MAAM,CAClB;;;;;;;;CASF,MAAM,cAAc,OAClB,QAAgC,IAChC,eAA+B,CAAC,MAEhC,MAAMA,wBACJ,GAAG,oBAAoB,YACvB,gBACA,cACA;EACE,QAAQ;EACR,QAAQ,OAAO,UAAU,WAAW,EAAE,MAAM,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM;CACxE,CACF;;;;;CAMF,MAAM,kBAAkB,OAAO,eAA+B,CAAC,MAC7D,MAAMA,wBACJ,GAAG,oBAAoB,iBACvB,gBACA,cACA,EAAE,QAAQ,MAAM,CAClB;;;;;;CAOF,MAAM,uBAAuB,OAC3B,eACA,eAA+B,CAAC,MAEhC,MAAMA,wBACJ,GAAG,oBAAoB,eAAe,iBACtC,gBACA,cACA,EAAE,QAAQ,MAAM,CAClB;CAEF,OAAO;EACL;EACA;EACA;EACA;EACA;CACF;AACF;;;;;;;AAQA,MAAa,oBAAoBC,6CAAe,eAAe"}
1
+ {"version":3,"file":"analytics.cjs","names":["editor","fetcher","createEndpoint"],"sources":["../../../src/getIntlayerAPI/analytics.ts"],"sourcesContent":["import type {\n AudienceRange,\n GetAnalyticsOverviewResult,\n GetAudienceResult,\n GetContentStatsResult,\n GetExperimentResultsResult,\n GetPageMetadataResult,\n IngestAnalyticsBody,\n IngestAnalyticsResult,\n} from '@intlayer/backend';\nimport { editor } from '@intlayer/config/built';\nimport type { IntlayerConfig } from '@intlayer/types/config';\nimport { createEndpoint } from '../cms/createIntlayerCMS';\nimport { type FetcherOptions, fetcher } from '../fetcher';\n\nexport const getAnalyticsAPI = (\n authAPIOptions: FetcherOptions = {},\n intlayerConfig?: IntlayerConfig\n) => {\n const backendURL = intlayerConfig?.editor?.backendURL ?? editor.backendURL;\n\n const ANALYTICS_API_ROUTE = `${backendURL}/api/analytics`;\n\n /**\n * Ingest a batch of analytics events. Public — attribution is by the\n * project's `clientId` in the body.\n * @param body - Session id, sdk version, and the collected events.\n * @returns The number of accepted events.\n */\n const sendEvents = async (\n body: IngestAnalyticsBody,\n otherOptions: FetcherOptions = {}\n ) =>\n await fetcher<IngestAnalyticsResult>(\n `${ANALYTICS_API_ROUTE}/events`,\n authAPIOptions,\n otherOptions,\n {\n method: 'POST',\n body,\n }\n );\n\n /**\n * Page/locale totals for the authenticated project.\n * @returns Views grouped by url and locale.\n */\n const getOverview = async (otherOptions: FetcherOptions = {}) =>\n await fetcher<GetAnalyticsOverviewResult>(\n `${ANALYTICS_API_ROUTE}/overview`,\n authAPIOptions,\n otherOptions,\n { method: 'GET' }\n );\n\n /**\n * Audience report: distinct visitors (today / 7d / window), page views, the\n * evolution series, and locale + country breakdowns.\n * @param range - Rolling window, either a named range (`1h`, `24h`, `7d`,\n * `30d`, `90d`, `6mo`, `1y`, `3y`) or a number of days (default 30).\n * @returns The audience statistics.\n */\n const getAudience = async (\n range: AudienceRange | number = 30,\n otherOptions: FetcherOptions = {}\n ) =>\n await fetcher<GetAudienceResult>(\n `${ANALYTICS_API_ROUTE}/audience`,\n authAPIOptions,\n otherOptions,\n {\n method: 'GET',\n params: typeof range === 'number' ? { days: String(range) } : { range },\n }\n );\n\n /**\n * Per-content exposure totals — \"which content is actually shown\".\n * @returns Exposures grouped by dictionary key, key path, and locale.\n */\n const getContentStats = async (otherOptions: FetcherOptions = {}) =>\n await fetcher<GetContentStatsResult>(\n `${ANALYTICS_API_ROUTE}/content-stats`,\n authAPIOptions,\n otherOptions,\n { method: 'GET' }\n );\n\n /**\n * A/B experiment results with per-variant conversion rates and significance.\n * @param experimentKey - The experiment to evaluate.\n * @returns The experiment result set.\n */\n const getExperimentResults = async (\n experimentKey: string,\n otherOptions: FetcherOptions = {}\n ) =>\n await fetcher<GetExperimentResultsResult>(\n `${ANALYTICS_API_ROUTE}/experiments/${experimentKey}`,\n authAPIOptions,\n otherOptions,\n { method: 'GET' }\n );\n\n /**\n * Page metadata (title and meta description) for a given url.\n * @param url - The page url to inspect.\n * @returns Title and description of the page.\n */\n const getPageMetadata = async (\n url: string,\n otherOptions: FetcherOptions = {}\n ) =>\n await fetcher<GetPageMetadataResult>(\n `${ANALYTICS_API_ROUTE}/page-metadata`,\n authAPIOptions,\n otherOptions,\n {\n method: 'GET',\n params: { url },\n }\n );\n\n return {\n sendEvents,\n getOverview,\n getAudience,\n getContentStats,\n getExperimentResults,\n getPageMetadata,\n };\n};\n\n/**\n * Authenticated `analytics` endpoint bound to an Intlayer CMS authenticator.\n *\n * Pass an authenticator created with `createIntlayerCMS`, or omit it to use\n * the build-time configuration (`@intlayer/config/built`).\n */\nexport const analyticsEndpoint = createEndpoint(getAnalyticsAPI);\n"],"mappings":";;;;;;AAeA,MAAa,mBACX,iBAAiC,CAAC,GAClC,mBACG;CAGH,MAAM,sBAAsB,GAFT,gBAAgB,QAAQ,cAAcA,8BAAO,WAEtB;;;;;;;CAQ1C,MAAM,aAAa,OACjB,MACA,eAA+B,CAAC,MAEhC,MAAMC,wBACJ,GAAG,oBAAoB,UACvB,gBACA,cACA;EACE,QAAQ;EACR;CACF,CACF;;;;;CAMF,MAAM,cAAc,OAAO,eAA+B,CAAC,MACzD,MAAMA,wBACJ,GAAG,oBAAoB,YACvB,gBACA,cACA,EAAE,QAAQ,MAAM,CAClB;;;;;;;;CASF,MAAM,cAAc,OAClB,QAAgC,IAChC,eAA+B,CAAC,MAEhC,MAAMA,wBACJ,GAAG,oBAAoB,YACvB,gBACA,cACA;EACE,QAAQ;EACR,QAAQ,OAAO,UAAU,WAAW,EAAE,MAAM,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM;CACxE,CACF;;;;;CAMF,MAAM,kBAAkB,OAAO,eAA+B,CAAC,MAC7D,MAAMA,wBACJ,GAAG,oBAAoB,iBACvB,gBACA,cACA,EAAE,QAAQ,MAAM,CAClB;;;;;;CAOF,MAAM,uBAAuB,OAC3B,eACA,eAA+B,CAAC,MAEhC,MAAMA,wBACJ,GAAG,oBAAoB,eAAe,iBACtC,gBACA,cACA,EAAE,QAAQ,MAAM,CAClB;;;;;;CAOF,MAAM,kBAAkB,OACtB,KACA,eAA+B,CAAC,MAEhC,MAAMA,wBACJ,GAAG,oBAAoB,iBACvB,gBACA,cACA;EACE,QAAQ;EACR,QAAQ,EAAE,IAAI;CAChB,CACF;CAEF,OAAO;EACL;EACA;EACA;EACA;EACA;EACA;CACF;AACF;;;;;;;AAQA,MAAa,oBAAoBC,6CAAe,eAAe"}
@@ -24,8 +24,9 @@ const getAuditAPI = (authAPIOptions = {}, intlayerConfig) => {
24
24
  });
25
25
  const scanUrl = async (body, otherOptions = {}) => {
26
26
  if (!body?.url) return;
27
- const { url, onMessage, onDone } = body;
27
+ const { url, refresh, onMessage, onDone } = body;
28
28
  const params = new URLSearchParams({ url });
29
+ if (refresh) params.set("refresh", "true");
29
30
  const endpoint = `${AUDIT_API_ROUTE}?${params.toString()}`;
30
31
  const response = await fetch(endpoint, {
31
32
  method: "GET",
@@ -82,7 +83,29 @@ const getAuditAPI = (authAPIOptions = {}, intlayerConfig) => {
82
83
  const cancelRecursiveAudit = async (params, otherOptions = {}) => await require_fetcher.fetcher(`${AUDIT_API_ROUTE}/recursive/${params?.jobId}/cancel`, authAPIOptions, otherOptions, { method: "POST" });
83
84
  const pauseRecursiveAudit = async (params, otherOptions = {}) => await require_fetcher.fetcher(`${AUDIT_API_ROUTE}/recursive/${params?.jobId}/pause`, authAPIOptions, otherOptions, { method: "POST" });
84
85
  const resumeRecursiveAudit = async (params, otherOptions = {}) => await require_fetcher.fetcher(`${AUDIT_API_ROUTE}/recursive/${params?.jobId}/resume`, authAPIOptions, otherOptions, { method: "POST" });
86
+ /**
87
+ * Admin — number of scanned domains using each technology.
88
+ */
89
+ const getTechnologyUsage = async (params, otherOptions = {}) => await require_fetcher.fetcher(`${AUDIT_API_ROUTE}/technologies`, authAPIOptions, otherOptions, {
90
+ method: "GET",
91
+ params
92
+ });
93
+ /**
94
+ * Admin — scanned hosts, most recently scanned first, filterable by
95
+ * technology.
96
+ */
97
+ const getScannedHosts = async (params, otherOptions = {}) => await require_fetcher.fetcher(`${AUDIT_API_ROUTE}/hosts`, authAPIOptions, otherOptions, {
98
+ method: "GET",
99
+ params
100
+ });
101
+ /**
102
+ * Admin — a scanned host with its stored scans, newest first.
103
+ */
104
+ const getScannedHost = async (params, otherOptions = {}) => await require_fetcher.fetcher(`${AUDIT_API_ROUTE}/hosts/${encodeURIComponent(params?.host ?? "")}`, authAPIOptions, otherOptions, { method: "GET" });
85
105
  return {
106
+ getTechnologyUsage,
107
+ getScannedHosts,
108
+ getScannedHost,
86
109
  discoverUrls,
87
110
  scanUrl,
88
111
  startRecursiveAudit,
@@ -1 +1 @@
1
- {"version":3,"file":"audit.cjs","names":["editor","fetcher","createEndpoint"],"sources":["../../../src/getIntlayerAPI/audit.ts"],"sourcesContent":["import type {\n AuditEvent,\n GetRecursiveAuditStatusResult,\n StartRecursiveAuditResult,\n} from '@intlayer/backend';\nimport { editor } from '@intlayer/config/built';\nimport type { IntlayerConfig } from '@intlayer/types/config';\nimport { createEndpoint } from '../cms/createIntlayerCMS';\nimport { type FetcherOptions, fetcher } from '../fetcher';\n\nexport type {\n AuditEvent,\n GetRecursiveAuditStatusResult,\n StartRecursiveAuditResult,\n};\n\nexport type ScanUrlBody = {\n url: string;\n onMessage?: (event: AuditEvent) => void;\n onDone?: () => void;\n};\n\nexport type DiscoverUrlsParams = {\n url: string;\n};\n\nexport type DiscoverUrlsResult = {\n urls: string[];\n};\n\nexport type StartRecursiveAuditBody = {\n url: string;\n urls?: string[];\n};\n\nexport type GetRecursiveAuditStatusParams = {\n jobId: string;\n};\n\nexport type RecursiveAuditJobParams = {\n jobId: string;\n};\n\nexport const getAuditAPI = (\n authAPIOptions: FetcherOptions = {},\n intlayerConfig?: IntlayerConfig\n) => {\n const backendURL = intlayerConfig?.editor?.backendURL ?? editor.backendURL;\n\n const AUDIT_API_ROUTE = `${backendURL}/api/scan`;\n\n /**\n * Streams a single-page SEO audit as Server-Sent Events.\n *\n * Usage:\n * ```ts\n * await audit.scanUrl({\n * url: 'https://example.com',\n * onMessage: (event) => console.log(event),\n * onDone: () => console.log('done'),\n * });\n * ```\n */\n const discoverUrls = async (\n params?: DiscoverUrlsParams,\n otherOptions: FetcherOptions = {}\n ) =>\n await fetcher<DiscoverUrlsResult>(\n `${AUDIT_API_ROUTE}/recursive/discover`,\n authAPIOptions,\n otherOptions,\n {\n method: 'GET',\n params,\n }\n );\n\n const scanUrl = async (\n body?: ScanUrlBody,\n otherOptions: FetcherOptions = {}\n ) => {\n if (!body?.url) return;\n\n const { url, onMessage, onDone } = body;\n\n const params = new URLSearchParams({ url });\n const endpoint = `${AUDIT_API_ROUTE}?${params.toString()}`;\n\n const response = await fetch(endpoint, {\n method: 'GET',\n headers: {\n Accept: 'text/event-stream',\n ...authAPIOptions.headers,\n ...otherOptions.headers,\n },\n credentials: 'include',\n signal: otherOptions.signal,\n });\n\n if (!response.ok) {\n let errorMessage = 'An error occurred';\n try {\n const errorData = await response.json();\n errorMessage = JSON.stringify(errorData.error) ?? errorMessage;\n } catch {\n try {\n const errorText = await response.text();\n if (errorText) errorMessage = errorText;\n } catch {\n // ignore\n }\n }\n throw new Error(errorMessage);\n }\n\n const reader = response.body?.getReader();\n if (!reader) throw new Error('No reader available');\n\n const decoder = new TextDecoder();\n let buffer = '';\n\n while (true) {\n const { done, value } = await reader.read();\n\n if (done) break;\n\n buffer += decoder.decode(value, { stream: true });\n const lines = buffer.split('\\n');\n buffer = lines.pop() ?? '';\n\n for (const line of lines) {\n if (line.startsWith('data: ')) {\n try {\n const event = JSON.parse(line.slice(6)) as AuditEvent;\n onMessage?.(event);\n } catch {\n // ignore malformed lines\n }\n }\n }\n }\n\n onDone?.();\n };\n\n /**\n * Starts a recursive audit job for the given URL.\n */\n const startRecursiveAudit = async (\n body?: StartRecursiveAuditBody,\n otherOptions: FetcherOptions = {}\n ) =>\n await fetcher<StartRecursiveAuditResult>(\n `${AUDIT_API_ROUTE}/recursive/start`,\n authAPIOptions,\n otherOptions,\n {\n method: 'POST',\n params: body?.url ? { url: body.url } : undefined,\n body: body?.urls !== undefined ? { urls: body.urls as any } : undefined,\n }\n );\n\n /**\n * Gets the status of a recursive audit job.\n */\n const getRecursiveAuditStatus = async (\n params?: GetRecursiveAuditStatusParams,\n otherOptions: FetcherOptions = {}\n ) =>\n await fetcher<GetRecursiveAuditStatusResult>(\n `${AUDIT_API_ROUTE}/recursive/${params?.jobId}`,\n authAPIOptions,\n otherOptions,\n {\n method: 'GET',\n }\n );\n\n const cancelRecursiveAudit = async (\n params?: RecursiveAuditJobParams,\n otherOptions: FetcherOptions = {}\n ) =>\n await fetcher<{ success: boolean }>(\n `${AUDIT_API_ROUTE}/recursive/${params?.jobId}/cancel`,\n authAPIOptions,\n otherOptions,\n { method: 'POST' }\n );\n\n const pauseRecursiveAudit = async (\n params?: RecursiveAuditJobParams,\n otherOptions: FetcherOptions = {}\n ) =>\n await fetcher<{ success: boolean }>(\n `${AUDIT_API_ROUTE}/recursive/${params?.jobId}/pause`,\n authAPIOptions,\n otherOptions,\n { method: 'POST' }\n );\n\n const resumeRecursiveAudit = async (\n params?: RecursiveAuditJobParams,\n otherOptions: FetcherOptions = {}\n ) =>\n await fetcher<{ success: boolean }>(\n `${AUDIT_API_ROUTE}/recursive/${params?.jobId}/resume`,\n authAPIOptions,\n otherOptions,\n { method: 'POST' }\n );\n\n return {\n discoverUrls,\n scanUrl,\n startRecursiveAudit,\n getRecursiveAuditStatus,\n cancelRecursiveAudit,\n pauseRecursiveAudit,\n resumeRecursiveAudit,\n };\n};\n\n/**\n * Authenticated `audit` endpoint bound to an Intlayer CMS authenticator.\n *\n * Pass an authenticator created with `createIntlayerCMS`, or omit it to use\n * the build-time configuration (`@intlayer/config/built`).\n */\nexport const auditEndpoint = createEndpoint(getAuditAPI);\n"],"mappings":"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"}
1
+ {"version":3,"file":"audit.cjs","names":["editor","fetcher","createEndpoint"],"sources":["../../../src/getIntlayerAPI/audit.ts"],"sourcesContent":["import type {\n AuditEvent,\n GetRecursiveAuditStatusResult,\n GetScannedHostParams,\n GetScannedHostResult,\n GetScannedHostsQuery,\n GetScannedHostsResult,\n GetTechnologyUsageQuery,\n GetTechnologyUsageResult,\n StartRecursiveAuditResult,\n} from '@intlayer/backend';\nimport { editor } from '@intlayer/config/built';\nimport type { IntlayerConfig } from '@intlayer/types/config';\nimport { createEndpoint } from '../cms/createIntlayerCMS';\nimport { type FetcherOptions, fetcher } from '../fetcher';\n\nexport type {\n AuditEvent,\n GetRecursiveAuditStatusResult,\n GetScannedHostParams,\n GetScannedHostResult,\n GetScannedHostsQuery,\n GetScannedHostsResult,\n GetTechnologyUsageQuery,\n GetTechnologyUsageResult,\n StartRecursiveAuditResult,\n};\n\nexport type ScanUrlBody = {\n url: string;\n /**\n * Force a new audit. Without it, a URL audited less than an hour ago is\n * replayed from cache (its first event carries `cachedAt`).\n */\n refresh?: boolean;\n onMessage?: (event: AuditEvent) => void;\n onDone?: () => void;\n};\n\nexport type DiscoverUrlsParams = {\n url: string;\n};\n\nexport type DiscoverUrlsResult = {\n urls: string[];\n};\n\nexport type StartRecursiveAuditBody = {\n url: string;\n urls?: string[];\n};\n\nexport type GetRecursiveAuditStatusParams = {\n jobId: string;\n};\n\nexport type RecursiveAuditJobParams = {\n jobId: string;\n};\n\nexport const getAuditAPI = (\n authAPIOptions: FetcherOptions = {},\n intlayerConfig?: IntlayerConfig\n) => {\n const backendURL = intlayerConfig?.editor?.backendURL ?? editor.backendURL;\n\n const AUDIT_API_ROUTE = `${backendURL}/api/scan`;\n\n /**\n * Streams a single-page SEO audit as Server-Sent Events.\n *\n * Usage:\n * ```ts\n * await audit.scanUrl({\n * url: 'https://example.com',\n * onMessage: (event) => console.log(event),\n * onDone: () => console.log('done'),\n * });\n * ```\n */\n const discoverUrls = async (\n params?: DiscoverUrlsParams,\n otherOptions: FetcherOptions = {}\n ) =>\n await fetcher<DiscoverUrlsResult>(\n `${AUDIT_API_ROUTE}/recursive/discover`,\n authAPIOptions,\n otherOptions,\n {\n method: 'GET',\n params,\n }\n );\n\n const scanUrl = async (\n body?: ScanUrlBody,\n otherOptions: FetcherOptions = {}\n ) => {\n if (!body?.url) return;\n\n const { url, refresh, onMessage, onDone } = body;\n\n const params = new URLSearchParams({ url });\n if (refresh) params.set('refresh', 'true');\n const endpoint = `${AUDIT_API_ROUTE}?${params.toString()}`;\n\n const response = await fetch(endpoint, {\n method: 'GET',\n headers: {\n Accept: 'text/event-stream',\n ...authAPIOptions.headers,\n ...otherOptions.headers,\n },\n credentials: 'include',\n signal: otherOptions.signal,\n });\n\n if (!response.ok) {\n let errorMessage = 'An error occurred';\n try {\n const errorData = await response.json();\n errorMessage = JSON.stringify(errorData.error) ?? errorMessage;\n } catch {\n try {\n const errorText = await response.text();\n if (errorText) errorMessage = errorText;\n } catch {\n // ignore\n }\n }\n throw new Error(errorMessage);\n }\n\n const reader = response.body?.getReader();\n if (!reader) throw new Error('No reader available');\n\n const decoder = new TextDecoder();\n let buffer = '';\n\n while (true) {\n const { done, value } = await reader.read();\n\n if (done) break;\n\n buffer += decoder.decode(value, { stream: true });\n const lines = buffer.split('\\n');\n buffer = lines.pop() ?? '';\n\n for (const line of lines) {\n if (line.startsWith('data: ')) {\n try {\n const event = JSON.parse(line.slice(6)) as AuditEvent;\n onMessage?.(event);\n } catch {\n // ignore malformed lines\n }\n }\n }\n }\n\n onDone?.();\n };\n\n /**\n * Starts a recursive audit job for the given URL.\n */\n const startRecursiveAudit = async (\n body?: StartRecursiveAuditBody,\n otherOptions: FetcherOptions = {}\n ) =>\n await fetcher<StartRecursiveAuditResult>(\n `${AUDIT_API_ROUTE}/recursive/start`,\n authAPIOptions,\n otherOptions,\n {\n method: 'POST',\n params: body?.url ? { url: body.url } : undefined,\n body: body?.urls !== undefined ? { urls: body.urls as any } : undefined,\n }\n );\n\n /**\n * Gets the status of a recursive audit job.\n */\n const getRecursiveAuditStatus = async (\n params?: GetRecursiveAuditStatusParams,\n otherOptions: FetcherOptions = {}\n ) =>\n await fetcher<GetRecursiveAuditStatusResult>(\n `${AUDIT_API_ROUTE}/recursive/${params?.jobId}`,\n authAPIOptions,\n otherOptions,\n {\n method: 'GET',\n }\n );\n\n const cancelRecursiveAudit = async (\n params?: RecursiveAuditJobParams,\n otherOptions: FetcherOptions = {}\n ) =>\n await fetcher<{ success: boolean }>(\n `${AUDIT_API_ROUTE}/recursive/${params?.jobId}/cancel`,\n authAPIOptions,\n otherOptions,\n { method: 'POST' }\n );\n\n const pauseRecursiveAudit = async (\n params?: RecursiveAuditJobParams,\n otherOptions: FetcherOptions = {}\n ) =>\n await fetcher<{ success: boolean }>(\n `${AUDIT_API_ROUTE}/recursive/${params?.jobId}/pause`,\n authAPIOptions,\n otherOptions,\n { method: 'POST' }\n );\n\n const resumeRecursiveAudit = async (\n params?: RecursiveAuditJobParams,\n otherOptions: FetcherOptions = {}\n ) =>\n await fetcher<{ success: boolean }>(\n `${AUDIT_API_ROUTE}/recursive/${params?.jobId}/resume`,\n authAPIOptions,\n otherOptions,\n { method: 'POST' }\n );\n\n /**\n * Admin — number of scanned domains using each technology.\n */\n const getTechnologyUsage = async (\n params?: GetTechnologyUsageQuery,\n otherOptions: FetcherOptions = {}\n ) =>\n await fetcher<GetTechnologyUsageResult>(\n `${AUDIT_API_ROUTE}/technologies`,\n authAPIOptions,\n otherOptions,\n { method: 'GET', params }\n );\n\n /**\n * Admin — scanned hosts, most recently scanned first, filterable by\n * technology.\n */\n const getScannedHosts = async (\n params?: GetScannedHostsQuery,\n otherOptions: FetcherOptions = {}\n ) =>\n await fetcher<GetScannedHostsResult>(\n `${AUDIT_API_ROUTE}/hosts`,\n authAPIOptions,\n otherOptions,\n { method: 'GET', params }\n );\n\n /**\n * Admin — a scanned host with its stored scans, newest first.\n */\n const getScannedHost = async (\n params?: GetScannedHostParams,\n otherOptions: FetcherOptions = {}\n ) =>\n await fetcher<GetScannedHostResult>(\n `${AUDIT_API_ROUTE}/hosts/${encodeURIComponent(params?.host ?? '')}`,\n authAPIOptions,\n otherOptions,\n { method: 'GET' }\n );\n\n return {\n getTechnologyUsage,\n getScannedHosts,\n getScannedHost,\n discoverUrls,\n scanUrl,\n startRecursiveAudit,\n getRecursiveAuditStatus,\n cancelRecursiveAudit,\n pauseRecursiveAudit,\n resumeRecursiveAudit,\n };\n};\n\n/**\n * Authenticated `audit` endpoint bound to an Intlayer CMS authenticator.\n *\n * Pass an authenticator created with `createIntlayerCMS`, or omit it to use\n * the build-time configuration (`@intlayer/config/built`).\n */\nexport const auditEndpoint = createEndpoint(getAuditAPI);\n"],"mappings":"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"}
@@ -37,6 +37,7 @@ exports.analyticsEndpoint = require_getIntlayerAPI_analytics.analyticsEndpoint;
37
37
  exports.assetEndpoint = require_getIntlayerAPI_asset.assetEndpoint;
38
38
  exports.auditEndpoint = require_getIntlayerAPI_audit.auditEndpoint;
39
39
  exports.bitbucketEndpoint = require_getIntlayerAPI_bitbucket.bitbucketEndpoint;
40
+ exports.clearAccessTokenCache = require_cms_createAuthManager.clearAccessTokenCache;
40
41
  exports.createAuthManager = require_cms_createAuthManager.createAuthManager;
41
42
  exports.createEndpoint = require_cms_createIntlayerCMS.createEndpoint;
42
43
  exports.createIntlayerCMS = require_cms_createIntlayerCMS.createIntlayerCMS;
@@ -5,6 +5,40 @@ import { defu } from "defu";
5
5
 
6
6
  //#region src/cms/createAuthManager.ts
7
7
  const ONE_MINUTE_MS = 6e4;
8
+ /**
9
+ * Process-wide token states, keyed by backend URL and credentials. Build
10
+ * steps and watch rebuilds each create their own manager; sharing the state
11
+ * lets them reuse one token instead of awaiting a new exchange every time.
12
+ */
13
+ const accessTokenStates = /* @__PURE__ */ new Map();
14
+ /** Clears every cached access token, forcing the next call to re-authenticate. */
15
+ const clearAccessTokenCache = () => {
16
+ accessTokenStates.clear();
17
+ };
18
+ /** Returns the shared token state for the given credentials, creating it if missing. */
19
+ const getAccessTokenState = (editorConfig) => {
20
+ const stateKey = [
21
+ editorConfig?.backendURL,
22
+ editorConfig?.clientId,
23
+ editorConfig?.clientSecret
24
+ ].join("\0");
25
+ let accessTokenState = accessTokenStates.get(stateKey);
26
+ if (!accessTokenState) {
27
+ accessTokenState = {};
28
+ accessTokenStates.set(stateKey, accessTokenState);
29
+ }
30
+ return accessTokenState;
31
+ };
32
+ /** HTTP statuses meaning the access token was rejected. */
33
+ const AUTHENTICATION_ERROR_STATUSES = /* @__PURE__ */ new Set([401, 403]);
34
+ /**
35
+ * Whether a failed call may succeed with a fresh token: an auth rejection, or
36
+ * an error without HTTP status. A `404` or `500` would fail again regardless.
37
+ */
38
+ const isAuthenticationError = (error) => {
39
+ const status = error?.status;
40
+ return typeof status !== "number" || AUTHENTICATION_ERROR_STATUSES.has(status);
41
+ };
8
42
  /** Whether the current runtime is a browser rather than a server. */
9
43
  const isBrowser = () => typeof window !== "undefined";
10
44
  /**
@@ -25,8 +59,8 @@ const getExpiryTimestamp = (token) => {
25
59
  * Creates an instance-scoped {@link AuthManager} handling the OAuth2
26
60
  * `client_credentials` flow (or a pre-issued CLI session token).
27
61
  *
28
- * Unlike a global token cache, each manager keeps its own token state, so
29
- * independent SDK instances never interfere with one another.
62
+ * Managers built with the same backend URL and credentials share one token
63
+ * state, so a process never exchanges credentials more than needed.
30
64
  *
31
65
  * @param intlayerConfig - Intlayer configuration carrying the `editor`
32
66
  * credentials. Defaults to the build-time configuration.
@@ -47,10 +81,9 @@ const createAuthManager = (intlayerConfig = { editor }, options = {}) => {
47
81
  ...fetcherOptions.headers ?? {},
48
82
  Authorization: `Bearer ${sessionToken}`
49
83
  };
50
- let accessToken;
51
- let expiryTimestamp;
52
- let pendingRefresh;
84
+ const accessTokenState = getAccessTokenState(resolvedConfig?.editor);
53
85
  const needsRefresh = () => {
86
+ const { accessToken, expiryTimestamp } = accessTokenState;
54
87
  if (!accessToken) return true;
55
88
  if (!expiryTimestamp) return false;
56
89
  return Date.now() + ONE_MINUTE_MS >= expiryTimestamp;
@@ -58,19 +91,19 @@ const createAuthManager = (intlayerConfig = { editor }, options = {}) => {
58
91
  const refreshToken = async () => {
59
92
  const doRefresh = async () => {
60
93
  const tokenData = (await getOAuthAPI({}, resolvedConfig).getOAuth2AccessToken())?.data;
61
- accessToken = tokenData?.accessToken;
62
- expiryTimestamp = getExpiryTimestamp(tokenData);
94
+ accessTokenState.accessToken = tokenData?.accessToken;
95
+ accessTokenState.expiryTimestamp = getExpiryTimestamp(tokenData);
63
96
  };
64
- pendingRefresh ??= doRefresh().finally(() => {
65
- pendingRefresh = void 0;
97
+ accessTokenState.pendingRefresh ??= doRefresh().finally(() => {
98
+ accessTokenState.pendingRefresh = void 0;
66
99
  });
67
- await pendingRefresh;
100
+ await accessTokenState.pendingRefresh;
68
101
  };
69
102
  const applyAuthHeader = () => {
70
- if (!accessToken) return;
103
+ if (!accessTokenState.accessToken) return;
71
104
  fetcherOptions.headers = {
72
105
  ...fetcherOptions.headers ?? {},
73
- Authorization: `Bearer ${accessToken}`
106
+ Authorization: `Bearer ${accessTokenState.accessToken}`
74
107
  };
75
108
  };
76
109
  const ensureAuth = async () => {
@@ -79,8 +112,8 @@ const createAuthManager = (intlayerConfig = { editor }, options = {}) => {
79
112
  applyAuthHeader();
80
113
  };
81
114
  const reset = () => {
82
- accessToken = void 0;
83
- expiryTimestamp = void 0;
115
+ accessTokenState.accessToken = void 0;
116
+ accessTokenState.expiryTimestamp = void 0;
84
117
  };
85
118
  const withAuth = (method, skipAuth = !hasCredentials) => {
86
119
  if (skipAuth && !sessionToken && isBrowser()) return async () => {
@@ -92,7 +125,7 @@ const createAuthManager = (intlayerConfig = { editor }, options = {}) => {
92
125
  try {
93
126
  return await method(...args);
94
127
  } catch (error) {
95
- if (sessionToken) throw error;
128
+ if (sessionToken || !isAuthenticationError(error)) throw error;
96
129
  await refreshToken();
97
130
  applyAuthHeader();
98
131
  return await method(...args);
@@ -120,5 +153,5 @@ const createAuthManager = (intlayerConfig = { editor }, options = {}) => {
120
153
  };
121
154
 
122
155
  //#endregion
123
- export { createAuthManager };
156
+ export { clearAccessTokenCache, createAuthManager };
124
157
  //# sourceMappingURL=createAuthManager.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"createAuthManager.mjs","names":[],"sources":["../../../src/cms/createAuthManager.ts"],"sourcesContent":["import { editor } from '@intlayer/config/built';\nimport { getEditorClientSecret } from '@intlayer/config/secrets';\nimport type { IntlayerConfig } from '@intlayer/types/config';\nimport { defu } from 'defu';\nimport type { FetcherOptions } from '../fetcher';\nimport { getOAuthAPI } from '../getIntlayerAPI/oAuth';\n\n/**\n * Shape of the various OAuth2 token payloads the backend may return. Only the\n * fields used to derive the expiration timestamp are described here.\n */\ntype OAuthTokenLike = {\n accessToken?: string;\n accessTokenExpiresAt?: string | Date;\n expires_in?: number;\n expiresIn?: number;\n expiresAt?: string | Date;\n};\n\n/**\n * Any asynchronous API method exposed by a domain section\n * (e.g. `getDictionaries`, `pushDictionaries`).\n */\ntype AsyncMethod<Arguments extends unknown[], Result> = (\n ...args: Arguments\n) => Promise<Result>;\n\n/**\n * A domain section factory: builds a plain object of async methods from shared\n * fetcher options and the Intlayer configuration.\n */\ntype SectionFactory<Section extends Record<string, unknown>> = (\n fetcherOptions: FetcherOptions,\n intlayerConfig: IntlayerConfig\n) => Section;\n\nconst ONE_MINUTE_MS = 60_000;\n\n/** Whether the current runtime is a browser rather than a server. */\nconst isBrowser = (): boolean => typeof window !== 'undefined';\n\n/**\n * Resolves the expiration timestamp (in ms since epoch) from an OAuth2\n * token-like payload, supporting both absolute dates and relative durations.\n */\nconst getExpiryTimestamp = (\n token: OAuthTokenLike | undefined\n): number | undefined => {\n if (!token) return undefined;\n\n const dateLike = (token.accessTokenExpiresAt ?? token.expiresAt) as\n | string\n | Date\n | undefined;\n\n if (dateLike) {\n const timestamp =\n typeof dateLike === 'string'\n ? Date.parse(dateLike)\n : dateLike.getTime?.();\n if (typeof timestamp === 'number' && Number.isFinite(timestamp)) {\n return timestamp;\n }\n }\n\n const seconds = token.expires_in ?? token.expiresIn;\n if (typeof seconds === 'number' && Number.isFinite(seconds)) {\n return Date.now() + seconds * 1000;\n }\n\n return undefined;\n};\n\n/**\n * Options accepted when creating an {@link AuthManager}.\n */\nexport type AuthManagerOptions = {\n /**\n * Base fetcher options merged into every request (e.g. custom headers).\n */\n baseFetcherOptions?: FetcherOptions;\n /**\n * A pre-issued CLI session token (prefixed with `clisession_`). When provided,\n * it is used directly as the Bearer token and the OAuth2 `client_credentials`\n * exchange is skipped entirely.\n */\n sessionToken?: string;\n};\n\n/**\n * Options accepted when binding a domain section to an {@link AuthManager}.\n */\nexport type BindSectionOptions = {\n /**\n * Forces auth injection on (`false`) or off (`true`). Defaults to enabled\n * whenever credentials are available.\n */\n skipAuth?: boolean;\n};\n\n/**\n * Manages CMS authentication for a set of API calls.\n *\n * The manager owns a single mutable {@link FetcherOptions} object\n * (`fetcherOptions`). Domain factories bound through it share the same\n * reference, so refreshing the token in place transparently authenticates every\n * call — without rebuilding the API clients.\n */\nexport type AuthManager = {\n /**\n * Mutable fetcher options carrying the `Authorization` header. Bound sections\n * share this reference so the injected token is reused everywhere.\n */\n fetcherOptions: FetcherOptions;\n /**\n * The resolved Intlayer configuration (merged with the build-time defaults).\n */\n intlayerConfig: IntlayerConfig;\n /**\n * `true` when credentials are available — either a session token or a\n * `clientId`/`clientSecret` pair in the configuration.\n */\n hasCredentials: boolean;\n /**\n * Ensures a valid access token is available and applies it to\n * `fetcherOptions`. Called automatically by {@link withAuth}.\n */\n ensureAuth: () => Promise<void>;\n /**\n * Forces a token refresh on the next call by clearing the cached token.\n */\n reset: () => void;\n /**\n * Wraps a single API method so it injects (and, on failure, refreshes) the\n * access token on every invocation while preserving the method's signature.\n */\n withAuth: <Arguments extends unknown[], Result>(\n method: AsyncMethod<Arguments, Result>,\n skipAuth?: boolean\n ) => AsyncMethod<Arguments, Result>;\n /**\n * Instantiates a domain factory with the managed fetcher options and config,\n * returning a section whose every method authenticates automatically.\n *\n * @example\n * ```ts\n * const dictionary = authManager.bindSection(getDictionaryAPI);\n * await dictionary.pushDictionaries(dictionaries);\n * ```\n */\n bindSection: <Section extends Record<string, unknown>>(\n factory: SectionFactory<Section>,\n options?: BindSectionOptions\n ) => Section;\n};\n\n/**\n * Creates an instance-scoped {@link AuthManager} handling the OAuth2\n * `client_credentials` flow (or a pre-issued CLI session token).\n *\n * Unlike a global token cache, each manager keeps its own token state, so\n * independent SDK instances never interfere with one another.\n *\n * @param intlayerConfig - Intlayer configuration carrying the `editor`\n * credentials. Defaults to the build-time configuration.\n * @param options - Optional session token and base fetcher options.\n */\nexport const createAuthManager = (\n intlayerConfig: Pick<IntlayerConfig, 'editor'> = { editor },\n options: AuthManagerOptions = {}\n): AuthManager => {\n const { baseFetcherOptions = {}, sessionToken } = options;\n\n // `editor` comes from the generated build-time configuration, which never\n // carries `clientSecret` — it would be inlined into client bundles. The\n // secret is supplied server-side by `@intlayer/config/secrets`, which\n // resolves to an inert stub in the browser; `hasCredentials` then stays false\n // there and the OAuth2 exchange never runs.\n //\n // Only consulted when the configuration declares a `clientId`, so a project\n // that comments its credentials out stays uncredentialed even with the\n // environment variables still defined.\n const resolvedConfig = defu(intlayerConfig ?? {}, {\n editor: {\n ...editor,\n clientSecret: editor?.clientId ? getEditorClientSecret() : undefined,\n },\n }) as IntlayerConfig;\n\n // `credentials: 'omit'` prevents the browser from attaching session cookies;\n // authentication is handled exclusively through the injected Bearer token.\n // This is required because the backend only allows credentialed requests for\n // whitelisted first-party origins.\n const fetcherOptions: FetcherOptions = {\n ...baseFetcherOptions,\n credentials: 'omit',\n };\n\n const hasCredentials =\n Boolean(sessionToken) ||\n Boolean(\n resolvedConfig?.editor?.clientId && resolvedConfig?.editor?.clientSecret\n );\n\n // A session token is used as-is and never expires within the SDK's concern,\n // so inject it once up-front and skip the OAuth2 exchange.\n if (sessionToken) {\n fetcherOptions.headers = {\n ...(fetcherOptions.headers ?? {}),\n Authorization: `Bearer ${sessionToken}`,\n } as HeadersInit;\n }\n\n let accessToken: string | undefined;\n let expiryTimestamp: number | undefined;\n let pendingRefresh: Promise<void> | undefined;\n\n const needsRefresh = (): boolean => {\n if (!accessToken) return true;\n if (!expiryTimestamp) return false; // Unknown expiry: assume usable until failure.\n\n // Refresh one minute before the actual expiry to avoid edge-of-expiry races.\n return Date.now() + ONE_MINUTE_MS >= expiryTimestamp;\n };\n\n const refreshToken = async (): Promise<void> => {\n const doRefresh = async (): Promise<void> => {\n const oAuthAPI = getOAuthAPI({}, resolvedConfig);\n const response = await oAuthAPI.getOAuth2AccessToken();\n const tokenData = response?.data as OAuthTokenLike | undefined;\n\n accessToken = tokenData?.accessToken;\n expiryTimestamp = getExpiryTimestamp(tokenData);\n };\n\n // De-duplicate concurrent refreshes so a burst of calls triggers one fetch.\n pendingRefresh ??= doRefresh().finally(() => {\n pendingRefresh = undefined;\n });\n\n await pendingRefresh;\n };\n\n const applyAuthHeader = (): void => {\n if (!accessToken) return;\n fetcherOptions.headers = {\n ...(fetcherOptions.headers ?? {}),\n Authorization: `Bearer ${accessToken}`,\n } as HeadersInit;\n };\n\n const ensureAuth = async (): Promise<void> => {\n if (sessionToken) return; // Already injected; nothing to refresh.\n if (needsRefresh()) {\n await refreshToken();\n }\n applyAuthHeader();\n };\n\n const reset = (): void => {\n accessToken = undefined;\n expiryTimestamp = undefined;\n };\n\n const withAuth = <Arguments extends unknown[], Result>(\n method: AsyncMethod<Arguments, Result>,\n skipAuth = !hasCredentials\n ): AsyncMethod<Arguments, Result> => {\n // In a browser the project credentials are structurally unavailable, so an\n // authenticated call would silently go out unauthenticated and come back\n // 401. Fail with an actionable message instead: a browser must authenticate\n // as the signed-in *user* (a session token, or the session cookie on a\n // first-party origin), never as the project.\n if (skipAuth && !sessionToken && isBrowser()) {\n return async () => {\n throw new Error(\n 'Intlayer CMS credentials are not available in the browser. ' +\n '`clientSecret` is a server-side credential and is never bundled. ' +\n 'Call this from a server route, or pass a user session token: ' +\n 'createIntlayerCMS(config, { sessionToken }).'\n );\n };\n }\n\n if (skipAuth) return method;\n\n return async (...args: Arguments): Promise<Result> => {\n await ensureAuth();\n\n try {\n return await method(...args);\n } catch (error) {\n // The token may have been revoked or expired early: refresh once and\n // retry before surfacing the error. A session token cannot be\n // refreshed, so re-throw immediately in that case.\n if (sessionToken) throw error;\n\n await refreshToken();\n applyAuthHeader();\n return await method(...args);\n }\n };\n };\n\n const bindSection = <Section extends Record<string, unknown>>(\n factory: SectionFactory<Section>,\n bindOptions: BindSectionOptions = {}\n ): Section => {\n const section = factory(fetcherOptions, resolvedConfig);\n const skipAuth = bindOptions.skipAuth ?? !hasCredentials;\n\n return new Proxy(section, {\n get(target, property, receiver) {\n const value = Reflect.get(target, property, receiver);\n\n if (typeof value !== 'function') return value;\n\n return withAuth(\n (value as AsyncMethod<unknown[], unknown>).bind(target),\n skipAuth\n );\n },\n });\n };\n\n return {\n fetcherOptions,\n intlayerConfig: resolvedConfig,\n hasCredentials,\n ensureAuth,\n reset,\n withAuth,\n bindSection,\n };\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"createAuthManager.mjs","names":[],"sources":["../../../src/cms/createAuthManager.ts"],"sourcesContent":["import { editor } from '@intlayer/config/built';\nimport { getEditorClientSecret } from '@intlayer/config/secrets';\nimport type { IntlayerConfig } from '@intlayer/types/config';\nimport { defu } from 'defu';\nimport type { FetcherOptions } from '../fetcher';\nimport { getOAuthAPI } from '../getIntlayerAPI/oAuth';\n\n/**\n * Shape of the various OAuth2 token payloads the backend may return. Only the\n * fields used to derive the expiration timestamp are described here.\n */\ntype OAuthTokenLike = {\n accessToken?: string;\n accessTokenExpiresAt?: string | Date;\n expires_in?: number;\n expiresIn?: number;\n expiresAt?: string | Date;\n};\n\n/**\n * Any asynchronous API method exposed by a domain section\n * (e.g. `getDictionaries`, `pushDictionaries`).\n */\ntype AsyncMethod<Arguments extends unknown[], Result> = (\n ...args: Arguments\n) => Promise<Result>;\n\n/**\n * A domain section factory: builds a plain object of async methods from shared\n * fetcher options and the Intlayer configuration.\n */\ntype SectionFactory<Section extends Record<string, unknown>> = (\n fetcherOptions: FetcherOptions,\n intlayerConfig: IntlayerConfig\n) => Section;\n\nconst ONE_MINUTE_MS = 60_000;\n\n/**\n * OAuth2 token state for one set of credentials, shared by every manager\n * built with them.\n */\ntype AccessTokenState = {\n accessToken?: string;\n expiryTimestamp?: number;\n pendingRefresh?: Promise<void>;\n};\n\n/**\n * Process-wide token states, keyed by backend URL and credentials. Build\n * steps and watch rebuilds each create their own manager; sharing the state\n * lets them reuse one token instead of awaiting a new exchange every time.\n */\nconst accessTokenStates = new Map<string, AccessTokenState>();\n\n/** Clears every cached access token, forcing the next call to re-authenticate. */\nexport const clearAccessTokenCache = (): void => {\n accessTokenStates.clear();\n};\n\n/** Returns the shared token state for the given credentials, creating it if missing. */\nconst getAccessTokenState = (\n editorConfig: IntlayerConfig['editor'] | undefined\n): AccessTokenState => {\n const stateKey = [\n editorConfig?.backendURL,\n editorConfig?.clientId,\n editorConfig?.clientSecret,\n ].join('\\0');\n\n let accessTokenState = accessTokenStates.get(stateKey);\n\n if (!accessTokenState) {\n accessTokenState = {};\n accessTokenStates.set(stateKey, accessTokenState);\n }\n\n return accessTokenState;\n};\n\n/** HTTP statuses meaning the access token was rejected. */\nconst AUTHENTICATION_ERROR_STATUSES = new Set([401, 403]);\n\n/**\n * Whether a failed call may succeed with a fresh token: an auth rejection, or\n * an error without HTTP status. A `404` or `500` would fail again regardless.\n */\nconst isAuthenticationError = (error: unknown): boolean => {\n const status = (error as { status?: unknown } | undefined)?.status;\n\n return (\n typeof status !== 'number' || AUTHENTICATION_ERROR_STATUSES.has(status)\n );\n};\n\n/** Whether the current runtime is a browser rather than a server. */\nconst isBrowser = (): boolean => typeof window !== 'undefined';\n\n/**\n * Resolves the expiration timestamp (in ms since epoch) from an OAuth2\n * token-like payload, supporting both absolute dates and relative durations.\n */\nconst getExpiryTimestamp = (\n token: OAuthTokenLike | undefined\n): number | undefined => {\n if (!token) return undefined;\n\n const dateLike = (token.accessTokenExpiresAt ?? token.expiresAt) as\n | string\n | Date\n | undefined;\n\n if (dateLike) {\n const timestamp =\n typeof dateLike === 'string'\n ? Date.parse(dateLike)\n : dateLike.getTime?.();\n if (typeof timestamp === 'number' && Number.isFinite(timestamp)) {\n return timestamp;\n }\n }\n\n const seconds = token.expires_in ?? token.expiresIn;\n if (typeof seconds === 'number' && Number.isFinite(seconds)) {\n return Date.now() + seconds * 1000;\n }\n\n return undefined;\n};\n\n/**\n * Options accepted when creating an {@link AuthManager}.\n */\nexport type AuthManagerOptions = {\n /**\n * Base fetcher options merged into every request (e.g. custom headers).\n */\n baseFetcherOptions?: FetcherOptions;\n /**\n * A pre-issued CLI session token (prefixed with `clisession_`). When provided,\n * it is used directly as the Bearer token and the OAuth2 `client_credentials`\n * exchange is skipped entirely.\n */\n sessionToken?: string;\n};\n\n/**\n * Options accepted when binding a domain section to an {@link AuthManager}.\n */\nexport type BindSectionOptions = {\n /**\n * Forces auth injection on (`false`) or off (`true`). Defaults to enabled\n * whenever credentials are available.\n */\n skipAuth?: boolean;\n};\n\n/**\n * Manages CMS authentication for a set of API calls.\n *\n * The manager owns a single mutable {@link FetcherOptions} object\n * (`fetcherOptions`). Domain factories bound through it share the same\n * reference, so refreshing the token in place transparently authenticates every\n * call — without rebuilding the API clients.\n */\nexport type AuthManager = {\n /**\n * Mutable fetcher options carrying the `Authorization` header. Bound sections\n * share this reference so the injected token is reused everywhere.\n */\n fetcherOptions: FetcherOptions;\n /**\n * The resolved Intlayer configuration (merged with the build-time defaults).\n */\n intlayerConfig: IntlayerConfig;\n /**\n * `true` when credentials are available — either a session token or a\n * `clientId`/`clientSecret` pair in the configuration.\n */\n hasCredentials: boolean;\n /**\n * Ensures a valid access token is available and applies it to\n * `fetcherOptions`. Called automatically by {@link withAuth}.\n */\n ensureAuth: () => Promise<void>;\n /**\n * Forces a token refresh on the next call by clearing the cached token.\n */\n reset: () => void;\n /**\n * Wraps a single API method so it injects (and, on failure, refreshes) the\n * access token on every invocation while preserving the method's signature.\n */\n withAuth: <Arguments extends unknown[], Result>(\n method: AsyncMethod<Arguments, Result>,\n skipAuth?: boolean\n ) => AsyncMethod<Arguments, Result>;\n /**\n * Instantiates a domain factory with the managed fetcher options and config,\n * returning a section whose every method authenticates automatically.\n *\n * @example\n * ```ts\n * const dictionary = authManager.bindSection(getDictionaryAPI);\n * await dictionary.pushDictionaries(dictionaries);\n * ```\n */\n bindSection: <Section extends Record<string, unknown>>(\n factory: SectionFactory<Section>,\n options?: BindSectionOptions\n ) => Section;\n};\n\n/**\n * Creates an instance-scoped {@link AuthManager} handling the OAuth2\n * `client_credentials` flow (or a pre-issued CLI session token).\n *\n * Managers built with the same backend URL and credentials share one token\n * state, so a process never exchanges credentials more than needed.\n *\n * @param intlayerConfig - Intlayer configuration carrying the `editor`\n * credentials. Defaults to the build-time configuration.\n * @param options - Optional session token and base fetcher options.\n */\nexport const createAuthManager = (\n intlayerConfig: Pick<IntlayerConfig, 'editor'> = { editor },\n options: AuthManagerOptions = {}\n): AuthManager => {\n const { baseFetcherOptions = {}, sessionToken } = options;\n\n // `editor` comes from the generated build-time configuration, which never\n // carries `clientSecret` — it would be inlined into client bundles. The\n // secret is supplied server-side by `@intlayer/config/secrets`, which\n // resolves to an inert stub in the browser; `hasCredentials` then stays false\n // there and the OAuth2 exchange never runs.\n //\n // Only consulted when the configuration declares a `clientId`, so a project\n // that comments its credentials out stays uncredentialed even with the\n // environment variables still defined.\n const resolvedConfig = defu(intlayerConfig ?? {}, {\n editor: {\n ...editor,\n clientSecret: editor?.clientId ? getEditorClientSecret() : undefined,\n },\n }) as IntlayerConfig;\n\n // `credentials: 'omit'` prevents the browser from attaching session cookies;\n // authentication is handled exclusively through the injected Bearer token.\n // This is required because the backend only allows credentialed requests for\n // whitelisted first-party origins.\n const fetcherOptions: FetcherOptions = {\n ...baseFetcherOptions,\n credentials: 'omit',\n };\n\n const hasCredentials =\n Boolean(sessionToken) ||\n Boolean(\n resolvedConfig?.editor?.clientId && resolvedConfig?.editor?.clientSecret\n );\n\n // A session token is used as-is and never expires within the SDK's concern,\n // so inject it once up-front and skip the OAuth2 exchange.\n if (sessionToken) {\n fetcherOptions.headers = {\n ...(fetcherOptions.headers ?? {}),\n Authorization: `Bearer ${sessionToken}`,\n } as HeadersInit;\n }\n\n const accessTokenState = getAccessTokenState(resolvedConfig?.editor);\n\n const needsRefresh = (): boolean => {\n const { accessToken, expiryTimestamp } = accessTokenState;\n\n if (!accessToken) return true;\n if (!expiryTimestamp) return false; // Unknown expiry: assume usable until failure.\n\n // Refresh one minute before the actual expiry to avoid edge-of-expiry races.\n return Date.now() + ONE_MINUTE_MS >= expiryTimestamp;\n };\n\n const refreshToken = async (): Promise<void> => {\n const doRefresh = async (): Promise<void> => {\n const oAuthAPI = getOAuthAPI({}, resolvedConfig);\n const response = await oAuthAPI.getOAuth2AccessToken();\n const tokenData = response?.data as OAuthTokenLike | undefined;\n\n accessTokenState.accessToken = tokenData?.accessToken;\n accessTokenState.expiryTimestamp = getExpiryTimestamp(tokenData);\n };\n\n // De-duplicate concurrent refreshes so a burst of calls triggers one fetch.\n accessTokenState.pendingRefresh ??= doRefresh().finally(() => {\n accessTokenState.pendingRefresh = undefined;\n });\n\n await accessTokenState.pendingRefresh;\n };\n\n const applyAuthHeader = (): void => {\n if (!accessTokenState.accessToken) return;\n fetcherOptions.headers = {\n ...(fetcherOptions.headers ?? {}),\n Authorization: `Bearer ${accessTokenState.accessToken}`,\n } as HeadersInit;\n };\n\n const ensureAuth = async (): Promise<void> => {\n if (sessionToken) return; // Already injected; nothing to refresh.\n if (needsRefresh()) {\n await refreshToken();\n }\n applyAuthHeader();\n };\n\n const reset = (): void => {\n accessTokenState.accessToken = undefined;\n accessTokenState.expiryTimestamp = undefined;\n };\n\n const withAuth = <Arguments extends unknown[], Result>(\n method: AsyncMethod<Arguments, Result>,\n skipAuth = !hasCredentials\n ): AsyncMethod<Arguments, Result> => {\n // In a browser the project credentials are structurally unavailable, so an\n // authenticated call would silently go out unauthenticated and come back\n // 401. Fail with an actionable message instead: a browser must authenticate\n // as the signed-in *user* (a session token, or the session cookie on a\n // first-party origin), never as the project.\n if (skipAuth && !sessionToken && isBrowser()) {\n return async () => {\n throw new Error(\n 'Intlayer CMS credentials are not available in the browser. ' +\n '`clientSecret` is a server-side credential and is never bundled. ' +\n 'Call this from a server route, or pass a user session token: ' +\n 'createIntlayerCMS(config, { sessionToken }).'\n );\n };\n }\n\n if (skipAuth) return method;\n\n return async (...args: Arguments): Promise<Result> => {\n await ensureAuth();\n\n try {\n return await method(...args);\n } catch (error) {\n // The token may have been revoked or expired early: refresh once and\n // retry before surfacing the error. A session token cannot be\n // refreshed, so re-throw immediately in that case.\n if (sessionToken || !isAuthenticationError(error)) throw error;\n\n await refreshToken();\n applyAuthHeader();\n return await method(...args);\n }\n };\n };\n\n const bindSection = <Section extends Record<string, unknown>>(\n factory: SectionFactory<Section>,\n bindOptions: BindSectionOptions = {}\n ): Section => {\n const section = factory(fetcherOptions, resolvedConfig);\n const skipAuth = bindOptions.skipAuth ?? !hasCredentials;\n\n return new Proxy(section, {\n get(target, property, receiver) {\n const value = Reflect.get(target, property, receiver);\n\n if (typeof value !== 'function') return value;\n\n return withAuth(\n (value as AsyncMethod<unknown[], unknown>).bind(target),\n skipAuth\n );\n },\n });\n };\n\n return {\n fetcherOptions,\n intlayerConfig: resolvedConfig,\n hasCredentials,\n ensureAuth,\n reset,\n withAuth,\n bindSection,\n };\n};\n"],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import { createAuthManager } from "./createAuthManager.mjs";
1
+ import { clearAccessTokenCache, createAuthManager } from "./createAuthManager.mjs";
2
2
  import { createEndpoint, createIntlayerCMS } from "./createIntlayerCMS.mjs";
3
3
 
4
- export { createAuthManager, createEndpoint, createIntlayerCMS };
4
+ export { clearAccessTokenCache, createAuthManager, createEndpoint, createIntlayerCMS };
@@ -42,12 +42,22 @@ const getAnalyticsAPI = (authAPIOptions = {}, intlayerConfig) => {
42
42
  * @returns The experiment result set.
43
43
  */
44
44
  const getExperimentResults = async (experimentKey, otherOptions = {}) => await fetcher(`${ANALYTICS_API_ROUTE}/experiments/${experimentKey}`, authAPIOptions, otherOptions, { method: "GET" });
45
+ /**
46
+ * Page metadata (title and meta description) for a given url.
47
+ * @param url - The page url to inspect.
48
+ * @returns Title and description of the page.
49
+ */
50
+ const getPageMetadata = async (url, otherOptions = {}) => await fetcher(`${ANALYTICS_API_ROUTE}/page-metadata`, authAPIOptions, otherOptions, {
51
+ method: "GET",
52
+ params: { url }
53
+ });
45
54
  return {
46
55
  sendEvents,
47
56
  getOverview,
48
57
  getAudience,
49
58
  getContentStats,
50
- getExperimentResults
59
+ getExperimentResults,
60
+ getPageMetadata
51
61
  };
52
62
  };
53
63
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"analytics.mjs","names":[],"sources":["../../../src/getIntlayerAPI/analytics.ts"],"sourcesContent":["import type {\n AudienceRange,\n GetAnalyticsOverviewResult,\n GetAudienceResult,\n GetContentStatsResult,\n GetExperimentResultsResult,\n IngestAnalyticsBody,\n IngestAnalyticsResult,\n} from '@intlayer/backend';\nimport { editor } from '@intlayer/config/built';\nimport type { IntlayerConfig } from '@intlayer/types/config';\nimport { createEndpoint } from '../cms/createIntlayerCMS';\nimport { type FetcherOptions, fetcher } from '../fetcher';\n\nexport const getAnalyticsAPI = (\n authAPIOptions: FetcherOptions = {},\n intlayerConfig?: IntlayerConfig\n) => {\n const backendURL = intlayerConfig?.editor?.backendURL ?? editor.backendURL;\n\n const ANALYTICS_API_ROUTE = `${backendURL}/api/analytics`;\n\n /**\n * Ingest a batch of analytics events. Public — attribution is by the\n * project's `clientId` in the body.\n * @param body - Session id, sdk version, and the collected events.\n * @returns The number of accepted events.\n */\n const sendEvents = async (\n body: IngestAnalyticsBody,\n otherOptions: FetcherOptions = {}\n ) =>\n await fetcher<IngestAnalyticsResult>(\n `${ANALYTICS_API_ROUTE}/events`,\n authAPIOptions,\n otherOptions,\n {\n method: 'POST',\n body,\n }\n );\n\n /**\n * Page/locale totals for the authenticated project.\n * @returns Views grouped by url and locale.\n */\n const getOverview = async (otherOptions: FetcherOptions = {}) =>\n await fetcher<GetAnalyticsOverviewResult>(\n `${ANALYTICS_API_ROUTE}/overview`,\n authAPIOptions,\n otherOptions,\n { method: 'GET' }\n );\n\n /**\n * Audience report: distinct visitors (today / 7d / window), page views, the\n * evolution series, and locale + country breakdowns.\n * @param range - Rolling window, either a named range (`1h`, `24h`, `7d`,\n * `30d`, `90d`, `6mo`, `1y`, `3y`) or a number of days (default 30).\n * @returns The audience statistics.\n */\n const getAudience = async (\n range: AudienceRange | number = 30,\n otherOptions: FetcherOptions = {}\n ) =>\n await fetcher<GetAudienceResult>(\n `${ANALYTICS_API_ROUTE}/audience`,\n authAPIOptions,\n otherOptions,\n {\n method: 'GET',\n params: typeof range === 'number' ? { days: String(range) } : { range },\n }\n );\n\n /**\n * Per-content exposure totals — \"which content is actually shown\".\n * @returns Exposures grouped by dictionary key, key path, and locale.\n */\n const getContentStats = async (otherOptions: FetcherOptions = {}) =>\n await fetcher<GetContentStatsResult>(\n `${ANALYTICS_API_ROUTE}/content-stats`,\n authAPIOptions,\n otherOptions,\n { method: 'GET' }\n );\n\n /**\n * A/B experiment results with per-variant conversion rates and significance.\n * @param experimentKey - The experiment to evaluate.\n * @returns The experiment result set.\n */\n const getExperimentResults = async (\n experimentKey: string,\n otherOptions: FetcherOptions = {}\n ) =>\n await fetcher<GetExperimentResultsResult>(\n `${ANALYTICS_API_ROUTE}/experiments/${experimentKey}`,\n authAPIOptions,\n otherOptions,\n { method: 'GET' }\n );\n\n return {\n sendEvents,\n getOverview,\n getAudience,\n getContentStats,\n getExperimentResults,\n };\n};\n\n/**\n * Authenticated `analytics` endpoint bound to an Intlayer CMS authenticator.\n *\n * Pass an authenticator created with `createIntlayerCMS`, or omit it to use\n * the build-time configuration (`@intlayer/config/built`).\n */\nexport const analyticsEndpoint = createEndpoint(getAnalyticsAPI);\n"],"mappings":";;;;;AAcA,MAAa,mBACX,iBAAiC,CAAC,GAClC,mBACG;CAGH,MAAM,sBAAsB,GAFT,gBAAgB,QAAQ,cAAc,OAAO,WAEtB;;;;;;;CAQ1C,MAAM,aAAa,OACjB,MACA,eAA+B,CAAC,MAEhC,MAAM,QACJ,GAAG,oBAAoB,UACvB,gBACA,cACA;EACE,QAAQ;EACR;CACF,CACF;;;;;CAMF,MAAM,cAAc,OAAO,eAA+B,CAAC,MACzD,MAAM,QACJ,GAAG,oBAAoB,YACvB,gBACA,cACA,EAAE,QAAQ,MAAM,CAClB;;;;;;;;CASF,MAAM,cAAc,OAClB,QAAgC,IAChC,eAA+B,CAAC,MAEhC,MAAM,QACJ,GAAG,oBAAoB,YACvB,gBACA,cACA;EACE,QAAQ;EACR,QAAQ,OAAO,UAAU,WAAW,EAAE,MAAM,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM;CACxE,CACF;;;;;CAMF,MAAM,kBAAkB,OAAO,eAA+B,CAAC,MAC7D,MAAM,QACJ,GAAG,oBAAoB,iBACvB,gBACA,cACA,EAAE,QAAQ,MAAM,CAClB;;;;;;CAOF,MAAM,uBAAuB,OAC3B,eACA,eAA+B,CAAC,MAEhC,MAAM,QACJ,GAAG,oBAAoB,eAAe,iBACtC,gBACA,cACA,EAAE,QAAQ,MAAM,CAClB;CAEF,OAAO;EACL;EACA;EACA;EACA;EACA;CACF;AACF;;;;;;;AAQA,MAAa,oBAAoB,eAAe,eAAe"}
1
+ {"version":3,"file":"analytics.mjs","names":[],"sources":["../../../src/getIntlayerAPI/analytics.ts"],"sourcesContent":["import type {\n AudienceRange,\n GetAnalyticsOverviewResult,\n GetAudienceResult,\n GetContentStatsResult,\n GetExperimentResultsResult,\n GetPageMetadataResult,\n IngestAnalyticsBody,\n IngestAnalyticsResult,\n} from '@intlayer/backend';\nimport { editor } from '@intlayer/config/built';\nimport type { IntlayerConfig } from '@intlayer/types/config';\nimport { createEndpoint } from '../cms/createIntlayerCMS';\nimport { type FetcherOptions, fetcher } from '../fetcher';\n\nexport const getAnalyticsAPI = (\n authAPIOptions: FetcherOptions = {},\n intlayerConfig?: IntlayerConfig\n) => {\n const backendURL = intlayerConfig?.editor?.backendURL ?? editor.backendURL;\n\n const ANALYTICS_API_ROUTE = `${backendURL}/api/analytics`;\n\n /**\n * Ingest a batch of analytics events. Public — attribution is by the\n * project's `clientId` in the body.\n * @param body - Session id, sdk version, and the collected events.\n * @returns The number of accepted events.\n */\n const sendEvents = async (\n body: IngestAnalyticsBody,\n otherOptions: FetcherOptions = {}\n ) =>\n await fetcher<IngestAnalyticsResult>(\n `${ANALYTICS_API_ROUTE}/events`,\n authAPIOptions,\n otherOptions,\n {\n method: 'POST',\n body,\n }\n );\n\n /**\n * Page/locale totals for the authenticated project.\n * @returns Views grouped by url and locale.\n */\n const getOverview = async (otherOptions: FetcherOptions = {}) =>\n await fetcher<GetAnalyticsOverviewResult>(\n `${ANALYTICS_API_ROUTE}/overview`,\n authAPIOptions,\n otherOptions,\n { method: 'GET' }\n );\n\n /**\n * Audience report: distinct visitors (today / 7d / window), page views, the\n * evolution series, and locale + country breakdowns.\n * @param range - Rolling window, either a named range (`1h`, `24h`, `7d`,\n * `30d`, `90d`, `6mo`, `1y`, `3y`) or a number of days (default 30).\n * @returns The audience statistics.\n */\n const getAudience = async (\n range: AudienceRange | number = 30,\n otherOptions: FetcherOptions = {}\n ) =>\n await fetcher<GetAudienceResult>(\n `${ANALYTICS_API_ROUTE}/audience`,\n authAPIOptions,\n otherOptions,\n {\n method: 'GET',\n params: typeof range === 'number' ? { days: String(range) } : { range },\n }\n );\n\n /**\n * Per-content exposure totals — \"which content is actually shown\".\n * @returns Exposures grouped by dictionary key, key path, and locale.\n */\n const getContentStats = async (otherOptions: FetcherOptions = {}) =>\n await fetcher<GetContentStatsResult>(\n `${ANALYTICS_API_ROUTE}/content-stats`,\n authAPIOptions,\n otherOptions,\n { method: 'GET' }\n );\n\n /**\n * A/B experiment results with per-variant conversion rates and significance.\n * @param experimentKey - The experiment to evaluate.\n * @returns The experiment result set.\n */\n const getExperimentResults = async (\n experimentKey: string,\n otherOptions: FetcherOptions = {}\n ) =>\n await fetcher<GetExperimentResultsResult>(\n `${ANALYTICS_API_ROUTE}/experiments/${experimentKey}`,\n authAPIOptions,\n otherOptions,\n { method: 'GET' }\n );\n\n /**\n * Page metadata (title and meta description) for a given url.\n * @param url - The page url to inspect.\n * @returns Title and description of the page.\n */\n const getPageMetadata = async (\n url: string,\n otherOptions: FetcherOptions = {}\n ) =>\n await fetcher<GetPageMetadataResult>(\n `${ANALYTICS_API_ROUTE}/page-metadata`,\n authAPIOptions,\n otherOptions,\n {\n method: 'GET',\n params: { url },\n }\n );\n\n return {\n sendEvents,\n getOverview,\n getAudience,\n getContentStats,\n getExperimentResults,\n getPageMetadata,\n };\n};\n\n/**\n * Authenticated `analytics` endpoint bound to an Intlayer CMS authenticator.\n *\n * Pass an authenticator created with `createIntlayerCMS`, or omit it to use\n * the build-time configuration (`@intlayer/config/built`).\n */\nexport const analyticsEndpoint = createEndpoint(getAnalyticsAPI);\n"],"mappings":";;;;;AAeA,MAAa,mBACX,iBAAiC,CAAC,GAClC,mBACG;CAGH,MAAM,sBAAsB,GAFT,gBAAgB,QAAQ,cAAc,OAAO,WAEtB;;;;;;;CAQ1C,MAAM,aAAa,OACjB,MACA,eAA+B,CAAC,MAEhC,MAAM,QACJ,GAAG,oBAAoB,UACvB,gBACA,cACA;EACE,QAAQ;EACR;CACF,CACF;;;;;CAMF,MAAM,cAAc,OAAO,eAA+B,CAAC,MACzD,MAAM,QACJ,GAAG,oBAAoB,YACvB,gBACA,cACA,EAAE,QAAQ,MAAM,CAClB;;;;;;;;CASF,MAAM,cAAc,OAClB,QAAgC,IAChC,eAA+B,CAAC,MAEhC,MAAM,QACJ,GAAG,oBAAoB,YACvB,gBACA,cACA;EACE,QAAQ;EACR,QAAQ,OAAO,UAAU,WAAW,EAAE,MAAM,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM;CACxE,CACF;;;;;CAMF,MAAM,kBAAkB,OAAO,eAA+B,CAAC,MAC7D,MAAM,QACJ,GAAG,oBAAoB,iBACvB,gBACA,cACA,EAAE,QAAQ,MAAM,CAClB;;;;;;CAOF,MAAM,uBAAuB,OAC3B,eACA,eAA+B,CAAC,MAEhC,MAAM,QACJ,GAAG,oBAAoB,eAAe,iBACtC,gBACA,cACA,EAAE,QAAQ,MAAM,CAClB;;;;;;CAOF,MAAM,kBAAkB,OACtB,KACA,eAA+B,CAAC,MAEhC,MAAM,QACJ,GAAG,oBAAoB,iBACvB,gBACA,cACA;EACE,QAAQ;EACR,QAAQ,EAAE,IAAI;CAChB,CACF;CAEF,OAAO;EACL;EACA;EACA;EACA;EACA;EACA;CACF;AACF;;;;;;;AAQA,MAAa,oBAAoB,eAAe,eAAe"}
@@ -23,8 +23,9 @@ const getAuditAPI = (authAPIOptions = {}, intlayerConfig) => {
23
23
  });
24
24
  const scanUrl = async (body, otherOptions = {}) => {
25
25
  if (!body?.url) return;
26
- const { url, onMessage, onDone } = body;
26
+ const { url, refresh, onMessage, onDone } = body;
27
27
  const params = new URLSearchParams({ url });
28
+ if (refresh) params.set("refresh", "true");
28
29
  const endpoint = `${AUDIT_API_ROUTE}?${params.toString()}`;
29
30
  const response = await fetch(endpoint, {
30
31
  method: "GET",
@@ -81,7 +82,29 @@ const getAuditAPI = (authAPIOptions = {}, intlayerConfig) => {
81
82
  const cancelRecursiveAudit = async (params, otherOptions = {}) => await fetcher(`${AUDIT_API_ROUTE}/recursive/${params?.jobId}/cancel`, authAPIOptions, otherOptions, { method: "POST" });
82
83
  const pauseRecursiveAudit = async (params, otherOptions = {}) => await fetcher(`${AUDIT_API_ROUTE}/recursive/${params?.jobId}/pause`, authAPIOptions, otherOptions, { method: "POST" });
83
84
  const resumeRecursiveAudit = async (params, otherOptions = {}) => await fetcher(`${AUDIT_API_ROUTE}/recursive/${params?.jobId}/resume`, authAPIOptions, otherOptions, { method: "POST" });
85
+ /**
86
+ * Admin — number of scanned domains using each technology.
87
+ */
88
+ const getTechnologyUsage = async (params, otherOptions = {}) => await fetcher(`${AUDIT_API_ROUTE}/technologies`, authAPIOptions, otherOptions, {
89
+ method: "GET",
90
+ params
91
+ });
92
+ /**
93
+ * Admin — scanned hosts, most recently scanned first, filterable by
94
+ * technology.
95
+ */
96
+ const getScannedHosts = async (params, otherOptions = {}) => await fetcher(`${AUDIT_API_ROUTE}/hosts`, authAPIOptions, otherOptions, {
97
+ method: "GET",
98
+ params
99
+ });
100
+ /**
101
+ * Admin — a scanned host with its stored scans, newest first.
102
+ */
103
+ const getScannedHost = async (params, otherOptions = {}) => await fetcher(`${AUDIT_API_ROUTE}/hosts/${encodeURIComponent(params?.host ?? "")}`, authAPIOptions, otherOptions, { method: "GET" });
84
104
  return {
105
+ getTechnologyUsage,
106
+ getScannedHosts,
107
+ getScannedHost,
85
108
  discoverUrls,
86
109
  scanUrl,
87
110
  startRecursiveAudit,
@@ -1 +1 @@
1
- {"version":3,"file":"audit.mjs","names":[],"sources":["../../../src/getIntlayerAPI/audit.ts"],"sourcesContent":["import type {\n AuditEvent,\n GetRecursiveAuditStatusResult,\n StartRecursiveAuditResult,\n} from '@intlayer/backend';\nimport { editor } from '@intlayer/config/built';\nimport type { IntlayerConfig } from '@intlayer/types/config';\nimport { createEndpoint } from '../cms/createIntlayerCMS';\nimport { type FetcherOptions, fetcher } from '../fetcher';\n\nexport type {\n AuditEvent,\n GetRecursiveAuditStatusResult,\n StartRecursiveAuditResult,\n};\n\nexport type ScanUrlBody = {\n url: string;\n onMessage?: (event: AuditEvent) => void;\n onDone?: () => void;\n};\n\nexport type DiscoverUrlsParams = {\n url: string;\n};\n\nexport type DiscoverUrlsResult = {\n urls: string[];\n};\n\nexport type StartRecursiveAuditBody = {\n url: string;\n urls?: string[];\n};\n\nexport type GetRecursiveAuditStatusParams = {\n jobId: string;\n};\n\nexport type RecursiveAuditJobParams = {\n jobId: string;\n};\n\nexport const getAuditAPI = (\n authAPIOptions: FetcherOptions = {},\n intlayerConfig?: IntlayerConfig\n) => {\n const backendURL = intlayerConfig?.editor?.backendURL ?? editor.backendURL;\n\n const AUDIT_API_ROUTE = `${backendURL}/api/scan`;\n\n /**\n * Streams a single-page SEO audit as Server-Sent Events.\n *\n * Usage:\n * ```ts\n * await audit.scanUrl({\n * url: 'https://example.com',\n * onMessage: (event) => console.log(event),\n * onDone: () => console.log('done'),\n * });\n * ```\n */\n const discoverUrls = async (\n params?: DiscoverUrlsParams,\n otherOptions: FetcherOptions = {}\n ) =>\n await fetcher<DiscoverUrlsResult>(\n `${AUDIT_API_ROUTE}/recursive/discover`,\n authAPIOptions,\n otherOptions,\n {\n method: 'GET',\n params,\n }\n );\n\n const scanUrl = async (\n body?: ScanUrlBody,\n otherOptions: FetcherOptions = {}\n ) => {\n if (!body?.url) return;\n\n const { url, onMessage, onDone } = body;\n\n const params = new URLSearchParams({ url });\n const endpoint = `${AUDIT_API_ROUTE}?${params.toString()}`;\n\n const response = await fetch(endpoint, {\n method: 'GET',\n headers: {\n Accept: 'text/event-stream',\n ...authAPIOptions.headers,\n ...otherOptions.headers,\n },\n credentials: 'include',\n signal: otherOptions.signal,\n });\n\n if (!response.ok) {\n let errorMessage = 'An error occurred';\n try {\n const errorData = await response.json();\n errorMessage = JSON.stringify(errorData.error) ?? errorMessage;\n } catch {\n try {\n const errorText = await response.text();\n if (errorText) errorMessage = errorText;\n } catch {\n // ignore\n }\n }\n throw new Error(errorMessage);\n }\n\n const reader = response.body?.getReader();\n if (!reader) throw new Error('No reader available');\n\n const decoder = new TextDecoder();\n let buffer = '';\n\n while (true) {\n const { done, value } = await reader.read();\n\n if (done) break;\n\n buffer += decoder.decode(value, { stream: true });\n const lines = buffer.split('\\n');\n buffer = lines.pop() ?? '';\n\n for (const line of lines) {\n if (line.startsWith('data: ')) {\n try {\n const event = JSON.parse(line.slice(6)) as AuditEvent;\n onMessage?.(event);\n } catch {\n // ignore malformed lines\n }\n }\n }\n }\n\n onDone?.();\n };\n\n /**\n * Starts a recursive audit job for the given URL.\n */\n const startRecursiveAudit = async (\n body?: StartRecursiveAuditBody,\n otherOptions: FetcherOptions = {}\n ) =>\n await fetcher<StartRecursiveAuditResult>(\n `${AUDIT_API_ROUTE}/recursive/start`,\n authAPIOptions,\n otherOptions,\n {\n method: 'POST',\n params: body?.url ? { url: body.url } : undefined,\n body: body?.urls !== undefined ? { urls: body.urls as any } : undefined,\n }\n );\n\n /**\n * Gets the status of a recursive audit job.\n */\n const getRecursiveAuditStatus = async (\n params?: GetRecursiveAuditStatusParams,\n otherOptions: FetcherOptions = {}\n ) =>\n await fetcher<GetRecursiveAuditStatusResult>(\n `${AUDIT_API_ROUTE}/recursive/${params?.jobId}`,\n authAPIOptions,\n otherOptions,\n {\n method: 'GET',\n }\n );\n\n const cancelRecursiveAudit = async (\n params?: RecursiveAuditJobParams,\n otherOptions: FetcherOptions = {}\n ) =>\n await fetcher<{ success: boolean }>(\n `${AUDIT_API_ROUTE}/recursive/${params?.jobId}/cancel`,\n authAPIOptions,\n otherOptions,\n { method: 'POST' }\n );\n\n const pauseRecursiveAudit = async (\n params?: RecursiveAuditJobParams,\n otherOptions: FetcherOptions = {}\n ) =>\n await fetcher<{ success: boolean }>(\n `${AUDIT_API_ROUTE}/recursive/${params?.jobId}/pause`,\n authAPIOptions,\n otherOptions,\n { method: 'POST' }\n );\n\n const resumeRecursiveAudit = async (\n params?: RecursiveAuditJobParams,\n otherOptions: FetcherOptions = {}\n ) =>\n await fetcher<{ success: boolean }>(\n `${AUDIT_API_ROUTE}/recursive/${params?.jobId}/resume`,\n authAPIOptions,\n otherOptions,\n { method: 'POST' }\n );\n\n return {\n discoverUrls,\n scanUrl,\n startRecursiveAudit,\n getRecursiveAuditStatus,\n cancelRecursiveAudit,\n pauseRecursiveAudit,\n resumeRecursiveAudit,\n };\n};\n\n/**\n * Authenticated `audit` endpoint bound to an Intlayer CMS authenticator.\n *\n * Pass an authenticator created with `createIntlayerCMS`, or omit it to use\n * the build-time configuration (`@intlayer/config/built`).\n */\nexport const auditEndpoint = createEndpoint(getAuditAPI);\n"],"mappings":";;;;;AA2CA,MAAa,eACX,iBAAiC,CAAC,GAClC,mBACG;CAGH,MAAM,kBAAkB,GAFL,gBAAgB,QAAQ,cAAc,OAAO,WAE1B;;;;;;;;;;;;;CActC,MAAM,eAAe,OACnB,QACA,eAA+B,CAAC,MAEhC,MAAM,QACJ,GAAG,gBAAgB,sBACnB,gBACA,cACA;EACE,QAAQ;EACR;CACF,CACF;CAEF,MAAM,UAAU,OACd,MACA,eAA+B,CAAC,MAC7B;EACH,IAAI,CAAC,MAAM,KAAK;EAEhB,MAAM,EAAE,KAAK,WAAW,WAAW;EAEnC,MAAM,SAAS,IAAI,gBAAgB,EAAE,IAAI,CAAC;EAC1C,MAAM,WAAW,GAAG,gBAAgB,GAAG,OAAO,SAAS;EAEvD,MAAM,WAAW,MAAM,MAAM,UAAU;GACrC,QAAQ;GACR,SAAS;IACP,QAAQ;IACR,GAAG,eAAe;IAClB,GAAG,aAAa;GAClB;GACA,aAAa;GACb,QAAQ,aAAa;EACvB,CAAC;EAED,IAAI,CAAC,SAAS,IAAI;GAChB,IAAI,eAAe;GACnB,IAAI;IACF,MAAM,YAAY,MAAM,SAAS,KAAK;IACtC,eAAe,KAAK,UAAU,UAAU,KAAK,KAAK;GACpD,QAAQ;IACN,IAAI;KACF,MAAM,YAAY,MAAM,SAAS,KAAK;KACtC,IAAI,WAAW,eAAe;IAChC,QAAQ,CAER;GACF;GACA,MAAM,IAAI,MAAM,YAAY;EAC9B;EAEA,MAAM,SAAS,SAAS,MAAM,UAAU;EACxC,IAAI,CAAC,QAAQ,MAAM,IAAI,MAAM,qBAAqB;EAElD,MAAM,UAAU,IAAI,YAAY;EAChC,IAAI,SAAS;EAEb,OAAO,MAAM;GACX,MAAM,EAAE,MAAM,UAAU,MAAM,OAAO,KAAK;GAE1C,IAAI,MAAM;GAEV,UAAU,QAAQ,OAAO,OAAO,EAAE,QAAQ,KAAK,CAAC;GAChD,MAAM,QAAQ,OAAO,MAAM,IAAI;GAC/B,SAAS,MAAM,IAAI,KAAK;GAExB,KAAK,MAAM,QAAQ,OACjB,IAAI,KAAK,WAAW,QAAQ,GAC1B,IAAI;IACF,MAAM,QAAQ,KAAK,MAAM,KAAK,MAAM,CAAC,CAAC;IACtC,YAAY,KAAK;GACnB,QAAQ,CAER;EAGN;EAEA,SAAS;CACX;;;;CAKA,MAAM,sBAAsB,OAC1B,MACA,eAA+B,CAAC,MAEhC,MAAM,QACJ,GAAG,gBAAgB,mBACnB,gBACA,cACA;EACE,QAAQ;EACR,QAAQ,MAAM,MAAM,EAAE,KAAK,KAAK,IAAI,IAAI;EACxC,MAAM,MAAM,SAAS,SAAY,EAAE,MAAM,KAAK,KAAY,IAAI;CAChE,CACF;;;;CAKF,MAAM,0BAA0B,OAC9B,QACA,eAA+B,CAAC,MAEhC,MAAM,QACJ,GAAG,gBAAgB,aAAa,QAAQ,SACxC,gBACA,cACA,EACE,QAAQ,MACV,CACF;CAEF,MAAM,uBAAuB,OAC3B,QACA,eAA+B,CAAC,MAEhC,MAAM,QACJ,GAAG,gBAAgB,aAAa,QAAQ,MAAM,UAC9C,gBACA,cACA,EAAE,QAAQ,OAAO,CACnB;CAEF,MAAM,sBAAsB,OAC1B,QACA,eAA+B,CAAC,MAEhC,MAAM,QACJ,GAAG,gBAAgB,aAAa,QAAQ,MAAM,SAC9C,gBACA,cACA,EAAE,QAAQ,OAAO,CACnB;CAEF,MAAM,uBAAuB,OAC3B,QACA,eAA+B,CAAC,MAEhC,MAAM,QACJ,GAAG,gBAAgB,aAAa,QAAQ,MAAM,UAC9C,gBACA,cACA,EAAE,QAAQ,OAAO,CACnB;CAEF,OAAO;EACL;EACA;EACA;EACA;EACA;EACA;EACA;CACF;AACF;;;;;;;AAQA,MAAa,gBAAgB,eAAe,WAAW"}
1
+ {"version":3,"file":"audit.mjs","names":[],"sources":["../../../src/getIntlayerAPI/audit.ts"],"sourcesContent":["import type {\n AuditEvent,\n GetRecursiveAuditStatusResult,\n GetScannedHostParams,\n GetScannedHostResult,\n GetScannedHostsQuery,\n GetScannedHostsResult,\n GetTechnologyUsageQuery,\n GetTechnologyUsageResult,\n StartRecursiveAuditResult,\n} from '@intlayer/backend';\nimport { editor } from '@intlayer/config/built';\nimport type { IntlayerConfig } from '@intlayer/types/config';\nimport { createEndpoint } from '../cms/createIntlayerCMS';\nimport { type FetcherOptions, fetcher } from '../fetcher';\n\nexport type {\n AuditEvent,\n GetRecursiveAuditStatusResult,\n GetScannedHostParams,\n GetScannedHostResult,\n GetScannedHostsQuery,\n GetScannedHostsResult,\n GetTechnologyUsageQuery,\n GetTechnologyUsageResult,\n StartRecursiveAuditResult,\n};\n\nexport type ScanUrlBody = {\n url: string;\n /**\n * Force a new audit. Without it, a URL audited less than an hour ago is\n * replayed from cache (its first event carries `cachedAt`).\n */\n refresh?: boolean;\n onMessage?: (event: AuditEvent) => void;\n onDone?: () => void;\n};\n\nexport type DiscoverUrlsParams = {\n url: string;\n};\n\nexport type DiscoverUrlsResult = {\n urls: string[];\n};\n\nexport type StartRecursiveAuditBody = {\n url: string;\n urls?: string[];\n};\n\nexport type GetRecursiveAuditStatusParams = {\n jobId: string;\n};\n\nexport type RecursiveAuditJobParams = {\n jobId: string;\n};\n\nexport const getAuditAPI = (\n authAPIOptions: FetcherOptions = {},\n intlayerConfig?: IntlayerConfig\n) => {\n const backendURL = intlayerConfig?.editor?.backendURL ?? editor.backendURL;\n\n const AUDIT_API_ROUTE = `${backendURL}/api/scan`;\n\n /**\n * Streams a single-page SEO audit as Server-Sent Events.\n *\n * Usage:\n * ```ts\n * await audit.scanUrl({\n * url: 'https://example.com',\n * onMessage: (event) => console.log(event),\n * onDone: () => console.log('done'),\n * });\n * ```\n */\n const discoverUrls = async (\n params?: DiscoverUrlsParams,\n otherOptions: FetcherOptions = {}\n ) =>\n await fetcher<DiscoverUrlsResult>(\n `${AUDIT_API_ROUTE}/recursive/discover`,\n authAPIOptions,\n otherOptions,\n {\n method: 'GET',\n params,\n }\n );\n\n const scanUrl = async (\n body?: ScanUrlBody,\n otherOptions: FetcherOptions = {}\n ) => {\n if (!body?.url) return;\n\n const { url, refresh, onMessage, onDone } = body;\n\n const params = new URLSearchParams({ url });\n if (refresh) params.set('refresh', 'true');\n const endpoint = `${AUDIT_API_ROUTE}?${params.toString()}`;\n\n const response = await fetch(endpoint, {\n method: 'GET',\n headers: {\n Accept: 'text/event-stream',\n ...authAPIOptions.headers,\n ...otherOptions.headers,\n },\n credentials: 'include',\n signal: otherOptions.signal,\n });\n\n if (!response.ok) {\n let errorMessage = 'An error occurred';\n try {\n const errorData = await response.json();\n errorMessage = JSON.stringify(errorData.error) ?? errorMessage;\n } catch {\n try {\n const errorText = await response.text();\n if (errorText) errorMessage = errorText;\n } catch {\n // ignore\n }\n }\n throw new Error(errorMessage);\n }\n\n const reader = response.body?.getReader();\n if (!reader) throw new Error('No reader available');\n\n const decoder = new TextDecoder();\n let buffer = '';\n\n while (true) {\n const { done, value } = await reader.read();\n\n if (done) break;\n\n buffer += decoder.decode(value, { stream: true });\n const lines = buffer.split('\\n');\n buffer = lines.pop() ?? '';\n\n for (const line of lines) {\n if (line.startsWith('data: ')) {\n try {\n const event = JSON.parse(line.slice(6)) as AuditEvent;\n onMessage?.(event);\n } catch {\n // ignore malformed lines\n }\n }\n }\n }\n\n onDone?.();\n };\n\n /**\n * Starts a recursive audit job for the given URL.\n */\n const startRecursiveAudit = async (\n body?: StartRecursiveAuditBody,\n otherOptions: FetcherOptions = {}\n ) =>\n await fetcher<StartRecursiveAuditResult>(\n `${AUDIT_API_ROUTE}/recursive/start`,\n authAPIOptions,\n otherOptions,\n {\n method: 'POST',\n params: body?.url ? { url: body.url } : undefined,\n body: body?.urls !== undefined ? { urls: body.urls as any } : undefined,\n }\n );\n\n /**\n * Gets the status of a recursive audit job.\n */\n const getRecursiveAuditStatus = async (\n params?: GetRecursiveAuditStatusParams,\n otherOptions: FetcherOptions = {}\n ) =>\n await fetcher<GetRecursiveAuditStatusResult>(\n `${AUDIT_API_ROUTE}/recursive/${params?.jobId}`,\n authAPIOptions,\n otherOptions,\n {\n method: 'GET',\n }\n );\n\n const cancelRecursiveAudit = async (\n params?: RecursiveAuditJobParams,\n otherOptions: FetcherOptions = {}\n ) =>\n await fetcher<{ success: boolean }>(\n `${AUDIT_API_ROUTE}/recursive/${params?.jobId}/cancel`,\n authAPIOptions,\n otherOptions,\n { method: 'POST' }\n );\n\n const pauseRecursiveAudit = async (\n params?: RecursiveAuditJobParams,\n otherOptions: FetcherOptions = {}\n ) =>\n await fetcher<{ success: boolean }>(\n `${AUDIT_API_ROUTE}/recursive/${params?.jobId}/pause`,\n authAPIOptions,\n otherOptions,\n { method: 'POST' }\n );\n\n const resumeRecursiveAudit = async (\n params?: RecursiveAuditJobParams,\n otherOptions: FetcherOptions = {}\n ) =>\n await fetcher<{ success: boolean }>(\n `${AUDIT_API_ROUTE}/recursive/${params?.jobId}/resume`,\n authAPIOptions,\n otherOptions,\n { method: 'POST' }\n );\n\n /**\n * Admin — number of scanned domains using each technology.\n */\n const getTechnologyUsage = async (\n params?: GetTechnologyUsageQuery,\n otherOptions: FetcherOptions = {}\n ) =>\n await fetcher<GetTechnologyUsageResult>(\n `${AUDIT_API_ROUTE}/technologies`,\n authAPIOptions,\n otherOptions,\n { method: 'GET', params }\n );\n\n /**\n * Admin — scanned hosts, most recently scanned first, filterable by\n * technology.\n */\n const getScannedHosts = async (\n params?: GetScannedHostsQuery,\n otherOptions: FetcherOptions = {}\n ) =>\n await fetcher<GetScannedHostsResult>(\n `${AUDIT_API_ROUTE}/hosts`,\n authAPIOptions,\n otherOptions,\n { method: 'GET', params }\n );\n\n /**\n * Admin — a scanned host with its stored scans, newest first.\n */\n const getScannedHost = async (\n params?: GetScannedHostParams,\n otherOptions: FetcherOptions = {}\n ) =>\n await fetcher<GetScannedHostResult>(\n `${AUDIT_API_ROUTE}/hosts/${encodeURIComponent(params?.host ?? '')}`,\n authAPIOptions,\n otherOptions,\n { method: 'GET' }\n );\n\n return {\n getTechnologyUsage,\n getScannedHosts,\n getScannedHost,\n discoverUrls,\n scanUrl,\n startRecursiveAudit,\n getRecursiveAuditStatus,\n cancelRecursiveAudit,\n pauseRecursiveAudit,\n resumeRecursiveAudit,\n };\n};\n\n/**\n * Authenticated `audit` endpoint bound to an Intlayer CMS authenticator.\n *\n * Pass an authenticator created with `createIntlayerCMS`, or omit it to use\n * the build-time configuration (`@intlayer/config/built`).\n */\nexport const auditEndpoint = createEndpoint(getAuditAPI);\n"],"mappings":"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"}
@@ -1,7 +1,7 @@
1
1
  import { BACKEND_INTERNAL_URL_ENVIRONMENT_VARIABLE, resolveInternalBackendUrl } from "./resolveInternalBackendUrl.mjs";
2
2
  import { FetcherError, fetcher, fetcherOptions } from "./fetcher.mjs";
3
3
  import { getOAuthAPI } from "./getIntlayerAPI/oAuth.mjs";
4
- import { createAuthManager } from "./cms/createAuthManager.mjs";
4
+ import { clearAccessTokenCache, createAuthManager } from "./cms/createAuthManager.mjs";
5
5
  import { createEndpoint, createIntlayerCMS } from "./cms/createIntlayerCMS.mjs";
6
6
  import { aiEndpoint, getAiAPI } from "./getIntlayerAPI/ai.mjs";
7
7
  import { analyticsEndpoint, getAnalyticsAPI } from "./getIntlayerAPI/analytics.mjs";
@@ -29,4 +29,4 @@ import { fetchDistantDictionaries } from "./distantDictionary/fetchDistantDictio
29
29
  import { fetchDistantDictionary } from "./distantDictionary/fetchDistantDictionary.mjs";
30
30
  import { getIntlayerAPI } from "./getIntlayerAPI/index.mjs";
31
31
 
32
- export { BACKEND_INTERNAL_URL_ENVIRONMENT_VARIABLE, FetcherError, aiEndpoint, analyticsEndpoint, assetEndpoint, auditEndpoint, bitbucketEndpoint, createAuthManager, createEndpoint, createIntlayerCMS, dictionaryEndpoint, editorEndpoint, environmentEndpoint, fetchDistantDictionaries, fetchDistantDictionary, fetcher, fetcherOptions, getAiAPI, getAnalyticsAPI, getAssetAPI, getAuditAPI, getBitbucketAPI, getDictionaryAPI, getEditorAPI, getEnvironmentAPI, getGithubAPI, getGitlabAPI, getIntlayerAPI, getIntlayerAPIProxy, getNewsletterAPI, getOAuthAPI, getOrganizationAPI, getProjectAPI, getReviewerAPI, getSearchAPI, getShowcaseProjectAPI, getStatusAPI, getStripeAPI, getTagAPI, getTranslateAPI, getUserAPI, githubEndpoint, gitlabEndpoint, newsletterEndpoint, organizationEndpoint, projectEndpoint, resolveInternalBackendUrl, reviewerEndpoint, searchEndpoint, showcaseProjectEndpoint, statusEndpoint, stripeEndpoint, tagEndpoint, translateEndpoint, userEndpoint };
32
+ export { BACKEND_INTERNAL_URL_ENVIRONMENT_VARIABLE, FetcherError, aiEndpoint, analyticsEndpoint, assetEndpoint, auditEndpoint, bitbucketEndpoint, clearAccessTokenCache, createAuthManager, createEndpoint, createIntlayerCMS, dictionaryEndpoint, editorEndpoint, environmentEndpoint, fetchDistantDictionaries, fetchDistantDictionary, fetcher, fetcherOptions, getAiAPI, getAnalyticsAPI, getAssetAPI, getAuditAPI, getBitbucketAPI, getDictionaryAPI, getEditorAPI, getEnvironmentAPI, getGithubAPI, getGitlabAPI, getIntlayerAPI, getIntlayerAPIProxy, getNewsletterAPI, getOAuthAPI, getOrganizationAPI, getProjectAPI, getReviewerAPI, getSearchAPI, getShowcaseProjectAPI, getStatusAPI, getStripeAPI, getTagAPI, getTranslateAPI, getUserAPI, githubEndpoint, gitlabEndpoint, newsletterEndpoint, organizationEndpoint, projectEndpoint, resolveInternalBackendUrl, reviewerEndpoint, searchEndpoint, showcaseProjectEndpoint, statusEndpoint, stripeEndpoint, tagEndpoint, translateEndpoint, userEndpoint };
@@ -11,6 +11,8 @@ type AsyncMethod<Arguments extends unknown[], Result> = (...args: Arguments) =>
11
11
  * fetcher options and the Intlayer configuration.
12
12
  */
13
13
  type SectionFactory<Section extends Record<string, unknown>> = (fetcherOptions: FetcherOptions, intlayerConfig: IntlayerConfig) => Section;
14
+ /** Clears every cached access token, forcing the next call to re-authenticate. */
15
+ export declare const clearAccessTokenCache: () => void;
14
16
  /**
15
17
  * Options accepted when creating an {@link AuthManager}.
16
18
  */
@@ -89,8 +91,8 @@ export type AuthManager = {
89
91
  * Creates an instance-scoped {@link AuthManager} handling the OAuth2
90
92
  * `client_credentials` flow (or a pre-issued CLI session token).
91
93
  *
92
- * Unlike a global token cache, each manager keeps its own token state, so
93
- * independent SDK instances never interfere with one another.
94
+ * Managers built with the same backend URL and credentials share one token
95
+ * state, so a process never exchanges credentials more than needed.
94
96
  *
95
97
  * @param intlayerConfig - Intlayer configuration carrying the `editor`
96
98
  * credentials. Defaults to the build-time configuration.
@@ -1 +1 @@
1
- {"version":3,"file":"createAuthManager.d.ts","names":[],"sources":["../../../src/cms/createAuthManager.ts"],"mappings":";;;;;;;KAuBK,YAAY,6BAA6B,cACzC,MAAM,cACN,QAAQ;;;;;KAMR,eAAe,gBAAgB,4BAClC,gBAAgB,gBAChB,gBAAgB,mBACb;;;;YA0CO;;;;EAIV,qBAAqB;;;;;;EAMrB;;;;;YAMU;;;;;EAKV;;;;;;;;;;YAWU;;;;;EAKV,gBAAgB;;;;EAIhB,gBAAgB;;;;;EAKhB;;;;;EAKA,kBAAkB;;;;EAIlB;;;;;EAKA,WAAW,6BAA6B,QACtC,QAAQ,YAAY,WAAW,SAC/B,uBACG,YAAY,WAAW;;;;;;;;;;;EAW5B,cAAc,gBAAgB,yBAC5B,SAAS,eAAe,UACxB,UAAU,uBACP;;;;;;;;;;;;;qBAcM,oBAAiB,iBACZ,KAAK,2BAAyB,UACrC,uBACR"}
1
+ {"version":3,"file":"createAuthManager.d.ts","names":[],"sources":["../../../src/cms/createAuthManager.ts"],"mappings":";;;;;;;KAuBK,YAAY,6BAA6B,cACzC,MAAM,cACN,QAAQ;;;;;KAMR,eAAe,gBAAgB,4BAClC,gBAAgB,gBAChB,gBAAgB,mBACb;;qBAsBQ;;;;YA6ED;;;;EAIV,qBAAqB;;;;;;EAMrB;;;;;YAMU;;;;;EAKV;;;;;;;;;;YAWU;;;;;EAKV,gBAAgB;;;;EAIhB,gBAAgB;;;;;EAKhB;;;;;EAKA,kBAAkB;;;;EAIlB;;;;;EAKA,WAAW,6BAA6B,QACtC,QAAQ,YAAY,WAAW,SAC/B,uBACG,YAAY,WAAW;;;;;;;;;;;EAW5B,cAAc,gBAAgB,yBAC5B,SAAS,eAAe,UACxB,UAAU,uBACP;;;;;;;;;;;;;qBAcM,oBAAiB,iBACZ,KAAK,2BAAyB,UACrC,uBACR"}
@@ -1,3 +1,3 @@
1
- import { AuthManager, AuthManagerOptions, BindSectionOptions, createAuthManager } from "./createAuthManager.js";
1
+ import { AuthManager, AuthManagerOptions, BindSectionOptions, clearAccessTokenCache, createAuthManager } from "./createAuthManager.js";
2
2
  import { CMSEndpoint, IntlayerCMS, IntlayerCMSOptions, createEndpoint, createIntlayerCMS } from "./createIntlayerCMS.js";
3
- export { AuthManager, AuthManagerOptions, BindSectionOptions, CMSEndpoint, IntlayerCMS, IntlayerCMSOptions, createAuthManager, createEndpoint, createIntlayerCMS };
3
+ export { AuthManager, AuthManagerOptions, BindSectionOptions, CMSEndpoint, IntlayerCMS, IntlayerCMSOptions, clearAccessTokenCache, createAuthManager, createEndpoint, createIntlayerCMS };
@@ -10,6 +10,7 @@ export declare const getAnalyticsAPI: (authAPIOptions?: FetcherOptions, intlayer
10
10
  getAudience: (range?: AudienceRange | number, otherOptions?: FetcherOptions) => Promise<GetAudienceResult>;
11
11
  getContentStats: (otherOptions?: FetcherOptions) => Promise<GetContentStatsResult>;
12
12
  getExperimentResults: (experimentKey: string, otherOptions?: FetcherOptions) => Promise<GetExperimentResultsResult>;
13
+ getPageMetadata: (url: string, otherOptions?: FetcherOptions) => Promise<GetPageMetadataResult>;
13
14
  };
14
15
  /**
15
16
  * Authenticated `analytics` endpoint bound to an Intlayer CMS authenticator.
@@ -23,6 +24,7 @@ export declare const analyticsEndpoint: CMSEndpoint<{
23
24
  getAudience: (range?: AudienceRange | number, otherOptions?: FetcherOptions) => Promise<GetAudienceResult>;
24
25
  getContentStats: (otherOptions?: FetcherOptions) => Promise<GetContentStatsResult>;
25
26
  getExperimentResults: (experimentKey: string, otherOptions?: FetcherOptions) => Promise<GetExperimentResultsResult>;
27
+ getPageMetadata: (url: string, otherOptions?: FetcherOptions) => Promise<GetPageMetadataResult>;
26
28
  }>;
27
29
  //#endregion
28
30
  //# sourceMappingURL=analytics.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"analytics.d.ts","names":[],"sources":["../../../src/getIntlayerAPI/analytics.ts"],"mappings":";;;;;;qBAca,kBAAe,iBACV,gBAAc,iBACb;EAaT,aAAA,MAAA,qBAAmB,eACX,mBAAc,QAAA;EAgBW,cAAA,eAAA,mBAAc,QAAA;EAgB9C,cAAA,QAAA,wBAAsB,eACf,mBAAc,QAAA;EAgBe,kBAAA,eAAA,mBAAc,QAAA;EAc1C,uBAAA,uBAAM,eACP,mBAAc,QAAA;;;;;;;;qBAwBnB,mBAxBmB;EAjEtB,aAAA,MAAA,qBAAmB,eACX,mBAAc,QAAA;EAgBW,cAAA,eAAA,mBAAc,QAAA;EAgB9C,cAAA,QAAA,wBAAsB,eACf,mBAAc,QAAA;EAgBe,kBAAA,eAAA,mBAAc,QAAA;EAc1C,uBAAA,uBAAM,eACP,mBAAc,QAAA"}
1
+ {"version":3,"file":"analytics.d.ts","names":[],"sources":["../../../src/getIntlayerAPI/analytics.ts"],"mappings":";;;;;;qBAea,kBAAe,iBACV,gBAAc,iBACb;EAaT,aAAA,MAAA,qBAAmB,eACX,mBAAc,QAAA;EAgBW,cAAA,eAAA,mBAAc,QAAA;EAgB9C,cAAA,QAAA,wBAAsB,eACf,mBAAc,QAAA;EAgBe,kBAAA,eAAA,mBAAc,QAAA;EAc1C,uBAAA,uBAAM,eACP,mBAAc,QAAA;EAevB,kBAAA,aAAM,eACG,mBAAc,QAAA;;;;;;;;qBA4BnB,mBA5BmB;EAjFtB,aAAA,MAAA,qBAAmB,eACX,mBAAc,QAAA;EAgBW,cAAA,eAAA,mBAAc,QAAA;EAgB9C,cAAA,QAAA,wBAAsB,eACf,mBAAc,QAAA;EAgBe,kBAAA,eAAA,mBAAc,QAAA;EAc1C,uBAAA,uBAAM,eACP,mBAAc,QAAA;EAevB,kBAAA,aAAM,eACG,mBAAc,QAAA"}
@@ -2,10 +2,15 @@ import { FetcherOptions } from "../fetcher.js";
2
2
  import { CMSEndpoint } from "../cms/createIntlayerCMS.js";
3
3
  import "../index.js";
4
4
  import { IntlayerConfig } from "@intlayer/types/config";
5
- import { AuditEvent, GetRecursiveAuditStatusResult, StartRecursiveAuditResult } from "@intlayer/backend";
5
+ import { AuditEvent, GetRecursiveAuditStatusResult, GetScannedHostParams, GetScannedHostResult, GetScannedHostsQuery, GetScannedHostsResult, GetTechnologyUsageQuery, GetTechnologyUsageResult, StartRecursiveAuditResult } from "@intlayer/backend";
6
6
  //#region src/getIntlayerAPI/audit.d.ts
7
7
  export type ScanUrlBody = {
8
8
  url: string;
9
+ /**
10
+ * Force a new audit. Without it, a URL audited less than an hour ago is
11
+ * replayed from cache (its first event carries `cachedAt`).
12
+ */
13
+ refresh?: boolean;
9
14
  onMessage?: (event: AuditEvent) => void;
10
15
  onDone?: () => void;
11
16
  };
@@ -26,6 +31,9 @@ export type RecursiveAuditJobParams = {
26
31
  jobId: string;
27
32
  };
28
33
  export declare const getAuditAPI: (authAPIOptions?: FetcherOptions, intlayerConfig?: IntlayerConfig) => {
34
+ getTechnologyUsage: (params?: GetTechnologyUsageQuery, otherOptions?: FetcherOptions) => Promise<GetTechnologyUsageResult>;
35
+ getScannedHosts: (params?: GetScannedHostsQuery, otherOptions?: FetcherOptions) => Promise<GetScannedHostsResult>;
36
+ getScannedHost: (params?: GetScannedHostParams, otherOptions?: FetcherOptions) => Promise<GetScannedHostResult>;
29
37
  discoverUrls: (params?: DiscoverUrlsParams, otherOptions?: FetcherOptions) => Promise<DiscoverUrlsResult>;
30
38
  scanUrl: (body?: ScanUrlBody, otherOptions?: FetcherOptions) => Promise<void>;
31
39
  startRecursiveAudit: (body?: StartRecursiveAuditBody, otherOptions?: FetcherOptions) => Promise<StartRecursiveAuditResult>;
@@ -47,6 +55,9 @@ export declare const getAuditAPI: (authAPIOptions?: FetcherOptions, intlayerConf
47
55
  * the build-time configuration (`@intlayer/config/built`).
48
56
  */
49
57
  export declare const auditEndpoint: CMSEndpoint<{
58
+ getTechnologyUsage: (params?: GetTechnologyUsageQuery, otherOptions?: FetcherOptions) => Promise<GetTechnologyUsageResult>;
59
+ getScannedHosts: (params?: GetScannedHostsQuery, otherOptions?: FetcherOptions) => Promise<GetScannedHostsResult>;
60
+ getScannedHost: (params?: GetScannedHostParams, otherOptions?: FetcherOptions) => Promise<GetScannedHostResult>;
50
61
  discoverUrls: (params?: DiscoverUrlsParams, otherOptions?: FetcherOptions) => Promise<DiscoverUrlsResult>;
51
62
  scanUrl: (body?: ScanUrlBody, otherOptions?: FetcherOptions) => Promise<void>;
52
63
  startRecursiveAudit: (body?: StartRecursiveAuditBody, otherOptions?: FetcherOptions) => Promise<StartRecursiveAuditResult>;
@@ -62,5 +73,5 @@ export declare const auditEndpoint: CMSEndpoint<{
62
73
  }>;
63
74
  }>;
64
75
  //#endregion
65
- export type { AuditEvent, GetRecursiveAuditStatusResult, StartRecursiveAuditResult };
76
+ export type { AuditEvent, GetRecursiveAuditStatusResult, GetScannedHostParams, GetScannedHostResult, GetScannedHostsQuery, GetScannedHostsResult, GetTechnologyUsageQuery, GetTechnologyUsageResult, StartRecursiveAuditResult };
66
77
  //# sourceMappingURL=audit.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"audit.d.ts","names":[],"sources":["../../../src/getIntlayerAPI/audit.ts"],"mappings":";;;;;;YAgBY;EACV;EACA,aAAa,OAAO;EACpB;;YAGU;EACV;;YAGU;EACV;;YAGU;EACV;EACA;;YAGU;EACV;;YAGU;EACV;;qBAGW,cAAW,iBACN,gBAAc,iBACb;EAmBN,eAAA,SAAA,oBAAkB,eACb,mBAAc,QAAA;EAarB,UAAA,OAAA,aAAW,eACJ,mBAAc;EAsErB,sBAAA,OAAA,yBAAuB,eAChB,mBAAc,QAAA;EAiBnB,0BAAA,SAAA,+BAA6B,eACxB,mBAAc,QAAA;EAYnB,uBAAA,SAAA,yBAAuB,eAClB,mBAAc;IAEH;;EAQhB,sBAAA,SAAA,yBAAuB,eAClB,mBAAc;IAEH;;EAQhB,uBAAA,SAAA,yBAAuB,eAClB,mBAAc;IAEH;;;;;;;;;qBAwBhB,eA1BmB;EA3InB,eAAA,SAAA,oBAAkB,eACb,mBAAc,QAAA;EAarB,UAAA,OAAA,aAAW,eACJ,mBAAc;EAsErB,sBAAA,OAAA,yBAAuB,eAChB,mBAAc,QAAA;EAiBnB,0BAAA,SAAA,+BAA6B,eACxB,mBAAc,QAAA;EAYnB,uBAAA,SAAA,yBAAuB,eAClB,mBAAc;IAEH;;EAQhB,sBAAA,SAAA,yBAAuB,eAClB,mBAAc;IAEH;;EAQhB,uBAAA,SAAA,yBAAuB,eAClB,mBAAc;IAEH"}
1
+ {"version":3,"file":"audit.d.ts","names":[],"sources":["../../../src/getIntlayerAPI/audit.ts"],"mappings":";;;;;;YA4BY;EACV;;;;;EAKA;EACA,aAAa,OAAO;EACpB;;YAGU;EACV;;YAGU;EACV;;YAGU;EACV;EACA;;YAGU;EACV;;YAGU;EACV;;qBAGW,cAAW,iBACN,gBAAc,iBACb;EA4KN,qBAAA,SAAA,yBAAuB,eAClB,mBAAc,QAAA;EAcnB,kBAAA,SAAA,sBAAoB,eACf,mBAAc,QAAA;EAanB,iBAAA,SAAA,sBAAoB,eACf,mBAAc,QAAA;EAvLnB,eAAA,SAAA,oBAAkB,eACb,mBAAc,QAAA;EAarB,UAAA,OAAA,aAAW,eACJ,mBAAc;EAuErB,sBAAA,OAAA,yBAAuB,eAChB,mBAAc,QAAA;EAiBnB,0BAAA,SAAA,+BAA6B,eACxB,mBAAc,QAAA;EAYnB,uBAAA,SAAA,yBAAuB,eAClB,mBAAc;IAEH;;EAQhB,sBAAA,SAAA,yBAAuB,eAClB,mBAAc;IAEH;;EAQhB,uBAAA,SAAA,yBAAuB,eAClB,mBAAc;IAEH;;;;;;;;;qBAsEhB,eAxEmB;EAanB,qBAAA,SAAA,yBAAuB,eAClB,mBAAc,QAAA;EAcnB,kBAAA,SAAA,sBAAoB,eACf,mBAAc,QAAA;EAanB,iBAAA,SAAA,sBAAoB,eACf,mBAAc,QAAA;EAvLnB,eAAA,SAAA,oBAAkB,eACb,mBAAc,QAAA;EAarB,UAAA,OAAA,aAAW,eACJ,mBAAc;EAuErB,sBAAA,OAAA,yBAAuB,eAChB,mBAAc,QAAA;EAiBnB,0BAAA,SAAA,+BAA6B,eACxB,mBAAc,QAAA;EAYnB,uBAAA,SAAA,yBAAuB,eAClB,mBAAc;IAEH;;EAQhB,sBAAA,SAAA,yBAAuB,eAClB,mBAAc;IAEH;;EAQhB,uBAAA,SAAA,yBAAuB,eAClB,mBAAc;IAEH"}
@@ -1,5 +1,5 @@
1
1
  import { FetcherError, FetcherOptions, fetcher, fetcherOptions } from "./fetcher.js";
2
- import { AuthManager, AuthManagerOptions, BindSectionOptions, createAuthManager } from "./cms/createAuthManager.js";
2
+ import { AuthManager, AuthManagerOptions, BindSectionOptions, clearAccessTokenCache, createAuthManager } from "./cms/createAuthManager.js";
3
3
  import { CMSEndpoint, IntlayerCMS, IntlayerCMSOptions, createEndpoint, createIntlayerCMS } from "./cms/createIntlayerCMS.js";
4
4
  import "./cms/index.js";
5
5
  import { fetchDistantDictionaries } from "./distantDictionary/fetchDistantDictionaries.js";
@@ -7,7 +7,7 @@ import { fetchDistantDictionary } from "./distantDictionary/fetchDistantDictiona
7
7
  import "./distantDictionary/index.js";
8
8
  import { analyticsEndpoint, getAnalyticsAPI } from "./getIntlayerAPI/analytics.js";
9
9
  import { assetEndpoint, getAssetAPI } from "./getIntlayerAPI/asset.js";
10
- import { AuditEvent, DiscoverUrlsParams, DiscoverUrlsResult, GetRecursiveAuditStatusParams, GetRecursiveAuditStatusResult, RecursiveAuditJobParams, ScanUrlBody, StartRecursiveAuditBody, StartRecursiveAuditResult, auditEndpoint, getAuditAPI } from "./getIntlayerAPI/audit.js";
10
+ import { AuditEvent, DiscoverUrlsParams, DiscoverUrlsResult, GetRecursiveAuditStatusParams, GetRecursiveAuditStatusResult, GetScannedHostParams, GetScannedHostResult, GetScannedHostsQuery, GetScannedHostsResult, GetTechnologyUsageQuery, GetTechnologyUsageResult, RecursiveAuditJobParams, ScanUrlBody, StartRecursiveAuditBody, StartRecursiveAuditResult, auditEndpoint, getAuditAPI } from "./getIntlayerAPI/audit.js";
11
11
  import { BitbucketAuthCallbackBody, BitbucketAuthCallbackResult, BitbucketCheckConfigBody, BitbucketCheckConfigResult, BitbucketGetAuthUrlResult, BitbucketGetConfigFileBody, BitbucketGetConfigFileResult, BitbucketListReposResult, BitbucketRepository, bitbucketEndpoint, getBitbucketAPI } from "./getIntlayerAPI/bitbucket.js";
12
12
  import { dictionaryEndpoint, getDictionaryAPI } from "./getIntlayerAPI/dictionary.js";
13
13
  import { editorEndpoint, getEditorAPI } from "./getIntlayerAPI/editor.js";
@@ -31,4 +31,4 @@ import { IntlayerAPIProxy, getIntlayerAPIProxy } from "./proxy.js";
31
31
  import { BACKEND_INTERNAL_URL_ENVIRONMENT_VARIABLE, resolveInternalBackendUrl } from "./resolveInternalBackendUrl.js";
32
32
  import { AIOptions } from "./types.js";
33
33
  import { AskDocQuestionBody, AskDocQuestionResult, AutocompleteBody, ChatBody, ChatResult, ClientAction, aiEndpoint, getAiAPI } from "./getIntlayerAPI/ai.js";
34
- export { type AIOptions, AskDocQuestionBody, type AskDocQuestionResult, type AuditEvent, AuthManager, AuthManagerOptions, AutocompleteBody, BACKEND_INTERNAL_URL_ENVIRONMENT_VARIABLE, BindSectionOptions, BitbucketAuthCallbackBody, BitbucketAuthCallbackResult, BitbucketCheckConfigBody, BitbucketCheckConfigResult, BitbucketGetAuthUrlResult, BitbucketGetConfigFileBody, BitbucketGetConfigFileResult, BitbucketListReposResult, BitbucketRepository, CMSEndpoint, ChatBody, type ChatResult, ClientAction, DiscoverUrlsParams, DiscoverUrlsResult, FetcherError, FetcherOptions, GetDemoSessionResult, GetRecursiveAuditStatusParams, type GetRecursiveAuditStatusResult, GitHubAuthCallbackBody, GitHubAuthCallbackResult, GitHubCheckConfigBody, GitHubCheckConfigResult, GitHubGetAuthUrlResult, GitHubGetConfigFileBody, GitHubGetConfigFileResult, GitHubGetTokenResult, GitHubListReposResult, GitHubRepository, GitLabAuthCallbackBody, GitLabAuthCallbackResult, GitLabCheckConfigBody, GitLabCheckConfigResult, GitLabGetAuthUrlResult, GitLabGetConfigFileBody, GitLabGetConfigFileResult, GitLabListProjectsResult, GitLabProject, IntlayerAPI, IntlayerAPIProxy, IntlayerCMS, IntlayerCMSOptions, OtherShowcaseProjectsQuery, RecursiveAuditJobParams, ScanUrlBody, ShowcaseProjectsQuery, StartRecursiveAuditBody, type StartRecursiveAuditResult, type TranslateDictionariesBody, type TranslateDictionariesResult, aiEndpoint, analyticsEndpoint, assetEndpoint, auditEndpoint, bitbucketEndpoint, createAuthManager, createEndpoint, createIntlayerCMS, dictionaryEndpoint, editorEndpoint, environmentEndpoint, fetchDistantDictionaries, fetchDistantDictionary, fetcher, fetcherOptions, getAiAPI, getAnalyticsAPI, getAssetAPI, getAuditAPI, getBitbucketAPI, getDictionaryAPI, getEditorAPI, getEnvironmentAPI, getGithubAPI, getGitlabAPI, getIntlayerAPI, getIntlayerAPIProxy, getNewsletterAPI, getOAuthAPI, getOrganizationAPI, getProjectAPI, getReviewerAPI, getSearchAPI, getShowcaseProjectAPI, getStatusAPI, getStripeAPI, getTagAPI, getTranslateAPI, getUserAPI, githubEndpoint, gitlabEndpoint, newsletterEndpoint, organizationEndpoint, projectEndpoint, resolveInternalBackendUrl, reviewerEndpoint, searchEndpoint, showcaseProjectEndpoint, statusEndpoint, stripeEndpoint, tagEndpoint, translateEndpoint, userEndpoint };
34
+ export { type AIOptions, AskDocQuestionBody, type AskDocQuestionResult, type AuditEvent, AuthManager, AuthManagerOptions, AutocompleteBody, BACKEND_INTERNAL_URL_ENVIRONMENT_VARIABLE, BindSectionOptions, BitbucketAuthCallbackBody, BitbucketAuthCallbackResult, BitbucketCheckConfigBody, BitbucketCheckConfigResult, BitbucketGetAuthUrlResult, BitbucketGetConfigFileBody, BitbucketGetConfigFileResult, BitbucketListReposResult, BitbucketRepository, CMSEndpoint, ChatBody, type ChatResult, ClientAction, DiscoverUrlsParams, DiscoverUrlsResult, FetcherError, FetcherOptions, GetDemoSessionResult, GetRecursiveAuditStatusParams, type GetRecursiveAuditStatusResult, type GetScannedHostParams, type GetScannedHostResult, type GetScannedHostsQuery, type GetScannedHostsResult, type GetTechnologyUsageQuery, type GetTechnologyUsageResult, GitHubAuthCallbackBody, GitHubAuthCallbackResult, GitHubCheckConfigBody, GitHubCheckConfigResult, GitHubGetAuthUrlResult, GitHubGetConfigFileBody, GitHubGetConfigFileResult, GitHubGetTokenResult, GitHubListReposResult, GitHubRepository, GitLabAuthCallbackBody, GitLabAuthCallbackResult, GitLabCheckConfigBody, GitLabCheckConfigResult, GitLabGetAuthUrlResult, GitLabGetConfigFileBody, GitLabGetConfigFileResult, GitLabListProjectsResult, GitLabProject, IntlayerAPI, IntlayerAPIProxy, IntlayerCMS, IntlayerCMSOptions, OtherShowcaseProjectsQuery, RecursiveAuditJobParams, ScanUrlBody, ShowcaseProjectsQuery, StartRecursiveAuditBody, type StartRecursiveAuditResult, type TranslateDictionariesBody, type TranslateDictionariesResult, aiEndpoint, analyticsEndpoint, assetEndpoint, auditEndpoint, bitbucketEndpoint, clearAccessTokenCache, createAuthManager, createEndpoint, createIntlayerCMS, dictionaryEndpoint, editorEndpoint, environmentEndpoint, fetchDistantDictionaries, fetchDistantDictionary, fetcher, fetcherOptions, getAiAPI, getAnalyticsAPI, getAssetAPI, getAuditAPI, getBitbucketAPI, getDictionaryAPI, getEditorAPI, getEnvironmentAPI, getGithubAPI, getGitlabAPI, getIntlayerAPI, getIntlayerAPIProxy, getNewsletterAPI, getOAuthAPI, getOrganizationAPI, getProjectAPI, getReviewerAPI, getSearchAPI, getShowcaseProjectAPI, getStatusAPI, getStripeAPI, getTagAPI, getTranslateAPI, getUserAPI, githubEndpoint, gitlabEndpoint, newsletterEndpoint, organizationEndpoint, projectEndpoint, resolveInternalBackendUrl, reviewerEndpoint, searchEndpoint, showcaseProjectEndpoint, statusEndpoint, stripeEndpoint, tagEndpoint, translateEndpoint, userEndpoint };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@intlayer/api",
3
- "version": "9.5.10",
3
+ "version": "9.5.12",
4
4
  "private": false,
5
5
  "description": "SDK for interacting with the Intlayer API, enabling content auditing, and managing organizations, projects, and users.",
6
6
  "keywords": [
@@ -265,8 +265,8 @@
265
265
  "typecheck": "tsc --noEmit --project tsconfig.types.json"
266
266
  },
267
267
  "dependencies": {
268
- "@intlayer/config": "9.5.10",
269
- "@intlayer/types": "9.5.10",
268
+ "@intlayer/config": "9.5.12",
269
+ "@intlayer/types": "9.5.12",
270
270
  "defu": "6.1.7"
271
271
  },
272
272
  "devDependencies": {