@kbve/droid 0.1.1 → 0.1.2
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/droid.mjs +2311 -203
- package/index.d.ts +3 -0
- package/lib/agents/api-types.d.ts +121 -0
- package/lib/agents/api-types.d.ts.map +1 -0
- package/lib/agents/auth/init.d.ts +8 -0
- package/lib/agents/auth/init.d.ts.map +1 -0
- package/lib/agents/cache.d.ts +9 -0
- package/lib/agents/cache.d.ts.map +1 -0
- package/lib/agents/client/callGuildVault.d.ts +3 -0
- package/lib/agents/client/callGuildVault.d.ts.map +1 -0
- package/lib/agents/client/callJson.d.ts +3 -0
- package/lib/agents/client/callJson.d.ts.map +1 -0
- package/lib/agents/config.d.ts +36 -0
- package/lib/agents/config.d.ts.map +1 -0
- package/lib/agents/constants.d.ts +11 -0
- package/lib/agents/constants.d.ts.map +1 -0
- package/lib/agents/createAgents.d.ts +4 -0
- package/lib/agents/createAgents.d.ts.map +1 -0
- package/lib/agents/ctx.d.ts +46 -0
- package/lib/agents/ctx.d.ts.map +1 -0
- package/lib/agents/discord/bot.d.ts +21 -0
- package/lib/agents/discord/bot.d.ts.map +1 -0
- package/lib/agents/discord/guilds.d.ts +8 -0
- package/lib/agents/discord/guilds.d.ts.map +1 -0
- package/lib/agents/formDrafts.d.ts +5 -0
- package/lib/agents/formDrafts.d.ts.map +1 -0
- package/lib/agents/generated/agents-schema.d.ts +73 -0
- package/lib/agents/generated/agents-schema.d.ts.map +1 -0
- package/lib/agents/generated/discordsh-agents-schema.d.ts +71 -0
- package/lib/agents/generated/discordsh-agents-schema.d.ts.map +1 -0
- package/lib/agents/github/backfill.d.ts +23 -0
- package/lib/agents/github/backfill.d.ts.map +1 -0
- package/lib/agents/github/events.d.ts +10 -0
- package/lib/agents/github/events.d.ts.map +1 -0
- package/lib/agents/github/pat.d.ts +13 -0
- package/lib/agents/github/pat.d.ts.map +1 -0
- package/lib/agents/github/repoConfig.d.ts +25 -0
- package/lib/agents/github/repoConfig.d.ts.map +1 -0
- package/lib/agents/github/tokens.d.ts +22 -0
- package/lib/agents/github/tokens.d.ts.map +1 -0
- package/lib/agents/github/webhook.d.ts +37 -0
- package/lib/agents/github/webhook.d.ts.map +1 -0
- package/lib/agents/index.d.ts +8 -0
- package/lib/agents/index.d.ts.map +1 -0
- package/lib/agents/state/atoms.d.ts +122 -0
- package/lib/agents/state/atoms.d.ts.map +1 -0
- package/lib/agents/types.d.ts +65 -0
- package/lib/agents/types.d.ts.map +1 -0
- package/lib/gateway/SupabaseGateway.d.ts.map +1 -1
- package/lib/gateway/types.d.ts +1 -1
- package/lib/gateway/types.d.ts.map +1 -1
- package/lib/workers/canvas-worker.js +6 -449
- package/lib/workers/data.d.ts.map +1 -1
- package/lib/workers/db-worker.d.ts +2 -2
- package/lib/workers/db-worker.d.ts.map +1 -1
- package/lib/workers/db-worker.js +14 -4170
- package/lib/workers/main.d.ts +0 -2
- package/lib/workers/main.d.ts.map +1 -1
- package/lib/workers/supabase-db-worker.js +11 -9325
- package/lib/workers/supabase-shared-worker.js +13 -12623
- package/lib/workers/ws-worker.d.ts +1 -2
- package/lib/workers/ws-worker.d.ts.map +1 -1
- package/lib/workers/ws-worker.js +6 -333
- package/main.js +5622 -4786
- package/package.json +2 -1
- package/src/index.ts +3 -0
- package/src/lib/agents/api-types.ts +170 -0
- package/src/lib/agents/auth/init.ts +200 -0
- package/src/lib/agents/cache.ts +127 -0
- package/src/lib/agents/client/callGuildVault.ts +71 -0
- package/src/lib/agents/client/callJson.ts +64 -0
- package/src/lib/agents/config.ts +83 -0
- package/src/lib/agents/constants.ts +11 -0
- package/src/lib/agents/createAgents.ts +62 -0
- package/src/lib/agents/ctx.ts +63 -0
- package/src/lib/agents/discord/bot.ts +169 -0
- package/src/lib/agents/discord/guilds.ts +106 -0
- package/src/lib/agents/formDrafts.ts +55 -0
- package/src/lib/agents/generated/agents-schema.ts +155 -0
- package/src/lib/agents/generated/discordsh-agents-schema.ts +96 -0
- package/src/lib/agents/github/backfill.ts +126 -0
- package/src/lib/agents/github/events.ts +131 -0
- package/src/lib/agents/github/pat.ts +150 -0
- package/src/lib/agents/github/repoConfig.ts +240 -0
- package/src/lib/agents/github/tokens.ts +222 -0
- package/src/lib/agents/github/webhook.ts +349 -0
- package/src/lib/agents/index.ts +35 -0
- package/src/lib/agents/state/atoms.ts +122 -0
- package/src/lib/agents/types.ts +83 -0
- package/src/lib/api.ts +71 -0
- package/src/lib/gateway/SupabaseGateway.ts +55 -2
- package/src/lib/gateway/WorkerCommunication.ts +1 -1
- package/src/lib/gateway/capabilities.spec.ts +144 -0
- package/src/lib/gateway/types.ts +97 -70
- package/src/lib/mod/module/supabase/mod-supabase.ts +10 -5
- package/src/lib/state/auth.ts +121 -3
- package/src/lib/state/bento.spec.ts +76 -0
- package/src/lib/state/bento.ts +51 -0
- package/src/lib/state/index.ts +64 -0
- package/src/lib/state/profile-sync.ts +214 -0
- package/src/lib/state/profile.ts +155 -0
- package/src/lib/state/staff.ts +128 -0
- package/src/lib/state/sync-bus.ts +109 -0
- package/src/lib/state/welcome-toast.spec.ts +101 -0
- package/src/lib/sw-recovery.spec.ts +165 -0
- package/src/lib/sw-recovery.ts +141 -0
- package/src/lib/types/event-schemas.spec.ts +120 -0
- package/src/lib/types/event-types.spec.ts +5 -2
- package/src/lib/types/event-types.ts +74 -0
- package/src/lib/workers/data.ts +5 -1
- package/src/lib/workers/db-worker.ts +34 -8
- package/src/lib/workers/main.ts +3 -18
- package/src/lib/workers/supabase-db-worker.ts +25 -3
- package/src/lib/workers/supabase-shared-worker.ts +43 -8
- package/src/lib/workers/ws-worker.spec.ts +41 -0
- package/src/lib/workers/ws-worker.ts +121 -28
- package/workers/main.js +2 -12
- package/comlink.js +0 -247
- package/index.js +0 -9263
- package/reference.js +0 -719
|
@@ -17,6 +17,8 @@ import {
|
|
|
17
17
|
import { SharedWorkerStrategy } from './strategies/SharedWorkerStrategy';
|
|
18
18
|
import { WebWorkerStrategy } from './strategies/WebWorkerStrategy';
|
|
19
19
|
import { DirectStrategy } from './strategies/DirectStrategy';
|
|
20
|
+
import { getWorkerCommunication } from './WorkerCommunication';
|
|
21
|
+
import { DroidEvents } from '../workers/events';
|
|
20
22
|
|
|
21
23
|
// Vite ?worker&url imports — bundles workers as JS and returns their URL
|
|
22
24
|
import defaultSharedWorkerUrl from '../workers/supabase-shared-worker?worker&url';
|
|
@@ -72,6 +74,22 @@ export class SupabaseGateway implements ISupabaseStrategy {
|
|
|
72
74
|
this.strategy = new DirectStrategy();
|
|
73
75
|
break;
|
|
74
76
|
}
|
|
77
|
+
|
|
78
|
+
// Forward worker-error events from BroadcastChannel to DroidEvents
|
|
79
|
+
if (typeof window !== 'undefined') {
|
|
80
|
+
const comm = getWorkerCommunication();
|
|
81
|
+
comm.on('worker-error', (payload) => {
|
|
82
|
+
DroidEvents.emit(
|
|
83
|
+
'worker-error',
|
|
84
|
+
payload as {
|
|
85
|
+
timestamp: number;
|
|
86
|
+
worker: 'shared' | 'db' | 'ws';
|
|
87
|
+
operation: string;
|
|
88
|
+
message: string;
|
|
89
|
+
},
|
|
90
|
+
);
|
|
91
|
+
});
|
|
92
|
+
}
|
|
75
93
|
}
|
|
76
94
|
|
|
77
95
|
getStrategyType(): StrategyType {
|
|
@@ -84,12 +102,47 @@ export class SupabaseGateway implements ISupabaseStrategy {
|
|
|
84
102
|
|
|
85
103
|
// Delegate all methods to the selected strategy
|
|
86
104
|
|
|
87
|
-
init(
|
|
105
|
+
async init(
|
|
88
106
|
url: string,
|
|
89
107
|
anonKey: string,
|
|
90
108
|
options?: Record<string, unknown>,
|
|
91
109
|
): Promise<SessionResponse> {
|
|
92
|
-
|
|
110
|
+
if (this.strategyType === 'direct') {
|
|
111
|
+
return this.strategy.init(url, anonKey, options);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const WORKER_INIT_FALLBACK_MS = 3500;
|
|
115
|
+
const primary = this.strategy.init(url, anonKey, options);
|
|
116
|
+
|
|
117
|
+
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
118
|
+
const fallback = new Promise<never>((_, reject) => {
|
|
119
|
+
timer = setTimeout(
|
|
120
|
+
() => reject(new Error('worker init stalled')),
|
|
121
|
+
WORKER_INIT_FALLBACK_MS,
|
|
122
|
+
);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
try {
|
|
126
|
+
const result = await Promise.race([primary, fallback]);
|
|
127
|
+
if (timer) clearTimeout(timer);
|
|
128
|
+
return result;
|
|
129
|
+
} catch (err) {
|
|
130
|
+
if (timer) clearTimeout(timer);
|
|
131
|
+
const previousType = this.strategyType;
|
|
132
|
+
console.warn(
|
|
133
|
+
`[SupabaseGateway] ${previousType} init did not respond in ${WORKER_INIT_FALLBACK_MS}ms — switching to direct strategy.`,
|
|
134
|
+
err,
|
|
135
|
+
);
|
|
136
|
+
this.strategy = new DirectStrategy();
|
|
137
|
+
this.strategyType = 'direct';
|
|
138
|
+
DroidEvents.emit('gateway-strategy-fallback', {
|
|
139
|
+
timestamp: Date.now(),
|
|
140
|
+
from: previousType,
|
|
141
|
+
to: 'direct',
|
|
142
|
+
reason: err instanceof Error ? err.message : String(err),
|
|
143
|
+
});
|
|
144
|
+
return this.strategy.init(url, anonKey, options);
|
|
145
|
+
}
|
|
93
146
|
}
|
|
94
147
|
|
|
95
148
|
on(event: string, callback: (payload: unknown) => void): () => void {
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
detectCapabilities,
|
|
4
|
+
getStrategyDescription,
|
|
5
|
+
logCapabilities,
|
|
6
|
+
selectStrategy,
|
|
7
|
+
} from './capabilities';
|
|
8
|
+
|
|
9
|
+
const realUA = navigator.userAgent;
|
|
10
|
+
|
|
11
|
+
afterEach(() => {
|
|
12
|
+
Object.defineProperty(navigator, 'userAgent', {
|
|
13
|
+
value: realUA,
|
|
14
|
+
configurable: true,
|
|
15
|
+
});
|
|
16
|
+
vi.restoreAllMocks();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
function setUA(ua: string) {
|
|
20
|
+
Object.defineProperty(navigator, 'userAgent', {
|
|
21
|
+
value: ua,
|
|
22
|
+
configurable: true,
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
describe('detectCapabilities', () => {
|
|
27
|
+
it('flags Safari when Safari is in UA but Chrome is not', () => {
|
|
28
|
+
setUA(
|
|
29
|
+
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Safari/605.1.15',
|
|
30
|
+
);
|
|
31
|
+
const caps = detectCapabilities();
|
|
32
|
+
expect(caps.isSafari).toBe(true);
|
|
33
|
+
expect(caps.isAndroid).toBe(false);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('does not flag Safari for Chrome on macOS', () => {
|
|
37
|
+
setUA(
|
|
38
|
+
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36',
|
|
39
|
+
);
|
|
40
|
+
const caps = detectCapabilities();
|
|
41
|
+
expect(caps.isSafari).toBe(false);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('flags Android UA', () => {
|
|
45
|
+
setUA(
|
|
46
|
+
'Mozilla/5.0 (Linux; Android 14; Pixel 8) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Mobile Safari/537.36',
|
|
47
|
+
);
|
|
48
|
+
const caps = detectCapabilities();
|
|
49
|
+
expect(caps.isAndroid).toBe(true);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe('selectStrategy', () => {
|
|
54
|
+
it('picks shared-worker on capable desktop', () => {
|
|
55
|
+
expect(
|
|
56
|
+
selectStrategy({
|
|
57
|
+
hasSharedWorker: true,
|
|
58
|
+
hasWorker: true,
|
|
59
|
+
hasBroadcastChannel: true,
|
|
60
|
+
isAndroid: false,
|
|
61
|
+
isSafari: false,
|
|
62
|
+
}),
|
|
63
|
+
).toBe('shared-worker');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('falls back to web-worker on Android even with SharedWorker support', () => {
|
|
67
|
+
expect(
|
|
68
|
+
selectStrategy({
|
|
69
|
+
hasSharedWorker: true,
|
|
70
|
+
hasWorker: true,
|
|
71
|
+
hasBroadcastChannel: true,
|
|
72
|
+
isAndroid: true,
|
|
73
|
+
isSafari: false,
|
|
74
|
+
}),
|
|
75
|
+
).toBe('web-worker');
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('falls back to web-worker without SharedWorker', () => {
|
|
79
|
+
expect(
|
|
80
|
+
selectStrategy({
|
|
81
|
+
hasSharedWorker: false,
|
|
82
|
+
hasWorker: true,
|
|
83
|
+
hasBroadcastChannel: true,
|
|
84
|
+
isAndroid: false,
|
|
85
|
+
isSafari: true,
|
|
86
|
+
}),
|
|
87
|
+
).toBe('web-worker');
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('falls back to direct without BroadcastChannel', () => {
|
|
91
|
+
expect(
|
|
92
|
+
selectStrategy({
|
|
93
|
+
hasSharedWorker: true,
|
|
94
|
+
hasWorker: true,
|
|
95
|
+
hasBroadcastChannel: false,
|
|
96
|
+
isAndroid: false,
|
|
97
|
+
isSafari: false,
|
|
98
|
+
}),
|
|
99
|
+
).toBe('direct');
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('returns direct when Worker is unavailable', () => {
|
|
103
|
+
expect(
|
|
104
|
+
selectStrategy({
|
|
105
|
+
hasSharedWorker: false,
|
|
106
|
+
hasWorker: false,
|
|
107
|
+
hasBroadcastChannel: false,
|
|
108
|
+
isAndroid: false,
|
|
109
|
+
isSafari: false,
|
|
110
|
+
}),
|
|
111
|
+
).toBe('direct');
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
describe('getStrategyDescription', () => {
|
|
116
|
+
it('returns a description for every strategy variant', () => {
|
|
117
|
+
expect(getStrategyDescription('shared-worker')).toMatch(/SharedWorker/);
|
|
118
|
+
expect(getStrategyDescription('web-worker')).toMatch(/WebSocket/);
|
|
119
|
+
expect(getStrategyDescription('direct')).toMatch(/Direct/);
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
describe('logCapabilities', () => {
|
|
124
|
+
it('writes a single console.log with the capability snapshot', () => {
|
|
125
|
+
const spy = vi
|
|
126
|
+
.spyOn(console, 'log')
|
|
127
|
+
.mockImplementation(() => undefined);
|
|
128
|
+
logCapabilities({
|
|
129
|
+
hasSharedWorker: true,
|
|
130
|
+
hasWorker: true,
|
|
131
|
+
hasBroadcastChannel: true,
|
|
132
|
+
isAndroid: false,
|
|
133
|
+
isSafari: false,
|
|
134
|
+
});
|
|
135
|
+
expect(spy).toHaveBeenCalledTimes(1);
|
|
136
|
+
const [, payload] = spy.mock.calls[0];
|
|
137
|
+
expect(payload).toMatchObject({
|
|
138
|
+
SharedWorker: 'yes',
|
|
139
|
+
Worker: 'yes',
|
|
140
|
+
BroadcastChannel: 'yes',
|
|
141
|
+
Platform: 'Desktop',
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
});
|
package/src/lib/gateway/types.ts
CHANGED
|
@@ -1,91 +1,118 @@
|
|
|
1
1
|
// Shared types for all gateway strategies
|
|
2
2
|
|
|
3
3
|
export interface SupabaseConfig {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
url: string;
|
|
5
|
+
anonKey: string;
|
|
6
|
+
options?: Record<string, unknown>;
|
|
7
7
|
}
|
|
8
8
|
|
|
9
9
|
export interface Session {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
10
|
+
access_token: string;
|
|
11
|
+
refresh_token: string;
|
|
12
|
+
user: Record<string, unknown>;
|
|
13
|
+
expires_at?: number;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
export interface SessionResponse {
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
session: Session | null;
|
|
18
|
+
user?: Record<string, unknown>;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
export interface WebSocketStatus {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
status: 'connected' | 'disconnected' | 'connecting' | 'error';
|
|
23
|
+
readyState: number | null;
|
|
24
|
+
error?: string;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
// Unified API that all strategies must implement
|
|
28
28
|
export interface ISupabaseStrategy {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
29
|
+
// Lifecycle
|
|
30
|
+
init(
|
|
31
|
+
url: string,
|
|
32
|
+
anonKey: string,
|
|
33
|
+
options?: Record<string, unknown>,
|
|
34
|
+
): Promise<SessionResponse>;
|
|
35
|
+
|
|
36
|
+
// Event handling
|
|
37
|
+
on(event: string, callback: (payload: unknown) => void): () => void;
|
|
38
|
+
|
|
39
|
+
// Auth
|
|
40
|
+
getSession(): Promise<SessionResponse>;
|
|
41
|
+
signInWithPassword(
|
|
42
|
+
email: string,
|
|
43
|
+
password: string,
|
|
44
|
+
): Promise<SessionResponse>;
|
|
45
|
+
signOut(): Promise<void>;
|
|
46
|
+
|
|
47
|
+
// Database
|
|
48
|
+
select(table: string, opts?: SelectOptions): Promise<unknown[]>;
|
|
49
|
+
insert(
|
|
50
|
+
table: string,
|
|
51
|
+
data: Record<string, unknown> | Record<string, unknown>[],
|
|
52
|
+
): Promise<unknown[]>;
|
|
53
|
+
update(
|
|
54
|
+
table: string,
|
|
55
|
+
data: Record<string, unknown>,
|
|
56
|
+
match: Record<string, unknown>,
|
|
57
|
+
): Promise<unknown[]>;
|
|
58
|
+
upsert(
|
|
59
|
+
table: string,
|
|
60
|
+
data: Record<string, unknown> | Record<string, unknown>[],
|
|
61
|
+
): Promise<unknown[]>;
|
|
62
|
+
delete(table: string, match: Record<string, unknown>): Promise<unknown[]>;
|
|
63
|
+
rpc(fn: string, args?: Record<string, unknown>): Promise<unknown>;
|
|
64
|
+
|
|
65
|
+
// Realtime
|
|
66
|
+
subscribePostgres(
|
|
67
|
+
key: string,
|
|
68
|
+
params: Record<string, unknown>,
|
|
69
|
+
callback: (payload: unknown) => void,
|
|
70
|
+
): () => void;
|
|
71
|
+
|
|
72
|
+
// WebSocket
|
|
73
|
+
connectWebSocket(wsUrl?: string): Promise<WebSocketStatus>;
|
|
74
|
+
disconnectWebSocket(): Promise<void>;
|
|
75
|
+
sendWebSocketMessage(data: unknown): Promise<void>;
|
|
76
|
+
getWebSocketStatus(): Promise<WebSocketStatus>;
|
|
77
|
+
onWebSocketMessage(callback: (message: unknown) => void): () => void;
|
|
78
|
+
onWebSocketStatus(callback: (status: WebSocketStatus) => void): () => void;
|
|
79
|
+
|
|
80
|
+
// Cleanup
|
|
81
|
+
terminate(): void;
|
|
61
82
|
}
|
|
62
83
|
|
|
63
84
|
export interface SelectOptions {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
85
|
+
columns?: string;
|
|
86
|
+
match?: Record<string, unknown>;
|
|
87
|
+
limit?: number;
|
|
67
88
|
}
|
|
68
89
|
|
|
69
90
|
// Worker communication messages
|
|
70
91
|
export interface WorkerMessage {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
92
|
+
id: string;
|
|
93
|
+
type: string;
|
|
94
|
+
payload?: unknown;
|
|
74
95
|
}
|
|
75
96
|
|
|
76
97
|
export interface WorkerResponse {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
98
|
+
id: string;
|
|
99
|
+
ok: boolean;
|
|
100
|
+
data?: unknown;
|
|
101
|
+
error?: string;
|
|
81
102
|
}
|
|
82
103
|
|
|
83
104
|
// BroadcastChannel event types
|
|
84
105
|
export interface BroadcastEvent {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
106
|
+
type:
|
|
107
|
+
| 'auth'
|
|
108
|
+
| 'ws.message'
|
|
109
|
+
| 'ws.status'
|
|
110
|
+
| 'realtime'
|
|
111
|
+
| 'ready'
|
|
112
|
+
| 'worker-error';
|
|
113
|
+
data?: unknown;
|
|
114
|
+
key?: string;
|
|
115
|
+
payload?: unknown;
|
|
89
116
|
}
|
|
90
117
|
|
|
91
118
|
// Strategy types
|
|
@@ -93,20 +120,20 @@ export type StrategyType = 'shared-worker' | 'web-worker' | 'direct';
|
|
|
93
120
|
|
|
94
121
|
// Browser capabilities
|
|
95
122
|
export interface BrowserCapabilities {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
123
|
+
hasSharedWorker: boolean;
|
|
124
|
+
hasWorker: boolean;
|
|
125
|
+
hasBroadcastChannel: boolean;
|
|
126
|
+
isAndroid: boolean;
|
|
127
|
+
isSafari: boolean;
|
|
101
128
|
}
|
|
102
129
|
|
|
103
130
|
// Gateway configuration
|
|
104
131
|
export interface GatewayConfig {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
132
|
+
workerUrls?: {
|
|
133
|
+
sharedWorker?: string | URL;
|
|
134
|
+
dbWorker?: string | URL;
|
|
135
|
+
webSocketWorker?: string | URL;
|
|
136
|
+
};
|
|
137
|
+
forceStrategy?: StrategyType;
|
|
138
|
+
poolSize?: number;
|
|
112
139
|
}
|
|
@@ -24,9 +24,8 @@ export const mod = {
|
|
|
24
24
|
|
|
25
25
|
async loadSupabaseClient() {
|
|
26
26
|
try {
|
|
27
|
-
const supabaseModule =
|
|
28
|
-
'https://cdn.jsdelivr.net/npm/@supabase/supabase-js/+esm'
|
|
29
|
-
);
|
|
27
|
+
const supabaseModule =
|
|
28
|
+
await import('https://cdn.jsdelivr.net/npm/@supabase/supabase-js/+esm');
|
|
30
29
|
console.log('[mod-supabase] Supabase module loaded');
|
|
31
30
|
return supabaseModule.createClient;
|
|
32
31
|
} catch (err) {
|
|
@@ -45,7 +44,10 @@ export const mod = {
|
|
|
45
44
|
}
|
|
46
45
|
},
|
|
47
46
|
|
|
48
|
-
async queryTestTable() {
|
|
47
|
+
async queryTestTable(): Promise<{
|
|
48
|
+
data: unknown[] | null;
|
|
49
|
+
error: unknown;
|
|
50
|
+
}> {
|
|
49
51
|
if (!supabase)
|
|
50
52
|
throw new Error(
|
|
51
53
|
'Supabase not configured. Call `configure()` first.',
|
|
@@ -54,7 +56,10 @@ export const mod = {
|
|
|
54
56
|
return { data, error };
|
|
55
57
|
},
|
|
56
58
|
|
|
57
|
-
async insertTest(payload: Record<string, unknown>) {
|
|
59
|
+
async insertTest(payload: Record<string, unknown>): Promise<{
|
|
60
|
+
data: unknown[] | null;
|
|
61
|
+
error: unknown;
|
|
62
|
+
}> {
|
|
58
63
|
if (!supabase)
|
|
59
64
|
throw new Error(
|
|
60
65
|
'Supabase not configured. Call `configure()` first.',
|
package/src/lib/state/auth.ts
CHANGED
|
@@ -1,10 +1,44 @@
|
|
|
1
|
-
import { map } from 'nanostores';
|
|
1
|
+
import { map, computed } from 'nanostores';
|
|
2
2
|
|
|
3
3
|
export type AuthTone = 'loading' | 'auth' | 'anon' | 'error';
|
|
4
4
|
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
// Bitwise auth flags — composable visibility for UI elements.
|
|
7
|
+
//
|
|
8
|
+
// A guest has: PUBLIC | ANON = 0b0011 (3)
|
|
9
|
+
// A user has: PUBLIC | AUTH = 0b0101 (5)
|
|
10
|
+
// Staff has: PUBLIC | AUTH | STAFF = 0b1101 (13)
|
|
11
|
+
//
|
|
12
|
+
// To check: (userFlags & requiredFlag) === requiredFlag
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
export const AuthFlags = {
|
|
15
|
+
NONE: 0b0000,
|
|
16
|
+
PUBLIC: 0b0001,
|
|
17
|
+
ANON: 0b0010,
|
|
18
|
+
AUTH: 0b0100,
|
|
19
|
+
STAFF: 0b1000,
|
|
20
|
+
} as const;
|
|
21
|
+
|
|
22
|
+
export type AuthFlag = (typeof AuthFlags)[keyof typeof AuthFlags];
|
|
23
|
+
|
|
24
|
+
/** Convenience presets for common flag combinations. */
|
|
25
|
+
export const AuthPresets = {
|
|
26
|
+
GUEST: AuthFlags.PUBLIC | AuthFlags.ANON,
|
|
27
|
+
USER: AuthFlags.PUBLIC | AuthFlags.AUTH,
|
|
28
|
+
STAFF: AuthFlags.PUBLIC | AuthFlags.AUTH | AuthFlags.STAFF,
|
|
29
|
+
LOADING: AuthFlags.NONE,
|
|
30
|
+
} as const;
|
|
31
|
+
|
|
32
|
+
/** Check if a user's flags satisfy a required flag bitmask. */
|
|
33
|
+
export function hasAuthFlag(userFlags: number, required: number): boolean {
|
|
34
|
+
return (userFlags & required) === required;
|
|
35
|
+
}
|
|
36
|
+
|
|
5
37
|
export interface AuthState {
|
|
6
38
|
tone: AuthTone;
|
|
39
|
+
flags: number;
|
|
7
40
|
name: string;
|
|
41
|
+
username: string | undefined;
|
|
8
42
|
avatar: string | undefined;
|
|
9
43
|
id: string;
|
|
10
44
|
error: string | undefined;
|
|
@@ -12,13 +46,97 @@ export interface AuthState {
|
|
|
12
46
|
|
|
13
47
|
const DEFAULT_AUTH: AuthState = {
|
|
14
48
|
tone: 'loading',
|
|
49
|
+
flags: AuthPresets.LOADING,
|
|
15
50
|
name: '',
|
|
51
|
+
username: undefined,
|
|
16
52
|
avatar: undefined,
|
|
17
53
|
id: '',
|
|
18
54
|
error: undefined,
|
|
19
55
|
};
|
|
20
56
|
|
|
21
|
-
|
|
57
|
+
const SB_AUTH_TOKEN_KEY = 'sb-auth-token';
|
|
58
|
+
const PROFILE_CACHE_KEY = 'cache:profile:me';
|
|
59
|
+
const STAFF_CACHE_KEY = 'cache:staff:perms';
|
|
60
|
+
|
|
61
|
+
function seedAuthFromStorage(): AuthState {
|
|
62
|
+
if (typeof localStorage === 'undefined') return { ...DEFAULT_AUTH };
|
|
63
|
+
|
|
64
|
+
let session: {
|
|
65
|
+
user?: { id?: string; user_metadata?: Record<string, unknown> };
|
|
66
|
+
} | null = null;
|
|
67
|
+
try {
|
|
68
|
+
const raw = localStorage.getItem(SB_AUTH_TOKEN_KEY);
|
|
69
|
+
if (!raw) return { ...DEFAULT_AUTH };
|
|
70
|
+
const parsed = JSON.parse(raw);
|
|
71
|
+
const inner = parsed?.currentSession ?? parsed;
|
|
72
|
+
if (inner?.user?.id) session = inner;
|
|
73
|
+
} catch {
|
|
74
|
+
return { ...DEFAULT_AUTH };
|
|
75
|
+
}
|
|
76
|
+
if (!session?.user?.id) return { ...DEFAULT_AUTH };
|
|
77
|
+
|
|
78
|
+
const meta = (session.user.user_metadata ?? {}) as Record<string, unknown>;
|
|
79
|
+
let username: string | undefined;
|
|
80
|
+
let avatar: string | undefined =
|
|
81
|
+
typeof meta['avatar_url'] === 'string'
|
|
82
|
+
? (meta['avatar_url'] as string)
|
|
83
|
+
: undefined;
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
const raw = localStorage.getItem(PROFILE_CACHE_KEY);
|
|
87
|
+
if (raw && raw !== 'null') {
|
|
88
|
+
const env = JSON.parse(raw);
|
|
89
|
+
if (env?.user_id === session.user.id) {
|
|
90
|
+
const profile = env.profile ?? {};
|
|
91
|
+
if (typeof profile.username === 'string')
|
|
92
|
+
username = profile.username;
|
|
93
|
+
avatar =
|
|
94
|
+
profile.discord?.avatar_url ||
|
|
95
|
+
profile.github?.avatar_url ||
|
|
96
|
+
profile.twitch?.avatar_url ||
|
|
97
|
+
avatar;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
} catch {
|
|
101
|
+
/* best effort */
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
let flags: number = AuthPresets.USER;
|
|
105
|
+
try {
|
|
106
|
+
const raw = localStorage.getItem(STAFF_CACHE_KEY);
|
|
107
|
+
if (raw && raw !== 'null') {
|
|
108
|
+
const env = JSON.parse(raw);
|
|
109
|
+
if (
|
|
110
|
+
env?.user_id === session.user.id &&
|
|
111
|
+
typeof env?.bitmask === 'number' &&
|
|
112
|
+
env.bitmask > 0
|
|
113
|
+
) {
|
|
114
|
+
flags = AuthPresets.STAFF;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
} catch {
|
|
118
|
+
/* best effort */
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
tone: 'auth',
|
|
123
|
+
flags,
|
|
124
|
+
name:
|
|
125
|
+
typeof meta['full_name'] === 'string'
|
|
126
|
+
? (meta['full_name'] as string)
|
|
127
|
+
: '',
|
|
128
|
+
username,
|
|
129
|
+
avatar,
|
|
130
|
+
id: session.user.id,
|
|
131
|
+
error: undefined,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export const $auth = map<AuthState>(seedAuthFromStorage());
|
|
136
|
+
|
|
137
|
+
export const $isStaff = computed($auth, (s) =>
|
|
138
|
+
hasAuthFlag(s.flags, AuthFlags.STAFF),
|
|
139
|
+
);
|
|
22
140
|
|
|
23
141
|
export function setAuth(state: Partial<AuthState>) {
|
|
24
142
|
const current = $auth.get();
|
|
@@ -26,5 +144,5 @@ export function setAuth(state: Partial<AuthState>) {
|
|
|
26
144
|
}
|
|
27
145
|
|
|
28
146
|
export function resetAuth() {
|
|
29
|
-
$auth.set({ ...DEFAULT_AUTH, tone: 'anon' });
|
|
147
|
+
$auth.set({ ...DEFAULT_AUTH, tone: 'anon', flags: AuthPresets.GUEST });
|
|
30
148
|
}
|