@somewhere-cloud/sdk 0.0.0-stage → 0.13.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/LICENSE +21 -0
- package/README.md +191 -2
- package/dist/cjs/auth/client.js +1059 -0
- package/dist/cjs/auth/client.js.map +1 -0
- package/dist/cjs/auth/continuation.js +27 -0
- package/dist/cjs/auth/continuation.js.map +1 -0
- package/dist/cjs/auth/index.js +21 -0
- package/dist/cjs/auth/index.js.map +1 -0
- package/dist/cjs/auth/react.js +295 -0
- package/dist/cjs/auth/react.js.map +1 -0
- package/dist/cjs/auth/server.js +368 -0
- package/dist/cjs/auth/server.js.map +1 -0
- package/dist/cjs/client.js +392 -0
- package/dist/cjs/client.js.map +1 -0
- package/dist/cjs/errors.js +16 -0
- package/dist/cjs/errors.js.map +1 -0
- package/dist/cjs/index.cjs +215 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/query-cache.js +91 -0
- package/dist/cjs/query-cache.js.map +1 -0
- package/dist/cjs/resources/auth.js +875 -0
- package/dist/cjs/resources/auth.js.map +1 -0
- package/dist/cjs/resources/calls.js +42 -0
- package/dist/cjs/resources/calls.js.map +1 -0
- package/dist/cjs/resources/chat.js +82 -0
- package/dist/cjs/resources/chat.js.map +1 -0
- package/dist/cjs/resources/db.js +124 -0
- package/dist/cjs/resources/db.js.map +1 -0
- package/dist/cjs/resources/emails.js +39 -0
- package/dist/cjs/resources/emails.js.map +1 -0
- package/dist/cjs/resources/fs.js +141 -0
- package/dist/cjs/resources/fs.js.map +1 -0
- package/dist/cjs/resources/functions.js +145 -0
- package/dist/cjs/resources/functions.js.map +1 -0
- package/dist/cjs/resources/inbox.js +76 -0
- package/dist/cjs/resources/inbox.js.map +1 -0
- package/dist/cjs/resources/payments.js +68 -0
- package/dist/cjs/resources/payments.js.map +1 -0
- package/dist/cjs/resources/postgrest.js +391 -0
- package/dist/cjs/resources/postgrest.js.map +1 -0
- package/dist/cjs/resources/projects.js +71 -0
- package/dist/cjs/resources/projects.js.map +1 -0
- package/dist/cjs/resources/storage.js +191 -0
- package/dist/cjs/resources/storage.js.map +1 -0
- package/dist/cjs/resources/tasks.js +99 -0
- package/dist/cjs/resources/tasks.js.map +1 -0
- package/dist/cjs/resources/video.js +50 -0
- package/dist/cjs/resources/video.js.map +1 -0
- package/dist/cjs/types.js +3 -0
- package/dist/cjs/types.js.map +1 -0
- package/dist/esm/auth/client.js +1054 -0
- package/dist/esm/auth/client.js.map +1 -0
- package/dist/esm/auth/continuation.js +22 -0
- package/dist/esm/auth/continuation.js.map +1 -0
- package/dist/esm/auth/index.js +5 -0
- package/dist/esm/auth/index.js.map +1 -0
- package/dist/esm/auth/react.js +279 -0
- package/dist/esm/auth/react.js.map +1 -0
- package/dist/esm/auth/server.js +365 -0
- package/dist/esm/auth/server.js.map +1 -0
- package/dist/esm/client.js +388 -0
- package/dist/esm/client.js.map +1 -0
- package/dist/esm/errors.js +12 -0
- package/dist/esm/errors.js.map +1 -0
- package/dist/esm/index.js +188 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/package.json +1 -0
- package/dist/esm/query-cache.js +87 -0
- package/dist/esm/query-cache.js.map +1 -0
- package/dist/esm/resources/auth.js +871 -0
- package/dist/esm/resources/auth.js.map +1 -0
- package/dist/esm/resources/calls.js +38 -0
- package/dist/esm/resources/calls.js.map +1 -0
- package/dist/esm/resources/chat.js +77 -0
- package/dist/esm/resources/chat.js.map +1 -0
- package/dist/esm/resources/db.js +120 -0
- package/dist/esm/resources/db.js.map +1 -0
- package/dist/esm/resources/emails.js +35 -0
- package/dist/esm/resources/emails.js.map +1 -0
- package/dist/esm/resources/fs.js +137 -0
- package/dist/esm/resources/fs.js.map +1 -0
- package/dist/esm/resources/functions.js +141 -0
- package/dist/esm/resources/functions.js.map +1 -0
- package/dist/esm/resources/inbox.js +70 -0
- package/dist/esm/resources/inbox.js.map +1 -0
- package/dist/esm/resources/payments.js +64 -0
- package/dist/esm/resources/payments.js.map +1 -0
- package/dist/esm/resources/postgrest.js +386 -0
- package/dist/esm/resources/postgrest.js.map +1 -0
- package/dist/esm/resources/projects.js +67 -0
- package/dist/esm/resources/projects.js.map +1 -0
- package/dist/esm/resources/storage.js +186 -0
- package/dist/esm/resources/storage.js.map +1 -0
- package/dist/esm/resources/tasks.js +95 -0
- package/dist/esm/resources/tasks.js.map +1 -0
- package/dist/esm/resources/video.js +46 -0
- package/dist/esm/resources/video.js.map +1 -0
- package/dist/esm/types.js +2 -0
- package/dist/esm/types.js.map +1 -0
- package/dist/types/auth/client.d.ts +272 -0
- package/dist/types/auth/client.d.ts.map +1 -0
- package/dist/types/auth/continuation.d.ts +10 -0
- package/dist/types/auth/continuation.d.ts.map +1 -0
- package/dist/types/auth/index.d.ts +2 -0
- package/dist/types/auth/index.d.ts.map +1 -0
- package/dist/types/auth/react.d.ts +140 -0
- package/dist/types/auth/react.d.ts.map +1 -0
- package/dist/types/auth/server.d.ts +124 -0
- package/dist/types/auth/server.d.ts.map +1 -0
- package/dist/types/client.d.ts +128 -0
- package/dist/types/client.d.ts.map +1 -0
- package/dist/types/errors.d.ts +17 -0
- package/dist/types/errors.d.ts.map +1 -0
- package/dist/types/index.d.ts +145 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/query-cache.d.ts +59 -0
- package/dist/types/query-cache.d.ts.map +1 -0
- package/dist/types/resources/auth.d.ts +188 -0
- package/dist/types/resources/auth.d.ts.map +1 -0
- package/dist/types/resources/calls.d.ts +27 -0
- package/dist/types/resources/calls.d.ts.map +1 -0
- package/dist/types/resources/chat.d.ts +24 -0
- package/dist/types/resources/chat.d.ts.map +1 -0
- package/dist/types/resources/db.d.ts +81 -0
- package/dist/types/resources/db.d.ts.map +1 -0
- package/dist/types/resources/emails.d.ts +20 -0
- package/dist/types/resources/emails.d.ts.map +1 -0
- package/dist/types/resources/fs.d.ts +68 -0
- package/dist/types/resources/fs.d.ts.map +1 -0
- package/dist/types/resources/functions.d.ts +30 -0
- package/dist/types/resources/functions.d.ts.map +1 -0
- package/dist/types/resources/inbox.d.ts +52 -0
- package/dist/types/resources/inbox.d.ts.map +1 -0
- package/dist/types/resources/payments.d.ts +31 -0
- package/dist/types/resources/payments.d.ts.map +1 -0
- package/dist/types/resources/postgrest.d.ts +121 -0
- package/dist/types/resources/postgrest.d.ts.map +1 -0
- package/dist/types/resources/projects.d.ts +79 -0
- package/dist/types/resources/projects.d.ts.map +1 -0
- package/dist/types/resources/storage.d.ts +63 -0
- package/dist/types/resources/storage.d.ts.map +1 -0
- package/dist/types/resources/tasks.d.ts +87 -0
- package/dist/types/resources/tasks.d.ts.map +1 -0
- package/dist/types/resources/video.d.ts +30 -0
- package/dist/types/resources/video.d.ts.map +1 -0
- package/dist/types/types.d.ts +379 -0
- package/dist/types/types.d.ts.map +1 -0
- package/package.json +86 -4
|
@@ -0,0 +1,871 @@
|
|
|
1
|
+
import { authData, isAuthUser, mfaChallenge } from '../auth/continuation.js';
|
|
2
|
+
import { SomewhereError } from '../errors.js';
|
|
3
|
+
/**
|
|
4
|
+
* Application auth client.
|
|
5
|
+
*
|
|
6
|
+
* const { data, error } = await sw.auth.signUp({ email, password })
|
|
7
|
+
* const { data, error } = await sw.auth.signInWithPassword({ email, password })
|
|
8
|
+
* const { data, error } = await sw.auth.signOut()
|
|
9
|
+
* const { data: { user } } = await sw.auth.getUser()
|
|
10
|
+
*
|
|
11
|
+
* Method names are retained for existing SDK callers.
|
|
12
|
+
*
|
|
13
|
+
* ONLY-PATH CONTRACT (tsk_acd56ee8):
|
|
14
|
+
*
|
|
15
|
+
* - COOKIE (the browser default, 0.6.0): sign-in posts to your app's own
|
|
16
|
+
* backend auth routes (`authPath`, default '/api/auth' — the standard
|
|
17
|
+
* `sw.auth.loginWithCookie` handler), which set the session as httpOnly
|
|
18
|
+
* cookies. The SDK holds NO tokens — nothing in localStorage, nothing XSS
|
|
19
|
+
* can read; it caches only the (non-secret) user object for optimistic
|
|
20
|
+
* rendering, and a network blip / 5xx NEVER reads as a logout. The server
|
|
21
|
+
* refreshes the cookie in-band on every request, so there is no client
|
|
22
|
+
* refresh logic at all.
|
|
23
|
+
*
|
|
24
|
+
* - HEADER (Node/CLI/native, or `authMode: 'header'`): compatibility/non-browser
|
|
25
|
+
* behavior retained under rule 9.
|
|
26
|
+
* On successful `signUp` / `signInWithPassword` the SDK swaps its
|
|
27
|
+
* in-memory auth header to the returned access token, so subsequent calls
|
|
28
|
+
* (db, storage, chat, emails) run with the user's JWT. The caller owns
|
|
29
|
+
* persistence — this is the manual/advanced mode.
|
|
30
|
+
*
|
|
31
|
+
* Compatibility bridge: a cookie-preferring client whose backend replies with tokens
|
|
32
|
+
* (an older/manual handler) falls back to header mode for that session, and
|
|
33
|
+
* an explicitly adopted header session (`setSession`) keeps header
|
|
34
|
+
* transport — nobody gets logged out by the default flip.
|
|
35
|
+
*/
|
|
36
|
+
/** localStorage key for the cached (non-secret) user object in cookie mode.
|
|
37
|
+
* Shared with @somewhere-cloud/sdk/auth and the legacy auth-package shim. */
|
|
38
|
+
const USER_CACHE_KEY = 'sw_auth_user';
|
|
39
|
+
function loadCachedUser() {
|
|
40
|
+
try {
|
|
41
|
+
if (typeof localStorage === 'undefined')
|
|
42
|
+
return null;
|
|
43
|
+
const raw = localStorage.getItem(USER_CACHE_KEY);
|
|
44
|
+
const parsed = raw ? JSON.parse(raw) : null;
|
|
45
|
+
if (isAuthUser(parsed))
|
|
46
|
+
return parsed;
|
|
47
|
+
localStorage.removeItem(USER_CACHE_KEY);
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
const pendingAuth = new WeakMap();
|
|
55
|
+
function authFailure(code, message, status = 400) {
|
|
56
|
+
return { data: { user: null, session: null }, error: new SomewhereError({
|
|
57
|
+
code, message, statusCode: status, retry: false, retryAfterMs: null,
|
|
58
|
+
}), status };
|
|
59
|
+
}
|
|
60
|
+
export class AuthClient {
|
|
61
|
+
constructor(client) {
|
|
62
|
+
this.client = client;
|
|
63
|
+
this.currentSession = null;
|
|
64
|
+
/**
|
|
65
|
+
* Cookie mode's only client-side state: the last-known user object.
|
|
66
|
+
* NOT a credential — the session itself lives in httpOnly cookies the
|
|
67
|
+
* page can't read. Cached (localStorage when available) so a reload
|
|
68
|
+
* paints the signed-in UI immediately; `/me` validates in the background.
|
|
69
|
+
*/
|
|
70
|
+
this.cookieUser = null;
|
|
71
|
+
this.authListeners = new Set();
|
|
72
|
+
pendingAuth.set(this, { token: null, generation: 0, queue: Promise.resolve() });
|
|
73
|
+
if (client.authMode === 'cookie')
|
|
74
|
+
this.cookieUser = loadCachedUser();
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Cookie transport applies only while no header session is held — an
|
|
78
|
+
* adopted token session (setSession / a tokens-returning backend) keeps
|
|
79
|
+
* header transport untouched for its lifetime.
|
|
80
|
+
*/
|
|
81
|
+
get cookieMode() {
|
|
82
|
+
return this.client.authMode === 'cookie' && this.currentSession === null;
|
|
83
|
+
}
|
|
84
|
+
/** The synthetic Session a cookie session presents: user, no readable tokens. */
|
|
85
|
+
cookieSession() {
|
|
86
|
+
return this.cookieUser ? { cookie_session: true, user: this.cookieUser } : null;
|
|
87
|
+
}
|
|
88
|
+
setCookieUser(next) {
|
|
89
|
+
if (next && !isAuthUser(next))
|
|
90
|
+
throw new Error('Auth response did not include a valid user.');
|
|
91
|
+
this.cookieUser = next;
|
|
92
|
+
try {
|
|
93
|
+
if (typeof localStorage === 'undefined')
|
|
94
|
+
return;
|
|
95
|
+
if (next)
|
|
96
|
+
localStorage.setItem(USER_CACHE_KEY, JSON.stringify(next));
|
|
97
|
+
else
|
|
98
|
+
localStorage.removeItem(USER_CACHE_KEY);
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
/* storage unavailable — the in-memory copy still works */
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
/* ─── Auth state changes ───────────────────────────── */
|
|
105
|
+
/**
|
|
106
|
+
* Subscribe to sign-in / sign-out / token-refresh / user-update events.
|
|
107
|
+
* The callback fires immediately with
|
|
108
|
+
* `('INITIAL_SESSION', currentSession)` (asynchronously), then on every
|
|
109
|
+
* later transition.
|
|
110
|
+
*
|
|
111
|
+
* const { data: { subscription } } =
|
|
112
|
+
* sw.auth.onAuthStateChange((event, session) => { ... })
|
|
113
|
+
* // later: subscription.unsubscribe()
|
|
114
|
+
*/
|
|
115
|
+
onAuthStateChange(callback) {
|
|
116
|
+
this.authListeners.add(callback);
|
|
117
|
+
// The client fires INITIAL_SESSION asynchronously after subscribe.
|
|
118
|
+
const fire = () => {
|
|
119
|
+
try {
|
|
120
|
+
callback('INITIAL_SESSION', this.currentSession ?? this.cookieSession());
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
/* listener threw — never let it break the caller */
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
if (typeof queueMicrotask === 'function')
|
|
127
|
+
queueMicrotask(fire);
|
|
128
|
+
else
|
|
129
|
+
Promise.resolve().then(fire);
|
|
130
|
+
return {
|
|
131
|
+
data: {
|
|
132
|
+
subscription: {
|
|
133
|
+
unsubscribe: () => {
|
|
134
|
+
this.authListeners.delete(callback);
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
emit(event) {
|
|
141
|
+
const session = this.currentSession ?? this.cookieSession();
|
|
142
|
+
for (const listener of this.authListeners) {
|
|
143
|
+
try {
|
|
144
|
+
listener(event, session);
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
/* listener threw — isolate it */
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
/* ─── Sign up / in / out ───────────────────────────────────── */
|
|
152
|
+
async signUp(credentials) {
|
|
153
|
+
return this.sessionMutation((guard, signal) => this.cookieMode
|
|
154
|
+
? this.runCookieSessionFlow('/signup', credentials, guard, signal)
|
|
155
|
+
: this.runAuthFlow('POST', '/auth/signup', credentials, guard, signal));
|
|
156
|
+
}
|
|
157
|
+
async signInWithPassword(credentials) {
|
|
158
|
+
return this.sessionMutation((guard, signal) => this.cookieMode
|
|
159
|
+
? this.runCookieSessionFlow('/login', credentials, guard, signal)
|
|
160
|
+
: this.runAuthFlow('POST', '/auth/login', credentials, guard, signal));
|
|
161
|
+
}
|
|
162
|
+
/** Auth-subpath-compatible alias. Result semantics stay SDK-shaped. */
|
|
163
|
+
async signIn(credentials) {
|
|
164
|
+
return this.signInWithPassword(credentials);
|
|
165
|
+
}
|
|
166
|
+
/** Send a passwordless magic-link/OTP email through the shared auth contract. */
|
|
167
|
+
async sendMagicLink(input) {
|
|
168
|
+
if (this.cookieMode) {
|
|
169
|
+
return this.runCookieAction('/magic-link', {
|
|
170
|
+
email: input.email,
|
|
171
|
+
redirect_uri: input.redirectUri,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
const projectId = this.client.requireProjectId(undefined, 'auth.sendMagicLink');
|
|
175
|
+
try {
|
|
176
|
+
await this.client.call('POST', '/auth/magic-link', {
|
|
177
|
+
auth: 'developer',
|
|
178
|
+
body: {
|
|
179
|
+
project_id: projectId,
|
|
180
|
+
email: input.email,
|
|
181
|
+
redirect_uri: input.redirectUri,
|
|
182
|
+
},
|
|
183
|
+
});
|
|
184
|
+
return { data: { sent: true }, error: null, status: 200 };
|
|
185
|
+
}
|
|
186
|
+
catch (err) {
|
|
187
|
+
if (err instanceof SomewhereError) {
|
|
188
|
+
return { data: null, error: err, status: err.statusCode };
|
|
189
|
+
}
|
|
190
|
+
throw err;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
/** Complete passwordless sign-in; distinct from the legacy password-reset verifyOtp alias. */
|
|
194
|
+
async verifyMagicLink(input) {
|
|
195
|
+
return this.sessionMutation((guard, signal) => this.cookieMode
|
|
196
|
+
? this.runCookieSessionFlow('/magic-link/verify', { token: input.token }, guard, signal)
|
|
197
|
+
: this.runAuthFlow('POST', '/auth/magic-link/verify', { token: input.token }, guard, signal));
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Returns a Google OAuth redirect URL from `signInWithOAuth`. The platform
|
|
201
|
+
* currently supports Google. The caller redirects the browser to `data.url`.
|
|
202
|
+
*/
|
|
203
|
+
async signInWithOAuth(options) {
|
|
204
|
+
if (options.provider !== 'google') {
|
|
205
|
+
return {
|
|
206
|
+
data: null,
|
|
207
|
+
error: new SomewhereError({
|
|
208
|
+
code: 'UNSUPPORTED_FEATURE',
|
|
209
|
+
message: `Provider ${options.provider} is not supported. Use 'google'.`,
|
|
210
|
+
statusCode: 400,
|
|
211
|
+
retry: false,
|
|
212
|
+
retryAfterMs: null,
|
|
213
|
+
}),
|
|
214
|
+
status: 400,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
const projectId = this.client.requireProjectId(undefined, 'auth.signInWithOAuth');
|
|
218
|
+
// Cookie mode's zero-code OAuth return: default the redirect to the
|
|
219
|
+
// backend handler's /callback route, which exchanges the ?code, sets the
|
|
220
|
+
// httpOnly cookies, and 302s home (sw.auth.googleCallbackWithCookie).
|
|
221
|
+
// The page never touches a token.
|
|
222
|
+
const cookieCallback = this.cookieMode && typeof location !== 'undefined'
|
|
223
|
+
? location.origin + this.client.authPath + '/callback'
|
|
224
|
+
: '';
|
|
225
|
+
const redirect = options.redirectTo ?? cookieCallback;
|
|
226
|
+
const url = `${this.client.baseUrl}/auth/google?project_id=${encodeURIComponent(projectId)}` +
|
|
227
|
+
(redirect ? `&redirect_uri=${encodeURIComponent(redirect)}` : '');
|
|
228
|
+
return { data: { provider: 'google', url }, error: null, status: 200 };
|
|
229
|
+
}
|
|
230
|
+
/** Finish the current sign-in; pending credentials remain private to this instance. */
|
|
231
|
+
completeMfa(input) {
|
|
232
|
+
const token = pendingAuth.get(this).token;
|
|
233
|
+
if (!token)
|
|
234
|
+
return Promise.resolve(authFailure('MFA_CHALLENGE_REQUIRED', 'Start signing in again; there is no pending MFA challenge.'));
|
|
235
|
+
return this.sessionMutation((guard, signal) => this.cookieMode
|
|
236
|
+
? this.runCookieSessionFlow('/mfa/challenge', { mfa_token: token, code: input.code }, guard, signal)
|
|
237
|
+
: this.runAuthFlow('POST', '/auth/mfa/challenge', { mfa_token: token, code: input.code }, guard, signal), true);
|
|
238
|
+
}
|
|
239
|
+
signOut() {
|
|
240
|
+
const state = pendingAuth.get(this);
|
|
241
|
+
state.token = null;
|
|
242
|
+
++state.generation;
|
|
243
|
+
const cookie = this.cookieMode;
|
|
244
|
+
const sessionToken = this.currentSession?.session_token;
|
|
245
|
+
const hadIdentity = this.cookieUser !== null || this.currentSession !== null;
|
|
246
|
+
// Clear immediately, but order the server revoke after earlier sign-ins.
|
|
247
|
+
this.setCookieUser(null);
|
|
248
|
+
this.currentSession = null;
|
|
249
|
+
this.client.clearSession();
|
|
250
|
+
if (hadIdentity)
|
|
251
|
+
this.emit('SIGNED_OUT');
|
|
252
|
+
const run = state.queue.then(() => this.performSignOut(cookie, sessionToken));
|
|
253
|
+
state.queue = run.catch(() => undefined);
|
|
254
|
+
return run;
|
|
255
|
+
}
|
|
256
|
+
async performSignOut(cookie, sessionToken) {
|
|
257
|
+
if (cookie) {
|
|
258
|
+
// Preserve the existing best-effort cookie logout Result contract.
|
|
259
|
+
try {
|
|
260
|
+
await this.client.rawFetch(this.client.authPath + '/logout', {
|
|
261
|
+
method: 'POST', credentials: 'include',
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
catch { /* local state is already cleared */ }
|
|
265
|
+
return { data: null, error: null, status: 200 };
|
|
266
|
+
}
|
|
267
|
+
try {
|
|
268
|
+
if (sessionToken) {
|
|
269
|
+
const projectId = this.client.requireProjectId(undefined, 'auth.signOut');
|
|
270
|
+
await this.client.call('POST', '/auth/logout', {
|
|
271
|
+
auth: 'developer', body: { project_id: projectId, session_token: sessionToken },
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
return { data: null, error: null, status: 200 };
|
|
275
|
+
}
|
|
276
|
+
catch (err) {
|
|
277
|
+
if (err instanceof SomewhereError)
|
|
278
|
+
return { data: null, error: err, status: err.statusCode };
|
|
279
|
+
throw err;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
/* ─── Session / user ────────────────────────────────────────── */
|
|
283
|
+
/**
|
|
284
|
+
* Returns the current session held in SDK memory — never makes an HTTP
|
|
285
|
+
* call. In cookie mode the session is the httpOnly cookie the browser
|
|
286
|
+
* owns: the returned Session has `cookie_session: true`, the cached user,
|
|
287
|
+
* and deliberately NO readable tokens. Call `getUser()` to validate it
|
|
288
|
+
* against the server.
|
|
289
|
+
*/
|
|
290
|
+
async getSession() {
|
|
291
|
+
if (this.cookieMode) {
|
|
292
|
+
return { data: { session: this.cookieSession() }, error: null, status: 200 };
|
|
293
|
+
}
|
|
294
|
+
return { data: { session: this.currentSession }, error: null, status: 200 };
|
|
295
|
+
}
|
|
296
|
+
/** Fetch the current user — `GET /v1/auth/me` (header mode) or the app's own `/me` route (cookie mode). */
|
|
297
|
+
async getUser() {
|
|
298
|
+
const mine = pendingAuth.get(this).generation;
|
|
299
|
+
if (this.cookieMode) {
|
|
300
|
+
// The browser owns the session, so always probe with credentials —
|
|
301
|
+
// the cookie can outlive a cleared cache. A 401 (or explicit null
|
|
302
|
+
// user) is DEFINITIVE signed-out; a blip or 5xx keeps the cached user
|
|
303
|
+
// — a wifi hiccup must never read as a logout.
|
|
304
|
+
try {
|
|
305
|
+
const res = await this.client.rawFetch(this.client.authPath + '/me', {
|
|
306
|
+
method: 'GET',
|
|
307
|
+
credentials: 'include',
|
|
308
|
+
});
|
|
309
|
+
if (mine !== pendingAuth.get(this).generation)
|
|
310
|
+
return { data: { user: this.cookieUser }, error: null, status: 0 };
|
|
311
|
+
if (res.status === 401) {
|
|
312
|
+
if (this.cookieUser) {
|
|
313
|
+
this.setCookieUser(null);
|
|
314
|
+
this.emit('SIGNED_OUT');
|
|
315
|
+
}
|
|
316
|
+
return { data: { user: null }, error: null, status: 401 };
|
|
317
|
+
}
|
|
318
|
+
if (!res.ok)
|
|
319
|
+
return { data: { user: this.cookieUser }, error: null, status: res.status };
|
|
320
|
+
const body = (await res.json().catch(() => null));
|
|
321
|
+
if (mine !== pendingAuth.get(this).generation)
|
|
322
|
+
return { data: { user: this.cookieUser }, error: null, status: 0 };
|
|
323
|
+
const candidate = body?.user ?? body?.data?.user ?? null;
|
|
324
|
+
if (candidate !== null && !isAuthUser(candidate))
|
|
325
|
+
return { data: { user: this.cookieUser }, error: new SomewhereError({ code: 'INVALID_AUTH_RESPONSE', message: 'Auth response did not include a valid user.', statusCode: 502, retry: false, retryAfterMs: null }), status: 502 };
|
|
326
|
+
const u = candidate;
|
|
327
|
+
const hadUser = this.cookieUser !== null;
|
|
328
|
+
this.setCookieUser(u);
|
|
329
|
+
if (!u && hadUser)
|
|
330
|
+
this.emit('SIGNED_OUT');
|
|
331
|
+
return { data: { user: u }, error: null, status: 200 };
|
|
332
|
+
}
|
|
333
|
+
catch {
|
|
334
|
+
// network blip — keep whatever we had
|
|
335
|
+
return { data: { user: this.cookieUser }, error: null, status: 0 };
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
try {
|
|
339
|
+
const result = await this.client.call('GET', '/auth/me');
|
|
340
|
+
return {
|
|
341
|
+
data: { user: result.user },
|
|
342
|
+
error: null,
|
|
343
|
+
status: 200,
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
catch (err) {
|
|
347
|
+
if (err instanceof SomewhereError) {
|
|
348
|
+
return { data: { user: null }, error: err, status: err.statusCode };
|
|
349
|
+
}
|
|
350
|
+
throw err;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* Restore a session the caller stored themselves (e.g. in localStorage).
|
|
355
|
+
* This flips the SDK's auth header to the supplied `access_token` so
|
|
356
|
+
* subsequent calls run as that user.
|
|
357
|
+
*/
|
|
358
|
+
async setSession(session) {
|
|
359
|
+
const state = pendingAuth.get(this);
|
|
360
|
+
state.token = null;
|
|
361
|
+
const mine = ++state.generation;
|
|
362
|
+
this.client.setSessionToken(session.access_token);
|
|
363
|
+
try {
|
|
364
|
+
const result = await this.client.call('GET', '/auth/me');
|
|
365
|
+
if (mine !== state.generation)
|
|
366
|
+
throw new SomewhereError({ code: 'AUTH_SUPERSEDED', message: 'A newer sign-in or sign-out replaced this request.', statusCode: 0, retry: false, retryAfterMs: null });
|
|
367
|
+
if (!isAuthUser(result.user))
|
|
368
|
+
throw new SomewhereError({ code: 'INVALID_AUTH_RESPONSE', message: 'Auth response did not include a valid user.', statusCode: 502, retry: false, retryAfterMs: null });
|
|
369
|
+
const fullSession = {
|
|
370
|
+
access_token: session.access_token,
|
|
371
|
+
session_token: session.session_token,
|
|
372
|
+
user: result.user,
|
|
373
|
+
};
|
|
374
|
+
this.currentSession = fullSession;
|
|
375
|
+
this.emit('SIGNED_IN');
|
|
376
|
+
return { data: { session: fullSession }, error: null, status: 200 };
|
|
377
|
+
}
|
|
378
|
+
catch (err) {
|
|
379
|
+
if (mine === state.generation) {
|
|
380
|
+
this.client.clearSession();
|
|
381
|
+
this.currentSession = null;
|
|
382
|
+
}
|
|
383
|
+
if (err instanceof SomewhereError) {
|
|
384
|
+
return { data: { session: null }, error: err, status: err.statusCode };
|
|
385
|
+
}
|
|
386
|
+
throw err;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
/** Patch the currently-signed-in user's profile (display name, metadata). */
|
|
390
|
+
async updateUser(attrs) {
|
|
391
|
+
const blocked = this.cookieModeNeedsBackend('updateUser');
|
|
392
|
+
if (blocked)
|
|
393
|
+
return blocked;
|
|
394
|
+
try {
|
|
395
|
+
const result = await this.client.call('PATCH', '/auth/users/me', {
|
|
396
|
+
auth: 'session',
|
|
397
|
+
body: { display_name: attrs.display_name, metadata: attrs.metadata },
|
|
398
|
+
});
|
|
399
|
+
if (this.currentSession) {
|
|
400
|
+
this.currentSession = { ...this.currentSession, user: result.user };
|
|
401
|
+
}
|
|
402
|
+
this.emit('USER_UPDATED');
|
|
403
|
+
return { data: { user: result.user }, error: null, status: 200 };
|
|
404
|
+
}
|
|
405
|
+
catch (err) {
|
|
406
|
+
if (err instanceof SomewhereError) {
|
|
407
|
+
return { data: { user: null }, error: err, status: err.statusCode };
|
|
408
|
+
}
|
|
409
|
+
throw err;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
/**
|
|
413
|
+
* Exchange a refresh token for a fresh `(access_token, refresh_token)` pair.
|
|
414
|
+
* Refresh tokens rotate — the old one is invalidated. Run this from your
|
|
415
|
+
* server, not browser code (the developer key is required).
|
|
416
|
+
*/
|
|
417
|
+
async refreshSession(refreshToken) {
|
|
418
|
+
if (this.cookieMode && refreshToken === undefined) {
|
|
419
|
+
// Cookie sessions refresh server-side in-band (fromRequest re-issues
|
|
420
|
+
// the cookie on every authed request) — there is no client-held pair
|
|
421
|
+
// to rotate. Probe /me so the call still answers "is the session
|
|
422
|
+
// alive?" with a fresh user.
|
|
423
|
+
const probe = await this.getUser();
|
|
424
|
+
const session = probe.data?.user ? this.cookieSession() : null;
|
|
425
|
+
if (session)
|
|
426
|
+
this.emit('TOKEN_REFRESHED');
|
|
427
|
+
return { data: { session }, error: null, status: session ? 200 : 401 };
|
|
428
|
+
}
|
|
429
|
+
const projectId = this.client.requireProjectId(undefined, 'auth.refreshSession');
|
|
430
|
+
const token = refreshToken ?? this.currentSession?.refresh_token;
|
|
431
|
+
if (!token) {
|
|
432
|
+
return {
|
|
433
|
+
data: { session: null },
|
|
434
|
+
error: new SomewhereError({
|
|
435
|
+
code: 'VALIDATION_ERROR',
|
|
436
|
+
message: 'No refresh_token in current session and none supplied.',
|
|
437
|
+
statusCode: 400,
|
|
438
|
+
retry: false,
|
|
439
|
+
retryAfterMs: null,
|
|
440
|
+
}),
|
|
441
|
+
status: 400,
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
try {
|
|
445
|
+
const result = await this.client.call('POST', '/auth/refresh', {
|
|
446
|
+
auth: 'developer',
|
|
447
|
+
body: { project_id: projectId, refresh_token: token },
|
|
448
|
+
});
|
|
449
|
+
this.client.setSessionToken(result.access_token);
|
|
450
|
+
// Re-fetch the user to keep the session shape consistent.
|
|
451
|
+
const me = await this.client.call('GET', '/auth/me');
|
|
452
|
+
const session = {
|
|
453
|
+
access_token: result.access_token,
|
|
454
|
+
refresh_token: result.refresh_token,
|
|
455
|
+
expires_in: result.expires_in,
|
|
456
|
+
session_token: this.currentSession?.session_token,
|
|
457
|
+
user: me.user,
|
|
458
|
+
};
|
|
459
|
+
this.currentSession = session;
|
|
460
|
+
this.emit('TOKEN_REFRESHED');
|
|
461
|
+
return { data: { session }, error: null, status: 200 };
|
|
462
|
+
}
|
|
463
|
+
catch (err) {
|
|
464
|
+
if (err instanceof SomewhereError) {
|
|
465
|
+
return { data: { session: null }, error: err, status: err.statusCode };
|
|
466
|
+
}
|
|
467
|
+
throw err;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
/**
|
|
471
|
+
* Change the password for the currently signed-in user. Pass
|
|
472
|
+
* `currentPassword` unless the user has never set one (e.g. signed up
|
|
473
|
+
* via Google). On success the platform wipes every session and refresh
|
|
474
|
+
* token for the user — they will need to log in again everywhere.
|
|
475
|
+
*/
|
|
476
|
+
async updatePassword(params) {
|
|
477
|
+
const blocked = this.cookieModeNeedsBackend('updatePassword');
|
|
478
|
+
if (blocked)
|
|
479
|
+
return blocked;
|
|
480
|
+
try {
|
|
481
|
+
const body = { new_password: params.newPassword };
|
|
482
|
+
if (params.currentPassword !== undefined)
|
|
483
|
+
body.current_password = params.currentPassword;
|
|
484
|
+
await this.client.call('POST', '/auth/update-password', { auth: 'session', body });
|
|
485
|
+
this.currentSession = null;
|
|
486
|
+
this.client.clearSession();
|
|
487
|
+
this.emit('SIGNED_OUT');
|
|
488
|
+
return { data: { updated: true }, error: null, status: 200 };
|
|
489
|
+
}
|
|
490
|
+
catch (err) {
|
|
491
|
+
if (err instanceof SomewhereError) {
|
|
492
|
+
return { data: null, error: err, status: err.statusCode };
|
|
493
|
+
}
|
|
494
|
+
throw err;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
/** Send the logged-in user a 6-digit email verification code. */
|
|
498
|
+
async resendVerification() {
|
|
499
|
+
const blocked = this.cookieModeNeedsBackend('resendVerification');
|
|
500
|
+
if (blocked)
|
|
501
|
+
return blocked;
|
|
502
|
+
try {
|
|
503
|
+
await this.client.call('POST', '/auth/resend-verification', { auth: 'session', body: {} });
|
|
504
|
+
return { data: { sent: true }, error: null, status: 200 };
|
|
505
|
+
}
|
|
506
|
+
catch (err) {
|
|
507
|
+
if (err instanceof SomewhereError) {
|
|
508
|
+
return { data: null, error: err, status: err.statusCode };
|
|
509
|
+
}
|
|
510
|
+
throw err;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
/** Verify the 6-digit code that was emailed to the user. */
|
|
514
|
+
async verifyEmail(code) {
|
|
515
|
+
const blocked = this.cookieModeNeedsBackend('verifyEmail');
|
|
516
|
+
if (blocked)
|
|
517
|
+
return blocked;
|
|
518
|
+
try {
|
|
519
|
+
await this.client.call('POST', '/auth/verify-email', { auth: 'session', body: { code } });
|
|
520
|
+
return { data: { verified: true }, error: null, status: 200 };
|
|
521
|
+
}
|
|
522
|
+
catch (err) {
|
|
523
|
+
if (err instanceof SomewhereError) {
|
|
524
|
+
return { data: null, error: err, status: err.statusCode };
|
|
525
|
+
}
|
|
526
|
+
throw err;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
/**
|
|
530
|
+
* Permanently delete the currently signed-in user. Wipes app_users
|
|
531
|
+
* row + sessions + refresh tokens + pending resets/verifications.
|
|
532
|
+
* App-level data your project stored about them is NOT touched —
|
|
533
|
+
* call your own cleanup before this.
|
|
534
|
+
*/
|
|
535
|
+
async deleteAccount() {
|
|
536
|
+
const blocked = this.cookieModeNeedsBackend('deleteAccount');
|
|
537
|
+
if (blocked)
|
|
538
|
+
return blocked;
|
|
539
|
+
try {
|
|
540
|
+
await this.client.call('DELETE', '/auth/users/me', { auth: 'session' });
|
|
541
|
+
this.currentSession = null;
|
|
542
|
+
this.client.clearSession();
|
|
543
|
+
this.emit('SIGNED_OUT');
|
|
544
|
+
return { data: { deleted: true }, error: null, status: 200 };
|
|
545
|
+
}
|
|
546
|
+
catch (err) {
|
|
547
|
+
if (err instanceof SomewhereError) {
|
|
548
|
+
return { data: null, error: err, status: err.statusCode };
|
|
549
|
+
}
|
|
550
|
+
throw err;
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
/** Trigger a password-reset email. */
|
|
554
|
+
async resetPasswordForEmail(email) {
|
|
555
|
+
const projectId = this.client.requireProjectId(undefined, 'auth.resetPasswordForEmail');
|
|
556
|
+
try {
|
|
557
|
+
await this.client.call('POST', '/auth/forgot', {
|
|
558
|
+
auth: 'developer',
|
|
559
|
+
body: { project_id: projectId, email },
|
|
560
|
+
});
|
|
561
|
+
return { data: { sent: true }, error: null, status: 200 };
|
|
562
|
+
}
|
|
563
|
+
catch (err) {
|
|
564
|
+
if (err instanceof SomewhereError) {
|
|
565
|
+
return { data: null, error: err, status: err.statusCode };
|
|
566
|
+
}
|
|
567
|
+
throw err;
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
/** Complete a password reset using the token from the email. */
|
|
571
|
+
async verifyPasswordReset(params) {
|
|
572
|
+
const projectId = this.client.requireProjectId(undefined, 'auth.verifyPasswordReset');
|
|
573
|
+
try {
|
|
574
|
+
await this.client.call('POST', '/auth/reset', {
|
|
575
|
+
auth: 'developer',
|
|
576
|
+
body: {
|
|
577
|
+
project_id: projectId,
|
|
578
|
+
token: params.token,
|
|
579
|
+
new_password: params.newPassword,
|
|
580
|
+
},
|
|
581
|
+
});
|
|
582
|
+
return { data: { reset: true }, error: null, status: 200 };
|
|
583
|
+
}
|
|
584
|
+
catch (err) {
|
|
585
|
+
if (err instanceof SomewhereError) {
|
|
586
|
+
return { data: null, error: err, status: err.statusCode };
|
|
587
|
+
}
|
|
588
|
+
throw err;
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
/**
|
|
592
|
+
* @deprecated This historical name verifies a password-reset token, not a
|
|
593
|
+
* magic-link/OTP sign-in. Use `verifyPasswordReset`; use `verifyMagicLink`
|
|
594
|
+
* for passwordless sign-in. Kept unchanged under rule 9.
|
|
595
|
+
*/
|
|
596
|
+
async verifyOtp(params) {
|
|
597
|
+
return this.verifyPasswordReset(params);
|
|
598
|
+
}
|
|
599
|
+
/* ─── Internal ────────────────────────────────────────────── */
|
|
600
|
+
sessionMutation(operation, completing = false) {
|
|
601
|
+
const state = pendingAuth.get(this);
|
|
602
|
+
if (!completing)
|
|
603
|
+
state.token = null;
|
|
604
|
+
const mine = ++state.generation;
|
|
605
|
+
const run = state.queue.then(async () => {
|
|
606
|
+
const controller = new AbortController();
|
|
607
|
+
const guard = () => {
|
|
608
|
+
if (controller.signal.aborted)
|
|
609
|
+
throw new SomewhereError({ code: 'AUTH_TIMEOUT', message: 'The auth request timed out. Its server outcome is unknown.', statusCode: 0, retry: false, retryAfterMs: null });
|
|
610
|
+
if (mine !== state.generation)
|
|
611
|
+
throw new SomewhereError({ code: 'AUTH_SUPERSEDED', message: 'A newer sign-in or sign-out replaced this request.', statusCode: 0, retry: false, retryAfterMs: null });
|
|
612
|
+
};
|
|
613
|
+
let timer;
|
|
614
|
+
const timeout = new Promise((resolve) => {
|
|
615
|
+
timer = setTimeout(() => {
|
|
616
|
+
controller.abort();
|
|
617
|
+
resolve(authFailure('AUTH_TIMEOUT', 'The auth request timed out. Its server outcome is unknown.', 0));
|
|
618
|
+
}, 30000);
|
|
619
|
+
});
|
|
620
|
+
try {
|
|
621
|
+
guard();
|
|
622
|
+
return await Promise.race([operation(guard, controller.signal), timeout]);
|
|
623
|
+
}
|
|
624
|
+
catch (err) {
|
|
625
|
+
if (err instanceof SomewhereError)
|
|
626
|
+
return { data: { user: null, session: null }, error: err, status: err.statusCode };
|
|
627
|
+
throw err;
|
|
628
|
+
}
|
|
629
|
+
finally {
|
|
630
|
+
clearTimeout(timer);
|
|
631
|
+
}
|
|
632
|
+
});
|
|
633
|
+
state.queue = run.catch(() => undefined);
|
|
634
|
+
return run;
|
|
635
|
+
}
|
|
636
|
+
/**
|
|
637
|
+
* Cookie-mode sign-in/up: POST to the app's own backend route (same
|
|
638
|
+
* origin), which calls `sw.auth.loginWithCookie` / `signupWithCookie`
|
|
639
|
+
* server-side and sets the httpOnly pair on the response. Never throws —
|
|
640
|
+
* expected failures (wrong password, duplicate email) come back as a
|
|
641
|
+
* `Result` error with the server's real code + message.
|
|
642
|
+
*/
|
|
643
|
+
async runCookieSessionFlow(path, payload, guard, signal) {
|
|
644
|
+
let res;
|
|
645
|
+
try {
|
|
646
|
+
res = await this.client.rawFetch(this.client.authPath + path, {
|
|
647
|
+
method: 'POST',
|
|
648
|
+
headers: {
|
|
649
|
+
'Content-Type': 'application/json',
|
|
650
|
+
Accept: 'application/json',
|
|
651
|
+
// Shared with @somewhere-cloud/sdk/server. Raw *WithCookie handlers
|
|
652
|
+
// ignore it; the adapter uses it to withhold tokens from the body.
|
|
653
|
+
'X-Sw-Auth-Mode': 'cookie',
|
|
654
|
+
},
|
|
655
|
+
body: JSON.stringify(payload),
|
|
656
|
+
signal,
|
|
657
|
+
credentials: 'include',
|
|
658
|
+
});
|
|
659
|
+
}
|
|
660
|
+
catch (err) {
|
|
661
|
+
// NETWORK BLIP — the request never reached the server. Not a logout,
|
|
662
|
+
// not a credential failure; surface it as retryable.
|
|
663
|
+
return {
|
|
664
|
+
data: { user: null, session: null },
|
|
665
|
+
error: new SomewhereError({
|
|
666
|
+
code: 'NETWORK_ERROR',
|
|
667
|
+
message: err instanceof Error ? err.message : 'Network error reaching your auth route.',
|
|
668
|
+
statusCode: 0,
|
|
669
|
+
retry: true,
|
|
670
|
+
retryAfterMs: null,
|
|
671
|
+
}),
|
|
672
|
+
status: 0,
|
|
673
|
+
};
|
|
674
|
+
}
|
|
675
|
+
const body = (await res.json().catch(() => null));
|
|
676
|
+
const d = (body && typeof body === 'object' && body.data && typeof body.data === 'object'
|
|
677
|
+
? body.data
|
|
678
|
+
: body);
|
|
679
|
+
guard();
|
|
680
|
+
const challenge = mfaChallenge(body);
|
|
681
|
+
if (challenge) {
|
|
682
|
+
pendingAuth.get(this).token = challenge.mfa_token;
|
|
683
|
+
return authFailure('MFA_REQUIRED', 'Enter an authenticator or backup code to finish signing in.', res.status);
|
|
684
|
+
}
|
|
685
|
+
if (!res.ok) {
|
|
686
|
+
return {
|
|
687
|
+
data: { user: null, session: null },
|
|
688
|
+
error: new SomewhereError({
|
|
689
|
+
code: d?.error ?? 'AUTH_ERROR',
|
|
690
|
+
message: d?.message ?? `Auth request failed (${res.status}).`,
|
|
691
|
+
statusCode: res.status,
|
|
692
|
+
retry: res.status >= 500,
|
|
693
|
+
retryAfterMs: null,
|
|
694
|
+
body: d?.mfa_required === true || authData(d?.user)?.mfa_required === true ? undefined : body,
|
|
695
|
+
}),
|
|
696
|
+
status: res.status,
|
|
697
|
+
};
|
|
698
|
+
}
|
|
699
|
+
// The standard cookie handler returns the bare user object; also accept
|
|
700
|
+
// a { user } wrapper.
|
|
701
|
+
const candidate = d?.user ?? d;
|
|
702
|
+
let user = isAuthUser(candidate) ? candidate : null;
|
|
703
|
+
if (d?.user != null && !user)
|
|
704
|
+
return authFailure('INVALID_AUTH_RESPONSE', 'Auth response did not include a valid user.', 502);
|
|
705
|
+
// Dual-mode fallback: a backend that replies with tokens (an older or
|
|
706
|
+
// manual handler) didn't set cookies — adopt the pair as a header
|
|
707
|
+
// session so sign-in still works exactly as 0.5.x.
|
|
708
|
+
const access = (d?.token ?? d?.access_token);
|
|
709
|
+
if (access) {
|
|
710
|
+
if (!user) {
|
|
711
|
+
guard();
|
|
712
|
+
try {
|
|
713
|
+
const me = await this.client.call('GET', '/auth/me', { auth: 'session', sessionBearer: access, signal });
|
|
714
|
+
guard();
|
|
715
|
+
user = isAuthUser(me.user) ? me.user : null;
|
|
716
|
+
}
|
|
717
|
+
catch (err) {
|
|
718
|
+
guard();
|
|
719
|
+
throw err;
|
|
720
|
+
}
|
|
721
|
+
if (!user) {
|
|
722
|
+
return authFailure('INVALID_AUTH_RESPONSE', 'Auth response did not include a valid user.', 502);
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
const session = {
|
|
726
|
+
access_token: access,
|
|
727
|
+
refresh_token: d?.refresh_token,
|
|
728
|
+
session_token: d?.session_token,
|
|
729
|
+
expires_in: d?.expires_in,
|
|
730
|
+
user: user,
|
|
731
|
+
};
|
|
732
|
+
guard();
|
|
733
|
+
this.client.setSessionToken(access);
|
|
734
|
+
this.currentSession = session;
|
|
735
|
+
pendingAuth.get(this).token = null;
|
|
736
|
+
this.emit('SIGNED_IN');
|
|
737
|
+
return { data: { user, session }, error: null, status: 200 };
|
|
738
|
+
}
|
|
739
|
+
// Cookie session established. If the handler didn't echo the user,
|
|
740
|
+
// fetch it through the cookie that was just set.
|
|
741
|
+
if (!user) {
|
|
742
|
+
const probe = await this.client.rawFetch(this.client.authPath + '/me', { method: 'GET', credentials: 'include', signal });
|
|
743
|
+
const response = authData(await probe.json().catch(() => null));
|
|
744
|
+
guard();
|
|
745
|
+
user = probe.ok && isAuthUser(response?.user) ? response.user : null;
|
|
746
|
+
}
|
|
747
|
+
guard();
|
|
748
|
+
if (!isAuthUser(user))
|
|
749
|
+
return authFailure('INVALID_AUTH_RESPONSE', 'Auth response did not include a valid user.', 502);
|
|
750
|
+
this.setCookieUser(user);
|
|
751
|
+
pendingAuth.get(this).token = null;
|
|
752
|
+
this.emit('SIGNED_IN');
|
|
753
|
+
return { data: { user, session: this.cookieSession() }, error: null, status: 200 };
|
|
754
|
+
}
|
|
755
|
+
/** Cookie-mode action that does not itself establish a session. */
|
|
756
|
+
async runCookieAction(path, body) {
|
|
757
|
+
try {
|
|
758
|
+
const res = await this.client.rawFetch(this.client.authPath + path, {
|
|
759
|
+
method: 'POST',
|
|
760
|
+
headers: {
|
|
761
|
+
'Content-Type': 'application/json',
|
|
762
|
+
Accept: 'application/json',
|
|
763
|
+
'X-Sw-Auth-Mode': 'cookie',
|
|
764
|
+
},
|
|
765
|
+
body: JSON.stringify(body),
|
|
766
|
+
credentials: 'include',
|
|
767
|
+
});
|
|
768
|
+
const responseBody = (await res.json().catch(() => null));
|
|
769
|
+
if (!res.ok) {
|
|
770
|
+
const detail = responseBody?.data ?? responseBody;
|
|
771
|
+
return {
|
|
772
|
+
data: null,
|
|
773
|
+
error: new SomewhereError({
|
|
774
|
+
code: detail?.error ?? 'AUTH_ERROR',
|
|
775
|
+
message: detail?.message ?? `Auth request failed (${res.status}).`,
|
|
776
|
+
statusCode: res.status,
|
|
777
|
+
retry: res.status >= 500,
|
|
778
|
+
retryAfterMs: null,
|
|
779
|
+
body: responseBody,
|
|
780
|
+
}),
|
|
781
|
+
status: res.status,
|
|
782
|
+
};
|
|
783
|
+
}
|
|
784
|
+
return { data: { sent: true }, error: null, status: res.status };
|
|
785
|
+
}
|
|
786
|
+
catch (err) {
|
|
787
|
+
return {
|
|
788
|
+
data: null,
|
|
789
|
+
error: new SomewhereError({
|
|
790
|
+
code: 'NETWORK_ERROR',
|
|
791
|
+
message: err instanceof Error ? err.message : 'Network error reaching your auth route.',
|
|
792
|
+
statusCode: 0,
|
|
793
|
+
retry: true,
|
|
794
|
+
retryAfterMs: null,
|
|
795
|
+
}),
|
|
796
|
+
status: 0,
|
|
797
|
+
};
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
/**
|
|
801
|
+
* The platform's user-profile endpoints authenticate with a Bearer JWT the
|
|
802
|
+
* cookie client deliberately doesn't have (the session is an httpOnly
|
|
803
|
+
* cookie scoped to YOUR app's backend, not the platform API). Teach the
|
|
804
|
+
* fix instead of failing with a misleading "sign in first".
|
|
805
|
+
*/
|
|
806
|
+
cookieModeNeedsBackend(method) {
|
|
807
|
+
if (!this.cookieMode || this.client.hasSession)
|
|
808
|
+
return null;
|
|
809
|
+
return {
|
|
810
|
+
data: null,
|
|
811
|
+
error: new SomewhereError({
|
|
812
|
+
code: 'COOKIE_MODE_BACKEND_ROUTE_REQUIRED',
|
|
813
|
+
message: `auth.${method} can't run from the browser in cookie mode — the session ` +
|
|
814
|
+
`lives in httpOnly cookies that only your app's backend can use. Add a ` +
|
|
815
|
+
`backend route that calls the matching sw.auth.* method server-side ` +
|
|
816
|
+
`(see platform_help('auth-client')), or create the client with ` +
|
|
817
|
+
`{ authMode: 'header' } for manual token control.`,
|
|
818
|
+
statusCode: 400,
|
|
819
|
+
retry: false,
|
|
820
|
+
retryAfterMs: null,
|
|
821
|
+
}),
|
|
822
|
+
status: 400,
|
|
823
|
+
};
|
|
824
|
+
}
|
|
825
|
+
async runAuthFlow(method, path, body, guard, signal) {
|
|
826
|
+
const projectId = this.client.requireProjectId(undefined, `auth${path}`);
|
|
827
|
+
try {
|
|
828
|
+
const result = await this.client.call(method, path, {
|
|
829
|
+
auth: 'developer',
|
|
830
|
+
body: { ...body, project_id: projectId },
|
|
831
|
+
signal,
|
|
832
|
+
});
|
|
833
|
+
guard();
|
|
834
|
+
const challenge = mfaChallenge(result);
|
|
835
|
+
if (challenge) {
|
|
836
|
+
pendingAuth.get(this).token = challenge.mfa_token;
|
|
837
|
+
return authFailure('MFA_REQUIRED', 'Enter an authenticator or backup code to finish signing in.');
|
|
838
|
+
}
|
|
839
|
+
if (!isAuthUser(result.user))
|
|
840
|
+
return authFailure('INVALID_AUTH_RESPONSE', 'Auth response did not include a valid user.', 502);
|
|
841
|
+
const access = result.access_token ?? result.token;
|
|
842
|
+
if (!access)
|
|
843
|
+
return authFailure('INVALID_AUTH_RESPONSE', 'Auth response did not include a session.', 502);
|
|
844
|
+
const session = {
|
|
845
|
+
access_token: access,
|
|
846
|
+
refresh_token: result.refresh_token,
|
|
847
|
+
session_token: result.session_token,
|
|
848
|
+
expires_in: result.expires_in,
|
|
849
|
+
user: result.user,
|
|
850
|
+
};
|
|
851
|
+
this.currentSession = session;
|
|
852
|
+
this.client.setSessionToken(access);
|
|
853
|
+
pendingAuth.get(this).token = null;
|
|
854
|
+
this.emit('SIGNED_IN');
|
|
855
|
+
return { data: { user: result.user, session }, error: null, status: 200 };
|
|
856
|
+
}
|
|
857
|
+
catch (err) {
|
|
858
|
+
if (err instanceof SomewhereError) {
|
|
859
|
+
guard();
|
|
860
|
+
const challenge = mfaChallenge(err.body);
|
|
861
|
+
if (challenge) {
|
|
862
|
+
pendingAuth.get(this).token = challenge.mfa_token;
|
|
863
|
+
return authFailure('MFA_REQUIRED', 'Enter an authenticator or backup code to finish signing in.', err.statusCode);
|
|
864
|
+
}
|
|
865
|
+
return { data: { user: null, session: null }, error: err, status: err.statusCode };
|
|
866
|
+
}
|
|
867
|
+
throw err;
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
//# sourceMappingURL=auth.js.map
|