@keboola/api-client-react 25.0.0 → 27.0.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/AGENTS.md ADDED
@@ -0,0 +1,138 @@
1
+ # @keboola/api-client-react
2
+
3
+ React bindings for [`@keboola/api-client`](../api-client/AGENTS.md). Owns
4
+ the context that holds the typed `ApiClient` instance and the hooks for
5
+ consumers to read it.
6
+
7
+ ## Why a separate package
8
+
9
+ `@keboola/api-client` is framework-agnostic (used from Node, tests, scripts).
10
+ Keeping React-specific glue out of it lets the SDK stay deployable to
11
+ non-React surfaces. This package is the **only** place that knows about
12
+ `React.Context`.
13
+
14
+ ## Surface
15
+
16
+ ```ts
17
+ import {
18
+ ApiClientProvider,
19
+ AuthProvider,
20
+ LegacyAuthProvider,
21
+ useApiClient,
22
+ useAuth,
23
+ useLegacyAuth,
24
+ useProject,
25
+ useStack,
26
+ } from '@keboola/api-client-react';
27
+ ```
28
+
29
+ A keboola client is `{ api, session }`. This package provides the two halves separately:
30
+
31
+ - `ApiClientProvider` — takes the flow-agnostic `api` and provides it to descendants. A pure
32
+ pass-through: no lifecycle, no fallback. `useApiClient()` returns that `api`.
33
+ - `LegacyAuthProvider` — takes a **legacy** `session` (+ `baseUrl`, `storageToken`,
34
+ optional `managementToken`). On mount it drives the session to ready
35
+ (`setBaseUrl` → `authenticateManagement?` → `authenticateStorageAsync`) and renders `fallback`
36
+ until `session.isStorageReady()`. Exposes the session via `useLegacyAuth()`.
37
+ - `AuthProvider` — takes a **bearer** `session` (+ `baseUrl`, optional `onAccessTokenChange`). The
38
+ bearer twin of `LegacyAuthProvider`. On mount it configures the stack and `restore()`s any
39
+ persisted session, then runs one of two modes, discriminated by whether a credential is supplied:
40
+ - **programmatic** — `credential` + `projectId` given; it drives itself to ready.
41
+ - **interactive** — neither given; the session starts unscoped and only the consumer can scope it,
42
+ so `anonymousFallback` is **required** by the type in this mode. Which slot actually renders at
43
+ any moment is the table below — restoring and bootstrapping take `loadingFallback` first.
44
+
45
+ It renders exactly one of four slots, by state:
46
+
47
+ | State | Renders |
48
+ | --------------------------------- | ------------------- |
49
+ | scoped + storage build ready | `children` |
50
+ | storage build failed | `errorFallback` |
51
+ | restoring / bootstrapping | `loadingFallback` |
52
+ | not yet scoped (interactive only) | `anonymousFallback` |
53
+
54
+ "Not yet scoped" is **not** the same as "signed out": the storage build sits at `idle` both for an
55
+ anonymous visitor and for an authenticated one with no project selected (zero projects, or just
56
+ used "Leave project"), and both land in `anonymousFallback`. Consumers that need to tell the two
57
+ apart have to read the session status themselves.
58
+
59
+ `children` may be a function — it is called only in the ready state and receives the scoped
60
+ `{ projectId, baseUrl }`, so consumers don't re-narrow the session state themselves:
61
+
62
+ ```tsx
63
+ <AuthProvider session={session} baseUrl={url} anonymousFallback={<Login />}>
64
+ {({ projectId }) => <App projectId={projectId} />}
65
+ </AuthProvider>
66
+ ```
67
+
68
+ `onAccessTokenChange` fires with the access token on the initial credential and after every
69
+ in-band rotation, so the host can mirror it (e.g. into a token store). Exposes the session via
70
+ `useAuth()`.
71
+
72
+ - `useApiClient()` — returns the `api`; **throws** outside `<ApiClientProvider />`. Use it from
73
+ query/mutation hooks.
74
+ - `useProject()` / `useStack()` — shortcut hooks for `api.context.project` / `api.context.stack`.
75
+ Same null-throwing contract.
76
+
77
+ Nesting: mount the auth provider outermost (it owns drive-to-ready) with `ApiClientProvider` inside
78
+ it, so product code only mounts once the `api` is ready:
79
+
80
+ ```tsx
81
+ <LegacyAuthProvider session={session} baseUrl={url} storageToken={token} fallback={<Loading />}>
82
+ <ApiClientProvider api={api}>{children}</ApiClientProvider>
83
+ </LegacyAuthProvider>
84
+ ```
85
+
86
+ For credential verification (dev login, project switching) there is no provider —
87
+ construct the real per-service client with the candidate host + token and call its
88
+ verify: `createStorageClient({ baseUrl, auth: { type: 'sapi-token', token } }).tokens.verify()`, or
89
+ `createManagementClient({ baseUrl, auth: { type: 'management-token', token }, withAuthMiddleware: false }).verifyToken()`
90
+ (the flag skips the session-managing auth middleware so it's a plain validity check).
91
+
92
+ ## Conventions
93
+
94
+ - The context value is typed `KeboolaApi | null` and the hooks throw on
95
+ null — see [apps/AGENTS.md](../../apps/AGENTS.md) ("State Management"
96
+ → "Tree-passing"). Don't soften this to optional types or `??` fallbacks;
97
+ a missing provider is a programming error.
98
+ - React is a **peerDependency** (^19.0.0). Don't import React types from
99
+ this package's own deps in consumers.
100
+ - The auth provider drives the session to ready exactly once on mount. Changing
101
+ its props after mount is a no-op by design — if you need a new auth context,
102
+ mount a fresh provider.
103
+
104
+ ## Non-component access (loaders, services, module scope)
105
+
106
+ `useApiClient()` is for React components only. A TanStack Router loader, a
107
+ module instantiated at import time, or any code running outside the React tree
108
+ can't call the hook. Don't reach for the context there — construct a client
109
+ directly:
110
+
111
+ ```ts
112
+ // Single service — the common case
113
+ import { createStorageClient } from '@keboola/api-client/storage';
114
+
115
+ const storage = createStorageClient({ baseUrl, auth: { type: 'sapi-token', token } });
116
+ const tables = await storage.tables.getTables();
117
+ ```
118
+
119
+ If you need several services from non-component code, create one client at app
120
+ bootstrap (`createLegacyKeboola(...)` / `createKeboola(...)`, drive its session to
121
+ ready, and export the `api`) and import that instance where needed — rather than
122
+ re-creating clients per call. See
123
+ [`@keboola/api-client` → Getting a client](../api-client/AGENTS.md#getting-a-client).
124
+
125
+ ## Canonical consumers
126
+
127
+ - `apps/kbc-ui/src/scripts/app.tsx` — production `ApiClientProvider` mount
128
+ - `apps/kbc-ui/src/scripts/modules/sandboxes/queries.ts` — `useApiClient`
129
+ inside a TanStack Query hook (the dominant pattern)
130
+
131
+ ## Don'ts
132
+
133
+ - **Never** create a parallel React context that wraps another client
134
+ instance. One client per app, mounted once at the top.
135
+ - **Never** drive `session` manually in a consumer — the auth provider
136
+ (`AuthProvider` / `LegacyAuthProvider`) owns the drive-to-ready lifecycle.
137
+ - **Never** import from a deep path (`@keboola/api-client-react/src/...`).
138
+ The package only exports `src/index.ts`.
package/dist/index.cjs CHANGED
@@ -106,12 +106,13 @@ var LegacyAuthProvider = ({
106
106
  storageToken,
107
107
  managementToken,
108
108
  fallback = null,
109
+ errorFallback = fallback,
109
110
  children
110
111
  }) => {
111
- const [isReady, setIsReady] = react.useState(() => session.isStorageReady());
112
+ const [storageState, setStorageState] = react.useState(() => session.debug().storage);
112
113
  const startedRef = react.useRef(false);
113
114
  react.useEffect(() => {
114
- const unsubscribe = session.subscribe(() => setIsReady(session.isStorageReady()));
115
+ const unsubscribe = session.subscribe((snapshot) => setStorageState(snapshot.storage));
115
116
  if (!startedRef.current) {
116
117
  startedRef.current = true;
117
118
  session.setBaseUrl(baseUrl);
@@ -120,7 +121,17 @@ var LegacyAuthProvider = ({
120
121
  }
121
122
  return unsubscribe;
122
123
  }, []);
123
- return /* @__PURE__ */ jsxRuntime.jsx(LegacyAuthContextProvider, { value: session, children: isReady ? children : fallback });
124
+ react.useEffect(() => {
125
+ if (storageState === "errored") {
126
+ console.error("Keboola session bootstrap failed: the storage build errored.");
127
+ }
128
+ }, [storageState]);
129
+ const content = (() => {
130
+ if (storageState === "ready") return children;
131
+ if (storageState === "errored") return errorFallback;
132
+ return fallback;
133
+ })();
134
+ return /* @__PURE__ */ jsxRuntime.jsx(LegacyAuthContextProvider, { value: session, children: content });
124
135
  };
125
136
 
126
137
  exports.ApiClientProvider = ApiClientProvider;
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/context.ts","../src/ApiClientProvider.tsx","../src/AuthProvider.tsx","../src/LegacyAuthProvider.tsx"],"names":["createContext","use","jsx","useState","useRef","useEffect"],"mappings":";;;;;;AAIA,IAAM,wBAAA,GAA2B,wDAAA;AACjC,IAAM,qBAAA,GAAwB,sDAAA;AAC9B,IAAM,mBAAA,GAAsB,oDAAA;AAC5B,IAAM,kBAAA,GAAqB,8CAAA;AAC3B,IAAM,yBAAA,GAA4B,0DAAA;AAIlC,IAAM,UAAA,GAAaA,oBAAiC,IAAI,CAAA;AACjD,IAAM,WAAA,GAAc,UAAA;AAI3B,IAAM,WAAA,GAAcA,oBAAoC,IAAI,CAAA;AACrD,IAAM,mBAAA,GAAsB,WAAA;AAEnC,IAAM,iBAAA,GAAoBA,oBAAoC,IAAI,CAAA;AAC3D,IAAM,yBAAA,GAA4B,iBAAA;AAElC,IAAM,eAAe,MAAM;AAChC,EAAA,MAAM,GAAA,GAAMC,UAAI,UAAU,CAAA;AAC1B,EAAA,IAAI,GAAA,KAAQ,IAAA,EAAM,MAAM,IAAI,MAAM,wBAAwB,CAAA;AAC1D,EAAA,OAAO,GAAA;AACT;AAEO,IAAM,aAAa,MAAM;AAC9B,EAAA,MAAM,GAAA,GAAMA,UAAI,UAAU,CAAA;AAC1B,EAAA,IAAI,GAAA,KAAQ,IAAA,EAAM,MAAM,IAAI,MAAM,qBAAqB,CAAA;AACvD,EAAA,OAAO,IAAI,OAAA,CAAQ,OAAA;AACrB;AAEO,IAAM,WAAW,MAAM;AAC5B,EAAA,MAAM,GAAA,GAAMA,UAAI,UAAU,CAAA;AAC1B,EAAA,IAAI,GAAA,KAAQ,IAAA,EAAM,MAAM,IAAI,MAAM,mBAAmB,CAAA;AACrD,EAAA,OAAO,IAAI,OAAA,CAAQ,KAAA;AACrB;AAGO,IAAM,UAAU,MAAM;AAC3B,EAAA,MAAM,OAAA,GAAUA,UAAI,WAAW,CAAA;AAC/B,EAAA,IAAI,OAAA,KAAY,IAAA,EAAM,MAAM,IAAI,MAAM,kBAAkB,CAAA;AACxD,EAAA,OAAO,OAAA;AACT;AAGO,IAAM,gBAAgB,MAAM;AACjC,EAAA,MAAM,OAAA,GAAUA,UAAI,iBAAiB,CAAA;AACrC,EAAA,IAAI,OAAA,KAAY,IAAA,EAAM,MAAM,IAAI,MAAM,yBAAyB,CAAA;AAC/D,EAAA,OAAO,OAAA;AACT;ACpCO,IAAM,iBAAA,GAAoB,CAAC,EAAE,GAAA,EAAK,QAAA,uBACvCC,cAAA,CAAC,WAAA,EAAA,EAAY,KAAA,EAAO,GAAA,EAAM,QAAA,EAAS;ACkC9B,IAAM,eAAe,CAAC;AAAA,EAC3B,OAAA;AAAA,EACA,OAAA;AAAA,EACA,UAAA;AAAA,EACA,SAAA;AAAA,EACA,mBAAA;AAAA,EACA,eAAA,GAAkB,IAAA;AAAA,EAClB,aAAA,GAAgB,IAAA;AAAA,EAChB,iBAAA,GAAoB,IAAA;AAAA,EACpB;AACF,CAAA,KAAa;AACX,EAAA,MAAM,CAAC,UAAU,WAAW,CAAA,GAAIC,eAAmB,MAAM,OAAA,CAAQ,OAAO,CAAA;AACxE,EAAA,MAAM,CAAC,aAAa,cAAc,CAAA,GAAIA,eAAS,MAAM,OAAA,CAAQ,mBAAmB,CAAA;AAChF,EAAA,MAAM,CAAC,aAAA,EAAe,gBAAgB,CAAA,GAAIA,eAA+B,IAAI,CAAA;AAC7E,EAAA,MAAM,iBAAA,GAAoBC,aAAO,KAAK,CAAA;AAEtC,EAAAC,eAAA,CAAU,MAAM,OAAA,CAAQ,SAAA,CAAU,WAAW,CAAA,EAAG,CAAC,OAAO,CAAC,CAAA;AAIzD,EAAAA,eAAA,CAAU,MAAM;AACd,IAAA,IAAI,YAAY,MAAA,EAAW;AAC3B,IAAA,IAAI,QAAA,CAAS,MAAA,CAAO,OAAA,KAAY,IAAA,EAAM;AACtC,IAAA,OAAA,CAAQ,WAAW,OAAO,CAAA;AAAA,EAC5B,GAAG,CAAC,OAAA,EAAS,SAAS,QAAA,CAAS,MAAA,CAAO,OAAO,CAAC,CAAA;AAM9C,EAAAA,eAAA,CAAU,MAAM;AACd,IAAA,IAAI,kBAAkB,OAAA,EAAS;AAC/B,IAAA,iBAAA,CAAkB,OAAA,GAAU,IAAA;AAC5B,IAAA,OAAA,CACG,OAAA,EAAQ,CACR,IAAA,CAAK,CAAC,MAAA,KAAW,gBAAA,CAAiB,MAAA,CAAO,MAAM,CAAC,CAAA,CAChD,KAAA,CAAM,CAAC,KAAA,KAAU;AAChB,MAAA,OAAA,CAAQ,KAAA,CAAM,yCAAyC,KAAK,CAAA;AAC5D,MAAA,gBAAA,CAAiB,OAAO,CAAA;AAAA,IAC1B,CAAC,CAAA,CACA,OAAA,CAAQ,MAAM,cAAA,CAAe,KAAK,CAAC,CAAA;AAAA,EACxC,CAAA,EAAG,CAAC,OAAO,CAAC,CAAA;AAEZ,EAAAA,eAAA,CAAU,MAAM;AACd,IAAA,IAAI,aAAA,KAAkB,IAAA,IAAQ,aAAA,KAAkB,UAAA,EAAY;AAC5D,IAAA,IAAI,eAAe,MAAA,EAAW;AAC9B,IAAA,IAAI,QAAA,CAAS,MAAA,CAAO,MAAA,KAAW,WAAA,EAAa;AAC5C,IAAA,OAAA,CAAQ,aAAa,UAAU,CAAA;AAAA,EACjC,CAAA,EAAG,CAAC,OAAA,EAAS,aAAA,EAAe,YAAY,QAAA,CAAS,MAAA,CAAO,MAAM,CAAC,CAAA;AAE/D,EAAAA,eAAA,CAAU,MAAM;AACd,IAAA,IAAI,aAAA,KAAkB,IAAA,IAAQ,aAAA,KAAkB,UAAA,EAAY;AAC5D,IAAA,IAAI,cAAc,MAAA,EAAW;AAC7B,IAAA,IAAI,QAAA,CAAS,MAAA,CAAO,MAAA,KAAW,eAAA,EAAiB;AAChD,IAAA,OAAA,CAAQ,WAAW,SAAS,CAAA;AAAA,EAC9B,CAAA,EAAG,CAAC,OAAA,EAAS,aAAA,EAAe,WAAW,QAAA,CAAS,MAAA,CAAO,MAAM,CAAC,CAAA;AAE9D,EAAA,MAAM,WAAA,GACJ,SAAS,MAAA,CAAO,MAAA,KAAW,cAAc,MAAA,GAAY,QAAA,CAAS,OAAO,UAAA,CAAW,WAAA;AAElF,EAAAA,eAAA,CAAU,MAAM;AACd,IAAA,IAAI,WAAA,KAAgB,MAAA,EAAW,mBAAA,GAAsB,WAAW,CAAA;AAAA,EAClE,CAAA,EAAG,CAAC,WAAA,EAAa,mBAAmB,CAAC,CAAA;AAGrC,EAAA,MAAM,cAAA,GAAiB,UAAA,KAAe,MAAA,IAAa,SAAA,KAAc,MAAA;AAEjE,EAAA,MAAM,WAAW,MAAM;AACrB,IAAA,IAAI,QAAA,CAAS,YAAY,OAAA,EAAS;AAChC,MAAA,IAAI,OAAO,QAAA,KAAa,UAAA,EAAY,OAAO,QAAA;AAE3C,MAAA,IAAI,QAAA,CAAS,MAAA,CAAO,MAAA,KAAW,QAAA,EAAU,OAAO,IAAA;AAChD,MAAA,MAAM,EAAE,SAAA,EAAW,eAAA,EAAiB,OAAA,EAAS,aAAA,KAAkB,QAAA,CAAS,MAAA;AACxE,MAAA,OAAO,SAAS,EAAE,SAAA,EAAW,eAAA,EAAiB,OAAA,EAAS,eAAe,CAAA;AAAA,IACxE;AACA,IAAA,IAAI,QAAA,CAAS,OAAA,KAAY,SAAA,EAAW,OAAO,aAAA;AAC3C,IAAA,IAAI,WAAA,IAAe,QAAA,CAAS,OAAA,KAAY,SAAA,IAAa,gBAAgB,OAAO,eAAA;AAC5E,IAAA,OAAO,iBAAA;AAAA,EACT,CAAA,GAAG;AAEH,EAAA,uBAAOH,cAAAA,CAAC,mBAAA,EAAA,EAAoB,KAAA,EAAO,SAAU,QAAA,EAAA,OAAA,EAAQ,CAAA;AACvD;AC3GO,IAAM,qBAAqB,CAAC;AAAA,EACjC,OAAA;AAAA,EACA,OAAA;AAAA,EACA,YAAA;AAAA,EACA,eAAA;AAAA,EACA,QAAA,GAAW,IAAA;AAAA,EACX;AACF,CAAA,KAAa;AACX,EAAA,MAAM,CAAC,SAAS,UAAU,CAAA,GAAIC,eAAkB,MAAM,OAAA,CAAQ,gBAAgB,CAAA;AAE9E,EAAA,MAAM,UAAA,GAAaC,aAAgB,KAAK,CAAA;AAExC,EAAAC,gBAAU,MAAM;AACd,IAAA,MAAM,WAAA,GAAc,QAAQ,SAAA,CAAU,MAAM,WAAW,OAAA,CAAQ,cAAA,EAAgB,CAAC,CAAA;AAEhF,IAAA,IAAI,CAAC,WAAW,OAAA,EAAS;AACvB,MAAA,UAAA,CAAW,OAAA,GAAU,IAAA;AACrB,MAAA,OAAA,CAAQ,WAAW,OAAO,CAAA;AAC1B,MAAA,IAAI,eAAA,EAAiB,OAAA,CAAQ,sBAAA,CAAuB,eAAe,CAAA;AAGnE,MAAA,KAAK,OAAA,CACF,wBAAA,CAAyB,YAAY,CAAA,CACrC,KAAA,CAAM,CAAC,KAAA,KAAU,OAAA,CAAQ,KAAA,CAAM,kCAAA,EAAoC,KAAK,CAAC,CAAA;AAAA,IAC9E;AAEA,IAAA,OAAO,WAAA;AAAA,EAGT,CAAA,EAAG,EAAE,CAAA;AAEL,EAAA,uBACEH,cAAAA,CAAC,yBAAA,EAAA,EAA0B,OAAO,OAAA,EAC/B,QAAA,EAAA,OAAA,GAAU,WAAW,QAAA,EACxB,CAAA;AAEJ","file":"index.cjs","sourcesContent":["import { createContext, use } from 'react';\n\nimport type { BearerSession, KeboolaApi, LegacySession } from '@keboola/api-client';\n\nconst MISSING_API_CLIENT_ERROR = 'useApiClient must be used within <ApiClientProvider />';\nconst MISSING_PROJECT_ERROR = 'useProject must be used within <ApiClientProvider />';\nconst MISSING_STACK_ERROR = 'useStack must be used within <ApiClientProvider />';\nconst MISSING_AUTH_ERROR = 'useAuth must be used within <AuthProvider />';\nconst MISSING_LEGACY_AUTH_ERROR = 'useLegacyAuth must be used within <LegacyAuthProvider />';\n\n// The product API (`auth` + `management` + every product namespace), flow-agnostic. Both the\n// bearer and legacy flows produce the same `KeboolaApi` surface, so consumers never branch on flow.\nconst ApiContext = createContext<KeboolaApi | null>(null);\nexport const ApiProvider = ApiContext;\n\n// The lifecycle sessions live in their own, flow-specific contexts so the auth surface a component\n// reaches for is explicit. Only the matching provider is mounted per flow.\nconst AuthContext = createContext<BearerSession | null>(null);\nexport const AuthContextProvider = AuthContext;\n\nconst LegacyAuthContext = createContext<LegacySession | null>(null);\nexport const LegacyAuthContextProvider = LegacyAuthContext;\n\nexport const useApiClient = () => {\n const api = use(ApiContext);\n if (api === null) throw new Error(MISSING_API_CLIENT_ERROR);\n return api;\n};\n\nexport const useProject = () => {\n const api = use(ApiContext);\n if (api === null) throw new Error(MISSING_PROJECT_ERROR);\n return api.context.project;\n};\n\nexport const useStack = () => {\n const api = use(ApiContext);\n if (api === null) throw new Error(MISSING_STACK_ERROR);\n return api.context.stack;\n};\n\n/** The bearer (OAuth) lifecycle session. Throws outside `<AuthProvider />`. */\nexport const useAuth = () => {\n const session = use(AuthContext);\n if (session === null) throw new Error(MISSING_AUTH_ERROR);\n return session;\n};\n\n/** The legacy (SAPI) lifecycle session. Throws outside `<LegacyAuthProvider />`. */\nexport const useLegacyAuth = () => {\n const session = use(LegacyAuthContext);\n if (session === null) throw new Error(MISSING_LEGACY_AUTH_ERROR);\n return session;\n};\n","import type { ReactNode } from 'react';\n\nimport type { KeboolaApi } from '@keboola/api-client';\n\nimport { ApiProvider } from './context';\n\ntype Props = {\n /** The flow-agnostic product API — the `api` half of a keboola client. */\n api: KeboolaApi;\n children: ReactNode;\n};\n\n/**\n * Provides the product API to `useApiClient()`. A pure pass-through: the auth lifecycle (driving\n * to ready, gating on a fallback) belongs to `AuthProvider` / `LegacyAuthProvider`, which mount\n * this provider once their session is ready.\n */\nexport const ApiClientProvider = ({ api, children }: Props) => (\n <ApiProvider value={api}>{children}</ApiProvider>\n);\n","import { useEffect, useRef, useState } from 'react';\nimport type { ReactNode } from 'react';\n\nimport type { BearerSession } from '@keboola/api-client';\n\nimport { AuthContextProvider } from './context';\n\ntype BearerCredential = Parameters<BearerSession['authenticate']>[0];\ntype Snapshot = ReturnType<BearerSession['debug']>;\ntype RestoreStatus = Awaited<ReturnType<BearerSession['restore']>>['status'];\n\n/** The session state guaranteed to exist once the storage build is ready. */\ntype ScopedState = { projectId: number; baseUrl: string };\n\ntype BaseProps = {\n session: BearerSession;\n onAccessTokenChange?: (accessToken: string) => void;\n /** Rendered while a persisted session is being restored, or the bootstrap is in flight. */\n loadingFallback?: ReactNode;\n /** Rendered when the storage build fails. */\n errorFallback?: ReactNode;\n /**\n * Rendered only once the session is scoped and the storage build is ready. Pass a function to\n * receive the scoped state (the project the session settled on) without re-narrowing it yourself.\n */\n children: ReactNode | ((scoped: ScopedState) => ReactNode);\n};\n\n/**\n * Two mutually exclusive bootstrap modes, discriminated by whether a credential is supplied:\n * - **programmatic** — `credential` + `projectId` given, so `baseUrl` is **required**: a credential\n * is bound to the stack that issued it and `authenticate` refuses an unconfigured session.\n * - **interactive** — none of the three given; the stack comes from the session instead — `restore()`\n * adopts the persisted one, otherwise the login UI sets it. `anonymousFallback` is **required**\n * here (a missing login UI would leave the app on a permanently blank screen).\n */\ntype Props = BaseProps &\n (\n | {\n credential: BearerCredential;\n projectId: number;\n baseUrl: string;\n anonymousFallback?: never;\n }\n | {\n credential?: undefined;\n projectId?: undefined;\n baseUrl?: never;\n anonymousFallback: ReactNode;\n }\n );\n\nexport const AuthProvider = ({\n session,\n baseUrl,\n credential,\n projectId,\n onAccessTokenChange,\n loadingFallback = null,\n errorFallback = null,\n anonymousFallback = null,\n children,\n}: Props) => {\n const [snapshot, setSnapshot] = useState<Snapshot>(() => session.debug());\n const [isRestoring, setIsRestoring] = useState(() => session.hasPersistedValue());\n const [restoreStatus, setRestoreStatus] = useState<RestoreStatus | null>(null);\n const restoreStartedRef = useRef(false);\n\n useEffect(() => session.subscribe(setSnapshot), [session]);\n\n // Programmatic only. Interactive leaves the stack unset: `restore()` adopts the persisted one, and\n // failing that the login UI picks it — so there is nothing to configure up front.\n useEffect(() => {\n if (baseUrl === undefined) return;\n if (snapshot.config.baseUrl !== null) return;\n session.setBaseUrl(baseUrl);\n }, [session, baseUrl, snapshot.config.baseUrl]);\n\n // Runs regardless of whether a stack is configured — with none, `restore()` adopts the stack the\n // persisted session belongs to. (Declared after the effect above, so programmatic mode has already\n // configured its stack by the time this runs.) The ref guards StrictMode's double-invoked effect:\n // `restoreStatus` only lands asynchronously, so it cannot prevent a second synchronous call.\n useEffect(() => {\n if (restoreStartedRef.current) return;\n restoreStartedRef.current = true;\n session\n .restore()\n .then((result) => setRestoreStatus(result.status))\n .catch((error) => {\n console.error('Keboola bearer session restore failed', error);\n setRestoreStatus('empty');\n })\n .finally(() => setIsRestoring(false));\n }, [session]);\n\n useEffect(() => {\n if (restoreStatus === null || restoreStatus === 'restored') return;\n if (credential === undefined) return;\n if (snapshot.config.status !== 'anonymous') return;\n session.authenticate(credential);\n }, [session, restoreStatus, credential, snapshot.config.status]);\n\n useEffect(() => {\n if (restoreStatus === null || restoreStatus === 'restored') return;\n if (projectId === undefined) return;\n if (snapshot.config.status !== 'authenticated') return;\n session.setProject(projectId);\n }, [session, restoreStatus, projectId, snapshot.config.status]);\n\n const accessToken =\n snapshot.config.status === 'anonymous' ? undefined : snapshot.config.credential.accessToken;\n\n useEffect(() => {\n if (accessToken !== undefined) onAccessTokenChange?.(accessToken);\n }, [accessToken, onAccessTokenChange]);\n\n // Programmatic mode drives itself to ready, so it must never fall through to `anonymousFallback`.\n const isProgrammatic = credential !== undefined && projectId !== undefined;\n\n const content = (() => {\n if (snapshot.storage === 'ready') {\n if (typeof children !== 'function') return children;\n // A ready storage build implies a scoped session; narrowing here is for the type checker.\n if (snapshot.config.status !== 'scoped') return null;\n const { projectId: scopedProjectId, baseUrl: scopedBaseUrl } = snapshot.config;\n return children({ projectId: scopedProjectId, baseUrl: scopedBaseUrl });\n }\n if (snapshot.storage === 'errored') return errorFallback;\n if (isRestoring || snapshot.storage === 'pending' || isProgrammatic) return loadingFallback;\n return anonymousFallback;\n })();\n\n return <AuthContextProvider value={session}>{content}</AuthContextProvider>;\n};\n","import { useEffect, useRef, useState } from 'react';\nimport type { ReactNode } from 'react';\n\nimport type { LegacySession } from '@keboola/api-client';\n\nimport { LegacyAuthContextProvider } from './context';\n\ntype Props = {\n session: LegacySession;\n /** Stack base URL. */\n baseUrl: string;\n /** SAPI token — drives the storage build to ready. */\n storageToken: string;\n /** Optional manage-api token — unlocks `api.management`. */\n managementToken?: string;\n /** Rendered until the storage build reaches ready. */\n fallback?: ReactNode;\n children: ReactNode;\n};\n\n/**\n * Owns the legacy (SAPI) lifecycle: on mount it configures the stack and drives the session to\n * ready, rendering `fallback` until the storage build resolves. The backend-injected SAPI token is\n * known up front, so this drives itself (unlike the interactive bearer `AuthProvider`). Exposes the\n * session via `useLegacyAuth()` and gates `children` behind readiness.\n */\nexport const LegacyAuthProvider = ({\n session,\n baseUrl,\n storageToken,\n managementToken,\n fallback = null,\n children,\n}: Props) => {\n const [isReady, setIsReady] = useState<boolean>(() => session.isStorageReady());\n // Guards the once-only drive against React StrictMode's double-mounted effect.\n const startedRef = useRef<boolean>(false);\n\n useEffect(() => {\n const unsubscribe = session.subscribe(() => setIsReady(session.isStorageReady()));\n\n if (!startedRef.current) {\n startedRef.current = true;\n session.setBaseUrl(baseUrl);\n if (managementToken) session.authenticateManagement(managementToken);\n // Fire-and-forget: `subscribe` flips `isReady` once the build resolves. A failure leaves the\n // fallback in place; log it so the bootstrap error isn't silent.\n void session\n .authenticateStorageAsync(storageToken)\n .catch((error) => console.error('Keboola session bootstrap failed', error));\n }\n\n return unsubscribe;\n // drive to ready only on initial mount\n // oxlint-disable-next-line react/exhaustive-deps\n }, []);\n\n return (\n <LegacyAuthContextProvider value={session}>\n {isReady ? children : fallback}\n </LegacyAuthContextProvider>\n );\n};\n"]}
1
+ {"version":3,"sources":["../src/context.ts","../src/ApiClientProvider.tsx","../src/AuthProvider.tsx","../src/LegacyAuthProvider.tsx"],"names":["createContext","use","jsx","useState","useRef","useEffect"],"mappings":"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","file":"index.cjs","sourcesContent":["import { createContext, use } from 'react';\n\nimport type { BearerSession, KeboolaApi, LegacySession } from '@keboola/api-client';\n\nconst MISSING_API_CLIENT_ERROR = 'useApiClient must be used within <ApiClientProvider />';\nconst MISSING_PROJECT_ERROR = 'useProject must be used within <ApiClientProvider />';\nconst MISSING_STACK_ERROR = 'useStack must be used within <ApiClientProvider />';\nconst MISSING_AUTH_ERROR = 'useAuth must be used within <AuthProvider />';\nconst MISSING_LEGACY_AUTH_ERROR = 'useLegacyAuth must be used within <LegacyAuthProvider />';\n\n// The product API (`auth` + `management` + every product namespace), flow-agnostic. Both the\n// bearer and legacy flows produce the same `KeboolaApi` surface, so consumers never branch on flow.\nconst ApiContext = createContext<KeboolaApi | null>(null);\nexport const ApiProvider = ApiContext;\n\n// The lifecycle sessions live in their own, flow-specific contexts so the auth surface a component\n// reaches for is explicit. Only the matching provider is mounted per flow.\nconst AuthContext = createContext<BearerSession | null>(null);\nexport const AuthContextProvider = AuthContext;\n\nconst LegacyAuthContext = createContext<LegacySession | null>(null);\nexport const LegacyAuthContextProvider = LegacyAuthContext;\n\nexport const useApiClient = () => {\n const api = use(ApiContext);\n if (api === null) throw new Error(MISSING_API_CLIENT_ERROR);\n return api;\n};\n\nexport const useProject = () => {\n const api = use(ApiContext);\n if (api === null) throw new Error(MISSING_PROJECT_ERROR);\n return api.context.project;\n};\n\nexport const useStack = () => {\n const api = use(ApiContext);\n if (api === null) throw new Error(MISSING_STACK_ERROR);\n return api.context.stack;\n};\n\n/** The bearer (OAuth) lifecycle session. Throws outside `<AuthProvider />`. */\nexport const useAuth = () => {\n const session = use(AuthContext);\n if (session === null) throw new Error(MISSING_AUTH_ERROR);\n return session;\n};\n\n/** The legacy (SAPI) lifecycle session. Throws outside `<LegacyAuthProvider />`. */\nexport const useLegacyAuth = () => {\n const session = use(LegacyAuthContext);\n if (session === null) throw new Error(MISSING_LEGACY_AUTH_ERROR);\n return session;\n};\n","import type { ReactNode } from 'react';\n\nimport type { KeboolaApi } from '@keboola/api-client';\n\nimport { ApiProvider } from './context';\n\ntype Props = {\n /** The flow-agnostic product API — the `api` half of a keboola client. */\n api: KeboolaApi;\n children: ReactNode;\n};\n\n/**\n * Provides the product API to `useApiClient()`. A pure pass-through: the auth lifecycle (driving\n * to ready, gating on a fallback) belongs to `AuthProvider` / `LegacyAuthProvider`, which mount\n * this provider once their session is ready.\n */\nexport const ApiClientProvider = ({ api, children }: Props) => (\n <ApiProvider value={api}>{children}</ApiProvider>\n);\n","import { useEffect, useRef, useState } from 'react';\nimport type { ReactNode } from 'react';\n\nimport type { BearerSession } from '@keboola/api-client';\n\nimport { AuthContextProvider } from './context';\n\ntype BearerCredential = Parameters<BearerSession['authenticate']>[0];\ntype Snapshot = ReturnType<BearerSession['debug']>;\ntype RestoreStatus = Awaited<ReturnType<BearerSession['restore']>>['status'];\n\n/** The session state guaranteed to exist once the storage build is ready. */\ntype ScopedState = { projectId: number; baseUrl: string };\n\ntype BaseProps = {\n session: BearerSession;\n onAccessTokenChange?: (accessToken: string) => void;\n /** Rendered while a persisted session is being restored, or the bootstrap is in flight. */\n loadingFallback?: ReactNode;\n /** Rendered when the storage build fails. */\n errorFallback?: ReactNode;\n /**\n * Rendered only once the session is scoped and the storage build is ready. Pass a function to\n * receive the scoped state (the project the session settled on) without re-narrowing it yourself.\n */\n children: ReactNode | ((scoped: ScopedState) => ReactNode);\n};\n\n/**\n * Two mutually exclusive bootstrap modes, discriminated by whether a credential is supplied:\n * - **programmatic** — `credential` + `projectId` given, so `baseUrl` is **required**: a credential\n * is bound to the stack that issued it and `authenticate` refuses an unconfigured session.\n * - **interactive** — none of the three given; the stack comes from the session instead — `restore()`\n * adopts the persisted one, otherwise the login UI sets it. `anonymousFallback` is **required**\n * here (a missing login UI would leave the app on a permanently blank screen).\n */\ntype Props = BaseProps &\n (\n | {\n credential: BearerCredential;\n projectId: number;\n baseUrl: string;\n anonymousFallback?: never;\n }\n | {\n credential?: undefined;\n projectId?: undefined;\n baseUrl?: never;\n anonymousFallback: ReactNode;\n }\n );\n\nexport const AuthProvider = ({\n session,\n baseUrl,\n credential,\n projectId,\n onAccessTokenChange,\n loadingFallback = null,\n errorFallback = null,\n anonymousFallback = null,\n children,\n}: Props) => {\n const [snapshot, setSnapshot] = useState<Snapshot>(() => session.debug());\n const [isRestoring, setIsRestoring] = useState(() => session.hasPersistedValue());\n const [restoreStatus, setRestoreStatus] = useState<RestoreStatus | null>(null);\n const restoreStartedRef = useRef(false);\n\n useEffect(() => session.subscribe(setSnapshot), [session]);\n\n // Programmatic only. Interactive leaves the stack unset: `restore()` adopts the persisted one, and\n // failing that the login UI picks it — so there is nothing to configure up front.\n useEffect(() => {\n if (baseUrl === undefined) return;\n if (snapshot.config.baseUrl !== null) return;\n session.setBaseUrl(baseUrl);\n }, [session, baseUrl, snapshot.config.baseUrl]);\n\n // Runs regardless of whether a stack is configured — with none, `restore()` adopts the stack the\n // persisted session belongs to. (Declared after the effect above, so programmatic mode has already\n // configured its stack by the time this runs.) The ref guards StrictMode's double-invoked effect:\n // `restoreStatus` only lands asynchronously, so it cannot prevent a second synchronous call.\n useEffect(() => {\n if (restoreStartedRef.current) return;\n restoreStartedRef.current = true;\n session\n .restore()\n .then((result) => setRestoreStatus(result.status))\n .catch((error) => {\n console.error('Keboola bearer session restore failed', error);\n setRestoreStatus('empty');\n })\n .finally(() => setIsRestoring(false));\n }, [session]);\n\n useEffect(() => {\n if (restoreStatus === null || restoreStatus === 'restored') return;\n if (credential === undefined) return;\n if (snapshot.config.status !== 'anonymous') return;\n session.authenticate(credential);\n }, [session, restoreStatus, credential, snapshot.config.status]);\n\n useEffect(() => {\n if (restoreStatus === null || restoreStatus === 'restored') return;\n if (projectId === undefined) return;\n if (snapshot.config.status !== 'authenticated') return;\n session.setProject(projectId);\n }, [session, restoreStatus, projectId, snapshot.config.status]);\n\n const accessToken =\n snapshot.config.status === 'anonymous' ? undefined : snapshot.config.credential.accessToken;\n\n useEffect(() => {\n if (accessToken !== undefined) onAccessTokenChange?.(accessToken);\n }, [accessToken, onAccessTokenChange]);\n\n // Programmatic mode drives itself to ready, so it must never fall through to `anonymousFallback`.\n const isProgrammatic = credential !== undefined && projectId !== undefined;\n\n const content = (() => {\n if (snapshot.storage === 'ready') {\n if (typeof children !== 'function') return children;\n // A ready storage build implies a scoped session; narrowing here is for the type checker.\n if (snapshot.config.status !== 'scoped') return null;\n const { projectId: scopedProjectId, baseUrl: scopedBaseUrl } = snapshot.config;\n return children({ projectId: scopedProjectId, baseUrl: scopedBaseUrl });\n }\n if (snapshot.storage === 'errored') return errorFallback;\n if (isRestoring || snapshot.storage === 'pending' || isProgrammatic) return loadingFallback;\n return anonymousFallback;\n })();\n\n return <AuthContextProvider value={session}>{content}</AuthContextProvider>;\n};\n","import { useEffect, useRef, useState } from 'react';\nimport type { ReactNode } from 'react';\n\nimport type { LegacySession } from '@keboola/api-client';\n\nimport { LegacyAuthContextProvider } from './context';\n\ntype Props = {\n session: LegacySession;\n /** Stack base URL. */\n baseUrl: string;\n /** SAPI token — drives the storage build to ready. */\n storageToken: string;\n /** Optional manage-api token — unlocks `api.management`. */\n managementToken?: string;\n /** Rendered until the storage build reaches ready. */\n fallback?: ReactNode;\n /** Rendered when the storage build fails — e.g. the SAPI token is expired or revoked. */\n errorFallback?: ReactNode;\n children: ReactNode;\n};\n\n/**\n * Owns the legacy (SAPI) lifecycle: on mount it configures the stack and drives the session to\n * ready, rendering `fallback` until the storage build resolves. The backend-injected SAPI token is\n * known up front, so this drives itself (unlike the interactive bearer `AuthProvider`). Exposes the\n * session via `useLegacyAuth()` and gates `children` behind readiness.\n */\nexport const LegacyAuthProvider = ({\n session,\n baseUrl,\n storageToken,\n managementToken,\n fallback = null,\n errorFallback = fallback,\n children,\n}: Props) => {\n // The build state, not just readiness: `errored` and `pending` render differently.\n const [storageState, setStorageState] = useState(() => session.debug().storage);\n // Guards the once-only drive against React StrictMode's double-mounted effect.\n const startedRef = useRef<boolean>(false);\n\n useEffect(() => {\n const unsubscribe = session.subscribe((snapshot) => setStorageState(snapshot.storage));\n\n if (!startedRef.current) {\n startedRef.current = true;\n session.setBaseUrl(baseUrl);\n if (managementToken) session.authenticateManagement(managementToken);\n // Fire-and-forget: `subscribe` moves `storageState` once the build resolves. A failed build\n // resolves rather than rejects, so this catch only covers a synchronous throw.\n void session\n .authenticateStorageAsync(storageToken)\n .catch((error) => console.error('Keboola session bootstrap failed', error));\n }\n\n return unsubscribe;\n // drive to ready only on initial mount\n // oxlint-disable-next-line react/exhaustive-deps\n }, []);\n\n // The only place a failed build is observable — nothing rejects, so without this it is silent.\n useEffect(() => {\n if (storageState === 'errored') {\n console.error('Keboola session bootstrap failed: the storage build errored.');\n }\n }, [storageState]);\n\n const content = (() => {\n if (storageState === 'ready') return children;\n if (storageState === 'errored') return errorFallback;\n return fallback;\n })();\n\n return <LegacyAuthContextProvider value={session}>{content}</LegacyAuthContextProvider>;\n};\n"]}
package/dist/index.d.cts CHANGED
@@ -204,6 +204,8 @@ type Props = {
204
204
  managementToken?: string;
205
205
  /** Rendered until the storage build reaches ready. */
206
206
  fallback?: ReactNode;
207
+ /** Rendered when the storage build fails — e.g. the SAPI token is expired or revoked. */
208
+ errorFallback?: ReactNode;
207
209
  children: ReactNode;
208
210
  };
209
211
  /**
@@ -212,6 +214,6 @@ type Props = {
212
214
  * known up front, so this drives itself (unlike the interactive bearer `AuthProvider`). Exposes the
213
215
  * session via `useLegacyAuth()` and gates `children` behind readiness.
214
216
  */
215
- declare const LegacyAuthProvider: ({ session, baseUrl, storageToken, managementToken, fallback, children, }: Props) => react_jsx_runtime.JSX.Element;
217
+ declare const LegacyAuthProvider: ({ session, baseUrl, storageToken, managementToken, fallback, errorFallback, children, }: Props) => react_jsx_runtime.JSX.Element;
216
218
 
217
219
  export { ApiClientProvider, AuthProvider, LegacyAuthProvider, useApiClient, useAuth, useLegacyAuth, useProject, useStack };
package/dist/index.d.ts CHANGED
@@ -204,6 +204,8 @@ type Props = {
204
204
  managementToken?: string;
205
205
  /** Rendered until the storage build reaches ready. */
206
206
  fallback?: ReactNode;
207
+ /** Rendered when the storage build fails — e.g. the SAPI token is expired or revoked. */
208
+ errorFallback?: ReactNode;
207
209
  children: ReactNode;
208
210
  };
209
211
  /**
@@ -212,6 +214,6 @@ type Props = {
212
214
  * known up front, so this drives itself (unlike the interactive bearer `AuthProvider`). Exposes the
213
215
  * session via `useLegacyAuth()` and gates `children` behind readiness.
214
216
  */
215
- declare const LegacyAuthProvider: ({ session, baseUrl, storageToken, managementToken, fallback, children, }: Props) => react_jsx_runtime.JSX.Element;
217
+ declare const LegacyAuthProvider: ({ session, baseUrl, storageToken, managementToken, fallback, errorFallback, children, }: Props) => react_jsx_runtime.JSX.Element;
216
218
 
217
219
  export { ApiClientProvider, AuthProvider, LegacyAuthProvider, useApiClient, useAuth, useLegacyAuth, useProject, useStack };
package/dist/index.js CHANGED
@@ -104,12 +104,13 @@ var LegacyAuthProvider = ({
104
104
  storageToken,
105
105
  managementToken,
106
106
  fallback = null,
107
+ errorFallback = fallback,
107
108
  children
108
109
  }) => {
109
- const [isReady, setIsReady] = useState(() => session.isStorageReady());
110
+ const [storageState, setStorageState] = useState(() => session.debug().storage);
110
111
  const startedRef = useRef(false);
111
112
  useEffect(() => {
112
- const unsubscribe = session.subscribe(() => setIsReady(session.isStorageReady()));
113
+ const unsubscribe = session.subscribe((snapshot) => setStorageState(snapshot.storage));
113
114
  if (!startedRef.current) {
114
115
  startedRef.current = true;
115
116
  session.setBaseUrl(baseUrl);
@@ -118,7 +119,17 @@ var LegacyAuthProvider = ({
118
119
  }
119
120
  return unsubscribe;
120
121
  }, []);
121
- return /* @__PURE__ */ jsx(LegacyAuthContextProvider, { value: session, children: isReady ? children : fallback });
122
+ useEffect(() => {
123
+ if (storageState === "errored") {
124
+ console.error("Keboola session bootstrap failed: the storage build errored.");
125
+ }
126
+ }, [storageState]);
127
+ const content = (() => {
128
+ if (storageState === "ready") return children;
129
+ if (storageState === "errored") return errorFallback;
130
+ return fallback;
131
+ })();
132
+ return /* @__PURE__ */ jsx(LegacyAuthContextProvider, { value: session, children: content });
122
133
  };
123
134
 
124
135
  export { ApiClientProvider, AuthProvider, LegacyAuthProvider, useApiClient, useAuth, useLegacyAuth, useProject, useStack };
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/context.ts","../src/ApiClientProvider.tsx","../src/AuthProvider.tsx","../src/LegacyAuthProvider.tsx"],"names":["jsx","useState","useRef","useEffect"],"mappings":"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","file":"index.js","sourcesContent":["import { createContext, use } from 'react';\n\nimport type { BearerSession, KeboolaApi, LegacySession } from '@keboola/api-client';\n\nconst MISSING_API_CLIENT_ERROR = 'useApiClient must be used within <ApiClientProvider />';\nconst MISSING_PROJECT_ERROR = 'useProject must be used within <ApiClientProvider />';\nconst MISSING_STACK_ERROR = 'useStack must be used within <ApiClientProvider />';\nconst MISSING_AUTH_ERROR = 'useAuth must be used within <AuthProvider />';\nconst MISSING_LEGACY_AUTH_ERROR = 'useLegacyAuth must be used within <LegacyAuthProvider />';\n\n// The product API (`auth` + `management` + every product namespace), flow-agnostic. Both the\n// bearer and legacy flows produce the same `KeboolaApi` surface, so consumers never branch on flow.\nconst ApiContext = createContext<KeboolaApi | null>(null);\nexport const ApiProvider = ApiContext;\n\n// The lifecycle sessions live in their own, flow-specific contexts so the auth surface a component\n// reaches for is explicit. Only the matching provider is mounted per flow.\nconst AuthContext = createContext<BearerSession | null>(null);\nexport const AuthContextProvider = AuthContext;\n\nconst LegacyAuthContext = createContext<LegacySession | null>(null);\nexport const LegacyAuthContextProvider = LegacyAuthContext;\n\nexport const useApiClient = () => {\n const api = use(ApiContext);\n if (api === null) throw new Error(MISSING_API_CLIENT_ERROR);\n return api;\n};\n\nexport const useProject = () => {\n const api = use(ApiContext);\n if (api === null) throw new Error(MISSING_PROJECT_ERROR);\n return api.context.project;\n};\n\nexport const useStack = () => {\n const api = use(ApiContext);\n if (api === null) throw new Error(MISSING_STACK_ERROR);\n return api.context.stack;\n};\n\n/** The bearer (OAuth) lifecycle session. Throws outside `<AuthProvider />`. */\nexport const useAuth = () => {\n const session = use(AuthContext);\n if (session === null) throw new Error(MISSING_AUTH_ERROR);\n return session;\n};\n\n/** The legacy (SAPI) lifecycle session. Throws outside `<LegacyAuthProvider />`. */\nexport const useLegacyAuth = () => {\n const session = use(LegacyAuthContext);\n if (session === null) throw new Error(MISSING_LEGACY_AUTH_ERROR);\n return session;\n};\n","import type { ReactNode } from 'react';\n\nimport type { KeboolaApi } from '@keboola/api-client';\n\nimport { ApiProvider } from './context';\n\ntype Props = {\n /** The flow-agnostic product API — the `api` half of a keboola client. */\n api: KeboolaApi;\n children: ReactNode;\n};\n\n/**\n * Provides the product API to `useApiClient()`. A pure pass-through: the auth lifecycle (driving\n * to ready, gating on a fallback) belongs to `AuthProvider` / `LegacyAuthProvider`, which mount\n * this provider once their session is ready.\n */\nexport const ApiClientProvider = ({ api, children }: Props) => (\n <ApiProvider value={api}>{children}</ApiProvider>\n);\n","import { useEffect, useRef, useState } from 'react';\nimport type { ReactNode } from 'react';\n\nimport type { BearerSession } from '@keboola/api-client';\n\nimport { AuthContextProvider } from './context';\n\ntype BearerCredential = Parameters<BearerSession['authenticate']>[0];\ntype Snapshot = ReturnType<BearerSession['debug']>;\ntype RestoreStatus = Awaited<ReturnType<BearerSession['restore']>>['status'];\n\n/** The session state guaranteed to exist once the storage build is ready. */\ntype ScopedState = { projectId: number; baseUrl: string };\n\ntype BaseProps = {\n session: BearerSession;\n onAccessTokenChange?: (accessToken: string) => void;\n /** Rendered while a persisted session is being restored, or the bootstrap is in flight. */\n loadingFallback?: ReactNode;\n /** Rendered when the storage build fails. */\n errorFallback?: ReactNode;\n /**\n * Rendered only once the session is scoped and the storage build is ready. Pass a function to\n * receive the scoped state (the project the session settled on) without re-narrowing it yourself.\n */\n children: ReactNode | ((scoped: ScopedState) => ReactNode);\n};\n\n/**\n * Two mutually exclusive bootstrap modes, discriminated by whether a credential is supplied:\n * - **programmatic** — `credential` + `projectId` given, so `baseUrl` is **required**: a credential\n * is bound to the stack that issued it and `authenticate` refuses an unconfigured session.\n * - **interactive** — none of the three given; the stack comes from the session instead — `restore()`\n * adopts the persisted one, otherwise the login UI sets it. `anonymousFallback` is **required**\n * here (a missing login UI would leave the app on a permanently blank screen).\n */\ntype Props = BaseProps &\n (\n | {\n credential: BearerCredential;\n projectId: number;\n baseUrl: string;\n anonymousFallback?: never;\n }\n | {\n credential?: undefined;\n projectId?: undefined;\n baseUrl?: never;\n anonymousFallback: ReactNode;\n }\n );\n\nexport const AuthProvider = ({\n session,\n baseUrl,\n credential,\n projectId,\n onAccessTokenChange,\n loadingFallback = null,\n errorFallback = null,\n anonymousFallback = null,\n children,\n}: Props) => {\n const [snapshot, setSnapshot] = useState<Snapshot>(() => session.debug());\n const [isRestoring, setIsRestoring] = useState(() => session.hasPersistedValue());\n const [restoreStatus, setRestoreStatus] = useState<RestoreStatus | null>(null);\n const restoreStartedRef = useRef(false);\n\n useEffect(() => session.subscribe(setSnapshot), [session]);\n\n // Programmatic only. Interactive leaves the stack unset: `restore()` adopts the persisted one, and\n // failing that the login UI picks it — so there is nothing to configure up front.\n useEffect(() => {\n if (baseUrl === undefined) return;\n if (snapshot.config.baseUrl !== null) return;\n session.setBaseUrl(baseUrl);\n }, [session, baseUrl, snapshot.config.baseUrl]);\n\n // Runs regardless of whether a stack is configured — with none, `restore()` adopts the stack the\n // persisted session belongs to. (Declared after the effect above, so programmatic mode has already\n // configured its stack by the time this runs.) The ref guards StrictMode's double-invoked effect:\n // `restoreStatus` only lands asynchronously, so it cannot prevent a second synchronous call.\n useEffect(() => {\n if (restoreStartedRef.current) return;\n restoreStartedRef.current = true;\n session\n .restore()\n .then((result) => setRestoreStatus(result.status))\n .catch((error) => {\n console.error('Keboola bearer session restore failed', error);\n setRestoreStatus('empty');\n })\n .finally(() => setIsRestoring(false));\n }, [session]);\n\n useEffect(() => {\n if (restoreStatus === null || restoreStatus === 'restored') return;\n if (credential === undefined) return;\n if (snapshot.config.status !== 'anonymous') return;\n session.authenticate(credential);\n }, [session, restoreStatus, credential, snapshot.config.status]);\n\n useEffect(() => {\n if (restoreStatus === null || restoreStatus === 'restored') return;\n if (projectId === undefined) return;\n if (snapshot.config.status !== 'authenticated') return;\n session.setProject(projectId);\n }, [session, restoreStatus, projectId, snapshot.config.status]);\n\n const accessToken =\n snapshot.config.status === 'anonymous' ? undefined : snapshot.config.credential.accessToken;\n\n useEffect(() => {\n if (accessToken !== undefined) onAccessTokenChange?.(accessToken);\n }, [accessToken, onAccessTokenChange]);\n\n // Programmatic mode drives itself to ready, so it must never fall through to `anonymousFallback`.\n const isProgrammatic = credential !== undefined && projectId !== undefined;\n\n const content = (() => {\n if (snapshot.storage === 'ready') {\n if (typeof children !== 'function') return children;\n // A ready storage build implies a scoped session; narrowing here is for the type checker.\n if (snapshot.config.status !== 'scoped') return null;\n const { projectId: scopedProjectId, baseUrl: scopedBaseUrl } = snapshot.config;\n return children({ projectId: scopedProjectId, baseUrl: scopedBaseUrl });\n }\n if (snapshot.storage === 'errored') return errorFallback;\n if (isRestoring || snapshot.storage === 'pending' || isProgrammatic) return loadingFallback;\n return anonymousFallback;\n })();\n\n return <AuthContextProvider value={session}>{content}</AuthContextProvider>;\n};\n","import { useEffect, useRef, useState } from 'react';\nimport type { ReactNode } from 'react';\n\nimport type { LegacySession } from '@keboola/api-client';\n\nimport { LegacyAuthContextProvider } from './context';\n\ntype Props = {\n session: LegacySession;\n /** Stack base URL. */\n baseUrl: string;\n /** SAPI token — drives the storage build to ready. */\n storageToken: string;\n /** Optional manage-api token — unlocks `api.management`. */\n managementToken?: string;\n /** Rendered until the storage build reaches ready. */\n fallback?: ReactNode;\n children: ReactNode;\n};\n\n/**\n * Owns the legacy (SAPI) lifecycle: on mount it configures the stack and drives the session to\n * ready, rendering `fallback` until the storage build resolves. The backend-injected SAPI token is\n * known up front, so this drives itself (unlike the interactive bearer `AuthProvider`). Exposes the\n * session via `useLegacyAuth()` and gates `children` behind readiness.\n */\nexport const LegacyAuthProvider = ({\n session,\n baseUrl,\n storageToken,\n managementToken,\n fallback = null,\n children,\n}: Props) => {\n const [isReady, setIsReady] = useState<boolean>(() => session.isStorageReady());\n // Guards the once-only drive against React StrictMode's double-mounted effect.\n const startedRef = useRef<boolean>(false);\n\n useEffect(() => {\n const unsubscribe = session.subscribe(() => setIsReady(session.isStorageReady()));\n\n if (!startedRef.current) {\n startedRef.current = true;\n session.setBaseUrl(baseUrl);\n if (managementToken) session.authenticateManagement(managementToken);\n // Fire-and-forget: `subscribe` flips `isReady` once the build resolves. A failure leaves the\n // fallback in place; log it so the bootstrap error isn't silent.\n void session\n .authenticateStorageAsync(storageToken)\n .catch((error) => console.error('Keboola session bootstrap failed', error));\n }\n\n return unsubscribe;\n // drive to ready only on initial mount\n // oxlint-disable-next-line react/exhaustive-deps\n }, []);\n\n return (\n <LegacyAuthContextProvider value={session}>\n {isReady ? children : fallback}\n </LegacyAuthContextProvider>\n );\n};\n"]}
1
+ {"version":3,"sources":["../src/context.ts","../src/ApiClientProvider.tsx","../src/AuthProvider.tsx","../src/LegacyAuthProvider.tsx"],"names":["jsx","useState","useRef","useEffect"],"mappings":"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","file":"index.js","sourcesContent":["import { createContext, use } from 'react';\n\nimport type { BearerSession, KeboolaApi, LegacySession } from '@keboola/api-client';\n\nconst MISSING_API_CLIENT_ERROR = 'useApiClient must be used within <ApiClientProvider />';\nconst MISSING_PROJECT_ERROR = 'useProject must be used within <ApiClientProvider />';\nconst MISSING_STACK_ERROR = 'useStack must be used within <ApiClientProvider />';\nconst MISSING_AUTH_ERROR = 'useAuth must be used within <AuthProvider />';\nconst MISSING_LEGACY_AUTH_ERROR = 'useLegacyAuth must be used within <LegacyAuthProvider />';\n\n// The product API (`auth` + `management` + every product namespace), flow-agnostic. Both the\n// bearer and legacy flows produce the same `KeboolaApi` surface, so consumers never branch on flow.\nconst ApiContext = createContext<KeboolaApi | null>(null);\nexport const ApiProvider = ApiContext;\n\n// The lifecycle sessions live in their own, flow-specific contexts so the auth surface a component\n// reaches for is explicit. Only the matching provider is mounted per flow.\nconst AuthContext = createContext<BearerSession | null>(null);\nexport const AuthContextProvider = AuthContext;\n\nconst LegacyAuthContext = createContext<LegacySession | null>(null);\nexport const LegacyAuthContextProvider = LegacyAuthContext;\n\nexport const useApiClient = () => {\n const api = use(ApiContext);\n if (api === null) throw new Error(MISSING_API_CLIENT_ERROR);\n return api;\n};\n\nexport const useProject = () => {\n const api = use(ApiContext);\n if (api === null) throw new Error(MISSING_PROJECT_ERROR);\n return api.context.project;\n};\n\nexport const useStack = () => {\n const api = use(ApiContext);\n if (api === null) throw new Error(MISSING_STACK_ERROR);\n return api.context.stack;\n};\n\n/** The bearer (OAuth) lifecycle session. Throws outside `<AuthProvider />`. */\nexport const useAuth = () => {\n const session = use(AuthContext);\n if (session === null) throw new Error(MISSING_AUTH_ERROR);\n return session;\n};\n\n/** The legacy (SAPI) lifecycle session. Throws outside `<LegacyAuthProvider />`. */\nexport const useLegacyAuth = () => {\n const session = use(LegacyAuthContext);\n if (session === null) throw new Error(MISSING_LEGACY_AUTH_ERROR);\n return session;\n};\n","import type { ReactNode } from 'react';\n\nimport type { KeboolaApi } from '@keboola/api-client';\n\nimport { ApiProvider } from './context';\n\ntype Props = {\n /** The flow-agnostic product API — the `api` half of a keboola client. */\n api: KeboolaApi;\n children: ReactNode;\n};\n\n/**\n * Provides the product API to `useApiClient()`. A pure pass-through: the auth lifecycle (driving\n * to ready, gating on a fallback) belongs to `AuthProvider` / `LegacyAuthProvider`, which mount\n * this provider once their session is ready.\n */\nexport const ApiClientProvider = ({ api, children }: Props) => (\n <ApiProvider value={api}>{children}</ApiProvider>\n);\n","import { useEffect, useRef, useState } from 'react';\nimport type { ReactNode } from 'react';\n\nimport type { BearerSession } from '@keboola/api-client';\n\nimport { AuthContextProvider } from './context';\n\ntype BearerCredential = Parameters<BearerSession['authenticate']>[0];\ntype Snapshot = ReturnType<BearerSession['debug']>;\ntype RestoreStatus = Awaited<ReturnType<BearerSession['restore']>>['status'];\n\n/** The session state guaranteed to exist once the storage build is ready. */\ntype ScopedState = { projectId: number; baseUrl: string };\n\ntype BaseProps = {\n session: BearerSession;\n onAccessTokenChange?: (accessToken: string) => void;\n /** Rendered while a persisted session is being restored, or the bootstrap is in flight. */\n loadingFallback?: ReactNode;\n /** Rendered when the storage build fails. */\n errorFallback?: ReactNode;\n /**\n * Rendered only once the session is scoped and the storage build is ready. Pass a function to\n * receive the scoped state (the project the session settled on) without re-narrowing it yourself.\n */\n children: ReactNode | ((scoped: ScopedState) => ReactNode);\n};\n\n/**\n * Two mutually exclusive bootstrap modes, discriminated by whether a credential is supplied:\n * - **programmatic** — `credential` + `projectId` given, so `baseUrl` is **required**: a credential\n * is bound to the stack that issued it and `authenticate` refuses an unconfigured session.\n * - **interactive** — none of the three given; the stack comes from the session instead — `restore()`\n * adopts the persisted one, otherwise the login UI sets it. `anonymousFallback` is **required**\n * here (a missing login UI would leave the app on a permanently blank screen).\n */\ntype Props = BaseProps &\n (\n | {\n credential: BearerCredential;\n projectId: number;\n baseUrl: string;\n anonymousFallback?: never;\n }\n | {\n credential?: undefined;\n projectId?: undefined;\n baseUrl?: never;\n anonymousFallback: ReactNode;\n }\n );\n\nexport const AuthProvider = ({\n session,\n baseUrl,\n credential,\n projectId,\n onAccessTokenChange,\n loadingFallback = null,\n errorFallback = null,\n anonymousFallback = null,\n children,\n}: Props) => {\n const [snapshot, setSnapshot] = useState<Snapshot>(() => session.debug());\n const [isRestoring, setIsRestoring] = useState(() => session.hasPersistedValue());\n const [restoreStatus, setRestoreStatus] = useState<RestoreStatus | null>(null);\n const restoreStartedRef = useRef(false);\n\n useEffect(() => session.subscribe(setSnapshot), [session]);\n\n // Programmatic only. Interactive leaves the stack unset: `restore()` adopts the persisted one, and\n // failing that the login UI picks it — so there is nothing to configure up front.\n useEffect(() => {\n if (baseUrl === undefined) return;\n if (snapshot.config.baseUrl !== null) return;\n session.setBaseUrl(baseUrl);\n }, [session, baseUrl, snapshot.config.baseUrl]);\n\n // Runs regardless of whether a stack is configured — with none, `restore()` adopts the stack the\n // persisted session belongs to. (Declared after the effect above, so programmatic mode has already\n // configured its stack by the time this runs.) The ref guards StrictMode's double-invoked effect:\n // `restoreStatus` only lands asynchronously, so it cannot prevent a second synchronous call.\n useEffect(() => {\n if (restoreStartedRef.current) return;\n restoreStartedRef.current = true;\n session\n .restore()\n .then((result) => setRestoreStatus(result.status))\n .catch((error) => {\n console.error('Keboola bearer session restore failed', error);\n setRestoreStatus('empty');\n })\n .finally(() => setIsRestoring(false));\n }, [session]);\n\n useEffect(() => {\n if (restoreStatus === null || restoreStatus === 'restored') return;\n if (credential === undefined) return;\n if (snapshot.config.status !== 'anonymous') return;\n session.authenticate(credential);\n }, [session, restoreStatus, credential, snapshot.config.status]);\n\n useEffect(() => {\n if (restoreStatus === null || restoreStatus === 'restored') return;\n if (projectId === undefined) return;\n if (snapshot.config.status !== 'authenticated') return;\n session.setProject(projectId);\n }, [session, restoreStatus, projectId, snapshot.config.status]);\n\n const accessToken =\n snapshot.config.status === 'anonymous' ? undefined : snapshot.config.credential.accessToken;\n\n useEffect(() => {\n if (accessToken !== undefined) onAccessTokenChange?.(accessToken);\n }, [accessToken, onAccessTokenChange]);\n\n // Programmatic mode drives itself to ready, so it must never fall through to `anonymousFallback`.\n const isProgrammatic = credential !== undefined && projectId !== undefined;\n\n const content = (() => {\n if (snapshot.storage === 'ready') {\n if (typeof children !== 'function') return children;\n // A ready storage build implies a scoped session; narrowing here is for the type checker.\n if (snapshot.config.status !== 'scoped') return null;\n const { projectId: scopedProjectId, baseUrl: scopedBaseUrl } = snapshot.config;\n return children({ projectId: scopedProjectId, baseUrl: scopedBaseUrl });\n }\n if (snapshot.storage === 'errored') return errorFallback;\n if (isRestoring || snapshot.storage === 'pending' || isProgrammatic) return loadingFallback;\n return anonymousFallback;\n })();\n\n return <AuthContextProvider value={session}>{content}</AuthContextProvider>;\n};\n","import { useEffect, useRef, useState } from 'react';\nimport type { ReactNode } from 'react';\n\nimport type { LegacySession } from '@keboola/api-client';\n\nimport { LegacyAuthContextProvider } from './context';\n\ntype Props = {\n session: LegacySession;\n /** Stack base URL. */\n baseUrl: string;\n /** SAPI token — drives the storage build to ready. */\n storageToken: string;\n /** Optional manage-api token — unlocks `api.management`. */\n managementToken?: string;\n /** Rendered until the storage build reaches ready. */\n fallback?: ReactNode;\n /** Rendered when the storage build fails — e.g. the SAPI token is expired or revoked. */\n errorFallback?: ReactNode;\n children: ReactNode;\n};\n\n/**\n * Owns the legacy (SAPI) lifecycle: on mount it configures the stack and drives the session to\n * ready, rendering `fallback` until the storage build resolves. The backend-injected SAPI token is\n * known up front, so this drives itself (unlike the interactive bearer `AuthProvider`). Exposes the\n * session via `useLegacyAuth()` and gates `children` behind readiness.\n */\nexport const LegacyAuthProvider = ({\n session,\n baseUrl,\n storageToken,\n managementToken,\n fallback = null,\n errorFallback = fallback,\n children,\n}: Props) => {\n // The build state, not just readiness: `errored` and `pending` render differently.\n const [storageState, setStorageState] = useState(() => session.debug().storage);\n // Guards the once-only drive against React StrictMode's double-mounted effect.\n const startedRef = useRef<boolean>(false);\n\n useEffect(() => {\n const unsubscribe = session.subscribe((snapshot) => setStorageState(snapshot.storage));\n\n if (!startedRef.current) {\n startedRef.current = true;\n session.setBaseUrl(baseUrl);\n if (managementToken) session.authenticateManagement(managementToken);\n // Fire-and-forget: `subscribe` moves `storageState` once the build resolves. A failed build\n // resolves rather than rejects, so this catch only covers a synchronous throw.\n void session\n .authenticateStorageAsync(storageToken)\n .catch((error) => console.error('Keboola session bootstrap failed', error));\n }\n\n return unsubscribe;\n // drive to ready only on initial mount\n // oxlint-disable-next-line react/exhaustive-deps\n }, []);\n\n // The only place a failed build is observable — nothing rejects, so without this it is silent.\n useEffect(() => {\n if (storageState === 'errored') {\n console.error('Keboola session bootstrap failed: the storage build errored.');\n }\n }, [storageState]);\n\n const content = (() => {\n if (storageState === 'ready') return children;\n if (storageState === 'errored') return errorFallback;\n return fallback;\n })();\n\n return <LegacyAuthContextProvider value={session}>{content}</LegacyAuthContextProvider>;\n};\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@keboola/api-client-react",
3
- "version": "25.0.0",
3
+ "version": "27.0.0",
4
4
  "description": "React bindings for @keboola/api-client",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -9,7 +9,8 @@
9
9
  "types": "./dist/index.d.ts",
10
10
  "files": [
11
11
  "dist",
12
- "README.md"
12
+ "README.md",
13
+ "AGENTS.md"
13
14
  ],
14
15
  "publishConfig": {
15
16
  "access": "public"
@@ -49,7 +50,7 @@
49
50
  },
50
51
  "peerDependencies": {
51
52
  "react": "^19.0.0",
52
- "@keboola/api-client": "^25.0.0"
53
+ "@keboola/api-client": "^27.0.0"
53
54
  },
54
55
  "devDependencies": {
55
56
  "@arethetypeswrong/cli": "^0.18.2",
@@ -71,8 +72,8 @@
71
72
  "typescript": "npm:@typescript/typescript6@^6.0.2",
72
73
  "vite": "^8.1.5",
73
74
  "vitest": "^4.1.10",
74
- "@keboola/api-client": "^25.0.0",
75
- "@keboola/oxlint-config": "^8.1.0",
75
+ "@keboola/api-client": "^27.0.0",
76
+ "@keboola/oxlint-config": "^10.0.1",
76
77
  "@keboola/tsconfig": "^0.2.0"
77
78
  },
78
79
  "scripts": {