@atlasauth/svelte 0.5.1 → 0.6.1

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,24 @@
1
1
  # @atlasauth/svelte changelog
2
2
 
3
+ ## 0.6.1
4
+ - **`<UserProfile>` can verify an address.** Each unverified email address now has a **Verify** button: it emails a code (`POST /v1/client/me/email_addresses/{id}/prepare_verification`) and shows a field to enter it (`attempt_verification`), then reloads the user. This includes the sign-up address on an app that does not require email verification, which now reads unverified until it is proven. New catalog keys: `userProfile.verify`, `userProfile.verifyConfirm`, `userProfile.verifyCodePlaceholder`, `userProfile.verifySendFailed`, `userProfile.verifyCodeRejected`.
5
+
6
+ ## 0.6.0
7
+ - **New `initAtlas({ onSignOut })` and `client.onSignOut(listener)`**: `onSignOut({ reason, sessionId })` fired once each time a signed-in user becomes signed out, on every
8
+ path: `reason` is `user` (this tab), `other_tab`, `session_expired`, `revoked` (the session was ended elsewhere,
9
+ detected on the next token refresh or re-check) or `unknown`. The same event is dispatched on `window` as
10
+ `atlas:signout`. Use it to reset an embedded widget (a support chat, a docs reader session).
11
+ - **`getToken({ template })`**: a short-lived token from a named JWT template for the current session (for
12
+ example `'chatly'`, a support-chat identity token, when the instance has a Chatly connection).
13
+ - **SDK version advisories.** Every Frontend API request carries `X-Atlas-SDK: @atlasauth/svelte/<version>`. When the
14
+ response carries `Atlas-SDK-Advisory: upgrade; min=<version>; reason=<code>` (a version with a known bug), the SDK
15
+ logs one console warning per advisory per page load. The Frontend API must allow the header in CORS, which Atlas
16
+ does; a reverse proxy that forwards a fixed `Access-Control-Allow-Headers` list must add `x-atlas-sdk`.
17
+ - Requires `@atlasauth/js` 0.12.0.
18
+ - The prebuilt forms draw the **Atlas captcha** (provider `atlas`, no keys needed) through `@atlasauth/js` 0.12.0,
19
+ with its words from the form's catalog. New catalog keys: `captcha.checking`, `captcha.verified`,
20
+ `captcha.puzzlePrompt`, `captcha.puzzleConfirm`, `captcha.puzzleTile`, `captcha.puzzleAlternative`.
21
+
3
22
  ## 0.5.1
4
23
  - **Fixed: email-code sign-in on instances without password sign-in.** After the email step the sign-in form now
5
24
  sends the code itself (`prepare_first_factor`), shows "We sent a code to …" with the code field and a
package/dist/accessors.js CHANGED
@@ -1,8 +1,9 @@
1
1
  import { derived, get, writable } from 'svelte/store';
2
- import { FapiClient, advanceWithCaptcha, createCaptchaController, createSignUp, fetchInstanceSettings, flowNeedsCaptcha, signInCaptchaOptions, flowFromPending, pollAttempt, prepareFactor, shouldKeepPolling, } from '@atlasauth/js';
2
+ import { FapiClient, advanceWithCaptcha, atlasCaptchaLabels, 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';
6
+ import { ATLAS_SVELTE_SDK } from './version';
6
7
  export function getUser() {
7
8
  const atlas = useAtlas();
8
9
  return derived([atlas.status, atlas.user], ([$status, $user]) => ({
@@ -68,6 +69,7 @@ function createFlow() {
68
69
  publishableKey: atlas.publishableKey,
69
70
  baseUrl: atlas.frontendApi,
70
71
  locale: atlas.locale,
72
+ sdk: ATLAS_SVELTE_SDK,
71
73
  });
72
74
  // Seeded from a redirect that came back mid-flow (e.g. OAuth needing a second
73
75
  // factor), so the user resumes at the demanded step instead of starting over.
@@ -77,8 +79,10 @@ function createFlow() {
77
79
  // §5 the per-flow captcha, driven by the instance's settings so turning it on
78
80
  // in the dashboard needs no code change. Kept in step with the settings and
79
81
  // the flow's step synchronously, before the components re-render.
80
- const captcha = createCaptchaController();
81
- const signUpCaptcha = createCaptchaController();
82
+ // The Atlas captcha's words follow the form's catalog.
83
+ const captchaSettings = { atlasLabels: () => atlasCaptchaLabels(t) };
84
+ const captcha = createCaptchaController(undefined, captchaSettings);
85
+ const signUpCaptcha = createCaptchaController(undefined, captchaSettings);
82
86
  let identifier = '';
83
87
  const unsubscribeCaptcha = derived([settings, state], (pair) => pair).subscribe(([current, flow]) => {
84
88
  captcha.configure(signInCaptchaOptions(current?.captchaFlows.signIn, flow, () => submit({})));
@@ -2,11 +2,50 @@
2
2
  import { getUser } from "../accessors";
3
3
  import { classFor } from "../appearance";
4
4
  import { translate } from "../i18n";
5
+ import { FapiClient } from "../fapi";
6
+ import { ATLAS_SVELTE_SDK } from "../version";
5
7
  const atlas = useAtlas();
6
8
  const user = getUser();
7
9
  const t = (key) => translate(atlas.catalog, key);
10
+ const client = new FapiClient({
11
+ publishableKey: atlas.publishableKey,
12
+ baseUrl: atlas.frontendApi,
13
+ locale: atlas.locale,
14
+ sdk: ATLAS_SVELTE_SDK
15
+ });
16
+ let verifying = null;
17
+ let code = "";
18
+ let busy = false;
19
+ let verifyError = null;
20
+ async function sendCode(id) {
21
+ verifyError = null;
22
+ busy = true;
23
+ const res = await client.post(`/v1/client/me/email_addresses/${id}/prepare_verification`);
24
+ busy = false;
25
+ if (!res.ok) {
26
+ verifyError = t("userProfile.verifySendFailed");
27
+ return;
28
+ }
29
+ code = "";
30
+ verifying = id;
31
+ }
32
+ async function confirmCode() {
33
+ if (!verifying) return;
34
+ verifyError = null;
35
+ busy = true;
36
+ const res = await client.post(`/v1/client/me/email_addresses/${verifying}/attempt_verification`, { code });
37
+ busy = false;
38
+ if (!res.ok) {
39
+ verifyError = t("userProfile.verifyCodeRejected");
40
+ return;
41
+ }
42
+ verifying = null;
43
+ await atlas.reload();
44
+ }
8
45
  $: current = $user.user;
9
46
  $: cardClass = classFor("card", atlas.appearance, "atlas-card");
47
+ $: inputClass = classFor("formFieldInput", atlas.appearance, "atlas-account-input");
48
+ $: buttonClass = classFor("formButtonSecondary", atlas.appearance, "atlas-button-secondary");
10
49
  </script>
11
50
 
12
51
  {#if current}
@@ -21,10 +60,31 @@ $: cardClass = classFor("card", atlas.appearance, "atlas-card");
21
60
  {email.email_address}
22
61
  {#if !email.verified}
23
62
  <em> — {t('userProfile.unverified')}</em>
63
+ {#if verifying === email.id}
64
+ <span data-atlas="email-verify">
65
+ <input
66
+ class={inputClass}
67
+ inputmode="numeric"
68
+ autocomplete="one-time-code"
69
+ placeholder={t('userProfile.verifyCodePlaceholder')}
70
+ bind:value={code}
71
+ />
72
+ <button class={buttonClass} type="button" disabled={busy} on:click={() => void confirmCode()}>
73
+ {t('userProfile.verifyConfirm')}
74
+ </button>
75
+ </span>
76
+ {:else}
77
+ <button class={buttonClass} type="button" disabled={busy} on:click={() => void sendCode(email.id)}>
78
+ {t('userProfile.verify')}
79
+ </button>
80
+ {/if}
24
81
  {/if}
25
82
  </li>
26
83
  {/each}
27
84
  </ul>
85
+ {#if verifyError}
86
+ <p role="alert">{verifyError}</p>
87
+ {/if}
28
88
  </section>
29
89
 
30
90
  <section>
package/dist/context.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { type Readable } from 'svelte/store';
2
- import { type ProviderToken, type SignOutOptions, type SignOutResult } from '@atlasauth/js';
2
+ import { type ProviderToken, type SignOutOptions, type SignOutResult, type SignOutEvent, type SignOutListener } 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';
@@ -35,6 +35,11 @@ export interface AtlasInitOptions {
35
35
  appearance?: Appearance;
36
36
  localization?: Catalog;
37
37
  fetchImpl?: typeof fetch;
38
+ /**
39
+ * Called once each time a signed-in user becomes signed out, on every path
40
+ * (see `AtlasClient.onSignOut`).
41
+ */
42
+ onSignOut?: (event: SignOutEvent) => void;
38
43
  /**
39
44
  * Skip the automatic browser boot (redirect handling + `/v1/client` load).
40
45
  * Useful in tests, or when the caller wants to drive `boot()` by hand.
@@ -79,7 +84,16 @@ export interface AtlasClient extends AtlasStores {
79
84
  id: string;
80
85
  status: string;
81
86
  } | null;
82
- getToken(): Promise<string | null>;
87
+ /** The session JWT; with `{ template }` a short-lived named-template token (e.g. `'chatly'`). */
88
+ getToken(options?: {
89
+ template?: string;
90
+ }): Promise<string | null>;
91
+ /**
92
+ * Listen for sign-out on every path: `reason` is `user`, `other_tab`,
93
+ * `session_expired`, `revoked` (ended elsewhere, seen on the next refresh) or
94
+ * `unknown`. Returns the unsubscribe. Also dispatched as `atlas:signout` on `window`.
95
+ */
96
+ onSignOut(listener: SignOutListener): () => void;
83
97
  /**
84
98
  * §6.6 the signed-in user's OWN live token at a connected provider (Google,
85
99
  * GitHub, …). Null when there is no usable token. The refresh token is never
package/dist/context.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { getContext, setContext } from 'svelte';
2
2
  import { derived, get, writable } from 'svelte/store';
3
- import { FapiClient, SignOutGuard, TokenCache, closeOAuthPopupAfterSignIn, completeOAuthPopupLanding, getProviderToken as fetchProviderToken, mapOAuthPopupPayload, msUntilRefresh, readExpiry, readRedirectResult, resolveFrontendApi, stripRedirectParams, catalogLocale, } from '@atlasauth/js';
3
+ import { FapiClient, SignOutGuard, TokenCache, closeOAuthPopupAfterSignIn, completeOAuthPopupLanding, getProviderToken as fetchProviderToken, mapOAuthPopupPayload, msUntilRefresh, readExpiry, readRedirectResult, resolveFrontendApi, stripRedirectParams, catalogLocale, SDK_HEADER, SignOutNotifier, getTemplateToken, reportSdkAdvisory, sdkHeaderValue, signOutReasonFromClient, } from '@atlasauth/js';
4
+ import { ATLAS_SVELTE_SDK } from './version';
4
5
  import { EN_US, withFallback } from './i18n';
5
6
  import { instanceDesignFrom } from './appearance';
6
7
  /** The context key. A `Symbol` so it cannot collide with an app's own context. */
@@ -55,18 +56,26 @@ export function createAtlasClient(options) {
55
56
  async function call(path, init) {
56
57
  if (!doFetch)
57
58
  throw new Error('No fetch implementation available.');
58
- return doFetch(`${baseUrl}${path}`, {
59
+ const response = await doFetch(`${baseUrl}${path}`, {
59
60
  ...init,
60
61
  // Cookies carry the session; a fetch without this looks identical to being
61
62
  // signed out.
62
63
  credentials: 'include',
63
64
  headers: {
64
65
  'x-publishable-key': publishableKey,
66
+ // Names this SDK so the Frontend API can flag a version with a known bug.
67
+ [SDK_HEADER]: sdkHeaderValue(ATLAS_SVELTE_SDK),
65
68
  ...(init?.body ? { 'content-type': 'application/json' } : {}),
66
69
  ...init?.headers,
67
70
  },
68
71
  });
72
+ reportSdkAdvisory(response, ATLAS_SVELTE_SDK);
73
+ return response;
69
74
  }
75
+ // ATL-S-30 one sign-out notifier for the client's lifetime.
76
+ const notifier = new SignOutNotifier();
77
+ if (options.onSignOut)
78
+ notifier.subscribe(options.onSignOut);
70
79
  /**
71
80
  * The sessions this browser signed out of (tombstones, shared by every tab via
72
81
  * localStorage per Frontend API). Nothing here re-adopts one — not a reload, a
@@ -78,6 +87,7 @@ export function createAtlasClient(options) {
78
87
  // store): drop the token and re-check, which refuses the session.
79
88
  const stopWatch = typeof window !== 'undefined'
80
89
  ? guard.watch(() => cache.peek()?.sessionId ?? get(session)?.id, () => {
90
+ notifier.expect('other_tab');
81
91
  cache.clear();
82
92
  void reload();
83
93
  })
@@ -92,13 +102,19 @@ export function createAtlasClient(options) {
92
102
  cache.set({ jwt, expiresAt, sessionId });
93
103
  claims.set(decodeClaims(jwt));
94
104
  }
95
- function clearSession() {
105
+ function clearSession(reason) {
106
+ // A signed-in → signed-out transition tells the sign-out listeners why.
107
+ const ended = get(status) === 'signed_in' ? (get(session)?.id ?? cache.peek()?.sessionId ?? null) : undefined;
96
108
  cache.clear();
97
109
  status.set('signed_out');
98
110
  user.set(null);
99
111
  session.set(null);
100
112
  claims.set(null);
101
113
  memberships.set([]);
114
+ if (ended !== undefined)
115
+ notifier.signedOut(ended, reason);
116
+ else
117
+ notifier.clear();
102
118
  }
103
119
  function scheduleRefresh() {
104
120
  if (timer)
@@ -122,9 +138,10 @@ export function createAtlasClient(options) {
122
138
  // A session this browser signed out of (the revoke never landed) is
123
139
  // refused: stay signed out; the guard re-tries the revoke in the background.
124
140
  if (!body.session || !body.user || guard.refuse(body.session.id)) {
125
- clearSession();
141
+ clearSession(body.session ? 'other_tab' : signOutReasonFromClient(body));
126
142
  return;
127
143
  }
144
+ notifier.clear();
128
145
  user.set(body.user);
129
146
  session.set(body.session);
130
147
  memberships.set(body.organization_memberships ?? []);
@@ -161,7 +178,11 @@ export function createAtlasClient(options) {
161
178
  await reload();
162
179
  }
163
180
  }
164
- async function getToken() {
181
+ async function getToken(opts) {
182
+ if (opts?.template) {
183
+ const sessionId = cache.peek()?.sessionId ?? get(session)?.id ?? null;
184
+ return sessionId ? ((await getTemplateToken(fapiClient, sessionId, opts.template))?.jwt ?? null) : null;
185
+ }
165
186
  // A 5s skew so a token that would die inside the caller's request is
166
187
  // refreshed first rather than handed over.
167
188
  const usable = cache.get(5_000);
@@ -170,7 +191,7 @@ export function createAtlasClient(options) {
170
191
  await refresh();
171
192
  return cache.get(5_000)?.jwt ?? null;
172
193
  }
173
- const fapiClient = new FapiClient({ publishableKey, baseUrl, fetchImpl: doFetch, locale });
194
+ const fapiClient = new FapiClient({ publishableKey, baseUrl, fetchImpl: doFetch, locale, sdk: ATLAS_SVELTE_SDK });
174
195
  function getProviderToken(provider) {
175
196
  return fetchProviderToken(fapiClient, provider);
176
197
  }
@@ -180,11 +201,12 @@ export function createAtlasClient(options) {
180
201
  // so a missing in-memory token never skips the server-side sign-out. It
181
202
  // tombstones the session first, revokes it (retried once) and runs OIDC
182
203
  // front-channel logout.
204
+ notifier.expect('user');
183
205
  const attempt = await guard.revoke(cache.peek()?.sessionId ?? get(session)?.id ?? null, options);
184
206
  if (timer)
185
207
  clearTimeout(timer);
186
208
  timer = null;
187
- clearSession();
209
+ clearSession('user');
188
210
  // The server promoted another session signed in on this browser: adopt it
189
211
  // deliberately (the revoke response already set its cookies).
190
212
  if (guard.promoted(attempt))
@@ -290,6 +312,7 @@ export function createAtlasClient(options) {
290
312
  isSignedIn,
291
313
  isLoaded,
292
314
  getToken,
315
+ onSignOut: (listener) => notifier.subscribe(listener),
293
316
  getProviderToken,
294
317
  signOut,
295
318
  setActiveOrganization,
package/dist/i18n.js CHANGED
@@ -50,6 +50,12 @@ export const EN_US = {
50
50
  'captcha.prompt': 'Complete the security check to continue.',
51
51
  'captcha.required': 'Please complete the security check, then try again.',
52
52
  'captcha.failed': "The security check didn't pass. Please try again.",
53
+ 'captcha.checking': 'Checking your browser…',
54
+ 'captcha.verified': 'Security check complete.',
55
+ 'captcha.puzzlePrompt': "Select every picture that shows the same shape as this one. Colour and size don't matter.",
56
+ 'captcha.puzzleConfirm': 'Confirm',
57
+ 'captcha.puzzleTile': 'Picture {n}',
58
+ 'captcha.puzzleAlternative': "Can't do the picture check? Use a longer automatic check instead.",
53
59
  'mfa.title': 'Two-step verification',
54
60
  'mfa.subtitle': 'Enter the code from your authenticator app.',
55
61
  'mfa.codeLabel': 'Code',
@@ -73,6 +79,11 @@ export const EN_US = {
73
79
  'userProfile.addEmail': 'Add an email address',
74
80
  'userProfile.makePrimary': 'Make primary',
75
81
  'userProfile.unverified': 'Unverified',
82
+ 'userProfile.verify': 'Verify',
83
+ 'userProfile.verifyConfirm': 'Confirm',
84
+ 'userProfile.verifyCodePlaceholder': 'Code from the email',
85
+ 'userProfile.verifySendFailed': 'We could not send a code right now. Try again in a minute.',
86
+ 'userProfile.verifyCodeRejected': 'That code was not accepted.',
76
87
  'userProfile.revokeDevice': 'Sign out',
77
88
  'userProfile.deleteAccount': 'Delete account',
78
89
  'organization.switcher': 'Switch organization',
package/dist/index.d.ts CHANGED
@@ -32,3 +32,6 @@ export { default as OrganizationProfile } from './components/OrganizationProfile
32
32
  export type { InstanceSettings, EmailDelivery, PublicSignUpField, CaptchaController, CaptchaFlowKey, CaptchaWidgetMode, FlowCaptcha, } from '@atlasauth/js';
33
33
  export { SignOutError } from '@atlasauth/js';
34
34
  export type { SignOutOptions, SignOutResult } from '@atlasauth/js';
35
+ export { SIGN_OUT_EVENT, onAtlasSignOut } from '@atlasauth/js';
36
+ export type { SignOutEvent, SignOutReason } from '@atlasauth/js';
37
+ export { ATLAS_SVELTE_SDK, ATLAS_SVELTE_VERSION } from './version';
package/dist/index.js CHANGED
@@ -35,3 +35,6 @@ export { default as OrganizationSwitcher } from './components/OrganizationSwitch
35
35
  export { default as OrganizationProfile } from './components/OrganizationProfile.svelte';
36
36
  // What `signOut()` resolves with, and what it rejects with under `{ strict: true }`.
37
37
  export { SignOutError } from '@atlasauth/js';
38
+ // `onSignOut({ reason })` event types + the `atlas:signout` window event; this SDK's version.
39
+ export { SIGN_OUT_EVENT, onAtlasSignOut } from '@atlasauth/js';
40
+ export { ATLAS_SVELTE_SDK, ATLAS_SVELTE_VERSION } from './version';
@@ -0,0 +1,4 @@
1
+ import type { AtlasSdkInfo } from '@atlasauth/js';
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.6.1";
4
+ export declare const ATLAS_SVELTE_SDK: AtlasSdkInfo;
@@ -0,0 +1,3 @@
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.6.1';
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.5.1",
3
+ "version": "0.6.1",
4
4
  "type": "module",
5
5
  "svelte": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -23,9 +23,9 @@
23
23
  },
24
24
  "dependencies": {
25
25
  "@atlasauth/design": "0.2.0",
26
+ "@atlasauth/js": "0.12.0",
26
27
  "@atlasauth/authz": "0.1.1",
27
- "@atlasauth/backend": "0.7.3",
28
- "@atlasauth/js": "0.10.0"
28
+ "@atlasauth/backend": "0.9.0"
29
29
  },
30
30
  "peerDependencies": {
31
31
  "svelte": ">=4",