@atlasauth/svelte 0.3.0 → 0.5.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/CHANGELOG.md CHANGED
@@ -1,5 +1,48 @@
1
1
  # @atlasauth/svelte changelog
2
2
 
3
+ ## 0.5.0
4
+ Requires `@atlasauth/js` 0.9.0.
5
+ - **Server errors render in the app's language.** `<SignIn>` / `<SignUp>` show the catalog string
6
+ `error.<reason>` for an error's `reason` (placeholders filled from its `meta`), falling back to the server's
7
+ message, then `error.generic`. With `localization: esES` a refused password reads
8
+ "Esas credenciales son incorrectas." instead of the server's English sentence.
9
+ - **`Accept-Language`.** The client's and the flows' Frontend API requests carry the `locale` tag of the
10
+ `localization` bundle, so the server localises `message` too.
11
+ - **Added 38 `error.<reason>` keys** to the built-in `EN_US` catalog (see `@atlasauth/localizations` 0.4.0).
12
+ - `AtlasClient` gains `errorCatalog?: Catalog` and `locale?: string`.
13
+
14
+
15
+ ## 0.4.0
16
+ - **Safer sign-out.** `signOut(options?)` (on the client, `getAuth()` and the `signOut()` helper) now
17
+ resolves `SignOutResult` — `{ revoked: boolean; activeSessionId?: string }` — instead of `void`
18
+ (existing `await signOut()` call sites keep working). The session is revoked on the Frontend API (retried
19
+ once) even when no token is cached (the session is looked up from the cookies), and is remembered
20
+ (a tombstone in `localStorage`, per Frontend API) so a reload, a token refresh, a sibling tab or a tab
21
+ opened later never re-adopts it while the refresh cookie is still valid; a revoke the server did not
22
+ confirm is re-tried in the background and the tombstone dropped once it lands. A session the server
23
+ promotes on revoke (`active_session_id`) is adopted. `signOut({ strict: true })` rejects with a
24
+ `SignOutError` (after the local sign-out) when the revoke was not confirmed. Built on `SignOutGuard`
25
+ from `@atlasauth/js`.
26
+ - **Sibling-tab sign-out.** A tab signed in to a session another tab signed out of now signs out too.
27
+ - **Popup landing.** On boot the client completes a severed OAuth popup landing
28
+ (`#atlas_popup_result=…`): it relays the result to the opener and closes when the opener takes it,
29
+ otherwise exchanges the ticket itself (or follows the continuation URL). After exchanging a redirect
30
+ ticket inside the Atlas popup window it hands back to the opener and closes the popup.
31
+ - **Email delivery.** The prebuilt components read `settings.emailDelivery`:
32
+ `<SignUp>` shows a "can't send email" notice (`signUp.emailUnavailable`) instead of the email + password
33
+ form when `passwordSignUp` is false; `<SignIn>` offers emailed first factors only while `available`.
34
+ - **`<SignIn>` first-factor choice.** When the server offers `email_code` / `email_link`, the first-factor
35
+ step shows "Email me a code" / "Email me a sign-in link" buttons next to the password field.
36
+ - **Added to `FlowController`:** `prepare(strategy: 'email_code' | 'email_link'): Promise<boolean>`.
37
+ - **Changed types:** `AtlasClient.signOut`, `UseAuth.signOut` and the `signOut()` helper return
38
+ `Promise<SignOutResult>`.
39
+ - **Added re-exports** from `@atlasauth/js`: `SignOutError` (class), and the types `SignOutResult`,
40
+ `SignOutOptions`, `EmailDelivery`.
41
+ - **Added catalog keys** (en-US): `signUp.emailUnavailable`, `signIn.emailCodeOption`,
42
+ `signIn.emailLinkOption`.
43
+ - `destroy()` now also stops the sibling-tab watch and cancels background revoke re-tries.
44
+ - Requires `@atlasauth/js` 0.8.0.
45
+
3
46
  ## 0.3.0
4
47
  - **`<SignUp>` now creates accounts.** Its first step is a create-account form (the instance's
5
48
  configured sign-up fields, email, password and the legal-consent row) that posts
@@ -1,5 +1,5 @@
1
1
  import { type Readable } from 'svelte/store';
2
- import { type CaptchaController, type FlowState, type InstanceSettings, type ProviderToken, type SignOutOptions } from '@atlasauth/js';
2
+ import { type CaptchaController, type FlowState, type InstanceSettings, type ProviderToken, type SignOutOptions, type SignOutResult } from '@atlasauth/js';
3
3
  import { type AuthStatus } from './context';
4
4
  import { type ProtectCondition } from './protect';
5
5
  import type { AtlasOrganizationMembership, AtlasSession, AtlasUser } from './types';
@@ -48,7 +48,12 @@ export interface UseAuth {
48
48
  getToken(): Promise<string | null>;
49
49
  /** §6.6 the user's own live access token at a connected social provider. */
50
50
  getProviderToken(provider: string): Promise<ProviderToken | null>;
51
- signOut(options?: SignOutOptions): Promise<void>;
51
+ /**
52
+ * Revoke the session and drop all in-memory state. Resolves
53
+ * `{ revoked, activeSessionId? }`; `{ strict: true }` rejects with a
54
+ * `SignOutError` when the server could not confirm the revoke.
55
+ */
56
+ signOut(options?: SignOutOptions): Promise<SignOutResult>;
52
57
  /** Mirrors `<Protect>`, for logic that is not a render decision. */
53
58
  has(condition: ProtectCondition): boolean;
54
59
  }
@@ -63,8 +68,12 @@ export interface UseOrganization {
63
68
  export declare function getOrganization(): Readable<UseOrganization>;
64
69
  /** Refresh-if-stale, then hand over the JWT. The `useAuth().getToken` shortcut. */
65
70
  export declare function getToken(): Promise<string | null>;
66
- /** Revoke the session and drop all in-memory state. */
67
- export declare function signOut(options?: SignOutOptions): Promise<void>;
71
+ /**
72
+ * Revoke the session and drop all in-memory state. Resolves
73
+ * `{ revoked, activeSessionId? }`; `{ strict: true }` rejects with a
74
+ * `SignOutError` (after the local sign-out) when the revoke was not confirmed.
75
+ */
76
+ export declare function signOut(options?: SignOutOptions): Promise<SignOutResult>;
68
77
  /**
69
78
  * A running sign-in / sign-up attempt: a reactive `state` store plus `submit`.
70
79
  *
@@ -84,10 +93,16 @@ export interface FlowController {
84
93
  * then posted with {@link submit}.
85
94
  */
86
95
  create(values: Record<string, string>, consent?: boolean): void;
96
+ /**
97
+ * Ask the server to email a sign-in code or link on the first-factor step
98
+ * (`POST …/prepare_first_factor`). Resolves true once it was sent; the code is
99
+ * then posted with {@link submit}, a link is collected by polling.
100
+ */
101
+ prepare(strategy: 'email_code' | 'email_link'): Promise<boolean>;
87
102
  /**
88
103
  * The instance's public sign-in / sign-up settings (captcha, sign-up fields,
89
- * legal consent) from `GET /v1/appearance`; null until loaded or when the
90
- * server could not be reached.
104
+ * legal consent, email delivery) from `GET /v1/appearance`; null until loaded
105
+ * or when the server could not be reached.
91
106
  */
92
107
  settings: Readable<InstanceSettings | null>;
93
108
  /** The sign-in captcha (identifier step / `needs_captcha` challenge). */
package/dist/accessors.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { derived, get, writable } from 'svelte/store';
2
- import { FapiClient, advanceWithCaptcha, createCaptchaController, createSignUp, fetchInstanceSettings, flowNeedsCaptcha, signInCaptchaOptions, flowFromPending, pollAttempt, shouldKeepPolling, } from '@atlasauth/js';
2
+ import { FapiClient, advanceWithCaptcha, createCaptchaController, createSignUp, fetchInstanceSettings, flowNeedsCaptcha, signInCaptchaOptions, flowFromPending, pollAttempt, prepareFactor, shouldKeepPolling, } from '@atlasauth/js';
3
3
  import { useAtlas } from './context';
4
4
  import { translate } from './i18n';
5
5
  import { evaluate } from './protect';
@@ -54,7 +54,11 @@ export function getOrganization() {
54
54
  export function getToken() {
55
55
  return useAtlas().getToken();
56
56
  }
57
- /** Revoke the session and drop all in-memory state. */
57
+ /**
58
+ * Revoke the session and drop all in-memory state. Resolves
59
+ * `{ revoked, activeSessionId? }`; `{ strict: true }` rejects with a
60
+ * `SignOutError` (after the local sign-out) when the revoke was not confirmed.
61
+ */
58
62
  export function signOut(options) {
59
63
  return useAtlas().signOut(options);
60
64
  }
@@ -63,6 +67,7 @@ function createFlow() {
63
67
  const client = new FapiClient({
64
68
  publishableKey: atlas.publishableKey,
65
69
  baseUrl: atlas.frontendApi,
70
+ locale: atlas.locale,
66
71
  });
67
72
  // Seeded from a redirect that came back mid-flow (e.g. OAuth needing a second
68
73
  // factor), so the user resumes at the demanded step instead of starting over.
@@ -146,6 +151,13 @@ function createFlow() {
146
151
  await finish(await createSignUp(client, values, { consent, captcha: signUpCaptcha, t }));
147
152
  })();
148
153
  }
154
+ async function prepare(strategy) {
155
+ state.update((current) => ({ ...current, busy: true, errors: [] }));
156
+ const next = await prepareFactor(client, get(state), strategy);
157
+ state.set({ ...next, busy: false });
158
+ maybeStartPolling();
159
+ return next.errors.length === 0;
160
+ }
149
161
  function destroy() {
150
162
  stopPolling();
151
163
  unsubscribeCaptcha();
@@ -156,6 +168,7 @@ function createFlow() {
156
168
  state: { subscribe: state.subscribe },
157
169
  submit,
158
170
  create,
171
+ prepare,
159
172
  settings: { subscribe: settings.subscribe },
160
173
  captcha,
161
174
  signUpCaptcha,
@@ -1,4 +1,8 @@
1
- <script>import { nextStep } from "@atlasauth/js";
1
+ <script>import {
2
+ localizedErrorMessage,
3
+ nextStep,
4
+ offeredFirstFactors
5
+ } from "@atlasauth/js";
2
6
  import { useAtlas } from "../context";
3
7
  import { classFor } from "../appearance";
4
8
  import { translate } from "../i18n";
@@ -9,17 +13,34 @@ export let errors = [];
9
13
  export let titleKey;
10
14
  export let onSubmit;
11
15
  export let captcha = null;
16
+ export let emailDelivery = null;
17
+ export let onPrepare = null;
12
18
  const atlas = useAtlas();
13
19
  const cls = (key, base) => classFor(key, atlas.appearance, base);
14
20
  const t = (key, vars) => translate(atlas.catalog, key, vars);
21
+ const errorText = (error) => localizedErrorMessage(error, atlas.errorCatalog ?? atlas.catalog, atlas.catalog);
15
22
  let values = {};
16
23
  $: step = attempt ? nextStep(attempt) : { kind: "collect_identifier" };
24
+ const strategyName = (s) => typeof s === "string" ? s : String(s?.strategy ?? "");
25
+ $: offered = step.kind === "collect_first_factor" ? offeredFirstFactors(step.strategies.map(strategyName), emailDelivery) : [];
26
+ $: emailOptions = onPrepare ? offered.filter((s) => s === "email_code" || s === "email_link") : [];
27
+ $: showPassword = offered.includes("password") || emailOptions.length === 0;
28
+ let prepared = null;
29
+ $: sent = prepared && prepared.attemptId === (attempt?.id ?? null) ? prepared.strategy : null;
30
+ async function prepare(strategy) {
31
+ if (!onPrepare) return;
32
+ const attemptId = attempt?.id ?? null;
33
+ if (await onPrepare(strategy)) prepared = { strategy, attemptId };
34
+ }
17
35
  $: fields = (() => {
18
36
  switch (step.kind) {
19
37
  case "collect_identifier":
20
38
  return [{ name: "identifier", labelKey: "signIn.identifierLabel", type: "email" }];
21
39
  case "collect_first_factor":
22
- return [{ name: "password", labelKey: "signIn.passwordLabel", type: "password" }];
40
+ if (sent === "email_code") {
41
+ return [{ name: "code", labelKey: "signIn.emailCodeLabel", type: "text" }];
42
+ }
43
+ return showPassword ? [{ name: "password", labelKey: "signIn.passwordLabel", type: "password" }] : [];
23
44
  case "collect_second_factor":
24
45
  return [{ name: "code", labelKey: "mfa.codeLabel", type: "text" }];
25
46
  case "enroll_second_factor":
@@ -43,9 +64,10 @@ $: message = (() => {
43
64
  return t("signIn.checkOtherTab");
44
65
  case "restart":
45
66
  return t("error.generic");
67
+ case "collect_first_factor":
68
+ return sent === "email_link" ? t("signIn.magicLinkSent") : null;
46
69
  case "collect_captcha":
47
70
  case "collect_identifier":
48
- case "collect_first_factor":
49
71
  case "collect_second_factor":
50
72
  case "enroll_second_factor":
51
73
  case "collect_email_code":
@@ -55,7 +77,9 @@ $: message = (() => {
55
77
  return t("error.generic");
56
78
  }
57
79
  })();
58
- $: showSubmit = step.kind !== "done" && step.kind !== "await_oauth" && message === null;
80
+ $: showSubmit = step.kind !== "done" && step.kind !== "await_oauth" && message === null && // Only emailed factors on offer: the option buttons are the way forward.
81
+ (step.kind !== "collect_first_factor" || fields.length > 0);
82
+ $: showEmailOptions = step.kind === "collect_first_factor" && message === null && sent === null && emailOptions.length > 0;
59
83
  $: showCaptcha = !!captcha && (step.kind === "collect_identifier" || step.kind === "collect_captcha");
60
84
  function handleSubmit(event) {
61
85
  event.preventDefault();
@@ -67,7 +91,7 @@ function handleSubmit(event) {
67
91
  <h1 class={cls('headerTitle', 'atlas-title')}>{t(titleKey)}</h1>
68
92
 
69
93
  {#each errors.filter((e) => !e.param) as error (error.code)}
70
- <p class={cls('formFieldError', 'atlas-error')} role="alert">{error.message}</p>
94
+ <p class={cls('formFieldError', 'atlas-error')} role="alert">{errorText(error)}</p>
71
95
  {/each}
72
96
 
73
97
  <form on:submit={handleSubmit}>
@@ -90,10 +114,23 @@ function handleSubmit(event) {
90
114
  on:input={(e) => (values[f.name] = e.currentTarget.value)}
91
115
  />
92
116
  {#each errors.filter((e) => e.param === f.name) as error (error.code)}
93
- <span class={cls('formFieldError', 'atlas-error')}>{error.message}</span>
117
+ <span class={cls('formFieldError', 'atlas-error')}>{errorText(error)}</span>
94
118
  {/each}
95
119
  </label>
96
120
  {/each}
121
+ {#if showEmailOptions}
122
+ {#each emailOptions as strategy (strategy)}
123
+ <button
124
+ class={cls('formButtonSecondary', 'atlas-button-secondary')}
125
+ type="button"
126
+ data-strategy={strategy}
127
+ disabled={busy}
128
+ on:click={() => void prepare(strategy)}
129
+ >
130
+ {t(strategy === 'email_code' ? 'signIn.emailCodeOption' : 'signIn.emailLinkOption')}
131
+ </button>
132
+ {/each}
133
+ {/if}
97
134
  {/if}
98
135
 
99
136
  {#if showCaptcha && captcha}
@@ -1,5 +1,5 @@
1
1
  import { SvelteComponent } from "svelte";
2
- import { type AttemptView, type CaptchaController, type FieldError } from '@atlasauth/js';
2
+ import { type AttemptView, type CaptchaController, type EmailDelivery, type FieldError } from '@atlasauth/js';
3
3
  declare const __propDef: {
4
4
  props: {
5
5
  /**
@@ -19,6 +19,14 @@ declare const __propDef: {
19
19
  * (an always-on sign-in captcha) and on the challenge step the server demands
20
20
  * with `needs_captcha`. Null when the flow draws none.
21
21
  */ captcha?: CaptchaController | null;
22
+ /**
23
+ * The instance's email-delivery facts (`settings.emailDelivery`). While the app
24
+ * cannot send email the first-factor step offers no email code / link option.
25
+ */ emailDelivery?: EmailDelivery | null;
26
+ /**
27
+ * Ask the server to email a sign-in code or link (`FlowController.prepare`).
28
+ * Without it the first-factor step offers the password only.
29
+ */ onPrepare?: ((strategy: "email_code" | "email_link") => Promise<boolean>) | null;
22
30
  };
23
31
  events: {
24
32
  [evt: string]: CustomEvent<any>;
@@ -16,5 +16,7 @@ $: captcha = drawn($settings, $flowState);
16
16
  errors={$flowState.errors}
17
17
  titleKey="signIn.title"
18
18
  onSubmit={flow.submit}
19
+ onPrepare={flow.prepare}
20
+ emailDelivery={$settings?.emailDelivery ?? null}
19
21
  {captcha}
20
22
  />
@@ -6,6 +6,8 @@ declare const __propDef: {
6
6
  * server-side attempt status. Contains no decision about what comes next; it
7
7
  * renders whatever `status` the server returned. Draws the instance's sign-in
8
8
  * captcha when it is required (always-on, or a `needs_captcha` challenge).
9
+ * Offers emailed first factors (code / link) only while the app can deliver
10
+ * email (`settings.emailDelivery`).
9
11
  */ afterUrl?: string | undefined;
10
12
  };
11
13
  events: {
@@ -1,4 +1,7 @@
1
- <script>import { useAtlas } from "../context";
1
+ <script>import {
2
+ localizedErrorMessage
3
+ } from "@atlasauth/js";
4
+ import { useAtlas } from "../context";
2
5
  import { classFor } from "../appearance";
3
6
  import { translate } from "../i18n";
4
7
  import CaptchaSlot from "./CaptchaSlot.svelte";
@@ -10,12 +13,14 @@ export let onSubmit;
10
13
  const atlas = useAtlas();
11
14
  const cls = (key, base) => classFor(key, atlas.appearance, base);
12
15
  const t = (key, vars) => translate(atlas.catalog, key, vars);
16
+ const errorText = (error) => localizedErrorMessage(error, atlas.errorCatalog ?? atlas.catalog, atlas.catalog);
13
17
  let values = {};
14
18
  let agreed = false;
15
19
  $: legal = settings?.legal;
16
20
  $: showLegal = !!legal && (!!legal.termsUrl || !!legal.privacyUrl);
17
21
  $: consentOk = !legal?.required || agreed;
18
22
  $: fields = settings?.signUpFields ?? [];
23
+ $: emailUnavailable = settings?.emailDelivery?.passwordSignUp === false;
19
24
  const label = (f) => f.required ? f.label : t("signUp.optionalField", { label: f.label });
20
25
  const inputType = (f) => ["email", "tel", "url", "number", "date"].includes(f.type) ? f.type : "text";
21
26
  const picked = (key) => values[key] ? values[key].split(", ") : [];
@@ -37,147 +42,151 @@ function handleSubmit(event) {
37
42
  <h1 class={cls('headerTitle', 'atlas-title')}>{t('signUp.title')}</h1>
38
43
 
39
44
  {#each errors.filter((e) => !e.param) as error (error.code)}
40
- <p class={cls('formFieldError', 'atlas-error')} role="alert">{error.message}</p>
45
+ <p class={cls('formFieldError', 'atlas-error')} role="alert">{errorText(error)}</p>
41
46
  {/each}
42
47
 
43
- <form on:submit={handleSubmit}>
44
- {#each fields as f (f.key)}
45
- {#if f.type === 'radio' || f.type === 'multiselect'}
46
- <!-- A choice group holds several inputs, so it can't sit inside one <label>. -->
47
- <div class={cls('formField', 'atlas-field')}>
48
- <span class={cls('formFieldLabel', 'atlas-field-label')}>{label(f)}</span>
49
- <span class="atlas-choices">
50
- {#each f.options as o (o)}
51
- <label class="atlas-choice">
52
- <input
53
- type={f.type === 'radio' ? 'radio' : 'checkbox'}
54
- name={f.key}
55
- value={o}
56
- checked={picked(f.key).includes(o)}
57
- on:change={(e) => toggle(f, o, e.currentTarget.checked)}
58
- />
59
- {o}
60
- </label>
61
- {/each}
62
- </span>
63
- {#each errors.filter((e) => e.param === f.key) as error (error.code)}
64
- <span class={cls('formFieldError', 'atlas-error')}>{error.message}</span>
65
- {/each}
66
- </div>
67
- {:else}
68
- <label class={cls('formField', 'atlas-field')}>
69
- <span class={cls('formFieldLabel', 'atlas-field-label')}>{label(f)}</span>
70
- {#if f.type === 'checkbox'}
71
- <input
72
- type="checkbox"
73
- name={f.key}
74
- checked={values[f.key] === 'true'}
75
- on:change={(e) => (values[f.key] = e.currentTarget.checked ? 'true' : '')}
76
- />
77
- {:else if f.type === 'select'}
78
- <select
79
- class={cls('formFieldInput', 'atlas-input')}
80
- name={f.key}
81
- value={values[f.key] ?? ''}
82
- required={f.required}
83
- on:change={(e) => (values[f.key] = e.currentTarget.value)}
84
- >
85
- <option value="">{f.placeholder ?? t('signUp.selectPlaceholder')}</option>
48
+ {#if emailUnavailable}
49
+ <p class={cls('formFieldError', 'atlas-error')} role="alert">{t('signUp.emailUnavailable')}</p>
50
+ {:else}
51
+ <form on:submit={handleSubmit}>
52
+ {#each fields as f (f.key)}
53
+ {#if f.type === 'radio' || f.type === 'multiselect'}
54
+ <!-- A choice group holds several inputs, so it can't sit inside one <label>. -->
55
+ <div class={cls('formField', 'atlas-field')}>
56
+ <span class={cls('formFieldLabel', 'atlas-field-label')}>{label(f)}</span>
57
+ <span class="atlas-choices">
86
58
  {#each f.options as o (o)}
87
- <option value={o}>{o}</option>
59
+ <label class="atlas-choice">
60
+ <input
61
+ type={f.type === 'radio' ? 'radio' : 'checkbox'}
62
+ name={f.key}
63
+ value={o}
64
+ checked={picked(f.key).includes(o)}
65
+ on:change={(e) => toggle(f, o, e.currentTarget.checked)}
66
+ />
67
+ {o}
68
+ </label>
88
69
  {/each}
89
- </select>
90
- {:else if f.type === 'textarea'}
91
- <textarea
92
- class={cls('formFieldInput', 'atlas-input')}
93
- name={f.key}
94
- rows="3"
95
- value={values[f.key] ?? ''}
96
- required={f.required}
97
- placeholder={f.placeholder ?? undefined}
98
- on:input={(e) => (values[f.key] = e.currentTarget.value)}
99
- />
100
- {:else}
101
- <input
102
- class={cls('formFieldInput', 'atlas-input')}
103
- type={inputType(f)}
104
- name={f.key}
105
- value={values[f.key] ?? ''}
106
- required={f.required}
107
- placeholder={f.placeholder ?? undefined}
108
- on:input={(e) => (values[f.key] = e.currentTarget.value)}
109
- />
110
- {/if}
111
- {#each errors.filter((e) => e.param === f.key) as error (error.code)}
112
- <span class={cls('formFieldError', 'atlas-error')}>{error.message}</span>
113
- {/each}
114
- </label>
115
- {/if}
116
- {/each}
117
-
118
- <label class={cls('formField', 'atlas-field')}>
119
- <span class={cls('formFieldLabel', 'atlas-field-label')}>{t('signUp.emailLabel')}</span>
120
- <input
121
- class={cls('formFieldInput', 'atlas-input')}
122
- type="email"
123
- name="email"
124
- autocomplete="email"
125
- required
126
- value={values.email ?? ''}
127
- on:input={(e) => (values.email = e.currentTarget.value)}
128
- />
129
- {#each errors.filter((e) => e.param === 'email') as error (error.code)}
130
- <span class={cls('formFieldError', 'atlas-error')}>{error.message}</span>
70
+ </span>
71
+ {#each errors.filter((e) => e.param === f.key) as error (error.code)}
72
+ <span class={cls('formFieldError', 'atlas-error')}>{errorText(error)}</span>
73
+ {/each}
74
+ </div>
75
+ {:else}
76
+ <label class={cls('formField', 'atlas-field')}>
77
+ <span class={cls('formFieldLabel', 'atlas-field-label')}>{label(f)}</span>
78
+ {#if f.type === 'checkbox'}
79
+ <input
80
+ type="checkbox"
81
+ name={f.key}
82
+ checked={values[f.key] === 'true'}
83
+ on:change={(e) => (values[f.key] = e.currentTarget.checked ? 'true' : '')}
84
+ />
85
+ {:else if f.type === 'select'}
86
+ <select
87
+ class={cls('formFieldInput', 'atlas-input')}
88
+ name={f.key}
89
+ value={values[f.key] ?? ''}
90
+ required={f.required}
91
+ on:change={(e) => (values[f.key] = e.currentTarget.value)}
92
+ >
93
+ <option value="">{f.placeholder ?? t('signUp.selectPlaceholder')}</option>
94
+ {#each f.options as o (o)}
95
+ <option value={o}>{o}</option>
96
+ {/each}
97
+ </select>
98
+ {:else if f.type === 'textarea'}
99
+ <textarea
100
+ class={cls('formFieldInput', 'atlas-input')}
101
+ name={f.key}
102
+ rows="3"
103
+ value={values[f.key] ?? ''}
104
+ required={f.required}
105
+ placeholder={f.placeholder ?? undefined}
106
+ on:input={(e) => (values[f.key] = e.currentTarget.value)}
107
+ />
108
+ {:else}
109
+ <input
110
+ class={cls('formFieldInput', 'atlas-input')}
111
+ type={inputType(f)}
112
+ name={f.key}
113
+ value={values[f.key] ?? ''}
114
+ required={f.required}
115
+ placeholder={f.placeholder ?? undefined}
116
+ on:input={(e) => (values[f.key] = e.currentTarget.value)}
117
+ />
118
+ {/if}
119
+ {#each errors.filter((e) => e.param === f.key) as error (error.code)}
120
+ <span class={cls('formFieldError', 'atlas-error')}>{errorText(error)}</span>
121
+ {/each}
122
+ </label>
123
+ {/if}
131
124
  {/each}
132
- </label>
133
125
 
134
- <label class={cls('formField', 'atlas-field')}>
135
- <span class={cls('formFieldLabel', 'atlas-field-label')}>{t('signUp.passwordLabel')}</span>
136
- <input
137
- class={cls('formFieldInput', 'atlas-input')}
138
- type="password"
139
- name="password"
140
- autocomplete="new-password"
141
- required
142
- value={values.password ?? ''}
143
- on:input={(e) => (values.password = e.currentTarget.value)}
144
- />
145
- {#each errors.filter((e) => e.param === 'password') as error (error.code)}
146
- <span class={cls('formFieldError', 'atlas-error')}>{error.message}</span>
147
- {/each}
148
- </label>
126
+ <label class={cls('formField', 'atlas-field')}>
127
+ <span class={cls('formFieldLabel', 'atlas-field-label')}>{t('signUp.emailLabel')}</span>
128
+ <input
129
+ class={cls('formFieldInput', 'atlas-input')}
130
+ type="email"
131
+ name="email"
132
+ autocomplete="email"
133
+ required
134
+ value={values.email ?? ''}
135
+ on:input={(e) => (values.email = e.currentTarget.value)}
136
+ />
137
+ {#each errors.filter((e) => e.param === 'email') as error (error.code)}
138
+ <span class={cls('formFieldError', 'atlas-error')}>{errorText(error)}</span>
139
+ {/each}
140
+ </label>
149
141
 
150
- {#if showLegal && legal}
151
- <label class={cls('formField', 'atlas-consent')}>
152
- {#if legal.required}
153
- <input type="checkbox" name="consent" bind:checked={agreed} />
154
- {/if}
155
- <span class="atlas-text">
156
- {t(legal.required ? 'signUp.consentAgree' : 'signUp.consentNotice')}
157
- {#if legal.termsUrl}
158
- <a class="atlas-link" href={legal.termsUrl} target="_blank" rel="noopener noreferrer"
159
- >{t('signUp.terms')}</a
160
- >{/if}{#if legal.termsUrl && legal.privacyUrl}{' '}{t('signUp.and')}{' '}{/if}{#if legal.privacyUrl}
161
- <a class="atlas-link" href={legal.privacyUrl} target="_blank" rel="noopener noreferrer"
162
- >{t('signUp.privacy')}</a
163
- >{/if}.
164
- </span>
165
- {#each errors.filter((e) => e.param === 'consent') as error (error.code)}
166
- <span class={cls('formFieldError', 'atlas-error')}>{error.message}</span>
142
+ <label class={cls('formField', 'atlas-field')}>
143
+ <span class={cls('formFieldLabel', 'atlas-field-label')}>{t('signUp.passwordLabel')}</span>
144
+ <input
145
+ class={cls('formFieldInput', 'atlas-input')}
146
+ type="password"
147
+ name="password"
148
+ autocomplete="new-password"
149
+ required
150
+ value={values.password ?? ''}
151
+ on:input={(e) => (values.password = e.currentTarget.value)}
152
+ />
153
+ {#each errors.filter((e) => e.param === 'password') as error (error.code)}
154
+ <span class={cls('formFieldError', 'atlas-error')}>{errorText(error)}</span>
167
155
  {/each}
168
156
  </label>
169
- {/if}
170
157
 
171
- {#if captcha}
172
- <CaptchaSlot controller={captcha} />
173
- {/if}
158
+ {#if showLegal && legal}
159
+ <label class={cls('formField', 'atlas-consent')}>
160
+ {#if legal.required}
161
+ <input type="checkbox" name="consent" bind:checked={agreed} />
162
+ {/if}
163
+ <span class="atlas-text">
164
+ {t(legal.required ? 'signUp.consentAgree' : 'signUp.consentNotice')}
165
+ {#if legal.termsUrl}
166
+ <a class="atlas-link" href={legal.termsUrl} target="_blank" rel="noopener noreferrer"
167
+ >{t('signUp.terms')}</a
168
+ >{/if}{#if legal.termsUrl && legal.privacyUrl}{' '}{t('signUp.and')}{' '}{/if}{#if legal.privacyUrl}
169
+ <a class="atlas-link" href={legal.privacyUrl} target="_blank" rel="noopener noreferrer"
170
+ >{t('signUp.privacy')}</a
171
+ >{/if}.
172
+ </span>
173
+ {#each errors.filter((e) => e.param === 'consent') as error (error.code)}
174
+ <span class={cls('formFieldError', 'atlas-error')}>{errorText(error)}</span>
175
+ {/each}
176
+ </label>
177
+ {/if}
178
+
179
+ {#if captcha}
180
+ <CaptchaSlot controller={captcha} />
181
+ {/if}
174
182
 
175
- <button
176
- class={cls('formButtonPrimary', 'atlas-button')}
177
- disabled={busy || !consentOk}
178
- type="submit"
179
- >
180
- {t('signUp.submit')}
181
- </button>
182
- </form>
183
+ <button
184
+ class={cls('formButtonPrimary', 'atlas-button')}
185
+ disabled={busy || !consentOk}
186
+ type="submit"
187
+ >
188
+ {t('signUp.submit')}
189
+ </button>
190
+ </form>
191
+ {/if}
183
192
  </div>
@@ -1,5 +1,5 @@
1
1
  import { SvelteComponent } from "svelte";
2
- import type { CaptchaController, FieldError, InstanceSettings } from '@atlasauth/js';
2
+ import { type CaptchaController, type FieldError, type InstanceSettings } from '@atlasauth/js';
3
3
  declare const __propDef: {
4
4
  props: {
5
5
  /**
@@ -7,6 +7,10 @@ declare const __propDef: {
7
7
  * fields, email + password, the legal-consent row and — when the instance
8
8
  * requires one on sign-up — the captcha widget. Values live in local state so
9
9
  * a refused submit never clears them.
10
+ *
11
+ * When the app cannot send email (`settings.emailDelivery.passwordSignUp` is
12
+ * false) an email + password sign-up could never be verified, so the form is
13
+ * replaced by a "can't send email" notice (`signUp.emailUnavailable`).
10
14
  */ settings?: InstanceSettings | null;
11
15
  busy?: boolean;
12
16
  errors?: FieldError[];
package/dist/context.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { type Readable } from 'svelte/store';
2
- import { type ProviderToken, type SignOutOptions } from '@atlasauth/js';
2
+ import { type ProviderToken, type SignOutOptions, type SignOutResult } from '@atlasauth/js';
3
3
  import { type Catalog } from './i18n';
4
4
  import { type Appearance, type InstanceDesign } from './appearance';
5
5
  import type { AtlasOrganizationMembership, AtlasSession, AtlasUser, SessionClaims } from './types';
@@ -62,6 +62,14 @@ export interface AtlasClient extends AtlasStores {
62
62
  frontendApi: string;
63
63
  appearance?: Appearance;
64
64
  catalog: Catalog;
65
+ /**
66
+ * The language layer of `catalog` (the `localization` bundle, without the
67
+ * English defaults underneath; `EN_US` when none is set). Server errors are
68
+ * looked up here as `error.<reason>`, falling back to the server's message.
69
+ */
70
+ errorCatalog?: Catalog;
71
+ /** Sent to the Frontend API as `Accept-Language`: the `locale` tag of the `localization` bundle. */
72
+ locale?: string;
65
73
  /**
66
74
  * §5.3: a sign-in attempt handed back by an OAuth redirect that still needs a
67
75
  * second factor (no session was issued). Null in the ordinary case. `<SignIn>`
@@ -78,7 +86,19 @@ export interface AtlasClient extends AtlasStores {
78
86
  * exposed; the server refreshes a stale token on read.
79
87
  */
80
88
  getProviderToken(provider: string): Promise<ProviderToken | null>;
81
- signOut(options?: SignOutOptions): Promise<void>;
89
+ /**
90
+ * Sign out of the active session: revoke it on the Frontend API (retried
91
+ * once), run front-channel logout, and drop all in-memory state.
92
+ *
93
+ * Resolves `{ revoked, activeSessionId? }`. `revoked: false` means the server
94
+ * could not confirm the revoke; the user is still signed out locally and this
95
+ * browser will not re-adopt the session (a reload, a refresh, a sibling tab or
96
+ * a tab opened later), while the revoke is re-tried in the background. When
97
+ * the server promotes another session signed in on this browser
98
+ * (`activeSessionId`), the client adopts it. With `{ strict: true }` a failed
99
+ * revoke rejects with a `SignOutError` (after the local sign-out).
100
+ */
101
+ signOut(options?: SignOutOptions): Promise<SignOutResult>;
82
102
  setActiveOrganization(organizationId: string | null): Promise<void>;
83
103
  reload(): Promise<void>;
84
104
  /**
@@ -92,7 +112,10 @@ export interface AtlasClient extends AtlasStores {
92
112
  * when there is none or it cannot be read. `<AtlasProvider>` applies it.
93
113
  */
94
114
  loadInstanceDesign(): Promise<InstanceDesign | null>;
95
- /** Cancel the scheduled refresh — call from a component's `onDestroy`. */
115
+ /**
116
+ * Cancel the scheduled refresh, the sibling-tab sign-out watch and any
117
+ * background revoke re-tries — call from a component's `onDestroy`.
118
+ */
96
119
  destroy(): void;
97
120
  }
98
121
  /**
package/dist/context.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { getContext, setContext } from 'svelte';
2
2
  import { derived, get, writable } from 'svelte/store';
3
- import { FapiClient, TokenCache, getProviderToken as fetchProviderToken, msUntilRefresh, readExpiry, readRedirectResult, resolveFrontendApi, stripRedirectParams, runFrontChannelLogoutFromResponse, } from '@atlasauth/js';
3
+ import { FapiClient, SignOutGuard, TokenCache, closeOAuthPopupAfterSignIn, completeOAuthPopupLanding, getProviderToken as fetchProviderToken, mapOAuthPopupPayload, msUntilRefresh, readExpiry, readRedirectResult, resolveFrontendApi, stripRedirectParams, catalogLocale, } from '@atlasauth/js';
4
4
  import { EN_US, withFallback } from './i18n';
5
5
  import { instanceDesignFrom } from './appearance';
6
6
  /** The context key. A `Symbol` so it cannot collide with an app's own context. */
@@ -29,6 +29,8 @@ export function createAtlasClient(options) {
29
29
  });
30
30
  const doFetch = options.fetchImpl ?? (typeof fetch !== 'undefined' ? fetch : undefined);
31
31
  const catalog = options.localization ? withFallback(options.localization) : EN_US;
32
+ const errorCatalog = options.localization ?? EN_US;
33
+ const locale = catalogLocale(options.localization);
32
34
  const status = writable('loading');
33
35
  const user = writable(null);
34
36
  const session = writable(null);
@@ -65,7 +67,25 @@ export function createAtlasClient(options) {
65
67
  },
66
68
  });
67
69
  }
70
+ /**
71
+ * The sessions this browser signed out of (tombstones, shared by every tab via
72
+ * localStorage per Frontend API). Nothing here re-adopts one — not a reload, a
73
+ * refresh, nor a tab opened later — even while the refresh cookie the revoke
74
+ * did not clear is still valid; the revoke is re-tried in the background.
75
+ */
76
+ const guard = new SignOutGuard({ request: call, frontendApi: baseUrl });
77
+ // Another tab signed out of THIS session (its tombstone reached the shared
78
+ // store): drop the token and re-check, which refuses the session.
79
+ const stopWatch = typeof window !== 'undefined'
80
+ ? guard.watch(() => cache.peek()?.sessionId ?? get(session)?.id, () => {
81
+ cache.clear();
82
+ void reload();
83
+ })
84
+ : () => undefined;
68
85
  function applyToken(jwt, sessionId) {
86
+ // An in-flight refresh must not resurrect a session this browser signed out of.
87
+ if (guard.isSignedOut(sessionId))
88
+ return;
69
89
  const expiresAt = readExpiry(jwt);
70
90
  if (!expiresAt)
71
91
  return;
@@ -99,7 +119,9 @@ export function createAtlasClient(options) {
99
119
  if (!response.ok)
100
120
  throw new Error('boot failed');
101
121
  const body = (await response.json());
102
- if (!body.session || !body.user) {
122
+ // A session this browser signed out of (the revoke never landed) is
123
+ // refused: stay signed out; the guard re-tries the revoke in the background.
124
+ if (!body.session || !body.user || guard.refuse(body.session.id)) {
103
125
  clearSession();
104
126
  return;
105
127
  }
@@ -148,25 +170,26 @@ export function createAtlasClient(options) {
148
170
  await refresh();
149
171
  return cache.get(5_000)?.jwt ?? null;
150
172
  }
151
- const fapiClient = new FapiClient({ publishableKey, baseUrl, fetchImpl: doFetch });
173
+ const fapiClient = new FapiClient({ publishableKey, baseUrl, fetchImpl: doFetch, locale });
152
174
  function getProviderToken(provider) {
153
175
  return fetchProviderToken(fapiClient, provider);
154
176
  }
155
177
  async function signOut(options) {
156
- const current = cache.peek();
157
- if (current) {
158
- const revoked = await call(`/v1/client/sessions/${current.sessionId}/revoke`, {
159
- method: 'POST',
160
- }).catch(() => undefined);
161
- // OIDC Front-Channel Logout: let the apps signed in through this session
162
- // clear theirs in this browser (hidden iframes, capped at 5 s). Best-effort.
163
- if (options?.frontchannel !== false)
164
- await runFrontChannelLogoutFromResponse(revoked);
165
- }
178
+ // The session to end: from the token cache, else the booted session, else
179
+ // (no state yet) whatever session the cookies carry — the guard looks it up,
180
+ // so a missing in-memory token never skips the server-side sign-out. It
181
+ // tombstones the session first, revokes it (retried once) and runs OIDC
182
+ // front-channel logout.
183
+ const attempt = await guard.revoke(cache.peek()?.sessionId ?? get(session)?.id ?? null, options);
166
184
  if (timer)
167
185
  clearTimeout(timer);
168
186
  timer = null;
169
187
  clearSession();
188
+ // The server promoted another session signed in on this browser: adopt it
189
+ // deliberately (the revoke response already set its cookies).
190
+ if (guard.promoted(attempt))
191
+ await reload();
192
+ return guard.settle(attempt, options);
170
193
  }
171
194
  async function setActiveOrganization(organizationId) {
172
195
  const current = cache.peek();
@@ -194,18 +217,44 @@ export function createAtlasClient(options) {
194
217
  * cookies, scrub the URL so a reload can't replay it, THEN boot. A redirect
195
218
  * that only carries a `__atlas_status` (a second factor is still owed) has no
196
219
  * ticket, so nothing is exchanged and the app boots signed-out.
220
+ *
221
+ * §oauth popup mode: this page may also be running INSIDE the sign-in popup.
222
+ * - As the landing of a popup whose opener was severed by a
223
+ * Cross-Origin-Opener-Policy (`#atlas_popup_result=…`): relay the result
224
+ * to the opener over BroadcastChannel and close. If no opener takes it
225
+ * (its tab is gone), finish the sign-in here like a redirect.
226
+ * - After finishing a sign-in here (the redirect flow ran in the popup):
227
+ * tell the waiting opener and close the popup.
197
228
  */
198
- const result = typeof window !== 'undefined' ? readRedirectResult(window.location.search) : null;
229
+ const fromUrl = typeof window !== 'undefined' ? readRedirectResult(window.location.search) : null;
230
+ let result = fromUrl;
231
+ const landing = typeof window !== 'undefined' ? await completeOAuthPopupLanding().catch(() => null) : null;
232
+ if (landing && !landing.delivered) {
233
+ const outcome = mapOAuthPopupPayload(landing.payload);
234
+ if (outcome?.ticket && outcome.attemptId) {
235
+ result = { attemptId: outcome.attemptId, ticket: outcome.ticket };
236
+ }
237
+ else if (outcome?.continuationUrl) {
238
+ // A second factor / connect / provider error: the redirect flow's page.
239
+ window.location.replace(outcome.continuationUrl);
240
+ return;
241
+ }
242
+ }
243
+ let exchanged = false;
199
244
  if (result?.ticket) {
200
- await call('/v1/client/tickets/exchange', {
245
+ const response = await call('/v1/client/tickets/exchange', {
201
246
  method: 'POST',
202
247
  body: JSON.stringify({ attempt_id: result.attemptId, ticket: result.ticket }),
203
248
  }).catch(() => undefined);
249
+ exchanged = !!response?.ok;
204
250
  }
205
- if (result && typeof window !== 'undefined') {
251
+ if (fromUrl && typeof window !== 'undefined') {
206
252
  window.history.replaceState({}, document.title, stripRedirectParams(window.location.href));
207
253
  }
208
254
  await reload();
255
+ // Signed in inside the Atlas popup: hand back to the opener and close.
256
+ if (exchanged)
257
+ void closeOAuthPopupAfterSignIn();
209
258
  }
210
259
  async function loadInstanceDesign() {
211
260
  try {
@@ -220,6 +269,8 @@ export function createAtlasClient(options) {
220
269
  if (timer)
221
270
  clearTimeout(timer);
222
271
  timer = null;
272
+ stopWatch();
273
+ guard.dispose();
223
274
  }
224
275
  const isSignedIn = derived(status, ($status) => $status === 'signed_in');
225
276
  const isLoaded = derived(status, ($status) => $status !== 'loading');
@@ -228,6 +279,8 @@ export function createAtlasClient(options) {
228
279
  frontendApi: baseUrl,
229
280
  appearance: options.appearance,
230
281
  catalog,
282
+ errorCatalog,
283
+ locale,
231
284
  pendingAttempt,
232
285
  status: { subscribe: status.subscribe },
233
286
  user: { subscribe: user.subscribe },
package/dist/i18n.js CHANGED
@@ -9,6 +9,7 @@
9
9
  * A missing key renders the KEY, not an empty string. A blank button is a bug
10
10
  * nobody can report; a button reading `signIn.submit` is one anyone can.
11
11
  */
12
+ import { EMAIL_UNAVAILABLE_MESSAGE } from '@atlasauth/js';
12
13
  export const EN_US = {
13
14
  'signIn.title': 'Sign in',
14
15
  'signIn.subtitle': 'to continue to {application}',
@@ -21,6 +22,8 @@ export const EN_US = {
21
22
  'signIn.orDivider': 'or',
22
23
  'signIn.socialButton': 'Continue with {provider}',
23
24
  'signIn.emailCodeLabel': 'Verification code',
25
+ 'signIn.emailCodeOption': 'Email me a code',
26
+ 'signIn.emailLinkOption': 'Email me a sign-in link',
24
27
  'signIn.magicLinkSent': 'Check your email for a sign-in link.',
25
28
  'signIn.checkOtherTab': 'You can close this tab — you are signed in where you started.',
26
29
  'signUp.title': 'Create your account',
@@ -36,6 +39,8 @@ export const EN_US = {
36
39
  'signUp.terms': 'Terms of Service',
37
40
  'signUp.privacy': 'Privacy Policy',
38
41
  'signUp.and': 'and',
42
+ // Shown instead of the email + password form when the app cannot send email.
43
+ 'signUp.emailUnavailable': EMAIL_UNAVAILABLE_MESSAGE,
39
44
  'captcha.prompt': 'Complete the security check to continue.',
40
45
  'captcha.required': 'Please complete the security check, then try again.',
41
46
  'captcha.failed': "The security check didn't pass. Please try again.",
@@ -71,6 +76,46 @@ export const EN_US = {
71
76
  'organization.roleLabel': 'Role',
72
77
  'error.generic': 'Something went wrong. Please try again.',
73
78
  'error.network': 'We could not reach the server. Check your connection.',
79
+ // Server errors, by the stable `reason` the Frontend API sends. Shown instead
80
+ // of the server's `message` (see `localizedErrorMessage` in @atlasauth/js).
81
+ 'error.form_password_incorrect': 'Those credentials are incorrect.',
82
+ 'error.form_code_incorrect': 'That code is incorrect.',
83
+ 'error.form_code_already_used': 'That code has already been used. Wait for a new one.',
84
+ 'error.form_code_expired': 'That code has expired. Request a new one.',
85
+ 'error.form_code_max_attempts': 'Too many incorrect codes. Request a new code.',
86
+ 'error.form_link_invalid': 'That sign-in link is invalid or has already been used.',
87
+ 'error.form_identifier_not_found': "We couldn't find an account with those details.",
88
+ 'error.form_identifier_exists': "There's already an account for this email. Sign in with {methods}.",
89
+ 'error.form_password_pwned': 'This password has appeared in a known data breach. Please choose a different one.',
90
+ 'error.form_password_too_short': 'Password must be at least {min_length} characters.',
91
+ 'error.form_password_too_long': 'Password must be at most {max_length} characters.',
92
+ 'error.form_password_requires_uppercase': 'Password must contain an uppercase letter.',
93
+ 'error.form_password_requires_lowercase': 'Password must contain a lowercase letter.',
94
+ 'error.form_password_requires_number': 'Password must contain a number.',
95
+ 'error.form_password_requires_symbol': 'Password must contain a symbol.',
96
+ 'error.form_password_not_allowed': 'That password is not allowed. Choose a different one.',
97
+ 'error.form_password_contains_identifier': 'Password must not contain your email or username.',
98
+ 'error.form_password_same_as_current': 'Your new password must be different from your current one.',
99
+ 'error.form_password_reused': 'Your new password must not match any of your last {history_count} passwords.',
100
+ 'error.password_expired': 'Your password has expired. Reset it to continue.',
101
+ 'error.email_not_verified': 'Your email address is not verified yet. Verify it before signing in with your password.',
102
+ 'error.sign_in_token_invalid': 'That sign-in token is invalid or has expired.',
103
+ 'error.too_many_attempts': 'Too many failed attempts. Try again later.',
104
+ 'error.too_many_requests': 'Too many requests. Please try again later.',
105
+ 'error.captcha_required': 'Please complete the security check, then try again.',
106
+ 'error.captcha_failed': "The security check didn't pass. Please try again.",
107
+ 'error.captcha_unavailable': 'The security check is unavailable right now. Please try again later.',
108
+ 'error.email_delivery_unavailable': "This app can't send email right now. Try another sign-in method or contact the app owner.",
109
+ 'error.strategy_disabled': 'This sign-in method is not available for this app.',
110
+ 'error.sign_up_restricted': 'Sign-up on this app is by invitation only.',
111
+ 'error.waitlisted': "You're on the waitlist. We'll email you when a place opens up.",
112
+ 'error.identifier_not_allowed': 'This email or phone number is not allowed on this app.',
113
+ 'error.disposable_email_blocked': 'Sign-up with a disposable or temporary email address is not allowed.',
114
+ 'error.sso_required': 'This organization requires signing in through single sign-on.',
115
+ 'error.sign_ins_disabled': 'Sign-ins are temporarily disabled.',
116
+ 'error.sign_ups_disabled': 'Sign-ups are temporarily disabled.',
117
+ 'error.high_risk_blocked': 'This sign-in was blocked because it looked unusual.',
118
+ 'error.sms_region_blocked': 'Text messages are not available for this phone number.',
74
119
  };
75
120
  /**
76
121
  * Look up a key and interpolate `{placeholders}`.
package/dist/index.d.ts CHANGED
@@ -29,4 +29,6 @@ export { default as UserButton } from './components/UserButton.svelte';
29
29
  export { default as UserProfile } from './components/UserProfile.svelte';
30
30
  export { default as OrganizationSwitcher } from './components/OrganizationSwitcher.svelte';
31
31
  export { default as OrganizationProfile } from './components/OrganizationProfile.svelte';
32
- export type { InstanceSettings, PublicSignUpField, CaptchaController, CaptchaFlowKey, CaptchaWidgetMode, FlowCaptcha, } from '@atlasauth/js';
32
+ export type { InstanceSettings, EmailDelivery, PublicSignUpField, CaptchaController, CaptchaFlowKey, CaptchaWidgetMode, FlowCaptcha, } from '@atlasauth/js';
33
+ export { SignOutError } from '@atlasauth/js';
34
+ export type { SignOutOptions, SignOutResult } from '@atlasauth/js';
package/dist/index.js CHANGED
@@ -33,3 +33,5 @@ export { default as UserButton } from './components/UserButton.svelte';
33
33
  export { default as UserProfile } from './components/UserProfile.svelte';
34
34
  export { default as OrganizationSwitcher } from './components/OrganizationSwitcher.svelte';
35
35
  export { default as OrganizationProfile } from './components/OrganizationProfile.svelte';
36
+ // What `signOut()` resolves with, and what it rejects with under `{ strict: true }`.
37
+ export { SignOutError } from '@atlasauth/js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atlasauth/svelte",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "type": "module",
5
5
  "svelte": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -22,10 +22,10 @@
22
22
  "./package.json": "./package.json"
23
23
  },
24
24
  "dependencies": {
25
- "@atlasauth/js": "0.6.0",
26
- "@atlasauth/backend": "0.7.0",
27
- "@atlasauth/design": "0.1.0",
28
- "@atlasauth/authz": "0.1.1"
25
+ "@atlasauth/design": "0.2.0",
26
+ "@atlasauth/js": "0.9.0",
27
+ "@atlasauth/authz": "0.1.1",
28
+ "@atlasauth/backend": "0.7.2"
29
29
  },
30
30
  "peerDependencies": {
31
31
  "svelte": ">=4",