@seamless-auth/svelte 0.1.0 → 0.2.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,22 @@
1
1
  # @seamless-auth/svelte
2
2
 
3
+ ## 0.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 59802e2: Add a confirmation step for magic links opened on another device.
8
+
9
+ A magic link opened on a device other than the one that asked for it now waits for the reader to approve it (auth API 0.19.0 and later). The bundled verify screens show which kind of device asked and when, and offer "Yes, sign in that device" or "No, this wasn't me". Approving signs in the requesting device and tells the reader to return to it; declining changes nothing.
10
+
11
+ `@seamless-auth/client` adds `client.confirmMagicLink(token)`, the `getMagicLinkConfirmation`, `isMagicLinkConfirmationRequired`, `describeMagicLinkDevice`, `magicLinkConfirmationPrompt` and `confirmMagicLinkSignIn` helpers, and the `MagicLinkConfirmation` and `MagicLinkDeviceClass` types. In bearer mode the verify call now sends the sign-in's ephemeral token when one is held. A custom verify screen that treats every failure as an invalid link should check `getMagicLinkConfirmation(error)` first.
12
+
13
+ The React Native README describes the new step for apps that handle magic links.
14
+
15
+ ### Patch Changes
16
+
17
+ - Updated dependencies [59802e2]
18
+ - @seamless-auth/client@0.6.0
19
+
3
20
  ## 0.1.0
4
21
 
5
22
  ### Minor Changes
package/README.md CHANGED
@@ -203,8 +203,8 @@ The screens never import a router. They navigate through an `AuthNavigator` set
203
203
  ## Headless use
204
204
 
205
205
  Every screen is a thin view over a flow in `@seamless-auth/client` (`beginSignIn`,
206
- `registerWithEmail`, `verifyOtp`, `watchMagicLink`, `finishMagicLinkSignIn`, `startOAuthSignIn`,
207
- `completeOAuthCallback`, `enrollPasskey`, and helpers). A custom screen calls the same flows with the
206
+ `registerWithEmail`, `verifyOtp`, `watchMagicLink`, `finishMagicLinkSignIn`,
207
+ `confirmMagicLinkSignIn`, `startOAuthSignIn`, `completeOAuthCallback`, `enrollPasskey`, and helpers). A custom screen calls the same flows with the
208
208
  session as its actions:
209
209
 
210
210
  ```ts
@@ -6,7 +6,8 @@
6
6
  <!--
7
7
  Where an emailed magic link lands. Verifies the link once, tells the tab that
8
8
  asked for it, and signs this tab in too when the link was opened in the same
9
- browser.
9
+ browser. Opened on another device, the link is held and the screen asks
10
+ whether to sign the requesting device in.
10
11
  -->
11
12
  <script module lang="ts">
12
13
  // Tokens this page has already spent. A link can be used once, and a screen
@@ -16,7 +17,13 @@
16
17
  </script>
17
18
 
18
19
  <script lang="ts">
19
- import { finishMagicLinkSignIn, type MagicLinkOutcome } from '@seamless-auth/client';
20
+ import {
21
+ confirmMagicLinkSignIn,
22
+ finishMagicLinkSignIn,
23
+ getMagicLinkConfirmation,
24
+ magicLinkConfirmationPrompt,
25
+ type MagicLinkOutcome,
26
+ } from '@seamless-auth/client';
20
27
  import { onMount } from 'svelte';
21
28
 
22
29
  import { getAuthNavigator, getSeamlessAuth } from '../context.js';
@@ -26,11 +33,37 @@
26
33
  const navigator = getAuthNavigator();
27
34
  let outcome = $state<MagicLinkOutcome | null>(null);
28
35
  let error = $state('');
36
+ let prompt = $state('');
37
+ let decision = $state<'pending' | 'confirming' | 'confirmed' | 'declined'>('pending');
38
+ let confirmError = $state('');
39
+ let heldToken: string | null = null;
40
+ let active = true;
41
+
42
+ async function confirm() {
43
+ if (!heldToken || decision === 'confirming') return;
44
+
45
+ decision = 'confirming';
46
+ confirmError = '';
47
+
48
+ const result = await confirmMagicLinkSignIn({ client: auth.client, token: heldToken });
49
+
50
+ if (!active) return;
51
+
52
+ if (result.error) {
53
+ decision = 'pending';
54
+ confirmError =
55
+ 'We could not confirm this sign-in. The link may have expired or already been used.';
56
+ return;
57
+ }
58
+
59
+ spent.add(heldToken);
60
+ decision = 'confirmed';
61
+ }
29
62
 
30
63
  // Browser only. A server render would spend the single-use link in a request
31
64
  // that carries none of this browser's cookies.
32
65
  onMount(() => {
33
- let active = true;
66
+ active = true;
34
67
  let redirect: ReturnType<typeof setTimeout> | undefined;
35
68
 
36
69
  void (async () => {
@@ -56,6 +89,15 @@
56
89
  if (!active) return;
57
90
 
58
91
  if (result.error) {
92
+ const held = getMagicLinkConfirmation(result.error);
93
+ if (held) {
94
+ // Held, not used: a remount may ask again.
95
+ spent.delete(token);
96
+ heldToken = token;
97
+ prompt = magicLinkConfirmationPrompt(held);
98
+ return;
99
+ }
100
+
59
101
  error = 'Failed to verify token';
60
102
  return;
61
103
  }
@@ -82,23 +124,72 @@
82
124
  </script>
83
125
 
84
126
  <AuthLayout>
85
- <div class="sa-center">
86
- <h1 class="sa-heading">Verifying your login</h1>
87
-
88
- {#if !outcome && !error}
89
- <div class="sa-spinner" aria-hidden="true"></div>
90
- <p class="sa-helper-text">Please wait while we securely verify your sign-in link.</p>
91
- {/if}
92
- {#if outcome === 'signed-in'}
93
- <p class="sa-success" role="status">Login verified. Redirecting...</p>
94
- {/if}
95
- {#if outcome === 'elsewhere'}
96
- <p class="sa-success" role="status">
97
- Login verified. Return to the device where you requested this link to continue.
98
- </p>
99
- {/if}
100
- {#if error}
101
- <p class="sa-error" role="alert">{error}</p>
127
+ {#if prompt}
128
+ {@const asking = decision === 'pending' || decision === 'confirming'}
129
+ <div class="sa-center">
130
+ <h1 class="sa-heading">Confirm this sign-in</h1>
131
+
132
+ {#if asking}
133
+ <p class="sa-description">{prompt}</p>
134
+ <p class="sa-helper-text">
135
+ This link was opened on a different device from the one that asked for it. Only
136
+ continue if you made that request.
137
+ </p>
138
+ {/if}
139
+ {#if decision === 'confirmed'}
140
+ <p class="sa-success" role="status">
141
+ Sign-in approved. You can return to your other device to continue.
142
+ </p>
143
+ {/if}
144
+ {#if decision === 'declined'}
145
+ <p class="sa-helper-text" role="status">
146
+ No one has been signed in. You can ignore the email and close this page.
147
+ </p>
148
+ {/if}
149
+ </div>
150
+
151
+ {#if asking}
152
+ <div class="sa-actions">
153
+ <button
154
+ type="button"
155
+ class="sa-button"
156
+ disabled={decision === 'confirming'}
157
+ onclick={() => void confirm()}
158
+ >
159
+ Yes, sign in that device
160
+ </button>
161
+ <button
162
+ type="button"
163
+ class="sa-secondary"
164
+ disabled={decision === 'confirming'}
165
+ onclick={() => (decision = 'declined')}
166
+ >
167
+ No, this wasn't me
168
+ </button>
169
+ </div>
170
+ {#if confirmError}
171
+ <p class="sa-error" role="alert">{confirmError}</p>
172
+ {/if}
102
173
  {/if}
103
- </div>
174
+ {:else}
175
+ <div class="sa-center">
176
+ <h1 class="sa-heading">Verifying your login</h1>
177
+
178
+ {#if !outcome && !error}
179
+ <div class="sa-spinner" aria-hidden="true"></div>
180
+ <p class="sa-helper-text">Please wait while we securely verify your sign-in link.</p>
181
+ {/if}
182
+ {#if outcome === 'signed-in'}
183
+ <p class="sa-success" role="status">Login verified. Redirecting...</p>
184
+ {/if}
185
+ {#if outcome === 'elsewhere'}
186
+ <p class="sa-success" role="status">
187
+ Login verified. Return to the device where you requested this link to continue.
188
+ </p>
189
+ {/if}
190
+ {#if error}
191
+ <p class="sa-error" role="alert">{error}</p>
192
+ {/if}
193
+ </div>
194
+ {/if}
104
195
  </AuthLayout>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@seamless-auth/svelte",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Svelte bindings for Seamless Auth: a rune-based session, SvelteKit guards, and optional sign-in screens.",
5
5
  "type": "module",
6
6
  "svelte": "./dist/index.js",
@@ -70,7 +70,7 @@
70
70
  }
71
71
  },
72
72
  "dependencies": {
73
- "@seamless-auth/client": "^0.5.0"
73
+ "@seamless-auth/client": "^0.6.0"
74
74
  },
75
75
  "sideEffects": [
76
76
  "*.css"