@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 +17 -0
- package/README.md +2 -2
- package/dist/screens/VerifyMagicLink.svelte +112 -21
- package/package.json +2 -2
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`,
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
73
|
+
"@seamless-auth/client": "^0.6.0"
|
|
74
74
|
},
|
|
75
75
|
"sideEffects": [
|
|
76
76
|
"*.css"
|