stellar-drive 1.0.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/README.md +607 -0
- package/dist/actions/remoteChange.d.ts +204 -0
- package/dist/actions/remoteChange.d.ts.map +1 -0
- package/dist/actions/remoteChange.js +424 -0
- package/dist/actions/remoteChange.js.map +1 -0
- package/dist/actions/truncateTooltip.d.ts +56 -0
- package/dist/actions/truncateTooltip.d.ts.map +1 -0
- package/dist/actions/truncateTooltip.js +312 -0
- package/dist/actions/truncateTooltip.js.map +1 -0
- package/dist/auth/crypto.d.ts +41 -0
- package/dist/auth/crypto.d.ts.map +1 -0
- package/dist/auth/crypto.js +50 -0
- package/dist/auth/crypto.js.map +1 -0
- package/dist/auth/deviceVerification.d.ts +283 -0
- package/dist/auth/deviceVerification.d.ts.map +1 -0
- package/dist/auth/deviceVerification.js +575 -0
- package/dist/auth/deviceVerification.js.map +1 -0
- package/dist/auth/displayUtils.d.ts +98 -0
- package/dist/auth/displayUtils.d.ts.map +1 -0
- package/dist/auth/displayUtils.js +145 -0
- package/dist/auth/displayUtils.js.map +1 -0
- package/dist/auth/loginGuard.d.ts +134 -0
- package/dist/auth/loginGuard.d.ts.map +1 -0
- package/dist/auth/loginGuard.js +276 -0
- package/dist/auth/loginGuard.js.map +1 -0
- package/dist/auth/offlineCredentials.d.ts +105 -0
- package/dist/auth/offlineCredentials.d.ts.map +1 -0
- package/dist/auth/offlineCredentials.js +176 -0
- package/dist/auth/offlineCredentials.js.map +1 -0
- package/dist/auth/offlineSession.d.ts +96 -0
- package/dist/auth/offlineSession.d.ts.map +1 -0
- package/dist/auth/offlineSession.js +145 -0
- package/dist/auth/offlineSession.js.map +1 -0
- package/dist/auth/resolveAuthState.d.ts +85 -0
- package/dist/auth/resolveAuthState.d.ts.map +1 -0
- package/dist/auth/resolveAuthState.js +249 -0
- package/dist/auth/resolveAuthState.js.map +1 -0
- package/dist/auth/singleUser.d.ts +498 -0
- package/dist/auth/singleUser.d.ts.map +1 -0
- package/dist/auth/singleUser.js +1282 -0
- package/dist/auth/singleUser.js.map +1 -0
- package/dist/bin/commands.d.ts +14 -0
- package/dist/bin/commands.d.ts.map +1 -0
- package/dist/bin/commands.js +68 -0
- package/dist/bin/commands.js.map +1 -0
- package/dist/bin/install-pwa.d.ts +41 -0
- package/dist/bin/install-pwa.d.ts.map +1 -0
- package/dist/bin/install-pwa.js +4594 -0
- package/dist/bin/install-pwa.js.map +1 -0
- package/dist/config.d.ts +249 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +395 -0
- package/dist/config.js.map +1 -0
- package/dist/conflicts.d.ts +306 -0
- package/dist/conflicts.d.ts.map +1 -0
- package/dist/conflicts.js +807 -0
- package/dist/conflicts.js.map +1 -0
- package/dist/crdt/awareness.d.ts +128 -0
- package/dist/crdt/awareness.d.ts.map +1 -0
- package/dist/crdt/awareness.js +284 -0
- package/dist/crdt/awareness.js.map +1 -0
- package/dist/crdt/channel.d.ts +165 -0
- package/dist/crdt/channel.d.ts.map +1 -0
- package/dist/crdt/channel.js +522 -0
- package/dist/crdt/channel.js.map +1 -0
- package/dist/crdt/config.d.ts +58 -0
- package/dist/crdt/config.d.ts.map +1 -0
- package/dist/crdt/config.js +123 -0
- package/dist/crdt/config.js.map +1 -0
- package/dist/crdt/helpers.d.ts +104 -0
- package/dist/crdt/helpers.d.ts.map +1 -0
- package/dist/crdt/helpers.js +116 -0
- package/dist/crdt/helpers.js.map +1 -0
- package/dist/crdt/offline.d.ts +58 -0
- package/dist/crdt/offline.d.ts.map +1 -0
- package/dist/crdt/offline.js +130 -0
- package/dist/crdt/offline.js.map +1 -0
- package/dist/crdt/persistence.d.ts +65 -0
- package/dist/crdt/persistence.d.ts.map +1 -0
- package/dist/crdt/persistence.js +171 -0
- package/dist/crdt/persistence.js.map +1 -0
- package/dist/crdt/provider.d.ts +109 -0
- package/dist/crdt/provider.d.ts.map +1 -0
- package/dist/crdt/provider.js +543 -0
- package/dist/crdt/provider.js.map +1 -0
- package/dist/crdt/store.d.ts +111 -0
- package/dist/crdt/store.d.ts.map +1 -0
- package/dist/crdt/store.js +158 -0
- package/dist/crdt/store.js.map +1 -0
- package/dist/crdt/types.d.ts +281 -0
- package/dist/crdt/types.d.ts.map +1 -0
- package/dist/crdt/types.js +26 -0
- package/dist/crdt/types.js.map +1 -0
- package/dist/data.d.ts +502 -0
- package/dist/data.d.ts.map +1 -0
- package/dist/data.js +862 -0
- package/dist/data.js.map +1 -0
- package/dist/database.d.ts +153 -0
- package/dist/database.d.ts.map +1 -0
- package/dist/database.js +325 -0
- package/dist/database.js.map +1 -0
- package/dist/debug.d.ts +87 -0
- package/dist/debug.d.ts.map +1 -0
- package/dist/debug.js +135 -0
- package/dist/debug.js.map +1 -0
- package/dist/demo.d.ts +131 -0
- package/dist/demo.d.ts.map +1 -0
- package/dist/demo.js +168 -0
- package/dist/demo.js.map +1 -0
- package/dist/deviceId.d.ts +47 -0
- package/dist/deviceId.d.ts.map +1 -0
- package/dist/deviceId.js +106 -0
- package/dist/deviceId.js.map +1 -0
- package/dist/diagnostics.d.ts +292 -0
- package/dist/diagnostics.d.ts.map +1 -0
- package/dist/diagnostics.js +378 -0
- package/dist/diagnostics.js.map +1 -0
- package/dist/engine.d.ts +230 -0
- package/dist/engine.d.ts.map +1 -0
- package/dist/engine.js +2636 -0
- package/dist/engine.js.map +1 -0
- package/dist/entries/actions.d.ts +16 -0
- package/dist/entries/actions.d.ts.map +1 -0
- package/dist/entries/actions.js +29 -0
- package/dist/entries/actions.js.map +1 -0
- package/dist/entries/auth.d.ts +19 -0
- package/dist/entries/auth.d.ts.map +1 -0
- package/dist/entries/auth.js +50 -0
- package/dist/entries/auth.js.map +1 -0
- package/dist/entries/config.d.ts +15 -0
- package/dist/entries/config.d.ts.map +1 -0
- package/dist/entries/config.js +20 -0
- package/dist/entries/config.js.map +1 -0
- package/dist/entries/crdt.d.ts +32 -0
- package/dist/entries/crdt.d.ts.map +1 -0
- package/dist/entries/crdt.js +52 -0
- package/dist/entries/crdt.js.map +1 -0
- package/dist/entries/kit.d.ts +22 -0
- package/dist/entries/kit.d.ts.map +1 -0
- package/dist/entries/kit.js +58 -0
- package/dist/entries/kit.js.map +1 -0
- package/dist/entries/stores.d.ts +22 -0
- package/dist/entries/stores.d.ts.map +1 -0
- package/dist/entries/stores.js +57 -0
- package/dist/entries/stores.js.map +1 -0
- package/dist/entries/types.d.ts +23 -0
- package/dist/entries/types.d.ts.map +1 -0
- package/dist/entries/types.js +12 -0
- package/dist/entries/types.js.map +1 -0
- package/dist/entries/utils.d.ts +12 -0
- package/dist/entries/utils.d.ts.map +1 -0
- package/dist/entries/utils.js +42 -0
- package/dist/entries/utils.js.map +1 -0
- package/dist/entries/vite.d.ts +20 -0
- package/dist/entries/vite.d.ts.map +1 -0
- package/dist/entries/vite.js +26 -0
- package/dist/entries/vite.js.map +1 -0
- package/dist/index.d.ts +77 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +234 -0
- package/dist/index.js.map +1 -0
- package/dist/kit/auth.d.ts +80 -0
- package/dist/kit/auth.d.ts.map +1 -0
- package/dist/kit/auth.js +75 -0
- package/dist/kit/auth.js.map +1 -0
- package/dist/kit/confirm.d.ts +111 -0
- package/dist/kit/confirm.d.ts.map +1 -0
- package/dist/kit/confirm.js +169 -0
- package/dist/kit/confirm.js.map +1 -0
- package/dist/kit/loads.d.ts +187 -0
- package/dist/kit/loads.d.ts.map +1 -0
- package/dist/kit/loads.js +208 -0
- package/dist/kit/loads.js.map +1 -0
- package/dist/kit/server.d.ts +175 -0
- package/dist/kit/server.d.ts.map +1 -0
- package/dist/kit/server.js +297 -0
- package/dist/kit/server.js.map +1 -0
- package/dist/kit/sw.d.ts +176 -0
- package/dist/kit/sw.d.ts.map +1 -0
- package/dist/kit/sw.js +320 -0
- package/dist/kit/sw.js.map +1 -0
- package/dist/queue.d.ts +306 -0
- package/dist/queue.d.ts.map +1 -0
- package/dist/queue.js +925 -0
- package/dist/queue.js.map +1 -0
- package/dist/realtime.d.ts +280 -0
- package/dist/realtime.d.ts.map +1 -0
- package/dist/realtime.js +1031 -0
- package/dist/realtime.js.map +1 -0
- package/dist/runtime/runtimeConfig.d.ts +110 -0
- package/dist/runtime/runtimeConfig.d.ts.map +1 -0
- package/dist/runtime/runtimeConfig.js +260 -0
- package/dist/runtime/runtimeConfig.js.map +1 -0
- package/dist/schema.d.ts +150 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +891 -0
- package/dist/schema.js.map +1 -0
- package/dist/stores/authState.d.ts +204 -0
- package/dist/stores/authState.d.ts.map +1 -0
- package/dist/stores/authState.js +336 -0
- package/dist/stores/authState.js.map +1 -0
- package/dist/stores/factories.d.ts +140 -0
- package/dist/stores/factories.d.ts.map +1 -0
- package/dist/stores/factories.js +157 -0
- package/dist/stores/factories.js.map +1 -0
- package/dist/stores/network.d.ts +48 -0
- package/dist/stores/network.d.ts.map +1 -0
- package/dist/stores/network.js +261 -0
- package/dist/stores/network.js.map +1 -0
- package/dist/stores/remoteChanges.d.ts +417 -0
- package/dist/stores/remoteChanges.d.ts.map +1 -0
- package/dist/stores/remoteChanges.js +626 -0
- package/dist/stores/remoteChanges.js.map +1 -0
- package/dist/stores/sync.d.ts +165 -0
- package/dist/stores/sync.d.ts.map +1 -0
- package/dist/stores/sync.js +275 -0
- package/dist/stores/sync.js.map +1 -0
- package/dist/supabase/auth.d.ts +219 -0
- package/dist/supabase/auth.d.ts.map +1 -0
- package/dist/supabase/auth.js +459 -0
- package/dist/supabase/auth.js.map +1 -0
- package/dist/supabase/client.d.ts +88 -0
- package/dist/supabase/client.d.ts.map +1 -0
- package/dist/supabase/client.js +313 -0
- package/dist/supabase/client.js.map +1 -0
- package/dist/supabase/validate.d.ts +118 -0
- package/dist/supabase/validate.d.ts.map +1 -0
- package/dist/supabase/validate.js +208 -0
- package/dist/supabase/validate.js.map +1 -0
- package/dist/sw/build/vite-plugin.d.ts +149 -0
- package/dist/sw/build/vite-plugin.d.ts.map +1 -0
- package/dist/sw/build/vite-plugin.js +517 -0
- package/dist/sw/build/vite-plugin.js.map +1 -0
- package/dist/sw/sw.js +664 -0
- package/dist/types.d.ts +363 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +18 -0
- package/dist/types.js.map +1 -0
- package/dist/utils.d.ts +85 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +156 -0
- package/dist/utils.js.map +1 -0
- package/package.json +117 -0
- package/src/components/DeferredChangesBanner.svelte +477 -0
- package/src/components/DemoBanner.svelte +110 -0
- package/src/components/SyncStatus.svelte +1732 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"displayUtils.d.ts","sourceRoot":"","sources":["../../src/auth/displayUtils.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,uBAAuB,CAAC;AACrD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,UAAU,CAAC;AAQnD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,wBAAgB,gBAAgB,CAC9B,OAAO,EAAE,OAAO,GAAG,IAAI,EACvB,cAAc,EAAE,kBAAkB,GAAG,IAAI,EACzC,QAAQ,GAAE,MAAmB,GAC5B,MAAM,CA6BR;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,aAAa,CAC3B,OAAO,EAAE,OAAO,GAAG,IAAI,EACvB,cAAc,EAAE,kBAAkB,GAAG,IAAI,GACxC,MAAM,CAaR;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,OAAO,GAAG,IAAI,EACvB,cAAc,EAAE,kBAAkB,GAAG,IAAI,EACzC,QAAQ,GAAE,MAAY,GACrB,MAAM,CAMR"}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Auth Display Utilities
|
|
3
|
+
*
|
|
4
|
+
* Pure helper functions that resolve user-facing display values (first name,
|
|
5
|
+
* user ID, avatar initial) from the auth state. Each function handles the
|
|
6
|
+
* full fallback chain across online (Supabase session) and offline (cached
|
|
7
|
+
* credentials) modes, so consuming components don't need to duplicate the
|
|
8
|
+
* resolution logic.
|
|
9
|
+
*
|
|
10
|
+
* Resolution strategy (consistent across all helpers):
|
|
11
|
+
* 1. Check the Supabase session (`Session.user`) first.
|
|
12
|
+
* 2. Fall back to the offline credential cache (`OfflineCredentials`).
|
|
13
|
+
* 3. Return a caller-provided fallback or a sensible default.
|
|
14
|
+
*
|
|
15
|
+
* These functions are stateless and framework-agnostic — they accept plain
|
|
16
|
+
* data and return plain values. Wrap them in `$derived` / `$derived.by` in
|
|
17
|
+
* Svelte 5 components to make them reactive.
|
|
18
|
+
*
|
|
19
|
+
* @module auth/displayUtils
|
|
20
|
+
*/
|
|
21
|
+
import { getUserProfile } from '../supabase/auth';
|
|
22
|
+
import { isDemoMode, getDemoConfig } from '../demo';
|
|
23
|
+
// =============================================================================
|
|
24
|
+
// PUBLIC API
|
|
25
|
+
// =============================================================================
|
|
26
|
+
/**
|
|
27
|
+
* Resolve the user's first name for greeting / display purposes.
|
|
28
|
+
*
|
|
29
|
+
* Fallback chain:
|
|
30
|
+
* 1. `firstName` / `first_name` from the Supabase session profile
|
|
31
|
+
* (extracted via `getUserProfile()`, which respects the app's
|
|
32
|
+
* `profileExtractor` config)
|
|
33
|
+
* 2. Email username (everything before `@`) from the Supabase session
|
|
34
|
+
* 3. `firstName` from the offline cached profile
|
|
35
|
+
* 4. Email username from the offline cached profile
|
|
36
|
+
* 5. The provided `fallback` string (default: `'Explorer'`)
|
|
37
|
+
*
|
|
38
|
+
* @param session - The current Supabase session, or `null`.
|
|
39
|
+
* @param offlineProfile - The cached offline credentials, or `null`.
|
|
40
|
+
* @param fallback - Value returned when no name can be resolved.
|
|
41
|
+
* Defaults to `'Explorer'`.
|
|
42
|
+
* @returns The resolved first name string.
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* ```ts
|
|
46
|
+
* // In a Svelte 5 component:
|
|
47
|
+
* const firstName = $derived(
|
|
48
|
+
* resolveFirstName($authState.session, $authState.offlineProfile)
|
|
49
|
+
* );
|
|
50
|
+
* ```
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* ```ts
|
|
54
|
+
* // With a custom fallback:
|
|
55
|
+
* const greeting = resolveFirstName(session, offline, 'there');
|
|
56
|
+
* // → "Hey, there!" when no name is available
|
|
57
|
+
* ```
|
|
58
|
+
*/
|
|
59
|
+
export function resolveFirstName(session, offlineProfile, fallback = 'Explorer') {
|
|
60
|
+
/* ── Demo: use mock profile from config ── */
|
|
61
|
+
if (isDemoMode()) {
|
|
62
|
+
const config = getDemoConfig();
|
|
63
|
+
if (config?.mockProfile.firstName) {
|
|
64
|
+
return config.mockProfile.firstName;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/* ── Online: check session profile fields ── */
|
|
68
|
+
if (session?.user) {
|
|
69
|
+
const profile = getUserProfile(session.user);
|
|
70
|
+
if (profile.firstName || profile.first_name) {
|
|
71
|
+
return (profile.firstName || profile.first_name);
|
|
72
|
+
}
|
|
73
|
+
if (session.user.email) {
|
|
74
|
+
return session.user.email.split('@')[0];
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
/* ── Offline: check cached credential profile ── */
|
|
78
|
+
if (offlineProfile?.profile?.firstName) {
|
|
79
|
+
return offlineProfile.profile.firstName;
|
|
80
|
+
}
|
|
81
|
+
if (offlineProfile?.email) {
|
|
82
|
+
return offlineProfile.email.split('@')[0];
|
|
83
|
+
}
|
|
84
|
+
return fallback;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Resolve the current user's UUID from auth state.
|
|
88
|
+
*
|
|
89
|
+
* Checks the Supabase session first, then falls back to the offline
|
|
90
|
+
* credential cache. Returns an empty string when no user is authenticated.
|
|
91
|
+
*
|
|
92
|
+
* @param session - The current Supabase session, or `null`.
|
|
93
|
+
* @param offlineProfile - The cached offline credentials, or `null`.
|
|
94
|
+
* @returns The user's UUID, or `''` if unauthenticated.
|
|
95
|
+
*
|
|
96
|
+
* @example
|
|
97
|
+
* ```ts
|
|
98
|
+
* const userId = resolveUserId(data.session, data.offlineProfile);
|
|
99
|
+
* if (!userId) {
|
|
100
|
+
* error = 'Not authenticated';
|
|
101
|
+
* return;
|
|
102
|
+
* }
|
|
103
|
+
* ```
|
|
104
|
+
*/
|
|
105
|
+
export function resolveUserId(session, offlineProfile) {
|
|
106
|
+
/* ── Demo: return synthetic user ID ── */
|
|
107
|
+
if (isDemoMode()) {
|
|
108
|
+
return 'demo-user';
|
|
109
|
+
}
|
|
110
|
+
if (session?.user?.id) {
|
|
111
|
+
return session.user.id;
|
|
112
|
+
}
|
|
113
|
+
if (offlineProfile?.userId) {
|
|
114
|
+
return offlineProfile.userId;
|
|
115
|
+
}
|
|
116
|
+
return '';
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Resolve a single uppercase initial letter for avatar display.
|
|
120
|
+
*
|
|
121
|
+
* Uses {@link resolveFirstName} to derive the name, then returns the
|
|
122
|
+
* first character uppercased. If the resolved name is empty, returns
|
|
123
|
+
* the `fallback` character.
|
|
124
|
+
*
|
|
125
|
+
* @param session - The current Supabase session, or `null`.
|
|
126
|
+
* @param offlineProfile - The cached offline credentials, or `null`.
|
|
127
|
+
* @param fallback - Character to use when no initial can be derived.
|
|
128
|
+
* Defaults to `'?'`.
|
|
129
|
+
* @returns A single uppercase character.
|
|
130
|
+
*
|
|
131
|
+
* @example
|
|
132
|
+
* ```svelte
|
|
133
|
+
* <span class="avatar">
|
|
134
|
+
* {resolveAvatarInitial($authState.session, $authState.offlineProfile)}
|
|
135
|
+
* </span>
|
|
136
|
+
* ```
|
|
137
|
+
*/
|
|
138
|
+
export function resolveAvatarInitial(session, offlineProfile, fallback = '?') {
|
|
139
|
+
const name = resolveFirstName(session, offlineProfile, '');
|
|
140
|
+
if (name) {
|
|
141
|
+
return name.charAt(0).toUpperCase();
|
|
142
|
+
}
|
|
143
|
+
return fallback;
|
|
144
|
+
}
|
|
145
|
+
//# sourceMappingURL=displayUtils.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"displayUtils.js","sourceRoot":"","sources":["../../src/auth/displayUtils.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAIH,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAClD,OAAO,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAEpD,gFAAgF;AAChF,aAAa;AACb,gFAAgF;AAEhF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,MAAM,UAAU,gBAAgB,CAC9B,OAAuB,EACvB,cAAyC,EACzC,WAAmB,UAAU;IAE7B,8CAA8C;IAC9C,IAAI,UAAU,EAAE,EAAE,CAAC;QACjB,MAAM,MAAM,GAAG,aAAa,EAAE,CAAC;QAC/B,IAAI,MAAM,EAAE,WAAW,CAAC,SAAS,EAAE,CAAC;YAClC,OAAO,MAAM,CAAC,WAAW,CAAC,SAAS,CAAC;QACtC,CAAC;IACH,CAAC;IAED,gDAAgD;IAChD,IAAI,OAAO,EAAE,IAAI,EAAE,CAAC;QAClB,MAAM,OAAO,GAAG,cAAc,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;QAC7C,IAAI,OAAO,CAAC,SAAS,IAAI,OAAO,CAAC,UAAU,EAAE,CAAC;YAC5C,OAAO,CAAC,OAAO,CAAC,SAAS,IAAI,OAAO,CAAC,UAAU,CAAW,CAAC;QAC7D,CAAC;QACD,IAAI,OAAO,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC;YACvB,OAAO,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;QAC1C,CAAC;IACH,CAAC;IAED,oDAAoD;IACpD,IAAI,cAAc,EAAE,OAAO,EAAE,SAAS,EAAE,CAAC;QACvC,OAAO,cAAc,CAAC,OAAO,CAAC,SAAmB,CAAC;IACpD,CAAC;IACD,IAAI,cAAc,EAAE,KAAK,EAAE,CAAC;QAC1B,OAAO,cAAc,CAAC,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;IAC5C,CAAC;IAED,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,UAAU,aAAa,CAC3B,OAAuB,EACvB,cAAyC;IAEzC,0CAA0C;IAC1C,IAAI,UAAU,EAAE,EAAE,CAAC;QACjB,OAAO,WAAW,CAAC;IACrB,CAAC;IAED,IAAI,OAAO,EAAE,IAAI,EAAE,EAAE,EAAE,CAAC;QACtB,OAAO,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;IACzB,CAAC;IACD,IAAI,cAAc,EAAE,MAAM,EAAE,CAAC;QAC3B,OAAO,cAAc,CAAC,MAAM,CAAC;IAC/B,CAAC;IACD,OAAO,EAAE,CAAC;AACZ,CAAC;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,UAAU,oBAAoB,CAClC,OAAuB,EACvB,cAAyC,EACzC,WAAmB,GAAG;IAEtB,MAAM,IAAI,GAAG,gBAAgB,CAAC,OAAO,EAAE,cAAc,EAAE,EAAE,CAAC,CAAC;IAC3D,IAAI,IAAI,EAAE,CAAC;QACT,OAAO,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC;IACtC,CAAC;IACD,OAAO,QAAQ,CAAC;AAClB,CAAC"}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Login Guard -- Local Credential Pre-Check & Rate Limiting
|
|
3
|
+
*
|
|
4
|
+
* Minimizes Supabase auth API requests by verifying credentials locally first.
|
|
5
|
+
* Only calls Supabase when the local hash matches (correct password) or when no
|
|
6
|
+
* local hash exists (with rate limiting).
|
|
7
|
+
*
|
|
8
|
+
* Architecture:
|
|
9
|
+
* - Maintains **in-memory-only** state (resets on page refresh) for failure
|
|
10
|
+
* counters and rate-limit timers.
|
|
11
|
+
* - Two operational strategies:
|
|
12
|
+
* 1. `local-match`: A cached hash exists and the user's input matches it.
|
|
13
|
+
* Proceed to Supabase for authoritative verification.
|
|
14
|
+
* 2. `no-cache`: No cached hash is available. Proceed to Supabase but apply
|
|
15
|
+
* exponential backoff on repeated failures.
|
|
16
|
+
* - After a configurable number of consecutive local mismatches, the cached hash
|
|
17
|
+
* is invalidated (it may be stale from a server-side password change) and the
|
|
18
|
+
* guard falls back to rate-limited Supabase mode.
|
|
19
|
+
*
|
|
20
|
+
* Security considerations:
|
|
21
|
+
* - The guard is a **client-side optimization**, not a security boundary. It
|
|
22
|
+
* reduces unnecessary network calls and provides a better UX (instant
|
|
23
|
+
* rejection for wrong passwords) but Supabase remains the authoritative
|
|
24
|
+
* verifier.
|
|
25
|
+
* - Rate limiting is in-memory only and resets on page refresh; server-side rate
|
|
26
|
+
* limits in Supabase are still the primary defense against brute-force.
|
|
27
|
+
* - Cached hashes are SHA-256 digests stored in IndexedDB. They are invalidated
|
|
28
|
+
* when stale-hash scenarios are detected (local match but Supabase rejects).
|
|
29
|
+
*
|
|
30
|
+
* @module auth/loginGuard
|
|
31
|
+
*/
|
|
32
|
+
/**
|
|
33
|
+
* Strategy used when proceeding to the Supabase auth call.
|
|
34
|
+
*
|
|
35
|
+
* - `'local-match'` -- The user's input matched a locally cached hash.
|
|
36
|
+
* Supabase is called for authoritative confirmation.
|
|
37
|
+
* - `'no-cache'` -- No local hash was available (or it was invalidated).
|
|
38
|
+
* Supabase is called directly, subject to rate limiting.
|
|
39
|
+
*/
|
|
40
|
+
export type PreCheckStrategy = 'local-match' | 'no-cache';
|
|
41
|
+
/**
|
|
42
|
+
* Result of the local pre-check.
|
|
43
|
+
*
|
|
44
|
+
* - `proceed: true` -- The caller should continue with the Supabase auth call.
|
|
45
|
+
* - `proceed: false` -- The attempt was rejected locally; `error` contains a
|
|
46
|
+
* user-facing message and `retryAfterMs` (if present) indicates how long
|
|
47
|
+
* the user should wait.
|
|
48
|
+
*/
|
|
49
|
+
export type PreCheckResult = {
|
|
50
|
+
proceed: true;
|
|
51
|
+
strategy: PreCheckStrategy;
|
|
52
|
+
} | {
|
|
53
|
+
proceed: false;
|
|
54
|
+
error: string;
|
|
55
|
+
retryAfterMs?: number;
|
|
56
|
+
};
|
|
57
|
+
/**
|
|
58
|
+
* Pre-check login credentials locally before calling Supabase.
|
|
59
|
+
*
|
|
60
|
+
* Reads `singleUserConfig.gateHash`, hashes input, and compares.
|
|
61
|
+
*
|
|
62
|
+
* Returns `{ proceed: true, strategy }` to allow Supabase call,
|
|
63
|
+
* or `{ proceed: false, error, retryAfterMs? }` to reject locally.
|
|
64
|
+
*
|
|
65
|
+
* @param input - The plaintext password or gate code entered by the user.
|
|
66
|
+
* @returns A promise resolving to a {@link PreCheckResult}.
|
|
67
|
+
*
|
|
68
|
+
* @example
|
|
69
|
+
* ```ts
|
|
70
|
+
* const result = await preCheckLogin(password);
|
|
71
|
+
* if (result.proceed) {
|
|
72
|
+
* const { error } = await supabase.auth.signInWithPassword({ email, password });
|
|
73
|
+
* if (error) await onLoginFailure(result.strategy);
|
|
74
|
+
* else onLoginSuccess();
|
|
75
|
+
* } else {
|
|
76
|
+
* showError(result.error);
|
|
77
|
+
* }
|
|
78
|
+
* ```
|
|
79
|
+
*
|
|
80
|
+
* @see {@link onLoginSuccess} -- must be called after a successful Supabase login.
|
|
81
|
+
* @see {@link onLoginFailure} -- must be called after a failed Supabase login.
|
|
82
|
+
*/
|
|
83
|
+
export declare function preCheckLogin(input: string): Promise<PreCheckResult>;
|
|
84
|
+
/**
|
|
85
|
+
* Called after a successful Supabase login.
|
|
86
|
+
*
|
|
87
|
+
* Resets all login guard counters (local failure count, rate-limit attempts,
|
|
88
|
+
* and the next-allowed-attempt timestamp) so the user starts fresh.
|
|
89
|
+
*
|
|
90
|
+
* @example
|
|
91
|
+
* ```ts
|
|
92
|
+
* const { error } = await supabase.auth.signInWithPassword({ email, password });
|
|
93
|
+
* if (!error) onLoginSuccess();
|
|
94
|
+
* ```
|
|
95
|
+
*/
|
|
96
|
+
export declare function onLoginSuccess(): void;
|
|
97
|
+
/**
|
|
98
|
+
* Called after a failed Supabase login.
|
|
99
|
+
*
|
|
100
|
+
* Behavior depends on the strategy that was used:
|
|
101
|
+
*
|
|
102
|
+
* - `'local-match'`: Supabase rejected a locally-matched password, meaning the
|
|
103
|
+
* cached hash is **stale** (password changed server-side). The cached hash is
|
|
104
|
+
* invalidated so future attempts go through rate-limited Supabase mode.
|
|
105
|
+
* - `'no-cache'`: Increment the rate-limit counter and apply exponential
|
|
106
|
+
* backoff (base * 2^(n-1), capped at MAX_DELAY_MS).
|
|
107
|
+
*
|
|
108
|
+
* @param strategy - The {@link PreCheckStrategy} that was returned by
|
|
109
|
+
* {@link preCheckLogin} for this attempt.
|
|
110
|
+
*
|
|
111
|
+
* @example
|
|
112
|
+
* ```ts
|
|
113
|
+
* const result = await preCheckLogin(password);
|
|
114
|
+
* if (result.proceed) {
|
|
115
|
+
* const { error } = await supabase.auth.signInWithPassword({ email, password });
|
|
116
|
+
* if (error) await onLoginFailure(result.strategy);
|
|
117
|
+
* }
|
|
118
|
+
* ```
|
|
119
|
+
*/
|
|
120
|
+
export declare function onLoginFailure(strategy: PreCheckStrategy): Promise<void>;
|
|
121
|
+
/**
|
|
122
|
+
* Full reset of all login guard state.
|
|
123
|
+
*
|
|
124
|
+
* Call on sign-out or app reset to clear failure counters and rate-limit
|
|
125
|
+
* timers so the next login attempt starts with a clean slate.
|
|
126
|
+
*
|
|
127
|
+
* @example
|
|
128
|
+
* ```ts
|
|
129
|
+
* await supabase.auth.signOut();
|
|
130
|
+
* resetLoginGuard();
|
|
131
|
+
* ```
|
|
132
|
+
*/
|
|
133
|
+
export declare function resetLoginGuard(): void;
|
|
134
|
+
//# sourceMappingURL=loginGuard.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"loginGuard.d.ts","sourceRoot":"","sources":["../../src/auth/loginGuard.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAmDH;;;;;;;GAOG;AACH,MAAM,MAAM,gBAAgB,GAAG,aAAa,GAAG,UAAU,CAAC;AAE1D;;;;;;;GAOG;AACH,MAAM,MAAM,cAAc,GACtB;IAAE,OAAO,EAAE,IAAI,CAAC;IAAC,QAAQ,EAAE,gBAAgB,CAAA;CAAE,GAC7C;IAAE,OAAO,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,YAAY,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAoD7D;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAsB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC,CAuE1E;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,cAAc,IAAI,IAAI,CAKrC;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAsB,cAAc,CAAC,QAAQ,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAiB9E;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,eAAe,IAAI,IAAI,CAKtC"}
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Login Guard -- Local Credential Pre-Check & Rate Limiting
|
|
3
|
+
*
|
|
4
|
+
* Minimizes Supabase auth API requests by verifying credentials locally first.
|
|
5
|
+
* Only calls Supabase when the local hash matches (correct password) or when no
|
|
6
|
+
* local hash exists (with rate limiting).
|
|
7
|
+
*
|
|
8
|
+
* Architecture:
|
|
9
|
+
* - Maintains **in-memory-only** state (resets on page refresh) for failure
|
|
10
|
+
* counters and rate-limit timers.
|
|
11
|
+
* - Two operational strategies:
|
|
12
|
+
* 1. `local-match`: A cached hash exists and the user's input matches it.
|
|
13
|
+
* Proceed to Supabase for authoritative verification.
|
|
14
|
+
* 2. `no-cache`: No cached hash is available. Proceed to Supabase but apply
|
|
15
|
+
* exponential backoff on repeated failures.
|
|
16
|
+
* - After a configurable number of consecutive local mismatches, the cached hash
|
|
17
|
+
* is invalidated (it may be stale from a server-side password change) and the
|
|
18
|
+
* guard falls back to rate-limited Supabase mode.
|
|
19
|
+
*
|
|
20
|
+
* Security considerations:
|
|
21
|
+
* - The guard is a **client-side optimization**, not a security boundary. It
|
|
22
|
+
* reduces unnecessary network calls and provides a better UX (instant
|
|
23
|
+
* rejection for wrong passwords) but Supabase remains the authoritative
|
|
24
|
+
* verifier.
|
|
25
|
+
* - Rate limiting is in-memory only and resets on page refresh; server-side rate
|
|
26
|
+
* limits in Supabase are still the primary defense against brute-force.
|
|
27
|
+
* - Cached hashes are SHA-256 digests stored in IndexedDB. They are invalidated
|
|
28
|
+
* when stale-hash scenarios are detected (local match but Supabase rejects).
|
|
29
|
+
*
|
|
30
|
+
* @module auth/loginGuard
|
|
31
|
+
*/
|
|
32
|
+
import { hashValue } from './crypto';
|
|
33
|
+
import { getEngineConfig } from '../config';
|
|
34
|
+
import { debugLog, debugWarn } from '../debug';
|
|
35
|
+
// =============================================================================
|
|
36
|
+
// CONSTANTS
|
|
37
|
+
// =============================================================================
|
|
38
|
+
/**
|
|
39
|
+
* Number of consecutive local hash mismatches before the cached hash is
|
|
40
|
+
* invalidated. Prevents a permanently stale hash from locking out the user.
|
|
41
|
+
*/
|
|
42
|
+
const LOCAL_FAILURE_THRESHOLD = 5;
|
|
43
|
+
/** Base delay (in milliseconds) for the first rate-limited retry. */
|
|
44
|
+
const BASE_DELAY_MS = 1000;
|
|
45
|
+
/** Maximum delay cap (in milliseconds) to prevent absurdly long waits. */
|
|
46
|
+
const MAX_DELAY_MS = 30000;
|
|
47
|
+
/** Multiplier for exponential backoff between rate-limited attempts. */
|
|
48
|
+
const BACKOFF_MULTIPLIER = 2;
|
|
49
|
+
// =============================================================================
|
|
50
|
+
// IN-MEMORY STATE
|
|
51
|
+
// =============================================================================
|
|
52
|
+
/**
|
|
53
|
+
* Tracks how many times the local hash comparison has failed consecutively.
|
|
54
|
+
* Once this reaches `LOCAL_FAILURE_THRESHOLD`, the cached hash is invalidated.
|
|
55
|
+
*/
|
|
56
|
+
let consecutiveLocalFailures = 0;
|
|
57
|
+
/**
|
|
58
|
+
* Number of failed Supabase login attempts in no-cache mode. Used to compute
|
|
59
|
+
* the exponential backoff delay.
|
|
60
|
+
*/
|
|
61
|
+
let rateLimitAttempts = 0;
|
|
62
|
+
/**
|
|
63
|
+
* Timestamp (ms since epoch) before which the next login attempt is blocked.
|
|
64
|
+
* Zero means no rate limit is active.
|
|
65
|
+
*/
|
|
66
|
+
let nextAllowedAttempt = 0;
|
|
67
|
+
// =============================================================================
|
|
68
|
+
// INTERNAL HELPERS
|
|
69
|
+
// =============================================================================
|
|
70
|
+
/**
|
|
71
|
+
* Check whether the current rate-limit window allows a new attempt.
|
|
72
|
+
*
|
|
73
|
+
* @returns An object indicating whether the attempt is allowed, and if not,
|
|
74
|
+
* how many milliseconds remain until the next allowed attempt.
|
|
75
|
+
*/
|
|
76
|
+
function checkRateLimit() {
|
|
77
|
+
const now = Date.now();
|
|
78
|
+
if (nextAllowedAttempt > now) {
|
|
79
|
+
return { allowed: false, retryAfterMs: nextAllowedAttempt - now };
|
|
80
|
+
}
|
|
81
|
+
return { allowed: true };
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Invalidate the locally cached gate hash in IndexedDB.
|
|
85
|
+
*
|
|
86
|
+
* Called when the guard determines the cached hash is stale (e.g., the user
|
|
87
|
+
* changed their PIN on another device, or too many consecutive local
|
|
88
|
+
* mismatches have occurred).
|
|
89
|
+
*
|
|
90
|
+
* @throws Never -- errors are caught and logged via `debugWarn`.
|
|
91
|
+
*/
|
|
92
|
+
async function invalidateCachedHash() {
|
|
93
|
+
try {
|
|
94
|
+
const config = getEngineConfig();
|
|
95
|
+
const db = config.db;
|
|
96
|
+
if (db) {
|
|
97
|
+
const record = await db.table('singleUserConfig').get('config');
|
|
98
|
+
if (record && record.gateHash) {
|
|
99
|
+
await db.table('singleUserConfig').update('config', {
|
|
100
|
+
gateHash: undefined,
|
|
101
|
+
updatedAt: new Date().toISOString()
|
|
102
|
+
});
|
|
103
|
+
debugLog('[LoginGuard] Invalidated single-user gateHash');
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
catch (e) {
|
|
108
|
+
debugWarn('[LoginGuard] Failed to invalidate cached hash:', e);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
// =============================================================================
|
|
112
|
+
// PUBLIC API
|
|
113
|
+
// =============================================================================
|
|
114
|
+
/**
|
|
115
|
+
* Pre-check login credentials locally before calling Supabase.
|
|
116
|
+
*
|
|
117
|
+
* Reads `singleUserConfig.gateHash`, hashes input, and compares.
|
|
118
|
+
*
|
|
119
|
+
* Returns `{ proceed: true, strategy }` to allow Supabase call,
|
|
120
|
+
* or `{ proceed: false, error, retryAfterMs? }` to reject locally.
|
|
121
|
+
*
|
|
122
|
+
* @param input - The plaintext password or gate code entered by the user.
|
|
123
|
+
* @returns A promise resolving to a {@link PreCheckResult}.
|
|
124
|
+
*
|
|
125
|
+
* @example
|
|
126
|
+
* ```ts
|
|
127
|
+
* const result = await preCheckLogin(password);
|
|
128
|
+
* if (result.proceed) {
|
|
129
|
+
* const { error } = await supabase.auth.signInWithPassword({ email, password });
|
|
130
|
+
* if (error) await onLoginFailure(result.strategy);
|
|
131
|
+
* else onLoginSuccess();
|
|
132
|
+
* } else {
|
|
133
|
+
* showError(result.error);
|
|
134
|
+
* }
|
|
135
|
+
* ```
|
|
136
|
+
*
|
|
137
|
+
* @see {@link onLoginSuccess} -- must be called after a successful Supabase login.
|
|
138
|
+
* @see {@link onLoginFailure} -- must be called after a failed Supabase login.
|
|
139
|
+
*/
|
|
140
|
+
export async function preCheckLogin(input) {
|
|
141
|
+
try {
|
|
142
|
+
let cachedHash;
|
|
143
|
+
const config = getEngineConfig();
|
|
144
|
+
const db = config.db;
|
|
145
|
+
if (db) {
|
|
146
|
+
const record = await db.table('singleUserConfig').get('config');
|
|
147
|
+
cachedHash = record?.gateHash;
|
|
148
|
+
}
|
|
149
|
+
if (cachedHash) {
|
|
150
|
+
/* We have a cached hash -- compare locally before touching the network. */
|
|
151
|
+
const inputHash = await hashValue(input);
|
|
152
|
+
if (inputHash === cachedHash) {
|
|
153
|
+
/* Local match -- proceed to Supabase for authoritative verification.
|
|
154
|
+
We never trust the local hash alone because it could be stale. */
|
|
155
|
+
debugLog('[LoginGuard] Local hash match, proceeding to Supabase');
|
|
156
|
+
return { proceed: true, strategy: 'local-match' };
|
|
157
|
+
}
|
|
158
|
+
/* Mismatch -- reject locally to avoid a needless Supabase round-trip. */
|
|
159
|
+
consecutiveLocalFailures++;
|
|
160
|
+
debugWarn(`[LoginGuard] Local hash mismatch (${consecutiveLocalFailures}/${LOCAL_FAILURE_THRESHOLD})`);
|
|
161
|
+
if (consecutiveLocalFailures >= LOCAL_FAILURE_THRESHOLD) {
|
|
162
|
+
/* Threshold exceeded -- the cached hash may be stale (password changed
|
|
163
|
+
on another device). Invalidate it so subsequent attempts go directly
|
|
164
|
+
to Supabase in rate-limited mode. */
|
|
165
|
+
debugWarn('[LoginGuard] Threshold exceeded, invalidating cached hash');
|
|
166
|
+
await invalidateCachedHash();
|
|
167
|
+
consecutiveLocalFailures = 0;
|
|
168
|
+
/* Fall through to rate-limited Supabase mode */
|
|
169
|
+
const rateCheck = checkRateLimit();
|
|
170
|
+
if (!rateCheck.allowed) {
|
|
171
|
+
return {
|
|
172
|
+
proceed: false,
|
|
173
|
+
error: 'Too many attempts. Please wait before trying again.',
|
|
174
|
+
retryAfterMs: rateCheck.retryAfterMs
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
return { proceed: true, strategy: 'no-cache' };
|
|
178
|
+
}
|
|
179
|
+
return { proceed: false, error: 'Incorrect password or code' };
|
|
180
|
+
}
|
|
181
|
+
/* No cached hash -- rate-limited Supabase mode */
|
|
182
|
+
const rateCheck = checkRateLimit();
|
|
183
|
+
if (!rateCheck.allowed) {
|
|
184
|
+
return {
|
|
185
|
+
proceed: false,
|
|
186
|
+
error: 'Too many attempts. Please wait before trying again.',
|
|
187
|
+
retryAfterMs: rateCheck.retryAfterMs
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
debugLog('[LoginGuard] No cached hash, proceeding to Supabase (rate-limited mode)');
|
|
191
|
+
return { proceed: true, strategy: 'no-cache' };
|
|
192
|
+
}
|
|
193
|
+
catch (e) {
|
|
194
|
+
/* On any error, allow Supabase call (fail open for auth). A strict
|
|
195
|
+
"fail closed" policy here would lock users out of their own app
|
|
196
|
+
due to an IndexedDB read error. */
|
|
197
|
+
debugWarn('[LoginGuard] Pre-check error, falling through to Supabase:', e);
|
|
198
|
+
return { proceed: true, strategy: 'no-cache' };
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Called after a successful Supabase login.
|
|
203
|
+
*
|
|
204
|
+
* Resets all login guard counters (local failure count, rate-limit attempts,
|
|
205
|
+
* and the next-allowed-attempt timestamp) so the user starts fresh.
|
|
206
|
+
*
|
|
207
|
+
* @example
|
|
208
|
+
* ```ts
|
|
209
|
+
* const { error } = await supabase.auth.signInWithPassword({ email, password });
|
|
210
|
+
* if (!error) onLoginSuccess();
|
|
211
|
+
* ```
|
|
212
|
+
*/
|
|
213
|
+
export function onLoginSuccess() {
|
|
214
|
+
consecutiveLocalFailures = 0;
|
|
215
|
+
rateLimitAttempts = 0;
|
|
216
|
+
nextAllowedAttempt = 0;
|
|
217
|
+
debugLog('[LoginGuard] Login success, counters reset');
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Called after a failed Supabase login.
|
|
221
|
+
*
|
|
222
|
+
* Behavior depends on the strategy that was used:
|
|
223
|
+
*
|
|
224
|
+
* - `'local-match'`: Supabase rejected a locally-matched password, meaning the
|
|
225
|
+
* cached hash is **stale** (password changed server-side). The cached hash is
|
|
226
|
+
* invalidated so future attempts go through rate-limited Supabase mode.
|
|
227
|
+
* - `'no-cache'`: Increment the rate-limit counter and apply exponential
|
|
228
|
+
* backoff (base * 2^(n-1), capped at MAX_DELAY_MS).
|
|
229
|
+
*
|
|
230
|
+
* @param strategy - The {@link PreCheckStrategy} that was returned by
|
|
231
|
+
* {@link preCheckLogin} for this attempt.
|
|
232
|
+
*
|
|
233
|
+
* @example
|
|
234
|
+
* ```ts
|
|
235
|
+
* const result = await preCheckLogin(password);
|
|
236
|
+
* if (result.proceed) {
|
|
237
|
+
* const { error } = await supabase.auth.signInWithPassword({ email, password });
|
|
238
|
+
* if (error) await onLoginFailure(result.strategy);
|
|
239
|
+
* }
|
|
240
|
+
* ```
|
|
241
|
+
*/
|
|
242
|
+
export async function onLoginFailure(strategy) {
|
|
243
|
+
if (strategy === 'local-match') {
|
|
244
|
+
/* Stale hash: local match but Supabase rejected -- invalidate the cache
|
|
245
|
+
so the user is not stuck in a loop of false local matches. */
|
|
246
|
+
debugWarn('[LoginGuard] Stale hash detected, invalidating cached hash');
|
|
247
|
+
await invalidateCachedHash();
|
|
248
|
+
}
|
|
249
|
+
else {
|
|
250
|
+
/* No-cache mode: apply exponential backoff to throttle brute-force
|
|
251
|
+
attempts that bypass the local pre-check. */
|
|
252
|
+
rateLimitAttempts++;
|
|
253
|
+
const delay = Math.min(BASE_DELAY_MS * Math.pow(BACKOFF_MULTIPLIER, rateLimitAttempts - 1), MAX_DELAY_MS);
|
|
254
|
+
nextAllowedAttempt = Date.now() + delay;
|
|
255
|
+
debugWarn(`[LoginGuard] Rate limit applied: ${delay}ms delay (attempt ${rateLimitAttempts})`);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Full reset of all login guard state.
|
|
260
|
+
*
|
|
261
|
+
* Call on sign-out or app reset to clear failure counters and rate-limit
|
|
262
|
+
* timers so the next login attempt starts with a clean slate.
|
|
263
|
+
*
|
|
264
|
+
* @example
|
|
265
|
+
* ```ts
|
|
266
|
+
* await supabase.auth.signOut();
|
|
267
|
+
* resetLoginGuard();
|
|
268
|
+
* ```
|
|
269
|
+
*/
|
|
270
|
+
export function resetLoginGuard() {
|
|
271
|
+
consecutiveLocalFailures = 0;
|
|
272
|
+
rateLimitAttempts = 0;
|
|
273
|
+
nextAllowedAttempt = 0;
|
|
274
|
+
debugLog('[LoginGuard] Guard reset');
|
|
275
|
+
}
|
|
276
|
+
//# sourceMappingURL=loginGuard.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"loginGuard.js","sourceRoot":"","sources":["../../src/auth/loginGuard.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AACrC,OAAO,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AAC5C,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AAE/C,gFAAgF;AAChF,YAAY;AACZ,gFAAgF;AAEhF;;;GAGG;AACH,MAAM,uBAAuB,GAAG,CAAC,CAAC;AAElC,qEAAqE;AACrE,MAAM,aAAa,GAAG,IAAI,CAAC;AAE3B,0EAA0E;AAC1E,MAAM,YAAY,GAAG,KAAK,CAAC;AAE3B,wEAAwE;AACxE,MAAM,kBAAkB,GAAG,CAAC,CAAC;AAE7B,gFAAgF;AAChF,kBAAkB;AAClB,gFAAgF;AAEhF;;;GAGG;AACH,IAAI,wBAAwB,GAAG,CAAC,CAAC;AAEjC;;;GAGG;AACH,IAAI,iBAAiB,GAAG,CAAC,CAAC;AAE1B;;;GAGG;AACH,IAAI,kBAAkB,GAAG,CAAC,CAAC;AA4B3B,gFAAgF;AAChF,mBAAmB;AACnB,gFAAgF;AAEhF;;;;;GAKG;AACH,SAAS,cAAc;IACrB,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;IACvB,IAAI,kBAAkB,GAAG,GAAG,EAAE,CAAC;QAC7B,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,YAAY,EAAE,kBAAkB,GAAG,GAAG,EAAE,CAAC;IACpE,CAAC;IACD,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;AAC3B,CAAC;AAED;;;;;;;;GAQG;AACH,KAAK,UAAU,oBAAoB;IACjC,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,eAAe,EAAE,CAAC;QACjC,MAAM,EAAE,GAAG,MAAM,CAAC,EAAE,CAAC;QACrB,IAAI,EAAE,EAAE,CAAC;YACP,MAAM,MAAM,GAAG,MAAM,EAAE,CAAC,KAAK,CAAC,kBAAkB,CAAC,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;YAChE,IAAI,MAAM,IAAI,MAAM,CAAC,QAAQ,EAAE,CAAC;gBAC9B,MAAM,EAAE,CAAC,KAAK,CAAC,kBAAkB,CAAC,CAAC,MAAM,CAAC,QAAQ,EAAE;oBAClD,QAAQ,EAAE,SAAS;oBACnB,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;iBACpC,CAAC,CAAC;gBACH,QAAQ,CAAC,+CAA+C,CAAC,CAAC;YAC5D,CAAC;QACH,CAAC;IACH,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACX,SAAS,CAAC,gDAAgD,EAAE,CAAC,CAAC,CAAC;IACjE,CAAC;AACH,CAAC;AAED,gFAAgF;AAChF,aAAa;AACb,gFAAgF;AAEhF;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,KAAa;IAC/C,IAAI,CAAC;QACH,IAAI,UAA8B,CAAC;QAEnC,MAAM,MAAM,GAAG,eAAe,EAAE,CAAC;QACjC,MAAM,EAAE,GAAG,MAAM,CAAC,EAAE,CAAC;QACrB,IAAI,EAAE,EAAE,CAAC;YACP,MAAM,MAAM,GAAG,MAAM,EAAE,CAAC,KAAK,CAAC,kBAAkB,CAAC,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;YAChE,UAAU,GAAG,MAAM,EAAE,QAAQ,CAAC;QAChC,CAAC;QAED,IAAI,UAAU,EAAE,CAAC;YACf,2EAA2E;YAC3E,MAAM,SAAS,GAAG,MAAM,SAAS,CAAC,KAAK,CAAC,CAAC;YAEzC,IAAI,SAAS,KAAK,UAAU,EAAE,CAAC;gBAC7B;oFACoE;gBACpE,QAAQ,CAAC,uDAAuD,CAAC,CAAC;gBAClE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,aAAa,EAAE,CAAC;YACpD,CAAC;YAED,yEAAyE;YACzE,wBAAwB,EAAE,CAAC;YAC3B,SAAS,CACP,qCAAqC,wBAAwB,IAAI,uBAAuB,GAAG,CAC5F,CAAC;YAEF,IAAI,wBAAwB,IAAI,uBAAuB,EAAE,CAAC;gBACxD;;uDAEuC;gBACvC,SAAS,CAAC,2DAA2D,CAAC,CAAC;gBACvE,MAAM,oBAAoB,EAAE,CAAC;gBAC7B,wBAAwB,GAAG,CAAC,CAAC;gBAE7B,gDAAgD;gBAChD,MAAM,SAAS,GAAG,cAAc,EAAE,CAAC;gBACnC,IAAI,CAAC,SAAS,CAAC,OAAO,EAAE,CAAC;oBACvB,OAAO;wBACL,OAAO,EAAE,KAAK;wBACd,KAAK,EAAE,qDAAqD;wBAC5D,YAAY,EAAE,SAAS,CAAC,YAAY;qBACrC,CAAC;gBACJ,CAAC;gBAED,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,UAAU,EAAE,CAAC;YACjD,CAAC;YAED,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,4BAA4B,EAAE,CAAC;QACjE,CAAC;QAED,kDAAkD;QAClD,MAAM,SAAS,GAAG,cAAc,EAAE,CAAC;QACnC,IAAI,CAAC,SAAS,CAAC,OAAO,EAAE,CAAC;YACvB,OAAO;gBACL,OAAO,EAAE,KAAK;gBACd,KAAK,EAAE,qDAAqD;gBAC5D,YAAY,EAAE,SAAS,CAAC,YAAY;aACrC,CAAC;QACJ,CAAC;QAED,QAAQ,CAAC,yEAAyE,CAAC,CAAC;QACpF,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,UAAU,EAAE,CAAC;IACjD,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACX;;6CAEqC;QACrC,SAAS,CAAC,4DAA4D,EAAE,CAAC,CAAC,CAAC;QAC3E,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,UAAU,EAAE,CAAC;IACjD,CAAC;AACH,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,cAAc;IAC5B,wBAAwB,GAAG,CAAC,CAAC;IAC7B,iBAAiB,GAAG,CAAC,CAAC;IACtB,kBAAkB,GAAG,CAAC,CAAC;IACvB,QAAQ,CAAC,4CAA4C,CAAC,CAAC;AACzD,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAM,CAAC,KAAK,UAAU,cAAc,CAAC,QAA0B;IAC7D,IAAI,QAAQ,KAAK,aAAa,EAAE,CAAC;QAC/B;wEACgE;QAChE,SAAS,CAAC,4DAA4D,CAAC,CAAC;QACxE,MAAM,oBAAoB,EAAE,CAAC;IAC/B,CAAC;SAAM,CAAC;QACN;uDAC+C;QAC/C,iBAAiB,EAAE,CAAC;QACpB,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CACpB,aAAa,GAAG,IAAI,CAAC,GAAG,CAAC,kBAAkB,EAAE,iBAAiB,GAAG,CAAC,CAAC,EACnE,YAAY,CACb,CAAC;QACF,kBAAkB,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,KAAK,CAAC;QACxC,SAAS,CAAC,oCAAoC,KAAK,qBAAqB,iBAAiB,GAAG,CAAC,CAAC;IAChG,CAAC;AACH,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,eAAe;IAC7B,wBAAwB,GAAG,CAAC,CAAC;IAC7B,iBAAiB,GAAG,CAAC,CAAC;IACtB,kBAAkB,GAAG,CAAC,CAAC;IACvB,QAAQ,CAAC,0BAA0B,CAAC,CAAC;AACvC,CAAC"}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Offline Credentials Management
|
|
3
|
+
*
|
|
4
|
+
* Handles caching, retrieval, and update of user credentials in IndexedDB
|
|
5
|
+
* for offline fallback support. When the user successfully authenticates
|
|
6
|
+
* online via Supabase, their credentials are cached locally (with the
|
|
7
|
+
* password SHA-256-hashed) so profile data is available while offline.
|
|
8
|
+
*
|
|
9
|
+
* Architecture:
|
|
10
|
+
* - Credentials are stored as a singleton record (key: `'current_user'`) in the
|
|
11
|
+
* `offlineCredentials` IndexedDB table.
|
|
12
|
+
* - Only one set of credentials is cached at a time.
|
|
13
|
+
* - The profile blob is extracted via the host app's `profileExtractor` config
|
|
14
|
+
* callback, or falls back to raw Supabase `user_metadata`.
|
|
15
|
+
*
|
|
16
|
+
* Security considerations:
|
|
17
|
+
* - Passwords are **always** hashed with SHA-256 before storage. The plaintext
|
|
18
|
+
* password is never persisted.
|
|
19
|
+
* - New writes always hash the password before storage.
|
|
20
|
+
* - A paranoid read-back verification is performed after `cacheOfflineCredentials`
|
|
21
|
+
* to ensure the password was actually persisted (guards against silent
|
|
22
|
+
* IndexedDB write failures).
|
|
23
|
+
* - Credentials are cleared on logout via `clearOfflineCredentials`.
|
|
24
|
+
*
|
|
25
|
+
* @module auth/offlineCredentials
|
|
26
|
+
*/
|
|
27
|
+
import type { OfflineCredentials } from '../types';
|
|
28
|
+
import type { User, Session } from '@supabase/supabase-js';
|
|
29
|
+
/**
|
|
30
|
+
* Cache user credentials for offline login.
|
|
31
|
+
*
|
|
32
|
+
* Called after a successful Supabase login to persist a hashed copy of the
|
|
33
|
+
* user's credentials in IndexedDB. Subsequent offline logins will verify
|
|
34
|
+
* against these cached credentials.
|
|
35
|
+
*
|
|
36
|
+
* @param email - The user's email address (used for offline identity matching).
|
|
37
|
+
* @param password - The user's plaintext password. Will be SHA-256-hashed before storage.
|
|
38
|
+
* @param user - The Supabase `User` object, used to extract `userId` and profile data.
|
|
39
|
+
* @param _session - The Supabase `Session` object. Currently unused but accepted for
|
|
40
|
+
* API symmetry with the online auth flow (reserved for future use).
|
|
41
|
+
*
|
|
42
|
+
* @throws {Error} If `email` or `password` is empty (prevents storing incomplete credentials).
|
|
43
|
+
* @throws {Error} If the write-back verification fails (password not persisted in IndexedDB).
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* ```ts
|
|
47
|
+
* const { data } = await supabase.auth.signInWithPassword({ email, password });
|
|
48
|
+
* if (data.user && data.session) {
|
|
49
|
+
* await cacheOfflineCredentials(email, password, data.user, data.session);
|
|
50
|
+
* }
|
|
51
|
+
* ```
|
|
52
|
+
*
|
|
53
|
+
* @see {@link getOfflineCredentials} to retrieve the cached credentials.
|
|
54
|
+
* @see {@link clearOfflineCredentials} to remove them on logout.
|
|
55
|
+
*/
|
|
56
|
+
export declare function cacheOfflineCredentials(email: string, password: string, user: User, _session: Session): Promise<void>;
|
|
57
|
+
/**
|
|
58
|
+
* Get cached offline credentials from IndexedDB.
|
|
59
|
+
*
|
|
60
|
+
* Returns the singleton `OfflineCredentials` record, or `null` if no
|
|
61
|
+
* credentials have been cached (e.g., user has never logged in online
|
|
62
|
+
* on this device).
|
|
63
|
+
*
|
|
64
|
+
* @returns The cached credentials, or `null` if none exist.
|
|
65
|
+
*
|
|
66
|
+
* @example
|
|
67
|
+
* ```ts
|
|
68
|
+
* const creds = await getOfflineCredentials();
|
|
69
|
+
* if (creds) {
|
|
70
|
+
* console.log('Cached user:', creds.email);
|
|
71
|
+
* }
|
|
72
|
+
* ```
|
|
73
|
+
*/
|
|
74
|
+
export declare function getOfflineCredentials(): Promise<OfflineCredentials | null>;
|
|
75
|
+
/**
|
|
76
|
+
* Update the user profile in cached credentials after an online profile update.
|
|
77
|
+
*
|
|
78
|
+
* Replaces the entire `profile` blob with the provided object and updates
|
|
79
|
+
* the `cachedAt` timestamp.
|
|
80
|
+
*
|
|
81
|
+
* @param profile - The new profile data to cache (e.g., `{ firstName, lastName, avatar }`).
|
|
82
|
+
*
|
|
83
|
+
* @example
|
|
84
|
+
* ```ts
|
|
85
|
+
* await supabase.auth.updateUser({ data: { firstName: 'Jane' } });
|
|
86
|
+
* await updateOfflineCredentialsProfile({ firstName: 'Jane', lastName: 'Doe' });
|
|
87
|
+
* ```
|
|
88
|
+
*/
|
|
89
|
+
export declare function updateOfflineCredentialsProfile(profile: Record<string, unknown>): Promise<void>;
|
|
90
|
+
/**
|
|
91
|
+
* Clear all cached offline credentials from IndexedDB.
|
|
92
|
+
*
|
|
93
|
+
* Must be called on logout to ensure no stale credentials remain on the
|
|
94
|
+
* device that could be used for unauthorized offline access.
|
|
95
|
+
*
|
|
96
|
+
* @example
|
|
97
|
+
* ```ts
|
|
98
|
+
* await supabase.auth.signOut();
|
|
99
|
+
* await clearOfflineCredentials();
|
|
100
|
+
* ```
|
|
101
|
+
*
|
|
102
|
+
* @see {@link cacheOfflineCredentials} for storing credentials on login.
|
|
103
|
+
*/
|
|
104
|
+
export declare function clearOfflineCredentials(): Promise<void>;
|
|
105
|
+
//# sourceMappingURL=offlineCredentials.d.ts.map
|