@prabhask5/stellar-engine 1.0.11 → 1.0.13
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 +31 -28
- package/dist/auth/deviceVerification.d.ts +57 -0
- package/dist/auth/deviceVerification.d.ts.map +1 -0
- package/dist/auth/deviceVerification.js +258 -0
- package/dist/auth/deviceVerification.js.map +1 -0
- package/dist/auth/resolveAuthState.d.ts.map +1 -1
- package/dist/auth/resolveAuthState.js +25 -83
- package/dist/auth/resolveAuthState.js.map +1 -1
- package/dist/auth/singleUser.d.ts +58 -8
- package/dist/auth/singleUser.d.ts.map +1 -1
- package/dist/auth/singleUser.js +345 -179
- package/dist/auth/singleUser.js.map +1 -1
- package/dist/config.d.ts +9 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js.map +1 -1
- package/dist/entries/auth.d.ts +2 -2
- package/dist/entries/auth.d.ts.map +1 -1
- package/dist/entries/auth.js +2 -2
- package/dist/entries/auth.js.map +1 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -2
- package/dist/index.js.map +1 -1
- package/dist/supabase/auth.d.ts +19 -1
- package/dist/supabase/auth.d.ts.map +1 -1
- package/dist/supabase/auth.js +66 -3
- package/dist/supabase/auth.js.map +1 -1
- package/dist/supabase/validate.d.ts +1 -1
- package/dist/supabase/validate.js +4 -4
- package/dist/supabase/validate.js.map +1 -1
- package/dist/types.d.ts +10 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@ A local-first, offline-capable sync engine for **SvelteKit + Supabase + Dexie**
|
|
|
13
13
|
- **Intent-based sync operations** -- operations preserve intent (`increment`, `set`, `create`, `delete`) instead of just final state, enabling smarter coalescing and conflict handling.
|
|
14
14
|
- **Three-tier conflict resolution** -- field-level diffing, numeric merge fields, and configurable exclusion lists let you resolve conflicts precisely rather than with blanket last-write-wins.
|
|
15
15
|
- **Offline authentication** -- credential caching and offline session tokens let users sign in and work without connectivity; sessions reconcile automatically on reconnect.
|
|
16
|
-
- **Single-user auth mode** -- for personal apps,
|
|
16
|
+
- **Single-user auth mode** -- for personal apps, use a simplified PIN code or password gate backed by real Supabase email/password auth. The user provides an email during setup; the PIN is padded to meet Supabase's minimum password length and verified server-side. Setup, unlock, lock, and gate change are all handled by the engine with full offline support.
|
|
17
17
|
- **Realtime subscriptions** -- Supabase Realtime channels push remote changes into local state instantly, with duplicate-delivery guards to prevent re-processing.
|
|
18
18
|
- **Operation coalescing** -- batches of rapid local writes (e.g., 50 individual increments) are compressed into a single outbound operation, reducing sync traffic dramatically.
|
|
19
19
|
- **Tombstone management** -- soft deletes are propagated cleanly, and stale tombstones are garbage-collected after a configurable retention period.
|
|
@@ -68,7 +68,7 @@ if (auth.authMode !== 'none') await startSyncEngine();
|
|
|
68
68
|
|
|
69
69
|
### Single-user mode
|
|
70
70
|
|
|
71
|
-
For personal apps with a PIN code gate
|
|
71
|
+
For personal apps with a PIN code gate backed by real Supabase email/password auth:
|
|
72
72
|
|
|
73
73
|
```ts
|
|
74
74
|
import { initEngine, startSyncEngine, supabase } from '@prabhask5/stellar-engine';
|
|
@@ -84,6 +84,8 @@ initEngine({
|
|
|
84
84
|
mode: 'single-user',
|
|
85
85
|
singleUser: { gateType: 'code', codeLength: 4 },
|
|
86
86
|
enableOfflineAuth: true,
|
|
87
|
+
// emailConfirmation: { enabled: true }, // require email confirmation on setup
|
|
88
|
+
// deviceVerification: { enabled: true }, // require OTP verification on new devices
|
|
87
89
|
},
|
|
88
90
|
});
|
|
89
91
|
|
|
@@ -91,9 +93,13 @@ await initConfig();
|
|
|
91
93
|
const auth = await resolveAuthState();
|
|
92
94
|
|
|
93
95
|
if (!auth.singleUserSetUp) {
|
|
94
|
-
// Show setup screen → call setupSingleUser(code, profile)
|
|
96
|
+
// Show setup screen → call setupSingleUser(code, profile, email)
|
|
97
|
+
// Returns { error, confirmationRequired }
|
|
98
|
+
// If confirmationRequired, prompt user to check email then call completeSingleUserSetup()
|
|
95
99
|
} else if (auth.authMode === 'none') {
|
|
96
100
|
// Show unlock screen → call unlockSingleUser(code)
|
|
101
|
+
// Returns { error, deviceVerificationRequired?, maskedEmail? }
|
|
102
|
+
// If deviceVerificationRequired, prompt for OTP then call completeDeviceVerification(tokenHash?)
|
|
97
103
|
} else {
|
|
98
104
|
await startSyncEngine();
|
|
99
105
|
}
|
|
@@ -107,7 +113,7 @@ Import only what you need via subpath exports:
|
|
|
107
113
|
|---|---|
|
|
108
114
|
| `@prabhask5/stellar-engine` | `initEngine`, `startSyncEngine`, `runFullSync`, `supabase`, `getDb`, `validateSupabaseCredentials`, `validateSchema` |
|
|
109
115
|
| `@prabhask5/stellar-engine/data` | All engine CRUD + query operations (`engineCreate`, `engineUpdate`, etc.) |
|
|
110
|
-
| `@prabhask5/stellar-engine/auth` | All auth functions (`signIn`, `signUp`, `resolveAuthState`, `isAdmin`, single-user: `setupSingleUser`, `unlockSingleUser`, `lockSingleUser`, etc.) |
|
|
116
|
+
| `@prabhask5/stellar-engine/auth` | All auth functions (`signIn`, `signUp`, `resolveAuthState`, `isAdmin`, single-user: `setupSingleUser`, `unlockSingleUser`, `lockSingleUser`, `completeSingleUserSetup`, `completeDeviceVerification`, `padPin`, etc.) |
|
|
111
117
|
| `@prabhask5/stellar-engine/stores` | Reactive stores + event subscriptions (`syncStatusStore`, `authState`, `onSyncComplete`, etc.) |
|
|
112
118
|
| `@prabhask5/stellar-engine/types` | All type exports (`Session`, `SyncEngineConfig`, `BatchOperation`, `SingleUserConfig`, etc.) |
|
|
113
119
|
| `@prabhask5/stellar-engine/utils` | Utility functions (`generateId`, `now`, `calculateNewOrder`, `snakeToCamel`, `debug`, etc.) |
|
|
@@ -130,34 +136,28 @@ Row-Level Security policies should scope reads and writes to the authenticated u
|
|
|
130
136
|
|
|
131
137
|
**Single-user mode additional requirements:**
|
|
132
138
|
|
|
133
|
-
Single-user mode
|
|
139
|
+
Single-user mode uses real Supabase email/password auth where the PIN is padded to meet Supabase's minimum password length. The user provides an email during setup, and the PIN is verified server-side.
|
|
140
|
+
|
|
141
|
+
If `deviceVerification` is enabled in the auth config, you need a `trusted_devices` table:
|
|
134
142
|
|
|
135
143
|
```sql
|
|
136
|
-
CREATE TABLE
|
|
144
|
+
CREATE TABLE trusted_devices (
|
|
137
145
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
is_deleted boolean NOT NULL DEFAULT false,
|
|
145
|
-
user_id uuid REFERENCES auth.users(id) ON DELETE CASCADE
|
|
146
|
+
user_id uuid REFERENCES auth.users(id) ON DELETE CASCADE NOT NULL,
|
|
147
|
+
device_id text NOT NULL,
|
|
148
|
+
device_label text,
|
|
149
|
+
trusted_at timestamptz DEFAULT now() NOT NULL,
|
|
150
|
+
last_used_at timestamptz DEFAULT now() NOT NULL,
|
|
151
|
+
UNIQUE(user_id, device_id)
|
|
146
152
|
);
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
-- RLS policy: authenticated users (anonymous sessions) can manage their own row
|
|
152
|
-
CREATE POLICY "Users can manage their own config"
|
|
153
|
-
ON single_user_config FOR ALL
|
|
154
|
-
USING (auth.uid() = user_id)
|
|
155
|
-
WITH CHECK (auth.uid() = user_id);
|
|
153
|
+
ALTER TABLE trusted_devices ENABLE ROW LEVEL SECURITY;
|
|
154
|
+
CREATE POLICY "Users manage own devices" ON trusted_devices FOR ALL
|
|
155
|
+
USING (auth.uid() = user_id) WITH CHECK (auth.uid() = user_id);
|
|
156
156
|
```
|
|
157
157
|
|
|
158
|
-
|
|
158
|
+
If `emailConfirmation` is enabled, Supabase email templates must be configured.
|
|
159
159
|
|
|
160
|
-
**Schema validation:** The engine automatically validates that all configured tables (and `
|
|
160
|
+
**Schema validation:** The engine automatically validates that all configured tables (and `trusted_devices` when `deviceVerification.enabled`) exist in Supabase on the first sync. Missing tables are reported via `syncStatusStore` and the debug console.
|
|
161
161
|
|
|
162
162
|
**Dexie (IndexedDB)**
|
|
163
163
|
|
|
@@ -260,18 +260,21 @@ Alternatively, you can provide a pre-created Dexie instance via the `db` config
|
|
|
260
260
|
|
|
261
261
|
### Single-user auth
|
|
262
262
|
|
|
263
|
-
For personal apps that
|
|
263
|
+
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.mode: 'single-user'` in the engine config.
|
|
264
264
|
|
|
265
265
|
| Export | Description |
|
|
266
266
|
|---|---|
|
|
267
267
|
| `isSingleUserSetUp()` | Check if initial setup is complete. |
|
|
268
268
|
| `getSingleUserInfo()` | Get display info (profile, gate type) for the unlock screen. |
|
|
269
|
-
| `setupSingleUser(gate, profile)` | First-time setup: create gate,
|
|
270
|
-
| `unlockSingleUser(gate)` | Verify gate and restore session (online or offline). |
|
|
269
|
+
| `setupSingleUser(gate, profile, email)` | First-time setup: create gate, Supabase email/password user, and store config. Returns `{ error, confirmationRequired }`. |
|
|
270
|
+
| `unlockSingleUser(gate)` | Verify gate and restore session (online or offline). Returns `{ error, deviceVerificationRequired?, maskedEmail? }`. |
|
|
271
|
+
| `completeSingleUserSetup()` | Called after the user confirms their email (when `emailConfirmation` is enabled). |
|
|
272
|
+
| `completeDeviceVerification(tokenHash?)` | Called after the user completes device OTP verification (when `deviceVerification` is enabled). |
|
|
271
273
|
| `lockSingleUser()` | Stop sync and reset auth state without destroying data. |
|
|
272
274
|
| `changeSingleUserGate(oldGate, newGate)` | Change the PIN code or password. |
|
|
273
275
|
| `updateSingleUserProfile(profile)` | Update profile in IndexedDB and Supabase metadata. |
|
|
274
276
|
| `resetSingleUser()` | Full reset: clear config, sign out, wipe local data. |
|
|
277
|
+
| `padPin(pin)` | Pad a PIN to meet Supabase's minimum password length requirement. |
|
|
275
278
|
|
|
276
279
|
### Queue
|
|
277
280
|
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Device Verification Module
|
|
3
|
+
*
|
|
4
|
+
* Manages trusted devices for single-user and multi-user modes.
|
|
5
|
+
* Uses Supabase `trusted_devices` table and `signInWithOtp()` for email-based
|
|
6
|
+
* device verification on untrusted devices.
|
|
7
|
+
*/
|
|
8
|
+
import type { TrustedDevice } from '../types';
|
|
9
|
+
/**
|
|
10
|
+
* Generate a human-readable device label (e.g. "Chrome on macOS").
|
|
11
|
+
*/
|
|
12
|
+
export declare function getDeviceLabel(): string;
|
|
13
|
+
/**
|
|
14
|
+
* Mask an email address for display (e.g. "pr••••@gmail.com").
|
|
15
|
+
*/
|
|
16
|
+
export declare function maskEmail(email: string): string;
|
|
17
|
+
/**
|
|
18
|
+
* Check if the current device is trusted for a given user.
|
|
19
|
+
* A device is trusted if it has a `trusted_devices` row with `last_used_at`
|
|
20
|
+
* within the configured trust duration.
|
|
21
|
+
*/
|
|
22
|
+
export declare function isDeviceTrusted(userId: string): Promise<boolean>;
|
|
23
|
+
/**
|
|
24
|
+
* Trust the current device for a user.
|
|
25
|
+
* Uses upsert on (user_id, device_id) unique constraint.
|
|
26
|
+
*/
|
|
27
|
+
export declare function trustCurrentDevice(userId: string): Promise<void>;
|
|
28
|
+
/**
|
|
29
|
+
* Update `last_used_at` for the current device (called on each successful login).
|
|
30
|
+
*/
|
|
31
|
+
export declare function touchTrustedDevice(userId: string): Promise<void>;
|
|
32
|
+
/**
|
|
33
|
+
* Get all trusted devices for a user.
|
|
34
|
+
*/
|
|
35
|
+
export declare function getTrustedDevices(userId: string): Promise<TrustedDevice[]>;
|
|
36
|
+
/**
|
|
37
|
+
* Remove a trusted device by ID.
|
|
38
|
+
*/
|
|
39
|
+
export declare function removeTrustedDevice(id: string): Promise<void>;
|
|
40
|
+
/**
|
|
41
|
+
* Send a device verification OTP email.
|
|
42
|
+
* Signs out first (untrusted device flow), then sends OTP.
|
|
43
|
+
*/
|
|
44
|
+
export declare function sendDeviceVerification(email: string): Promise<{
|
|
45
|
+
error: string | null;
|
|
46
|
+
}>;
|
|
47
|
+
/**
|
|
48
|
+
* Verify a device verification OTP token hash (from email link).
|
|
49
|
+
*/
|
|
50
|
+
export declare function verifyDeviceCode(tokenHash: string): Promise<{
|
|
51
|
+
error: string | null;
|
|
52
|
+
}>;
|
|
53
|
+
/**
|
|
54
|
+
* Get the current device ID (exposed for consumers).
|
|
55
|
+
*/
|
|
56
|
+
export declare function getCurrentDeviceId(): string;
|
|
57
|
+
//# sourceMappingURL=deviceVerification.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"deviceVerification.d.ts","sourceRoot":"","sources":["../../src/auth/deviceVerification.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAqC9C;;GAEG;AACH,wBAAgB,cAAc,IAAI,MAAM,CAqBvC;AAMD;;GAEG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAO/C;AAMD;;;;GAIG;AACH,wBAAsB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAyBtE;AAED;;;GAGG;AACH,wBAAsB,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAwBtE;AAED;;GAEG;AACH,wBAAsB,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAgBtE;AAED;;GAEG;AACH,wBAAsB,iBAAiB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,EAAE,CAAC,CAkBhF;AAED;;GAEG;AACH,wBAAsB,mBAAmB,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAenE;AAMD;;;GAGG;AACH,wBAAsB,sBAAsB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC;IAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,CAAC,CAqB7F;AAED;;GAEG;AACH,wBAAsB,gBAAgB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC;IAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,CAAC,CAkB3F;AAED;;GAEG;AACH,wBAAgB,kBAAkB,IAAI,MAAM,CAE3C"}
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Device Verification Module
|
|
3
|
+
*
|
|
4
|
+
* Manages trusted devices for single-user and multi-user modes.
|
|
5
|
+
* Uses Supabase `trusted_devices` table and `signInWithOtp()` for email-based
|
|
6
|
+
* device verification on untrusted devices.
|
|
7
|
+
*/
|
|
8
|
+
import { getEngineConfig } from '../config';
|
|
9
|
+
import { supabase } from '../supabase/client';
|
|
10
|
+
import { getDeviceId } from '../deviceId';
|
|
11
|
+
import { debugLog, debugWarn, debugError } from '../debug';
|
|
12
|
+
const DEFAULT_TRUST_DURATION_DAYS = 90;
|
|
13
|
+
// ============================================================
|
|
14
|
+
// HELPERS
|
|
15
|
+
// ============================================================
|
|
16
|
+
function getDb() {
|
|
17
|
+
const db = getEngineConfig().db;
|
|
18
|
+
if (!db)
|
|
19
|
+
throw new Error('Database not initialized.');
|
|
20
|
+
return db;
|
|
21
|
+
}
|
|
22
|
+
function getTrustDurationDays() {
|
|
23
|
+
return getEngineConfig().auth?.deviceVerification?.trustDurationDays ?? DEFAULT_TRUST_DURATION_DAYS;
|
|
24
|
+
}
|
|
25
|
+
function snakeToCamelDevice(row) {
|
|
26
|
+
return {
|
|
27
|
+
id: row.id,
|
|
28
|
+
userId: row.user_id,
|
|
29
|
+
deviceId: row.device_id,
|
|
30
|
+
deviceLabel: row.device_label,
|
|
31
|
+
trustedAt: row.trusted_at,
|
|
32
|
+
lastUsedAt: row.last_used_at,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
// ============================================================
|
|
36
|
+
// DEVICE LABEL
|
|
37
|
+
// ============================================================
|
|
38
|
+
/**
|
|
39
|
+
* Generate a human-readable device label (e.g. "Chrome on macOS").
|
|
40
|
+
*/
|
|
41
|
+
export function getDeviceLabel() {
|
|
42
|
+
if (typeof navigator === 'undefined')
|
|
43
|
+
return 'Unknown device';
|
|
44
|
+
const ua = navigator.userAgent;
|
|
45
|
+
let browser = 'Browser';
|
|
46
|
+
let os = '';
|
|
47
|
+
// Detect browser
|
|
48
|
+
if (ua.includes('Firefox'))
|
|
49
|
+
browser = 'Firefox';
|
|
50
|
+
else if (ua.includes('Edg/'))
|
|
51
|
+
browser = 'Edge';
|
|
52
|
+
else if (ua.includes('Chrome') && !ua.includes('Edg/'))
|
|
53
|
+
browser = 'Chrome';
|
|
54
|
+
else if (ua.includes('Safari') && !ua.includes('Chrome'))
|
|
55
|
+
browser = 'Safari';
|
|
56
|
+
// Detect OS
|
|
57
|
+
if (ua.includes('Mac OS X'))
|
|
58
|
+
os = 'macOS';
|
|
59
|
+
else if (ua.includes('Windows'))
|
|
60
|
+
os = 'Windows';
|
|
61
|
+
else if (ua.includes('Linux'))
|
|
62
|
+
os = 'Linux';
|
|
63
|
+
else if (ua.includes('Android'))
|
|
64
|
+
os = 'Android';
|
|
65
|
+
else if (ua.includes('iPhone') || ua.includes('iPad'))
|
|
66
|
+
os = 'iOS';
|
|
67
|
+
return os ? `${browser} on ${os}` : browser;
|
|
68
|
+
}
|
|
69
|
+
// ============================================================
|
|
70
|
+
// EMAIL MASKING
|
|
71
|
+
// ============================================================
|
|
72
|
+
/**
|
|
73
|
+
* Mask an email address for display (e.g. "pr••••@gmail.com").
|
|
74
|
+
*/
|
|
75
|
+
export function maskEmail(email) {
|
|
76
|
+
const [local, domain] = email.split('@');
|
|
77
|
+
if (!domain)
|
|
78
|
+
return email;
|
|
79
|
+
const visible = Math.min(2, local.length);
|
|
80
|
+
const masked = local.slice(0, visible) + '\u2022'.repeat(Math.max(1, local.length - visible));
|
|
81
|
+
return `${masked}@${domain}`;
|
|
82
|
+
}
|
|
83
|
+
// ============================================================
|
|
84
|
+
// DEVICE TRUST QUERIES
|
|
85
|
+
// ============================================================
|
|
86
|
+
/**
|
|
87
|
+
* Check if the current device is trusted for a given user.
|
|
88
|
+
* A device is trusted if it has a `trusted_devices` row with `last_used_at`
|
|
89
|
+
* within the configured trust duration.
|
|
90
|
+
*/
|
|
91
|
+
export async function isDeviceTrusted(userId) {
|
|
92
|
+
try {
|
|
93
|
+
const deviceId = getDeviceId();
|
|
94
|
+
const trustDays = getTrustDurationDays();
|
|
95
|
+
const cutoff = new Date();
|
|
96
|
+
cutoff.setDate(cutoff.getDate() - trustDays);
|
|
97
|
+
const { data, error } = await supabase
|
|
98
|
+
.from('trusted_devices')
|
|
99
|
+
.select('id, last_used_at')
|
|
100
|
+
.eq('user_id', userId)
|
|
101
|
+
.eq('device_id', deviceId)
|
|
102
|
+
.gte('last_used_at', cutoff.toISOString())
|
|
103
|
+
.limit(1);
|
|
104
|
+
if (error) {
|
|
105
|
+
debugWarn('[DeviceVerification] Trust check failed:', error.message);
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
return (data?.length ?? 0) > 0;
|
|
109
|
+
}
|
|
110
|
+
catch (e) {
|
|
111
|
+
debugError('[DeviceVerification] Trust check error:', e);
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Trust the current device for a user.
|
|
117
|
+
* Uses upsert on (user_id, device_id) unique constraint.
|
|
118
|
+
*/
|
|
119
|
+
export async function trustCurrentDevice(userId) {
|
|
120
|
+
try {
|
|
121
|
+
const deviceId = getDeviceId();
|
|
122
|
+
const label = getDeviceLabel();
|
|
123
|
+
const now = new Date().toISOString();
|
|
124
|
+
const { error } = await supabase
|
|
125
|
+
.from('trusted_devices')
|
|
126
|
+
.upsert({
|
|
127
|
+
user_id: userId,
|
|
128
|
+
device_id: deviceId,
|
|
129
|
+
device_label: label,
|
|
130
|
+
trusted_at: now,
|
|
131
|
+
last_used_at: now,
|
|
132
|
+
}, { onConflict: 'user_id,device_id' });
|
|
133
|
+
if (error) {
|
|
134
|
+
debugError('[DeviceVerification] Trust device failed:', error.message);
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
debugLog('[DeviceVerification] Device trusted:', label);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
catch (e) {
|
|
141
|
+
debugError('[DeviceVerification] Trust device error:', e);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Update `last_used_at` for the current device (called on each successful login).
|
|
146
|
+
*/
|
|
147
|
+
export async function touchTrustedDevice(userId) {
|
|
148
|
+
try {
|
|
149
|
+
const deviceId = getDeviceId();
|
|
150
|
+
const { error } = await supabase
|
|
151
|
+
.from('trusted_devices')
|
|
152
|
+
.update({ last_used_at: new Date().toISOString() })
|
|
153
|
+
.eq('user_id', userId)
|
|
154
|
+
.eq('device_id', deviceId);
|
|
155
|
+
if (error) {
|
|
156
|
+
debugWarn('[DeviceVerification] Touch device failed:', error.message);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
catch (e) {
|
|
160
|
+
debugWarn('[DeviceVerification] Touch device error:', e);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Get all trusted devices for a user.
|
|
165
|
+
*/
|
|
166
|
+
export async function getTrustedDevices(userId) {
|
|
167
|
+
try {
|
|
168
|
+
const { data, error } = await supabase
|
|
169
|
+
.from('trusted_devices')
|
|
170
|
+
.select('id, user_id, device_id, device_label, trusted_at, last_used_at')
|
|
171
|
+
.eq('user_id', userId)
|
|
172
|
+
.order('last_used_at', { ascending: false });
|
|
173
|
+
if (error) {
|
|
174
|
+
debugError('[DeviceVerification] Get devices failed:', error.message);
|
|
175
|
+
return [];
|
|
176
|
+
}
|
|
177
|
+
return (data || []).map(snakeToCamelDevice);
|
|
178
|
+
}
|
|
179
|
+
catch (e) {
|
|
180
|
+
debugError('[DeviceVerification] Get devices error:', e);
|
|
181
|
+
return [];
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Remove a trusted device by ID.
|
|
186
|
+
*/
|
|
187
|
+
export async function removeTrustedDevice(id) {
|
|
188
|
+
try {
|
|
189
|
+
const { error } = await supabase
|
|
190
|
+
.from('trusted_devices')
|
|
191
|
+
.delete()
|
|
192
|
+
.eq('id', id);
|
|
193
|
+
if (error) {
|
|
194
|
+
debugError('[DeviceVerification] Remove device failed:', error.message);
|
|
195
|
+
}
|
|
196
|
+
else {
|
|
197
|
+
debugLog('[DeviceVerification] Device removed:', id);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
catch (e) {
|
|
201
|
+
debugError('[DeviceVerification] Remove device error:', e);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
// ============================================================
|
|
205
|
+
// OTP VERIFICATION FLOW
|
|
206
|
+
// ============================================================
|
|
207
|
+
/**
|
|
208
|
+
* Send a device verification OTP email.
|
|
209
|
+
* Signs out first (untrusted device flow), then sends OTP.
|
|
210
|
+
*/
|
|
211
|
+
export async function sendDeviceVerification(email) {
|
|
212
|
+
try {
|
|
213
|
+
// Sign out first — untrusted device should not retain a session
|
|
214
|
+
await supabase.auth.signOut();
|
|
215
|
+
const { error } = await supabase.auth.signInWithOtp({
|
|
216
|
+
email,
|
|
217
|
+
options: { shouldCreateUser: false },
|
|
218
|
+
});
|
|
219
|
+
if (error) {
|
|
220
|
+
debugError('[DeviceVerification] Send OTP failed:', error.message);
|
|
221
|
+
return { error: error.message };
|
|
222
|
+
}
|
|
223
|
+
debugLog('[DeviceVerification] OTP sent to:', maskEmail(email));
|
|
224
|
+
return { error: null };
|
|
225
|
+
}
|
|
226
|
+
catch (e) {
|
|
227
|
+
debugError('[DeviceVerification] Send OTP error:', e);
|
|
228
|
+
return { error: e instanceof Error ? e.message : 'Failed to send verification email' };
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Verify a device verification OTP token hash (from email link).
|
|
233
|
+
*/
|
|
234
|
+
export async function verifyDeviceCode(tokenHash) {
|
|
235
|
+
try {
|
|
236
|
+
const { error } = await supabase.auth.verifyOtp({
|
|
237
|
+
token_hash: tokenHash,
|
|
238
|
+
type: 'email',
|
|
239
|
+
});
|
|
240
|
+
if (error) {
|
|
241
|
+
debugError('[DeviceVerification] Verify OTP failed:', error.message);
|
|
242
|
+
return { error: error.message };
|
|
243
|
+
}
|
|
244
|
+
debugLog('[DeviceVerification] OTP verified successfully');
|
|
245
|
+
return { error: null };
|
|
246
|
+
}
|
|
247
|
+
catch (e) {
|
|
248
|
+
debugError('[DeviceVerification] Verify OTP error:', e);
|
|
249
|
+
return { error: e instanceof Error ? e.message : 'Verification failed' };
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Get the current device ID (exposed for consumers).
|
|
254
|
+
*/
|
|
255
|
+
export function getCurrentDeviceId() {
|
|
256
|
+
return getDeviceId();
|
|
257
|
+
}
|
|
258
|
+
//# sourceMappingURL=deviceVerification.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"deviceVerification.js","sourceRoot":"","sources":["../../src/auth/deviceVerification.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"resolveAuthState.d.ts","sourceRoot":"","sources":["../../src/auth/resolveAuthState.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,uBAAuB,CAAC;AACrD,OAAO,KAAK,EAAE,kBAAkB,EAAoB,MAAM,UAAU,CAAC;
|
|
1
|
+
{"version":3,"file":"resolveAuthState.d.ts","sourceRoot":"","sources":["../../src/auth/resolveAuthState.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,uBAAuB,CAAC;AACrD,OAAO,KAAK,EAAE,kBAAkB,EAAoB,MAAM,UAAU,CAAC;AAUrE,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,OAAO,GAAG,IAAI,CAAC;IACxB,QAAQ,EAAE,UAAU,GAAG,SAAS,GAAG,MAAM,CAAC;IAC1C,cAAc,EAAE,kBAAkB,GAAG,IAAI,CAAC;IAC1C,0FAA0F;IAC1F,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B;AAED;;;;;;;GAOG;AACH,wBAAsB,gBAAgB,IAAI,OAAO,CAAC,eAAe,CAAC,CA8DjE"}
|
|
@@ -91,21 +91,34 @@ async function resolveSingleUserAuthState() {
|
|
|
91
91
|
if (!db) {
|
|
92
92
|
return { session: null, authMode: 'none', offlineProfile: null, singleUserSetUp: false };
|
|
93
93
|
}
|
|
94
|
-
|
|
95
|
-
const isOffline = typeof navigator !== 'undefined' && !navigator.onLine;
|
|
96
|
-
// Multi-device: if no local config but online, check Supabase
|
|
97
|
-
if (!config && !isOffline) {
|
|
98
|
-
config = await fetchAndCacheRemoteConfig(db);
|
|
99
|
-
}
|
|
94
|
+
const config = await db.table('singleUserConfig').get('config');
|
|
100
95
|
if (!config) {
|
|
96
|
+
// No local config — user hasn't set up on this device.
|
|
97
|
+
// With real email/password auth, new devices go through the login flow
|
|
98
|
+
// (email + PIN) which creates the local config after signInWithPassword.
|
|
99
|
+
return { session: null, authMode: 'none', offlineProfile: null, singleUserSetUp: false };
|
|
100
|
+
}
|
|
101
|
+
if (!config.email) {
|
|
102
|
+
// Legacy config from anonymous auth era — no email means user needs to
|
|
103
|
+
// go through the new setup flow (email + PIN). Old anonymous data won't
|
|
104
|
+
// be accessible under ownership-based RLS anyway.
|
|
105
|
+
// Nuke all legacy auth artifacts so the user gets a clean slate.
|
|
106
|
+
debugLog('[Auth] Legacy config without email detected, clearing old auth state');
|
|
107
|
+
try {
|
|
108
|
+
await db.table('singleUserConfig').delete('config');
|
|
109
|
+
await db.table('offlineCredentials').delete('current_user');
|
|
110
|
+
await db.table('offlineSession').delete('current_session');
|
|
111
|
+
}
|
|
112
|
+
catch (e) {
|
|
113
|
+
debugWarn('[Auth] Failed to clear legacy auth state:', e);
|
|
114
|
+
}
|
|
101
115
|
return { session: null, authMode: 'none', offlineProfile: null, singleUserSetUp: false };
|
|
102
116
|
}
|
|
103
117
|
// Config exists — check session
|
|
104
118
|
let session = await getSession();
|
|
105
119
|
// If session exists but access token is expired, try refreshing before giving up.
|
|
106
|
-
//
|
|
107
|
-
//
|
|
108
|
-
// the access token expires (default: 1 hour).
|
|
120
|
+
// Refresh tokens outlive the access token — without this, users are forced to
|
|
121
|
+
// re-enter the PIN on every page load once the access token expires (default: 1 hour).
|
|
109
122
|
if (session && isSessionExpired(session)) {
|
|
110
123
|
debugLog('[Auth] Single-user session expired, attempting refresh...');
|
|
111
124
|
try {
|
|
@@ -125,12 +138,10 @@ async function resolveSingleUserAuthState() {
|
|
|
125
138
|
}
|
|
126
139
|
const hasValidSession = session && !isSessionExpired(session);
|
|
127
140
|
if (hasValidSession) {
|
|
128
|
-
// Ensure Supabase single_user_config is populated (for existing users who
|
|
129
|
-
// set up before the Supabase write was added, and for extension access)
|
|
130
|
-
ensureRemoteConfig(config).catch(() => { });
|
|
131
141
|
return { session, authMode: 'supabase', offlineProfile: null, singleUserSetUp: true };
|
|
132
142
|
}
|
|
133
143
|
// Check for offline session
|
|
144
|
+
const isOffline = typeof navigator !== 'undefined' && !navigator.onLine;
|
|
134
145
|
if (isOffline) {
|
|
135
146
|
// Even expired cached Supabase session is usable offline
|
|
136
147
|
if (session) {
|
|
@@ -141,8 +152,8 @@ async function resolveSingleUserAuthState() {
|
|
|
141
152
|
const offlineProfile = {
|
|
142
153
|
id: 'current_user',
|
|
143
154
|
userId: offlineSession.userId,
|
|
144
|
-
email: '',
|
|
145
|
-
password: config.gateHash,
|
|
155
|
+
email: config.email || '',
|
|
156
|
+
password: config.gateHash || '',
|
|
146
157
|
profile: config.profile,
|
|
147
158
|
cachedAt: new Date().toISOString()
|
|
148
159
|
};
|
|
@@ -157,73 +168,4 @@ async function resolveSingleUserAuthState() {
|
|
|
157
168
|
return { session: null, authMode: 'none', offlineProfile: null, singleUserSetUp: false };
|
|
158
169
|
}
|
|
159
170
|
}
|
|
160
|
-
/**
|
|
161
|
-
* Multi-device support: fetch config from Supabase and cache locally.
|
|
162
|
-
* Called when a new device has no local config but is online.
|
|
163
|
-
*/
|
|
164
|
-
async function fetchAndCacheRemoteConfig(db) {
|
|
165
|
-
try {
|
|
166
|
-
// Need a session to query — sign in anonymously
|
|
167
|
-
const { data: sessionData } = await supabase.auth.getSession();
|
|
168
|
-
if (!sessionData?.session) {
|
|
169
|
-
const { error } = await supabase.auth.signInAnonymously();
|
|
170
|
-
if (error) {
|
|
171
|
-
debugWarn('[Auth] Anonymous sign-in failed during remote config fetch:', error.message);
|
|
172
|
-
return null;
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
const { data, error } = await supabase
|
|
176
|
-
.from('single_user_config')
|
|
177
|
-
.select('gate_type, code_length, gate_hash, profile')
|
|
178
|
-
.eq('id', 'config')
|
|
179
|
-
.single();
|
|
180
|
-
if (error || !data)
|
|
181
|
-
return null;
|
|
182
|
-
const engineConfig = getEngineConfig();
|
|
183
|
-
const singleUserOpts = engineConfig.auth?.singleUser;
|
|
184
|
-
const config = {
|
|
185
|
-
id: 'config',
|
|
186
|
-
gateType: data.gate_type,
|
|
187
|
-
codeLength: data.code_length ?? singleUserOpts?.codeLength,
|
|
188
|
-
gateHash: data.gate_hash,
|
|
189
|
-
profile: data.profile || {},
|
|
190
|
-
setupAt: new Date().toISOString(),
|
|
191
|
-
updatedAt: new Date().toISOString()
|
|
192
|
-
};
|
|
193
|
-
await db.table('singleUserConfig').put(config);
|
|
194
|
-
debugLog('[Auth] Fetched remote config and cached locally (multi-device)');
|
|
195
|
-
return config;
|
|
196
|
-
}
|
|
197
|
-
catch (e) {
|
|
198
|
-
debugWarn('[Auth] Failed to fetch remote config:', e);
|
|
199
|
-
return null;
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
/**
|
|
203
|
-
* Ensure Supabase single_user_config is populated.
|
|
204
|
-
* Syncs from local IndexedDB if the Supabase row is missing.
|
|
205
|
-
*/
|
|
206
|
-
async function ensureRemoteConfig(config) {
|
|
207
|
-
try {
|
|
208
|
-
const { data } = await supabase
|
|
209
|
-
.from('single_user_config')
|
|
210
|
-
.select('id')
|
|
211
|
-
.eq('id', 'config')
|
|
212
|
-
.single();
|
|
213
|
-
if (data)
|
|
214
|
-
return; // Already exists
|
|
215
|
-
// Missing — sync from local
|
|
216
|
-
await supabase.from('single_user_config').upsert({
|
|
217
|
-
id: 'config',
|
|
218
|
-
gate_type: config.gateType,
|
|
219
|
-
code_length: config.codeLength,
|
|
220
|
-
gate_hash: config.gateHash,
|
|
221
|
-
profile: config.profile,
|
|
222
|
-
});
|
|
223
|
-
debugLog('[Auth] Synced local config to Supabase (ensureRemoteConfig)');
|
|
224
|
-
}
|
|
225
|
-
catch (e) {
|
|
226
|
-
debugWarn('[Auth] Failed to ensure remote config:', e);
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
171
|
//# sourceMappingURL=resolveAuthState.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"resolveAuthState.js","sourceRoot":"","sources":["../../src/auth/resolveAuthState.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAIH,OAAO,EAAE,UAAU,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAChE,OAAO,EAAE,sBAAsB,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AAC/E,OAAO,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAC7D,OAAO,EAAE,eAAe,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AACvD,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAC9C,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;
|
|
1
|
+
{"version":3,"file":"resolveAuthState.js","sourceRoot":"","sources":["../../src/auth/resolveAuthState.ts"],"names":[],"mappings":"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"}
|