@atlasauth/svelte 0.15.1 → 0.16.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,9 @@
1
1
  # @atlasauth/svelte changelog
2
2
 
3
+ ## 0.16.0 — 2026-11-06
4
+
5
+ - Uses `@atlasauth/js` 0.34.0. Emailed links now need the browser that started the flow or the code shown on the starting tab; the components show the code, ask for it on the link page, and offer every factor the instance has on at enrolment.
6
+
3
7
  ## 0.15.1 — 2026-11-05
4
8
 
5
9
  - Uses `@atlasauth/js` 0.33.1: a sign-up polls only while it waits for a proof, so a poll can't collect a ticket past the recovery codes.
package/dist/accessors.js CHANGED
@@ -137,8 +137,11 @@ function createFlow(options = {}) {
137
137
  return;
138
138
  pollTimer = setInterval(() => {
139
139
  void (async () => {
140
- const { state: next, ticket } = await pollAttempt(client, get(state));
141
- state.set(next);
140
+ const polled = get(state);
141
+ const { state: next, ticket } = await pollAttempt(client, polled);
142
+ // A resend rotated the secret meanwhile: this answer is stale.
143
+ if (get(state).pollSecret === polled.pollSecret)
144
+ state.set(next);
142
145
  if (!shouldKeepPolling(next))
143
146
  stopPolling();
144
147
  if (!ticket || !next.attempt)
@@ -1,5 +1,6 @@
1
1
  <script>import {
2
2
  awaitingSignUpLink,
3
+ enrollmentOptionKey,
3
4
  enrollmentSubmitKey,
4
5
  groupSetupKey,
5
6
  localizedErrorMessage,
@@ -25,6 +26,7 @@ export let onEmailSecondFactor = null;
25
26
  export let onSmsSecondFactor = null;
26
27
  export let enrollment = null;
27
28
  export let enrollmentState = null;
29
+ export let matchCode = null;
28
30
  export let emailVerification = "code";
29
31
  export let onResend = null;
30
32
  export let onPolicyDecline = null;
@@ -214,6 +216,9 @@ function handleSubmit(event) {
214
216
  </button>
215
217
  {/if}
216
218
  {/if}
219
+ {#if matchCode && (step.kind === 'collect_email_code' || (step.kind === 'collect_first_factor' && sent === 'email_link'))}
220
+ <p class="atlas-text" data-atlas="link-match-code">{t('link.matchCodeHint')} <strong><code>{matchCode}</code></strong></p>
221
+ {/if}
217
222
  {#if enrolling && enrollmentState}
218
223
  {#if enrollmentState.method === null}
219
224
  <p class="atlas-text">{t('mfa.enrollChoose')}</p>
@@ -225,10 +230,29 @@ function handleSubmit(event) {
225
230
  disabled={busy || enrollmentState.busy}
226
231
  on:click={() => chooseMethod(m)}
227
232
  >
228
- {t(m === 'totp' ? 'mfa.enrollTotpOption' : 'mfa.enrollSmsOption')}
233
+ {t(enrollmentOptionKey(m))}
229
234
  </button>
230
235
  {/each}
231
- {:else if enrollmentState.method === 'totp'}
236
+ {:else if enrollmentState.method === 'yubikey'}
237
+ <label class="atlas-field">
238
+ <span>{t('mfa.yubikeyLabel')}</span>
239
+ <input class={cls('formFieldInput', 'atlas-input')} type="text" name="otp" autocomplete="off" value={values.otp ?? ''} on:input={(e) => (values.otp = e.currentTarget.value)} />
240
+ </label>
241
+ {#each errors.filter((e) => e.param === 'otp') as error (error.code)}
242
+ <span class={cls('formFieldError', 'atlas-error')}>{errorText(error)}</span>
243
+ {/each}
244
+ {:else if enrollmentState.method === 'duo' || enrollmentState.method === 'passkey'}
245
+ {#if !enrollmentState.enrollment}
246
+ <p class="atlas-text" aria-live="polite">{enrollmentState.busy ? t('mfa.enrollPreparing') : ''}</p>
247
+ {:else if enrollmentState.method === 'passkey'}
248
+ <p class="atlas-text">{t('mfa.enrollPasskeyHelp')}</p>
249
+ {:else}
250
+ <p class="atlas-text">{t('mfa.enrollDuoHelp')}</p>
251
+ {#if enrollmentState.enrollment.authorizationUrl}
252
+ <a class="atlas-link" data-atlas="mfa-duo-open" href={enrollmentState.enrollment.authorizationUrl} target="_blank" rel="noopener noreferrer">{t('mfa.enrollDuoOpen')}</a>
253
+ {/if}
254
+ {/if}
255
+ {:else if enrollmentState.method === 'totp' || enrollmentState.method === 'hotp'}
232
256
  {#if !enrollmentState.enrollment}
233
257
  <p class="atlas-text" aria-live="polite">{enrollmentState.busy ? t('mfa.enrollPreparing') : ''}</p>
234
258
  {:else}
@@ -245,13 +269,13 @@ function handleSubmit(event) {
245
269
  <span>{t('mfa.enrollFirstCodeLabel')}</span>
246
270
  <input class={cls('formFieldInput', 'atlas-input')} type="text" name="code" autocomplete="one-time-code" inputmode="numeric" value={values.code ?? ''} on:input={(e) => (values.code = e.currentTarget.value)} />
247
271
  </label>
248
- {#if enrollmentState.totpCodes >= 2}
272
+ {#if enrollmentState.method === 'totp' && enrollmentState.totpCodes >= 2}
249
273
  <label class="atlas-field">
250
274
  <span>{t('mfa.enrollSecondCodeLabel')}</span>
251
275
  <input class={cls('formFieldInput', 'atlas-input')} type="text" name="secondCode" autocomplete="one-time-code" inputmode="numeric" value={values.secondCode ?? ''} on:input={(e) => (values.secondCode = e.currentTarget.value)} />
252
276
  </label>
253
277
  {/if}
254
- {#each errors.filter((e) => e.param === 'codes') as error (error.code)}
278
+ {#each errors.filter((e) => e.param === 'codes' || e.param === 'code') as error (error.code)}
255
279
  <span class={cls('formFieldError', 'atlas-error')}>{errorText(error)}</span>
256
280
  {/each}
257
281
  {/if}
@@ -291,7 +315,7 @@ function handleSubmit(event) {
291
315
  disabled={busy || enrollmentState.busy}
292
316
  on:click={() => chooseMethod(other)}
293
317
  >
294
- {t(other === 'totp' ? 'mfa.enrollTotpOption' : 'mfa.enrollSmsOption')}
318
+ {t(enrollmentOptionKey(other))}
295
319
  </button>
296
320
  {/each}
297
321
  {/if}
@@ -48,6 +48,7 @@ declare const __propDef: {
48
48
  * asks for two authenticator codes only.
49
49
  */ enrollment?: MfaEnrollmentController | null;
50
50
  enrollmentState?: MfaEnrollmentState | null;
51
+ /** `FlowState.matchCode`: shown while a mailed link waits, to type if it's opened on another device. */ matchCode?: string | null;
51
52
  /**
52
53
  * The instance's `account.emailVerification`. On `link` a sign-up's
53
54
  * verification step has nothing to type: it says to open the emailed link
@@ -0,0 +1,47 @@
1
+ <script>import { onMount } from "svelte";
2
+ import { FapiClient, createEmailLinkConfirmation, localizedErrorMessage } from "@atlasauth/js";
3
+ import { useAtlas } from "../context";
4
+ import { classFor } from "../appearance";
5
+ import { translate } from "../i18n";
6
+ import { ATLAS_SVELTE_SDK } from "../version";
7
+ export let link;
8
+ const atlas = useAtlas();
9
+ const cls = (key, base) => classFor(key, atlas.appearance, base);
10
+ const t = (key) => translate(atlas.catalog, key);
11
+ const errorText = (error) => localizedErrorMessage(error, atlas.errorCatalog ?? atlas.catalog, atlas.catalog);
12
+ const controller = createEmailLinkConfirmation(
13
+ new FapiClient({ publishableKey: atlas.publishableKey, baseUrl: atlas.frontendApi, locale: atlas.locale, sdk: ATLAS_SVELTE_SDK }),
14
+ link
15
+ );
16
+ let state = controller.state;
17
+ let code = "";
18
+ onMount(() => {
19
+ const stop = controller.subscribe((next) => state = next);
20
+ void controller.start();
21
+ return stop;
22
+ });
23
+ </script>
24
+
25
+ <div class={cls('card', 'atlas-card')} data-atlas="email-link" data-atlas-status={state.status}>
26
+ <h1 class={cls('headerTitle', 'atlas-title')}>{t('link.title')}</h1>
27
+ {#if state.status === 'needs_code'}
28
+ <form on:submit|preventDefault={() => void controller.submitCode(code)}>
29
+ <p class="atlas-text">{t('link.enterCodeHelp')}</p>
30
+ <label class="atlas-field">
31
+ <span>{t('link.codeLabel')}</span>
32
+ <input class={cls('formFieldInput', 'atlas-input')} type="text" name="match_code" autocomplete="off" inputmode="numeric" required bind:value={code} />
33
+ </label>
34
+ {#each state.errors as error (error.code)}
35
+ <p class={cls('formFieldError', 'atlas-error')} role="alert">{errorText(error)}</p>
36
+ {/each}
37
+ <button class={cls('formButtonPrimary', 'atlas-button')} type="submit" disabled={state.busy}>{t('link.confirm')}</button>
38
+ </form>
39
+ {:else}
40
+ <p class="atlas-text" role="status" aria-live="polite">
41
+ {t(state.status === 'confirmed' ? 'link.confirmed' : state.status === 'invalid' ? 'link.invalid' : 'link.checking')}
42
+ </p>
43
+ {/if}
44
+ {#if state.resetUrl}
45
+ <p class="atlas-text"><a class="atlas-link" data-atlas-reset-link="" href={state.resetUrl}>{t('signIn.forgotPassword')}</a></p>
46
+ {/if}
47
+ </div>
@@ -0,0 +1,23 @@
1
+ import { SvelteComponent } from "svelte";
2
+ import { type EmailLink } from '@atlasauth/js';
3
+ declare const __propDef: {
4
+ props: {
5
+ /**
6
+ * What `<SignIn>` / `<SignUp>` draw when opened from a mailed link: tries this
7
+ * browser's stored secret, then asks for the code the starting tab shows. A
8
+ * success signs nobody in here: the starting tab continues on its own.
9
+ */ link: EmailLink;
10
+ };
11
+ events: {
12
+ [evt: string]: CustomEvent<any>;
13
+ };
14
+ slots: {};
15
+ exports?: {} | undefined;
16
+ bindings?: string | undefined;
17
+ };
18
+ export type EmailLinkPageProps = typeof __propDef.props;
19
+ export type EmailLinkPageEvents = typeof __propDef.events;
20
+ export type EmailLinkPageSlots = typeof __propDef.slots;
21
+ export default class EmailLinkPage extends SvelteComponent<EmailLinkPageProps, EmailLinkPageEvents, EmailLinkPageSlots> {
22
+ }
23
+ export {};
@@ -1,10 +1,12 @@
1
1
  <script>import { onDestroy, onMount } from "svelte";
2
2
  import { lastUsedMethodLabel, passkeysSupported, pendingRecoveryCodes, signUp as hostedSignUp } from "@atlasauth/js";
3
+ import { emailLinkFromUrl } from "@atlasauth/js";
3
4
  import { signIn } from "../accessors";
4
5
  import { useAtlas } from "../context";
5
6
  import { classFor } from "../appearance";
6
7
  import { translate } from "../i18n";
7
8
  import AttemptFlow from "./AttemptFlow.svelte";
9
+ import EmailLinkPage from "./EmailLinkPage.svelte";
8
10
  import RecoveryCodes from "./RecoveryCodes.svelte";
9
11
  export let afterUrl = void 0;
10
12
  export let signUpUrl = void 0;
@@ -52,8 +54,15 @@ function submitIdentifier(values) {
52
54
  flow.passkey.stopAutofill();
53
55
  flow.submit(values);
54
56
  }
57
+ let emailLink = null;
58
+ onMount(() => {
59
+ emailLink = emailLinkFromUrl(void 0, "sign_in");
60
+ });
55
61
  </script>
56
62
 
63
+ {#if emailLink}
64
+ <EmailLinkPage link={emailLink} />
65
+ {:else}
57
66
  {#if lastLabel}
58
67
  <p class={classFor('badge', atlas.appearance, 'atlas-text atlas-last-used-hint')} data-atlas="last-used" data-atlas-method={$lastMethod}>
59
68
  {t('signIn.lastUsed')}: {lastLabel}
@@ -85,6 +94,7 @@ function submitIdentifier(values) {
85
94
  titleKey="signIn.title"
86
95
  onSubmit={submitIdentifier}
87
96
  onPrepare={flow.prepare}
97
+ matchCode={$flowState.matchCode ?? null}
88
98
  passwordHold={$flowState.passwordHold ?? null}
89
99
  emailVerification={$settings?.account.emailVerification ?? 'code'}
90
100
  onEmailSecondFactor={$settings?.account.emailSecondFactor ? flow.sendEmailSecondFactor : null}
@@ -114,3 +124,4 @@ function submitIdentifier(values) {
114
124
  {t('signIn.noAccount')} <a class="atlas-link" href={promptHref}>{t('signIn.signUpLink')}</a>
115
125
  </p>
116
126
  {/if}
127
+ {/if}
@@ -1,10 +1,12 @@
1
1
  <script>import { onDestroy, onMount } from "svelte";
2
2
  import { awaitingPasskey, existingAccountResetUrl, passkeysSupported, pendingExistingAccountNotice, pendingRecoveryCodes, signInAfterProofUrl, signUpProvedByLink } from "@atlasauth/js";
3
+ import { emailLinkFromUrl } from "@atlasauth/js";
3
4
  import { signUp } from "../accessors";
4
5
  import { classFor } from "../appearance";
5
6
  import { useAtlas } from "../context";
6
7
  import { translate } from "../i18n";
7
8
  import AttemptFlow from "./AttemptFlow.svelte";
9
+ import EmailLinkPage from "./EmailLinkPage.svelte";
8
10
  import PasskeySignUpStep from "./PasskeySignUpStep.svelte";
9
11
  import RecoveryCodes from "./RecoveryCodes.svelte";
10
12
  import SignUpForm from "./SignUpForm.svelte";
@@ -32,8 +34,15 @@ $: recoveryCodes = pendingRecoveryCodes($flowState);
32
34
  function switchMode() {
33
35
  mode = usingPasskey ? "password" : "passkey";
34
36
  }
37
+ let emailLink = null;
38
+ onMount(() => {
39
+ emailLink = emailLinkFromUrl(void 0, "sign_up");
40
+ });
35
41
  </script>
36
42
 
43
+ {#if emailLink}
44
+ <EmailLinkPage link={emailLink} />
45
+ {:else}
37
46
  {#if recoveryCodes}
38
47
  <RecoveryCodes codes={recoveryCodes} busy={$flowState.busy} onSaved={flow.saveRecoveryCodes} />
39
48
  {:else if pendingExistingAccountNotice($flowState)}
@@ -70,6 +79,7 @@ function switchMode() {
70
79
  onSubmit={flow.submit}
71
80
  emailVerification={$settings?.account.emailVerification ?? 'code'}
72
81
  onResend={flow.resendSignUp}
82
+ matchCode={$flowState.matchCode ?? null}
73
83
  onEmailSecondFactor={$settings?.account.emailSecondFactor ? flow.sendEmailSecondFactor : null}
74
84
  onSmsSecondFactor={$settings && flow.mfaChoices().sms ? flow.sendSmsSecondFactor : null}
75
85
  enrollment={flow.enrollment}
@@ -88,3 +98,4 @@ function switchMode() {
88
98
  onSwitchMode={canPasskey ? switchMode : undefined}
89
99
  />
90
100
  {/if}
101
+ {/if}
package/dist/i18n.js CHANGED
@@ -83,6 +83,23 @@ export const EN_US = {
83
83
  'mfa.enrollChoose': 'Set up a second step for signing in.',
84
84
  'mfa.enrollTotpOption': 'Use an authenticator app',
85
85
  'mfa.enrollSmsOption': 'Use text messages',
86
+ 'link.title': "Confirm it's you",
87
+ 'link.matchCodeHint': 'Opening the link on another device? Enter this code there.',
88
+ 'link.checking': 'Checking your link…',
89
+ 'link.enterCodeHelp': 'Enter the code shown on the device where you started.',
90
+ 'link.codeLabel': 'Code',
91
+ 'link.confirm': 'Confirm',
92
+ 'link.confirmed': 'Confirmed. You can close this tab and go back to where you started.',
93
+ 'link.invalid': 'This link is no longer valid. Go back to where you started and ask for a new one.',
94
+ 'mfa.enrollHotpOption': 'Use a counter-based code generator',
95
+ 'mfa.enrollYubikeyOption': 'Use a YubiKey',
96
+ 'mfa.enrollDuoOption': 'Use Duo',
97
+ 'mfa.enrollPasskeyOption': 'Use a passkey',
98
+ 'mfa.yubikeyLabel': 'Touch your YubiKey',
99
+ 'mfa.enrollDuoHelp': 'Open Duo and approve the request, then come back and continue.',
100
+ 'mfa.enrollDuoOpen': 'Open Duo',
101
+ 'mfa.enrollPasskeyHelp': 'Create a passkey on this device to use as your second step. It must ask for your PIN or biometrics.',
102
+ 'mfa.enrollPasskeyCreate': 'Create passkey',
86
103
  'mfa.enrollScan': 'Scan this QR code with your authenticator app, or enter the setup key.',
87
104
  'mfa.enrollQrAlt': 'QR code for your authenticator app',
88
105
  'mfa.enrollSetupKey': 'Setup key',
@@ -181,6 +198,7 @@ export const EN_US = {
181
198
  'error.form_password_reused': 'Your new password must not match any of your last {history_count} passwords.',
182
199
  'error.password_expired': 'Your password has expired. Reset it to continue.',
183
200
  'error.email_not_verified': 'Your email address is not verified yet. Verify it before signing in with your password.',
201
+ 'error.mfa_setup_unavailable': "This account needs 2-Step Verification, but it can't be set up here. Contact the app's support to continue.",
184
202
  'error.sign_in_token_invalid': 'That sign-in token is invalid or has expired.',
185
203
  'error.too_many_attempts': 'Too many failed attempts. Try again later.',
186
204
  'error.too_many_requests': 'Too many requests. Please try again later.',
package/dist/version.d.ts CHANGED
@@ -1,4 +1,4 @@
1
1
  import type { AtlasSdkInfo } from '@atlasauth/js';
2
2
  /** This package's version, sent as `X-Atlas-SDK` (kept in step with package.json by a test). */
3
- export declare const ATLAS_SVELTE_VERSION = "0.15.1";
3
+ export declare const ATLAS_SVELTE_VERSION = "0.16.0";
4
4
  export declare const ATLAS_SVELTE_SDK: AtlasSdkInfo;
package/dist/version.js CHANGED
@@ -1,3 +1,3 @@
1
1
  /** This package's version, sent as `X-Atlas-SDK` (kept in step with package.json by a test). */
2
- export const ATLAS_SVELTE_VERSION = '0.15.1';
2
+ export const ATLAS_SVELTE_VERSION = '0.16.0';
3
3
  export const ATLAS_SVELTE_SDK = { name: '@atlasauth/svelte', version: ATLAS_SVELTE_VERSION };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atlasauth/svelte",
3
- "version": "0.15.1",
3
+ "version": "0.16.0",
4
4
  "type": "module",
5
5
  "svelte": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -23,8 +23,8 @@
23
23
  },
24
24
  "dependencies": {
25
25
  "@atlasauth/design": "0.3.0",
26
- "@atlasauth/js": "0.33.1",
27
26
  "@atlasauth/authz": "0.3.0",
27
+ "@atlasauth/js": "0.34.0",
28
28
  "@atlasauth/backend": "0.39.0"
29
29
  },
30
30
  "peerDependencies": {