@sena-ai/platform-core 1.4.0

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 (158) hide show
  1. package/dist/app.d.ts +9 -0
  2. package/dist/app.d.ts.map +1 -0
  3. package/dist/app.js +147 -0
  4. package/dist/app.js.map +1 -0
  5. package/dist/auth/handler.d.ts +19 -0
  6. package/dist/auth/handler.d.ts.map +1 -0
  7. package/dist/auth/handler.js +213 -0
  8. package/dist/auth/handler.js.map +1 -0
  9. package/dist/auth/session.d.ts +16 -0
  10. package/dist/auth/session.d.ts.map +1 -0
  11. package/dist/auth/session.js +54 -0
  12. package/dist/auth/session.js.map +1 -0
  13. package/dist/db/d1/index.d.ts +14 -0
  14. package/dist/db/d1/index.d.ts.map +1 -0
  15. package/dist/db/d1/index.js +252 -0
  16. package/dist/db/d1/index.js.map +1 -0
  17. package/dist/db/d1/schema.d.ts +610 -0
  18. package/dist/db/d1/schema.d.ts.map +1 -0
  19. package/dist/db/d1/schema.js +58 -0
  20. package/dist/db/d1/schema.js.map +1 -0
  21. package/dist/db/mysql/index.d.ts +14 -0
  22. package/dist/db/mysql/index.d.ts.map +1 -0
  23. package/dist/db/mysql/index.js +248 -0
  24. package/dist/db/mysql/index.js.map +1 -0
  25. package/dist/db/mysql/schema.d.ts +562 -0
  26. package/dist/db/mysql/schema.d.ts.map +1 -0
  27. package/dist/db/mysql/schema.js +61 -0
  28. package/dist/db/mysql/schema.js.map +1 -0
  29. package/dist/db/postgresql/index.d.ts +14 -0
  30. package/dist/db/postgresql/index.d.ts.map +1 -0
  31. package/dist/db/postgresql/index.js +246 -0
  32. package/dist/db/postgresql/index.js.map +1 -0
  33. package/dist/db/postgresql/schema.d.ts +591 -0
  34. package/dist/db/postgresql/schema.d.ts.map +1 -0
  35. package/dist/db/postgresql/schema.js +64 -0
  36. package/dist/db/postgresql/schema.js.map +1 -0
  37. package/dist/index.d.ts +6 -0
  38. package/dist/index.d.ts.map +1 -0
  39. package/dist/index.js +3 -0
  40. package/dist/index.js.map +1 -0
  41. package/dist/relay/api-proxy.d.ts +10 -0
  42. package/dist/relay/api-proxy.d.ts.map +1 -0
  43. package/dist/relay/api-proxy.js +40 -0
  44. package/dist/relay/api-proxy.js.map +1 -0
  45. package/dist/runtime/cf/crypto.d.ts +7 -0
  46. package/dist/runtime/cf/crypto.d.ts.map +1 -0
  47. package/dist/runtime/cf/crypto.js +48 -0
  48. package/dist/runtime/cf/crypto.js.map +1 -0
  49. package/dist/runtime/cf/index.d.ts +20 -0
  50. package/dist/runtime/cf/index.d.ts.map +1 -0
  51. package/dist/runtime/cf/index.js +14 -0
  52. package/dist/runtime/cf/index.js.map +1 -0
  53. package/dist/runtime/cf/relay.d.ts +11 -0
  54. package/dist/runtime/cf/relay.d.ts.map +1 -0
  55. package/dist/runtime/cf/relay.js +57 -0
  56. package/dist/runtime/cf/relay.js.map +1 -0
  57. package/dist/runtime/cf/vault.d.ts +7 -0
  58. package/dist/runtime/cf/vault.d.ts.map +1 -0
  59. package/dist/runtime/cf/vault.js +68 -0
  60. package/dist/runtime/cf/vault.js.map +1 -0
  61. package/dist/runtime/node/crypto.d.ts +6 -0
  62. package/dist/runtime/node/crypto.d.ts.map +1 -0
  63. package/dist/runtime/node/crypto.js +26 -0
  64. package/dist/runtime/node/crypto.js.map +1 -0
  65. package/dist/runtime/node/index.d.ts +17 -0
  66. package/dist/runtime/node/index.d.ts.map +1 -0
  67. package/dist/runtime/node/index.js +14 -0
  68. package/dist/runtime/node/index.js.map +1 -0
  69. package/dist/runtime/node/relay.d.ts +6 -0
  70. package/dist/runtime/node/relay.d.ts.map +1 -0
  71. package/dist/runtime/node/relay.js +73 -0
  72. package/dist/runtime/node/relay.js.map +1 -0
  73. package/dist/runtime/node/vault.d.ts +7 -0
  74. package/dist/runtime/node/vault.d.ts.map +1 -0
  75. package/dist/runtime/node/vault.js +41 -0
  76. package/dist/runtime/node/vault.js.map +1 -0
  77. package/dist/slack/events.d.ts +15 -0
  78. package/dist/slack/events.d.ts.map +1 -0
  79. package/dist/slack/events.js +63 -0
  80. package/dist/slack/events.js.map +1 -0
  81. package/dist/slack/oauth.d.ts +13 -0
  82. package/dist/slack/oauth.d.ts.map +1 -0
  83. package/dist/slack/oauth.js +90 -0
  84. package/dist/slack/oauth.js.map +1 -0
  85. package/dist/slack/provisioner.d.ts +60 -0
  86. package/dist/slack/provisioner.d.ts.map +1 -0
  87. package/dist/slack/provisioner.js +156 -0
  88. package/dist/slack/provisioner.js.map +1 -0
  89. package/dist/types/crypto.d.ts +15 -0
  90. package/dist/types/crypto.d.ts.map +1 -0
  91. package/dist/types/crypto.js +2 -0
  92. package/dist/types/crypto.js.map +1 -0
  93. package/dist/types/index.d.ts +6 -0
  94. package/dist/types/index.d.ts.map +1 -0
  95. package/dist/types/index.js +2 -0
  96. package/dist/types/index.js.map +1 -0
  97. package/dist/types/platform.d.ts +25 -0
  98. package/dist/types/platform.d.ts.map +1 -0
  99. package/dist/types/platform.js +2 -0
  100. package/dist/types/platform.js.map +1 -0
  101. package/dist/types/relay.d.ts +16 -0
  102. package/dist/types/relay.d.ts.map +1 -0
  103. package/dist/types/relay.js +2 -0
  104. package/dist/types/relay.js.map +1 -0
  105. package/dist/types/repository.d.ts +78 -0
  106. package/dist/types/repository.d.ts.map +1 -0
  107. package/dist/types/repository.js +6 -0
  108. package/dist/types/repository.js.map +1 -0
  109. package/dist/types/vault.d.ts +9 -0
  110. package/dist/types/vault.d.ts.map +1 -0
  111. package/dist/types/vault.js +2 -0
  112. package/dist/types/vault.js.map +1 -0
  113. package/dist/web/api.d.ts +9 -0
  114. package/dist/web/api.d.ts.map +1 -0
  115. package/dist/web/api.js +144 -0
  116. package/dist/web/api.js.map +1 -0
  117. package/dist/web/pages.d.ts +4 -0
  118. package/dist/web/pages.d.ts.map +1 -0
  119. package/dist/web/pages.js +401 -0
  120. package/dist/web/pages.js.map +1 -0
  121. package/dist/web/setup.d.ts +5 -0
  122. package/dist/web/setup.d.ts.map +1 -0
  123. package/dist/web/setup.js +208 -0
  124. package/dist/web/setup.js.map +1 -0
  125. package/package.json +46 -0
  126. package/src/app.ts +221 -0
  127. package/src/auth/handler.ts +343 -0
  128. package/src/auth/session.ts +89 -0
  129. package/src/db/d1/index.ts +304 -0
  130. package/src/db/d1/schema.ts +62 -0
  131. package/src/db/mysql/index.ts +301 -0
  132. package/src/db/mysql/schema.ts +78 -0
  133. package/src/db/postgresql/index.ts +311 -0
  134. package/src/db/postgresql/schema.ts +82 -0
  135. package/src/index.ts +21 -0
  136. package/src/relay/api-proxy.ts +61 -0
  137. package/src/runtime/cf/crypto.ts +74 -0
  138. package/src/runtime/cf/index.ts +31 -0
  139. package/src/runtime/cf/relay.ts +74 -0
  140. package/src/runtime/cf/vault.ts +99 -0
  141. package/src/runtime/node/crypto.ts +33 -0
  142. package/src/runtime/node/index.ts +28 -0
  143. package/src/runtime/node/relay.ts +98 -0
  144. package/src/runtime/node/vault.ts +50 -0
  145. package/src/slack/events.ts +92 -0
  146. package/src/slack/oauth.ts +127 -0
  147. package/src/slack/provisioner.ts +256 -0
  148. package/src/types/crypto.ts +14 -0
  149. package/src/types/index.ts +14 -0
  150. package/src/types/platform.ts +31 -0
  151. package/src/types/relay.ts +16 -0
  152. package/src/types/repository.ts +93 -0
  153. package/src/types/vault.ts +8 -0
  154. package/src/web/api.ts +204 -0
  155. package/src/web/pages.ts +458 -0
  156. package/src/web/setup.ts +270 -0
  157. package/tsconfig.json +19 -0
  158. package/tsconfig.tsbuildinfo +1 -0
@@ -0,0 +1,78 @@
1
+ import {
2
+ mysqlTableCreator,
3
+ varchar,
4
+ text,
5
+ mysqlEnum,
6
+ datetime,
7
+ uniqueIndex,
8
+ } from 'drizzle-orm/mysql-core'
9
+
10
+ /**
11
+ * Table prefix.
12
+ * When set, all tables get a `{prefix}_` prefix.
13
+ * E.g.: 'sena' -> sena_bots, sena_config_tokens
14
+ */
15
+ export const TABLE_PREFIX = ''
16
+ const mysqlTable = mysqlTableCreator((name) =>
17
+ TABLE_PREFIX ? `${TABLE_PREFIX}_${name}` : name,
18
+ )
19
+
20
+ export const bots = mysqlTable(
21
+ 'bots',
22
+ {
23
+ id: varchar('id', { length: 36 }).primaryKey(),
24
+ name: varchar('name', { length: 255 }).notNull(),
25
+ botUsername: varchar('bot_username', { length: 80 }).notNull().default(''),
26
+ profileImageUrl: text('profile_image_url'),
27
+ connectKey: varchar('connect_key', { length: 255 }).notNull(),
28
+ slackAppId: varchar('slack_app_id', { length: 64 }),
29
+ slackTeamId: varchar('slack_team_id', { length: 64 }),
30
+ botTokenEnc: text('bot_token_enc'),
31
+ signingSecretEnc: text('signing_secret_enc'),
32
+ clientId: varchar('client_id', { length: 128 }),
33
+ clientSecretEnc: text('client_secret_enc'),
34
+ manifestJson: text('manifest_json'),
35
+ status: mysqlEnum('status', ['pending', 'active', 'disabled'])
36
+ .notNull()
37
+ .default('pending'),
38
+ createdAt: datetime('created_at')
39
+ .notNull()
40
+ .$defaultFn(() => new Date()),
41
+ updatedAt: datetime('updated_at')
42
+ .notNull()
43
+ .$defaultFn(() => new Date())
44
+ .$onUpdateFn(() => new Date()),
45
+ },
46
+ (table) => [uniqueIndex('idx_bots_connect_key').on(table.connectKey)],
47
+ )
48
+
49
+ export const configTokens = mysqlTable('config_tokens', {
50
+ workspaceId: varchar('workspace_id', { length: 64 }).primaryKey(),
51
+ accessTokenEnc: text('access_token_enc').notNull(),
52
+ refreshTokenEnc: text('refresh_token_enc').notNull(),
53
+ expiresAt: datetime('expires_at').notNull(),
54
+ updatedAt: datetime('updated_at')
55
+ .notNull()
56
+ .$defaultFn(() => new Date())
57
+ .$onUpdateFn(() => new Date()),
58
+ })
59
+
60
+ export const oauthStates = mysqlTable('oauth_states', {
61
+ state: varchar('state', { length: 64 }).primaryKey(),
62
+ botId: varchar('bot_id', { length: 36 }).notNull(),
63
+ expiresAt: datetime('expires_at').notNull(),
64
+ })
65
+
66
+ export const workspaceAdminConfig = mysqlTable('workspace_admin_config', {
67
+ workspaceId: varchar('workspace_id', { length: 64 }).primaryKey(),
68
+ slackClientId: varchar('slack_client_id', { length: 128 }),
69
+ slackClientSecretEnc: text('slack_client_secret_enc'),
70
+ dCookieEnc: text('d_cookie_enc'),
71
+ xoxcTokenEnc: text('xoxc_token_enc'),
72
+ workspaceDomain: varchar('workspace_domain', { length: 255 }),
73
+ updatedAt: datetime('updated_at')
74
+ .notNull()
75
+ .$defaultFn(() => new Date())
76
+ .$onUpdateFn(() => new Date()),
77
+ updatedByUserId: varchar('updated_by_user_id', { length: 36 }),
78
+ })
@@ -0,0 +1,311 @@
1
+ import { drizzle, type PostgresJsDatabase } from 'drizzle-orm/postgres-js'
2
+ import postgres from 'postgres'
3
+ import { eq, and, lt } from 'drizzle-orm'
4
+ import type {
5
+ BotRow,
6
+ ConfigTokenRow,
7
+ WorkspaceAdminConfigRow,
8
+ BotRepository,
9
+ ConfigTokenRepository,
10
+ OAuthStateRepository,
11
+ WorkspaceAdminConfigRepository,
12
+ } from '../../types/repository.js'
13
+ import * as schema from './schema.js'
14
+
15
+ export type PostgreSQLDatabase = PostgresJsDatabase<typeof schema>
16
+
17
+ export function initPostgreSQLDb(databaseUrl: string): PostgreSQLDatabase {
18
+ const client = postgres(databaseUrl)
19
+ return drizzle(client, { schema })
20
+ }
21
+
22
+ function rowToBot(row: typeof schema.bots.$inferSelect): BotRow {
23
+ return {
24
+ id: row.id,
25
+ name: row.name,
26
+ botUsername: row.botUsername,
27
+ profileImageUrl: row.profileImageUrl,
28
+ connectKey: row.connectKey,
29
+ slackAppId: row.slackAppId,
30
+ slackTeamId: row.slackTeamId,
31
+ botTokenEnc: row.botTokenEnc,
32
+ signingSecretEnc: row.signingSecretEnc,
33
+ clientId: row.clientId,
34
+ clientSecretEnc: row.clientSecretEnc,
35
+ manifestJson: row.manifestJson,
36
+ status: row.status,
37
+ createdAt: row.createdAt,
38
+ updatedAt: row.updatedAt,
39
+ }
40
+ }
41
+
42
+ function rowToWorkspaceAdminConfig(
43
+ row: typeof schema.workspaceAdminConfig.$inferSelect,
44
+ ): WorkspaceAdminConfigRow {
45
+ return {
46
+ workspaceId: row.workspaceId,
47
+ slackClientId: row.slackClientId,
48
+ slackClientSecretEnc: row.slackClientSecretEnc,
49
+ dCookieEnc: row.dCookieEnc,
50
+ xoxcTokenEnc: row.xoxcTokenEnc,
51
+ workspaceDomain: row.workspaceDomain,
52
+ updatedAt: row.updatedAt,
53
+ updatedByUserId: row.updatedByUserId,
54
+ }
55
+ }
56
+
57
+ export interface PostgreSQLRepositories {
58
+ bots: BotRepository
59
+ configTokens: ConfigTokenRepository
60
+ oauthStates: OAuthStateRepository
61
+ workspaceAdminConfig: WorkspaceAdminConfigRepository
62
+ }
63
+
64
+ export function createPostgreSQLRepositories(
65
+ db: PostgreSQLDatabase,
66
+ ): PostgreSQLRepositories {
67
+ return {
68
+ bots: createBotRepository(db),
69
+ configTokens: createConfigTokenRepository(db),
70
+ oauthStates: createOAuthStateRepository(db),
71
+ workspaceAdminConfig: createWorkspaceAdminConfigRepository(db),
72
+ }
73
+ }
74
+
75
+ function createBotRepository(db: PostgreSQLDatabase): BotRepository {
76
+ return {
77
+ async findById(id) {
78
+ const [row] = await db.select().from(schema.bots).where(eq(schema.bots.id, id)).limit(1)
79
+ return row ? rowToBot(row) : null
80
+ },
81
+
82
+ async findByConnectKey(connectKey) {
83
+ const [row] = await db
84
+ .select()
85
+ .from(schema.bots)
86
+ .where(eq(schema.bots.connectKey, connectKey))
87
+ .limit(1)
88
+ return row ? rowToBot(row) : null
89
+ },
90
+
91
+ async findByConnectKeyAndStatus(connectKey, status) {
92
+ const [row] = await db
93
+ .select()
94
+ .from(schema.bots)
95
+ .where(
96
+ and(
97
+ eq(schema.bots.connectKey, connectKey),
98
+ eq(schema.bots.status, status),
99
+ ),
100
+ )
101
+ .limit(1)
102
+ return row ? rowToBot(row) : null
103
+ },
104
+
105
+ async findByIdAndStatus(id, status) {
106
+ const [row] = await db
107
+ .select()
108
+ .from(schema.bots)
109
+ .where(and(eq(schema.bots.id, id), eq(schema.bots.status, status)))
110
+ .limit(1)
111
+ return row ? rowToBot(row) : null
112
+ },
113
+
114
+ async findAll() {
115
+ const rows = await db.select().from(schema.bots).orderBy(schema.bots.createdAt)
116
+ return rows.map(rowToBot)
117
+ },
118
+
119
+ async findAllSummary() {
120
+ const rows = await db
121
+ .select({
122
+ id: schema.bots.id,
123
+ name: schema.bots.name,
124
+ profileImageUrl: schema.bots.profileImageUrl,
125
+ slackAppId: schema.bots.slackAppId,
126
+ slackTeamId: schema.bots.slackTeamId,
127
+ status: schema.bots.status,
128
+ createdAt: schema.bots.createdAt,
129
+ })
130
+ .from(schema.bots)
131
+ .orderBy(schema.bots.createdAt)
132
+ return rows
133
+ },
134
+
135
+ async create(bot) {
136
+ await db.insert(schema.bots).values({
137
+ id: bot.id,
138
+ name: bot.name,
139
+ botUsername: bot.botUsername,
140
+ profileImageUrl: bot.profileImageUrl,
141
+ connectKey: bot.connectKey,
142
+ slackAppId: bot.slackAppId,
143
+ slackTeamId: bot.slackTeamId,
144
+ botTokenEnc: bot.botTokenEnc,
145
+ signingSecretEnc: bot.signingSecretEnc,
146
+ clientId: bot.clientId,
147
+ clientSecretEnc: bot.clientSecretEnc,
148
+ manifestJson: bot.manifestJson,
149
+ status: bot.status,
150
+ })
151
+ },
152
+
153
+ async update(id, data) {
154
+ await db.update(schema.bots).set(data).where(eq(schema.bots.id, id))
155
+ },
156
+
157
+ async delete(id) {
158
+ await db.delete(schema.bots).where(eq(schema.bots.id, id))
159
+ },
160
+ }
161
+ }
162
+
163
+ function createConfigTokenRepository(
164
+ db: PostgreSQLDatabase,
165
+ ): ConfigTokenRepository {
166
+ return {
167
+ async findByWorkspaceId(id) {
168
+ const [row] = await db
169
+ .select()
170
+ .from(schema.configTokens)
171
+ .where(eq(schema.configTokens.workspaceId, id))
172
+ .limit(1)
173
+ if (!row) return null
174
+ return {
175
+ workspaceId: row.workspaceId,
176
+ accessTokenEnc: row.accessTokenEnc,
177
+ refreshTokenEnc: row.refreshTokenEnc,
178
+ expiresAt: row.expiresAt,
179
+ updatedAt: row.updatedAt,
180
+ }
181
+ },
182
+
183
+ async findAll() {
184
+ const rows = await db.select().from(schema.configTokens)
185
+ return rows.map(
186
+ (row): ConfigTokenRow => ({
187
+ workspaceId: row.workspaceId,
188
+ accessTokenEnc: row.accessTokenEnc,
189
+ refreshTokenEnc: row.refreshTokenEnc,
190
+ expiresAt: row.expiresAt,
191
+ updatedAt: row.updatedAt,
192
+ }),
193
+ )
194
+ },
195
+
196
+ async upsert(row) {
197
+ await db
198
+ .insert(schema.configTokens)
199
+ .values({
200
+ workspaceId: row.workspaceId,
201
+ accessTokenEnc: row.accessTokenEnc,
202
+ refreshTokenEnc: row.refreshTokenEnc,
203
+ expiresAt: row.expiresAt,
204
+ })
205
+ .onConflictDoUpdate({
206
+ target: schema.configTokens.workspaceId,
207
+ set: {
208
+ accessTokenEnc: row.accessTokenEnc,
209
+ refreshTokenEnc: row.refreshTokenEnc,
210
+ expiresAt: row.expiresAt,
211
+ },
212
+ })
213
+ },
214
+ }
215
+ }
216
+
217
+ function createOAuthStateRepository(
218
+ db: PostgreSQLDatabase,
219
+ ): OAuthStateRepository {
220
+ return {
221
+ async create(row) {
222
+ await db.insert(schema.oauthStates).values({
223
+ state: row.state,
224
+ botId: row.botId,
225
+ expiresAt: row.expiresAt,
226
+ })
227
+ },
228
+
229
+ async consume(state) {
230
+ const [row] = await db
231
+ .select()
232
+ .from(schema.oauthStates)
233
+ .where(eq(schema.oauthStates.state, state))
234
+ .limit(1)
235
+ if (!row) return null
236
+
237
+ if (row.expiresAt < new Date()) {
238
+ await db.delete(schema.oauthStates).where(eq(schema.oauthStates.state, state))
239
+ return null
240
+ }
241
+
242
+ await db.delete(schema.oauthStates).where(eq(schema.oauthStates.state, state))
243
+
244
+ return {
245
+ state: row.state,
246
+ botId: row.botId,
247
+ expiresAt: row.expiresAt,
248
+ }
249
+ },
250
+
251
+ async deleteExpired() {
252
+ await db.delete(schema.oauthStates).where(lt(schema.oauthStates.expiresAt, new Date()))
253
+ },
254
+ }
255
+ }
256
+
257
+ function createWorkspaceAdminConfigRepository(
258
+ db: PostgreSQLDatabase,
259
+ ): WorkspaceAdminConfigRepository {
260
+ return {
261
+ async findByWorkspaceId(workspaceId) {
262
+ const [row] = await db
263
+ .select()
264
+ .from(schema.workspaceAdminConfig)
265
+ .where(eq(schema.workspaceAdminConfig.workspaceId, workspaceId))
266
+ .limit(1)
267
+ return row ? rowToWorkspaceAdminConfig(row) : null
268
+ },
269
+
270
+ async findAll() {
271
+ const rows = await db.select().from(schema.workspaceAdminConfig)
272
+ return rows.map(rowToWorkspaceAdminConfig)
273
+ },
274
+
275
+ async upsert(config) {
276
+ await db
277
+ .insert(schema.workspaceAdminConfig)
278
+ .values({
279
+ workspaceId: config.workspaceId,
280
+ slackClientId: config.slackClientId,
281
+ slackClientSecretEnc: config.slackClientSecretEnc,
282
+ dCookieEnc: config.dCookieEnc,
283
+ xoxcTokenEnc: config.xoxcTokenEnc,
284
+ workspaceDomain: config.workspaceDomain,
285
+ updatedByUserId: config.updatedByUserId,
286
+ })
287
+ .onConflictDoUpdate({
288
+ target: schema.workspaceAdminConfig.workspaceId,
289
+ set: {
290
+ slackClientId: config.slackClientId,
291
+ slackClientSecretEnc: config.slackClientSecretEnc,
292
+ dCookieEnc: config.dCookieEnc,
293
+ xoxcTokenEnc: config.xoxcTokenEnc,
294
+ workspaceDomain: config.workspaceDomain,
295
+ updatedByUserId: config.updatedByUserId,
296
+ updatedAt: new Date(),
297
+ },
298
+ })
299
+ },
300
+ }
301
+ }
302
+
303
+ // Re-export schema for drizzle-kit
304
+ export {
305
+ TABLE_PREFIX,
306
+ botStatusEnum,
307
+ bots,
308
+ configTokens,
309
+ oauthStates,
310
+ workspaceAdminConfig,
311
+ } from './schema.js'
@@ -0,0 +1,82 @@
1
+ import {
2
+ pgTableCreator,
3
+ varchar,
4
+ text,
5
+ pgEnum,
6
+ timestamp,
7
+ uniqueIndex,
8
+ } from 'drizzle-orm/pg-core'
9
+
10
+ /**
11
+ * Table prefix.
12
+ * When set, all tables get a `{prefix}_` prefix.
13
+ * E.g.: 'sena' -> sena_bots, sena_config_tokens
14
+ */
15
+ export const TABLE_PREFIX = ''
16
+ const pgTable = pgTableCreator((name) =>
17
+ TABLE_PREFIX ? `${TABLE_PREFIX}_${name}` : name,
18
+ )
19
+
20
+ export const botStatusEnum = pgEnum('bot_status', [
21
+ 'pending',
22
+ 'active',
23
+ 'disabled',
24
+ ])
25
+
26
+ export const bots = pgTable(
27
+ 'bots',
28
+ {
29
+ id: varchar('id', { length: 36 }).primaryKey(),
30
+ name: varchar('name', { length: 255 }).notNull(),
31
+ botUsername: varchar('bot_username', { length: 80 }).notNull().default(''),
32
+ profileImageUrl: text('profile_image_url'),
33
+ connectKey: varchar('connect_key', { length: 255 }).notNull(),
34
+ slackAppId: varchar('slack_app_id', { length: 64 }),
35
+ slackTeamId: varchar('slack_team_id', { length: 64 }),
36
+ botTokenEnc: text('bot_token_enc'),
37
+ signingSecretEnc: text('signing_secret_enc'),
38
+ clientId: varchar('client_id', { length: 128 }),
39
+ clientSecretEnc: text('client_secret_enc'),
40
+ manifestJson: text('manifest_json'),
41
+ status: botStatusEnum('status').notNull().default('pending'),
42
+ createdAt: timestamp('created_at', { mode: 'date' })
43
+ .notNull()
44
+ .$defaultFn(() => new Date()),
45
+ updatedAt: timestamp('updated_at', { mode: 'date' })
46
+ .notNull()
47
+ .$defaultFn(() => new Date())
48
+ .$onUpdateFn(() => new Date()),
49
+ },
50
+ (table) => [uniqueIndex('idx_bots_connect_key').on(table.connectKey)],
51
+ )
52
+
53
+ export const configTokens = pgTable('config_tokens', {
54
+ workspaceId: varchar('workspace_id', { length: 64 }).primaryKey(),
55
+ accessTokenEnc: text('access_token_enc').notNull(),
56
+ refreshTokenEnc: text('refresh_token_enc').notNull(),
57
+ expiresAt: timestamp('expires_at', { mode: 'date' }).notNull(),
58
+ updatedAt: timestamp('updated_at', { mode: 'date' })
59
+ .notNull()
60
+ .$defaultFn(() => new Date())
61
+ .$onUpdateFn(() => new Date()),
62
+ })
63
+
64
+ export const oauthStates = pgTable('oauth_states', {
65
+ state: varchar('state', { length: 64 }).primaryKey(),
66
+ botId: varchar('bot_id', { length: 36 }).notNull(),
67
+ expiresAt: timestamp('expires_at', { mode: 'date' }).notNull(),
68
+ })
69
+
70
+ export const workspaceAdminConfig = pgTable('workspace_admin_config', {
71
+ workspaceId: varchar('workspace_id', { length: 64 }).primaryKey(),
72
+ slackClientId: varchar('slack_client_id', { length: 128 }),
73
+ slackClientSecretEnc: text('slack_client_secret_enc'),
74
+ dCookieEnc: text('d_cookie_enc'),
75
+ xoxcTokenEnc: text('xoxc_token_enc'),
76
+ workspaceDomain: varchar('workspace_domain', { length: 255 }),
77
+ updatedAt: timestamp('updated_at', { mode: 'date' })
78
+ .notNull()
79
+ .$defaultFn(() => new Date())
80
+ .$onUpdateFn(() => new Date()),
81
+ updatedByUserId: varchar('updated_by_user_id', { length: 36 }),
82
+ })
package/src/index.ts ADDED
@@ -0,0 +1,21 @@
1
+ export { createApp } from './app.js'
2
+ export type { CreateAppResult } from './app.js'
3
+ export { createProvisioner } from './slack/provisioner.js'
4
+ export type { Provisioner } from './slack/provisioner.js'
5
+
6
+ // Re-export all types
7
+ export type {
8
+ Vault,
9
+ RelayHub,
10
+ CryptoProvider,
11
+ BotRow,
12
+ ConfigTokenRow,
13
+ OAuthStateRow,
14
+ WorkspaceAdminConfigRow,
15
+ BotRepository,
16
+ ConfigTokenRepository,
17
+ OAuthStateRepository,
18
+ WorkspaceAdminConfigRepository,
19
+ Platform,
20
+ AppConfig,
21
+ } from './types/index.js'
@@ -0,0 +1,61 @@
1
+ import { Hono } from 'hono'
2
+ import type { Vault } from '../types/vault.js'
3
+ import type { BotRepository } from '../types/repository.js'
4
+
5
+ /**
6
+ * Slack API proxy.
7
+ * Local runtimes send POST /relay/api with Slack API calls.
8
+ * The proxy decrypts the bot_token from Vault and forwards the request.
9
+ */
10
+ export function createApiProxy(botRepo: BotRepository, vault: Vault) {
11
+ const app = new Hono()
12
+
13
+ app.post('/relay/api', async (c) => {
14
+ const connectKey = c.req.header('x-connect-key')
15
+ if (!connectKey) {
16
+ return c.json(
17
+ { ok: false, error: 'missing x-connect-key header' },
18
+ 401,
19
+ )
20
+ }
21
+
22
+ const bot = await botRepo.findByConnectKeyAndStatus(connectKey, 'active')
23
+ if (!bot) {
24
+ return c.json(
25
+ { ok: false, error: 'invalid connect_key or bot not active' },
26
+ 401,
27
+ )
28
+ }
29
+
30
+ if (!bot.botTokenEnc) {
31
+ return c.json({ ok: false, error: 'bot has no token configured' }, 500)
32
+ }
33
+
34
+ const botToken = await vault.decrypt(bot.botTokenEnc)
35
+
36
+ const body = await c.req.json<{
37
+ method: string
38
+ params: Record<string, unknown>
39
+ }>()
40
+
41
+ if (!body.method) {
42
+ return c.json({ ok: false, error: 'missing method field' }, 400)
43
+ }
44
+
45
+ const slackUrl = `https://slack.com/api/${body.method}`
46
+
47
+ const slackRes = await fetch(slackUrl, {
48
+ method: 'POST',
49
+ headers: {
50
+ Authorization: `Bearer ${botToken}`,
51
+ 'Content-Type': 'application/json; charset=utf-8',
52
+ },
53
+ body: JSON.stringify(body.params || {}),
54
+ })
55
+
56
+ const slackData = await slackRes.json()
57
+ return c.json(slackData as Record<string, unknown>)
58
+ })
59
+
60
+ return app
61
+ }
@@ -0,0 +1,74 @@
1
+ import type { CryptoProvider } from '../../types/crypto.js'
2
+
3
+ /**
4
+ * CryptoProvider implementation using Web Crypto API.
5
+ * Compatible with CF Workers runtime.
6
+ */
7
+ export function createCfCrypto(): CryptoProvider {
8
+ return {
9
+ async randomHex(byteLength: number): Promise<string> {
10
+ const bytes = crypto.getRandomValues(new Uint8Array(byteLength))
11
+ return Array.from(bytes)
12
+ .map((b) => b.toString(16).padStart(2, '0'))
13
+ .join('')
14
+ },
15
+
16
+ uuid(): string {
17
+ return crypto.randomUUID()
18
+ },
19
+
20
+ async hmacSha256(key: string, data: string): Promise<string> {
21
+ const encoder = new TextEncoder()
22
+ const cryptoKey = await crypto.subtle.importKey(
23
+ 'raw',
24
+ encoder.encode(key),
25
+ { name: 'HMAC', hash: 'SHA-256' },
26
+ false,
27
+ ['sign'],
28
+ )
29
+
30
+ const signature = await crypto.subtle.sign(
31
+ 'HMAC',
32
+ cryptoKey,
33
+ encoder.encode(data),
34
+ )
35
+
36
+ return Array.from(new Uint8Array(signature))
37
+ .map((b) => b.toString(16).padStart(2, '0'))
38
+ .join('')
39
+ },
40
+
41
+ async timingSafeEqual(a: string, b: string): Promise<boolean> {
42
+ const encoder = new TextEncoder()
43
+ const bufA = encoder.encode(a)
44
+ const bufB = encoder.encode(b)
45
+
46
+ if (bufA.length !== bufB.length) return false
47
+
48
+ // Import both as HMAC keys and compare by signing
49
+ // This provides constant-time comparison without node:crypto
50
+ const key = crypto.getRandomValues(new Uint8Array(32))
51
+ const cryptoKey = await crypto.subtle.importKey(
52
+ 'raw',
53
+ key,
54
+ { name: 'HMAC', hash: 'SHA-256' },
55
+ false,
56
+ ['sign'],
57
+ )
58
+
59
+ const [sigA, sigB] = await Promise.all([
60
+ crypto.subtle.sign('HMAC', cryptoKey, bufA),
61
+ crypto.subtle.sign('HMAC', cryptoKey, bufB),
62
+ ])
63
+
64
+ const arrA = new Uint8Array(sigA)
65
+ const arrB = new Uint8Array(sigB)
66
+
67
+ let result = 0
68
+ for (let i = 0; i < arrA.length; i++) {
69
+ result |= arrA[i] ^ arrB[i]
70
+ }
71
+ return result === 0
72
+ },
73
+ }
74
+ }
@@ -0,0 +1,31 @@
1
+ import type { Vault } from '../../types/vault.js'
2
+ import type { RelayHub } from '../../types/relay.js'
3
+ import type { CryptoProvider } from '../../types/crypto.js'
4
+ import { createCfVault } from './vault.js'
5
+ import { createCfCrypto } from './crypto.js'
6
+ import { createCfRelay } from './relay.js'
7
+
8
+ export interface CfEnv {
9
+ RELAY_DO: DurableObjectNamespace
10
+ VAULT_MASTER_KEY: string
11
+ PLATFORM_BASE_URL: string
12
+ SLACK_WORKSPACE_ID: string
13
+ }
14
+
15
+ export interface CfRuntime {
16
+ vault: Vault
17
+ relay: RelayHub
18
+ crypto: CryptoProvider
19
+ }
20
+
21
+ /**
22
+ * Create runtime services for Cloudflare Workers (vault, relay, crypto).
23
+ * Does NOT include DB repositories -- those are created separately via the DB subpath.
24
+ */
25
+ export async function createCfRuntime(env: CfEnv): Promise<CfRuntime> {
26
+ const vault = await createCfVault(env.VAULT_MASTER_KEY)
27
+ const crypto = createCfCrypto()
28
+ const relay = createCfRelay(env.RELAY_DO)
29
+
30
+ return { vault, relay, crypto }
31
+ }