@atlasauth/react 0.1.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.
@@ -0,0 +1,59 @@
1
+ import { type ReactNode } from 'react';
2
+ import { type ProviderToken } from '@atlasauth/js';
3
+ import { type Appearance } from './appearance';
4
+ import { type Catalog } from './i18n';
5
+ import type { AtlasOrganizationMembership, AtlasSession, AtlasUser, SessionClaims } from './types';
6
+ /**
7
+ * §10.1 `<AtlasProvider publishableKey>`.
8
+ *
9
+ * Holds exactly one thing that matters: the token, in memory. Not
10
+ * localStorage, not a cookie this code writes — the refresh token is HttpOnly
11
+ * so browser script never touches it, and the short-lived JWT lives only as
12
+ * long as the tab. A provider that persisted either would survive the tab and
13
+ * become the thing an XSS reads first.
14
+ */
15
+ export type AuthStatus = 'loading' | 'signed_in' | 'signed_out';
16
+ interface AtlasContextValue {
17
+ publishableKey: string;
18
+ frontendApi: string;
19
+ status: AuthStatus;
20
+ user: AtlasUser | null;
21
+ session: AtlasSession | null;
22
+ claims: SessionClaims | null;
23
+ memberships: AtlasOrganizationMembership[];
24
+ appearance?: Appearance;
25
+ catalog: Catalog;
26
+ /**
27
+ * §5.3: a sign-in attempt handed back by an OAuth redirect that still needs a
28
+ * second factor (no session was issued). Null in the ordinary case. The
29
+ * <SignIn> flow seeds itself from this so the user resumes at the 2FA step
30
+ * instead of starting over.
31
+ */
32
+ pendingAttempt: {
33
+ id: string;
34
+ status: string;
35
+ } | null;
36
+ getToken(): Promise<string | null>;
37
+ /**
38
+ * §6.6 the signed-in user's OWN live token at a connected provider (Google,
39
+ * GitHub, …), so the app can call that provider's API. Null when there is no
40
+ * usable token — no linked account, or the provider granted / kept none. The
41
+ * server refreshes a stale token on read; the refresh token is never exposed.
42
+ */
43
+ getProviderToken(provider: string): Promise<ProviderToken | null>;
44
+ signOut(): Promise<void>;
45
+ setActiveOrganization(organizationId: string | null): Promise<void>;
46
+ reload(): Promise<void>;
47
+ }
48
+ export interface AtlasProviderProps {
49
+ publishableKey: string;
50
+ /** The instance's FAPI origin. */
51
+ frontendApi?: string;
52
+ appearance?: Appearance;
53
+ localization?: Catalog;
54
+ children: ReactNode;
55
+ fetchImpl?: typeof fetch;
56
+ }
57
+ export declare function AtlasProvider(props: AtlasProviderProps): import("react").JSX.Element;
58
+ export declare function useAtlas(): AtlasContextValue;
59
+ export {};
@@ -0,0 +1,248 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AtlasProvider = AtlasProvider;
4
+ exports.useAtlas = useAtlas;
5
+ const jsx_runtime_1 = require("react/jsx-runtime");
6
+ const react_1 = require("react");
7
+ const js_1 = require("@atlasauth/js");
8
+ const appearance_1 = require("./appearance");
9
+ const i18n_1 = require("./i18n");
10
+ const AtlasContext = (0, react_1.createContext)(null);
11
+ function AtlasProvider(props) {
12
+ const { publishableKey, children, appearance, localization } = props;
13
+ const baseUrl = props.frontendApi ?? '';
14
+ const doFetch = props.fetchImpl ?? fetch;
15
+ const [status, setStatus] = (0, react_1.useState)('loading');
16
+ const [user, setUser] = (0, react_1.useState)(null);
17
+ const [session, setSession] = (0, react_1.useState)(null);
18
+ const [claims, setClaims] = (0, react_1.useState)(null);
19
+ const [memberships, setMemberships] = (0, react_1.useState)([]);
20
+ const cache = (0, react_1.useRef)(new js_1.TokenCache());
21
+ const timer = (0, react_1.useRef)(null);
22
+ /**
23
+ * Captured synchronously during the first render (not in an effect) so it is
24
+ * already in context before <SignIn> reads it — a needs_second_factor redirect
25
+ * carries a status but no ticket, and that is what the 2FA resume seeds from.
26
+ */
27
+ const [pendingAttempt] = (0, react_1.useState)(() => {
28
+ if (typeof window === 'undefined')
29
+ return null;
30
+ const result = (0, js_1.readRedirectResult)(window.location.search);
31
+ return result?.status && !result.ticket
32
+ ? { id: result.attemptId, status: result.status }
33
+ : null;
34
+ });
35
+ const call = (0, react_1.useCallback)(async (path, init) => {
36
+ return doFetch(`${baseUrl}${path}`, {
37
+ ...init,
38
+ // Cookies carry the session; a fetch without this looks identical to
39
+ // being signed out.
40
+ credentials: 'include',
41
+ headers: {
42
+ 'x-publishable-key': publishableKey,
43
+ ...(init?.body ? { 'content-type': 'application/json' } : {}),
44
+ ...init?.headers,
45
+ },
46
+ });
47
+ }, [baseUrl, doFetch, publishableKey]);
48
+ const applyToken = (0, react_1.useCallback)((jwt, sessionId) => {
49
+ const expiresAt = (0, js_1.readExpiry)(jwt);
50
+ if (!expiresAt)
51
+ return;
52
+ cache.current.set({ jwt, expiresAt, sessionId });
53
+ setClaims(decodeClaims(jwt));
54
+ }, []);
55
+ const reload = (0, react_1.useCallback)(async () => {
56
+ try {
57
+ const response = await call('/v1/client');
58
+ if (!response.ok)
59
+ throw new Error('boot failed');
60
+ const body = (await response.json());
61
+ if (!body.session || !body.user) {
62
+ cache.current.clear();
63
+ setStatus('signed_out');
64
+ setUser(null);
65
+ setSession(null);
66
+ setClaims(null);
67
+ setMemberships([]);
68
+ return;
69
+ }
70
+ setUser(body.user);
71
+ setSession(body.session);
72
+ setMemberships(body.organization_memberships ?? []);
73
+ if (body.jwt)
74
+ applyToken(body.jwt, body.session.id);
75
+ setStatus('signed_in');
76
+ }
77
+ catch {
78
+ /**
79
+ * A failed boot is NOT signed-out. Rendering a sign-in form because the
80
+ * network hiccuped throws away a perfectly good session and makes the
81
+ * user re-authenticate for no reason.
82
+ */
83
+ setStatus((current) => (current === 'loading' ? 'signed_out' : current));
84
+ }
85
+ }, [applyToken, call]);
86
+ const refresh = (0, react_1.useCallback)(async () => {
87
+ const current = cache.current.peek();
88
+ if (!current)
89
+ return;
90
+ try {
91
+ const response = await call(`/v1/client/sessions/${current.sessionId}/tokens`, {
92
+ method: 'POST',
93
+ });
94
+ if (!response.ok)
95
+ throw new Error('refresh failed');
96
+ const body = (await response.json());
97
+ applyToken(body.jwt, body.session_id);
98
+ }
99
+ catch {
100
+ // The session may genuinely be gone. Re-boot rather than guess.
101
+ await reload();
102
+ }
103
+ }, [applyToken, call, reload]);
104
+ /** §7.4: proactive, jittered refresh scheduled from the token's own expiry. */
105
+ (0, react_1.useEffect)(() => {
106
+ if (status !== 'signed_in')
107
+ return;
108
+ const token = cache.current.peek();
109
+ if (!token)
110
+ return;
111
+ const delay = (0, js_1.msUntilRefresh)({ token, now: Date.now() });
112
+ timer.current = setTimeout(() => void refresh(), delay);
113
+ return () => {
114
+ if (timer.current)
115
+ clearTimeout(timer.current);
116
+ };
117
+ }, [status, claims, refresh]);
118
+ /**
119
+ * §6.3 step 3: complete a redirect-based sign-in. An OAuth or hosted-page flow
120
+ * lands back here with a one-time ticket in the URL; exchange it for cookies,
121
+ * scrub the URL so a reload can't replay it, THEN boot. A redirect that only
122
+ * carries a `__atlas_status` (a second factor is still owed) has no ticket, so
123
+ * nothing is exchanged and the app boots signed-out — exactly as intended.
124
+ */
125
+ (0, react_1.useEffect)(() => {
126
+ const result = typeof window !== 'undefined' ? (0, js_1.readRedirectResult)(window.location.search) : null;
127
+ void (async () => {
128
+ if (result?.ticket) {
129
+ await call('/v1/client/tickets/exchange', {
130
+ method: 'POST',
131
+ body: JSON.stringify({ attempt_id: result.attemptId, ticket: result.ticket }),
132
+ }).catch(() => undefined);
133
+ }
134
+ if (result && typeof window !== 'undefined') {
135
+ window.history.replaceState({}, document.title, (0, js_1.stripRedirectParams)(window.location.href));
136
+ }
137
+ await reload();
138
+ })();
139
+ // Runs once on mount (empty deps): the redirect is handled exactly once, and
140
+ // re-running would only re-read an already-scrubbed URL. `reload`/`call` are
141
+ // stable across renders.
142
+ }, []);
143
+ const getToken = (0, react_1.useCallback)(async () => {
144
+ // A 5s skew so a token that would die inside the caller's request is
145
+ // refreshed first rather than handed over.
146
+ const usable = cache.current.get(5_000);
147
+ if (usable)
148
+ return usable.jwt;
149
+ await refresh();
150
+ return cache.current.get(5_000)?.jwt ?? null;
151
+ }, [refresh]);
152
+ /**
153
+ * The FAPI client for the token-vault read. Reuses the exact same
154
+ * credentials-and-publishable-key request `call` makes, so the browser sends
155
+ * one consistent shape and the framework-agnostic `@atlasauth/js` helper is the
156
+ * single implementation the React SDK surfaces.
157
+ */
158
+ const fapiClient = (0, react_1.useMemo)(() => new js_1.FapiClient({ publishableKey, baseUrl, fetchImpl: doFetch }), [publishableKey, baseUrl, doFetch]);
159
+ const getProviderToken = (0, react_1.useCallback)((provider) => (0, js_1.getProviderToken)(fapiClient, provider), [fapiClient]);
160
+ const signOut = (0, react_1.useCallback)(async () => {
161
+ const current = cache.current.peek();
162
+ if (current) {
163
+ await call(`/v1/client/sessions/${current.sessionId}/revoke`, { method: 'POST' }).catch(() => undefined);
164
+ }
165
+ cache.current.clear();
166
+ setStatus('signed_out');
167
+ setUser(null);
168
+ setSession(null);
169
+ setClaims(null);
170
+ setMemberships([]);
171
+ }, [call]);
172
+ const setActiveOrganization = (0, react_1.useCallback)(async (organizationId) => {
173
+ const current = cache.current.peek();
174
+ if (!current)
175
+ return;
176
+ const response = await call(`/v1/client/sessions/${current.sessionId}/touch`, {
177
+ method: 'POST',
178
+ body: JSON.stringify({ active_organization_id: organizationId }),
179
+ });
180
+ if (!response.ok)
181
+ return;
182
+ const body = (await response.json());
183
+ // The new token carries the new org claims; re-boot would work too but
184
+ // costs a round trip for information already in hand.
185
+ applyToken(body.jwt, current.sessionId);
186
+ await reload();
187
+ }, [applyToken, call, reload]);
188
+ const catalog = (0, react_1.useMemo)(() => (localization ? (0, i18n_1.withFallback)(localization) : i18n_1.EN_US), [localization]);
189
+ const value = (0, react_1.useMemo)(() => ({
190
+ publishableKey,
191
+ frontendApi: baseUrl,
192
+ status,
193
+ user,
194
+ session,
195
+ claims,
196
+ memberships,
197
+ appearance,
198
+ catalog,
199
+ pendingAttempt,
200
+ getToken,
201
+ getProviderToken,
202
+ signOut,
203
+ setActiveOrganization,
204
+ reload,
205
+ }), [
206
+ appearance,
207
+ baseUrl,
208
+ publishableKey,
209
+ catalog,
210
+ claims,
211
+ getToken,
212
+ getProviderToken,
213
+ memberships,
214
+ pendingAttempt,
215
+ reload,
216
+ session,
217
+ setActiveOrganization,
218
+ signOut,
219
+ status,
220
+ user,
221
+ ]);
222
+ const style = (0, react_1.useMemo)(() => (0, appearance_1.cssVariables)((0, appearance_1.resolveTokens)(appearance)), [appearance]);
223
+ return ((0, jsx_runtime_1.jsx)(AtlasContext.Provider, { value: value, children: (0, jsx_runtime_1.jsx)("div", { className: "atlas-root", style: style, children: children }) }));
224
+ }
225
+ function useAtlas() {
226
+ const value = (0, react_1.useContext)(AtlasContext);
227
+ if (!value) {
228
+ /**
229
+ * A thrown error rather than a null-ish default. A hook returning
230
+ * `{ user: null }` outside the provider is indistinguishable from a
231
+ * signed-out user, and the developer spends an afternoon on it.
232
+ */
233
+ throw new Error('Atlas hooks must be used inside <AtlasProvider>.');
234
+ }
235
+ return value;
236
+ }
237
+ function decodeClaims(jwt) {
238
+ const parts = jwt.split('.');
239
+ if (parts.length !== 3)
240
+ return null;
241
+ try {
242
+ return JSON.parse(atob(parts[1].replace(/-/g, '+').replace(/_/g, '/')));
243
+ }
244
+ catch {
245
+ return null;
246
+ }
247
+ }
248
+ //# sourceMappingURL=AtlasProvider.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AtlasProvider.js","sourceRoot":"","sources":["../src/AtlasProvider.tsx"],"names":[],"mappings":"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"}
@@ -0,0 +1,85 @@
1
+ /**
2
+ * §10.3 theming.
3
+ *
4
+ * "appearance prop: design tokens (colors, radius, font, spacing) + per-element
5
+ * class overrides (elements: { formButtonPrimary: "…" }). No iframe — components
6
+ * render in the host DOM so Tailwind/CSS just works."
7
+ *
8
+ * Rendering in the host DOM is the decision everything else follows from. An
9
+ * iframe would isolate styles perfectly and be unusable: it cannot be sized to
10
+ * its content reliably, it breaks password managers, it breaks autofill, and a
11
+ * Tailwind class the customer writes has no effect inside it. Sharing the DOM
12
+ * means the customer's CSS reaches our markup, which is exactly what they want
13
+ * and what makes an override system necessary rather than decorative.
14
+ *
15
+ * Element classes are APPENDED to ours, never replacing them. A customer adding
16
+ * `className="rounded-xl"` wants a rounder button, not an unstyled one — and
17
+ * replacement is a support burden where every override starts by rebuilding the
18
+ * component's base styles from scratch.
19
+ */
20
+ export interface DesignTokens {
21
+ colorPrimary?: string;
22
+ colorText?: string;
23
+ colorBackground?: string;
24
+ colorDanger?: string;
25
+ colorBorder?: string;
26
+ borderRadius?: string;
27
+ fontFamily?: string;
28
+ spacingUnit?: string;
29
+ }
30
+ /** Every element a customer may target. Named for what it IS, not where it sits. */
31
+ export type ElementKey = 'root' | 'card' | 'headerTitle' | 'headerSubtitle' | 'formField' | 'formFieldLabel' | 'formFieldInput' | 'formFieldError' | 'formButtonPrimary' | 'formButtonSecondary' | 'socialButton' | 'dividerText' | 'footerAction' | 'avatar' | 'menu' | 'menuItem' | 'badge';
32
+ export interface Appearance {
33
+ /** A prebuilt theme to start from. */
34
+ baseTheme?: keyof typeof THEMES;
35
+ variables?: DesignTokens;
36
+ elements?: Partial<Record<ElementKey, string>>;
37
+ }
38
+ export declare const DEFAULT_TOKENS: Required<DesignTokens>;
39
+ /**
40
+ * §10.3: "prebuilt themes shipped (default, dark, neobrutalist as a demo of the
41
+ * override system)". Neobrutalist exists to prove the token set is expressive
42
+ * enough for a theme that looks nothing like the default — a theming system
43
+ * that only produces tasteful variations of itself is not a theming system.
44
+ */
45
+ export declare const THEMES: {
46
+ readonly default: Required<DesignTokens>;
47
+ readonly dark: {
48
+ readonly colorText: "#e6e9ef";
49
+ readonly colorBackground: "#14171c";
50
+ readonly colorBorder: "#242932";
51
+ readonly colorPrimary: "#6ea8fe";
52
+ readonly colorDanger: string;
53
+ readonly borderRadius: string;
54
+ readonly fontFamily: string;
55
+ readonly spacingUnit: string;
56
+ };
57
+ readonly neobrutalist: {
58
+ readonly colorPrimary: "#ffe600";
59
+ readonly colorText: "#000000";
60
+ readonly colorBackground: "#ffffff";
61
+ readonly colorBorder: "#000000";
62
+ readonly borderRadius: "0px";
63
+ readonly fontFamily: "\"Courier New\", ui-monospace, monospace";
64
+ readonly colorDanger: string;
65
+ readonly spacingUnit: string;
66
+ };
67
+ };
68
+ /**
69
+ * Resolve tokens: defaults, then the base theme, then explicit variables.
70
+ *
71
+ * Later wins, and `undefined` never overwrites. A customer setting one variable
72
+ * on the dark theme expects the other eleven to stay dark, not silently revert
73
+ * to the light defaults.
74
+ */
75
+ export declare function resolveTokens(appearance?: Appearance): Required<DesignTokens>;
76
+ /** Tokens as CSS custom properties, for the host DOM to inherit. */
77
+ export declare function cssVariables(tokens: Required<DesignTokens>): Record<string, string>;
78
+ /**
79
+ * Combine our base class with the customer's override.
80
+ *
81
+ * Appended, never replaced — a customer adding `rounded-xl` wants a rounder
82
+ * button, not an unstyled one. Theirs comes last so it wins on equal
83
+ * specificity, which is the behaviour anyone writing Tailwind expects.
84
+ */
85
+ export declare function classFor(element: ElementKey, appearance: Appearance | undefined, base: string): string;
@@ -0,0 +1,98 @@
1
+ "use strict";
2
+ /**
3
+ * §10.3 theming.
4
+ *
5
+ * "appearance prop: design tokens (colors, radius, font, spacing) + per-element
6
+ * class overrides (elements: { formButtonPrimary: "…" }). No iframe — components
7
+ * render in the host DOM so Tailwind/CSS just works."
8
+ *
9
+ * Rendering in the host DOM is the decision everything else follows from. An
10
+ * iframe would isolate styles perfectly and be unusable: it cannot be sized to
11
+ * its content reliably, it breaks password managers, it breaks autofill, and a
12
+ * Tailwind class the customer writes has no effect inside it. Sharing the DOM
13
+ * means the customer's CSS reaches our markup, which is exactly what they want
14
+ * and what makes an override system necessary rather than decorative.
15
+ *
16
+ * Element classes are APPENDED to ours, never replacing them. A customer adding
17
+ * `className="rounded-xl"` wants a rounder button, not an unstyled one — and
18
+ * replacement is a support burden where every override starts by rebuilding the
19
+ * component's base styles from scratch.
20
+ */
21
+ Object.defineProperty(exports, "__esModule", { value: true });
22
+ exports.THEMES = exports.DEFAULT_TOKENS = void 0;
23
+ exports.resolveTokens = resolveTokens;
24
+ exports.cssVariables = cssVariables;
25
+ exports.classFor = classFor;
26
+ exports.DEFAULT_TOKENS = {
27
+ colorPrimary: '#5b5bd6',
28
+ colorText: '#1c1c1f',
29
+ colorBackground: '#ffffff',
30
+ colorDanger: '#d64545',
31
+ colorBorder: '#e4e4e9',
32
+ borderRadius: '8px',
33
+ fontFamily: 'ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif',
34
+ spacingUnit: '4px',
35
+ };
36
+ /**
37
+ * §10.3: "prebuilt themes shipped (default, dark, neobrutalist as a demo of the
38
+ * override system)". Neobrutalist exists to prove the token set is expressive
39
+ * enough for a theme that looks nothing like the default — a theming system
40
+ * that only produces tasteful variations of itself is not a theming system.
41
+ */
42
+ exports.THEMES = {
43
+ default: exports.DEFAULT_TOKENS,
44
+ dark: {
45
+ ...exports.DEFAULT_TOKENS,
46
+ colorText: '#e6e9ef',
47
+ colorBackground: '#14171c',
48
+ colorBorder: '#242932',
49
+ colorPrimary: '#6ea8fe',
50
+ },
51
+ neobrutalist: {
52
+ ...exports.DEFAULT_TOKENS,
53
+ colorPrimary: '#ffe600',
54
+ colorText: '#000000',
55
+ colorBackground: '#ffffff',
56
+ colorBorder: '#000000',
57
+ borderRadius: '0px',
58
+ fontFamily: '"Courier New", ui-monospace, monospace',
59
+ },
60
+ };
61
+ /**
62
+ * Resolve tokens: defaults, then the base theme, then explicit variables.
63
+ *
64
+ * Later wins, and `undefined` never overwrites. A customer setting one variable
65
+ * on the dark theme expects the other eleven to stay dark, not silently revert
66
+ * to the light defaults.
67
+ */
68
+ function resolveTokens(appearance) {
69
+ const base = appearance?.baseTheme ? exports.THEMES[appearance.baseTheme] : exports.DEFAULT_TOKENS;
70
+ const overrides = appearance?.variables ?? {};
71
+ const resolved = { ...exports.DEFAULT_TOKENS, ...base };
72
+ for (const [key, value] of Object.entries(overrides)) {
73
+ if (value !== undefined)
74
+ resolved[key] = value;
75
+ }
76
+ return resolved;
77
+ }
78
+ /** Tokens as CSS custom properties, for the host DOM to inherit. */
79
+ function cssVariables(tokens) {
80
+ const vars = {};
81
+ for (const [key, value] of Object.entries(tokens)) {
82
+ // camelCase → --atlas-kebab-case, which is what a customer writes in CSS.
83
+ vars[`--atlas-${key.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`)}`] = value;
84
+ }
85
+ return vars;
86
+ }
87
+ /**
88
+ * Combine our base class with the customer's override.
89
+ *
90
+ * Appended, never replaced — a customer adding `rounded-xl` wants a rounder
91
+ * button, not an unstyled one. Theirs comes last so it wins on equal
92
+ * specificity, which is the behaviour anyone writing Tailwind expects.
93
+ */
94
+ function classFor(element, appearance, base) {
95
+ const override = appearance?.elements?.[element];
96
+ return override ? `${base} ${override}` : base;
97
+ }
98
+ //# sourceMappingURL=appearance.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"appearance.js","sourceRoot":"","sources":["../src/appearance.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;GAkBG;;;AAoFH,sCASC;AAGD,oCAOC;AASD,4BAOC;AA/EY,QAAA,cAAc,GAA2B;IACpD,YAAY,EAAE,SAAS;IACvB,SAAS,EAAE,SAAS;IACpB,eAAe,EAAE,SAAS;IAC1B,WAAW,EAAE,SAAS;IACtB,WAAW,EAAE,SAAS;IACtB,YAAY,EAAE,KAAK;IACnB,UAAU,EAAE,iEAAiE;IAC7E,WAAW,EAAE,KAAK;CACnB,CAAC;AAEF;;;;;GAKG;AACU,QAAA,MAAM,GAAG;IACpB,OAAO,EAAE,sBAAc;IACvB,IAAI,EAAE;QACJ,GAAG,sBAAc;QACjB,SAAS,EAAE,SAAS;QACpB,eAAe,EAAE,SAAS;QAC1B,WAAW,EAAE,SAAS;QACtB,YAAY,EAAE,SAAS;KACxB;IACD,YAAY,EAAE;QACZ,GAAG,sBAAc;QACjB,YAAY,EAAE,SAAS;QACvB,SAAS,EAAE,SAAS;QACpB,eAAe,EAAE,SAAS;QAC1B,WAAW,EAAE,SAAS;QACtB,YAAY,EAAE,KAAK;QACnB,UAAU,EAAE,wCAAwC;KACrD;CACwD,CAAC;AAE5D;;;;;;GAMG;AACH,SAAgB,aAAa,CAAC,UAAuB;IACnD,MAAM,IAAI,GAAG,UAAU,EAAE,SAAS,CAAC,CAAC,CAAC,cAAM,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,sBAAc,CAAC;IACnF,MAAM,SAAS,GAAG,UAAU,EAAE,SAAS,IAAI,EAAE,CAAC;IAE9C,MAAM,QAAQ,GAAG,EAAE,GAAG,sBAAc,EAAE,GAAG,IAAI,EAAE,CAAC;IAChD,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,EAAE,CAAC;QACrD,IAAI,KAAK,KAAK,SAAS;YAAE,QAAQ,CAAC,GAAyB,CAAC,GAAG,KAAK,CAAC;IACvE,CAAC;IACD,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED,oEAAoE;AACpE,SAAgB,YAAY,CAAC,MAA8B;IACzD,MAAM,IAAI,GAA2B,EAAE,CAAC;IACxC,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;QAClD,0EAA0E;QAC1E,IAAI,CAAC,WAAW,GAAG,CAAC,OAAO,CAAC,QAAQ,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,CAAC,WAAW,EAAE,EAAE,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC;IACjF,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;;;GAMG;AACH,SAAgB,QAAQ,CACtB,OAAmB,EACnB,UAAkC,EAClC,IAAY;IAEZ,MAAM,QAAQ,GAAG,UAAU,EAAE,QAAQ,EAAE,CAAC,OAAO,CAAC,CAAC;IACjD,OAAO,QAAQ,CAAC,CAAC,CAAC,GAAG,IAAI,IAAI,QAAQ,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;AACjD,CAAC"}
@@ -0,0 +1,40 @@
1
+ import { type ReactNode } from 'react';
2
+ import { type ProtectCondition } from './protect';
3
+ export declare function SignedIn({ children }: {
4
+ children: ReactNode;
5
+ }): import("react").JSX.Element | null;
6
+ export declare function SignedOut({ children }: {
7
+ children: ReactNode;
8
+ }): import("react").JSX.Element | null;
9
+ /** §10.2: render children only while the SDK is still loading. */
10
+ export declare function AtlasLoading({ children }: {
11
+ children: ReactNode;
12
+ }): import("react").JSX.Element | null;
13
+ /** §10.2: render children only once the SDK has finished loading — the
14
+ * symmetric partner of {@link AtlasLoading}, so a consumer can show a spinner
15
+ * and its resolved content without hand-rolling the inverse condition. */
16
+ export declare function AtlasLoaded({ children }: {
17
+ children: ReactNode;
18
+ }): import("react").JSX.Element | null;
19
+ /**
20
+ * §10.2 `<Protect permission="…">`.
21
+ *
22
+ * A rendering helper, never an authorization boundary — anyone can flip the
23
+ * condition in devtools. The server checking the same permission is what
24
+ * actually stops them, and this component's job is only to avoid showing a
25
+ * button that would fail.
26
+ */
27
+ export declare function Protect({ children, fallback, ...condition }: ProtectCondition & {
28
+ children: ReactNode;
29
+ fallback?: ReactNode;
30
+ }): import("react").JSX.Element | null;
31
+ export interface FlowProps {
32
+ /** Where to send the user once the flow completes. */
33
+ afterUrl?: string;
34
+ }
35
+ export declare function SignIn(_props?: FlowProps): import("react").JSX.Element;
36
+ export declare function SignUp(_props?: FlowProps): import("react").JSX.Element;
37
+ export declare function UserButton(): import("react").JSX.Element | null;
38
+ export declare function OrganizationSwitcher(): import("react").JSX.Element;
39
+ export declare function UserProfile(): import("react").JSX.Element | null;
40
+ export declare function OrganizationProfile(): import("react").JSX.Element | null;