mnfst 0.5.223 → 0.5.225
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/lib/manifest.appwrite.auth.js +384 -71
- package/lib/manifest.appwrite.data.js +6 -3
- package/lib/manifest.data.js +220 -53
- package/lib/manifest.integrity.json +4 -4
- package/lib/manifest.sw.js +1 -1
- package/package.json +1 -1
|
@@ -70,6 +70,7 @@ async function getAppwriteConfig() {
|
|
|
70
70
|
|
|
71
71
|
const magicEnabled = authMethods.includes("magic");
|
|
72
72
|
const otpEnabled = authMethods.includes("otp");
|
|
73
|
+
const phoneEnabled = authMethods.includes("phone");
|
|
73
74
|
const oauthEnabled = authMethods.includes("oauth");
|
|
74
75
|
|
|
75
76
|
// Teams (presence of teams object enables it)
|
|
@@ -92,7 +93,7 @@ async function getAppwriteConfig() {
|
|
|
92
93
|
: toBool(appwriteConfig.auth.teams.authenticated);
|
|
93
94
|
|
|
94
95
|
// Guest upgrade: preserve the anonymous account + teams on sign-in (magic/oauth;
|
|
95
|
-
// OTP can't convert anonymous accounts). Defaults to guestTeams.
|
|
96
|
+
// email/phone OTP can't convert anonymous accounts). Defaults to guestTeams.
|
|
96
97
|
const guestUpgrade = appwriteConfig.auth?.guestUpgrade !== undefined
|
|
97
98
|
? toBool(appwriteConfig.auth.guestUpgrade)
|
|
98
99
|
: guestTeams;
|
|
@@ -135,6 +136,7 @@ async function getAppwriteConfig() {
|
|
|
135
136
|
anonymous: guestAuto, // back-compat alias
|
|
136
137
|
magic: magicEnabled,
|
|
137
138
|
otp: otpEnabled,
|
|
139
|
+
phone: phoneEnabled,
|
|
138
140
|
oauth: oauthEnabled,
|
|
139
141
|
teams: teamsEnabled,
|
|
140
142
|
permanentTeams: permanentTeams,
|
|
@@ -218,6 +220,32 @@ function initializeAuthStore() {
|
|
|
218
220
|
|
|
219
221
|
// Cross-tab synchronization using localStorage events
|
|
220
222
|
const STORAGE_KEY = 'manifest:auth:state';
|
|
223
|
+
// Cap on holding scoped data for a guest/callback sign-in that may fail
|
|
224
|
+
const SIGN_IN_PENDING_MS = 5000;
|
|
225
|
+
['manifest:auth:login', 'manifest:auth:anonymous', 'manifest:auth:logout', 'manifest:auth:session-cleared'].forEach(type =>
|
|
226
|
+
window.addEventListener(type, () => { const store = Alpine.store('auth'); if (store) { store._signInPendingUntil = 0; store._authEpoch++; markVerified(store); } }));
|
|
227
|
+
|
|
228
|
+
// Unverified session: recheck on reconnect, on return to the tab, and on a bounded backoff
|
|
229
|
+
const RECHECK_DELAYS_MS = [2000, 5000, 15000, 60000];
|
|
230
|
+
const recheck = () => { Alpine.store('auth')?._recheckSession?.(); };
|
|
231
|
+
window.addEventListener('online', recheck);
|
|
232
|
+
if (typeof document !== 'undefined') document.addEventListener('visibilitychange', () => { if (document.visibilityState === 'visible') recheck(); });
|
|
233
|
+
|
|
234
|
+
function markVerified(store) {
|
|
235
|
+
store._sessionUnverified = false;
|
|
236
|
+
store._recheckAttempt = 0;
|
|
237
|
+
if (store._recheckTimer) { clearTimeout(store._recheckTimer); store._recheckTimer = null; }
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function scheduleRecheck(store) {
|
|
241
|
+
if (!store._sessionUnverified || store._recheckTimer) return;
|
|
242
|
+
const delay = RECHECK_DELAYS_MS[Math.min(store._recheckAttempt++, RECHECK_DELAYS_MS.length - 1)];
|
|
243
|
+
store._recheckTimer = setTimeout(() => { store._recheckTimer = null; store._recheckSession(); }, delay);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Appwrite's "no session" answer; anything else (offline, 5xx) leaves the session unknown
|
|
247
|
+
const NO_SESSION_TYPES = ['general_unauthorized_scope', 'user_unauthorized', 'user_session_not_found', 'user_jwt_invalid', 'user_not_found', 'user_blocked'];
|
|
248
|
+
const isNoSession = error => error?.code === 401 || NO_SESSION_TYPES.includes(error?.type);
|
|
221
249
|
|
|
222
250
|
// Session fields safe to mirror across tabs. Excludes `secret` and provider
|
|
223
251
|
// tokens — this copy is only for UI cross-tab sync, not the auth of record.
|
|
@@ -243,7 +271,10 @@ function initializeAuthStore() {
|
|
|
243
271
|
const state = JSON.parse(e.newValue);
|
|
244
272
|
const store = Alpine.store('auth');
|
|
245
273
|
if (store) {
|
|
274
|
+
const prevId = store.user?.$id || null;
|
|
246
275
|
// Update store state from other tab
|
|
276
|
+
store._authEpoch++;
|
|
277
|
+
if (state.isAuthenticated) markVerified(store);
|
|
247
278
|
store.isAuthenticated = state.isAuthenticated;
|
|
248
279
|
store.isAnonymous = state.isAnonymous;
|
|
249
280
|
store.user = state.user;
|
|
@@ -253,6 +284,15 @@ function initializeAuthStore() {
|
|
|
253
284
|
store.otpSent = state.otpSent || false;
|
|
254
285
|
store.otpExpired = state.otpExpired || false;
|
|
255
286
|
store.error = state.error;
|
|
287
|
+
// Identity ended or changed in another tab: scoped data must drop this tab's rows
|
|
288
|
+
const nextId = state.isAuthenticated ? (state.user?.$id || null) : null;
|
|
289
|
+
if (prevId && nextId !== prevId) {
|
|
290
|
+
if (!nextId) {
|
|
291
|
+
store.teams = [];
|
|
292
|
+
store.currentTeam = null;
|
|
293
|
+
}
|
|
294
|
+
window.dispatchEvent(new CustomEvent('manifest:auth:session-cleared'));
|
|
295
|
+
}
|
|
256
296
|
}
|
|
257
297
|
} catch (error) {
|
|
258
298
|
// Failed to sync state from other tab
|
|
@@ -262,6 +302,8 @@ function initializeAuthStore() {
|
|
|
262
302
|
|
|
263
303
|
// Helper to sync state to localStorage (for cross-tab communication)
|
|
264
304
|
function syncStateToStorage(store) {
|
|
305
|
+
// An unverified signed-out state must not sign other tabs out
|
|
306
|
+
if (store._sessionUnverified && !store.isAuthenticated) return;
|
|
265
307
|
try {
|
|
266
308
|
const state = {
|
|
267
309
|
isAuthenticated: store.isAuthenticated,
|
|
@@ -289,10 +331,10 @@ function initializeAuthStore() {
|
|
|
289
331
|
error: null,
|
|
290
332
|
magicLinkSent: false,
|
|
291
333
|
magicLinkExpired: false,
|
|
292
|
-
otpSent: false, //
|
|
293
|
-
otpExpired: false, //
|
|
294
|
-
otpPhrase: null, // Email OTP: security phrase to display (when enabled)
|
|
295
|
-
_otpUserId: null, //
|
|
334
|
+
otpSent: false, // OTP (email/phone): a code has been sent and is awaiting entry
|
|
335
|
+
otpExpired: false, // OTP (email/phone): the entered code was wrong/expired
|
|
336
|
+
otpPhrase: null, // Email OTP: security phrase to display (when enabled; phone has none)
|
|
337
|
+
_otpUserId: null, // OTP: userId returned by createEmailToken/createPhoneToken, used by verifyOTP
|
|
296
338
|
teams: [], // List of user's teams
|
|
297
339
|
currentTeam: null, // Currently selected/active team
|
|
298
340
|
_teamsPollInterval: null, // Interval ID for teams polling (deprecated, use realtime instead)
|
|
@@ -340,6 +382,11 @@ function initializeAuthStore() {
|
|
|
340
382
|
_appwrite: null,
|
|
341
383
|
_guestAuto: false,
|
|
342
384
|
_guestManual: false,
|
|
385
|
+
_signInPendingUntil: 0, // signed-out init with guest-auto or an auth callback still to sign in
|
|
386
|
+
_sessionUnverified: false, // init could not reach Appwrite: signed out locally, never broadcast
|
|
387
|
+
_authEpoch: 0, // bumped by every identity change; an in-flight recheck from an older epoch is dropped
|
|
388
|
+
_recheckTimer: null,
|
|
389
|
+
_recheckAttempt: 0,
|
|
343
390
|
guestManualEnabled: false,
|
|
344
391
|
_oauthProvider: null, // Store OAuth provider name (google, github, etc.) when login is initiated
|
|
345
392
|
_syncStateToStorage: syncStateToStorage,
|
|
@@ -545,57 +592,18 @@ function initializeAuthStore() {
|
|
|
545
592
|
|
|
546
593
|
// Try to restore existing session
|
|
547
594
|
try {
|
|
548
|
-
|
|
549
|
-
const sessionsResponse = await appwrite.account.listSessions();
|
|
550
|
-
const allSessions = sessionsResponse.sessions || [];
|
|
551
|
-
const currentSession = allSessions.find(s => s.current === true) || allSessions[0];
|
|
552
|
-
|
|
553
|
-
if (currentSession) {
|
|
554
|
-
this.session = currentSession;
|
|
555
|
-
this.isAuthenticated = true;
|
|
556
|
-
this.isAnonymous = currentSession.provider === 'anonymous';
|
|
557
|
-
|
|
558
|
-
// Restore OAuth provider from storage (persists across redirects/refresh)
|
|
559
|
-
if (!this.isAnonymous && currentSession.provider !== 'magic-url') {
|
|
560
|
-
try {
|
|
561
|
-
// Try localStorage first (persists across redirects), fallback to sessionStorage
|
|
562
|
-
let storedProvider = localStorage.getItem('manifest:oauth:provider');
|
|
563
|
-
if (!storedProvider) {
|
|
564
|
-
storedProvider = sessionStorage.getItem('manifest:oauth:provider');
|
|
565
|
-
}
|
|
566
|
-
if (storedProvider) {
|
|
567
|
-
this._oauthProvider = storedProvider;
|
|
568
|
-
}
|
|
569
|
-
} catch (e) {
|
|
570
|
-
// Storage error
|
|
571
|
-
}
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
// If guest is disabled but we have anonymous session, clear it
|
|
575
|
-
if (this.isAnonymous && !this._guestAuto && !this._guestManual) {
|
|
576
|
-
try {
|
|
577
|
-
await appwrite.account.deleteSession(this.session.$id);
|
|
578
|
-
this.isAuthenticated = false;
|
|
579
|
-
this.isAnonymous = false;
|
|
580
|
-
this.user = null;
|
|
581
|
-
this.session = null;
|
|
582
|
-
} catch (deleteError) {
|
|
583
|
-
// Failed to delete guest session
|
|
584
|
-
}
|
|
585
|
-
}
|
|
586
|
-
} else {
|
|
587
|
-
this.isAuthenticated = true; // User exists, session might be managed by cookies
|
|
588
|
-
this.isAnonymous = false;
|
|
589
|
-
}
|
|
595
|
+
await this._restoreSession(appwrite);
|
|
590
596
|
|
|
591
597
|
// Team loading is deferred (not awaited) — runs in the background after
|
|
592
598
|
// manifest:auth:initialized so a session gate isn't held up by it.
|
|
593
599
|
} catch (error) {
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
600
|
+
this._clearIdentity();
|
|
601
|
+
// Signed out for now; other tabs keep their session
|
|
602
|
+
if (!isNoSession(error)) {
|
|
603
|
+
this._sessionUnverified = true;
|
|
604
|
+
scheduleRecheck(this);
|
|
605
|
+
console.warn('[Manifest Appwrite Auth] Session check failed (signed out until reachable):', error?.message || error);
|
|
606
|
+
}
|
|
599
607
|
}
|
|
600
608
|
|
|
601
609
|
// Sync state to localStorage
|
|
@@ -611,6 +619,17 @@ function initializeAuthStore() {
|
|
|
611
619
|
this._initialized = true;
|
|
612
620
|
this._initializing = false;
|
|
613
621
|
|
|
622
|
+
// Guest-auto / callback sign-in follows: scoped data holds instead of flashing signed-out
|
|
623
|
+
if (!this.isAuthenticated) {
|
|
624
|
+
let callback = false;
|
|
625
|
+
try {
|
|
626
|
+
// Team invites need an existing session (acceptInvite refuses signed-out), so they don't sign in
|
|
627
|
+
const info = window.ManifestAppwriteAuthCallbacks?.detect?.();
|
|
628
|
+
callback = !!(info?.hasCallback && !info.isTeamInvite);
|
|
629
|
+
} catch (e) { /* no-op */ }
|
|
630
|
+
if (this._guestAuto || callback) this._signInPendingUntil = Date.now() + SIGN_IN_PENDING_MS;
|
|
631
|
+
}
|
|
632
|
+
|
|
614
633
|
// Fire as soon as identity is known, before teams load, so a session
|
|
615
634
|
// gate / splash clears in a few hundred ms.
|
|
616
635
|
window.dispatchEvent(new CustomEvent('manifest:auth:initialized', {
|
|
@@ -633,6 +652,95 @@ function initializeAuthStore() {
|
|
|
633
652
|
}
|
|
634
653
|
},
|
|
635
654
|
|
|
655
|
+
// Adopt the session Appwrite reports (throws when there is none or it is unreachable)
|
|
656
|
+
async _restoreSession(appwrite, isCurrent = () => true) {
|
|
657
|
+
const user = await appwrite.account.get();
|
|
658
|
+
const sessionsResponse = await appwrite.account.listSessions();
|
|
659
|
+
if (!isCurrent()) return false;
|
|
660
|
+
this.user = user;
|
|
661
|
+
const allSessions = sessionsResponse.sessions || [];
|
|
662
|
+
const currentSession = allSessions.find(s => s.current === true) || allSessions[0];
|
|
663
|
+
|
|
664
|
+
if (currentSession) {
|
|
665
|
+
this.session = currentSession;
|
|
666
|
+
this.isAuthenticated = true;
|
|
667
|
+
this.isAnonymous = currentSession.provider === 'anonymous';
|
|
668
|
+
|
|
669
|
+
// Restore OAuth provider from storage (persists across redirects/refresh)
|
|
670
|
+
if (!this.isAnonymous && currentSession.provider !== 'magic-url') {
|
|
671
|
+
try {
|
|
672
|
+
// Try localStorage first (persists across redirects), fallback to sessionStorage
|
|
673
|
+
let storedProvider = localStorage.getItem('manifest:oauth:provider');
|
|
674
|
+
if (!storedProvider) {
|
|
675
|
+
storedProvider = sessionStorage.getItem('manifest:oauth:provider');
|
|
676
|
+
}
|
|
677
|
+
if (storedProvider) {
|
|
678
|
+
this._oauthProvider = storedProvider;
|
|
679
|
+
}
|
|
680
|
+
} catch (e) {
|
|
681
|
+
// Storage error
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
// If guest is disabled but we have anonymous session, clear it
|
|
686
|
+
if (this.isAnonymous && !this._guestAuto && !this._guestManual) {
|
|
687
|
+
try {
|
|
688
|
+
await appwrite.account.deleteSession(this.session.$id);
|
|
689
|
+
this.isAuthenticated = false;
|
|
690
|
+
this.isAnonymous = false;
|
|
691
|
+
this.user = null;
|
|
692
|
+
this.session = null;
|
|
693
|
+
} catch (deleteError) {
|
|
694
|
+
// Failed to delete guest session
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
} else {
|
|
698
|
+
this.isAuthenticated = true; // User exists, session might be managed by cookies
|
|
699
|
+
this.isAnonymous = false;
|
|
700
|
+
}
|
|
701
|
+
return true;
|
|
702
|
+
},
|
|
703
|
+
|
|
704
|
+
// Offline-booted tab: re-run the session check once Appwrite is reachable
|
|
705
|
+
async _recheckSession() {
|
|
706
|
+
if (!this._sessionUnverified || this._rechecking || !this._appwrite) return;
|
|
707
|
+
if (this._recheckTimer) { clearTimeout(this._recheckTimer); this._recheckTimer = null; }
|
|
708
|
+
this._rechecking = true;
|
|
709
|
+
const epoch = this._authEpoch;
|
|
710
|
+
const current = () => this._authEpoch === epoch && this._sessionUnverified && !this.isAuthenticated;
|
|
711
|
+
try {
|
|
712
|
+
if (!await this._restoreSession(this._appwrite, current)) return;
|
|
713
|
+
} catch (error) {
|
|
714
|
+
if (!current()) return;
|
|
715
|
+
if (!isNoSession(error)) { scheduleRecheck(this); return; }
|
|
716
|
+
this._clearIdentity();
|
|
717
|
+
markVerified(this);
|
|
718
|
+
syncStateToStorage(this);
|
|
719
|
+
if (this._guestAuto && this._createAnonymousSession) await this._createAnonymousSession();
|
|
720
|
+
return;
|
|
721
|
+
} finally {
|
|
722
|
+
this._rechecking = false;
|
|
723
|
+
}
|
|
724
|
+
markVerified(this);
|
|
725
|
+
const cfg = await config.getAppwriteConfig();
|
|
726
|
+
if (this._authEpoch !== epoch) return;
|
|
727
|
+
if (this.isAuthenticated && cfg?.teams && (!this.isAnonymous || cfg.guestTeams)) {
|
|
728
|
+
try { await this._loadTeamsAndSeed(cfg); } catch (e) { console.warn('[Manifest Appwrite Auth] Team load failed:', e?.message || e); }
|
|
729
|
+
if (this._authEpoch !== epoch) return;
|
|
730
|
+
}
|
|
731
|
+
syncStateToStorage(this);
|
|
732
|
+
if (this.isAuthenticated) {
|
|
733
|
+
window.dispatchEvent(new CustomEvent(this.isAnonymous ? 'manifest:auth:anonymous' : 'manifest:auth:login', { detail: { user: this.user } }));
|
|
734
|
+
}
|
|
735
|
+
},
|
|
736
|
+
|
|
737
|
+
_clearIdentity() {
|
|
738
|
+
this.isAuthenticated = false;
|
|
739
|
+
this.isAnonymous = false;
|
|
740
|
+
this.user = null;
|
|
741
|
+
this.session = null;
|
|
742
|
+
},
|
|
743
|
+
|
|
636
744
|
// Clear team state when the identity changes to a different user (e.g. guest
|
|
637
745
|
// replaced on OTP sign-in). Otherwise stale currentTeam/teams cause 404s.
|
|
638
746
|
_resetTeamsState() {
|
|
@@ -804,6 +912,8 @@ function initializeAuthStore() {
|
|
|
804
912
|
return { success: true };
|
|
805
913
|
}
|
|
806
914
|
|
|
915
|
+
this._authEpoch++;
|
|
916
|
+
markVerified(this);
|
|
807
917
|
this.inProgress = true;
|
|
808
918
|
|
|
809
919
|
try {
|
|
@@ -825,7 +935,7 @@ function initializeAuthStore() {
|
|
|
825
935
|
this.magicLinkSent = false;
|
|
826
936
|
this.magicLinkExpired = false;
|
|
827
937
|
|
|
828
|
-
// Clear
|
|
938
|
+
// Clear OTP flags (email/phone)
|
|
829
939
|
this.otpSent = false;
|
|
830
940
|
this.otpExpired = false;
|
|
831
941
|
this.otpPhrase = null;
|
|
@@ -847,15 +957,12 @@ function initializeAuthStore() {
|
|
|
847
957
|
|
|
848
958
|
// Restore to guest state after logout (guest-auto only, and not when
|
|
849
959
|
// already a guest — don't mint a new guest on top).
|
|
960
|
+
let restored = false;
|
|
850
961
|
if (!this.isAnonymous && this._guestAuto && this._createAnonymousSession) {
|
|
851
962
|
await this._createAnonymousSession();
|
|
852
|
-
|
|
853
|
-
// Clear auth state completely
|
|
854
|
-
this.isAuthenticated = false;
|
|
855
|
-
this.isAnonymous = false;
|
|
856
|
-
this.user = null;
|
|
857
|
-
this.session = null;
|
|
963
|
+
restored = this.isAuthenticated && this.isAnonymous;
|
|
858
964
|
}
|
|
965
|
+
if (!restored) this._clearIdentity();
|
|
859
966
|
|
|
860
967
|
syncStateToStorage(this);
|
|
861
968
|
window.dispatchEvent(new CustomEvent('manifest:auth:logout'));
|
|
@@ -893,6 +1000,10 @@ function initializeAuthStore() {
|
|
|
893
1000
|
// Clear teams on logout error too
|
|
894
1001
|
this.teams = [];
|
|
895
1002
|
this.currentTeam = null;
|
|
1003
|
+
if (!this.isAuthenticated) this._clearIdentity();
|
|
1004
|
+
// Expired session (401) is still a logout: scoped data must drop the old rows
|
|
1005
|
+
syncStateToStorage(this);
|
|
1006
|
+
window.dispatchEvent(new CustomEvent('manifest:auth:logout'));
|
|
896
1007
|
return { success: false, error: error.message };
|
|
897
1008
|
} finally {
|
|
898
1009
|
this.inProgress = false;
|
|
@@ -905,6 +1016,8 @@ function initializeAuthStore() {
|
|
|
905
1016
|
return { success: false, error: 'Appwrite not configured' };
|
|
906
1017
|
}
|
|
907
1018
|
|
|
1019
|
+
this._authEpoch++;
|
|
1020
|
+
markVerified(this);
|
|
908
1021
|
this.inProgress = true;
|
|
909
1022
|
|
|
910
1023
|
try {
|
|
@@ -944,6 +1057,10 @@ function initializeAuthStore() {
|
|
|
944
1057
|
this.session = null;
|
|
945
1058
|
this.magicLinkSent = false;
|
|
946
1059
|
this.magicLinkExpired = false;
|
|
1060
|
+
this.teams = [];
|
|
1061
|
+
this.currentTeam = null;
|
|
1062
|
+
syncStateToStorage(this);
|
|
1063
|
+
window.dispatchEvent(new CustomEvent('manifest:auth:session-cleared'));
|
|
947
1064
|
return { success: false, error: error.message };
|
|
948
1065
|
} finally {
|
|
949
1066
|
this.inProgress = false;
|
|
@@ -958,10 +1075,12 @@ function initializeAuthStore() {
|
|
|
958
1075
|
|
|
959
1076
|
try {
|
|
960
1077
|
this.user = await this._appwrite.account.get();
|
|
1078
|
+
if (this.isAuthenticated) markVerified(this);
|
|
961
1079
|
syncStateToStorage(this);
|
|
962
1080
|
return this.user;
|
|
963
1081
|
} catch (error) {
|
|
964
|
-
|
|
1082
|
+
if (!isNoSession(error)) throw error;
|
|
1083
|
+
markVerified(this);
|
|
965
1084
|
this.isAuthenticated = false;
|
|
966
1085
|
this.isAnonymous = false;
|
|
967
1086
|
this.user = null;
|
|
@@ -1038,8 +1157,8 @@ async function initializeAppwriteAuthPlugin() {
|
|
|
1038
1157
|
}
|
|
1039
1158
|
}
|
|
1040
1159
|
|
|
1041
|
-
// If no session and guest-auto is enabled, create guest session
|
|
1042
|
-
if (!store.isAuthenticated && store._guestAuto && store._createAnonymousSession) {
|
|
1160
|
+
// If no session and guest-auto is enabled, create guest session (not while the session check is unreachable)
|
|
1161
|
+
if (!store.isAuthenticated && !store._sessionUnverified && store._guestAuto && store._createAnonymousSession) {
|
|
1043
1162
|
store._createAnonymousSession();
|
|
1044
1163
|
}
|
|
1045
1164
|
}, { once: true });
|
|
@@ -6177,7 +6296,8 @@ window.ManifestAppwriteAuthMagicLinks = {
|
|
|
6177
6296
|
/* Auth email OTP (one-time passcode) */
|
|
6178
6297
|
|
|
6179
6298
|
// Two-step in-page flow (no redirect): createEmailOTP(email) emails a code + returns
|
|
6180
|
-
// a userId, verifyOTP(code) creates the session.
|
|
6299
|
+
// a userId, verifyOTP(code) creates the session. verifyOTP/submitOTP are shared
|
|
6300
|
+
// with phone OTP (users.phone.js).
|
|
6181
6301
|
// Gotcha: Appwrite can't convert an anonymous guest via OTP — a guest verifying an OTP
|
|
6182
6302
|
// gets a fresh account (guest teams lost). Use magic links for guest upgrade.
|
|
6183
6303
|
|
|
@@ -6247,17 +6367,20 @@ function initializeEmailOTP() {
|
|
|
6247
6367
|
this._appwrite = await config.getAppwriteClient();
|
|
6248
6368
|
}
|
|
6249
6369
|
if (!this._appwrite) {
|
|
6250
|
-
|
|
6370
|
+
this.error = 'Appwrite not configured';
|
|
6371
|
+
return { success: false, error: this.error };
|
|
6251
6372
|
}
|
|
6252
6373
|
|
|
6253
6374
|
// Don't allow OTP request if already signed in (non-anonymous)
|
|
6254
6375
|
if (this.isAuthenticated && !this.isAnonymous) {
|
|
6255
|
-
|
|
6376
|
+
this.error = 'Already signed in. Please logout first.';
|
|
6377
|
+
return { success: false, error: this.error };
|
|
6256
6378
|
}
|
|
6257
6379
|
|
|
6258
6380
|
const appwriteConfig = await config.getAppwriteConfig();
|
|
6259
6381
|
if (appwriteConfig && !appwriteConfig.otp) {
|
|
6260
|
-
|
|
6382
|
+
this.error = 'Email OTP authentication is not enabled';
|
|
6383
|
+
return { success: false, error: this.error };
|
|
6261
6384
|
}
|
|
6262
6385
|
|
|
6263
6386
|
// OTP can't convert a guest — warn so lost guest teams aren't a surprise.
|
|
@@ -6267,10 +6390,8 @@ function initializeEmailOTP() {
|
|
|
6267
6390
|
|
|
6268
6391
|
const account = this._appwrite.account;
|
|
6269
6392
|
if (typeof account.createEmailToken !== 'function') {
|
|
6270
|
-
|
|
6271
|
-
|
|
6272
|
-
error: 'Email OTP method not available. Please ensure you are using a recent Appwrite SDK.'
|
|
6273
|
-
};
|
|
6393
|
+
this.error = 'Email OTP method not available. Please ensure you are using a recent Appwrite SDK.';
|
|
6394
|
+
return { success: false, error: this.error };
|
|
6274
6395
|
}
|
|
6275
6396
|
|
|
6276
6397
|
this.inProgress = true;
|
|
@@ -6316,7 +6437,8 @@ function initializeEmailOTP() {
|
|
|
6316
6437
|
const { email, inputEl, dataObj } = resolveEmailInput(emailInputOrRef);
|
|
6317
6438
|
|
|
6318
6439
|
if (!email || !email.trim()) {
|
|
6319
|
-
|
|
6440
|
+
this.error = 'Email is required';
|
|
6441
|
+
return { success: false, error: this.error };
|
|
6320
6442
|
}
|
|
6321
6443
|
|
|
6322
6444
|
const result = await this.createEmailOTP(email.trim(), options);
|
|
@@ -6490,6 +6612,197 @@ window.ManifestAppwriteAuthEmailOTP = {
|
|
|
6490
6612
|
};
|
|
6491
6613
|
|
|
6492
6614
|
|
|
6615
|
+
/* Auth phone OTP (SMS one-time passcode) */
|
|
6616
|
+
|
|
6617
|
+
// Two-step in-page flow (no redirect): createPhoneOTP(phone) texts a code + returns
|
|
6618
|
+
// a userId, then the shared verifyOTP(code)/submitOTP() (users.otp.js) creates the
|
|
6619
|
+
// session. Phone numbers must be E.164 (+ country code).
|
|
6620
|
+
// Gotcha: like email OTP, Appwrite can't convert an anonymous guest via phone OTP —
|
|
6621
|
+
// a guest verifying a code gets a fresh account. Use magic links for guest upgrade.
|
|
6622
|
+
|
|
6623
|
+
function initializePhoneOTP() {
|
|
6624
|
+
if (typeof Alpine === 'undefined') {
|
|
6625
|
+
return;
|
|
6626
|
+
}
|
|
6627
|
+
|
|
6628
|
+
const config = window.ManifestAppwriteAuthConfig;
|
|
6629
|
+
if (!config) {
|
|
6630
|
+
return;
|
|
6631
|
+
}
|
|
6632
|
+
|
|
6633
|
+
// Resolve a phone from an input/selector/{ phone } object/string, or auto-find
|
|
6634
|
+
// the nearest tel input. Returns { phone, inputEl, dataObj }.
|
|
6635
|
+
function resolvePhoneInput(phoneInputOrRef) {
|
|
6636
|
+
let phone = null;
|
|
6637
|
+
let inputEl = null;
|
|
6638
|
+
let dataObj = null;
|
|
6639
|
+
|
|
6640
|
+
if (phoneInputOrRef === undefined || phoneInputOrRef === null) {
|
|
6641
|
+
let eventTarget = (typeof window !== 'undefined' && window.event) ? window.event.target : null;
|
|
6642
|
+
if (eventTarget) {
|
|
6643
|
+
const form = eventTarget.closest('form');
|
|
6644
|
+
const scope = form || eventTarget.parentElement;
|
|
6645
|
+
if (scope) {
|
|
6646
|
+
inputEl = scope.querySelector('input[type="tel"]');
|
|
6647
|
+
if (inputEl) phone = inputEl.value;
|
|
6648
|
+
}
|
|
6649
|
+
}
|
|
6650
|
+
if (!inputEl) {
|
|
6651
|
+
inputEl = document.querySelector('input[type="tel"]');
|
|
6652
|
+
if (inputEl) phone = inputEl.value;
|
|
6653
|
+
}
|
|
6654
|
+
} else if (typeof phoneInputOrRef === 'string') {
|
|
6655
|
+
try {
|
|
6656
|
+
const element = document.querySelector(phoneInputOrRef);
|
|
6657
|
+
if (element && element.tagName === 'INPUT' && element.type === 'tel') {
|
|
6658
|
+
inputEl = element;
|
|
6659
|
+
phone = element.value;
|
|
6660
|
+
} else {
|
|
6661
|
+
phone = phoneInputOrRef; // Treat as a direct phone string
|
|
6662
|
+
}
|
|
6663
|
+
} catch (e) {
|
|
6664
|
+
phone = phoneInputOrRef; // Invalid selector -> treat as phone string
|
|
6665
|
+
}
|
|
6666
|
+
} else if (phoneInputOrRef && typeof phoneInputOrRef === 'object') {
|
|
6667
|
+
if (phoneInputOrRef.tagName === 'INPUT' || phoneInputOrRef.matches?.('input[type="tel"]')) {
|
|
6668
|
+
inputEl = phoneInputOrRef;
|
|
6669
|
+
phone = inputEl.value;
|
|
6670
|
+
} else if ('phone' in phoneInputOrRef) {
|
|
6671
|
+
phone = phoneInputOrRef.phone;
|
|
6672
|
+
dataObj = phoneInputOrRef;
|
|
6673
|
+
}
|
|
6674
|
+
}
|
|
6675
|
+
|
|
6676
|
+
return { phone, inputEl, dataObj };
|
|
6677
|
+
}
|
|
6678
|
+
|
|
6679
|
+
const waitForStore = () => {
|
|
6680
|
+
const store = Alpine.store('auth');
|
|
6681
|
+
if (store && !store.createPhoneOTP) {
|
|
6682
|
+
// Step 1: text a passcode. Verification reuses verifyOTP/submitOTP.
|
|
6683
|
+
store.createPhoneOTP = async function (phone) {
|
|
6684
|
+
if (!this._appwrite) {
|
|
6685
|
+
this._appwrite = await config.getAppwriteClient();
|
|
6686
|
+
}
|
|
6687
|
+
if (!this._appwrite) {
|
|
6688
|
+
this.error = 'Appwrite not configured';
|
|
6689
|
+
return { success: false, error: this.error };
|
|
6690
|
+
}
|
|
6691
|
+
|
|
6692
|
+
// Don't allow OTP request if already signed in (non-anonymous)
|
|
6693
|
+
if (this.isAuthenticated && !this.isAnonymous) {
|
|
6694
|
+
this.error = 'Already signed in. Please logout first.';
|
|
6695
|
+
return { success: false, error: this.error };
|
|
6696
|
+
}
|
|
6697
|
+
|
|
6698
|
+
const appwriteConfig = await config.getAppwriteConfig();
|
|
6699
|
+
if (appwriteConfig && !appwriteConfig.phone) {
|
|
6700
|
+
this.error = 'Phone OTP authentication is not enabled';
|
|
6701
|
+
return { success: false, error: this.error };
|
|
6702
|
+
}
|
|
6703
|
+
|
|
6704
|
+
// Normalize to E.164: strip separators, require the country code.
|
|
6705
|
+
const normalized = String(phone).replace(/[\s().-]/g, '');
|
|
6706
|
+
if (!/^\+\d{6,15}$/.test(normalized)) {
|
|
6707
|
+
this.error = 'Phone number must be in international format, e.g. +14155550123';
|
|
6708
|
+
return { success: false, error: this.error };
|
|
6709
|
+
}
|
|
6710
|
+
|
|
6711
|
+
// Phone OTP can't convert a guest — warn so lost guest teams aren't a surprise.
|
|
6712
|
+
if (this.isAnonymous && appwriteConfig?.guestUpgrade) {
|
|
6713
|
+
console.warn('[Manifest Appwrite Auth] Phone OTP cannot upgrade a guest account (Appwrite limitation); the guest session and any guest-created teams will be replaced. Use magic links for guest upgrade.');
|
|
6714
|
+
}
|
|
6715
|
+
|
|
6716
|
+
const account = this._appwrite.account;
|
|
6717
|
+
if (typeof account.createPhoneToken !== 'function') {
|
|
6718
|
+
this.error = 'Phone OTP method not available. Please ensure you are using a recent Appwrite SDK.';
|
|
6719
|
+
return { success: false, error: this.error };
|
|
6720
|
+
}
|
|
6721
|
+
|
|
6722
|
+
this.inProgress = true;
|
|
6723
|
+
this.error = null;
|
|
6724
|
+
this.otpExpired = false;
|
|
6725
|
+
|
|
6726
|
+
try {
|
|
6727
|
+
const uniqueId = (window.Appwrite?.ID?.unique) ? window.Appwrite.ID.unique() : 'unique()';
|
|
6728
|
+
const token = await account.createPhoneToken(uniqueId, normalized);
|
|
6729
|
+
|
|
6730
|
+
// Stash the userId Appwrite assigned; verifyOTP needs it to complete login.
|
|
6731
|
+
this._otpUserId = token.userId;
|
|
6732
|
+
this.otpPhrase = null; // phone tokens have no security phrase
|
|
6733
|
+
this.otpSent = true;
|
|
6734
|
+
this.otpExpired = false;
|
|
6735
|
+
this.error = null;
|
|
6736
|
+
|
|
6737
|
+
window.dispatchEvent(new CustomEvent('manifest:auth:otp-sent', {
|
|
6738
|
+
detail: { phone: normalized }
|
|
6739
|
+
}));
|
|
6740
|
+
|
|
6741
|
+
return { success: true, message: 'OTP sent by SMS' };
|
|
6742
|
+
} catch (error) {
|
|
6743
|
+
// Appwrite returns 501 when phone auth/SMS isn't set up — surface an
|
|
6744
|
+
// actionable message rather than the raw error.
|
|
6745
|
+
const code = error.code || error.statusCode;
|
|
6746
|
+
const notEnabled = code === 501 || /not implemented/i.test(error.message || '');
|
|
6747
|
+
this.error = notEnabled
|
|
6748
|
+
? 'Phone OTP is not enabled for this Appwrite project. Enable Phone under Auth → Settings and configure an SMS provider under Messaging.'
|
|
6749
|
+
: error.message;
|
|
6750
|
+
this.otpSent = false;
|
|
6751
|
+
this.otpExpired = false;
|
|
6752
|
+
return { success: false, error: this.error };
|
|
6753
|
+
} finally {
|
|
6754
|
+
this.inProgress = false;
|
|
6755
|
+
}
|
|
6756
|
+
};
|
|
6757
|
+
|
|
6758
|
+
// Convenience: resolve the phone from an input/selector/object/string and send.
|
|
6759
|
+
// Clears the phone input on success (mirrors sendEmailOTP).
|
|
6760
|
+
store.sendPhoneOTP = async function (phoneInputOrRef) {
|
|
6761
|
+
const { phone, inputEl, dataObj } = resolvePhoneInput(phoneInputOrRef);
|
|
6762
|
+
|
|
6763
|
+
if (!phone || !String(phone).trim()) {
|
|
6764
|
+
this.error = 'Phone number is required';
|
|
6765
|
+
return { success: false, error: this.error };
|
|
6766
|
+
}
|
|
6767
|
+
|
|
6768
|
+
const result = await this.createPhoneOTP(String(phone).trim());
|
|
6769
|
+
|
|
6770
|
+
if (result.success) {
|
|
6771
|
+
Promise.resolve().then(() => {
|
|
6772
|
+
if (inputEl) {
|
|
6773
|
+
inputEl.value = '';
|
|
6774
|
+
inputEl.dispatchEvent(new Event('input', { bubbles: true }));
|
|
6775
|
+
} else if (dataObj) {
|
|
6776
|
+
dataObj.phone = '';
|
|
6777
|
+
}
|
|
6778
|
+
});
|
|
6779
|
+
}
|
|
6780
|
+
|
|
6781
|
+
return result;
|
|
6782
|
+
};
|
|
6783
|
+
} else if (!store) {
|
|
6784
|
+
setTimeout(waitForStore, 50);
|
|
6785
|
+
}
|
|
6786
|
+
};
|
|
6787
|
+
|
|
6788
|
+
setTimeout(waitForStore, 100);
|
|
6789
|
+
}
|
|
6790
|
+
|
|
6791
|
+
// Initialize when Alpine is ready
|
|
6792
|
+
document.addEventListener('alpine:init', () => {
|
|
6793
|
+
try {
|
|
6794
|
+
initializePhoneOTP();
|
|
6795
|
+
} catch (error) {
|
|
6796
|
+
// Failed to initialize phone OTP
|
|
6797
|
+
}
|
|
6798
|
+
});
|
|
6799
|
+
|
|
6800
|
+
// Export phone OTP interface
|
|
6801
|
+
window.ManifestAppwriteAuthPhoneOTP = {
|
|
6802
|
+
initialize: initializePhoneOTP
|
|
6803
|
+
};
|
|
6804
|
+
|
|
6805
|
+
|
|
6493
6806
|
/* Auth OAuth */
|
|
6494
6807
|
|
|
6495
6808
|
// Add OAuth methods to auth store
|