@intlayer/api 9.4.0-canary.0 → 9.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -0
- package/dist/cjs/cms/createAuthManager.cjs +10 -1
- package/dist/cjs/cms/createAuthManager.cjs.map +1 -1
- package/dist/cjs/public/createPublicClient.cjs +123 -0
- package/dist/cjs/public/createPublicClient.cjs.map +1 -0
- package/dist/cjs/public/index.cjs +4 -0
- package/dist/esm/cms/createAuthManager.mjs +10 -1
- package/dist/esm/cms/createAuthManager.mjs.map +1 -1
- package/dist/esm/public/createPublicClient.mjs +122 -0
- package/dist/esm/public/createPublicClient.mjs.map +1 -0
- package/dist/esm/public/index.mjs +3 -0
- package/dist/types/cms/createAuthManager.d.ts.map +1 -1
- package/dist/types/public/createPublicClient.d.ts +34 -0
- package/dist/types/public/createPublicClient.d.ts.map +1 -0
- package/dist/types/public/index.d.ts +2 -0
- package/package.json +11 -3
package/README.md
CHANGED
|
@@ -214,6 +214,7 @@ Explore our comprehensive documentation to get started with Intlayer and learn h
|
|
|
214
214
|
<li><a href="https://intlayer.org/doc/environment/vite-and-vue" rel=''>Vite + Vue</a></li>
|
|
215
215
|
<li><a href="https://intlayer.org/doc/environment/nuxt-and-vue" rel=''>Vite + Nuxt</a></li>
|
|
216
216
|
<li><a href="https://intlayer.org/doc/environment/vite-and-solid" rel=''>Vite + Solid</a></li>
|
|
217
|
+
<li><a href="https://intlayer.org/doc/environment/solid-start" rel=''>Solid Start</a></li>
|
|
217
218
|
<li><a href="https://intlayer.org/doc/environment/angular" rel=''>Angular</a></li>
|
|
218
219
|
<li>
|
|
219
220
|
<a href="https://intlayer.org/doc/environment/express" rel=''>Backend</a>
|
|
@@ -223,7 +224,9 @@ Explore our comprehensive documentation to get started with Intlayer and learn h
|
|
|
223
224
|
<li><a href="https://intlayer.org/doc/environment/fastify" rel=''>Fastify</a></li>
|
|
224
225
|
<li><a href="https://intlayer.org/doc/environment/adonisjs" rel=''>AdonisJS</a></li>
|
|
225
226
|
<li><a href="https://intlayer.org/doc/environment/hono" rel=''>Hono</a></li>
|
|
227
|
+
<li><a href="https://intlayer.org/doc/environment/elysia" rel=''>Elysia</a></li>
|
|
226
228
|
</ul>
|
|
229
|
+
|
|
227
230
|
</li>
|
|
228
231
|
</ul>
|
|
229
232
|
</details>
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
|
2
2
|
const require_getIntlayerAPI_oAuth = require('../getIntlayerAPI/oAuth.cjs');
|
|
3
3
|
let _intlayer_config_built = require("@intlayer/config/built");
|
|
4
|
+
let _intlayer_config_secrets = require("@intlayer/config/secrets");
|
|
4
5
|
let defu = require("defu");
|
|
5
6
|
|
|
6
7
|
//#region src/cms/createAuthManager.ts
|
|
7
8
|
const ONE_MINUTE_MS = 6e4;
|
|
9
|
+
/** Whether the current runtime is a browser rather than a server. */
|
|
10
|
+
const isBrowser = () => typeof window !== "undefined";
|
|
8
11
|
/**
|
|
9
12
|
* Resolves the expiration timestamp (in ms since epoch) from an OAuth2
|
|
10
13
|
* token-like payload, supporting both absolute dates and relative durations.
|
|
@@ -32,7 +35,10 @@ const getExpiryTimestamp = (token) => {
|
|
|
32
35
|
*/
|
|
33
36
|
const createAuthManager = (intlayerConfig = { editor: _intlayer_config_built.editor }, options = {}) => {
|
|
34
37
|
const { baseFetcherOptions = {}, sessionToken } = options;
|
|
35
|
-
const resolvedConfig = (0, defu.defu)(intlayerConfig ?? {}, { editor:
|
|
38
|
+
const resolvedConfig = (0, defu.defu)(intlayerConfig ?? {}, { editor: {
|
|
39
|
+
..._intlayer_config_built.editor,
|
|
40
|
+
clientSecret: _intlayer_config_built.editor?.clientId ? (0, _intlayer_config_secrets.getEditorClientSecret)() : void 0
|
|
41
|
+
} });
|
|
36
42
|
const fetcherOptions = {
|
|
37
43
|
...baseFetcherOptions,
|
|
38
44
|
credentials: "omit"
|
|
@@ -78,6 +84,9 @@ const createAuthManager = (intlayerConfig = { editor: _intlayer_config_built.edi
|
|
|
78
84
|
expiryTimestamp = void 0;
|
|
79
85
|
};
|
|
80
86
|
const withAuth = (method, skipAuth = !hasCredentials) => {
|
|
87
|
+
if (skipAuth && !sessionToken && isBrowser()) return async () => {
|
|
88
|
+
throw new Error("Intlayer CMS credentials are not available in the browser. `clientSecret` is a server-side credential and is never bundled. Call this from a server route, or pass a user session token: createIntlayerCMS(config, { sessionToken }).");
|
|
89
|
+
};
|
|
81
90
|
if (skipAuth) return method;
|
|
82
91
|
return async (...args) => {
|
|
83
92
|
await ensureAuth();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"createAuthManager.cjs","names":["getOAuthAPI"],"sources":["../../../src/cms/createAuthManager.ts"],"sourcesContent":["import { editor } from '@intlayer/config/built';\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 * 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 const resolvedConfig = defu(intlayerConfig ?? {}, {\n editor,\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 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":";;;;;;AAmCA,MAAM,gBAAgB;;;;;AAMtB,MAAM,sBACJ,UACuB;CACvB,IAAI,CAAC,OAAO,OAAO;CAEnB,MAAM,WAAY,MAAM,wBAAwB,MAAM;CAKtD,IAAI,UAAU;EACZ,MAAM,YACJ,OAAO,aAAa,WAChB,KAAK,MAAM,QAAQ,IACnB,SAAS,UAAU;EACzB,IAAI,OAAO,cAAc,YAAY,OAAO,SAAS,SAAS,GAC5D,OAAO;CAEX;CAEA,MAAM,UAAU,MAAM,cAAc,MAAM;CAC1C,IAAI,OAAO,YAAY,YAAY,OAAO,SAAS,OAAO,GACxD,OAAO,KAAK,IAAI,IAAI,UAAU;AAIlC;;;;;;;;;;;;AAgGA,MAAa,qBACX,iBAAiD,EAAE,sCAAO,GAC1D,UAA8B,CAAC,MACf;CAChB,MAAM,EAAE,qBAAqB,CAAC,GAAG,iBAAiB;CAElD,MAAM,qBAAiB,WAAK,kBAAkB,CAAC,GAAG,EAChD,sCACF,CAAC;CAMD,MAAM,iBAAiC;EACrC,GAAG;EACH,aAAa;CACf;CAEA,MAAM,iBACJ,QAAQ,YAAY,KACpB,QACE,gBAAgB,QAAQ,YAAY,gBAAgB,QAAQ,YAC9D;CAIF,IAAI,cACF,eAAe,UAAU;EACvB,GAAI,eAAe,WAAW,CAAC;EAC/B,eAAe,UAAU;CAC3B;CAGF,IAAI;CACJ,IAAI;CACJ,IAAI;CAEJ,MAAM,qBAA8B;EAClC,IAAI,CAAC,aAAa,OAAO;EACzB,IAAI,CAAC,iBAAiB,OAAO;EAG7B,OAAO,KAAK,IAAI,IAAI,iBAAiB;CACvC;CAEA,MAAM,eAAe,YAA2B;EAC9C,MAAM,YAAY,YAA2B;GAG3C,MAAM,aAAY,MAFDA,yCAAY,CAAC,GAAG,cACH,CAAC,CAAC,qBAAqB,EAC3B,EAAE;GAE5B,cAAc,WAAW;GACzB,kBAAkB,mBAAmB,SAAS;EAChD;EAGA,mBAAmB,UAAU,CAAC,CAAC,cAAc;GAC3C,iBAAiB;EACnB,CAAC;EAED,MAAM;CACR;CAEA,MAAM,wBAA8B;EAClC,IAAI,CAAC,aAAa;EAClB,eAAe,UAAU;GACvB,GAAI,eAAe,WAAW,CAAC;GAC/B,eAAe,UAAU;EAC3B;CACF;CAEA,MAAM,aAAa,YAA2B;EAC5C,IAAI,cAAc;EAClB,IAAI,aAAa,GACf,MAAM,aAAa;EAErB,gBAAgB;CAClB;CAEA,MAAM,cAAoB;EACxB,cAAc;EACd,kBAAkB;CACpB;CAEA,MAAM,YACJ,QACA,WAAW,CAAC,mBACuB;EACnC,IAAI,UAAU,OAAO;EAErB,OAAO,OAAO,GAAG,SAAqC;GACpD,MAAM,WAAW;GAEjB,IAAI;IACF,OAAO,MAAM,OAAO,GAAG,IAAI;GAC7B,SAAS,OAAO;IAId,IAAI,cAAc,MAAM;IAExB,MAAM,aAAa;IACnB,gBAAgB;IAChB,OAAO,MAAM,OAAO,GAAG,IAAI;GAC7B;EACF;CACF;CAEA,MAAM,eACJ,SACA,cAAkC,CAAC,MACvB;EACZ,MAAM,UAAU,QAAQ,gBAAgB,cAAc;EACtD,MAAM,WAAW,YAAY,YAAY,CAAC;EAE1C,OAAO,IAAI,MAAM,SAAS,EACxB,IAAI,QAAQ,UAAU,UAAU;GAC9B,MAAM,QAAQ,QAAQ,IAAI,QAAQ,UAAU,QAAQ;GAEpD,IAAI,OAAO,UAAU,YAAY,OAAO;GAExC,OAAO,SACJ,MAA0C,KAAK,MAAM,GACtD,QACF;EACF,EACF,CAAC;CACH;CAEA,OAAO;EACL;EACA,gBAAgB;EAChB;EACA;EACA;EACA;EACA;CACF;AACF"}
|
|
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":";;;;;;;AAoCA,MAAM,gBAAgB;;AAGtB,MAAM,kBAA2B,OAAO,WAAW;;;;;AAMnD,MAAM,sBACJ,UACuB;CACvB,IAAI,CAAC,OAAO,OAAO;CAEnB,MAAM,WAAY,MAAM,wBAAwB,MAAM;CAKtD,IAAI,UAAU;EACZ,MAAM,YACJ,OAAO,aAAa,WAChB,KAAK,MAAM,QAAQ,IACnB,SAAS,UAAU;EACzB,IAAI,OAAO,cAAc,YAAY,OAAO,SAAS,SAAS,GAC5D,OAAO;CAEX;CAEA,MAAM,UAAU,MAAM,cAAc,MAAM;CAC1C,IAAI,OAAO,YAAY,YAAY,OAAO,SAAS,OAAO,GACxD,OAAO,KAAK,IAAI,IAAI,UAAU;AAIlC;;;;;;;;;;;;AAgGA,MAAa,qBACX,iBAAiD,EAAE,sCAAO,GAC1D,UAA8B,CAAC,MACf;CAChB,MAAM,EAAE,qBAAqB,CAAC,GAAG,iBAAiB;CAWlD,MAAM,qBAAiB,WAAK,kBAAkB,CAAC,GAAG,EAChD,QAAQ;EACN,GAAGA;EACH,cAAcA,+BAAQ,eAAWC,gDAAsB,IAAI;CAC7D,EACF,CAAC;CAMD,MAAM,iBAAiC;EACrC,GAAG;EACH,aAAa;CACf;CAEA,MAAM,iBACJ,QAAQ,YAAY,KACpB,QACE,gBAAgB,QAAQ,YAAY,gBAAgB,QAAQ,YAC9D;CAIF,IAAI,cACF,eAAe,UAAU;EACvB,GAAI,eAAe,WAAW,CAAC;EAC/B,eAAe,UAAU;CAC3B;CAGF,IAAI;CACJ,IAAI;CACJ,IAAI;CAEJ,MAAM,qBAA8B;EAClC,IAAI,CAAC,aAAa,OAAO;EACzB,IAAI,CAAC,iBAAiB,OAAO;EAG7B,OAAO,KAAK,IAAI,IAAI,iBAAiB;CACvC;CAEA,MAAM,eAAe,YAA2B;EAC9C,MAAM,YAAY,YAA2B;GAG3C,MAAM,aAAY,MAFDC,yCAAY,CAAC,GAAG,cACH,CAAC,CAAC,qBAAqB,EAC3B,EAAE;GAE5B,cAAc,WAAW;GACzB,kBAAkB,mBAAmB,SAAS;EAChD;EAGA,mBAAmB,UAAU,CAAC,CAAC,cAAc;GAC3C,iBAAiB;EACnB,CAAC;EAED,MAAM;CACR;CAEA,MAAM,wBAA8B;EAClC,IAAI,CAAC,aAAa;EAClB,eAAe,UAAU;GACvB,GAAI,eAAe,WAAW,CAAC;GAC/B,eAAe,UAAU;EAC3B;CACF;CAEA,MAAM,aAAa,YAA2B;EAC5C,IAAI,cAAc;EAClB,IAAI,aAAa,GACf,MAAM,aAAa;EAErB,gBAAgB;CAClB;CAEA,MAAM,cAAoB;EACxB,cAAc;EACd,kBAAkB;CACpB;CAEA,MAAM,YACJ,QACA,WAAW,CAAC,mBACuB;EAMnC,IAAI,YAAY,CAAC,gBAAgB,UAAU,GACzC,OAAO,YAAY;GACjB,MAAM,IAAI,MACR,uOAIF;EACF;EAGF,IAAI,UAAU,OAAO;EAErB,OAAO,OAAO,GAAG,SAAqC;GACpD,MAAM,WAAW;GAEjB,IAAI;IACF,OAAO,MAAM,OAAO,GAAG,IAAI;GAC7B,SAAS,OAAO;IAId,IAAI,cAAc,MAAM;IAExB,MAAM,aAAa;IACnB,gBAAgB;IAChB,OAAO,MAAM,OAAO,GAAG,IAAI;GAC7B;EACF;CACF;CAEA,MAAM,eACJ,SACA,cAAkC,CAAC,MACvB;EACZ,MAAM,UAAU,QAAQ,gBAAgB,cAAc;EACtD,MAAM,WAAW,YAAY,YAAY,CAAC;EAE1C,OAAO,IAAI,MAAM,SAAS,EACxB,IAAI,QAAQ,UAAU,UAAU;GAC9B,MAAM,QAAQ,QAAQ,IAAI,QAAQ,UAAU,QAAQ;GAEpD,IAAI,OAAO,UAAU,YAAY,OAAO;GAExC,OAAO,SACJ,MAA0C,KAAK,MAAM,GACtD,QACF;EACF,EACF,CAAC;CACH;CAEA,OAAO;EACL;EACA,gBAAgB;EAChB;EACA;EACA;EACA;EACA;CACF;AACF"}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
|
2
|
+
let _intlayer_config_built = require("@intlayer/config/built");
|
|
3
|
+
|
|
4
|
+
//#region src/public/createPublicClient.ts
|
|
5
|
+
/**
|
|
6
|
+
* Browser-safe Intlayer client, authenticated without any confidential
|
|
7
|
+
* credential.
|
|
8
|
+
*
|
|
9
|
+
* This is the Google-Analytics model applied to the whole SDK: the page ships
|
|
10
|
+
* only the **public** `editor.clientId`, exchanges it for a short-lived scoped
|
|
11
|
+
* token, and calls the API directly. No server route, no server action, and no
|
|
12
|
+
* `clientSecret` in the bundle.
|
|
13
|
+
*
|
|
14
|
+
* What it can do is deliberately narrow, and that is what makes shipping the
|
|
15
|
+
* key safe: read the project's published dictionary content — the content the
|
|
16
|
+
* page already renders — and submit analytics events. Writing content, reading
|
|
17
|
+
* a project, an organization or a user still require a real credential
|
|
18
|
+
* (server-side) or a signed-in user.
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```ts
|
|
22
|
+
* import { createPublicClient } from '@intlayer/api/public';
|
|
23
|
+
*
|
|
24
|
+
* const client = createPublicClient();
|
|
25
|
+
*
|
|
26
|
+
* const keys = await client.getDictionaryKeys();
|
|
27
|
+
* const [dictionary] = await client.getDictionaries(['navbar']);
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
/** `sessionStorage` key holding the cached token for this tab. */
|
|
31
|
+
const TOKEN_STORAGE_KEY = "__intlayer_public_token__";
|
|
32
|
+
/**
|
|
33
|
+
* Refresh margin: a token within this window of expiry is treated as expired,
|
|
34
|
+
* so a request is never sent with a token that dies in flight.
|
|
35
|
+
*/
|
|
36
|
+
const EXPIRY_MARGIN_MS = 6e4;
|
|
37
|
+
/** Reads the cached token for this tab, ignoring an unusable cache. */
|
|
38
|
+
const readStoredToken = () => {
|
|
39
|
+
try {
|
|
40
|
+
const raw = globalThis.sessionStorage?.getItem(TOKEN_STORAGE_KEY);
|
|
41
|
+
if (!raw) return null;
|
|
42
|
+
const parsed = JSON.parse(raw);
|
|
43
|
+
if (typeof parsed?.token !== "string") return null;
|
|
44
|
+
if (typeof parsed?.expiresAt !== "number") return null;
|
|
45
|
+
return parsed;
|
|
46
|
+
} catch {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
/** Persists the token for this tab. Best-effort: storage may be unavailable. */
|
|
51
|
+
const writeStoredToken = (cached) => {
|
|
52
|
+
try {
|
|
53
|
+
globalThis.sessionStorage?.setItem(TOKEN_STORAGE_KEY, JSON.stringify(cached));
|
|
54
|
+
} catch {}
|
|
55
|
+
};
|
|
56
|
+
/**
|
|
57
|
+
* Creates a {@link PublicClient} for the configured project.
|
|
58
|
+
*
|
|
59
|
+
* @param options - Optional configuration override.
|
|
60
|
+
* @returns A client that authenticates itself with the public project key.
|
|
61
|
+
*/
|
|
62
|
+
const createPublicClient = ({ intlayerConfig } = {}) => {
|
|
63
|
+
const resolvedEditor = intlayerConfig?.editor ?? _intlayer_config_built.editor;
|
|
64
|
+
const backendURL = (resolvedEditor?.backendURL ?? "").replace(/\/$/, "");
|
|
65
|
+
const clientId = resolvedEditor?.clientId;
|
|
66
|
+
let cached = null;
|
|
67
|
+
let pendingExchange = null;
|
|
68
|
+
const isUsable = (candidate) => candidate !== null && candidate.expiresAt - EXPIRY_MARGIN_MS > Date.now();
|
|
69
|
+
const exchange = async () => {
|
|
70
|
+
const response = await fetch(`${backendURL}/api/public/token`, {
|
|
71
|
+
method: "POST",
|
|
72
|
+
headers: { "Content-Type": "application/json" },
|
|
73
|
+
body: JSON.stringify({ clientId }),
|
|
74
|
+
credentials: "omit",
|
|
75
|
+
mode: "cors"
|
|
76
|
+
});
|
|
77
|
+
if (!response.ok) return null;
|
|
78
|
+
const payload = await response.json();
|
|
79
|
+
const token = payload?.data?.token;
|
|
80
|
+
const expiresIn = payload?.data?.expiresIn ?? 0;
|
|
81
|
+
if (!token || expiresIn <= 0) return null;
|
|
82
|
+
cached = {
|
|
83
|
+
token,
|
|
84
|
+
expiresAt: Date.now() + expiresIn * 1e3
|
|
85
|
+
};
|
|
86
|
+
writeStoredToken(cached);
|
|
87
|
+
return token;
|
|
88
|
+
};
|
|
89
|
+
const getToken = async () => {
|
|
90
|
+
if (isUsable(cached)) return cached.token;
|
|
91
|
+
const stored = readStoredToken();
|
|
92
|
+
if (isUsable(stored)) {
|
|
93
|
+
cached = stored;
|
|
94
|
+
return stored.token;
|
|
95
|
+
}
|
|
96
|
+
if (!clientId) return null;
|
|
97
|
+
pendingExchange ??= exchange().finally(() => {
|
|
98
|
+
pendingExchange = null;
|
|
99
|
+
});
|
|
100
|
+
return await pendingExchange;
|
|
101
|
+
};
|
|
102
|
+
/** Issues an authorised GET against the public surface. */
|
|
103
|
+
const request = async (path, fallback) => {
|
|
104
|
+
const token = await getToken();
|
|
105
|
+
if (!token) return fallback;
|
|
106
|
+
const response = await fetch(`${backendURL}/api/public${path}`, {
|
|
107
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
108
|
+
credentials: "omit",
|
|
109
|
+
mode: "cors"
|
|
110
|
+
});
|
|
111
|
+
if (!response.ok) return fallback;
|
|
112
|
+
return (await response.json())?.data ?? fallback;
|
|
113
|
+
};
|
|
114
|
+
return {
|
|
115
|
+
getToken,
|
|
116
|
+
getDictionaryKeys: () => request("/dictionaries/keys", []),
|
|
117
|
+
getDictionaries: (keys) => request(`/dictionaries?keys=${encodeURIComponent(keys.join(","))}`, [])
|
|
118
|
+
};
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
//#endregion
|
|
122
|
+
exports.createPublicClient = createPublicClient;
|
|
123
|
+
//# sourceMappingURL=createPublicClient.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"createPublicClient.cjs","names":["editor"],"sources":["../../../src/public/createPublicClient.ts"],"sourcesContent":["import type { DictionaryAPI } from '@intlayer/backend';\nimport { editor } from '@intlayer/config/built';\nimport type { IntlayerConfig } from '@intlayer/types/config';\n\n/**\n * Browser-safe Intlayer client, authenticated without any confidential\n * credential.\n *\n * This is the Google-Analytics model applied to the whole SDK: the page ships\n * only the **public** `editor.clientId`, exchanges it for a short-lived scoped\n * token, and calls the API directly. No server route, no server action, and no\n * `clientSecret` in the bundle.\n *\n * What it can do is deliberately narrow, and that is what makes shipping the\n * key safe: read the project's published dictionary content — the content the\n * page already renders — and submit analytics events. Writing content, reading\n * a project, an organization or a user still require a real credential\n * (server-side) or a signed-in user.\n *\n * @example\n * ```ts\n * import { createPublicClient } from '@intlayer/api/public';\n *\n * const client = createPublicClient();\n *\n * const keys = await client.getDictionaryKeys();\n * const [dictionary] = await client.getDictionaries(['navbar']);\n * ```\n */\n\n/** `sessionStorage` key holding the cached token for this tab. */\nconst TOKEN_STORAGE_KEY = '__intlayer_public_token__';\n\n/**\n * Refresh margin: a token within this window of expiry is treated as expired,\n * so a request is never sent with a token that dies in flight.\n */\nconst EXPIRY_MARGIN_MS = 60 * 1000;\n\ntype CachedToken = {\n token: string;\n /** Epoch milliseconds. */\n expiresAt: number;\n};\n\n/** Reads the cached token for this tab, ignoring an unusable cache. */\nconst readStoredToken = (): CachedToken | null => {\n try {\n const raw = globalThis.sessionStorage?.getItem(TOKEN_STORAGE_KEY);\n if (!raw) return null;\n\n const parsed = JSON.parse(raw) as CachedToken;\n\n if (typeof parsed?.token !== 'string') return null;\n if (typeof parsed?.expiresAt !== 'number') return null;\n\n return parsed;\n } catch {\n // Private mode, disabled storage, or a corrupted entry — start fresh.\n return null;\n }\n};\n\n/** Persists the token for this tab. Best-effort: storage may be unavailable. */\nconst writeStoredToken = (cached: CachedToken): void => {\n try {\n globalThis.sessionStorage?.setItem(\n TOKEN_STORAGE_KEY,\n JSON.stringify(cached)\n );\n } catch {\n /* ignore — the in-memory cache still applies for this page */\n }\n};\n\n/** Options accepted by {@link createPublicClient}. */\nexport type PublicClientOptions = {\n /** Overrides the build-time `editor` configuration. */\n intlayerConfig?: Pick<IntlayerConfig, 'editor'>;\n};\n\n/** The credential-free client returned by {@link createPublicClient}. */\nexport type PublicClient = {\n /**\n * Returns the current token, exchanging the public `clientId` for one when\n * needed. `null` when the project is unknown or the origin is not allowed.\n */\n getToken: () => Promise<string | null>;\n /** Every dictionary key published for the project. */\n getDictionaryKeys: () => Promise<string[]>;\n /**\n * Published dictionary content for the given keys.\n *\n * @param keys - Dictionary keys to fetch (at most 100 per call).\n */\n getDictionaries: (keys: string[]) => Promise<DictionaryAPI[]>;\n};\n\n/**\n * Creates a {@link PublicClient} for the configured project.\n *\n * @param options - Optional configuration override.\n * @returns A client that authenticates itself with the public project key.\n */\nexport const createPublicClient = ({\n intlayerConfig,\n}: PublicClientOptions = {}): PublicClient => {\n const resolvedEditor = intlayerConfig?.editor ?? editor;\n const backendURL = (resolvedEditor?.backendURL ?? '').replace(/\\/$/, '');\n const clientId = resolvedEditor?.clientId;\n\n let cached: CachedToken | null = null;\n let pendingExchange: Promise<string | null> | null = null;\n\n const isUsable = (candidate: CachedToken | null): candidate is CachedToken =>\n candidate !== null && candidate.expiresAt - EXPIRY_MARGIN_MS > Date.now();\n\n const exchange = async (): Promise<string | null> => {\n const response = await fetch(`${backendURL}/api/public/token`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ clientId }),\n // Authorised by `Origin`, never by cookies.\n credentials: 'omit',\n mode: 'cors',\n });\n\n if (!response.ok) return null;\n\n const payload = (await response.json()) as {\n data?: { token?: string | null; expiresIn?: number };\n };\n\n const token = payload?.data?.token;\n const expiresIn = payload?.data?.expiresIn ?? 0;\n\n if (!token || expiresIn <= 0) return null;\n\n cached = { token, expiresAt: Date.now() + expiresIn * 1000 };\n writeStoredToken(cached);\n\n return token;\n };\n\n const getToken = async (): Promise<string | null> => {\n if (isUsable(cached)) return cached.token;\n\n const stored = readStoredToken();\n if (isUsable(stored)) {\n cached = stored;\n return stored.token;\n }\n\n if (!clientId) return null;\n\n // De-duplicate concurrent exchanges so a burst of calls triggers one fetch.\n pendingExchange ??= exchange().finally(() => {\n pendingExchange = null;\n });\n\n return await pendingExchange;\n };\n\n /** Issues an authorised GET against the public surface. */\n const request = async <Result>(\n path: string,\n fallback: Result\n ): Promise<Result> => {\n const token = await getToken();\n\n if (!token) return fallback;\n\n const response = await fetch(`${backendURL}/api/public${path}`, {\n headers: { Authorization: `Bearer ${token}` },\n credentials: 'omit',\n mode: 'cors',\n });\n\n if (!response.ok) return fallback;\n\n const payload = (await response.json()) as { data?: Result };\n\n return payload?.data ?? fallback;\n };\n\n return {\n getToken,\n getDictionaryKeys: () => request<string[]>('/dictionaries/keys', []),\n getDictionaries: (keys) =>\n request<DictionaryAPI[]>(\n `/dictionaries?keys=${encodeURIComponent(keys.join(','))}`,\n []\n ),\n };\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+BA,MAAM,oBAAoB;;;;;AAM1B,MAAM,mBAAmB;;AASzB,MAAM,wBAA4C;CAChD,IAAI;EACF,MAAM,MAAM,WAAW,gBAAgB,QAAQ,iBAAiB;EAChE,IAAI,CAAC,KAAK,OAAO;EAEjB,MAAM,SAAS,KAAK,MAAM,GAAG;EAE7B,IAAI,OAAO,QAAQ,UAAU,UAAU,OAAO;EAC9C,IAAI,OAAO,QAAQ,cAAc,UAAU,OAAO;EAElD,OAAO;CACT,QAAQ;EAEN,OAAO;CACT;AACF;;AAGA,MAAM,oBAAoB,WAA8B;CACtD,IAAI;EACF,WAAW,gBAAgB,QACzB,mBACA,KAAK,UAAU,MAAM,CACvB;CACF,QAAQ,CAER;AACF;;;;;;;AA+BA,MAAa,sBAAsB,EACjC,mBACuB,CAAC,MAAoB;CAC5C,MAAM,iBAAiB,gBAAgB,UAAUA;CACjD,MAAM,cAAc,gBAAgB,cAAc,GAAE,CAAE,QAAQ,OAAO,EAAE;CACvE,MAAM,WAAW,gBAAgB;CAEjC,IAAI,SAA6B;CACjC,IAAI,kBAAiD;CAErD,MAAM,YAAY,cAChB,cAAc,QAAQ,UAAU,YAAY,mBAAmB,KAAK,IAAI;CAE1E,MAAM,WAAW,YAAoC;EACnD,MAAM,WAAW,MAAM,MAAM,GAAG,WAAW,oBAAoB;GAC7D,QAAQ;GACR,SAAS,EAAE,gBAAgB,mBAAmB;GAC9C,MAAM,KAAK,UAAU,EAAE,SAAS,CAAC;GAEjC,aAAa;GACb,MAAM;EACR,CAAC;EAED,IAAI,CAAC,SAAS,IAAI,OAAO;EAEzB,MAAM,UAAW,MAAM,SAAS,KAAK;EAIrC,MAAM,QAAQ,SAAS,MAAM;EAC7B,MAAM,YAAY,SAAS,MAAM,aAAa;EAE9C,IAAI,CAAC,SAAS,aAAa,GAAG,OAAO;EAErC,SAAS;GAAE;GAAO,WAAW,KAAK,IAAI,IAAI,YAAY;EAAK;EAC3D,iBAAiB,MAAM;EAEvB,OAAO;CACT;CAEA,MAAM,WAAW,YAAoC;EACnD,IAAI,SAAS,MAAM,GAAG,OAAO,OAAO;EAEpC,MAAM,SAAS,gBAAgB;EAC/B,IAAI,SAAS,MAAM,GAAG;GACpB,SAAS;GACT,OAAO,OAAO;EAChB;EAEA,IAAI,CAAC,UAAU,OAAO;EAGtB,oBAAoB,SAAS,CAAC,CAAC,cAAc;GAC3C,kBAAkB;EACpB,CAAC;EAED,OAAO,MAAM;CACf;;CAGA,MAAM,UAAU,OACd,MACA,aACoB;EACpB,MAAM,QAAQ,MAAM,SAAS;EAE7B,IAAI,CAAC,OAAO,OAAO;EAEnB,MAAM,WAAW,MAAM,MAAM,GAAG,WAAW,aAAa,QAAQ;GAC9D,SAAS,EAAE,eAAe,UAAU,QAAQ;GAC5C,aAAa;GACb,MAAM;EACR,CAAC;EAED,IAAI,CAAC,SAAS,IAAI,OAAO;EAIzB,QAAO,MAFgB,SAAS,KAAK,EAEvB,EAAE,QAAQ;CAC1B;CAEA,OAAO;EACL;EACA,yBAAyB,QAAkB,sBAAsB,CAAC,CAAC;EACnE,kBAAkB,SAChB,QACE,sBAAsB,mBAAmB,KAAK,KAAK,GAAG,CAAC,KACvD,CAAC,CACH;CACJ;AACF"}
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { getOAuthAPI } from "../getIntlayerAPI/oAuth.mjs";
|
|
2
2
|
import { editor } from "@intlayer/config/built";
|
|
3
|
+
import { getEditorClientSecret } from "@intlayer/config/secrets";
|
|
3
4
|
import { defu } from "defu";
|
|
4
5
|
|
|
5
6
|
//#region src/cms/createAuthManager.ts
|
|
6
7
|
const ONE_MINUTE_MS = 6e4;
|
|
8
|
+
/** Whether the current runtime is a browser rather than a server. */
|
|
9
|
+
const isBrowser = () => typeof window !== "undefined";
|
|
7
10
|
/**
|
|
8
11
|
* Resolves the expiration timestamp (in ms since epoch) from an OAuth2
|
|
9
12
|
* token-like payload, supporting both absolute dates and relative durations.
|
|
@@ -31,7 +34,10 @@ const getExpiryTimestamp = (token) => {
|
|
|
31
34
|
*/
|
|
32
35
|
const createAuthManager = (intlayerConfig = { editor }, options = {}) => {
|
|
33
36
|
const { baseFetcherOptions = {}, sessionToken } = options;
|
|
34
|
-
const resolvedConfig = defu(intlayerConfig ?? {}, { editor
|
|
37
|
+
const resolvedConfig = defu(intlayerConfig ?? {}, { editor: {
|
|
38
|
+
...editor,
|
|
39
|
+
clientSecret: editor?.clientId ? getEditorClientSecret() : void 0
|
|
40
|
+
} });
|
|
35
41
|
const fetcherOptions = {
|
|
36
42
|
...baseFetcherOptions,
|
|
37
43
|
credentials: "omit"
|
|
@@ -77,6 +83,9 @@ const createAuthManager = (intlayerConfig = { editor }, options = {}) => {
|
|
|
77
83
|
expiryTimestamp = void 0;
|
|
78
84
|
};
|
|
79
85
|
const withAuth = (method, skipAuth = !hasCredentials) => {
|
|
86
|
+
if (skipAuth && !sessionToken && isBrowser()) return async () => {
|
|
87
|
+
throw new Error("Intlayer CMS credentials are not available in the browser. `clientSecret` is a server-side credential and is never bundled. Call this from a server route, or pass a user session token: createIntlayerCMS(config, { sessionToken }).");
|
|
88
|
+
};
|
|
80
89
|
if (skipAuth) return method;
|
|
81
90
|
return async (...args) => {
|
|
82
91
|
await ensureAuth();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"createAuthManager.mjs","names":[],"sources":["../../../src/cms/createAuthManager.ts"],"sourcesContent":["import { editor } from '@intlayer/config/built';\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 * 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 const resolvedConfig = defu(intlayerConfig ?? {}, {\n editor,\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 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":";;;;;AAmCA,MAAM,gBAAgB;;;;;AAMtB,MAAM,sBACJ,UACuB;CACvB,IAAI,CAAC,OAAO,OAAO;CAEnB,MAAM,WAAY,MAAM,wBAAwB,MAAM;CAKtD,IAAI,UAAU;EACZ,MAAM,YACJ,OAAO,aAAa,WAChB,KAAK,MAAM,QAAQ,IACnB,SAAS,UAAU;EACzB,IAAI,OAAO,cAAc,YAAY,OAAO,SAAS,SAAS,GAC5D,OAAO;CAEX;CAEA,MAAM,UAAU,MAAM,cAAc,MAAM;CAC1C,IAAI,OAAO,YAAY,YAAY,OAAO,SAAS,OAAO,GACxD,OAAO,KAAK,IAAI,IAAI,UAAU;AAIlC;;;;;;;;;;;;AAgGA,MAAa,qBACX,iBAAiD,EAAE,OAAO,GAC1D,UAA8B,CAAC,MACf;CAChB,MAAM,EAAE,qBAAqB,CAAC,GAAG,iBAAiB;CAElD,MAAM,iBAAiB,KAAK,kBAAkB,CAAC,GAAG,EAChD,OACF,CAAC;CAMD,MAAM,iBAAiC;EACrC,GAAG;EACH,aAAa;CACf;CAEA,MAAM,iBACJ,QAAQ,YAAY,KACpB,QACE,gBAAgB,QAAQ,YAAY,gBAAgB,QAAQ,YAC9D;CAIF,IAAI,cACF,eAAe,UAAU;EACvB,GAAI,eAAe,WAAW,CAAC;EAC/B,eAAe,UAAU;CAC3B;CAGF,IAAI;CACJ,IAAI;CACJ,IAAI;CAEJ,MAAM,qBAA8B;EAClC,IAAI,CAAC,aAAa,OAAO;EACzB,IAAI,CAAC,iBAAiB,OAAO;EAG7B,OAAO,KAAK,IAAI,IAAI,iBAAiB;CACvC;CAEA,MAAM,eAAe,YAA2B;EAC9C,MAAM,YAAY,YAA2B;GAG3C,MAAM,aAAY,MAFD,YAAY,CAAC,GAAG,cACH,CAAC,CAAC,qBAAqB,EAC3B,EAAE;GAE5B,cAAc,WAAW;GACzB,kBAAkB,mBAAmB,SAAS;EAChD;EAGA,mBAAmB,UAAU,CAAC,CAAC,cAAc;GAC3C,iBAAiB;EACnB,CAAC;EAED,MAAM;CACR;CAEA,MAAM,wBAA8B;EAClC,IAAI,CAAC,aAAa;EAClB,eAAe,UAAU;GACvB,GAAI,eAAe,WAAW,CAAC;GAC/B,eAAe,UAAU;EAC3B;CACF;CAEA,MAAM,aAAa,YAA2B;EAC5C,IAAI,cAAc;EAClB,IAAI,aAAa,GACf,MAAM,aAAa;EAErB,gBAAgB;CAClB;CAEA,MAAM,cAAoB;EACxB,cAAc;EACd,kBAAkB;CACpB;CAEA,MAAM,YACJ,QACA,WAAW,CAAC,mBACuB;EACnC,IAAI,UAAU,OAAO;EAErB,OAAO,OAAO,GAAG,SAAqC;GACpD,MAAM,WAAW;GAEjB,IAAI;IACF,OAAO,MAAM,OAAO,GAAG,IAAI;GAC7B,SAAS,OAAO;IAId,IAAI,cAAc,MAAM;IAExB,MAAM,aAAa;IACnB,gBAAgB;IAChB,OAAO,MAAM,OAAO,GAAG,IAAI;GAC7B;EACF;CACF;CAEA,MAAM,eACJ,SACA,cAAkC,CAAC,MACvB;EACZ,MAAM,UAAU,QAAQ,gBAAgB,cAAc;EACtD,MAAM,WAAW,YAAY,YAAY,CAAC;EAE1C,OAAO,IAAI,MAAM,SAAS,EACxB,IAAI,QAAQ,UAAU,UAAU;GAC9B,MAAM,QAAQ,QAAQ,IAAI,QAAQ,UAAU,QAAQ;GAEpD,IAAI,OAAO,UAAU,YAAY,OAAO;GAExC,OAAO,SACJ,MAA0C,KAAK,MAAM,GACtD,QACF;EACF,EACF,CAAC;CACH;CAEA,OAAO;EACL;EACA,gBAAgB;EAChB;EACA;EACA;EACA;EACA;CACF;AACF"}
|
|
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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"}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { editor } from "@intlayer/config/built";
|
|
2
|
+
|
|
3
|
+
//#region src/public/createPublicClient.ts
|
|
4
|
+
/**
|
|
5
|
+
* Browser-safe Intlayer client, authenticated without any confidential
|
|
6
|
+
* credential.
|
|
7
|
+
*
|
|
8
|
+
* This is the Google-Analytics model applied to the whole SDK: the page ships
|
|
9
|
+
* only the **public** `editor.clientId`, exchanges it for a short-lived scoped
|
|
10
|
+
* token, and calls the API directly. No server route, no server action, and no
|
|
11
|
+
* `clientSecret` in the bundle.
|
|
12
|
+
*
|
|
13
|
+
* What it can do is deliberately narrow, and that is what makes shipping the
|
|
14
|
+
* key safe: read the project's published dictionary content — the content the
|
|
15
|
+
* page already renders — and submit analytics events. Writing content, reading
|
|
16
|
+
* a project, an organization or a user still require a real credential
|
|
17
|
+
* (server-side) or a signed-in user.
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```ts
|
|
21
|
+
* import { createPublicClient } from '@intlayer/api/public';
|
|
22
|
+
*
|
|
23
|
+
* const client = createPublicClient();
|
|
24
|
+
*
|
|
25
|
+
* const keys = await client.getDictionaryKeys();
|
|
26
|
+
* const [dictionary] = await client.getDictionaries(['navbar']);
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
/** `sessionStorage` key holding the cached token for this tab. */
|
|
30
|
+
const TOKEN_STORAGE_KEY = "__intlayer_public_token__";
|
|
31
|
+
/**
|
|
32
|
+
* Refresh margin: a token within this window of expiry is treated as expired,
|
|
33
|
+
* so a request is never sent with a token that dies in flight.
|
|
34
|
+
*/
|
|
35
|
+
const EXPIRY_MARGIN_MS = 6e4;
|
|
36
|
+
/** Reads the cached token for this tab, ignoring an unusable cache. */
|
|
37
|
+
const readStoredToken = () => {
|
|
38
|
+
try {
|
|
39
|
+
const raw = globalThis.sessionStorage?.getItem(TOKEN_STORAGE_KEY);
|
|
40
|
+
if (!raw) return null;
|
|
41
|
+
const parsed = JSON.parse(raw);
|
|
42
|
+
if (typeof parsed?.token !== "string") return null;
|
|
43
|
+
if (typeof parsed?.expiresAt !== "number") return null;
|
|
44
|
+
return parsed;
|
|
45
|
+
} catch {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
/** Persists the token for this tab. Best-effort: storage may be unavailable. */
|
|
50
|
+
const writeStoredToken = (cached) => {
|
|
51
|
+
try {
|
|
52
|
+
globalThis.sessionStorage?.setItem(TOKEN_STORAGE_KEY, JSON.stringify(cached));
|
|
53
|
+
} catch {}
|
|
54
|
+
};
|
|
55
|
+
/**
|
|
56
|
+
* Creates a {@link PublicClient} for the configured project.
|
|
57
|
+
*
|
|
58
|
+
* @param options - Optional configuration override.
|
|
59
|
+
* @returns A client that authenticates itself with the public project key.
|
|
60
|
+
*/
|
|
61
|
+
const createPublicClient = ({ intlayerConfig } = {}) => {
|
|
62
|
+
const resolvedEditor = intlayerConfig?.editor ?? editor;
|
|
63
|
+
const backendURL = (resolvedEditor?.backendURL ?? "").replace(/\/$/, "");
|
|
64
|
+
const clientId = resolvedEditor?.clientId;
|
|
65
|
+
let cached = null;
|
|
66
|
+
let pendingExchange = null;
|
|
67
|
+
const isUsable = (candidate) => candidate !== null && candidate.expiresAt - EXPIRY_MARGIN_MS > Date.now();
|
|
68
|
+
const exchange = async () => {
|
|
69
|
+
const response = await fetch(`${backendURL}/api/public/token`, {
|
|
70
|
+
method: "POST",
|
|
71
|
+
headers: { "Content-Type": "application/json" },
|
|
72
|
+
body: JSON.stringify({ clientId }),
|
|
73
|
+
credentials: "omit",
|
|
74
|
+
mode: "cors"
|
|
75
|
+
});
|
|
76
|
+
if (!response.ok) return null;
|
|
77
|
+
const payload = await response.json();
|
|
78
|
+
const token = payload?.data?.token;
|
|
79
|
+
const expiresIn = payload?.data?.expiresIn ?? 0;
|
|
80
|
+
if (!token || expiresIn <= 0) return null;
|
|
81
|
+
cached = {
|
|
82
|
+
token,
|
|
83
|
+
expiresAt: Date.now() + expiresIn * 1e3
|
|
84
|
+
};
|
|
85
|
+
writeStoredToken(cached);
|
|
86
|
+
return token;
|
|
87
|
+
};
|
|
88
|
+
const getToken = async () => {
|
|
89
|
+
if (isUsable(cached)) return cached.token;
|
|
90
|
+
const stored = readStoredToken();
|
|
91
|
+
if (isUsable(stored)) {
|
|
92
|
+
cached = stored;
|
|
93
|
+
return stored.token;
|
|
94
|
+
}
|
|
95
|
+
if (!clientId) return null;
|
|
96
|
+
pendingExchange ??= exchange().finally(() => {
|
|
97
|
+
pendingExchange = null;
|
|
98
|
+
});
|
|
99
|
+
return await pendingExchange;
|
|
100
|
+
};
|
|
101
|
+
/** Issues an authorised GET against the public surface. */
|
|
102
|
+
const request = async (path, fallback) => {
|
|
103
|
+
const token = await getToken();
|
|
104
|
+
if (!token) return fallback;
|
|
105
|
+
const response = await fetch(`${backendURL}/api/public${path}`, {
|
|
106
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
107
|
+
credentials: "omit",
|
|
108
|
+
mode: "cors"
|
|
109
|
+
});
|
|
110
|
+
if (!response.ok) return fallback;
|
|
111
|
+
return (await response.json())?.data ?? fallback;
|
|
112
|
+
};
|
|
113
|
+
return {
|
|
114
|
+
getToken,
|
|
115
|
+
getDictionaryKeys: () => request("/dictionaries/keys", []),
|
|
116
|
+
getDictionaries: (keys) => request(`/dictionaries?keys=${encodeURIComponent(keys.join(","))}`, [])
|
|
117
|
+
};
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
//#endregion
|
|
121
|
+
export { createPublicClient };
|
|
122
|
+
//# sourceMappingURL=createPublicClient.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"createPublicClient.mjs","names":[],"sources":["../../../src/public/createPublicClient.ts"],"sourcesContent":["import type { DictionaryAPI } from '@intlayer/backend';\nimport { editor } from '@intlayer/config/built';\nimport type { IntlayerConfig } from '@intlayer/types/config';\n\n/**\n * Browser-safe Intlayer client, authenticated without any confidential\n * credential.\n *\n * This is the Google-Analytics model applied to the whole SDK: the page ships\n * only the **public** `editor.clientId`, exchanges it for a short-lived scoped\n * token, and calls the API directly. No server route, no server action, and no\n * `clientSecret` in the bundle.\n *\n * What it can do is deliberately narrow, and that is what makes shipping the\n * key safe: read the project's published dictionary content — the content the\n * page already renders — and submit analytics events. Writing content, reading\n * a project, an organization or a user still require a real credential\n * (server-side) or a signed-in user.\n *\n * @example\n * ```ts\n * import { createPublicClient } from '@intlayer/api/public';\n *\n * const client = createPublicClient();\n *\n * const keys = await client.getDictionaryKeys();\n * const [dictionary] = await client.getDictionaries(['navbar']);\n * ```\n */\n\n/** `sessionStorage` key holding the cached token for this tab. */\nconst TOKEN_STORAGE_KEY = '__intlayer_public_token__';\n\n/**\n * Refresh margin: a token within this window of expiry is treated as expired,\n * so a request is never sent with a token that dies in flight.\n */\nconst EXPIRY_MARGIN_MS = 60 * 1000;\n\ntype CachedToken = {\n token: string;\n /** Epoch milliseconds. */\n expiresAt: number;\n};\n\n/** Reads the cached token for this tab, ignoring an unusable cache. */\nconst readStoredToken = (): CachedToken | null => {\n try {\n const raw = globalThis.sessionStorage?.getItem(TOKEN_STORAGE_KEY);\n if (!raw) return null;\n\n const parsed = JSON.parse(raw) as CachedToken;\n\n if (typeof parsed?.token !== 'string') return null;\n if (typeof parsed?.expiresAt !== 'number') return null;\n\n return parsed;\n } catch {\n // Private mode, disabled storage, or a corrupted entry — start fresh.\n return null;\n }\n};\n\n/** Persists the token for this tab. Best-effort: storage may be unavailable. */\nconst writeStoredToken = (cached: CachedToken): void => {\n try {\n globalThis.sessionStorage?.setItem(\n TOKEN_STORAGE_KEY,\n JSON.stringify(cached)\n );\n } catch {\n /* ignore — the in-memory cache still applies for this page */\n }\n};\n\n/** Options accepted by {@link createPublicClient}. */\nexport type PublicClientOptions = {\n /** Overrides the build-time `editor` configuration. */\n intlayerConfig?: Pick<IntlayerConfig, 'editor'>;\n};\n\n/** The credential-free client returned by {@link createPublicClient}. */\nexport type PublicClient = {\n /**\n * Returns the current token, exchanging the public `clientId` for one when\n * needed. `null` when the project is unknown or the origin is not allowed.\n */\n getToken: () => Promise<string | null>;\n /** Every dictionary key published for the project. */\n getDictionaryKeys: () => Promise<string[]>;\n /**\n * Published dictionary content for the given keys.\n *\n * @param keys - Dictionary keys to fetch (at most 100 per call).\n */\n getDictionaries: (keys: string[]) => Promise<DictionaryAPI[]>;\n};\n\n/**\n * Creates a {@link PublicClient} for the configured project.\n *\n * @param options - Optional configuration override.\n * @returns A client that authenticates itself with the public project key.\n */\nexport const createPublicClient = ({\n intlayerConfig,\n}: PublicClientOptions = {}): PublicClient => {\n const resolvedEditor = intlayerConfig?.editor ?? editor;\n const backendURL = (resolvedEditor?.backendURL ?? '').replace(/\\/$/, '');\n const clientId = resolvedEditor?.clientId;\n\n let cached: CachedToken | null = null;\n let pendingExchange: Promise<string | null> | null = null;\n\n const isUsable = (candidate: CachedToken | null): candidate is CachedToken =>\n candidate !== null && candidate.expiresAt - EXPIRY_MARGIN_MS > Date.now();\n\n const exchange = async (): Promise<string | null> => {\n const response = await fetch(`${backendURL}/api/public/token`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ clientId }),\n // Authorised by `Origin`, never by cookies.\n credentials: 'omit',\n mode: 'cors',\n });\n\n if (!response.ok) return null;\n\n const payload = (await response.json()) as {\n data?: { token?: string | null; expiresIn?: number };\n };\n\n const token = payload?.data?.token;\n const expiresIn = payload?.data?.expiresIn ?? 0;\n\n if (!token || expiresIn <= 0) return null;\n\n cached = { token, expiresAt: Date.now() + expiresIn * 1000 };\n writeStoredToken(cached);\n\n return token;\n };\n\n const getToken = async (): Promise<string | null> => {\n if (isUsable(cached)) return cached.token;\n\n const stored = readStoredToken();\n if (isUsable(stored)) {\n cached = stored;\n return stored.token;\n }\n\n if (!clientId) return null;\n\n // De-duplicate concurrent exchanges so a burst of calls triggers one fetch.\n pendingExchange ??= exchange().finally(() => {\n pendingExchange = null;\n });\n\n return await pendingExchange;\n };\n\n /** Issues an authorised GET against the public surface. */\n const request = async <Result>(\n path: string,\n fallback: Result\n ): Promise<Result> => {\n const token = await getToken();\n\n if (!token) return fallback;\n\n const response = await fetch(`${backendURL}/api/public${path}`, {\n headers: { Authorization: `Bearer ${token}` },\n credentials: 'omit',\n mode: 'cors',\n });\n\n if (!response.ok) return fallback;\n\n const payload = (await response.json()) as { data?: Result };\n\n return payload?.data ?? fallback;\n };\n\n return {\n getToken,\n getDictionaryKeys: () => request<string[]>('/dictionaries/keys', []),\n getDictionaries: (keys) =>\n request<DictionaryAPI[]>(\n `/dictionaries?keys=${encodeURIComponent(keys.join(','))}`,\n []\n ),\n };\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+BA,MAAM,oBAAoB;;;;;AAM1B,MAAM,mBAAmB;;AASzB,MAAM,wBAA4C;CAChD,IAAI;EACF,MAAM,MAAM,WAAW,gBAAgB,QAAQ,iBAAiB;EAChE,IAAI,CAAC,KAAK,OAAO;EAEjB,MAAM,SAAS,KAAK,MAAM,GAAG;EAE7B,IAAI,OAAO,QAAQ,UAAU,UAAU,OAAO;EAC9C,IAAI,OAAO,QAAQ,cAAc,UAAU,OAAO;EAElD,OAAO;CACT,QAAQ;EAEN,OAAO;CACT;AACF;;AAGA,MAAM,oBAAoB,WAA8B;CACtD,IAAI;EACF,WAAW,gBAAgB,QACzB,mBACA,KAAK,UAAU,MAAM,CACvB;CACF,QAAQ,CAER;AACF;;;;;;;AA+BA,MAAa,sBAAsB,EACjC,mBACuB,CAAC,MAAoB;CAC5C,MAAM,iBAAiB,gBAAgB,UAAU;CACjD,MAAM,cAAc,gBAAgB,cAAc,GAAE,CAAE,QAAQ,OAAO,EAAE;CACvE,MAAM,WAAW,gBAAgB;CAEjC,IAAI,SAA6B;CACjC,IAAI,kBAAiD;CAErD,MAAM,YAAY,cAChB,cAAc,QAAQ,UAAU,YAAY,mBAAmB,KAAK,IAAI;CAE1E,MAAM,WAAW,YAAoC;EACnD,MAAM,WAAW,MAAM,MAAM,GAAG,WAAW,oBAAoB;GAC7D,QAAQ;GACR,SAAS,EAAE,gBAAgB,mBAAmB;GAC9C,MAAM,KAAK,UAAU,EAAE,SAAS,CAAC;GAEjC,aAAa;GACb,MAAM;EACR,CAAC;EAED,IAAI,CAAC,SAAS,IAAI,OAAO;EAEzB,MAAM,UAAW,MAAM,SAAS,KAAK;EAIrC,MAAM,QAAQ,SAAS,MAAM;EAC7B,MAAM,YAAY,SAAS,MAAM,aAAa;EAE9C,IAAI,CAAC,SAAS,aAAa,GAAG,OAAO;EAErC,SAAS;GAAE;GAAO,WAAW,KAAK,IAAI,IAAI,YAAY;EAAK;EAC3D,iBAAiB,MAAM;EAEvB,OAAO;CACT;CAEA,MAAM,WAAW,YAAoC;EACnD,IAAI,SAAS,MAAM,GAAG,OAAO,OAAO;EAEpC,MAAM,SAAS,gBAAgB;EAC/B,IAAI,SAAS,MAAM,GAAG;GACpB,SAAS;GACT,OAAO,OAAO;EAChB;EAEA,IAAI,CAAC,UAAU,OAAO;EAGtB,oBAAoB,SAAS,CAAC,CAAC,cAAc;GAC3C,kBAAkB;EACpB,CAAC;EAED,OAAO,MAAM;CACf;;CAGA,MAAM,UAAU,OACd,MACA,aACoB;EACpB,MAAM,QAAQ,MAAM,SAAS;EAE7B,IAAI,CAAC,OAAO,OAAO;EAEnB,MAAM,WAAW,MAAM,MAAM,GAAG,WAAW,aAAa,QAAQ;GAC9D,SAAS,EAAE,eAAe,UAAU,QAAQ;GAC5C,aAAa;GACb,MAAM;EACR,CAAC;EAED,IAAI,CAAC,SAAS,IAAI,OAAO;EAIzB,QAAO,MAFgB,SAAS,KAAK,EAEvB,EAAE,QAAQ;CAC1B;CAEA,OAAO;EACL;EACA,yBAAyB,QAAkB,sBAAsB,CAAC,CAAC;EACnE,kBAAkB,SAChB,QACE,sBAAsB,mBAAmB,KAAK,KAAK,GAAG,CAAC,KACvD,CAAC,CACH;CACJ;AACF"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"createAuthManager.d.ts","names":[],"sources":["../../../src/cms/createAuthManager.ts"],"mappings":";;;;;;;
|
|
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;;;;KA0CO;;;;EAIV,qBAAqB;;;;;;EAMrB;;;;;KAMU;;;;;EAKV;;;;;;;;;;KAWU;;;;;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;;;;;;;;;;;;;cAcM,oBAAiB,iBACZ,KAAK,2BAAyB,UACrC,uBACR"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { IntlayerConfig } from "@intlayer/types/config";
|
|
2
|
+
import { DictionaryAPI } from "@intlayer/backend";
|
|
3
|
+
//#region src/public/createPublicClient.d.ts
|
|
4
|
+
/** Options accepted by {@link createPublicClient}. */
|
|
5
|
+
type PublicClientOptions = {
|
|
6
|
+
/** Overrides the build-time `editor` configuration. */
|
|
7
|
+
intlayerConfig?: Pick<IntlayerConfig, 'editor'>;
|
|
8
|
+
};
|
|
9
|
+
/** The credential-free client returned by {@link createPublicClient}. */
|
|
10
|
+
type PublicClient = {
|
|
11
|
+
/**
|
|
12
|
+
* Returns the current token, exchanging the public `clientId` for one when
|
|
13
|
+
* needed. `null` when the project is unknown or the origin is not allowed.
|
|
14
|
+
*/
|
|
15
|
+
getToken: () => Promise<string | null>;
|
|
16
|
+
/** Every dictionary key published for the project. */
|
|
17
|
+
getDictionaryKeys: () => Promise<string[]>;
|
|
18
|
+
/**
|
|
19
|
+
* Published dictionary content for the given keys.
|
|
20
|
+
*
|
|
21
|
+
* @param keys - Dictionary keys to fetch (at most 100 per call).
|
|
22
|
+
*/
|
|
23
|
+
getDictionaries: (keys: string[]) => Promise<DictionaryAPI[]>;
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Creates a {@link PublicClient} for the configured project.
|
|
27
|
+
*
|
|
28
|
+
* @param options - Optional configuration override.
|
|
29
|
+
* @returns A client that authenticates itself with the public project key.
|
|
30
|
+
*/
|
|
31
|
+
declare const createPublicClient: ({ intlayerConfig }?: PublicClientOptions) => PublicClient;
|
|
32
|
+
//#endregion
|
|
33
|
+
export { PublicClient, PublicClientOptions, createPublicClient };
|
|
34
|
+
//# sourceMappingURL=createPublicClient.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"createPublicClient.d.ts","names":[],"sources":["../../../src/public/createPublicClient.ts"],"mappings":";;;;KA4EY;;EAEV,iBAAiB,KAAK;;;KAIZ;;;;;EAKV,gBAAgB;;EAEhB,yBAAyB;;;;;;EAMzB,kBAAkB,mBAAmB,QAAQ;;;;;;;;cASlC,uBAAkB,mBAE5B,wBAA2B"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@intlayer/api",
|
|
3
|
-
"version": "9.4.0
|
|
3
|
+
"version": "9.4.0",
|
|
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": [
|
|
@@ -48,6 +48,11 @@
|
|
|
48
48
|
"require": "./dist/cjs/getIntlayerAPI/analytics.cjs",
|
|
49
49
|
"import": "./dist/esm/getIntlayerAPI/analytics.mjs"
|
|
50
50
|
},
|
|
51
|
+
"./public": {
|
|
52
|
+
"types": "./dist/types/public/index.d.ts",
|
|
53
|
+
"require": "./dist/cjs/public/index.cjs",
|
|
54
|
+
"import": "./dist/esm/public/index.mjs"
|
|
55
|
+
},
|
|
51
56
|
"./asset": {
|
|
52
57
|
"types": "./dist/types/getIntlayerAPI/asset.d.ts",
|
|
53
58
|
"require": "./dist/cjs/getIntlayerAPI/asset.cjs",
|
|
@@ -166,6 +171,9 @@
|
|
|
166
171
|
"analytics": [
|
|
167
172
|
"./dist/types/getIntlayerAPI/analytics.d.ts"
|
|
168
173
|
],
|
|
174
|
+
"public": [
|
|
175
|
+
"./dist/types/public/index.d.ts"
|
|
176
|
+
],
|
|
169
177
|
"audit": [
|
|
170
178
|
"./dist/types/getIntlayerAPI/audit.d.ts"
|
|
171
179
|
],
|
|
@@ -257,11 +265,11 @@
|
|
|
257
265
|
},
|
|
258
266
|
"dependencies": {
|
|
259
267
|
"@intlayer/config": "9.4.0-canary.0",
|
|
260
|
-
"@intlayer/types": "9.4.0
|
|
268
|
+
"@intlayer/types": "9.4.0",
|
|
261
269
|
"defu": "6.1.7"
|
|
262
270
|
},
|
|
263
271
|
"devDependencies": {
|
|
264
|
-
"@types/node": "26.
|
|
272
|
+
"@types/node": "26.3.0",
|
|
265
273
|
"@utils/ts-config": "1.0.4",
|
|
266
274
|
"@utils/ts-config-types": "1.0.4",
|
|
267
275
|
"@utils/tsdown-config": "1.0.4",
|