@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.
- package/dist/app.d.ts +9 -0
- package/dist/app.d.ts.map +1 -0
- package/dist/app.js +147 -0
- package/dist/app.js.map +1 -0
- package/dist/auth/handler.d.ts +19 -0
- package/dist/auth/handler.d.ts.map +1 -0
- package/dist/auth/handler.js +213 -0
- package/dist/auth/handler.js.map +1 -0
- package/dist/auth/session.d.ts +16 -0
- package/dist/auth/session.d.ts.map +1 -0
- package/dist/auth/session.js +54 -0
- package/dist/auth/session.js.map +1 -0
- package/dist/db/d1/index.d.ts +14 -0
- package/dist/db/d1/index.d.ts.map +1 -0
- package/dist/db/d1/index.js +252 -0
- package/dist/db/d1/index.js.map +1 -0
- package/dist/db/d1/schema.d.ts +610 -0
- package/dist/db/d1/schema.d.ts.map +1 -0
- package/dist/db/d1/schema.js +58 -0
- package/dist/db/d1/schema.js.map +1 -0
- package/dist/db/mysql/index.d.ts +14 -0
- package/dist/db/mysql/index.d.ts.map +1 -0
- package/dist/db/mysql/index.js +248 -0
- package/dist/db/mysql/index.js.map +1 -0
- package/dist/db/mysql/schema.d.ts +562 -0
- package/dist/db/mysql/schema.d.ts.map +1 -0
- package/dist/db/mysql/schema.js +61 -0
- package/dist/db/mysql/schema.js.map +1 -0
- package/dist/db/postgresql/index.d.ts +14 -0
- package/dist/db/postgresql/index.d.ts.map +1 -0
- package/dist/db/postgresql/index.js +246 -0
- package/dist/db/postgresql/index.js.map +1 -0
- package/dist/db/postgresql/schema.d.ts +591 -0
- package/dist/db/postgresql/schema.d.ts.map +1 -0
- package/dist/db/postgresql/schema.js +64 -0
- package/dist/db/postgresql/schema.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/relay/api-proxy.d.ts +10 -0
- package/dist/relay/api-proxy.d.ts.map +1 -0
- package/dist/relay/api-proxy.js +40 -0
- package/dist/relay/api-proxy.js.map +1 -0
- package/dist/runtime/cf/crypto.d.ts +7 -0
- package/dist/runtime/cf/crypto.d.ts.map +1 -0
- package/dist/runtime/cf/crypto.js +48 -0
- package/dist/runtime/cf/crypto.js.map +1 -0
- package/dist/runtime/cf/index.d.ts +20 -0
- package/dist/runtime/cf/index.d.ts.map +1 -0
- package/dist/runtime/cf/index.js +14 -0
- package/dist/runtime/cf/index.js.map +1 -0
- package/dist/runtime/cf/relay.d.ts +11 -0
- package/dist/runtime/cf/relay.d.ts.map +1 -0
- package/dist/runtime/cf/relay.js +57 -0
- package/dist/runtime/cf/relay.js.map +1 -0
- package/dist/runtime/cf/vault.d.ts +7 -0
- package/dist/runtime/cf/vault.d.ts.map +1 -0
- package/dist/runtime/cf/vault.js +68 -0
- package/dist/runtime/cf/vault.js.map +1 -0
- package/dist/runtime/node/crypto.d.ts +6 -0
- package/dist/runtime/node/crypto.d.ts.map +1 -0
- package/dist/runtime/node/crypto.js +26 -0
- package/dist/runtime/node/crypto.js.map +1 -0
- package/dist/runtime/node/index.d.ts +17 -0
- package/dist/runtime/node/index.d.ts.map +1 -0
- package/dist/runtime/node/index.js +14 -0
- package/dist/runtime/node/index.js.map +1 -0
- package/dist/runtime/node/relay.d.ts +6 -0
- package/dist/runtime/node/relay.d.ts.map +1 -0
- package/dist/runtime/node/relay.js +73 -0
- package/dist/runtime/node/relay.js.map +1 -0
- package/dist/runtime/node/vault.d.ts +7 -0
- package/dist/runtime/node/vault.d.ts.map +1 -0
- package/dist/runtime/node/vault.js +41 -0
- package/dist/runtime/node/vault.js.map +1 -0
- package/dist/slack/events.d.ts +15 -0
- package/dist/slack/events.d.ts.map +1 -0
- package/dist/slack/events.js +63 -0
- package/dist/slack/events.js.map +1 -0
- package/dist/slack/oauth.d.ts +13 -0
- package/dist/slack/oauth.d.ts.map +1 -0
- package/dist/slack/oauth.js +90 -0
- package/dist/slack/oauth.js.map +1 -0
- package/dist/slack/provisioner.d.ts +60 -0
- package/dist/slack/provisioner.d.ts.map +1 -0
- package/dist/slack/provisioner.js +156 -0
- package/dist/slack/provisioner.js.map +1 -0
- package/dist/types/crypto.d.ts +15 -0
- package/dist/types/crypto.d.ts.map +1 -0
- package/dist/types/crypto.js +2 -0
- package/dist/types/crypto.js.map +1 -0
- package/dist/types/index.d.ts +6 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +2 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/platform.d.ts +25 -0
- package/dist/types/platform.d.ts.map +1 -0
- package/dist/types/platform.js +2 -0
- package/dist/types/platform.js.map +1 -0
- package/dist/types/relay.d.ts +16 -0
- package/dist/types/relay.d.ts.map +1 -0
- package/dist/types/relay.js +2 -0
- package/dist/types/relay.js.map +1 -0
- package/dist/types/repository.d.ts +78 -0
- package/dist/types/repository.d.ts.map +1 -0
- package/dist/types/repository.js +6 -0
- package/dist/types/repository.js.map +1 -0
- package/dist/types/vault.d.ts +9 -0
- package/dist/types/vault.d.ts.map +1 -0
- package/dist/types/vault.js +2 -0
- package/dist/types/vault.js.map +1 -0
- package/dist/web/api.d.ts +9 -0
- package/dist/web/api.d.ts.map +1 -0
- package/dist/web/api.js +144 -0
- package/dist/web/api.js.map +1 -0
- package/dist/web/pages.d.ts +4 -0
- package/dist/web/pages.d.ts.map +1 -0
- package/dist/web/pages.js +401 -0
- package/dist/web/pages.js.map +1 -0
- package/dist/web/setup.d.ts +5 -0
- package/dist/web/setup.d.ts.map +1 -0
- package/dist/web/setup.js +208 -0
- package/dist/web/setup.js.map +1 -0
- package/package.json +46 -0
- package/src/app.ts +221 -0
- package/src/auth/handler.ts +343 -0
- package/src/auth/session.ts +89 -0
- package/src/db/d1/index.ts +304 -0
- package/src/db/d1/schema.ts +62 -0
- package/src/db/mysql/index.ts +301 -0
- package/src/db/mysql/schema.ts +78 -0
- package/src/db/postgresql/index.ts +311 -0
- package/src/db/postgresql/schema.ts +82 -0
- package/src/index.ts +21 -0
- package/src/relay/api-proxy.ts +61 -0
- package/src/runtime/cf/crypto.ts +74 -0
- package/src/runtime/cf/index.ts +31 -0
- package/src/runtime/cf/relay.ts +74 -0
- package/src/runtime/cf/vault.ts +99 -0
- package/src/runtime/node/crypto.ts +33 -0
- package/src/runtime/node/index.ts +28 -0
- package/src/runtime/node/relay.ts +98 -0
- package/src/runtime/node/vault.ts +50 -0
- package/src/slack/events.ts +92 -0
- package/src/slack/oauth.ts +127 -0
- package/src/slack/provisioner.ts +256 -0
- package/src/types/crypto.ts +14 -0
- package/src/types/index.ts +14 -0
- package/src/types/platform.ts +31 -0
- package/src/types/relay.ts +16 -0
- package/src/types/repository.ts +93 -0
- package/src/types/vault.ts +8 -0
- package/src/web/api.ts +204 -0
- package/src/web/pages.ts +458 -0
- package/src/web/setup.ts +270 -0
- package/tsconfig.json +19 -0
- 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
|
+
}
|