mnfst 0.5.162 → 0.5.164

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.
@@ -1,19 +1,9 @@
1
- /* Manifest Appwrite Auth
1
+ /* Manifest Appwrite Auth — config resolution
2
2
  /* By Andrew Matlock under MIT license
3
- /* https://github.com/andrewmatlock/Manifest
4
- /*
5
- /* Supports authentication with an Appwrite project
6
- /* Requires Alpine JS (alpinejs.dev) to operate
7
3
  */
8
4
 
9
- /* Auth config */
10
-
11
- // Refuse strings that still contain an unresolved ${VAR} reference. The loader
12
- // runs window.ManifestDataConfig.interpolateManifest at manifest-load time, so
13
- // by the time we read these fields the env-var substitution has already been
14
- // applied. Anything still matching ${VAR} is an undefined env var — passing it
15
- // to Appwrite would either silently fail or, worse, be sent verbatim as an
16
- // HTTP header value, leaking the env var name. Loud-fail instead.
5
+ // Reject strings still holding an unresolved ${VAR} — an undefined env var that
6
+ // would leak verbatim into an Appwrite HTTP header. Loud-fail instead.
17
7
  function resolvedOrNull(value, fieldName) {
18
8
  if (typeof value !== 'string') return value;
19
9
  if (/\$\{[^}]+\}/.test(value)) {
@@ -23,7 +13,7 @@ function resolvedOrNull(value, fieldName) {
23
13
  return value;
24
14
  }
25
15
 
26
- // Load manifest if not already loaded (loader may set __manifestLoaded / registry.manifest)
16
+ // Load manifest if not already loaded
27
17
  async function ensureManifest() {
28
18
  if (window.ManifestComponentsRegistry?.manifest) {
29
19
  return window.ManifestComponentsRegistry.manifest;
@@ -35,7 +25,10 @@ async function ensureManifest() {
35
25
  try {
36
26
  const manifestUrl = (document.querySelector('link[rel="manifest"]')?.getAttribute('href')) || '/manifest.json';
37
27
  const response = await fetch(manifestUrl);
38
- return await response.json();
28
+ const manifest = await response.json();
29
+ // No-loader path: resolve ${VAR} placeholders the dynamic loader would have.
30
+ window.ManifestDataConfig?.interpolateManifest?.(manifest);
31
+ return manifest;
39
32
  } catch (error) {
40
33
  return null;
41
34
  }
@@ -51,26 +44,21 @@ async function getAppwriteConfig() {
51
44
  const appwriteConfig = manifest.appwrite;
52
45
  const endpoint = resolvedOrNull(appwriteConfig.endpoint, 'endpoint');
53
46
  const projectId = resolvedOrNull(appwriteConfig.projectId, 'projectId');
54
- // Optional dev key to bypass rate limits in development. The schema
55
- // documents `${VAR_NAME}` interpolation for this field specifically —
56
- // refuse to forward a literal placeholder as an HTTP header.
47
+ // Optional dev key to bypass rate limits in development.
57
48
  const devKey = appwriteConfig.devKey ? resolvedOrNull(appwriteConfig.devKey, 'devKey') : undefined;
58
49
 
59
50
  if (!endpoint || !projectId) {
60
51
  return null;
61
52
  }
62
- // devKey is optional: if the user supplied one but it failed to resolve,
63
- // resolvedOrNull returned null (and logged) — drop the field rather than
64
- // initialize Appwrite with a literal `${VAR}` header.
53
+ // Supplied-but-unresolved devKey: drop the config rather than send a literal ${VAR} header.
65
54
  if (appwriteConfig.devKey && devKey === null) {
66
55
  return null;
67
56
  }
68
57
 
69
- // Get auth methods from config (defaults to ["magic", "oauth"] if not specified)
58
+ // Auth methods (defaults to magic + oauth)
70
59
  const authMethods = appwriteConfig.auth?.methods || ["magic", "oauth"];
71
60
 
72
- // Guest session support: "guest"/"guest-auto" = automatic, "guest-manual" = manual only.
73
- // ("guest" is the schema's documented spelling; "guest-auto" is accepted as a synonym.)
61
+ // Guest sessions: "guest"/"guest-auto" = automatic, "guest-manual" = manual only.
74
62
  const guestAuto = authMethods.includes("guest") || authMethods.includes("guest-auto");
75
63
  const guestManual = authMethods.includes("guest-manual");
76
64
  const hasGuest = guestAuto || guestManual;
@@ -79,41 +67,34 @@ async function getAppwriteConfig() {
79
67
  const otpEnabled = authMethods.includes("otp");
80
68
  const oauthEnabled = authMethods.includes("oauth");
81
69
 
82
- // Teams support: presence of teams object enables it
70
+ // Teams (presence of teams object enables it)
83
71
  const teamsEnabled = !!appwriteConfig.auth?.teams;
84
- const permanentTeams = appwriteConfig.auth?.teams?.permanent || null; // Array of team names (immutable)
85
- const templateTeams = appwriteConfig.auth?.teams?.template || null; // Array of team names (can be deleted and reapplied)
86
- const teamsPollInterval = appwriteConfig.auth?.teams?.pollInterval || null; // Polling interval in milliseconds (null = disabled)
87
- const guestTeams = !!appwriteConfig.auth?.teams?.guests; // Seed default teams for guest (anonymous) sessions
88
-
89
- // Guest upgrade: preserve the anonymous account (and its teams) when a guest signs in,
90
- // rather than discarding it and minting a fresh user. Supported by magic links and OAuth;
91
- // email OTP cannot convert anonymous accounts (Appwrite limitation). Defaults to guestTeams,
92
- // since orphaning guest-created teams on signup is the failure mode guest teams introduce.
72
+ const permanentTeams = appwriteConfig.auth?.teams?.permanent || null; // immutable
73
+ const templateTeams = appwriteConfig.auth?.teams?.template || null; // deletable + reappliable
74
+ const teamsPollInterval = appwriteConfig.auth?.teams?.pollInterval || null; // ms, null = disabled
75
+ const guestTeams = !!appwriteConfig.auth?.teams?.guests; // seed default teams for guests
76
+
77
+ // Guest upgrade: preserve the anonymous account + teams on sign-in (magic/oauth;
78
+ // OTP can't convert anonymous accounts). Defaults to guestTeams.
93
79
  const guestUpgrade = appwriteConfig.auth?.guestUpgrade !== undefined
94
80
  ? !!appwriteConfig.auth.guestUpgrade
95
81
  : guestTeams;
96
82
 
97
- // Default roles: permanent (cannot be deleted) and template (can be deleted)
98
- // These are objects mapping role names to permissions: { "Admin": ["inviteMembers", ...] }
99
- const permanentRoles = appwriteConfig.auth?.roles?.permanent || null; // Object: { "RoleName": ["permission1", ...] }
100
- const templateRoles = appwriteConfig.auth?.roles?.template || null; // Object: { "RoleName": ["permission1", ...] }
83
+ // Default roles: { "RoleName": ["permission", ...] }
84
+ const permanentRoles = appwriteConfig.auth?.roles?.permanent || null; // not deletable
85
+ const templateRoles = appwriteConfig.auth?.roles?.template || null; // deletable
101
86
 
102
- // Member roles: derived from permanent and template roles (merged)
103
- // This is used for role normalization, permission checking, and creatorRole logic
87
+ // Member roles: permanent + template merged (fallback to legacy memberRoles)
104
88
  const memberRoles = permanentRoles || templateRoles
105
89
  ? { ...(permanentRoles || {}), ...(templateRoles || {}) }
106
- : (appwriteConfig.auth?.memberRoles || null); // Fallback to legacy memberRoles if roles not defined
90
+ : (appwriteConfig.auth?.memberRoles || null);
107
91
 
108
- // Creator role: string reference to a role in memberRoles (role creator gets by default)
92
+ // Creator role: memberRoles key the creator gets by default (legacy singular)
109
93
  const creatorRole = appwriteConfig.auth?.creatorRole || null;
110
94
 
111
- // Creator roles (plural): the custom role(s) the team creator is assigned, atomically,
112
- // at team creation. Accepts string | string[] | null. An explicit null / [] means
113
- // "no template role creator holds only Appwrite's intrinsic owner". Takes precedence
114
- // over the legacy singular `creatorRole`. When NEITHER is set, createTeam keeps its
115
- // historical default (first defined role). Resolved to: array (configured, [] = owner-only)
116
- // or null (unconfigured → use the historical default).
95
+ // Creator roles (plural): role(s) assigned to the team creator atomically at creation.
96
+ // string | string[] | null; explicit null/[] = owner-only. Wins over creatorRole.
97
+ // Resolves to array (configured) or null (use historical default of first role).
117
98
  let creatorRoles = null;
118
99
  if (appwriteConfig.auth && Object.prototype.hasOwnProperty.call(appwriteConfig.auth, 'creatorRoles')) {
119
100
  const raw = appwriteConfig.auth.creatorRoles;
@@ -122,35 +103,34 @@ async function getAppwriteConfig() {
122
103
  creatorRoles = [creatorRole];
123
104
  }
124
105
 
125
- // Guest migration: id of the deployed guest-migration Appwrite Function. When set,
126
- // a guest's teams are carried over to the account they sign into via OTP (which
127
- // Appwrite can't convert in place). See templates/guest-migration-function.
106
+ // Guest migration: deployed Appwrite Function id that carries guest teams to
107
+ // the OTP account (which Appwrite can't convert in place).
128
108
  const guestMigrationFunctionId = appwriteConfig.auth?.guestMigration?.functionId || null;
129
109
 
130
110
  return {
131
111
  endpoint,
132
112
  projectId,
133
- devKey, // Optional dev key for development
113
+ devKey,
134
114
  authMethods,
135
115
  guest: hasGuest,
136
116
  guestAuto: guestAuto,
137
117
  guestManual: guestManual,
138
- anonymous: guestAuto, // For backwards compatibility with existing code
118
+ anonymous: guestAuto, // back-compat alias
139
119
  magic: magicEnabled,
140
- otp: otpEnabled, // Email one-time-passcode authentication
120
+ otp: otpEnabled,
141
121
  oauth: oauthEnabled,
142
122
  teams: teamsEnabled,
143
- permanentTeams: permanentTeams, // Array of team names (cannot be deleted)
144
- templateTeams: templateTeams, // Array of team names (can be deleted and reapplied)
145
- teamsPollInterval: teamsPollInterval, // Polling interval in milliseconds (null = disabled)
146
- guestTeams: guestTeams, // Seed default teams for guest (anonymous) sessions
147
- guestUpgrade: guestUpgrade, // Preserve guest account + teams on sign-in (magic/oauth)
148
- guestMigrationFunctionId: guestMigrationFunctionId, // Carry guest teams to the OTP account via this function
149
- memberRoles: memberRoles, // Role definitions: { "RoleName": ["permission1", "permission2"] }
150
- permanentRoles: permanentRoles, // Object: { "RoleName": ["permission1", ...] } (cannot be deleted)
151
- templateRoles: templateRoles, // Object: { "RoleName": ["permission1", ...] } (can be deleted)
152
- creatorRole: creatorRole, // String reference to memberRoles key (legacy singular)
153
- creatorRoles: creatorRoles // array (configured; [] = owner-only) or null (use historical default)
123
+ permanentTeams: permanentTeams,
124
+ templateTeams: templateTeams,
125
+ teamsPollInterval: teamsPollInterval,
126
+ guestTeams: guestTeams,
127
+ guestUpgrade: guestUpgrade,
128
+ guestMigrationFunctionId: guestMigrationFunctionId,
129
+ memberRoles: memberRoles,
130
+ permanentRoles: permanentRoles,
131
+ templateRoles: templateRoles,
132
+ creatorRole: creatorRole,
133
+ creatorRoles: creatorRoles
154
134
  };
155
135
  }
156
136
 
@@ -161,7 +141,6 @@ let appwriteTeams = null;
161
141
  let appwriteUsers = null;
162
142
 
163
143
  async function getAppwriteClient() {
164
- // Check if Appwrite SDK is loaded
165
144
  if (!window.Appwrite || !window.Appwrite.Client || !window.Appwrite.Account) {
166
145
  return null;
167
146
  }
@@ -176,8 +155,7 @@ async function getAppwriteClient() {
176
155
  .setEndpoint(config.endpoint)
177
156
  .setProject(config.projectId);
178
157
 
179
- // Add dev key header if provided (bypasses rate limits in development)
180
- // See: https://appwrite.io/docs/advanced/platform/rate-limits#dev-keys
158
+ // Dev key header bypasses rate limits in development.
181
159
  if (config.devKey) {
182
160
  appwriteClient.headers['X-Appwrite-Dev-Key'] = config.devKey;
183
161
  }
@@ -185,7 +163,6 @@ async function getAppwriteClient() {
185
163
  appwriteAccount = new window.Appwrite.Account(appwriteClient);
186
164
  appwriteTeams = new window.Appwrite.Teams(appwriteClient);
187
165
 
188
- // Initialize Users service if available (for fetching user details)
189
166
  if (window.Appwrite.Users) {
190
167
  appwriteUsers = new window.Appwrite.Users(appwriteClient);
191
168
  }
@@ -195,9 +172,9 @@ async function getAppwriteClient() {
195
172
  client: appwriteClient,
196
173
  account: appwriteAccount,
197
174
  teams: appwriteTeams,
198
- users: appwriteUsers, // Add users service for fetching user details
199
- functions: window.Appwrite?.Functions ? new window.Appwrite.Functions(appwriteClient) : null, // For guest-migration function calls
200
- realtime: window.Appwrite?.Realtime ? new window.Appwrite.Realtime(appwriteClient) : null // Realtime service for subscriptions
175
+ users: appwriteUsers,
176
+ functions: window.Appwrite?.Functions ? new window.Appwrite.Functions(appwriteClient) : null,
177
+ realtime: window.Appwrite?.Realtime ? new window.Appwrite.Realtime(appwriteClient) : null
201
178
  };
202
179
  }
203
180
 
@@ -224,12 +201,8 @@ function initializeAuthStore() {
224
201
  // Cross-tab synchronization using localStorage events
225
202
  const STORAGE_KEY = 'manifest:auth:state';
226
203
 
227
- // Whitelist of Appwrite Session fields safe to mirror across tabs.
228
- // CRITICALLY excludes `secret` (the bearer credential), `providerAccessToken`,
229
- // `providerRefreshToken`, and `providerAccessTokenExpiry`. The cookie set
230
- // on the Appwrite domain is the actual auth of record; this localStorage
231
- // copy only supports UI cross-tab sync ("someone just logged in here").
232
- // An XSS on this origin must not be able to lift session secrets out.
204
+ // Session fields safe to mirror across tabs. Excludes `secret` and provider
205
+ // tokens this copy is only for UI cross-tab sync, not the auth of record.
233
206
  const SAFE_SESSION_FIELDS = [
234
207
  '$id', 'userId', 'provider', 'expire', 'current',
235
208
  'clientName', 'osName', 'osCode', 'deviceName',
@@ -429,8 +402,7 @@ function initializeAuthStore() {
429
402
 
430
403
  // Get personal team (convenience getter - returns first default team)
431
404
  get personalTeam() {
432
- // This is async, so we can't use a getter directly
433
- // Return null and let users call getPersonalTeam() or getDefaultTeams() directly
405
+ // Lookup is async; use getPersonalTeam()/getDefaultTeams() instead
434
406
  return null;
435
407
  },
436
408
 
@@ -450,19 +422,16 @@ function initializeAuthStore() {
450
422
  }
451
423
  },
452
424
 
453
- // Get OAuth provider name (google, github, etc.) or null for non-OAuth methods
454
- // Uses stored provider from loginOAuth() call, or falls back to session.provider
455
- // For existing sessions without stored provider, triggers async fetch from Appwrite identities
425
+ // OAuth provider name (google, github, ), or null for non-OAuth methods.
426
+ // Reads stored provider, else falls back to session.provider / identities fetch.
456
427
  getProvider() {
457
428
  if (!this.session) {
458
429
  return null;
459
430
  }
460
431
  const sessionProvider = this.session.provider;
461
432
 
462
- // For OAuth, return the stored provider name (google, github, etc.)
463
- // session.provider returns "oauth2" generically, so we use _oauthProvider.
464
- // Only OAuth sessions have a provider name — magic/otp/phone/anonymous don't,
465
- // so gate on getMethod() to avoid a pointless identities lookup for those.
433
+ // session.provider is generically "oauth2", so use _oauthProvider. Gate on
434
+ // getMethod() so non-OAuth sessions skip the pointless identities lookup.
466
435
  if (this.getMethod() === 'oauth') {
467
436
  // Try to get from store first, then localStorage, then sessionStorage
468
437
  let provider = this._oauthProvider;
@@ -568,8 +537,7 @@ function initializeAuthStore() {
568
537
  this.isAuthenticated = true;
569
538
  this.isAnonymous = currentSession.provider === 'anonymous';
570
539
 
571
- // Restore OAuth provider from localStorage if available (persists across redirects)
572
- // This ensures provider name persists across page refreshes
540
+ // Restore OAuth provider from storage (persists across redirects/refresh)
573
541
  if (!this.isAnonymous && currentSession.provider !== 'magic-url') {
574
542
  try {
575
543
  // Try localStorage first (persists across redirects), fallback to sessionStorage
@@ -602,10 +570,8 @@ function initializeAuthStore() {
602
570
  this.isAnonymous = false;
603
571
  }
604
572
 
605
- // NOTE: team loading is intentionally NOT awaited here it runs in
606
- // the background after manifest:auth:initialized fires (see below),
607
- // so a session gate (app splash) isn't held up by team listing +
608
- // member hydration.
573
+ // Team loading is deferred (not awaited) — runs in the background after
574
+ // manifest:auth:initialized so a session gate isn't held up by it.
609
575
  } catch (error) {
610
576
  // No existing session - this is expected
611
577
  this.isAuthenticated = false;
@@ -617,11 +583,8 @@ function initializeAuthStore() {
617
583
  // Sync state to localStorage
618
584
  syncStateToStorage(this);
619
585
  } catch (error) {
620
- // init() is a passive lifecycle step (session restore / setup), not a
621
- // user action. Resolve to a clean signed-out state and log — do NOT park
622
- // the raw message in $auth.error, which is reserved for sign-in/action
623
- // failures the user can act on (a sign-in card binding $auth.error must
624
- // never show benign "no session" / "blocked account" scope errors).
586
+ // Passive lifecycle step: resolve to signed-out and log. Don't set
587
+ // $auth.error that's reserved for user-actionable sign-in failures.
625
588
  console.warn('[Manifest Appwrite Auth] Session restore failed (treating as signed out):', error?.message || error);
626
589
  this.isAuthenticated = false;
627
590
  this.isAnonymous = false;
@@ -630,9 +593,8 @@ function initializeAuthStore() {
630
593
  this._initialized = true;
631
594
  this._initializing = false;
632
595
 
633
- // Dispatch initialized event - let callback handlers process after.
634
- // Fires as soon as identity is known (one round-trip), NOT after teams
635
- // load — so a session gate / splash clears in a few hundred ms.
596
+ // Fire as soon as identity is known, before teams load, so a session
597
+ // gate / splash clears in a few hundred ms.
636
598
  window.dispatchEvent(new CustomEvent('manifest:auth:initialized', {
637
599
  detail: {
638
600
  isAuthenticated: this.isAuthenticated,
@@ -641,9 +603,8 @@ function initializeAuthStore() {
641
603
  }));
642
604
  }
643
605
 
644
- // Background, non-blocking: load + seed teams AFTER initialized fired.
645
- // $auth.teams / currentTeam populate reactively when ready; a
646
- // manifest:auth:teams-loaded event fires for anything that needs the full set.
606
+ // Background load + seed teams after init. Populates reactively; fires
607
+ // manifest:auth:teams-loaded for anything needing the full set.
647
608
  if (appwriteConfig && this.isAuthenticated && appwriteConfig.teams
648
609
  && (!this.isAnonymous || appwriteConfig.guestTeams)) {
649
610
  this._loadTeamsAndSeed(appwriteConfig)
@@ -654,11 +615,8 @@ function initializeAuthStore() {
654
615
  }
655
616
  },
656
617
 
657
- // Clear all team-related state. Used when the active identity changes to a
658
- // DIFFERENT user e.g. a guest replaced by a fresh account on OTP sign-in
659
- // (Appwrite can't convert anonymous → OTP). Without this, the previous user's
660
- // currentTeam/teams leak into the new session and listTeams queries teams the
661
- // new user can't access, producing 404s on prefs/memberships.
618
+ // Clear team state when the identity changes to a different user (e.g. guest
619
+ // replaced on OTP sign-in). Otherwise stale currentTeam/teams cause 404s.
662
620
  _resetTeamsState() {
663
621
  this.teams = [];
664
622
  this.currentTeam = null;
@@ -671,10 +629,8 @@ function initializeAuthStore() {
671
629
  }
672
630
  },
673
631
 
674
- // Call the deployed guest-migration function (templates/guest-migration-function).
675
- // The current Appwrite session authenticates the callAppwrite forwards the
676
- // user id to the function. Returns the parsed JSON response, or null on any
677
- // failure (migration is best-effort: a failure must never block sign-in).
632
+ // Call the deployed guest-migration function; the current session authenticates it.
633
+ // Returns parsed JSON, or null on failure (best-effort never blocks sign-in).
678
634
  async _callGuestMigration(path, body) {
679
635
  const appwriteConfig = await config.getAppwriteConfig();
680
636
  const fnId = appwriteConfig?.guestMigrationFunctionId;
@@ -693,17 +649,15 @@ function initializeAuthStore() {
693
649
  }
694
650
  },
695
651
 
696
- // Load the user's teams and seed any configured default (permanent/template)
697
- // teams. Shared by the guest, magic-link, OAuth, and init/restore paths.
652
+ // Load the user's teams and seed configured defaults. Shared by the guest,
653
+ // magic-link, OAuth, and init/restore paths.
698
654
  async _loadTeamsAndSeed(appwriteConfig) {
699
655
  const cfg = appwriteConfig || await config.getAppwriteConfig();
700
656
  if (!cfg?.teams) {
701
657
  return;
702
658
  }
703
- // Startup race: init() (and an early requestGuest) can reach here before
704
- // teams.core.js / teams.defaults.js have finished wiring listTeams +
705
- // ensureDefaultTeams onto the store. Wait briefly for them rather than
706
- // silently skipping (which left guests with no teams on reload).
659
+ // Startup race: we can arrive before teams.core/defaults wire listTeams +
660
+ // ensureDefaultTeams onto the store. Wait briefly rather than skip.
707
661
  const needsSeed = !!(cfg.permanentTeams || cfg.templateTeams);
708
662
  const ready = () => typeof this.listTeams === 'function'
709
663
  && (!needsSeed || typeof window.ManifestAppwriteAuthTeamsDefaults?.ensureDefaultTeams === 'function');
@@ -763,9 +717,8 @@ function initializeAuthStore() {
763
717
  // Ignore
764
718
  }
765
719
 
766
- // Guests are full Appwrite sessions, so they can own teams. When
767
- // auth.teams.guests is enabled, seed their default teams just like a
768
- // signed-in user; otherwise keep the historical behavior (no teams).
720
+ // Guests are full sessions and can own teams: seed defaults when
721
+ // guestTeams is enabled, otherwise none.
769
722
  const cfg = await config.getAppwriteConfig();
770
723
  if (cfg?.guestTeams) {
771
724
  await this._loadTeamsAndSeed(cfg);
@@ -856,18 +809,8 @@ function initializeAuthStore() {
856
809
  this.teams = [];
857
810
  this.currentTeam = null;
858
811
 
859
- // Clear deleted teams tracking for this user (optional - uncomment if you want to clear on logout)
860
- // try {
861
- // const userId = this.user?.$id;
862
- // if (userId) {
863
- // localStorage.removeItem(`manifest:deleted-teams:${userId}`);
864
- // }
865
- // } catch (e) {
866
- // // Ignore
867
- // }
868
-
869
- // Restore to guest state after logout (if guest-auto is enabled)
870
- // This only applies to non-guest sessions - if logging out from guest, don't create a new guest
812
+ // Restore to guest state after logout (guest-auto only, and not when
813
+ // already a guest — don't mint a new guest on top).
871
814
  if (!this.isAnonymous && this._guestAuto && this._createAnonymousSession) {
872
815
  await this._createAnonymousSession();
873
816
  } else {
@@ -4773,19 +4716,8 @@ function initializeTeamsConvenience() {
4773
4716
  const waitForStore = () => {
4774
4717
  const store = Alpine.store('auth');
4775
4718
  if (store) {
4776
- // Decide whether to (re)attach convenience methods. Use a sentinel
4777
- // that ONLY this module defines `createTeamFromName`. The earlier
4778
- // sentinel (`isCreatingTeam`) was unreliable: the store itself
4779
- // defines an `isCreatingTeam()` stub at init (see manifest.appwrite
4780
- // .auth.store.js — the "Stub team convenience methods" block), so
4781
- // `typeof store.isCreatingTeam === 'function'` is true BEFORE this
4782
- // module runs. The check then fired false-positive and skipped the
4783
- // whole `if (needsReinitialization)` block below, leaving the real
4784
- // convenience methods (startEditingMember, createTeamFromName,
4785
- // cancelEditingMember, saveEditingMember, deleteMember, leaveTeam,
4786
- // toggleInviteRole, etc.) unattached — surfacing as
4787
- // "$auth.startEditingMember is not a function" the moment a user
4788
- // clicked an edit-member button.
4719
+ // Sentinel must be a method ONLY this module defines. isCreatingTeam
4720
+ // won't do the store stubs it at init, so it reads present too early.
4789
4721
  const needsReinitialization = typeof store.createTeamFromName !== 'function';
4790
4722
 
4791
4723
  // Ensure cache properties are initialized (methods are already in store)
@@ -4867,8 +4799,7 @@ function initializeTeamsConvenience() {
4867
4799
  };
4868
4800
  }
4869
4801
 
4870
- // CRITICAL: Check if convenience methods exist - use isCreatingTeam as the key check
4871
- // This ensures methods are re-added if the store was replaced or methods were lost after idle
4802
+ // Re-attach methods if the store was replaced or methods were lost after idle
4872
4803
  if (needsReinitialization) {
4873
4804
  // Convenience method: create team using newTeamName property
4874
4805
  store.createTeamFromName = async function () {
@@ -5398,9 +5329,8 @@ function initializeTeamsConvenience() {
5398
5329
  const allRoles = this.allTeamRoles({ $id: teamId });
5399
5330
  const permissions = allRoles && allRoles[roleName] ? [...allRoles[roleName]] : [];
5400
5331
 
5401
- // Set editing state SYNCHRONOUSLY before the async dropdown fetch
5402
- // below so a caller that mutates editingRole.permissions immediately
5403
- // after this isn't racing (and getting overwritten by) that fetch.
5332
+ // Set state synchronously, before the async fetch below, so a caller
5333
+ // mutating editingRole.permissions right after isn't overwritten.
5404
5334
  this.editingRole = {
5405
5335
  teamId: teamId,
5406
5336
  oldRoleName: roleName,
@@ -5419,12 +5349,8 @@ function initializeTeamsConvenience() {
5419
5349
  // The UI will use editingRole.permissions or pendingPermissions for existing roles
5420
5350
  };
5421
5351
 
5422
- // Reactive-safe role-permission write. Takes a plain array and persists
5423
- // straight to team prefs via updateUserRole — WITHOUT going through the
5424
- // reactive editingRole object. Use this for programmatic edits; mutating
5425
- // $auth.editingRole.permissions directly is fragile under Alpine (the
5426
- // $auth proxy wraps the already-reactive editingRole and can recurse on
5427
- // get). updateUserRole refreshes the role cache, so allTeamRoles() re-reads.
5352
+ // Reactive-safe write: persist via updateUserRole, bypassing the reactive
5353
+ // editingRole (mutating $auth.editingRole.permissions directly recurses).
5428
5354
  store.updateRolePermissions = async function (teamId, roleName, permissions) {
5429
5355
  if (!this.updateUserRole) {
5430
5356
  return { success: false, error: 'Roles module not ready' };
@@ -5435,9 +5361,8 @@ function initializeTeamsConvenience() {
5435
5361
  return await this.updateUserRole(teamId, roleName, plain);
5436
5362
  };
5437
5363
 
5438
- // Safely set the permissions on the in-progress edit (replaces editingRole
5439
- // with a fresh object holding a plain array, rather than mutating the
5440
- // reactive nested array). Pair with saveEditingRole().
5364
+ // Set permissions on the in-progress edit by replacing editingRole with a
5365
+ // fresh object (not mutating the reactive nested array). Pair with saveEditingRole().
5441
5366
  store.setEditingPermissions = function (permissions) {
5442
5367
  if (!this.editingRole) {
5443
5368
  return;
@@ -5541,10 +5466,8 @@ function initializeTeamsConvenience() {
5541
5466
  return userRoles.includes('owner');
5542
5467
  };
5543
5468
 
5544
- // Synchronous version for Alpine.js bindings (x-show / :disabled).
5545
- // Resolves from the cached role definitions (allTeamRoles) so it matches
5546
- // the async hasTeamPermission() — including custom permission keys, not
5547
- // just the six built-ins the permission cache pre-computes.
5469
+ // Sync version for Alpine bindings. Resolves from cached allTeamRoles so it
5470
+ // matches async hasTeamPermission, including custom (non-built-in) keys.
5548
5471
  store.hasTeamPermissionSync = function (permission) {
5549
5472
  if (!this.currentTeam || !this.currentTeamMemberships || !this.user) {
5550
5473
  return false;
@@ -5616,12 +5539,9 @@ function initializeTeamsConvenience() {
5616
5539
  return userRoles;
5617
5540
  };
5618
5541
 
5619
- } // End of if (!store.isCreatingTeam || typeof store.isCreatingTeam !== 'function')
5620
-
5621
- // Note: hasPermission from roles module takes (userRoles, permission, teamId)
5622
- // hasTeamPermission is the convenience wrapper for current team
5542
+ } // End needsReinitialization
5623
5543
 
5624
- // Update cache when team is viewed (wrap existing viewTeam if it exists)
5544
+ // Wrap viewTeam to refresh permission cache after a team is viewed
5625
5545
  if (store.viewTeam && !store._viewTeamWrapped) {
5626
5546
  const originalViewTeam = store.viewTeam;
5627
5547
  store.viewTeam = async function (team) {
@@ -6232,15 +6152,10 @@ window.ManifestAppwriteAuthMagicLinks = {
6232
6152
 
6233
6153
  /* Auth email OTP (one-time passcode) */
6234
6154
 
6235
- // Email OTP is a two-step, in-page flow (no redirect, unlike magic links):
6236
- // 1. createEmailOTP(email) -> Appwrite emails a 6-digit code, returns a userId
6237
- // 2. verifyOTP(code) -> createSession(userId, code) completes login
6238
- // Because there is no URL round-trip, this module never touches users.callbacks.js.
6239
- //
6240
- // NOTE: Appwrite does NOT support converting an anonymous (guest) session via email
6241
- // OTP — only magic links, phone, email/password, and OAuth can do that. So when a
6242
- // guest verifies an OTP we mint a fresh account (the anonymous session is deleted),
6243
- // which discards any guest-created teams. Use magic links if you need guest upgrade.
6155
+ // Two-step in-page flow (no redirect): createEmailOTP(email) emails a code + returns
6156
+ // a userId, verifyOTP(code) creates the session.
6157
+ // Gotcha: Appwrite can't convert an anonymous guest via OTP — a guest verifying an OTP
6158
+ // gets a fresh account (guest teams lost). Use magic links for guest upgrade.
6244
6159
 
6245
6160
  function initializeEmailOTP() {
6246
6161
  if (typeof Alpine === 'undefined') {
@@ -6252,9 +6167,8 @@ function initializeEmailOTP() {
6252
6167
  return;
6253
6168
  }
6254
6169
 
6255
- // Resolve an email string from the same range of inputs sendMagicLink accepts:
6256
- // an input element, a selector, an Alpine { email } object, a bare string, or
6257
- // nothing (auto-find the nearest email input). Returns { email, inputEl, dataObj }.
6170
+ // Resolve an email from an input/selector/{ email } object/string, or auto-find
6171
+ // the nearest email input. Returns { email, inputEl, dataObj }.
6258
6172
  function resolveEmailInput(emailInputOrRef) {
6259
6173
  let email = null;
6260
6174
  let inputEl = null;
@@ -6302,9 +6216,8 @@ function initializeEmailOTP() {
6302
6216
  const waitForStore = () => {
6303
6217
  const store = Alpine.store('auth');
6304
6218
  if (store && !store.createEmailOTP) {
6305
- // Step 1: request a one-time passcode by email.
6306
- // Pass { phrase: true } to enable Appwrite's security phrase (anti-phishing);
6307
- // when enabled the phrase is stored on the store as `otpPhrase` for display.
6219
+ // Step 1: email a passcode. { phrase: true } enables Appwrite's anti-phishing
6220
+ // security phrase, surfaced on the store as `otpPhrase`.
6308
6221
  store.createEmailOTP = async function (email, options = {}) {
6309
6222
  if (!this._appwrite) {
6310
6223
  this._appwrite = await config.getAppwriteClient();
@@ -6323,8 +6236,7 @@ function initializeEmailOTP() {
6323
6236
  return { success: false, error: 'Email OTP authentication is not enabled' };
6324
6237
  }
6325
6238
 
6326
- // Appwrite can't convert an anonymous account via OTP. Warn (once) so guest
6327
- // teams aren't silently lost; the login still proceeds as a fresh account.
6239
+ // OTP can't convert a guest warn so lost guest teams aren't a surprise.
6328
6240
  if (this.isAnonymous && appwriteConfig?.guestUpgrade) {
6329
6241
  console.warn('[Manifest Appwrite Auth] Email 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.');
6330
6242
  }
@@ -6359,8 +6271,8 @@ function initializeEmailOTP() {
6359
6271
 
6360
6272
  return { success: true, message: 'OTP sent to email', phrase: this.otpPhrase };
6361
6273
  } catch (error) {
6362
- // Appwrite returns 501 Not Implemented when Email OTP isn't enabled
6363
- // for the project. Surface an actionable message instead of the raw error.
6274
+ // Appwrite returns 501 when Email OTP isn't enabled — surface an
6275
+ // actionable message rather than the raw error.
6364
6276
  const code = error.code || error.statusCode;
6365
6277
  const notEnabled = code === 501 || /not implemented/i.test(error.message || '');
6366
6278
  this.error = notEnabled
@@ -6421,10 +6333,8 @@ function initializeEmailOTP() {
6421
6333
  const appwriteConfig = await config.getAppwriteConfig();
6422
6334
  const wasGuest = this.isAnonymous;
6423
6335
 
6424
- // Guest team carryover: OTP can't convert the anonymous account, so we
6425
- // migrate its teams to the new account instead. Issue the migration
6426
- // ticket NOW, while the guest session is still authenticated — it's
6427
- // redeemed after the new session exists. Best-effort; never blocks login.
6336
+ // Guest team carryover: issue the migration ticket now, while the guest
6337
+ // session is still authenticated; redeemed after the new session exists.
6428
6338
  let migrationTicket = null;
6429
6339
  if (wasGuest && appwriteConfig?.guestMigrationFunctionId && this._callGuestMigration) {
6430
6340
  const prep = await this._callGuestMigration('/prepare', {});
@@ -6452,16 +6362,14 @@ function initializeEmailOTP() {
6452
6362
  this._otpUserId = null;
6453
6363
  this.error = null;
6454
6364
 
6455
- // OTP replaces any prior guest with a different account (no conversion),
6456
- // so clear the guest's team state before loading the new user's teams —
6457
- // otherwise the stale currentTeam triggers 404s in listTeams.
6365
+ // Clear the guest's team state before loading the new user's teams,
6366
+ // else the stale currentTeam triggers 404s in listTeams.
6458
6367
  if (this._resetTeamsState) {
6459
6368
  this._resetTeamsState();
6460
6369
  }
6461
6370
 
6462
- // Redeem the migration ticket as the new account: carries the guest's
6463
- // teams over. Best-effort a failure leaves the guest's teams for GC
6464
- // but never blocks the (already successful) sign-in.
6371
+ // Redeem the migration ticket as the new account to carry teams over.
6372
+ // Best-effort; failure never blocks the already-successful sign-in.
6465
6373
  if (migrationTicket && this._callGuestMigration) {
6466
6374
  await this._callGuestMigration('/commit', { ticket: migrationTicket });
6467
6375
  }
@@ -1214,9 +1214,7 @@ window.ManifestDataQueries = {
1214
1214
  };
1215
1215
 
1216
1216
 
1217
- /* Manifest Data Sources - Pagination */
1218
-
1219
- // Pagination helper functions for Appwrite data sources
1217
+ /* Manifest Data Sources - Pagination (Appwrite) */
1220
1218
 
1221
1219
  /**
1222
1220
  * Get first page of results (cursor-based)