@atlasauth/svelte 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.
- package/LICENSE +21 -0
- package/README.md +231 -0
- package/dist/accessors.d.ts +81 -0
- package/dist/accessors.js +126 -0
- package/dist/appearance.d.ts +85 -0
- package/dist/appearance.js +91 -0
- package/dist/components/AtlasLoaded.svelte +7 -0
- package/dist/components/AtlasLoaded.svelte.d.ts +18 -0
- package/dist/components/AtlasLoading.svelte +7 -0
- package/dist/components/AtlasLoading.svelte.d.ts +18 -0
- package/dist/components/AtlasProvider.svelte +23 -0
- package/dist/components/AtlasProvider.svelte.d.ts +33 -0
- package/dist/components/AttemptFlow.svelte +95 -0
- package/dist/components/AttemptFlow.svelte.d.ts +30 -0
- package/dist/components/OrganizationProfile.svelte +22 -0
- package/dist/components/OrganizationProfile.svelte.d.ts +16 -0
- package/dist/components/OrganizationSwitcher.svelte +25 -0
- package/dist/components/OrganizationSwitcher.svelte.d.ts +16 -0
- package/dist/components/Protect.svelte +21 -0
- package/dist/components/Protect.svelte.d.ts +32 -0
- package/dist/components/SignIn.svelte +16 -0
- package/dist/components/SignIn.svelte.d.ts +22 -0
- package/dist/components/SignUp.svelte +16 -0
- package/dist/components/SignUp.svelte.d.ts +21 -0
- package/dist/components/SignedIn.svelte +7 -0
- package/dist/components/SignedIn.svelte.d.ts +18 -0
- package/dist/components/SignedOut.svelte +7 -0
- package/dist/components/SignedOut.svelte.d.ts +18 -0
- package/dist/components/UserButton.svelte +40 -0
- package/dist/components/UserButton.svelte.d.ts +16 -0
- package/dist/components/UserProfile.svelte +35 -0
- package/dist/components/UserProfile.svelte.d.ts +16 -0
- package/dist/context.d.ts +105 -0
- package/dist/context.js +258 -0
- package/dist/fapi.d.ts +11 -0
- package/dist/fapi.js +11 -0
- package/dist/i18n.d.ts +23 -0
- package/dist/i18n.js +77 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.js +35 -0
- package/dist/protect.d.ts +18 -0
- package/dist/protect.js +18 -0
- package/dist/server.d.ts +101 -0
- package/dist/server.js +87 -0
- package/dist/types.d.ts +42 -0
- package/dist/types.js +1 -0
- package/package.json +57 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Atlas
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
# @atlas/svelte
|
|
2
|
+
|
|
3
|
+
The official **Svelte / SvelteKit** SDK for Atlas — the Svelte peer of
|
|
4
|
+
[`@atlas/react`](../react). Same surface, expressed the Svelte way: a client
|
|
5
|
+
installed through **context**, reactive auth state exposed as **stores**, and
|
|
6
|
+
`.svelte` **components** for the common flows.
|
|
7
|
+
|
|
8
|
+
It wraps [`@atlas/js`](../js), the framework-agnostic browser client, exactly as
|
|
9
|
+
the React SDK does — so both frameworks drive the same FAPI endpoints through one
|
|
10
|
+
implementation and cannot diverge in what the server sees.
|
|
11
|
+
|
|
12
|
+
- Works with Svelte **4 and 5** (uses `export let` / stores; runes are optional).
|
|
13
|
+
- The refresh token is HttpOnly and never touched by this code; only the
|
|
14
|
+
short-lived JWT lives in memory, only as long as the tab.
|
|
15
|
+
|
|
16
|
+
> Install & build note: this package builds with `svelte-package` and tests with
|
|
17
|
+
> Vitest + `@testing-library/svelte` under the toolchain declared in
|
|
18
|
+
> `package.json`. It is wired for the pnpm workspace (`workspace:*` deps) and has
|
|
19
|
+
> not been `pnpm install`ed in this checkout.
|
|
20
|
+
|
|
21
|
+
## Install
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
pnpm add @atlas/svelte
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Peer dependencies: `svelte >= 4`. `@sveltejs/kit` is an **optional** peer, needed
|
|
28
|
+
only for the server helper.
|
|
29
|
+
|
|
30
|
+
## Quick start
|
|
31
|
+
|
|
32
|
+
### 1. Install the client — `initAtlas` or `<AtlasProvider>`
|
|
33
|
+
|
|
34
|
+
`initAtlas` puts the Atlas client into Svelte context. Call it once, high in the
|
|
35
|
+
tree — typically from your root layout, or use the `<AtlasProvider>` convenience
|
|
36
|
+
component which also paints the appearance tokens onto a host-DOM wrapper.
|
|
37
|
+
|
|
38
|
+
```svelte
|
|
39
|
+
<!-- src/routes/+layout.svelte -->
|
|
40
|
+
<script lang="ts">
|
|
41
|
+
import { AtlasProvider } from '@atlas/svelte';
|
|
42
|
+
</script>
|
|
43
|
+
|
|
44
|
+
<AtlasProvider publishableKey="pk_live_…" frontendApi="https://<instance>.atlas.dev">
|
|
45
|
+
<slot />
|
|
46
|
+
</AtlasProvider>
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Or imperatively, from any component's `<script>`:
|
|
50
|
+
|
|
51
|
+
```svelte
|
|
52
|
+
<script lang="ts">
|
|
53
|
+
import { initAtlas } from '@atlas/svelte';
|
|
54
|
+
const atlas = initAtlas({ publishableKey: 'pk_live_…' });
|
|
55
|
+
</script>
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
`initAtlas` boots automatically in the browser (handles an OAuth/hosted-page
|
|
59
|
+
redirect, then loads `/v1/client`) and is a no-op during SSR.
|
|
60
|
+
|
|
61
|
+
### 2. Read state — stores & accessors
|
|
62
|
+
|
|
63
|
+
Every accessor returns a **store**, so `$`-prefix it in a template and it stays
|
|
64
|
+
live. They mirror the React hooks one-to-one:
|
|
65
|
+
|
|
66
|
+
| React hook | Svelte accessor | Returns a store of … |
|
|
67
|
+
| ----------------- | -------------------- | ------------------------------------------------------ |
|
|
68
|
+
| `useUser()` | `getUser()` | `{ isLoaded, isSignedIn, user, getProviderToken }` |
|
|
69
|
+
| `useSession()` | `getSession()` | `{ isLoaded, session }` |
|
|
70
|
+
| `useAuth()` | `getAuth()` | `{ isLoaded, isSignedIn, userId, sessionId, orgId, orgRole, getToken, getProviderToken, signOut, has }` |
|
|
71
|
+
| `useOrganization()` | `getOrganization()` | `{ isLoaded, organization, membership, memberships, setActive }` |
|
|
72
|
+
| `useAtlas()` | `useAtlas()` | the client itself (raw stores + actions) |
|
|
73
|
+
|
|
74
|
+
```svelte
|
|
75
|
+
<script lang="ts">
|
|
76
|
+
import { getUser, getAuth } from '@atlas/svelte';
|
|
77
|
+
const user = getUser();
|
|
78
|
+
const auth = getAuth();
|
|
79
|
+
|
|
80
|
+
async function callApi() {
|
|
81
|
+
const token = await $auth.getToken(); // fresh JWT, refreshed if stale
|
|
82
|
+
// fetch('/api/thing', { headers: { authorization: `Bearer ${token}` } })
|
|
83
|
+
}
|
|
84
|
+
</script>
|
|
85
|
+
|
|
86
|
+
{#if $user.isLoaded}
|
|
87
|
+
<p>Hello {$user.user?.first_name ?? 'stranger'}</p>
|
|
88
|
+
{/if}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
The raw stores are also on the client: `useAtlas().user`, `.session`,
|
|
92
|
+
`.status`, `.claims`, `.memberships`, `.isSignedIn`, `.isLoaded`.
|
|
93
|
+
|
|
94
|
+
**Action helpers** (import and call from a component `<script>`): `getToken()`,
|
|
95
|
+
`signOut()`, and `signIn()` / `signUp()` (which return a flow controller —
|
|
96
|
+
`{ state, submit, destroy }` — the same driver `<SignIn>` uses).
|
|
97
|
+
|
|
98
|
+
### 3. Components
|
|
99
|
+
|
|
100
|
+
Mirror `@atlas/react`'s `components.tsx`:
|
|
101
|
+
|
|
102
|
+
| Component | Purpose |
|
|
103
|
+
| ----------------------- | ------------------------------------------------------------- |
|
|
104
|
+
| `<SignedIn>` | renders its slot only when signed in |
|
|
105
|
+
| `<SignedOut>` | renders its slot only when signed out |
|
|
106
|
+
| `<AtlasLoading>` | slot only while the SDK is still booting |
|
|
107
|
+
| `<AtlasLoaded>` | slot only once booted (symmetric partner of `<AtlasLoading>`) |
|
|
108
|
+
| `<Protect>` | slot when the auth condition holds; `fallback` slot otherwise |
|
|
109
|
+
| `<SignIn>` / `<SignUp>` | full server-driven, multi-step flows |
|
|
110
|
+
| `<UserButton>` | avatar + account menu |
|
|
111
|
+
| `<UserProfile>` | account-management card |
|
|
112
|
+
| `<OrganizationSwitcher>`| switch the active organization |
|
|
113
|
+
| `<OrganizationProfile>` | active organization's card |
|
|
114
|
+
|
|
115
|
+
```svelte
|
|
116
|
+
<script lang="ts">
|
|
117
|
+
import { SignedIn, SignedOut, SignIn, UserButton, Protect } from '@atlas/svelte';
|
|
118
|
+
</script>
|
|
119
|
+
|
|
120
|
+
<SignedOut><SignIn /></SignedOut>
|
|
121
|
+
<SignedIn><UserButton /></SignedIn>
|
|
122
|
+
|
|
123
|
+
<Protect permission="org:sys_memberships:manage">
|
|
124
|
+
<button>Invite member</button>
|
|
125
|
+
<p slot="fallback">You don't have access.</p>
|
|
126
|
+
</Protect>
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
> `<Protect>` is a **rendering** helper, never an authorization boundary —
|
|
130
|
+
> anyone can flip the condition in devtools. The server checking the same
|
|
131
|
+
> permission is what actually stops them.
|
|
132
|
+
|
|
133
|
+
### Theming & i18n
|
|
134
|
+
|
|
135
|
+
Same as `@atlas/react`: pass `appearance` (design tokens + per-element class
|
|
136
|
+
overrides — components render in the host DOM, no iframe, so Tailwind/CSS just
|
|
137
|
+
work) and `localization` (a flat i18n catalog, merged over `en-US`) to
|
|
138
|
+
`initAtlas` / `<AtlasProvider>`.
|
|
139
|
+
|
|
140
|
+
```svelte
|
|
141
|
+
<AtlasProvider
|
|
142
|
+
publishableKey="pk_live_…"
|
|
143
|
+
appearance={{ baseTheme: 'dark', elements: { formButtonPrimary: 'rounded-xl' } }}
|
|
144
|
+
>
|
|
145
|
+
<slot />
|
|
146
|
+
</AtlasProvider>
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## SvelteKit SSR
|
|
150
|
+
|
|
151
|
+
The server helper lives at the separate `@atlas/svelte/server` entry (it depends
|
|
152
|
+
on `@atlas/backend` and must never enter a browser bundle). It verifies the
|
|
153
|
+
session JWT **locally at the edge** — the JWKS is cached in-process, so a warm
|
|
154
|
+
server verifies with no network round-trip.
|
|
155
|
+
|
|
156
|
+
### `hooks.server.ts`
|
|
157
|
+
|
|
158
|
+
```ts
|
|
159
|
+
import { handleAtlas } from '@atlas/svelte/server';
|
|
160
|
+
|
|
161
|
+
export const handle = handleAtlas({
|
|
162
|
+
jwksUrl: 'https://<instance>.atlas.dev/.well-known/jwks.json',
|
|
163
|
+
issuer: 'https://<instance>.atlas.dev',
|
|
164
|
+
// authorizedParties: ['https://app.example.com'], // optional azp allowlist
|
|
165
|
+
});
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
This resolves the session once per request and stashes it on
|
|
169
|
+
`event.locals.auth`.
|
|
170
|
+
|
|
171
|
+
### In a `load` or endpoint
|
|
172
|
+
|
|
173
|
+
```ts
|
|
174
|
+
// +layout.server.ts
|
|
175
|
+
import { getServerAuth } from '@atlas/svelte/server';
|
|
176
|
+
|
|
177
|
+
export const load = (event) => {
|
|
178
|
+
const auth = getServerAuth(event);
|
|
179
|
+
return { userId: auth.userId, isSignedIn: auth.isSignedIn };
|
|
180
|
+
};
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
```ts
|
|
184
|
+
// a protected +page.server.ts
|
|
185
|
+
import { getServerAuth } from '@atlas/svelte/server';
|
|
186
|
+
import { error } from '@sveltejs/kit';
|
|
187
|
+
|
|
188
|
+
export const load = (event) => {
|
|
189
|
+
const auth = getServerAuth(event);
|
|
190
|
+
try {
|
|
191
|
+
auth.protect({ permission: 'org:reports:read' }); // throws ForbiddenError if unmet
|
|
192
|
+
} catch {
|
|
193
|
+
throw error(403, 'Forbidden');
|
|
194
|
+
}
|
|
195
|
+
return { ok: true };
|
|
196
|
+
};
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
`getServerAuth(event).has(condition)` answers a condition without throwing;
|
|
200
|
+
`.protect(condition)` asserts it (throwing `ForbiddenError`). Both use the shared
|
|
201
|
+
`@atlas/authz` primitive — the same one `<Protect>` evaluates in the browser — so
|
|
202
|
+
a check reads identically on the client, here, and in the React/Next SDKs.
|
|
203
|
+
|
|
204
|
+
For strong typing of `event.locals.auth`, add to `src/app.d.ts`:
|
|
205
|
+
|
|
206
|
+
```ts
|
|
207
|
+
import type { ServerAuth } from '@atlas/svelte/server';
|
|
208
|
+
declare global {
|
|
209
|
+
namespace App {
|
|
210
|
+
interface Locals {
|
|
211
|
+
auth: ServerAuth;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
export {};
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
## Development
|
|
219
|
+
|
|
220
|
+
```bash
|
|
221
|
+
pnpm --filter @atlas/svelte build # svelte-package → dist/
|
|
222
|
+
pnpm --filter @atlas/svelte typecheck # svelte-check
|
|
223
|
+
pnpm --filter @atlas/svelte test # vitest (jsdom + @testing-library/svelte)
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
Tests cover the context init, the reactive stores, and `<SignedIn>`/`<SignedOut>`
|
|
227
|
+
(with `@atlas/js` mocked).
|
|
228
|
+
|
|
229
|
+
## License
|
|
230
|
+
|
|
231
|
+
MIT — see [LICENSE](./LICENSE).
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { type Readable } from 'svelte/store';
|
|
2
|
+
import { type FlowState, type ProviderToken } from '@atlasauth/js';
|
|
3
|
+
import { type ProtectCondition } from './protect';
|
|
4
|
+
import type { AtlasOrganizationMembership, AtlasSession, AtlasUser } from './types';
|
|
5
|
+
/**
|
|
6
|
+
* §10.1 accessors — the Svelte peer of React's hooks (`useUser`, `useSession`,
|
|
7
|
+
* `useAuth`, `useOrganization`).
|
|
8
|
+
*
|
|
9
|
+
* React returns a plain snapshot that re-runs on every render. Svelte has no
|
|
10
|
+
* render loop, so each accessor returns a **derived store** instead: `$user`
|
|
11
|
+
* in a template updates exactly when the underlying state changes, and
|
|
12
|
+
* `get(user)` reads it once in plain code.
|
|
13
|
+
*
|
|
14
|
+
* Every one carries `isLoaded`, and that is not ceremony. Without it a component
|
|
15
|
+
* cannot tell "still booting" from "signed out", so it flashes a sign-in form
|
|
16
|
+
* for a fraction of a second on every page load.
|
|
17
|
+
*/
|
|
18
|
+
export interface UseUser {
|
|
19
|
+
isLoaded: boolean;
|
|
20
|
+
isSignedIn: boolean;
|
|
21
|
+
user: AtlasUser | null;
|
|
22
|
+
/**
|
|
23
|
+
* §6.6 the signed-in user's own live token at a connected provider — the
|
|
24
|
+
* `user.getToken({ provider })` equivalent. Always the caller's own token,
|
|
25
|
+
* refreshed on read if stale, never the refresh token.
|
|
26
|
+
*/
|
|
27
|
+
getProviderToken(provider: string): Promise<ProviderToken | null>;
|
|
28
|
+
}
|
|
29
|
+
export declare function getUser(): Readable<UseUser>;
|
|
30
|
+
export interface UseSession {
|
|
31
|
+
isLoaded: boolean;
|
|
32
|
+
session: AtlasSession | null;
|
|
33
|
+
}
|
|
34
|
+
export declare function getSession(): Readable<UseSession>;
|
|
35
|
+
export interface UseAuth {
|
|
36
|
+
isLoaded: boolean;
|
|
37
|
+
isSignedIn: boolean;
|
|
38
|
+
userId: string | null;
|
|
39
|
+
sessionId: string | null;
|
|
40
|
+
orgId: string | null;
|
|
41
|
+
orgRole: string | null;
|
|
42
|
+
/** Always fresh: refreshes first if the cached token is close to expiry. */
|
|
43
|
+
getToken(): Promise<string | null>;
|
|
44
|
+
/** §6.6 the user's own live access token at a connected social provider. */
|
|
45
|
+
getProviderToken(provider: string): Promise<ProviderToken | null>;
|
|
46
|
+
signOut(): Promise<void>;
|
|
47
|
+
/** Mirrors `<Protect>`, for logic that is not a render decision. */
|
|
48
|
+
has(condition: ProtectCondition): boolean;
|
|
49
|
+
}
|
|
50
|
+
export declare function getAuth(): Readable<UseAuth>;
|
|
51
|
+
export interface UseOrganization {
|
|
52
|
+
isLoaded: boolean;
|
|
53
|
+
organization: AtlasOrganizationMembership['organization'] | null;
|
|
54
|
+
membership: AtlasOrganizationMembership | null;
|
|
55
|
+
memberships: AtlasOrganizationMembership[];
|
|
56
|
+
setActive(organizationId: string | null): Promise<void>;
|
|
57
|
+
}
|
|
58
|
+
export declare function getOrganization(): Readable<UseOrganization>;
|
|
59
|
+
/** Refresh-if-stale, then hand over the JWT. The `useAuth().getToken` shortcut. */
|
|
60
|
+
export declare function getToken(): Promise<string | null>;
|
|
61
|
+
/** Revoke the session and drop all in-memory state. */
|
|
62
|
+
export declare function signOut(): Promise<void>;
|
|
63
|
+
/**
|
|
64
|
+
* A running sign-in / sign-up attempt: a reactive `state` store plus `submit`.
|
|
65
|
+
*
|
|
66
|
+
* This is the imperative peer of React's `useFlow`. The whole flow is
|
|
67
|
+
* server-driven — `advance` (from `@atlasauth/js`) owns the step-to-endpoint
|
|
68
|
+
* mapping, so this controller never decides what comes next. It posts whatever
|
|
69
|
+
* the current step needs, renders the status the server returned, and on
|
|
70
|
+
* completion exchanges the one-time ticket for cookies then re-boots.
|
|
71
|
+
*/
|
|
72
|
+
export interface FlowController {
|
|
73
|
+
state: Readable<FlowState>;
|
|
74
|
+
submit(values: Record<string, string>): void;
|
|
75
|
+
/** Stop the magic-link poll loop — call from `onDestroy`. */
|
|
76
|
+
destroy(): void;
|
|
77
|
+
}
|
|
78
|
+
/** Start (or resume) a sign-in flow. Drives `<SignIn>`; usable standalone too. */
|
|
79
|
+
export declare function signIn(): FlowController;
|
|
80
|
+
/** Start a sign-up flow. Drives `<SignUp>`; usable standalone too. */
|
|
81
|
+
export declare function signUp(): FlowController;
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { derived, get, writable } from 'svelte/store';
|
|
2
|
+
import { FapiClient, advance, flowFromPending, pollAttempt, shouldKeepPolling, } from '@atlasauth/js';
|
|
3
|
+
import { useAtlas } from './context';
|
|
4
|
+
import { evaluate } from './protect';
|
|
5
|
+
export function getUser() {
|
|
6
|
+
const atlas = useAtlas();
|
|
7
|
+
return derived([atlas.status, atlas.user], ([$status, $user]) => ({
|
|
8
|
+
isLoaded: $status !== 'loading',
|
|
9
|
+
isSignedIn: $status === 'signed_in',
|
|
10
|
+
user: $user,
|
|
11
|
+
getProviderToken: atlas.getProviderToken,
|
|
12
|
+
}));
|
|
13
|
+
}
|
|
14
|
+
export function getSession() {
|
|
15
|
+
const atlas = useAtlas();
|
|
16
|
+
return derived([atlas.status, atlas.session], ([$status, $session]) => ({
|
|
17
|
+
isLoaded: $status !== 'loading',
|
|
18
|
+
session: $session,
|
|
19
|
+
}));
|
|
20
|
+
}
|
|
21
|
+
export function getAuth() {
|
|
22
|
+
const atlas = useAtlas();
|
|
23
|
+
return derived([atlas.status, atlas.claims], ([$status, $claims]) => ({
|
|
24
|
+
isLoaded: $status !== 'loading',
|
|
25
|
+
isSignedIn: $status === 'signed_in',
|
|
26
|
+
userId: $claims?.sub ?? null,
|
|
27
|
+
sessionId: $claims?.sid ?? null,
|
|
28
|
+
orgId: $claims?.org_id ?? null,
|
|
29
|
+
orgRole: $claims?.org_role ?? null,
|
|
30
|
+
getToken: atlas.getToken,
|
|
31
|
+
getProviderToken: atlas.getProviderToken,
|
|
32
|
+
signOut: atlas.signOut,
|
|
33
|
+
has: (condition) => evaluate($claims, condition).allowed,
|
|
34
|
+
}));
|
|
35
|
+
}
|
|
36
|
+
export function getOrganization() {
|
|
37
|
+
const atlas = useAtlas();
|
|
38
|
+
return derived([atlas.status, atlas.claims, atlas.memberships], ([$status, $claims, $memberships]) => {
|
|
39
|
+
const active = $memberships.find((m) => m.organization.id === $claims?.org_id) ?? null;
|
|
40
|
+
return {
|
|
41
|
+
isLoaded: $status !== 'loading',
|
|
42
|
+
organization: active?.organization ?? null,
|
|
43
|
+
membership: active,
|
|
44
|
+
memberships: $memberships,
|
|
45
|
+
setActive: atlas.setActiveOrganization,
|
|
46
|
+
};
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
/* ------------------------------------------------------------------ actions */
|
|
50
|
+
/** Refresh-if-stale, then hand over the JWT. The `useAuth().getToken` shortcut. */
|
|
51
|
+
export function getToken() {
|
|
52
|
+
return useAtlas().getToken();
|
|
53
|
+
}
|
|
54
|
+
/** Revoke the session and drop all in-memory state. */
|
|
55
|
+
export function signOut() {
|
|
56
|
+
return useAtlas().signOut();
|
|
57
|
+
}
|
|
58
|
+
function createFlow() {
|
|
59
|
+
const atlas = useAtlas();
|
|
60
|
+
const client = new FapiClient({
|
|
61
|
+
publishableKey: atlas.publishableKey,
|
|
62
|
+
baseUrl: atlas.frontendApi,
|
|
63
|
+
});
|
|
64
|
+
// Seeded from a redirect that came back mid-flow (e.g. OAuth needing a second
|
|
65
|
+
// factor), so the user resumes at the demanded step instead of starting over.
|
|
66
|
+
const state = writable(flowFromPending(atlas.pendingAttempt));
|
|
67
|
+
let pollTimer = null;
|
|
68
|
+
function stopPolling() {
|
|
69
|
+
if (pollTimer)
|
|
70
|
+
clearInterval(pollTimer);
|
|
71
|
+
pollTimer = null;
|
|
72
|
+
}
|
|
73
|
+
/** §5.3: collect a sign-in completed by a link opened on another device. */
|
|
74
|
+
function maybeStartPolling() {
|
|
75
|
+
if (pollTimer || !shouldKeepPolling(get(state)))
|
|
76
|
+
return;
|
|
77
|
+
pollTimer = setInterval(() => {
|
|
78
|
+
void (async () => {
|
|
79
|
+
const { state: next, ticket } = await pollAttempt(client, get(state));
|
|
80
|
+
state.set(next);
|
|
81
|
+
if (!shouldKeepPolling(next))
|
|
82
|
+
stopPolling();
|
|
83
|
+
if (!ticket || !next.attempt)
|
|
84
|
+
return;
|
|
85
|
+
// The ticket is exchanged by a normal request, so no session token has
|
|
86
|
+
// ever appeared in a URL.
|
|
87
|
+
await client.post('/v1/client/tickets/exchange', {
|
|
88
|
+
attempt_id: next.attempt.id,
|
|
89
|
+
ticket,
|
|
90
|
+
});
|
|
91
|
+
await atlas.reload();
|
|
92
|
+
})();
|
|
93
|
+
}, 2_000);
|
|
94
|
+
}
|
|
95
|
+
function submit(values) {
|
|
96
|
+
void (async () => {
|
|
97
|
+
state.update((current) => ({ ...current, busy: true, errors: [] }));
|
|
98
|
+
const next = await advance(client, get(state), values);
|
|
99
|
+
state.set(next);
|
|
100
|
+
// On completion the response carries a one-time ticket (§7.1). Exchange it
|
|
101
|
+
// for cookies over a same-origin request — FAPI is cross-origin to the app
|
|
102
|
+
// and cannot set the cookie on the completion response itself — THEN
|
|
103
|
+
// re-boot so the rest of the app sees a signed-in user.
|
|
104
|
+
if (next.attempt?.status === 'complete') {
|
|
105
|
+
if (next.ticket) {
|
|
106
|
+
await client.post('/v1/client/tickets/exchange', {
|
|
107
|
+
attempt_id: next.attempt.id,
|
|
108
|
+
ticket: next.ticket,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
await atlas.reload();
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
maybeStartPolling();
|
|
115
|
+
})();
|
|
116
|
+
}
|
|
117
|
+
return { state: { subscribe: state.subscribe }, submit, destroy: stopPolling };
|
|
118
|
+
}
|
|
119
|
+
/** Start (or resume) a sign-in flow. Drives `<SignIn>`; usable standalone too. */
|
|
120
|
+
export function signIn() {
|
|
121
|
+
return createFlow();
|
|
122
|
+
}
|
|
123
|
+
/** Start a sign-up flow. Drives `<SignUp>`; usable standalone too. */
|
|
124
|
+
export function signUp() {
|
|
125
|
+
return createFlow();
|
|
126
|
+
}
|
|
@@ -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,91 @@
|
|
|
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 const DEFAULT_TOKENS = {
|
|
21
|
+
colorPrimary: '#5b5bd6',
|
|
22
|
+
colorText: '#1c1c1f',
|
|
23
|
+
colorBackground: '#ffffff',
|
|
24
|
+
colorDanger: '#d64545',
|
|
25
|
+
colorBorder: '#e4e4e9',
|
|
26
|
+
borderRadius: '8px',
|
|
27
|
+
fontFamily: 'ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif',
|
|
28
|
+
spacingUnit: '4px',
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* §10.3: "prebuilt themes shipped (default, dark, neobrutalist as a demo of the
|
|
32
|
+
* override system)". Neobrutalist exists to prove the token set is expressive
|
|
33
|
+
* enough for a theme that looks nothing like the default — a theming system
|
|
34
|
+
* that only produces tasteful variations of itself is not a theming system.
|
|
35
|
+
*/
|
|
36
|
+
export const THEMES = {
|
|
37
|
+
default: DEFAULT_TOKENS,
|
|
38
|
+
dark: {
|
|
39
|
+
...DEFAULT_TOKENS,
|
|
40
|
+
colorText: '#e6e9ef',
|
|
41
|
+
colorBackground: '#14171c',
|
|
42
|
+
colorBorder: '#242932',
|
|
43
|
+
colorPrimary: '#6ea8fe',
|
|
44
|
+
},
|
|
45
|
+
neobrutalist: {
|
|
46
|
+
...DEFAULT_TOKENS,
|
|
47
|
+
colorPrimary: '#ffe600',
|
|
48
|
+
colorText: '#000000',
|
|
49
|
+
colorBackground: '#ffffff',
|
|
50
|
+
colorBorder: '#000000',
|
|
51
|
+
borderRadius: '0px',
|
|
52
|
+
fontFamily: '"Courier New", ui-monospace, monospace',
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
/**
|
|
56
|
+
* Resolve tokens: defaults, then the base theme, then explicit variables.
|
|
57
|
+
*
|
|
58
|
+
* Later wins, and `undefined` never overwrites. A customer setting one variable
|
|
59
|
+
* on the dark theme expects the other eleven to stay dark, not silently revert
|
|
60
|
+
* to the light defaults.
|
|
61
|
+
*/
|
|
62
|
+
export function resolveTokens(appearance) {
|
|
63
|
+
const base = appearance?.baseTheme ? THEMES[appearance.baseTheme] : DEFAULT_TOKENS;
|
|
64
|
+
const overrides = appearance?.variables ?? {};
|
|
65
|
+
const resolved = { ...DEFAULT_TOKENS, ...base };
|
|
66
|
+
for (const [key, value] of Object.entries(overrides)) {
|
|
67
|
+
if (value !== undefined)
|
|
68
|
+
resolved[key] = value;
|
|
69
|
+
}
|
|
70
|
+
return resolved;
|
|
71
|
+
}
|
|
72
|
+
/** Tokens as CSS custom properties, for the host DOM to inherit. */
|
|
73
|
+
export function cssVariables(tokens) {
|
|
74
|
+
const vars = {};
|
|
75
|
+
for (const [key, value] of Object.entries(tokens)) {
|
|
76
|
+
// camelCase → --atlas-kebab-case, which is what a customer writes in CSS.
|
|
77
|
+
vars[`--atlas-${key.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`)}`] = value;
|
|
78
|
+
}
|
|
79
|
+
return vars;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Combine our base class with the customer's override.
|
|
83
|
+
*
|
|
84
|
+
* Appended, never replaced — a customer adding `rounded-xl` wants a rounder
|
|
85
|
+
* button, not an unstyled one. Theirs comes last so it wins on equal
|
|
86
|
+
* specificity, which is the behaviour anyone writing Tailwind expects.
|
|
87
|
+
*/
|
|
88
|
+
export function classFor(element, appearance, base) {
|
|
89
|
+
const override = appearance?.elements?.[element];
|
|
90
|
+
return override ? `${base} ${override}` : base;
|
|
91
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { SvelteComponent } from "svelte";
|
|
2
|
+
declare const __propDef: {
|
|
3
|
+
props: Record<string, never>;
|
|
4
|
+
events: {
|
|
5
|
+
[evt: string]: CustomEvent<any>;
|
|
6
|
+
};
|
|
7
|
+
slots: {
|
|
8
|
+
default: {};
|
|
9
|
+
};
|
|
10
|
+
exports?: {} | undefined;
|
|
11
|
+
bindings?: string | undefined;
|
|
12
|
+
};
|
|
13
|
+
export type AtlasLoadedProps = typeof __propDef.props;
|
|
14
|
+
export type AtlasLoadedEvents = typeof __propDef.events;
|
|
15
|
+
export type AtlasLoadedSlots = typeof __propDef.slots;
|
|
16
|
+
export default class AtlasLoaded extends SvelteComponent<AtlasLoadedProps, AtlasLoadedEvents, AtlasLoadedSlots> {
|
|
17
|
+
}
|
|
18
|
+
export {};
|