@kbve/droid 0.0.6 → 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.
Files changed (190) hide show
  1. package/droid.mjs +2312 -74
  2. package/index.d.ts +32 -0
  3. package/lib/agents/api-types.d.ts +121 -0
  4. package/lib/agents/api-types.d.ts.map +1 -0
  5. package/lib/agents/auth/init.d.ts +8 -0
  6. package/lib/agents/auth/init.d.ts.map +1 -0
  7. package/lib/agents/cache.d.ts +9 -0
  8. package/lib/agents/cache.d.ts.map +1 -0
  9. package/lib/agents/client/callGuildVault.d.ts +3 -0
  10. package/lib/agents/client/callGuildVault.d.ts.map +1 -0
  11. package/lib/agents/client/callJson.d.ts +3 -0
  12. package/lib/agents/client/callJson.d.ts.map +1 -0
  13. package/lib/agents/config.d.ts +36 -0
  14. package/lib/agents/config.d.ts.map +1 -0
  15. package/lib/agents/constants.d.ts +11 -0
  16. package/lib/agents/constants.d.ts.map +1 -0
  17. package/lib/agents/createAgents.d.ts +4 -0
  18. package/lib/agents/createAgents.d.ts.map +1 -0
  19. package/lib/agents/ctx.d.ts +46 -0
  20. package/lib/agents/ctx.d.ts.map +1 -0
  21. package/lib/agents/discord/bot.d.ts +21 -0
  22. package/lib/agents/discord/bot.d.ts.map +1 -0
  23. package/lib/agents/discord/guilds.d.ts +8 -0
  24. package/lib/agents/discord/guilds.d.ts.map +1 -0
  25. package/lib/agents/formDrafts.d.ts +5 -0
  26. package/lib/agents/formDrafts.d.ts.map +1 -0
  27. package/lib/agents/generated/agents-schema.d.ts +73 -0
  28. package/lib/agents/generated/agents-schema.d.ts.map +1 -0
  29. package/lib/agents/generated/discordsh-agents-schema.d.ts +71 -0
  30. package/lib/agents/generated/discordsh-agents-schema.d.ts.map +1 -0
  31. package/lib/agents/github/backfill.d.ts +23 -0
  32. package/lib/agents/github/backfill.d.ts.map +1 -0
  33. package/lib/agents/github/events.d.ts +10 -0
  34. package/lib/agents/github/events.d.ts.map +1 -0
  35. package/lib/agents/github/pat.d.ts +13 -0
  36. package/lib/agents/github/pat.d.ts.map +1 -0
  37. package/lib/agents/github/repoConfig.d.ts +25 -0
  38. package/lib/agents/github/repoConfig.d.ts.map +1 -0
  39. package/lib/agents/github/tokens.d.ts +22 -0
  40. package/lib/agents/github/tokens.d.ts.map +1 -0
  41. package/lib/agents/github/webhook.d.ts +37 -0
  42. package/lib/agents/github/webhook.d.ts.map +1 -0
  43. package/lib/agents/index.d.ts +8 -0
  44. package/lib/agents/index.d.ts.map +1 -0
  45. package/lib/agents/state/atoms.d.ts +122 -0
  46. package/lib/agents/state/atoms.d.ts.map +1 -0
  47. package/lib/agents/types.d.ts +65 -0
  48. package/lib/agents/types.d.ts.map +1 -0
  49. package/lib/gateway/SupabaseGateway.d.ts +38 -0
  50. package/lib/gateway/SupabaseGateway.d.ts.map +1 -0
  51. package/lib/gateway/WorkerCommunication.d.ts +39 -0
  52. package/lib/gateway/WorkerCommunication.d.ts.map +1 -0
  53. package/lib/gateway/WorkerPool.d.ts +55 -0
  54. package/lib/gateway/WorkerPool.d.ts.map +1 -0
  55. package/lib/gateway/capabilities.d.ts +23 -0
  56. package/lib/gateway/capabilities.d.ts.map +1 -0
  57. package/lib/gateway/index.d.ts +9 -0
  58. package/lib/gateway/index.d.ts.map +1 -0
  59. package/lib/gateway/strategies/DirectStrategy.d.ts +52 -0
  60. package/lib/gateway/strategies/DirectStrategy.d.ts.map +1 -0
  61. package/lib/gateway/strategies/SharedWorkerStrategy.d.ts +46 -0
  62. package/lib/gateway/strategies/SharedWorkerStrategy.d.ts.map +1 -0
  63. package/lib/gateway/strategies/WebWorkerStrategy.d.ts +45 -0
  64. package/lib/gateway/strategies/WebWorkerStrategy.d.ts.map +1 -0
  65. package/lib/gateway/types.d.ts +81 -0
  66. package/lib/gateway/types.d.ts.map +1 -0
  67. package/lib/workers/canvas-ui-renderer.d.ts +25 -0
  68. package/lib/workers/canvas-ui-renderer.d.ts.map +1 -0
  69. package/lib/workers/canvas-worker.d.ts +14 -0
  70. package/lib/workers/canvas-worker.d.ts.map +1 -0
  71. package/lib/workers/canvas-worker.js +5 -302
  72. package/lib/workers/data.d.ts +40 -0
  73. package/lib/workers/data.d.ts.map +1 -0
  74. package/lib/workers/db-worker.d.ts +69 -0
  75. package/lib/workers/db-worker.d.ts.map +1 -0
  76. package/lib/workers/db-worker.js +13 -3636
  77. package/lib/workers/events.d.ts +11 -0
  78. package/lib/workers/events.d.ts.map +1 -0
  79. package/lib/workers/flexbuilder.d.ts +4 -0
  80. package/lib/workers/flexbuilder.d.ts.map +1 -0
  81. package/lib/workers/init.d.ts +11 -0
  82. package/lib/workers/init.d.ts.map +1 -0
  83. package/lib/workers/main.d.ts +105 -0
  84. package/lib/workers/main.d.ts.map +1 -0
  85. package/lib/workers/supabase-db-worker.d.ts +2 -0
  86. package/lib/workers/supabase-db-worker.d.ts.map +1 -0
  87. package/lib/workers/supabase-db-worker.js +23 -0
  88. package/lib/workers/supabase-shared-worker.d.ts +2 -0
  89. package/lib/workers/supabase-shared-worker.d.ts.map +1 -0
  90. package/lib/workers/supabase-shared-worker.js +25 -0
  91. package/lib/workers/tools.d.ts +4 -0
  92. package/lib/workers/tools.d.ts.map +1 -0
  93. package/lib/workers/validate.d.ts +15 -0
  94. package/lib/workers/validate.d.ts.map +1 -0
  95. package/lib/workers/ws-worker.d.ts +10 -0
  96. package/lib/workers/ws-worker.d.ts.map +1 -0
  97. package/lib/workers/ws-worker.js +5 -273
  98. package/main.js +5711 -0
  99. package/package.json +18 -6
  100. package/src/index.ts +41 -13
  101. package/src/lib/agents/api-types.ts +170 -0
  102. package/src/lib/agents/auth/init.ts +200 -0
  103. package/src/lib/agents/cache.ts +127 -0
  104. package/src/lib/agents/client/callGuildVault.ts +71 -0
  105. package/src/lib/agents/client/callJson.ts +64 -0
  106. package/src/lib/agents/config.ts +83 -0
  107. package/src/lib/agents/constants.ts +11 -0
  108. package/src/lib/agents/createAgents.ts +62 -0
  109. package/src/lib/agents/ctx.ts +63 -0
  110. package/src/lib/agents/discord/bot.ts +169 -0
  111. package/src/lib/agents/discord/guilds.ts +106 -0
  112. package/src/lib/agents/formDrafts.ts +55 -0
  113. package/src/lib/agents/generated/agents-schema.ts +155 -0
  114. package/src/lib/agents/generated/discordsh-agents-schema.ts +96 -0
  115. package/src/lib/agents/github/backfill.ts +126 -0
  116. package/src/lib/agents/github/events.ts +131 -0
  117. package/src/lib/agents/github/pat.ts +150 -0
  118. package/src/lib/agents/github/repoConfig.ts +240 -0
  119. package/src/lib/agents/github/tokens.ts +222 -0
  120. package/src/lib/agents/github/webhook.ts +349 -0
  121. package/src/lib/agents/index.ts +35 -0
  122. package/src/lib/agents/state/atoms.ts +122 -0
  123. package/src/lib/agents/types.ts +83 -0
  124. package/src/lib/api.ts +71 -0
  125. package/src/lib/droid.spec.ts +14 -19
  126. package/src/lib/droid.ts +48 -4
  127. package/src/lib/gateway/SupabaseGateway.ts +236 -0
  128. package/src/lib/gateway/WorkerCommunication.ts +132 -0
  129. package/src/lib/gateway/WorkerPool.ts +304 -0
  130. package/src/lib/gateway/capabilities.spec.ts +144 -0
  131. package/src/lib/gateway/capabilities.ts +80 -0
  132. package/src/lib/gateway/index.ts +15 -0
  133. package/src/lib/gateway/strategies/DirectStrategy.ts +443 -0
  134. package/src/lib/gateway/strategies/SharedWorkerStrategy.ts +278 -0
  135. package/src/lib/gateway/strategies/WebWorkerStrategy.ts +278 -0
  136. package/src/lib/gateway/types.ts +139 -0
  137. package/src/lib/mod/mod-manager.ts +13 -8
  138. package/src/lib/mod/module/bento/mod-bento.worker.ts +1 -1
  139. package/src/lib/mod/module/supabase/mod-supabase.spec.ts +21 -25
  140. package/src/lib/mod/module/supabase/mod-supabase.ts +73 -0
  141. package/src/lib/mod/module/supabase/mod-supabase.worker.ts +1 -56
  142. package/src/lib/state/auth.spec.ts +58 -0
  143. package/src/lib/state/auth.ts +148 -0
  144. package/src/lib/state/bento.spec.ts +76 -0
  145. package/src/lib/state/bento.ts +51 -0
  146. package/src/lib/state/index.ts +98 -0
  147. package/src/lib/state/overlay-manager.ts +125 -0
  148. package/src/lib/state/profile-sync.ts +214 -0
  149. package/src/lib/state/profile.ts +155 -0
  150. package/src/lib/state/router.ts +24 -0
  151. package/src/lib/state/staff.ts +128 -0
  152. package/src/lib/state/sync-bus.ts +109 -0
  153. package/src/lib/state/theme-sync.ts +75 -0
  154. package/src/lib/state/toasts.spec.ts +106 -0
  155. package/src/lib/state/toasts.ts +34 -0
  156. package/src/lib/state/ui.spec.ts +148 -0
  157. package/src/lib/state/ui.ts +45 -0
  158. package/src/lib/state/welcome-toast.spec.ts +101 -0
  159. package/src/lib/state/welcome-toast.ts +76 -0
  160. package/src/lib/sw-recovery.spec.ts +165 -0
  161. package/src/lib/sw-recovery.ts +141 -0
  162. package/src/lib/types/bento.spec.ts +116 -0
  163. package/src/lib/types/bento.ts +94 -55
  164. package/src/lib/types/event-schemas.spec.ts +120 -0
  165. package/src/lib/types/event-types.spec.ts +96 -0
  166. package/src/lib/types/event-types.ts +113 -3
  167. package/src/lib/types/modules.ts +12 -5
  168. package/src/lib/types/ui-event-types.ts +66 -0
  169. package/src/lib/workers/canvas-ui-renderer.ts +266 -0
  170. package/src/lib/workers/canvas-worker.ts +94 -9
  171. package/src/lib/workers/data.ts +64 -37
  172. package/src/lib/workers/db-worker.ts +105 -32
  173. package/src/lib/workers/events.spec.ts +208 -0
  174. package/src/lib/workers/events.ts +27 -14
  175. package/src/lib/workers/flexbuilder.ts +2 -2
  176. package/src/lib/workers/init.ts +25 -17
  177. package/src/lib/workers/main.ts +506 -327
  178. package/src/lib/workers/supabase-db-worker.ts +243 -0
  179. package/src/lib/workers/supabase-shared-worker.ts +607 -0
  180. package/src/lib/workers/validate.ts +64 -0
  181. package/src/lib/workers/ws-worker.spec.ts +41 -0
  182. package/src/lib/workers/ws-worker.ts +255 -31
  183. package/src/setup-vitest.ts +4 -2
  184. package/src/types.ts +30 -0
  185. package/src/vite-env.d.ts +1 -0
  186. package/workers/main.js +2 -1125
  187. package/comlink.js +0 -253
  188. package/index.js +0 -2972
  189. package/reference.js +0 -259
  190. package/src/types.d.ts +0 -31
@@ -1,11 +1,16 @@
1
- import { wrap, proxy } from 'comlink';
2
- import type { Remote } from 'comlink';
3
- import type { ModManager, ModHandle, ModMeta, BaseModAPI } from '../types/modules';
4
- import type { KBVEGlobal } from '../../types';
1
+ import { wrap } from 'comlink';
2
+ import type {
3
+ ModManager,
4
+ ModHandle,
5
+ ModMeta,
6
+ BaseModAPI,
7
+ } from '../types/modules';
5
8
 
6
9
  let _modManager: ModManager | null = null;
7
10
 
8
- export async function getModManager(modWorkerResolver?: (url: string) => string): Promise<ModManager> {
11
+ export async function getModManager(
12
+ modWorkerResolver?: (url: string) => string,
13
+ ): Promise<ModManager> {
9
14
  if (_modManager) return _modManager;
10
15
 
11
16
  const registry: Record<string, ModHandle> = {};
@@ -15,7 +20,7 @@ export async function getModManager(modWorkerResolver?: (url: string) => string)
15
20
  const worker = new Worker(resolvedURL, { type: 'module' });
16
21
  const instance = wrap<BaseModAPI>(worker);
17
22
 
18
- const meta: ModMeta = await instance.getMeta?.() ?? {
23
+ const meta: ModMeta = (await instance.getMeta?.()) ?? {
19
24
  name: 'unknown',
20
25
  version: '0.0.1',
21
26
  };
@@ -51,7 +56,7 @@ export async function getModManager(modWorkerResolver?: (url: string) => string)
51
56
  }
52
57
 
53
58
  function list() {
54
- return Object.values(registry).map(m => m.meta);
59
+ return Object.values(registry).map((m) => m.meta);
55
60
  }
56
61
 
57
62
  async function reload(id: string): Promise<ModHandle> {
@@ -70,4 +75,4 @@ export async function getModManager(modWorkerResolver?: (url: string) => string)
70
75
  };
71
76
 
72
77
  return _modManager;
73
- }
78
+ }
@@ -8,7 +8,7 @@ import {
8
8
  BENTO_VARIANT_CLASS_MAP,
9
9
  } from '../../../types/bento';
10
10
 
11
- let emitToMain: ((msg: any) => void) | undefined;
11
+ let emitToMain: ((msg: unknown) => void) | undefined;
12
12
 
13
13
  function createVNodeFromTile(tile: BentoTile): VirtualNode {
14
14
  const variant = tile.variant ?? 'default';
@@ -1,30 +1,26 @@
1
- import { describe, it, expect, beforeAll } from 'vitest';
2
- import { droid } from '../../../droid';
3
- import type { Remote } from 'comlink';
4
- import type { ModHandle, SupabaseModAPI } from '../../../types/modules';
1
+ import { describe, it, expect } from 'vitest';
2
+ import { mod } from './mod-supabase';
5
3
 
6
- console.log('[spec] Starting mod-supabase test file...');
7
-
8
-
9
- let mod: Remote<SupabaseModAPI>;
10
- let meta: { name: string; version: string };
11
4
  describe('mod-supabase', () => {
12
- it('should have meta info', async () => {
13
- await droid();
14
- if (!window.kbve?.mod) {
15
- throw new Error('Mod manager not available');
16
- }
5
+ it('should have meta info', () => {
6
+ const meta = mod.getMeta();
17
7
 
18
- console.log('[test] Loading Supabase mod...');
19
- const modURL = new URL('./mod-supabase.worker.ts', import.meta.url);
20
- console.log('Resolved worker URL:', modURL.href);
8
+ expect(meta.name).toBe('supabase');
9
+ expect(meta.version).toBe('1.0.0');
10
+ expect(meta.description).toBeDefined();
11
+ expect(meta.author).toBe('kbve');
12
+ });
21
13
 
22
- const handle = await window.kbve.mod.load(modURL.href);
23
- const mod: Remote<SupabaseModAPI> = handle.instance;
24
- const meta = handle.meta;
14
+ it('should accept init context', () => {
15
+ mod.init({ test: true });
16
+ // init stores context internally — no throw = success
17
+ });
25
18
 
26
- console.log('[test] Supabase mod loaded:', meta);
27
- expect(meta.name).toBe('supabase');
28
- expect(meta.version).toBeDefined();
29
- }, 30_000);
30
- });
19
+ it('should reject queries before configure', async () => {
20
+ await expect(mod.queryTestTable()).rejects.toThrow('Supabase not configured');
21
+ });
22
+
23
+ it('should reject inserts before configure', async () => {
24
+ await expect(mod.insertTest({ foo: 'bar' })).rejects.toThrow('Supabase not configured');
25
+ });
26
+ });
@@ -0,0 +1,73 @@
1
+ import type { SupabaseClient } from '@supabase/supabase-js';
2
+ import type { ModInitContext } from '../../../types/modules';
3
+
4
+ let supabase: SupabaseClient | null = null;
5
+ let initialized = false;
6
+
7
+ const state = {
8
+ ctx: null as ModInitContext | null,
9
+ url: '',
10
+ key: '',
11
+ };
12
+
13
+ export const mod = {
14
+ getMeta: () => ({
15
+ name: 'supabase',
16
+ version: '1.0.0',
17
+ description: 'Supabase mod for async DB access',
18
+ author: 'kbve',
19
+ }),
20
+
21
+ init(ctx: ModInitContext) {
22
+ state.ctx = ctx;
23
+ },
24
+
25
+ async loadSupabaseClient() {
26
+ try {
27
+ const supabaseModule =
28
+ await import('https://cdn.jsdelivr.net/npm/@supabase/supabase-js/+esm');
29
+ console.log('[mod-supabase] Supabase module loaded');
30
+ return supabaseModule.createClient;
31
+ } catch (err) {
32
+ console.error('[mod-supabase] Failed to load Supabase:', err);
33
+ throw err;
34
+ }
35
+ },
36
+
37
+ async configure(url: string, key: string) {
38
+ if (!initialized) {
39
+ const createClient = await this.loadSupabaseClient();
40
+ supabase = createClient(url, key);
41
+ state.url = url;
42
+ state.key = key;
43
+ initialized = true;
44
+ }
45
+ },
46
+
47
+ async queryTestTable(): Promise<{
48
+ data: unknown[] | null;
49
+ error: unknown;
50
+ }> {
51
+ if (!supabase)
52
+ throw new Error(
53
+ 'Supabase not configured. Call `configure()` first.',
54
+ );
55
+ const { data, error } = await supabase.from('test').select('*');
56
+ return { data, error };
57
+ },
58
+
59
+ async insertTest(payload: Record<string, unknown>): Promise<{
60
+ data: unknown[] | null;
61
+ error: unknown;
62
+ }> {
63
+ if (!supabase)
64
+ throw new Error(
65
+ 'Supabase not configured. Call `configure()` first.',
66
+ );
67
+ const { data, error } = await supabase
68
+ .from('test')
69
+ .insert(payload)
70
+ .select();
71
+ return { data, error };
72
+ },
73
+ };
@@ -1,59 +1,4 @@
1
1
  import { expose } from 'comlink';
2
- import type { SupabaseClient } from '@supabase/supabase-js';
3
-
4
- let supabase: SupabaseClient | null = null;
5
- let initialized = false;
6
-
7
- const state = {
8
- ctx: null as any,
9
- url: '',
10
- key: '',
11
- };
12
-
13
- const mod = {
14
- getMeta: () => ({
15
- name: 'supabase',
16
- version: '1.0.0',
17
- description: 'Supabase mod for async DB access',
18
- author: 'kbve',
19
- }),
20
-
21
- init(ctx: any) {
22
- state.ctx = ctx;
23
- },
24
-
25
- async loadSupabaseClient() {
26
- try {
27
- const supabaseModule = await import('https://cdn.jsdelivr.net/npm/@supabase/supabase-js/+esm');
28
- console.log('[mod-supabase] Supabase module loaded');
29
- return supabaseModule.createClient;
30
- } catch (err) {
31
- console.error('[mod-supabase] Failed to load Supabase:', err);
32
- throw err;
33
- }
34
- },
35
-
36
- async configure(url: string, key: string) {
37
- if (!initialized) {
38
- const createClient = await this.loadSupabaseClient();
39
- supabase = createClient(url, key);
40
- state.url = url;
41
- state.key = key;
42
- initialized = true;
43
- }
44
- },
45
-
46
- async queryTestTable() {
47
- if (!supabase) throw new Error('Supabase not configured. Call `configure()` first.');
48
- const { data, error } = await supabase.from('test').select('*');
49
- return { data, error };
50
- },
51
-
52
- async insertTest(payload: Record<string, any>) {
53
- if (!supabase) throw new Error('Supabase not configured. Call `configure()` first.');
54
- const { data, error } = await supabase.from('test').insert(payload).select();
55
- return { data, error };
56
- }
57
- };
2
+ import { mod } from './mod-supabase';
58
3
 
59
4
  expose(mod);
@@ -0,0 +1,58 @@
1
+ import { describe, it, expect, beforeEach } from 'vitest';
2
+ import { $auth, setAuth, resetAuth } from './auth';
3
+
4
+ beforeEach(() => {
5
+ resetAuth();
6
+ // resetAuth sets tone to 'anon', restore to default 'loading' for clean tests
7
+ $auth.set({
8
+ tone: 'loading',
9
+ name: '',
10
+ avatar: undefined,
11
+ id: '',
12
+ error: undefined,
13
+ });
14
+ });
15
+
16
+ describe('Auth state', () => {
17
+ it('starts with loading tone', () => {
18
+ expect($auth.get().tone).toBe('loading');
19
+ });
20
+
21
+ it('setAuth merges partial state', () => {
22
+ setAuth({ tone: 'auth', name: 'Alice', id: 'u1' });
23
+
24
+ const state = $auth.get();
25
+ expect(state.tone).toBe('auth');
26
+ expect(state.name).toBe('Alice');
27
+ expect(state.id).toBe('u1');
28
+ expect(state.avatar).toBeUndefined();
29
+ });
30
+
31
+ it('setAuth preserves existing fields', () => {
32
+ setAuth({ tone: 'auth', name: 'Alice', id: 'u1' });
33
+ setAuth({ avatar: 'https://example.com/avatar.png' });
34
+
35
+ const state = $auth.get();
36
+ expect(state.name).toBe('Alice');
37
+ expect(state.avatar).toBe('https://example.com/avatar.png');
38
+ });
39
+
40
+ it('setAuth can set error state', () => {
41
+ setAuth({ tone: 'error', error: 'Token expired' });
42
+
43
+ expect($auth.get().tone).toBe('error');
44
+ expect($auth.get().error).toBe('Token expired');
45
+ });
46
+
47
+ it('resetAuth clears to anon defaults', () => {
48
+ setAuth({ tone: 'auth', name: 'Alice', id: 'u1', avatar: 'img.png' });
49
+ resetAuth();
50
+
51
+ const state = $auth.get();
52
+ expect(state.tone).toBe('anon');
53
+ expect(state.name).toBe('');
54
+ expect(state.id).toBe('');
55
+ expect(state.avatar).toBeUndefined();
56
+ expect(state.error).toBeUndefined();
57
+ });
58
+ });
@@ -0,0 +1,148 @@
1
+ import { map, computed } from 'nanostores';
2
+
3
+ export type AuthTone = 'loading' | 'auth' | 'anon' | 'error';
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
+
37
+ export interface AuthState {
38
+ tone: AuthTone;
39
+ flags: number;
40
+ name: string;
41
+ username: string | undefined;
42
+ avatar: string | undefined;
43
+ id: string;
44
+ error: string | undefined;
45
+ }
46
+
47
+ const DEFAULT_AUTH: AuthState = {
48
+ tone: 'loading',
49
+ flags: AuthPresets.LOADING,
50
+ name: '',
51
+ username: undefined,
52
+ avatar: undefined,
53
+ id: '',
54
+ error: undefined,
55
+ };
56
+
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
+ );
140
+
141
+ export function setAuth(state: Partial<AuthState>) {
142
+ const current = $auth.get();
143
+ $auth.set({ ...current, ...state });
144
+ }
145
+
146
+ export function resetAuth() {
147
+ $auth.set({ ...DEFAULT_AUTH, tone: 'anon', flags: AuthPresets.GUEST });
148
+ }
@@ -0,0 +1,76 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import {
3
+ $bentoShipment,
4
+ $bentoLedger,
5
+ shipBentoWord,
6
+ advanceBentoShipment,
7
+ resetBentoFlow,
8
+ } from './bento';
9
+
10
+ beforeEach(() => {
11
+ resetBentoFlow();
12
+ });
13
+
14
+ describe('Bento flow state', () => {
15
+ it('shipBentoWord creates a shipped shipment', () => {
16
+ const shipment = shipBentoWord('games');
17
+ expect(shipment.word).toBe('games');
18
+ expect(shipment.stage).toBe('shipped');
19
+ expect($bentoShipment.get()).toEqual(shipment);
20
+ expect($bentoLedger.get()).toEqual([shipment]);
21
+ });
22
+
23
+ it('shipBentoWord assigns unique incrementing ids', () => {
24
+ const a = shipBentoWord('games');
25
+ const b = shipBentoWord('apps');
26
+ expect(b.id).toBeGreaterThan(a.id);
27
+ });
28
+
29
+ it('advanceBentoShipment updates stage and preserves word', () => {
30
+ const shipment = shipBentoWord('games');
31
+ const arrived = advanceBentoShipment(shipment.id, 'arrived');
32
+ expect(arrived?.word).toBe('games');
33
+ expect(arrived?.stage).toBe('arrived');
34
+ expect($bentoShipment.get()?.stage).toBe('arrived');
35
+ expect($bentoLedger.get()).toHaveLength(1);
36
+ });
37
+
38
+ it('advanceBentoShipment returns null for unknown id', () => {
39
+ expect(advanceBentoShipment(999, 'arrived')).toBeNull();
40
+ });
41
+
42
+ it('advanceBentoShipment returns null when stage unchanged', () => {
43
+ const shipment = shipBentoWord('games');
44
+ expect(advanceBentoShipment(shipment.id, 'shipped')).toBeNull();
45
+ });
46
+
47
+ it('listeners see each lifecycle transition', () => {
48
+ const handler = vi.fn();
49
+ const unsub = $bentoShipment.listen(handler);
50
+
51
+ const shipment = shipBentoWord('games');
52
+ advanceBentoShipment(shipment.id, 'arrived');
53
+ advanceBentoShipment(shipment.id, 'delivered');
54
+
55
+ expect(handler).toHaveBeenCalledTimes(3);
56
+ expect(handler.mock.calls.map(([s]) => s.stage)).toEqual([
57
+ 'shipped',
58
+ 'arrived',
59
+ 'delivered',
60
+ ]);
61
+ unsub();
62
+ });
63
+
64
+ it('ledger caps at 24 shipments', () => {
65
+ for (let i = 0; i < 30; i++) shipBentoWord(`word-${i}`);
66
+ expect($bentoLedger.get()).toHaveLength(24);
67
+ expect($bentoLedger.get()[23].word).toBe('word-29');
68
+ });
69
+
70
+ it('resetBentoFlow clears state', () => {
71
+ shipBentoWord('games');
72
+ resetBentoFlow();
73
+ expect($bentoShipment.get()).toBeNull();
74
+ expect($bentoLedger.get()).toEqual([]);
75
+ });
76
+ });
@@ -0,0 +1,51 @@
1
+ import { atom } from 'nanostores';
2
+
3
+ export type BentoStage = 'shipped' | 'arrived' | 'delivered' | 'dropped';
4
+
5
+ export interface BentoShipment {
6
+ id: number;
7
+ word: string;
8
+ stage: BentoStage;
9
+ at: number;
10
+ }
11
+
12
+ const LEDGER_MAX = 24;
13
+
14
+ let nextShipmentId = 1;
15
+
16
+ export const $bentoShipment = atom<BentoShipment | null>(null);
17
+ export const $bentoLedger = atom<readonly BentoShipment[]>([]);
18
+
19
+ function record(shipment: BentoShipment) {
20
+ const rest = $bentoLedger.get().filter((s) => s.id !== shipment.id);
21
+ $bentoLedger.set([...rest, shipment].slice(-LEDGER_MAX));
22
+ $bentoShipment.set(shipment);
23
+ }
24
+
25
+ export function shipBentoWord(word: string): BentoShipment {
26
+ const shipment: BentoShipment = {
27
+ id: nextShipmentId++,
28
+ word,
29
+ stage: 'shipped',
30
+ at: Date.now(),
31
+ };
32
+ record(shipment);
33
+ return shipment;
34
+ }
35
+
36
+ export function advanceBentoShipment(
37
+ id: number,
38
+ stage: BentoStage,
39
+ ): BentoShipment | null {
40
+ const current = $bentoLedger.get().find((s) => s.id === id);
41
+ if (!current || current.stage === stage) return null;
42
+ const updated: BentoShipment = { ...current, stage, at: Date.now() };
43
+ record(updated);
44
+ return updated;
45
+ }
46
+
47
+ export function resetBentoFlow() {
48
+ nextShipmentId = 1;
49
+ $bentoShipment.set(null);
50
+ $bentoLedger.set([]);
51
+ }
@@ -0,0 +1,98 @@
1
+ export {
2
+ $auth,
3
+ $isStaff,
4
+ setAuth,
5
+ resetAuth,
6
+ AuthFlags,
7
+ AuthPresets,
8
+ hasAuthFlag,
9
+ type AuthTone,
10
+ type AuthFlag,
11
+ type AuthState,
12
+ } from './auth';
13
+
14
+ export { $currentPath, bootRouter } from './router';
15
+
16
+ export {
17
+ $activeTooltip,
18
+ $drawerOpen,
19
+ $modalId,
20
+ openTooltip,
21
+ closeTooltip,
22
+ openDrawer,
23
+ closeDrawer,
24
+ openModal,
25
+ closeModal,
26
+ } from './ui';
27
+
28
+ export {
29
+ $bentoShipment,
30
+ $bentoLedger,
31
+ shipBentoWord,
32
+ advanceBentoShipment,
33
+ resetBentoFlow,
34
+ type BentoStage,
35
+ type BentoShipment,
36
+ } from './bento';
37
+
38
+ export { $toasts, addToast, removeToast } from './toasts';
39
+
40
+ export { showWelcomeToast } from './welcome-toast';
41
+
42
+ export {
43
+ syncThemeToDexie,
44
+ broadcastThemeChange,
45
+ observeThemeChanges,
46
+ } from './theme-sync';
47
+
48
+ export { OverlayManager } from './overlay-manager';
49
+ export type { RenderPath } from './overlay-manager';
50
+
51
+ export {
52
+ $profileCache,
53
+ clearProfileCache,
54
+ fetchAndCacheProfile,
55
+ fetchProfileFromApi,
56
+ getProfileFromCache,
57
+ PROFILE_CACHE_TTL_MS,
58
+ PROFILE_DEFAULT_API_BASE,
59
+ readProfileForFastPaint,
60
+ readSupabaseSessionFromStorage,
61
+ setProfileCache,
62
+ type CachedSupabaseSession,
63
+ type DroidProfile,
64
+ type FetchProfileOptions,
65
+ type ProfileCacheEnvelope,
66
+ type ProfileFastPaintResult,
67
+ } from './profile';
68
+
69
+ export {
70
+ $staffPermissions,
71
+ applyStaffFlagFromCache,
72
+ bootStaffPermissions,
73
+ clearStaffPermsCache,
74
+ fetchAndCacheStaffPermissions,
75
+ fetchStaffPermissionsFromApi,
76
+ getStaffPermsFromCache,
77
+ setStaffPermsCache,
78
+ STAFF_CACHE_TTL_MS,
79
+ type FetchStaffPermsOptions,
80
+ type StaffPermsEnvelope,
81
+ } from './staff';
82
+
83
+ export {
84
+ broadcastProfileClear,
85
+ broadcastProfileRefresh,
86
+ broadcastStaffClear,
87
+ broadcastStaffRefresh,
88
+ installSyncBusListener,
89
+ type SyncBusMessage,
90
+ } from './sync-bus';
91
+
92
+ export {
93
+ disposeProfileSync,
94
+ installProfileSync,
95
+ refreshProfileSyncNow,
96
+ type AuthEventLike,
97
+ type ProfileSyncOptions,
98
+ } from './profile-sync';