@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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kbve/droid",
3
- "version": "0.0.6",
3
+ "version": "0.1.2",
4
4
  "description": "A manager for web, shared and service workers, aimming to make it as modular and upgradable.",
5
5
  "keywords": [
6
6
  "workers",
@@ -27,11 +27,15 @@
27
27
  },
28
28
  "./lib/workers/canvas-worker": "./lib/workers/canvas-worker.js",
29
29
  "./lib/workers/db-worker": "./lib/workers/db-worker.js",
30
- "./lib/workers/ws-worker": "./lib/workers/ws-worker.js"
30
+ "./lib/workers/ws-worker": "./lib/workers/ws-worker.js",
31
+ "./lib/workers/supabase-shared-worker": "./lib/workers/supabase-shared-worker.js",
32
+ "./lib/workers/supabase-db-worker": "./lib/workers/supabase-db-worker.js"
31
33
  },
32
34
  "files": [
33
35
  "workers/",
34
36
  "lib/workers/",
37
+ "lib/gateway/",
38
+ "lib/agents/",
35
39
  "assets/",
36
40
  "src/",
37
41
  "comlink*.js",
@@ -45,13 +49,21 @@
45
49
  "*.json"
46
50
  ],
47
51
  "dependencies": {
48
- "tslib": "^2.3.0",
49
52
  "comlink": "^4.3.1",
50
- "@nanostores/persistent": "^0.7.1",
51
- "dexie": "^3.2.4",
52
- "zod": "^3.22.4",
53
+ "nanostores": "^1.1.0",
54
+ "@nanostores/persistent": "^1.3.3",
55
+ "dexie": "^4.0.9",
56
+ "zod": "^4.0.0",
53
57
  "flatbuffers": "^25.2.10"
54
58
  },
59
+ "peerDependencies": {
60
+ "@supabase/supabase-js": "^2.95.3"
61
+ },
62
+ "peerDependenciesMeta": {
63
+ "@supabase/supabase-js": {
64
+ "optional": true
65
+ }
66
+ },
55
67
  "publishConfig": {
56
68
  "access": "public"
57
69
  }
package/src/index.ts CHANGED
@@ -1,21 +1,49 @@
1
+ export type { KBVEGlobal } from './types';
2
+
1
3
  export * from './lib/droid';
2
4
  export * from './lib/types/bento';
5
+ export * from './lib/types/event-types';
6
+ export * from './lib/types/panel-types';
7
+ export * from './lib/types/ui-event-types';
8
+ export * from './lib/sw-recovery';
9
+ export * from './lib/workers/events';
3
10
  export * from './lib/mod/mod-urls';
11
+ export * from './lib/gateway';
12
+ export * from './lib/state';
13
+ export * from './lib/agents';
14
+ export * from './lib/api';
15
+
16
+ export type { VirtualNode } from './lib/types/modules';
17
+
18
+ // Worker-side validation
19
+ export { validateUIMessage } from './lib/workers/validate';
20
+ export type {
21
+ ValidatedWorkerMessage,
22
+ UIMessageType,
23
+ } from './lib/workers/validate';
24
+
25
+ // Canvas UI renderer (for direct worker usage)
26
+ export { CanvasUIRenderer } from './lib/workers/canvas-ui-renderer';
27
+
28
+ // Vite ?worker&url imports — bundles each worker as JS and returns the URL string
29
+ import canvasWorkerUrl from './lib/workers/canvas-worker?worker&url';
30
+ import dbWorkerUrl from './lib/workers/db-worker?worker&url';
31
+ import wsWorkerUrl from './lib/workers/ws-worker?worker&url';
32
+ import supabaseSharedWorkerUrl from './lib/workers/supabase-shared-worker?worker&url';
33
+ import supabaseDbWorkerUrl from './lib/workers/supabase-db-worker?worker&url';
4
34
 
5
35
  export const workerURLs = {
6
- canvasWorker: new URL(
7
- './lib/workers/canvas-worker.js',
8
- import.meta.url,
9
- ),
10
- dbWorker: new URL('./lib/workers/db-worker.js', import.meta.url),
11
- wsWorker: new URL('./lib/workers/ws-worker.js', import.meta.url),
36
+ canvasWorker: canvasWorkerUrl,
37
+ dbWorker: dbWorkerUrl,
38
+ wsWorker: wsWorkerUrl,
39
+ supabaseSharedWorker: supabaseSharedWorkerUrl,
40
+ supabaseDbWorker: supabaseDbWorkerUrl,
12
41
  };
13
42
 
14
43
  export const workerStrings = {
15
- canvasWorker: new URL(
16
- './lib/workers/canvas-worker.js',
17
- import.meta.url,
18
- ).toString(),
19
- dbWorker: new URL('./lib/workers/db-worker.js', import.meta.url).toString(),
20
- wsWorker: new URL('./lib/workers/ws-worker.js', import.meta.url).toString(),
21
- };
44
+ canvasWorker: canvasWorkerUrl,
45
+ dbWorker: dbWorkerUrl,
46
+ wsWorker: wsWorkerUrl,
47
+ supabaseSharedWorker: supabaseSharedWorkerUrl,
48
+ supabaseDbWorker: supabaseDbWorkerUrl,
49
+ };
@@ -0,0 +1,170 @@
1
+ import type { AgentsStore } from './state/atoms';
2
+ import type { TokenServiceValue } from './generated/agents-schema';
3
+ import type {
4
+ AgentTokenRow,
5
+ BackfillResult,
6
+ BotConfigFormDraft,
7
+ DiscordForumChannel,
8
+ DiscordshConfig,
9
+ GithubRepoHook,
10
+ PatValidation,
11
+ Result,
12
+ WebhookInstallResult,
13
+ } from './types';
14
+
15
+ export interface AgentsMethods {
16
+ // auth
17
+ initAuth(force?: boolean): Promise<void>;
18
+ resyncOwnedGuilds(): Promise<boolean>;
19
+ signInWithDiscord(): Promise<void>;
20
+
21
+ // guilds
22
+ loadOwnedGuilds(force?: boolean): Promise<void>;
23
+ selectGuild(guildId: string): void;
24
+ refreshSelectedGuild(): Promise<void>;
25
+
26
+ // tokens
27
+ loadTokens(guildId: string, force?: boolean): Promise<void>;
28
+ addToken(input: {
29
+ tokenName: string;
30
+ service: TokenServiceValue;
31
+ tokenValue: string;
32
+ description?: string | null;
33
+ }): Promise<Result<{ tokenId: string }>>;
34
+ deleteToken(tokenId: string): Promise<Result>;
35
+ peekToken(
36
+ service: TokenServiceValue,
37
+ ): Promise<Result<{ value: string | null }>>;
38
+ toggleToken(tokenId: string, isActive: boolean): Promise<Result>;
39
+ hasService(service: TokenServiceValue): AgentTokenRow | null;
40
+
41
+ // repo allowlist
42
+ getRepoAllowlist(): Promise<
43
+ Result<{ repos: string[]; raw: string | null }>
44
+ >;
45
+ setRepoAllowlist(repos: string[]): Promise<Result>;
46
+ hydrateRepoAllowlistDraft(guildId: string, repos: string[]): void;
47
+ patchRepoAllowlistDraft(guildId: string, repos: string[]): void;
48
+ clearRepoAllowlistDraft(guildId: string): void;
49
+ ensureRepoAllowlistLoaded(
50
+ guildId: string,
51
+ force?: boolean,
52
+ ): Promise<Result>;
53
+ saveRepoAllowlistDraft(guildId: string): Promise<Result>;
54
+
55
+ // bot config
56
+ getBotConfig(): Promise<Result<{ config: DiscordshConfig }>>;
57
+ setBotConfig(config: DiscordshConfig): Promise<Result>;
58
+ hydrateBotConfigDraft(guildId: string, cfg: DiscordshConfig): void;
59
+ patchBotConfigDraft(
60
+ guildId: string,
61
+ patch: Partial<BotConfigFormDraft>,
62
+ ): void;
63
+ clearBotConfigDraft(guildId: string): void;
64
+ ensureBotConfigLoaded(guildId: string, force?: boolean): Promise<Result>;
65
+ saveBotConfigDraft(guildId: string): Promise<Result>;
66
+
67
+ // webhook secret drafts
68
+ setWebhookDraft(guildId: string, secret: string | null): void;
69
+ clearWebhookError(guildId: string): void;
70
+ saveWebhookDraft(
71
+ guildId: string,
72
+ tokenName: string,
73
+ description: string,
74
+ ): Promise<Result<{ tokenId: string }>>;
75
+
76
+ // PAT
77
+ setPatDraft(guildId: string, pat: string): void;
78
+ clearPatState(guildId: string): void;
79
+ validatePatForGuild(guildId: string): Promise<Result>;
80
+ savePatForGuild(
81
+ guildId: string,
82
+ tokenName: string,
83
+ ): Promise<Result<{ tokenId: string }>>;
84
+ validateGithubPat(pat: string): Promise<Result<PatValidation>>;
85
+
86
+ // backfill
87
+ patchBackfillDraft(
88
+ guildId: string,
89
+ partial: Partial<{ owner: string; repo: string }>,
90
+ ): void;
91
+ clearBackfillResult(guildId: string): void;
92
+ runBackfillForGuild(guildId: string): Promise<void>;
93
+ runBackfill(input: {
94
+ owner: string;
95
+ repo: string;
96
+ state?: 'open' | 'closed' | 'all';
97
+ maxPages?: number;
98
+ perPage?: number;
99
+ }): Promise<BackfillResult | { ok: false; error: string }>;
100
+
101
+ // webhook install / ping / verify
102
+ setWebhookInstallSelected(guildId: string, repo: string): void;
103
+ clearWebhookInstallResult(guildId: string): void;
104
+ installWebhookForGuild(guildId: string): Promise<void>;
105
+ pingWebhookForGuild(guildId: string): Promise<void>;
106
+ verifyWebhookInstall(guildId: string, repoFull: string): Promise<void>;
107
+ installRepoWebhook(
108
+ guildId: string,
109
+ owner: string,
110
+ repo: string,
111
+ ): Promise<WebhookInstallResult | { ok: false; error: string }>;
112
+ pingRepoWebhook(
113
+ guildId: string,
114
+ owner: string,
115
+ repo: string,
116
+ ): Promise<
117
+ | { ok: true; hookId: number; url: string }
118
+ | { ok: false; error: string; retryAfterMs?: number }
119
+ >;
120
+ listRepoWebhooks(
121
+ guildId: string,
122
+ owner: string,
123
+ repo: string,
124
+ ): Promise<Result<{ expectedUrl: string; hooks: GithubRepoHook[] }>>;
125
+ rotateWebhookForGuild(
126
+ guildId: string,
127
+ owner: string,
128
+ repo: string,
129
+ ): Promise<void>;
130
+ deleteWebhookForGuild(
131
+ guildId: string,
132
+ owner: string,
133
+ repo: string,
134
+ ): Promise<Result>;
135
+ loadWebhookDeliveries(
136
+ guildId: string,
137
+ owner: string,
138
+ repo: string,
139
+ limit?: number,
140
+ ): Promise<void>;
141
+ webhookUrlFor(guildId: string): string;
142
+
143
+ // discord bot / channels
144
+ ensureGuildChannelsLoaded(guildId: string, force?: boolean): Promise<void>;
145
+ ensureBotMembershipLoaded(guildId: string, force?: boolean): Promise<void>;
146
+ invalidateBotMembership(guildId: string): void;
147
+ isBotMember(
148
+ guildId: string,
149
+ ): Promise<
150
+ | { ok: true; isMember: boolean; joinedAt: string | null }
151
+ | { ok: false; error: string }
152
+ >;
153
+ listForumChannels(
154
+ guildId: string,
155
+ ): Promise<Result<{ channels: DiscordForumChannel[] }>>;
156
+ botInstallUrl(guildId?: string): string | null;
157
+ fetchBotInstallUrl(guildId: string): Promise<string | null>;
158
+
159
+ // events
160
+ loadEventStats(guildId: string): Promise<void>;
161
+ loadFailedEvents(guildId: string, limit?: number): Promise<void>;
162
+ loadPendingEvents(guildId: string, limit?: number): Promise<void>;
163
+ requeueEvent(
164
+ guildId: string,
165
+ eventId: number,
166
+ reason?: string,
167
+ ): Promise<Result>;
168
+ }
169
+
170
+ export type AgentsApi = AgentsStore & AgentsMethods;
@@ -0,0 +1,200 @@
1
+ import {
2
+ BOOTSTRAP_COOLDOWN_MS,
3
+ FORCED_REFRESH_COOLDOWN_MS,
4
+ INIT_DEDUP_MS,
5
+ } from '../constants';
6
+ import {
7
+ extractJwtOwnedGuildIds,
8
+ loadCachedGuilds,
9
+ parseJwtPayload,
10
+ } from '../cache';
11
+ import type { AgentsCtx } from '../ctx';
12
+ import type { AgentsApi } from '../api-types';
13
+
14
+ export function makeAuth(ctx: AgentsCtx, api: AgentsApi) {
15
+ const { store, config, endpoints, runtime } = ctx;
16
+
17
+ async function bootstrapDiscord(providerToken: string): Promise<boolean> {
18
+ const accessToken = store.$accessToken.get();
19
+ if (!accessToken) return false;
20
+ const now = Date.now();
21
+ if (
22
+ runtime.lastBootstrapOk &&
23
+ now - runtime.lastBootstrapAt < BOOTSTRAP_COOLDOWN_MS
24
+ ) {
25
+ return true;
26
+ }
27
+ if (runtime.bootstrapInflight) return runtime.bootstrapInflight;
28
+ runtime.bootstrapInflight = (async () => {
29
+ try {
30
+ const resp = await fetch(endpoints.discordBootstrap, {
31
+ method: 'POST',
32
+ headers: {
33
+ 'Content-Type': 'application/json',
34
+ Authorization: `Bearer ${accessToken}`,
35
+ },
36
+ body: JSON.stringify({ provider_token: providerToken }),
37
+ });
38
+ if (!resp.ok) {
39
+ if (resp.status === 429) {
40
+ runtime.lastBootstrapAt = Date.now();
41
+ runtime.lastBootstrapOk = true;
42
+ }
43
+ return false;
44
+ }
45
+ runtime.lastBootstrapAt = Date.now();
46
+ runtime.lastBootstrapOk = true;
47
+ return true;
48
+ } catch {
49
+ return false;
50
+ } finally {
51
+ runtime.bootstrapInflight = null;
52
+ }
53
+ })();
54
+ return runtime.bootstrapInflight;
55
+ }
56
+
57
+ async function forceTokenRefresh(): Promise<boolean> {
58
+ const now = Date.now();
59
+ if (now - runtime.lastForcedRefreshAt < FORCED_REFRESH_COOLDOWN_MS) {
60
+ return false;
61
+ }
62
+ if (runtime.refreshInflight) return runtime.refreshInflight;
63
+ runtime.refreshInflight = (async () => {
64
+ try {
65
+ const accessToken = await config.refreshSession();
66
+ if (!accessToken) {
67
+ console.warn('[agents] refreshSession returned no session');
68
+ return false;
69
+ }
70
+ store.$accessToken.set(accessToken);
71
+ runtime.lastForcedRefreshAt = Date.now();
72
+ return true;
73
+ } catch (e) {
74
+ console.warn('[agents] refreshSession threw:', e);
75
+ return false;
76
+ } finally {
77
+ runtime.refreshInflight = null;
78
+ }
79
+ })();
80
+ return runtime.refreshInflight;
81
+ }
82
+
83
+ async function resyncOwnedGuilds(): Promise<boolean> {
84
+ const providerToken = store.$providerToken.get();
85
+ if (!providerToken) return false;
86
+ const ok = await bootstrapDiscord(providerToken);
87
+ if (!ok) return false;
88
+ return await forceTokenRefresh();
89
+ }
90
+
91
+ async function runInitAuth(): Promise<void> {
92
+ try {
93
+ const session = await config.loadSession();
94
+ const accessToken = session.accessToken;
95
+
96
+ if (!accessToken) {
97
+ store.$authState.set('unauthenticated');
98
+ return;
99
+ }
100
+
101
+ store.$accessToken.set(accessToken);
102
+
103
+ const jwtPayload = parseJwtPayload(accessToken);
104
+ const userId = (jwtPayload?.['sub'] as string | undefined) ?? null;
105
+ if (userId) store.$userId.set(userId);
106
+
107
+ let providerToken: string | null = session.providerToken;
108
+ if (!providerToken) {
109
+ providerToken = config.getDiscordProviderToken();
110
+ }
111
+
112
+ if (providerToken) {
113
+ store.$providerToken.set(providerToken);
114
+ store.$authState.set('authenticated');
115
+ const cachedOwned = userId ? loadCachedGuilds(userId) : null;
116
+ if (cachedOwned && cachedOwned.length > 0) {
117
+ store.$guilds.set(cachedOwned);
118
+ if (cachedOwned.length === 1) {
119
+ api.selectGuild(cachedOwned[0].id);
120
+ }
121
+ }
122
+ return;
123
+ }
124
+
125
+ const cached = userId ? loadCachedGuilds(userId) : null;
126
+ const jwtIds = extractJwtOwnedGuildIds(accessToken);
127
+ const jwtSet = new Set(jwtIds);
128
+
129
+ if (cached && cached.length > 0) {
130
+ const filtered =
131
+ jwtIds.length > 0
132
+ ? cached.filter((g) => jwtSet.has(g.id))
133
+ : cached;
134
+ store.$guilds.set(filtered);
135
+ store.$guildsStale.set(true);
136
+ store.$authState.set('authenticated');
137
+ if (filtered.length === 1) {
138
+ api.selectGuild(filtered[0].id);
139
+ }
140
+ return;
141
+ }
142
+
143
+ if (jwtIds.length > 0) {
144
+ store.$guilds.set(
145
+ jwtIds.map((id) => ({
146
+ id,
147
+ name: `Guild #${id.slice(-6)}`,
148
+ icon: null,
149
+ owner: true,
150
+ permissions: '0',
151
+ })),
152
+ );
153
+ store.$guildsStale.set(true);
154
+ store.$authState.set('authenticated');
155
+ if (jwtIds.length === 1) api.selectGuild(jwtIds[0]);
156
+ return;
157
+ }
158
+
159
+ store.$authState.set('discord_reauth_required');
160
+ } catch {
161
+ store.$authState.set('unauthenticated');
162
+ } finally {
163
+ runtime.lastInitAt = Date.now();
164
+ }
165
+ }
166
+
167
+ async function initAuth(force = false): Promise<void> {
168
+ if (!force) {
169
+ if (runtime.initPromise) return runtime.initPromise;
170
+ if (
171
+ runtime.lastInitAt > 0 &&
172
+ Date.now() - runtime.lastInitAt < INIT_DEDUP_MS &&
173
+ store.$authState.get() !== 'loading'
174
+ ) {
175
+ return;
176
+ }
177
+ }
178
+ runtime.initPromise = runInitAuth();
179
+ try {
180
+ await runtime.initPromise;
181
+ } finally {
182
+ runtime.initPromise = null;
183
+ }
184
+ }
185
+
186
+ async function signInWithDiscord(): Promise<void> {
187
+ try {
188
+ await config.signInWithDiscord();
189
+ } catch (e) {
190
+ config.notify({
191
+ message:
192
+ e instanceof Error ? e.message : 'Discord sign-in failed',
193
+ severity: 'error',
194
+ duration: 5000,
195
+ });
196
+ }
197
+ }
198
+
199
+ return { initAuth, resyncOwnedGuilds, signInWithDiscord };
200
+ }
@@ -0,0 +1,127 @@
1
+ import {
2
+ GUILDS_CACHE_KEY,
3
+ GUILDS_CACHE_TTL_MS,
4
+ TOKENS_CACHE_KEY,
5
+ TOKENS_CACHE_TTL_MS,
6
+ } from './constants';
7
+ import type { AgentTokenRow, DiscordGuild } from './types';
8
+
9
+ export function parseJwtPayload(jwt: string): Record<string, unknown> | null {
10
+ try {
11
+ const parts = jwt.split('.');
12
+ if (parts.length < 2) return null;
13
+ const b64 = parts[1].replace(/-/g, '+').replace(/_/g, '/');
14
+ const padded = b64 + '==='.slice((b64.length + 3) % 4);
15
+ const json = atob(padded);
16
+ return JSON.parse(json) as Record<string, unknown>;
17
+ } catch {
18
+ return null;
19
+ }
20
+ }
21
+
22
+ export function extractJwtOwnedGuildIds(jwt: string): string[] {
23
+ const payload = parseJwtPayload(jwt);
24
+ if (!payload || !Array.isArray(payload['owned_guilds'])) return [];
25
+ return payload['owned_guilds'].filter(
26
+ (g): g is string => typeof g === 'string' && /^[0-9]{17,20}$/.test(g),
27
+ );
28
+ }
29
+
30
+ interface CachedGuildsBlob {
31
+ user_id: string;
32
+ guilds: DiscordGuild[];
33
+ cached_at: number;
34
+ }
35
+
36
+ export function loadCachedGuilds(userId: string): DiscordGuild[] | null {
37
+ try {
38
+ const raw = localStorage.getItem(GUILDS_CACHE_KEY);
39
+ if (!raw) return null;
40
+ const blob = JSON.parse(raw) as CachedGuildsBlob;
41
+ if (blob.user_id !== userId) return null;
42
+ if (Date.now() - blob.cached_at > GUILDS_CACHE_TTL_MS) return null;
43
+ if (!Array.isArray(blob.guilds)) return null;
44
+ return blob.guilds;
45
+ } catch {
46
+ return null;
47
+ }
48
+ }
49
+
50
+ export function saveCachedGuilds(userId: string, guilds: DiscordGuild[]): void {
51
+ try {
52
+ const blob: CachedGuildsBlob = {
53
+ user_id: userId,
54
+ guilds,
55
+ cached_at: Date.now(),
56
+ };
57
+ localStorage.setItem(GUILDS_CACHE_KEY, JSON.stringify(blob));
58
+ } catch {
59
+ /* non-fatal */
60
+ }
61
+ }
62
+
63
+ interface CachedTokensBlob {
64
+ user_id: string;
65
+ tokens_by_guild: Record<
66
+ string,
67
+ { tokens: AgentTokenRow[]; cached_at: number }
68
+ >;
69
+ }
70
+
71
+ export function loadCachedTokens(
72
+ userId: string,
73
+ guildId: string,
74
+ ): AgentTokenRow[] | null {
75
+ try {
76
+ const raw = localStorage.getItem(TOKENS_CACHE_KEY);
77
+ if (!raw) return null;
78
+ const blob = JSON.parse(raw) as CachedTokensBlob;
79
+ if (blob.user_id !== userId) return null;
80
+ const entry = blob.tokens_by_guild?.[guildId];
81
+ if (!entry) return null;
82
+ if (Date.now() - entry.cached_at > TOKENS_CACHE_TTL_MS) return null;
83
+ return entry.tokens;
84
+ } catch {
85
+ return null;
86
+ }
87
+ }
88
+
89
+ export function saveCachedTokens(
90
+ userId: string,
91
+ guildId: string,
92
+ tokens: AgentTokenRow[],
93
+ ): void {
94
+ try {
95
+ const raw = localStorage.getItem(TOKENS_CACHE_KEY);
96
+ let blob: CachedTokensBlob;
97
+ try {
98
+ blob = raw
99
+ ? (JSON.parse(raw) as CachedTokensBlob)
100
+ : { user_id: userId, tokens_by_guild: {} };
101
+ } catch {
102
+ blob = { user_id: userId, tokens_by_guild: {} };
103
+ }
104
+ if (blob.user_id !== userId) {
105
+ blob = { user_id: userId, tokens_by_guild: {} };
106
+ }
107
+ blob.tokens_by_guild[guildId] = { tokens, cached_at: Date.now() };
108
+ localStorage.setItem(TOKENS_CACHE_KEY, JSON.stringify(blob));
109
+ } catch {
110
+ /* non-fatal */
111
+ }
112
+ }
113
+
114
+ export function invalidateCachedTokens(userId: string, guildId: string): void {
115
+ try {
116
+ const raw = localStorage.getItem(TOKENS_CACHE_KEY);
117
+ if (!raw) return;
118
+ const blob = JSON.parse(raw) as CachedTokensBlob;
119
+ if (blob.user_id !== userId) return;
120
+ if (blob.tokens_by_guild?.[guildId]) {
121
+ delete blob.tokens_by_guild[guildId];
122
+ localStorage.setItem(TOKENS_CACHE_KEY, JSON.stringify(blob));
123
+ }
124
+ } catch {
125
+ /* non-fatal */
126
+ }
127
+ }
@@ -0,0 +1,71 @@
1
+ import type { AgentsCtx, CallGuildVault } from '../ctx';
2
+
3
+ export function makeCallGuildVault(ctx: AgentsCtx): CallGuildVault {
4
+ return async function callGuildVault(
5
+ command: string,
6
+ accessToken: string,
7
+ extra: Record<string, unknown>,
8
+ opts: { retriedAfterResync?: boolean } = {},
9
+ ) {
10
+ try {
11
+ const resp = await fetch(ctx.endpoints.guildVault, {
12
+ method: 'POST',
13
+ headers: {
14
+ 'Content-Type': 'application/json',
15
+ Authorization: `Bearer ${accessToken}`,
16
+ },
17
+ body: JSON.stringify({ command, ...extra }),
18
+ });
19
+ const text = await resp.text();
20
+ let body: unknown;
21
+ try {
22
+ body = JSON.parse(text);
23
+ } catch {
24
+ return {
25
+ ok: false,
26
+ error: `HTTP ${resp.status}: ${text.slice(0, 200)}`,
27
+ };
28
+ }
29
+ if (!resp.ok) {
30
+ const b = body as {
31
+ error?: string;
32
+ sqlstate?: string | null;
33
+ hint?: string | null;
34
+ context?: string | null;
35
+ } | null;
36
+ const errMsg = b?.error ?? '';
37
+ const looksStale =
38
+ resp.status === 403 &&
39
+ /owned_guilds/i.test(errMsg) &&
40
+ !opts.retriedAfterResync;
41
+ if (looksStale) {
42
+ const resynced = await ctx.resyncOwnedGuilds();
43
+ if (resynced) {
44
+ const newToken = ctx.store.$accessToken.get();
45
+ if (newToken) {
46
+ return ctx.callGuildVault(
47
+ command,
48
+ newToken,
49
+ extra,
50
+ {
51
+ retriedAfterResync: true,
52
+ },
53
+ );
54
+ }
55
+ }
56
+ }
57
+ const parts: string[] = [errMsg || `HTTP ${resp.status}`];
58
+ if (b?.sqlstate) parts.push(`sqlstate=${b.sqlstate}`);
59
+ if (b?.hint) parts.push(`hint=${b.hint}`);
60
+ if (b?.context) parts.push(`context=${b.context}`);
61
+ return { ok: false, error: parts.join(' · ') };
62
+ }
63
+ return { ok: true, body };
64
+ } catch (e) {
65
+ return {
66
+ ok: false,
67
+ error: e instanceof Error ? e.message : 'Network error',
68
+ };
69
+ }
70
+ };
71
+ }