@prabhask5/stellar-engine 1.1.8 → 1.1.9
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 +8 -17
- package/dist/auth/crypto.d.ts +0 -23
- package/dist/auth/crypto.d.ts.map +1 -1
- package/dist/auth/crypto.js +0 -25
- package/dist/auth/crypto.js.map +1 -1
- package/dist/auth/deviceVerification.d.ts +2 -2
- package/dist/auth/deviceVerification.js +2 -2
- package/dist/auth/loginGuard.d.ts +7 -14
- package/dist/auth/loginGuard.d.ts.map +1 -1
- package/dist/auth/loginGuard.js +27 -62
- package/dist/auth/loginGuard.js.map +1 -1
- package/dist/auth/offlineCredentials.d.ts +6 -59
- package/dist/auth/offlineCredentials.d.ts.map +1 -1
- package/dist/auth/offlineCredentials.js +8 -111
- package/dist/auth/offlineCredentials.js.map +1 -1
- package/dist/auth/resolveAuthState.d.ts +14 -18
- package/dist/auth/resolveAuthState.d.ts.map +1 -1
- package/dist/auth/resolveAuthState.js +16 -58
- package/dist/auth/resolveAuthState.js.map +1 -1
- package/dist/auth/singleUser.js +4 -4
- package/dist/auth/singleUser.js.map +1 -1
- package/dist/bin/install-pwa.js +1929 -158
- package/dist/bin/install-pwa.js.map +1 -1
- package/dist/config.d.ts +3 -7
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +1 -1
- package/dist/config.js.map +1 -1
- package/dist/entries/auth.d.ts +8 -13
- package/dist/entries/auth.d.ts.map +1 -1
- package/dist/entries/auth.js +11 -40
- package/dist/entries/auth.js.map +1 -1
- package/dist/entries/types.d.ts +0 -1
- package/dist/entries/types.d.ts.map +1 -1
- package/dist/index.d.ts +3 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -18
- package/dist/index.js.map +1 -1
- package/dist/kit/loads.d.ts +3 -5
- package/dist/kit/loads.d.ts.map +1 -1
- package/dist/kit/loads.js +2 -10
- package/dist/kit/loads.js.map +1 -1
- package/dist/stores/authState.d.ts +2 -2
- package/dist/stores/authState.js +1 -1
- package/dist/supabase/auth.d.ts +14 -181
- package/dist/supabase/auth.d.ts.map +1 -1
- package/dist/supabase/auth.js +17 -317
- package/dist/supabase/auth.js.map +1 -1
- package/package.json +1 -1
- package/dist/auth/admin.d.ts +0 -49
- package/dist/auth/admin.d.ts.map +0 -1
- package/dist/auth/admin.js +0 -66
- package/dist/auth/admin.js.map +0 -1
- package/dist/auth/offlineLogin.d.ts +0 -120
- package/dist/auth/offlineLogin.d.ts.map +0 -1
- package/dist/auth/offlineLogin.js +0 -142
- package/dist/auth/offlineLogin.js.map +0 -1
package/README.md
CHANGED
|
@@ -81,7 +81,6 @@ initEngine({
|
|
|
81
81
|
tables: [/* ... */],
|
|
82
82
|
database: {/* ... */},
|
|
83
83
|
auth: {
|
|
84
|
-
mode: 'single-user',
|
|
85
84
|
singleUser: { gateType: 'code', codeLength: 4 },
|
|
86
85
|
enableOfflineAuth: true,
|
|
87
86
|
// emailConfirmation: { enabled: true }, // require email confirmation on setup
|
|
@@ -134,7 +133,7 @@ The command creates a full SvelteKit 2 + Svelte 5 project with:
|
|
|
134
133
|
| `src/routes/+layout.svelte` | Auth state hydration via `hydrateAuthState()` | App shell (navbar, tab bar, overlays) |
|
|
135
134
|
| `src/routes/+page.svelte` | Imports `resolveFirstName`, `onSyncComplete`, `authState`; derives `firstName` reactively | Home page UI |
|
|
136
135
|
| `src/routes/+error.svelte` | — | Error page UI |
|
|
137
|
-
| `src/routes/setup/+page.ts` | Config check, session validation
|
|
136
|
+
| `src/routes/setup/+page.ts` | Config check, session validation via `getConfig()`, `getValidSession()` | — (fully managed) |
|
|
138
137
|
| `src/routes/setup/+page.svelte` | Imports `setConfig`, `isOnline`, `pollForNewServiceWorker` | Setup wizard UI |
|
|
139
138
|
| `src/routes/policy/+page.svelte` | — | Privacy policy content |
|
|
140
139
|
| `src/routes/login/+page.svelte` | All auth functions: `setupSingleUser`, `unlockSingleUser`, `getSingleUserInfo`, `completeSingleUserSetup`, `completeDeviceVerification`, `pollDeviceVerification`, `fetchRemoteGateConfig`, `linkSingleUserDevice`, `sendDeviceVerification` | Login page UI |
|
|
@@ -168,7 +167,7 @@ Import only what you need via subpath exports:
|
|
|
168
167
|
|---|---|
|
|
169
168
|
| `@prabhask5/stellar-engine` | `initEngine`, `startSyncEngine`, `runFullSync`, `supabase`, `getDb`, `validateSupabaseCredentials`, `validateSchema` |
|
|
170
169
|
| `@prabhask5/stellar-engine/data` | All engine CRUD + query operations (`engineCreate`, `engineUpdate`, etc.) |
|
|
171
|
-
| `@prabhask5/stellar-engine/auth` | All auth functions (`
|
|
170
|
+
| `@prabhask5/stellar-engine/auth` | All auth functions (`resolveAuthState`, `signOut`, `setupSingleUser`, `unlockSingleUser`, `lockSingleUser`, `completeSingleUserSetup`, `completeDeviceVerification`, `changeSingleUserEmail`, `completeSingleUserEmailChange`, `padPin`, etc.) |
|
|
172
171
|
| `@prabhask5/stellar-engine/stores` | Reactive stores + event subscriptions (`syncStatusStore`, `authState`, `onSyncComplete`, etc.) |
|
|
173
172
|
| `@prabhask5/stellar-engine/types` | All type exports (`Session`, `SyncEngineConfig`, `BatchOperation`, `SingleUserConfig`, etc.) |
|
|
174
173
|
| `@prabhask5/stellar-engine/utils` | Utility functions (`generateId`, `now`, `calculateNewOrder`, `snakeToCamel`, `debug`, etc.) |
|
|
@@ -300,30 +299,22 @@ Alternatively, you can provide a pre-created Dexie instance via the `db` config
|
|
|
300
299
|
| `markEntityModified(table, id)` | Record that an entity was recently modified locally (prevents incoming realtime from overwriting). |
|
|
301
300
|
| `onSyncComplete(callback)` | Register a callback invoked after each successful sync cycle. |
|
|
302
301
|
|
|
303
|
-
### Auth
|
|
302
|
+
### Auth Utilities
|
|
304
303
|
|
|
305
304
|
| Export | Description |
|
|
306
305
|
|---|---|
|
|
307
|
-
| `
|
|
308
|
-
| `
|
|
309
|
-
| `changePassword` / `resendConfirmationEmail` | Account management. |
|
|
310
|
-
| `changeEmail(newEmail)` | Request email change (sends confirmation to new address). Returns `{ error, confirmationRequired }`. |
|
|
311
|
-
| `completeEmailChange()` | Finalize email change after confirmation. Refreshes session and updates cached credentials. |
|
|
306
|
+
| `signOut` | Full teardown: stops sync, clears caches, signs out of Supabase. |
|
|
307
|
+
| `resendConfirmationEmail` | Resend signup confirmation email. |
|
|
312
308
|
| `getUserProfile` / `updateProfile` | Profile read/write via Supabase user metadata. |
|
|
309
|
+
| `verifyOtp` | Verify OTP token hash from confirmation email links. |
|
|
310
|
+
| `getValidSession` | Get a non-expired Supabase session, or `null`. |
|
|
313
311
|
| `resolveFirstName(session, offline, fallback?)` | Resolve display name from session or offline profile with configurable fallback. |
|
|
314
312
|
| `resolveUserId(session, offline)` | Extract user UUID from session or offline credentials. |
|
|
315
313
|
| `resolveAvatarInitial(session, offline, fallback?)` | Single uppercase initial for avatar display. |
|
|
316
314
|
|
|
317
|
-
### Offline auth
|
|
318
|
-
|
|
319
|
-
| Export | Description |
|
|
320
|
-
|---|---|
|
|
321
|
-
| `cacheOfflineCredentials` / `getOfflineCredentials` / `verifyOfflineCredentials` / `clearOfflineCredentials` | Store and verify credentials locally for offline sign-in. |
|
|
322
|
-
| `createOfflineSession` / `getValidOfflineSession` / `clearOfflineSession` | Manage offline session tokens in IndexedDB. |
|
|
323
|
-
|
|
324
315
|
### Single-user auth
|
|
325
316
|
|
|
326
|
-
For personal apps that use a simplified PIN or password gate. Uses real Supabase email/password auth where the PIN is padded to meet minimum password length. Enable by setting `auth.
|
|
317
|
+
For personal apps that use a simplified PIN or password gate. Uses real Supabase email/password auth where the PIN is padded to meet minimum password length. Enable by setting `auth.singleUser` in the engine config.
|
|
327
318
|
|
|
328
319
|
| Export | Description |
|
|
329
320
|
|---|---|
|
package/dist/auth/crypto.d.ts
CHANGED
|
@@ -17,10 +17,6 @@
|
|
|
17
17
|
* password storage on its own. It is acceptable here because the hashed values
|
|
18
18
|
* are only stored in the client-side IndexedDB for offline pre-checking and
|
|
19
19
|
* are never transmitted to a server.
|
|
20
|
-
* - The `isAlreadyHashed` helper uses a regex heuristic (64-char hex). If a
|
|
21
|
-
* user's actual password happens to be a 64-char hex string, it will be
|
|
22
|
-
* misidentified as already hashed. This is an acceptable edge case given the
|
|
23
|
-
* vanishingly low probability and the local-only usage context.
|
|
24
20
|
*
|
|
25
21
|
* @module auth/crypto
|
|
26
22
|
*/
|
|
@@ -40,25 +36,6 @@
|
|
|
40
36
|
* // hashed === 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855' (example)
|
|
41
37
|
* ```
|
|
42
38
|
*
|
|
43
|
-
* @see {@link isAlreadyHashed} to check whether a string is already a hex digest.
|
|
44
39
|
*/
|
|
45
40
|
export declare function hashValue(value: string): Promise<string>;
|
|
46
|
-
/**
|
|
47
|
-
* Check if a stored value is already hashed (64-character hex string).
|
|
48
|
-
*
|
|
49
|
-
* Used to distinguish between legacy plaintext credentials and modern
|
|
50
|
-
* SHA-256-hashed credentials in IndexedDB, enabling backward-compatible
|
|
51
|
-
* verification without a migration step.
|
|
52
|
-
*
|
|
53
|
-
* @param value - The string to test.
|
|
54
|
-
* @returns `true` if the value matches the pattern of a SHA-256 hex digest
|
|
55
|
-
* (exactly 64 lowercase hexadecimal characters), `false` otherwise.
|
|
56
|
-
*
|
|
57
|
-
* @example
|
|
58
|
-
* ```ts
|
|
59
|
-
* isAlreadyHashed('abc123'); // false
|
|
60
|
-
* isAlreadyHashed('e3b0c442...b855'); // true (64-char hex)
|
|
61
|
-
* ```
|
|
62
|
-
*/
|
|
63
|
-
export declare function isAlreadyHashed(value: string): boolean;
|
|
64
41
|
//# sourceMappingURL=crypto.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"crypto.d.ts","sourceRoot":"","sources":["../../src/auth/crypto.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"crypto.d.ts","sourceRoot":"","sources":["../../src/auth/crypto.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAMH;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAsB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAM9D"}
|
package/dist/auth/crypto.js
CHANGED
|
@@ -17,10 +17,6 @@
|
|
|
17
17
|
* password storage on its own. It is acceptable here because the hashed values
|
|
18
18
|
* are only stored in the client-side IndexedDB for offline pre-checking and
|
|
19
19
|
* are never transmitted to a server.
|
|
20
|
-
* - The `isAlreadyHashed` helper uses a regex heuristic (64-char hex). If a
|
|
21
|
-
* user's actual password happens to be a 64-char hex string, it will be
|
|
22
|
-
* misidentified as already hashed. This is an acceptable edge case given the
|
|
23
|
-
* vanishingly low probability and the local-only usage context.
|
|
24
20
|
*
|
|
25
21
|
* @module auth/crypto
|
|
26
22
|
*/
|
|
@@ -43,7 +39,6 @@
|
|
|
43
39
|
* // hashed === 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855' (example)
|
|
44
40
|
* ```
|
|
45
41
|
*
|
|
46
|
-
* @see {@link isAlreadyHashed} to check whether a string is already a hex digest.
|
|
47
42
|
*/
|
|
48
43
|
export async function hashValue(value) {
|
|
49
44
|
const encoder = new TextEncoder();
|
|
@@ -52,24 +47,4 @@ export async function hashValue(value) {
|
|
|
52
47
|
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
|
53
48
|
return hashArray.map((b) => b.toString(16).padStart(2, '0')).join('');
|
|
54
49
|
}
|
|
55
|
-
/**
|
|
56
|
-
* Check if a stored value is already hashed (64-character hex string).
|
|
57
|
-
*
|
|
58
|
-
* Used to distinguish between legacy plaintext credentials and modern
|
|
59
|
-
* SHA-256-hashed credentials in IndexedDB, enabling backward-compatible
|
|
60
|
-
* verification without a migration step.
|
|
61
|
-
*
|
|
62
|
-
* @param value - The string to test.
|
|
63
|
-
* @returns `true` if the value matches the pattern of a SHA-256 hex digest
|
|
64
|
-
* (exactly 64 lowercase hexadecimal characters), `false` otherwise.
|
|
65
|
-
*
|
|
66
|
-
* @example
|
|
67
|
-
* ```ts
|
|
68
|
-
* isAlreadyHashed('abc123'); // false
|
|
69
|
-
* isAlreadyHashed('e3b0c442...b855'); // true (64-char hex)
|
|
70
|
-
* ```
|
|
71
|
-
*/
|
|
72
|
-
export function isAlreadyHashed(value) {
|
|
73
|
-
return /^[0-9a-f]{64}$/.test(value);
|
|
74
|
-
}
|
|
75
50
|
//# sourceMappingURL=crypto.js.map
|
package/dist/auth/crypto.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"crypto.js","sourceRoot":"","sources":["../../src/auth/crypto.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"crypto.js","sourceRoot":"","sources":["../../src/auth/crypto.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,gFAAgF;AAChF,aAAa;AACb,gFAAgF;AAEhF;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,CAAC,KAAK,UAAU,SAAS,CAAC,KAAa;IAC3C,MAAM,OAAO,GAAG,IAAI,WAAW,EAAE,CAAC;IAClC,MAAM,IAAI,GAAG,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IACnC,MAAM,UAAU,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,SAAS,EAAE,IAAI,CAAC,CAAC;IAC/D,MAAM,SAAS,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,UAAU,CAAC,UAAU,CAAC,CAAC,CAAC;IACzD,OAAO,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;AACxE,CAAC"}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @fileoverview Device Verification Module
|
|
3
3
|
*
|
|
4
|
-
* Manages the trusted device registry and email-based OTP verification flow
|
|
5
|
-
*
|
|
4
|
+
* Manages the trusted device registry and email-based OTP verification flow.
|
|
5
|
+
* When device verification is
|
|
6
6
|
* enabled, only devices present in the Supabase `trusted_devices` table (with
|
|
7
7
|
* a recent `last_used_at` timestamp) are allowed to proceed after
|
|
8
8
|
* authentication. Untrusted devices must complete an email OTP challenge.
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @fileoverview Device Verification Module
|
|
3
3
|
*
|
|
4
|
-
* Manages the trusted device registry and email-based OTP verification flow
|
|
5
|
-
*
|
|
4
|
+
* Manages the trusted device registry and email-based OTP verification flow.
|
|
5
|
+
* When device verification is
|
|
6
6
|
* enabled, only devices present in the Supabase `trusted_devices` table (with
|
|
7
7
|
* a recent `last_used_at` timestamp) are allowed to proceed after
|
|
8
8
|
* authentication. Untrusted devices must complete an email OTP challenge.
|
|
@@ -57,25 +57,20 @@ export type PreCheckResult = {
|
|
|
57
57
|
/**
|
|
58
58
|
* Pre-check login credentials locally before calling Supabase.
|
|
59
59
|
*
|
|
60
|
-
*
|
|
61
|
-
* For multi-user mode: reads offline credentials, matches email + hashes password, compares.
|
|
60
|
+
* Reads `singleUserConfig.gateHash`, hashes input, and compares.
|
|
62
61
|
*
|
|
63
62
|
* Returns `{ proceed: true, strategy }` to allow Supabase call,
|
|
64
63
|
* or `{ proceed: false, error, retryAfterMs? }` to reject locally.
|
|
65
64
|
*
|
|
66
65
|
* @param input - The plaintext password or gate code entered by the user.
|
|
67
|
-
* @param mode - The auth mode (`'single-user'` or `'multi-user'`), which
|
|
68
|
-
* determines which IndexedDB table holds the cached hash.
|
|
69
|
-
* @param email - (Multi-user only) The email address to match against cached
|
|
70
|
-
* credentials. Ignored in single-user mode.
|
|
71
66
|
* @returns A promise resolving to a {@link PreCheckResult}.
|
|
72
67
|
*
|
|
73
68
|
* @example
|
|
74
69
|
* ```ts
|
|
75
|
-
* const result = await preCheckLogin(password
|
|
70
|
+
* const result = await preCheckLogin(password);
|
|
76
71
|
* if (result.proceed) {
|
|
77
72
|
* const { error } = await supabase.auth.signInWithPassword({ email, password });
|
|
78
|
-
* if (error) await onLoginFailure(result.strategy
|
|
73
|
+
* if (error) await onLoginFailure(result.strategy);
|
|
79
74
|
* else onLoginSuccess();
|
|
80
75
|
* } else {
|
|
81
76
|
* showError(result.error);
|
|
@@ -85,7 +80,7 @@ export type PreCheckResult = {
|
|
|
85
80
|
* @see {@link onLoginSuccess} -- must be called after a successful Supabase login.
|
|
86
81
|
* @see {@link onLoginFailure} -- must be called after a failed Supabase login.
|
|
87
82
|
*/
|
|
88
|
-
export declare function preCheckLogin(input: string
|
|
83
|
+
export declare function preCheckLogin(input: string): Promise<PreCheckResult>;
|
|
89
84
|
/**
|
|
90
85
|
* Called after a successful Supabase login.
|
|
91
86
|
*
|
|
@@ -112,19 +107,17 @@ export declare function onLoginSuccess(): void;
|
|
|
112
107
|
*
|
|
113
108
|
* @param strategy - The {@link PreCheckStrategy} that was returned by
|
|
114
109
|
* {@link preCheckLogin} for this attempt.
|
|
115
|
-
* @param mode - The auth mode, used to determine which IndexedDB table to
|
|
116
|
-
* invalidate if the hash is stale. Defaults to `'multi-user'`.
|
|
117
110
|
*
|
|
118
111
|
* @example
|
|
119
112
|
* ```ts
|
|
120
|
-
* const result = await preCheckLogin(password
|
|
113
|
+
* const result = await preCheckLogin(password);
|
|
121
114
|
* if (result.proceed) {
|
|
122
115
|
* const { error } = await supabase.auth.signInWithPassword({ email, password });
|
|
123
|
-
* if (error) await onLoginFailure(result.strategy
|
|
116
|
+
* if (error) await onLoginFailure(result.strategy);
|
|
124
117
|
* }
|
|
125
118
|
* ```
|
|
126
119
|
*/
|
|
127
|
-
export declare function onLoginFailure(strategy: PreCheckStrategy
|
|
120
|
+
export declare function onLoginFailure(strategy: PreCheckStrategy): Promise<void>;
|
|
128
121
|
/**
|
|
129
122
|
* Full reset of all login guard state.
|
|
130
123
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"loginGuard.d.ts","sourceRoot":"","sources":["../../src/auth/loginGuard.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;
|
|
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"}
|
package/dist/auth/loginGuard.js
CHANGED
|
@@ -30,7 +30,6 @@
|
|
|
30
30
|
* @module auth/loginGuard
|
|
31
31
|
*/
|
|
32
32
|
import { hashValue } from './crypto';
|
|
33
|
-
import { getOfflineCredentials } from './offlineCredentials';
|
|
34
33
|
import { getEngineConfig } from '../config';
|
|
35
34
|
import { debugLog, debugWarn } from '../debug';
|
|
36
35
|
// =============================================================================
|
|
@@ -82,45 +81,26 @@ function checkRateLimit() {
|
|
|
82
81
|
return { allowed: true };
|
|
83
82
|
}
|
|
84
83
|
/**
|
|
85
|
-
* Invalidate the locally cached
|
|
84
|
+
* Invalidate the locally cached gate hash in IndexedDB.
|
|
86
85
|
*
|
|
87
86
|
* Called when the guard determines the cached hash is stale (e.g., the user
|
|
88
|
-
* changed their
|
|
87
|
+
* changed their PIN on another device, or too many consecutive local
|
|
89
88
|
* mismatches have occurred).
|
|
90
89
|
*
|
|
91
|
-
* @param mode - Whether the app is running in single-user or multi-user mode,
|
|
92
|
-
* which determines which IndexedDB table to update.
|
|
93
|
-
*
|
|
94
90
|
* @throws Never -- errors are caught and logged via `debugWarn`.
|
|
95
91
|
*/
|
|
96
|
-
async function invalidateCachedHash(
|
|
92
|
+
async function invalidateCachedHash() {
|
|
97
93
|
try {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
debugLog('[LoginGuard] Invalidated single-user gateHash');
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
else {
|
|
113
|
-
const config = getEngineConfig();
|
|
114
|
-
const db = config.db;
|
|
115
|
-
if (db) {
|
|
116
|
-
const record = await db.table('offlineCredentials').get('current_user');
|
|
117
|
-
if (record && record.password) {
|
|
118
|
-
await db.table('offlineCredentials').update('current_user', {
|
|
119
|
-
password: undefined,
|
|
120
|
-
cachedAt: new Date().toISOString()
|
|
121
|
-
});
|
|
122
|
-
debugLog('[LoginGuard] Invalidated offline credentials password hash');
|
|
123
|
-
}
|
|
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');
|
|
124
104
|
}
|
|
125
105
|
}
|
|
126
106
|
}
|
|
@@ -134,25 +114,20 @@ async function invalidateCachedHash(mode) {
|
|
|
134
114
|
/**
|
|
135
115
|
* Pre-check login credentials locally before calling Supabase.
|
|
136
116
|
*
|
|
137
|
-
*
|
|
138
|
-
* For multi-user mode: reads offline credentials, matches email + hashes password, compares.
|
|
117
|
+
* Reads `singleUserConfig.gateHash`, hashes input, and compares.
|
|
139
118
|
*
|
|
140
119
|
* Returns `{ proceed: true, strategy }` to allow Supabase call,
|
|
141
120
|
* or `{ proceed: false, error, retryAfterMs? }` to reject locally.
|
|
142
121
|
*
|
|
143
122
|
* @param input - The plaintext password or gate code entered by the user.
|
|
144
|
-
* @param mode - The auth mode (`'single-user'` or `'multi-user'`), which
|
|
145
|
-
* determines which IndexedDB table holds the cached hash.
|
|
146
|
-
* @param email - (Multi-user only) The email address to match against cached
|
|
147
|
-
* credentials. Ignored in single-user mode.
|
|
148
123
|
* @returns A promise resolving to a {@link PreCheckResult}.
|
|
149
124
|
*
|
|
150
125
|
* @example
|
|
151
126
|
* ```ts
|
|
152
|
-
* const result = await preCheckLogin(password
|
|
127
|
+
* const result = await preCheckLogin(password);
|
|
153
128
|
* if (result.proceed) {
|
|
154
129
|
* const { error } = await supabase.auth.signInWithPassword({ email, password });
|
|
155
|
-
* if (error) await onLoginFailure(result.strategy
|
|
130
|
+
* if (error) await onLoginFailure(result.strategy);
|
|
156
131
|
* else onLoginSuccess();
|
|
157
132
|
* } else {
|
|
158
133
|
* showError(result.error);
|
|
@@ -162,22 +137,14 @@ async function invalidateCachedHash(mode) {
|
|
|
162
137
|
* @see {@link onLoginSuccess} -- must be called after a successful Supabase login.
|
|
163
138
|
* @see {@link onLoginFailure} -- must be called after a failed Supabase login.
|
|
164
139
|
*/
|
|
165
|
-
export async function preCheckLogin(input
|
|
140
|
+
export async function preCheckLogin(input) {
|
|
166
141
|
try {
|
|
167
142
|
let cachedHash;
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
cachedHash = record?.gateHash;
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
else {
|
|
177
|
-
const creds = await getOfflineCredentials();
|
|
178
|
-
if (creds && email && creds.email === email && creds.password) {
|
|
179
|
-
cachedHash = creds.password;
|
|
180
|
-
}
|
|
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;
|
|
181
148
|
}
|
|
182
149
|
if (cachedHash) {
|
|
183
150
|
/* We have a cached hash -- compare locally before touching the network. */
|
|
@@ -196,7 +163,7 @@ export async function preCheckLogin(input, mode, email) {
|
|
|
196
163
|
on another device). Invalidate it so subsequent attempts go directly
|
|
197
164
|
to Supabase in rate-limited mode. */
|
|
198
165
|
debugWarn('[LoginGuard] Threshold exceeded, invalidating cached hash');
|
|
199
|
-
await invalidateCachedHash(
|
|
166
|
+
await invalidateCachedHash();
|
|
200
167
|
consecutiveLocalFailures = 0;
|
|
201
168
|
/* Fall through to rate-limited Supabase mode */
|
|
202
169
|
const rateCheck = checkRateLimit();
|
|
@@ -262,24 +229,22 @@ export function onLoginSuccess() {
|
|
|
262
229
|
*
|
|
263
230
|
* @param strategy - The {@link PreCheckStrategy} that was returned by
|
|
264
231
|
* {@link preCheckLogin} for this attempt.
|
|
265
|
-
* @param mode - The auth mode, used to determine which IndexedDB table to
|
|
266
|
-
* invalidate if the hash is stale. Defaults to `'multi-user'`.
|
|
267
232
|
*
|
|
268
233
|
* @example
|
|
269
234
|
* ```ts
|
|
270
|
-
* const result = await preCheckLogin(password
|
|
235
|
+
* const result = await preCheckLogin(password);
|
|
271
236
|
* if (result.proceed) {
|
|
272
237
|
* const { error } = await supabase.auth.signInWithPassword({ email, password });
|
|
273
|
-
* if (error) await onLoginFailure(result.strategy
|
|
238
|
+
* if (error) await onLoginFailure(result.strategy);
|
|
274
239
|
* }
|
|
275
240
|
* ```
|
|
276
241
|
*/
|
|
277
|
-
export async function onLoginFailure(strategy
|
|
242
|
+
export async function onLoginFailure(strategy) {
|
|
278
243
|
if (strategy === 'local-match') {
|
|
279
244
|
/* Stale hash: local match but Supabase rejected -- invalidate the cache
|
|
280
245
|
so the user is not stuck in a loop of false local matches. */
|
|
281
246
|
debugWarn('[LoginGuard] Stale hash detected, invalidating cached hash');
|
|
282
|
-
await invalidateCachedHash(
|
|
247
|
+
await invalidateCachedHash();
|
|
283
248
|
}
|
|
284
249
|
else {
|
|
285
250
|
/* No-cache mode: apply exponential backoff to throttle brute-force
|
|
@@ -1 +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,
|
|
1
|
+
{"version":3,"file":"loginGuard.js","sourceRoot":"","sources":["../../src/auth/loginGuard.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,25 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @fileoverview Offline Credentials Management
|
|
3
3
|
*
|
|
4
|
-
* Handles caching, retrieval,
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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
8
|
*
|
|
9
9
|
* Architecture:
|
|
10
10
|
* - Credentials are stored as a singleton record (key: `'current_user'`) in the
|
|
11
11
|
* `offlineCredentials` IndexedDB table.
|
|
12
|
-
* - Only one set of credentials is cached at a time.
|
|
13
|
-
* last successfully authenticated user's credentials are stored.
|
|
12
|
+
* - Only one set of credentials is cached at a time.
|
|
14
13
|
* - The profile blob is extracted via the host app's `profileExtractor` config
|
|
15
14
|
* callback, or falls back to raw Supabase `user_metadata`.
|
|
16
15
|
*
|
|
17
16
|
* Security considerations:
|
|
18
17
|
* - Passwords are **always** hashed with SHA-256 before storage. The plaintext
|
|
19
18
|
* password is never persisted.
|
|
20
|
-
* -
|
|
21
|
-
* supported in `verifyOfflineCredentials` via the `isAlreadyHashed` check,
|
|
22
|
-
* but new writes always hash.
|
|
19
|
+
* - New writes always hash the password before storage.
|
|
23
20
|
* - A paranoid read-back verification is performed after `cacheOfflineCredentials`
|
|
24
21
|
* to ensure the password was actually persisted (guards against silent
|
|
25
22
|
* IndexedDB write failures).
|
|
@@ -75,56 +72,6 @@ export declare function cacheOfflineCredentials(email: string, password: string,
|
|
|
75
72
|
* ```
|
|
76
73
|
*/
|
|
77
74
|
export declare function getOfflineCredentials(): Promise<OfflineCredentials | null>;
|
|
78
|
-
/**
|
|
79
|
-
* Verify email and password against cached credentials.
|
|
80
|
-
*
|
|
81
|
-
* Performs a multi-field comparison:
|
|
82
|
-
* 1. Checks that cached credentials exist.
|
|
83
|
-
* 2. Verifies the `userId` matches (prevents cross-user attacks).
|
|
84
|
-
* 3. Verifies the `email` matches (prevents credential reuse across accounts).
|
|
85
|
-
* 4. Verifies the password by hashing the input and comparing against the
|
|
86
|
-
* stored hash (or plaintext for legacy records).
|
|
87
|
-
*
|
|
88
|
-
* @param email - The email to verify against cached credentials.
|
|
89
|
-
* @param password - The plaintext password to verify.
|
|
90
|
-
* @param expectedUserId - The Supabase user ID that the credentials should belong to.
|
|
91
|
-
* Prevents offline login with credentials cached from a
|
|
92
|
-
* different user.
|
|
93
|
-
* @returns An object with `valid: true` on success, or `valid: false` with a
|
|
94
|
-
* `reason` string identifying which check failed.
|
|
95
|
-
*
|
|
96
|
-
* @example
|
|
97
|
-
* ```ts
|
|
98
|
-
* const result = await verifyOfflineCredentials(email, password, userId);
|
|
99
|
-
* if (result.valid) {
|
|
100
|
-
* await createOfflineSession(userId);
|
|
101
|
-
* } else {
|
|
102
|
-
* console.warn('Verification failed:', result.reason);
|
|
103
|
-
* }
|
|
104
|
-
* ```
|
|
105
|
-
*
|
|
106
|
-
* @see {@link cacheOfflineCredentials} for how credentials are stored.
|
|
107
|
-
*/
|
|
108
|
-
export declare function verifyOfflineCredentials(email: string, password: string, expectedUserId: string): Promise<{
|
|
109
|
-
valid: boolean;
|
|
110
|
-
reason?: string;
|
|
111
|
-
}>;
|
|
112
|
-
/**
|
|
113
|
-
* Update the cached password hash after an online password change.
|
|
114
|
-
*
|
|
115
|
-
* Should be called whenever the user changes their password while online,
|
|
116
|
-
* so the offline cache stays in sync and the login guard does not flag
|
|
117
|
-
* the old hash as stale.
|
|
118
|
-
*
|
|
119
|
-
* @param newPassword - The new plaintext password. Will be SHA-256-hashed before storage.
|
|
120
|
-
*
|
|
121
|
-
* @example
|
|
122
|
-
* ```ts
|
|
123
|
-
* await supabase.auth.updateUser({ password: newPassword });
|
|
124
|
-
* await updateOfflineCredentialsPassword(newPassword);
|
|
125
|
-
* ```
|
|
126
|
-
*/
|
|
127
|
-
export declare function updateOfflineCredentialsPassword(newPassword: string): Promise<void>;
|
|
128
75
|
/**
|
|
129
76
|
* Update the user profile in cached credentials after an online profile update.
|
|
130
77
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"offlineCredentials.d.ts","sourceRoot":"","sources":["../../src/auth/offlineCredentials.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"offlineCredentials.d.ts","sourceRoot":"","sources":["../../src/auth/offlineCredentials.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAGH,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,UAAU,CAAC;AACnD,OAAO,KAAK,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,uBAAuB,CAAC;AAmB3D;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAsB,uBAAuB,CAC3C,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,IAAI,EACV,QAAQ,EAAE,OAAO,GAChB,OAAO,CAAC,IAAI,CAAC,CAyCf;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAsB,qBAAqB,IAAI,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC,CAQhF;AAMD;;;;;;;;;;;;;GAaG;AACH,wBAAsB,+BAA+B,CACnD,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC/B,OAAO,CAAC,IAAI,CAAC,CAWf;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAsB,uBAAuB,IAAI,OAAO,CAAC,IAAI,CAAC,CAG7D"}
|