@emulators/slack 0.3.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/index.js ADDED
@@ -0,0 +1,1313 @@
1
+ // src/store.ts
2
+ function getSlackStore(store) {
3
+ return {
4
+ teams: store.collection("slack.teams", ["team_id"]),
5
+ users: store.collection("slack.users", ["user_id", "email"]),
6
+ channels: store.collection("slack.channels", ["channel_id", "name"]),
7
+ messages: store.collection("slack.messages", ["ts", "channel_id"]),
8
+ bots: store.collection("slack.bots", ["bot_id"]),
9
+ oauthApps: store.collection("slack.oauth_apps", ["client_id"]),
10
+ incomingWebhooks: store.collection("slack.incoming_webhooks", ["token"])
11
+ };
12
+ }
13
+
14
+ // src/helpers.ts
15
+ import { randomBytes } from "crypto";
16
+ var tsCounter = 0;
17
+ function generateSlackId(prefix) {
18
+ return prefix + randomBytes(5).toString("hex").toUpperCase().slice(0, 9);
19
+ }
20
+ function generateTs() {
21
+ const now = Math.floor(Date.now() / 1e3);
22
+ tsCounter++;
23
+ return `${now}.${String(tsCounter).padStart(6, "0")}`;
24
+ }
25
+ function slackOk(c, data) {
26
+ return c.json({ ok: true, ...data });
27
+ }
28
+ function slackError(c, error, status = 200) {
29
+ return c.json({ ok: false, error }, status);
30
+ }
31
+ async function parseSlackBody(c) {
32
+ const contentType = c.req.header("Content-Type") ?? "";
33
+ const rawText = await c.req.text();
34
+ if (contentType.includes("application/json")) {
35
+ try {
36
+ return JSON.parse(rawText);
37
+ } catch {
38
+ return {};
39
+ }
40
+ }
41
+ const params = new URLSearchParams(rawText);
42
+ const result = {};
43
+ for (const [key, value] of params) {
44
+ result[key] = value;
45
+ }
46
+ return result;
47
+ }
48
+
49
+ // src/routes/auth.ts
50
+ function authRoutes(ctx) {
51
+ const { app, store } = ctx;
52
+ const ss = () => getSlackStore(store);
53
+ app.post("/api/auth.test", (c) => {
54
+ const authUser = c.get("authUser");
55
+ if (!authUser) {
56
+ return slackError(c, "not_authed");
57
+ }
58
+ const user = ss().users.findOneBy("user_id", authUser.login) ?? ss().users.all().find((u) => u.name === authUser.login);
59
+ if (!user) {
60
+ return slackError(c, "invalid_auth");
61
+ }
62
+ const team = ss().teams.all()[0];
63
+ return slackOk(c, {
64
+ url: `https://${team?.domain ?? "emulate"}.slack.com/`,
65
+ team: team?.name ?? "Emulate",
66
+ user: user.name,
67
+ team_id: team?.team_id ?? "T000000001",
68
+ user_id: user.user_id,
69
+ bot_id: user.is_bot ? user.user_id : void 0
70
+ });
71
+ });
72
+ }
73
+
74
+ // src/routes/chat.ts
75
+ function chatRoutes(ctx) {
76
+ const { app, store, webhooks } = ctx;
77
+ const ss = () => getSlackStore(store);
78
+ app.post("/api/chat.postMessage", async (c) => {
79
+ const authUser = c.get("authUser");
80
+ if (!authUser) return slackError(c, "not_authed");
81
+ const body = await parseSlackBody(c);
82
+ const channel = typeof body.channel === "string" ? body.channel : "";
83
+ const text = typeof body.text === "string" ? body.text : "";
84
+ const thread_ts = typeof body.thread_ts === "string" ? body.thread_ts : void 0;
85
+ if (!channel) return slackError(c, "channel_not_found");
86
+ const ch = ss().channels.findOneBy("channel_id", channel) ?? ss().channels.findOneBy("name", channel);
87
+ if (!ch) return slackError(c, "channel_not_found");
88
+ const ts = generateTs();
89
+ const msg = ss().messages.insert({
90
+ ts,
91
+ channel_id: ch.channel_id,
92
+ user: authUser.login,
93
+ text,
94
+ type: "message",
95
+ thread_ts,
96
+ reply_count: 0,
97
+ reply_users: [],
98
+ reactions: []
99
+ });
100
+ if (thread_ts) {
101
+ const parent = ss().messages.all().find((m) => m.ts === thread_ts && m.channel_id === ch.channel_id);
102
+ if (parent) {
103
+ const replyUsers = parent.reply_users.includes(authUser.login) ? parent.reply_users : [...parent.reply_users, authUser.login];
104
+ ss().messages.update(parent.id, {
105
+ reply_count: parent.reply_count + 1,
106
+ reply_users: replyUsers
107
+ });
108
+ }
109
+ }
110
+ await webhooks.dispatch("message", {
111
+ type: "event_callback",
112
+ event: {
113
+ type: "message",
114
+ channel: ch.channel_id,
115
+ user: authUser.login,
116
+ text,
117
+ ts,
118
+ thread_ts
119
+ }
120
+ });
121
+ return slackOk(c, {
122
+ channel: ch.channel_id,
123
+ ts,
124
+ message: {
125
+ text: msg.text,
126
+ user: msg.user,
127
+ type: msg.type,
128
+ ts: msg.ts,
129
+ thread_ts: msg.thread_ts
130
+ }
131
+ });
132
+ });
133
+ app.post("/api/chat.update", async (c) => {
134
+ const authUser = c.get("authUser");
135
+ if (!authUser) return slackError(c, "not_authed");
136
+ const body = await parseSlackBody(c);
137
+ const channel = typeof body.channel === "string" ? body.channel : "";
138
+ const ts = typeof body.ts === "string" ? body.ts : "";
139
+ const text = typeof body.text === "string" ? body.text : "";
140
+ if (!channel || !ts) return slackError(c, "message_not_found");
141
+ const msg = ss().messages.all().find((m) => m.ts === ts && m.channel_id === channel);
142
+ if (!msg) return slackError(c, "message_not_found");
143
+ ss().messages.update(msg.id, { text });
144
+ return slackOk(c, {
145
+ channel,
146
+ ts,
147
+ text
148
+ });
149
+ });
150
+ app.post("/api/chat.delete", async (c) => {
151
+ const authUser = c.get("authUser");
152
+ if (!authUser) return slackError(c, "not_authed");
153
+ const body = await parseSlackBody(c);
154
+ const channel = typeof body.channel === "string" ? body.channel : "";
155
+ const ts = typeof body.ts === "string" ? body.ts : "";
156
+ if (!channel || !ts) return slackError(c, "message_not_found");
157
+ const msg = ss().messages.all().find((m) => m.ts === ts && m.channel_id === channel);
158
+ if (!msg) return slackError(c, "message_not_found");
159
+ ss().messages.delete(msg.id);
160
+ return slackOk(c, { channel, ts });
161
+ });
162
+ app.post("/api/chat.meMessage", async (c) => {
163
+ const authUser = c.get("authUser");
164
+ if (!authUser) return slackError(c, "not_authed");
165
+ const body = await parseSlackBody(c);
166
+ const channel = typeof body.channel === "string" ? body.channel : "";
167
+ const text = typeof body.text === "string" ? body.text : "";
168
+ if (!channel) return slackError(c, "channel_not_found");
169
+ const ch = ss().channels.findOneBy("channel_id", channel) ?? ss().channels.findOneBy("name", channel);
170
+ if (!ch) return slackError(c, "channel_not_found");
171
+ const ts = generateTs();
172
+ ss().messages.insert({
173
+ ts,
174
+ channel_id: ch.channel_id,
175
+ user: authUser.login,
176
+ text,
177
+ type: "message",
178
+ subtype: "me_message",
179
+ reply_count: 0,
180
+ reply_users: [],
181
+ reactions: []
182
+ });
183
+ return slackOk(c, { channel: ch.channel_id, ts });
184
+ });
185
+ }
186
+
187
+ // src/routes/conversations.ts
188
+ function conversationsRoutes(ctx) {
189
+ const { app, store } = ctx;
190
+ const ss = () => getSlackStore(store);
191
+ app.post("/api/conversations.list", async (c) => {
192
+ const authUser = c.get("authUser");
193
+ if (!authUser) return slackError(c, "not_authed");
194
+ const body = await parseSlackBody(c);
195
+ const limit = Math.min(Number(body.limit) || 100, 1e3);
196
+ const cursor = typeof body.cursor === "string" ? body.cursor : "";
197
+ const allChannels = ss().channels.all().filter((ch) => !ch.is_archived);
198
+ let startIndex = 0;
199
+ if (cursor) {
200
+ const idx = allChannels.findIndex((ch) => ch.channel_id === cursor);
201
+ if (idx >= 0) startIndex = idx;
202
+ }
203
+ const page = allChannels.slice(startIndex, startIndex + limit);
204
+ const nextCursor = startIndex + limit < allChannels.length ? allChannels[startIndex + limit].channel_id : "";
205
+ return slackOk(c, {
206
+ channels: page.map(formatChannel),
207
+ response_metadata: { next_cursor: nextCursor }
208
+ });
209
+ });
210
+ app.post("/api/conversations.info", async (c) => {
211
+ const authUser = c.get("authUser");
212
+ if (!authUser) return slackError(c, "not_authed");
213
+ const body = await parseSlackBody(c);
214
+ const channel = typeof body.channel === "string" ? body.channel : "";
215
+ const ch = ss().channels.findOneBy("channel_id", channel);
216
+ if (!ch) return slackError(c, "channel_not_found");
217
+ return slackOk(c, { channel: formatChannel(ch) });
218
+ });
219
+ app.post("/api/conversations.create", async (c) => {
220
+ const authUser = c.get("authUser");
221
+ if (!authUser) return slackError(c, "not_authed");
222
+ const body = await parseSlackBody(c);
223
+ const name = typeof body.name === "string" ? body.name : "";
224
+ const isPrivate = body.is_private === true || body.is_private === "true";
225
+ if (!name) return slackError(c, "invalid_name_specials");
226
+ const existing = ss().channels.findOneBy("name", name);
227
+ if (existing) return slackError(c, "name_taken");
228
+ const team = ss().teams.all()[0];
229
+ const channelId = generateSlackId("C");
230
+ const now = Math.floor(Date.now() / 1e3);
231
+ const ch = ss().channels.insert({
232
+ channel_id: channelId,
233
+ team_id: team?.team_id ?? "T000000001",
234
+ name,
235
+ is_channel: !isPrivate,
236
+ is_private: isPrivate,
237
+ is_archived: false,
238
+ topic: { value: "", creator: "", last_set: 0 },
239
+ purpose: { value: "", creator: authUser.login, last_set: now },
240
+ members: [authUser.login],
241
+ creator: authUser.login,
242
+ num_members: 1
243
+ });
244
+ return slackOk(c, { channel: formatChannel(ch) });
245
+ });
246
+ app.post("/api/conversations.history", async (c) => {
247
+ const authUser = c.get("authUser");
248
+ if (!authUser) return slackError(c, "not_authed");
249
+ const body = await parseSlackBody(c);
250
+ const channel = typeof body.channel === "string" ? body.channel : "";
251
+ const limit = Math.min(Number(body.limit) || 100, 1e3);
252
+ const cursor = typeof body.cursor === "string" ? body.cursor : "";
253
+ if (!channel) return slackError(c, "channel_not_found");
254
+ const ch = ss().channels.findOneBy("channel_id", channel);
255
+ if (!ch) return slackError(c, "channel_not_found");
256
+ const allMessages = ss().messages.findBy("channel_id", channel).filter((m) => !m.thread_ts || m.thread_ts === m.ts).sort((a, b) => b.ts > a.ts ? 1 : -1);
257
+ let startIndex = 0;
258
+ if (cursor) {
259
+ const idx = allMessages.findIndex((m) => m.ts === cursor);
260
+ if (idx >= 0) startIndex = idx;
261
+ }
262
+ const page = allMessages.slice(startIndex, startIndex + limit);
263
+ const hasMore = startIndex + limit < allMessages.length;
264
+ const nextCursor = hasMore ? allMessages[startIndex + limit].ts : "";
265
+ return slackOk(c, {
266
+ messages: page.map(formatMessage),
267
+ has_more: hasMore,
268
+ response_metadata: { next_cursor: nextCursor }
269
+ });
270
+ });
271
+ app.post("/api/conversations.replies", async (c) => {
272
+ const authUser = c.get("authUser");
273
+ if (!authUser) return slackError(c, "not_authed");
274
+ const body = await parseSlackBody(c);
275
+ const channel = typeof body.channel === "string" ? body.channel : "";
276
+ const ts = typeof body.ts === "string" ? body.ts : "";
277
+ if (!channel || !ts) return slackError(c, "channel_not_found");
278
+ const allMessages = ss().messages.findBy("channel_id", channel).filter((m) => m.ts === ts || m.thread_ts === ts).sort((a, b) => a.ts > b.ts ? 1 : -1);
279
+ return slackOk(c, {
280
+ messages: allMessages.map(formatMessage),
281
+ has_more: false
282
+ });
283
+ });
284
+ app.post("/api/conversations.join", async (c) => {
285
+ const authUser = c.get("authUser");
286
+ if (!authUser) return slackError(c, "not_authed");
287
+ const body = await parseSlackBody(c);
288
+ const channel = typeof body.channel === "string" ? body.channel : "";
289
+ const ch = ss().channels.findOneBy("channel_id", channel);
290
+ if (!ch) return slackError(c, "channel_not_found");
291
+ if (!ch.members.includes(authUser.login)) {
292
+ ss().channels.update(ch.id, {
293
+ members: [...ch.members, authUser.login],
294
+ num_members: ch.num_members + 1
295
+ });
296
+ }
297
+ const updated = ss().channels.findOneBy("channel_id", channel);
298
+ return slackOk(c, { channel: formatChannel(updated) });
299
+ });
300
+ app.post("/api/conversations.leave", async (c) => {
301
+ const authUser = c.get("authUser");
302
+ if (!authUser) return slackError(c, "not_authed");
303
+ const body = await parseSlackBody(c);
304
+ const channel = typeof body.channel === "string" ? body.channel : "";
305
+ const ch = ss().channels.findOneBy("channel_id", channel);
306
+ if (!ch) return slackError(c, "channel_not_found");
307
+ if (ch.members.includes(authUser.login)) {
308
+ ss().channels.update(ch.id, {
309
+ members: ch.members.filter((m) => m !== authUser.login),
310
+ num_members: Math.max(0, ch.num_members - 1)
311
+ });
312
+ }
313
+ return slackOk(c, {});
314
+ });
315
+ app.post("/api/conversations.members", async (c) => {
316
+ const authUser = c.get("authUser");
317
+ if (!authUser) return slackError(c, "not_authed");
318
+ const body = await parseSlackBody(c);
319
+ const channel = typeof body.channel === "string" ? body.channel : "";
320
+ const ch = ss().channels.findOneBy("channel_id", channel);
321
+ if (!ch) return slackError(c, "channel_not_found");
322
+ return slackOk(c, {
323
+ members: ch.members,
324
+ response_metadata: { next_cursor: "" }
325
+ });
326
+ });
327
+ }
328
+ function formatChannel(ch) {
329
+ return {
330
+ id: ch.channel_id,
331
+ name: ch.name,
332
+ is_channel: ch.is_channel,
333
+ is_private: ch.is_private,
334
+ is_archived: ch.is_archived,
335
+ topic: ch.topic,
336
+ purpose: ch.purpose,
337
+ creator: ch.creator,
338
+ num_members: ch.num_members,
339
+ created: Math.floor(new Date(ch.created_at).getTime() / 1e3)
340
+ };
341
+ }
342
+ function formatMessage(msg) {
343
+ return {
344
+ type: msg.type,
345
+ user: msg.user,
346
+ text: msg.text,
347
+ ts: msg.ts,
348
+ ...msg.subtype ? { subtype: msg.subtype } : {},
349
+ ...msg.thread_ts ? { thread_ts: msg.thread_ts } : {},
350
+ ...msg.reply_count > 0 ? { reply_count: msg.reply_count, reply_users: msg.reply_users } : {},
351
+ ...msg.reactions.length > 0 ? { reactions: msg.reactions } : {}
352
+ };
353
+ }
354
+
355
+ // src/routes/users.ts
356
+ function usersRoutes(ctx) {
357
+ const { app, store } = ctx;
358
+ const ss = () => getSlackStore(store);
359
+ app.post("/api/users.list", async (c) => {
360
+ const authUser = c.get("authUser");
361
+ if (!authUser) return slackError(c, "not_authed");
362
+ const body = await parseSlackBody(c);
363
+ const limit = Math.min(Number(body.limit) || 100, 1e3);
364
+ const cursor = typeof body.cursor === "string" ? body.cursor : "";
365
+ const allUsers = ss().users.all().filter((u) => !u.deleted);
366
+ let startIndex = 0;
367
+ if (cursor) {
368
+ const idx = allUsers.findIndex((u) => u.user_id === cursor);
369
+ if (idx >= 0) startIndex = idx;
370
+ }
371
+ const page = allUsers.slice(startIndex, startIndex + limit);
372
+ const nextCursor = startIndex + limit < allUsers.length ? allUsers[startIndex + limit].user_id : "";
373
+ return slackOk(c, {
374
+ members: page.map(formatUser),
375
+ response_metadata: { next_cursor: nextCursor }
376
+ });
377
+ });
378
+ app.post("/api/users.info", async (c) => {
379
+ const authUser = c.get("authUser");
380
+ if (!authUser) return slackError(c, "not_authed");
381
+ const body = await parseSlackBody(c);
382
+ const userId = typeof body.user === "string" ? body.user : "";
383
+ const user = ss().users.findOneBy("user_id", userId);
384
+ if (!user) return slackError(c, "user_not_found");
385
+ return slackOk(c, { user: formatUser(user) });
386
+ });
387
+ app.post("/api/users.lookupByEmail", async (c) => {
388
+ const authUser = c.get("authUser");
389
+ if (!authUser) return slackError(c, "not_authed");
390
+ const body = await parseSlackBody(c);
391
+ const email = typeof body.email === "string" ? body.email : "";
392
+ if (!email) return slackError(c, "users_not_found");
393
+ const user = ss().users.findOneBy("email", email);
394
+ if (!user) return slackError(c, "users_not_found");
395
+ return slackOk(c, { user: formatUser(user) });
396
+ });
397
+ }
398
+ function formatUser(u) {
399
+ return {
400
+ id: u.user_id,
401
+ team_id: u.team_id,
402
+ name: u.name,
403
+ real_name: u.real_name,
404
+ is_admin: u.is_admin,
405
+ is_bot: u.is_bot,
406
+ deleted: u.deleted,
407
+ profile: u.profile
408
+ };
409
+ }
410
+
411
+ // src/routes/reactions.ts
412
+ function reactionsRoutes(ctx) {
413
+ const { app, store, webhooks } = ctx;
414
+ const ss = () => getSlackStore(store);
415
+ app.post("/api/reactions.add", async (c) => {
416
+ const authUser = c.get("authUser");
417
+ if (!authUser) return slackError(c, "not_authed");
418
+ const body = await parseSlackBody(c);
419
+ const channel = typeof body.channel === "string" ? body.channel : "";
420
+ const timestamp = typeof body.timestamp === "string" ? body.timestamp : "";
421
+ const name = typeof body.name === "string" ? body.name : "";
422
+ if (!name) return slackError(c, "invalid_name");
423
+ const msg = ss().messages.all().find((m) => m.ts === timestamp && m.channel_id === channel);
424
+ if (!msg) return slackError(c, "message_not_found");
425
+ const reactions = [...msg.reactions];
426
+ const existing = reactions.find((r) => r.name === name);
427
+ if (existing) {
428
+ if (existing.users.includes(authUser.login)) {
429
+ return slackError(c, "already_reacted");
430
+ }
431
+ existing.users.push(authUser.login);
432
+ existing.count++;
433
+ } else {
434
+ reactions.push({ name, users: [authUser.login], count: 1 });
435
+ }
436
+ ss().messages.update(msg.id, { reactions });
437
+ await webhooks.dispatch("reaction_added", {
438
+ type: "event_callback",
439
+ event: {
440
+ type: "reaction_added",
441
+ user: authUser.login,
442
+ reaction: name,
443
+ item: { type: "message", channel, ts: timestamp }
444
+ }
445
+ });
446
+ return slackOk(c, {});
447
+ });
448
+ app.post("/api/reactions.remove", async (c) => {
449
+ const authUser = c.get("authUser");
450
+ if (!authUser) return slackError(c, "not_authed");
451
+ const body = await parseSlackBody(c);
452
+ const channel = typeof body.channel === "string" ? body.channel : "";
453
+ const timestamp = typeof body.timestamp === "string" ? body.timestamp : "";
454
+ const name = typeof body.name === "string" ? body.name : "";
455
+ if (!name) return slackError(c, "invalid_name");
456
+ const msg = ss().messages.all().find((m) => m.ts === timestamp && m.channel_id === channel);
457
+ if (!msg) return slackError(c, "message_not_found");
458
+ const reactions = [...msg.reactions];
459
+ const existing = reactions.find((r) => r.name === name);
460
+ if (!existing || !existing.users.includes(authUser.login)) {
461
+ return slackError(c, "no_reaction");
462
+ }
463
+ existing.users = existing.users.filter((u) => u !== authUser.login);
464
+ existing.count--;
465
+ const filtered = reactions.filter((r) => r.count > 0);
466
+ ss().messages.update(msg.id, { reactions: filtered });
467
+ await webhooks.dispatch("reaction_removed", {
468
+ type: "event_callback",
469
+ event: {
470
+ type: "reaction_removed",
471
+ user: authUser.login,
472
+ reaction: name,
473
+ item: { type: "message", channel, ts: timestamp }
474
+ }
475
+ });
476
+ return slackOk(c, {});
477
+ });
478
+ app.post("/api/reactions.get", async (c) => {
479
+ const authUser = c.get("authUser");
480
+ if (!authUser) return slackError(c, "not_authed");
481
+ const body = await parseSlackBody(c);
482
+ const channel = typeof body.channel === "string" ? body.channel : "";
483
+ const timestamp = typeof body.timestamp === "string" ? body.timestamp : "";
484
+ const msg = ss().messages.all().find((m) => m.ts === timestamp && m.channel_id === channel);
485
+ if (!msg) return slackError(c, "message_not_found");
486
+ return slackOk(c, {
487
+ type: "message",
488
+ message: {
489
+ type: msg.type,
490
+ text: msg.text,
491
+ ts: msg.ts,
492
+ reactions: msg.reactions
493
+ }
494
+ });
495
+ });
496
+ }
497
+
498
+ // src/routes/team.ts
499
+ function teamRoutes(ctx) {
500
+ const { app, store } = ctx;
501
+ const ss = () => getSlackStore(store);
502
+ app.post("/api/team.info", (c) => {
503
+ const authUser = c.get("authUser");
504
+ if (!authUser) return slackError(c, "not_authed");
505
+ const team = ss().teams.all()[0];
506
+ if (!team) return slackError(c, "team_not_found");
507
+ return slackOk(c, {
508
+ team: {
509
+ id: team.team_id,
510
+ name: team.name,
511
+ domain: team.domain
512
+ }
513
+ });
514
+ });
515
+ app.post("/api/bots.info", async (c) => {
516
+ const authUser = c.get("authUser");
517
+ if (!authUser) return slackError(c, "not_authed");
518
+ const body = await parseSlackBody(c);
519
+ const botId = typeof body.bot === "string" ? body.bot : "";
520
+ const bot = ss().bots.findOneBy("bot_id", botId);
521
+ if (!bot) return slackError(c, "bot_not_found");
522
+ return slackOk(c, {
523
+ bot: {
524
+ id: bot.bot_id,
525
+ name: bot.name,
526
+ deleted: bot.deleted,
527
+ icons: bot.icons
528
+ }
529
+ });
530
+ });
531
+ }
532
+
533
+ // src/routes/oauth.ts
534
+ import { randomBytes as randomBytes2 } from "crypto";
535
+
536
+ // ../core/dist/index.js
537
+ import { Hono } from "hono";
538
+ import { cors } from "hono/cors";
539
+ import { readFileSync } from "fs";
540
+ import { fileURLToPath } from "url";
541
+ import { dirname, join } from "path";
542
+ import { timingSafeEqual } from "crypto";
543
+ function createErrorHandler(documentationUrl) {
544
+ return async (c, next) => {
545
+ if (documentationUrl) {
546
+ c.set("docsUrl", documentationUrl);
547
+ }
548
+ await next();
549
+ };
550
+ }
551
+ var errorHandler = createErrorHandler();
552
+ var isDebug = typeof process !== "undefined" && (process.env.DEBUG === "1" || process.env.DEBUG === "true" || process.env.EMULATE_DEBUG === "1");
553
+ function debug(label, ...args) {
554
+ if (isDebug) {
555
+ console.log(`[${label}]`, ...args);
556
+ }
557
+ }
558
+ var __dirname = dirname(fileURLToPath(import.meta.url));
559
+ var FONTS = {
560
+ "geist-sans.woff2": readFileSync(join(__dirname, "fonts", "geist-sans.woff2")),
561
+ "GeistPixel-Square.woff2": readFileSync(join(__dirname, "fonts", "GeistPixel-Square.woff2"))
562
+ };
563
+ function escapeHtml(s) {
564
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
565
+ }
566
+ function escapeAttr(s) {
567
+ return escapeHtml(s).replace(/'/g, "&#39;");
568
+ }
569
+ var CSS = `
570
+ @font-face{
571
+ font-family:'Geist';font-style:normal;font-weight:100 900;font-display:swap;
572
+ src:url('/_emulate/fonts/geist-sans.woff2') format('woff2');
573
+ }
574
+ @font-face{
575
+ font-family:'Geist Pixel';font-style:normal;font-weight:400;font-display:swap;
576
+ src:url('/_emulate/fonts/GeistPixel-Square.woff2') format('woff2');
577
+ }
578
+ *{box-sizing:border-box;margin:0;padding:0}
579
+ body{
580
+ font-family:'Geist',-apple-system,BlinkMacSystemFont,sans-serif;
581
+ background:#000;color:#33ff00;min-height:100vh;
582
+ -webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;
583
+ }
584
+ .emu-bar{
585
+ border-bottom:1px solid #0a3300;padding:10px 20px;
586
+ display:flex;align-items:center;gap:10px;font-size:.8125rem;color:#1a8c00;
587
+ }
588
+ .emu-bar-title{font-weight:600;color:#33ff00;font-family:'Geist Pixel',monospace;}
589
+ .emu-bar-links{margin-left:auto;display:flex;gap:16px;}
590
+ .emu-bar-links a{
591
+ color:#1a8c00;font-size:.75rem;text-decoration:none;transition:color .15s;
592
+ }
593
+ .emu-bar-links a:hover{color:#33ff00;}
594
+ .emu-bar-links a .full{display:inline;}
595
+ .emu-bar-links a .short{display:none;}
596
+ @media(max-width:600px){
597
+ .emu-bar-links a .full{display:none;}
598
+ .emu-bar-links a .short{display:inline;}
599
+ }
600
+
601
+ .content{
602
+ display:flex;align-items:center;justify-content:center;
603
+ min-height:calc(100vh - 42px);padding:24px 16px;
604
+ }
605
+ .content-inner{width:100%;max-width:420px;}
606
+ .card-title{
607
+ font-family:'Geist Pixel',monospace;
608
+ font-size:1.125rem;font-weight:600;margin-bottom:4px;color:#33ff00;
609
+ }
610
+ .card-subtitle{color:#1a8c00;font-size:.8125rem;margin-bottom:18px;line-height:1.45;}
611
+ .powered-by{
612
+ position:fixed;bottom:0;left:0;right:0;
613
+ text-align:center;padding:12px;font-size:.6875rem;color:#0a3300;
614
+ font-family:'Geist Pixel',monospace;
615
+ }
616
+ .powered-by a{color:#1a8c00;text-decoration:none;transition:color .15s;}
617
+ .powered-by a:hover{color:#33ff00;}
618
+
619
+ .error-title{
620
+ font-family:'Geist Pixel',monospace;
621
+ color:#ff4444;font-size:1.125rem;font-weight:600;margin-bottom:8px;
622
+ }
623
+ .error-msg{color:#1a8c00;font-size:.875rem;line-height:1.5;}
624
+ .error-card{text-align:center;}
625
+
626
+ .user-form{margin-bottom:8px;}
627
+ .user-form:last-of-type{margin-bottom:0;}
628
+ .user-btn{
629
+ width:100%;display:flex;align-items:center;gap:12px;
630
+ padding:10px 12px;border:1px solid #0a3300;border-radius:8px;
631
+ background:#000;color:inherit;cursor:pointer;text-align:left;
632
+ font:inherit;transition:border-color .15s;
633
+ }
634
+ .user-btn:hover{border-color:#33ff00;}
635
+ .avatar{
636
+ width:36px;height:36px;border-radius:50%;
637
+ background:#0a3300;color:#33ff00;font-weight:600;font-size:.875rem;
638
+ display:flex;align-items:center;justify-content:center;flex-shrink:0;
639
+ font-family:'Geist Pixel',monospace;
640
+ }
641
+ .user-text{min-width:0;}
642
+ .user-login{font-weight:600;font-size:.875rem;display:block;color:#33ff00;}
643
+ .user-meta{color:#1a8c00;font-size:.75rem;margin-top:1px;}
644
+ .user-email{font-size:.6875rem;color:#116600;word-break:break-all;margin-top:1px;}
645
+
646
+ .settings-layout{
647
+ max-width:920px;margin:0 auto;padding:28px 20px;
648
+ display:flex;gap:28px;
649
+ }
650
+ .settings-sidebar{width:200px;flex-shrink:0;}
651
+ .settings-sidebar a{
652
+ display:block;padding:6px 10px;border-radius:6px;color:#1a8c00;
653
+ text-decoration:none;font-size:.8125rem;transition:color .15s;
654
+ }
655
+ .settings-sidebar a:hover{color:#33ff00;}
656
+ .settings-sidebar a.active{color:#33ff00;font-weight:600;}
657
+ .settings-main{flex:1;min-width:0;}
658
+
659
+ .s-card{
660
+ padding:18px 0;margin-bottom:14px;border-bottom:1px solid #0a3300;
661
+ }
662
+ .s-card:last-child{border-bottom:none;}
663
+ .s-card-header{display:flex;align-items:center;gap:14px;margin-bottom:14px;}
664
+ .s-icon{
665
+ width:42px;height:42px;border-radius:8px;
666
+ background:#0a3300;display:flex;align-items:center;justify-content:center;
667
+ font-size:1.125rem;font-weight:700;color:#116600;flex-shrink:0;
668
+ font-family:'Geist Pixel',monospace;
669
+ }
670
+ .s-title{
671
+ font-family:'Geist Pixel',monospace;
672
+ font-size:1.25rem;font-weight:600;color:#33ff00;
673
+ }
674
+ .s-subtitle{font-size:.75rem;color:#1a8c00;margin-top:2px;}
675
+ .section-heading{
676
+ font-size:.9375rem;font-weight:600;margin-bottom:10px;color:#33ff00;
677
+ display:flex;align-items:center;justify-content:space-between;
678
+ }
679
+ .perm-list{list-style:none;}
680
+ .perm-list li{padding:5px 0;font-size:.8125rem;display:flex;align-items:center;gap:6px;color:#1a8c00;}
681
+ .check{color:#33ff00;}
682
+ .org-row{
683
+ display:flex;align-items:center;gap:8px;padding:7px 0;
684
+ border-bottom:1px solid #0a3300;font-size:.8125rem;
685
+ }
686
+ .org-row:last-child{border-bottom:none;}
687
+ .org-icon{
688
+ width:22px;height:22px;border-radius:4px;background:#0a3300;
689
+ display:flex;align-items:center;justify-content:center;
690
+ font-size:.625rem;font-weight:700;color:#116600;flex-shrink:0;
691
+ font-family:'Geist Pixel',monospace;
692
+ }
693
+ .org-name{font-weight:600;color:#33ff00;}
694
+ .badge{font-size:.6875rem;padding:1px 7px;border-radius:999px;font-weight:500;}
695
+ .badge-granted{background:#0a3300;color:#33ff00;}
696
+ .badge-denied{background:#1a0a0a;color:#ff4444;}
697
+ .badge-requested{background:#0a3300;color:#1a8c00;}
698
+ .btn-revoke{
699
+ display:inline-block;padding:5px 14px;border-radius:6px;
700
+ border:1px solid #0a3300;background:transparent;color:#ff4444;
701
+ font-size:.75rem;font-weight:600;cursor:pointer;transition:border-color .15s;
702
+ }
703
+ .btn-revoke:hover{border-color:#ff4444;}
704
+ .info-text{color:#1a8c00;font-size:.75rem;line-height:1.5;margin-top:10px;}
705
+ .app-link{
706
+ display:flex;align-items:center;gap:12px;padding:12px;
707
+ border:1px solid #0a3300;border-radius:8px;background:#000;
708
+ text-decoration:none;color:inherit;margin-bottom:8px;transition:border-color .15s;
709
+ }
710
+ .app-link:hover{border-color:#33ff00;}
711
+ .app-link-name{font-weight:600;font-size:.875rem;color:#33ff00;}
712
+ .app-link-scopes{font-size:.6875rem;color:#1a8c00;margin-top:1px;}
713
+ .empty{color:#1a8c00;text-align:center;padding:28px 0;font-size:.875rem;}
714
+ `;
715
+ var POWERED_BY = `<div class="powered-by">Powered by <a href="https://emulate.dev" target="_blank" rel="noopener">emulate</a></div>`;
716
+ function emuBar(service) {
717
+ const title = service ? `${escapeHtml(service)} Emulator` : "Emulator";
718
+ return `<div class="emu-bar">
719
+ <span class="emu-bar-title">${title}</span>
720
+ <nav class="emu-bar-links">
721
+ <a href="https://github.com/vercel-labs/emulate/issues" target="_blank" rel="noopener"><span class="full">Report Issue</span><span class="short">Report</span></a>
722
+ <a href="https://github.com/vercel-labs/emulate" target="_blank" rel="noopener"><span class="full">Source Code</span><span class="short">Source</span></a>
723
+ <a href="https://emulate.dev" target="_blank" rel="noopener"><span class="full">Learn More</span><span class="short">Learn</span></a>
724
+ </nav>
725
+ </div>`;
726
+ }
727
+ function head(title) {
728
+ return `<!DOCTYPE html>
729
+ <html lang="en">
730
+ <head>
731
+ <meta charset="utf-8"/>
732
+ <meta name="viewport" content="width=device-width,initial-scale=1"/>
733
+ <title>${escapeHtml(title)} | emulate</title>
734
+ <style>${CSS}</style>
735
+ </head>`;
736
+ }
737
+ function renderCardPage(title, subtitle, body, service) {
738
+ return `${head(title)}
739
+ <body>
740
+ ${emuBar(service)}
741
+ <div class="content">
742
+ <div class="content-inner">
743
+ <div class="card-title">${escapeHtml(title)}</div>
744
+ <div class="card-subtitle">${subtitle}</div>
745
+ ${body}
746
+ </div>
747
+ </div>
748
+ ${POWERED_BY}
749
+ </body></html>`;
750
+ }
751
+ function renderErrorPage(title, message, service) {
752
+ return `${head(title)}
753
+ <body>
754
+ ${emuBar(service)}
755
+ <div class="content">
756
+ <div class="content-inner error-card">
757
+ <div class="error-title">${escapeHtml(title)}</div>
758
+ <div class="error-msg">${escapeHtml(message)}</div>
759
+ </div>
760
+ </div>
761
+ ${POWERED_BY}
762
+ </body></html>`;
763
+ }
764
+ function renderSettingsPage(title, sidebarHtml, bodyHtml, service) {
765
+ return `${head(title)}
766
+ <body>
767
+ ${emuBar(service)}
768
+ <div class="settings-layout">
769
+ <nav class="settings-sidebar">${sidebarHtml}</nav>
770
+ <div class="settings-main">${bodyHtml}</div>
771
+ </div>
772
+ ${POWERED_BY}
773
+ </body></html>`;
774
+ }
775
+ function renderUserButton(opts) {
776
+ const hiddens = Object.entries(opts.hiddenFields).map(([k, v]) => `<input type="hidden" name="${escapeAttr(k)}" value="${escapeAttr(v)}"/>`).join("");
777
+ const nameLine = opts.name ? `<div class="user-meta">${escapeHtml(opts.name)}</div>` : "";
778
+ const emailLine = opts.email ? `<div class="user-email">${escapeHtml(opts.email)}</div>` : "";
779
+ return `<form class="user-form" method="post" action="${escapeAttr(opts.formAction)}">
780
+ ${hiddens}
781
+ <button type="submit" class="user-btn">
782
+ <span class="avatar">${escapeHtml(opts.letter)}</span>
783
+ <span class="user-text">
784
+ <span class="user-login">${escapeHtml(opts.login)}</span>
785
+ ${nameLine}${emailLine}
786
+ </span>
787
+ </button>
788
+ </form>`;
789
+ }
790
+ function normalizeUri(uri) {
791
+ try {
792
+ const u = new URL(uri);
793
+ return `${u.origin}${u.pathname.replace(/\/+$/, "")}`;
794
+ } catch {
795
+ return uri.replace(/\/+$/, "").split("?")[0];
796
+ }
797
+ }
798
+ function matchesRedirectUri(incoming, registered) {
799
+ const normalized = normalizeUri(incoming);
800
+ return registered.some((r) => normalizeUri(r) === normalized);
801
+ }
802
+ function constantTimeSecretEqual(a, b) {
803
+ const bufA = Buffer.from(a, "utf-8");
804
+ const bufB = Buffer.from(b, "utf-8");
805
+ if (bufA.length !== bufB.length) return false;
806
+ return timingSafeEqual(bufA, bufB);
807
+ }
808
+ function bodyStr(v) {
809
+ if (typeof v === "string") return v;
810
+ if (Array.isArray(v) && typeof v[0] === "string") return v[0];
811
+ return "";
812
+ }
813
+
814
+ // src/routes/oauth.ts
815
+ var PENDING_CODE_TTL_MS = 10 * 60 * 1e3;
816
+ var SERVICE_LABEL = "Slack";
817
+ function getPendingCodes(store) {
818
+ let map = store.getData("slack.oauth.pendingCodes");
819
+ if (!map) {
820
+ map = /* @__PURE__ */ new Map();
821
+ store.setData("slack.oauth.pendingCodes", map);
822
+ }
823
+ return map;
824
+ }
825
+ function isPendingCodeExpired(p) {
826
+ return Date.now() - p.created_at > PENDING_CODE_TTL_MS;
827
+ }
828
+ function oauthRoutes({ app, store, baseUrl, tokenMap }) {
829
+ const ss = () => getSlackStore(store);
830
+ app.get("/oauth/v2/authorize", (c) => {
831
+ const client_id = c.req.query("client_id") ?? "";
832
+ const redirect_uri = c.req.query("redirect_uri") ?? "";
833
+ const scope = c.req.query("scope") ?? "";
834
+ const state = c.req.query("state") ?? "";
835
+ const appsConfigured = ss().oauthApps.all().length > 0;
836
+ let appName = "";
837
+ if (appsConfigured) {
838
+ const oauthApp = ss().oauthApps.findOneBy("client_id", client_id);
839
+ if (!oauthApp) {
840
+ return c.html(
841
+ renderErrorPage("Application not found", `The client_id '${client_id}' is not registered.`, SERVICE_LABEL),
842
+ 400
843
+ );
844
+ }
845
+ if (redirect_uri && !matchesRedirectUri(redirect_uri, oauthApp.redirect_uris)) {
846
+ return c.html(
847
+ renderErrorPage("Redirect URI mismatch", "The redirect_uri is not registered for this application.", SERVICE_LABEL),
848
+ 400
849
+ );
850
+ }
851
+ appName = oauthApp.name;
852
+ }
853
+ const subtitleText = appName ? `Authorize <strong>${escapeHtml(appName)}</strong> to access your Slack workspace.` : "Choose a user to authorize.";
854
+ const users = ss().users.all().filter((u) => !u.deleted && !u.is_bot);
855
+ const userButtons = users.map((user) => {
856
+ return renderUserButton({
857
+ letter: (user.name[0] ?? "?").toUpperCase(),
858
+ login: user.name,
859
+ name: user.real_name,
860
+ email: user.email,
861
+ formAction: "/oauth/v2/authorize/callback",
862
+ hiddenFields: {
863
+ user_id: user.user_id,
864
+ redirect_uri,
865
+ scope,
866
+ state,
867
+ client_id
868
+ }
869
+ });
870
+ }).join("\n");
871
+ const body = users.length === 0 ? '<p class="empty">No users in the emulator store.</p>' : userButtons;
872
+ return c.html(renderCardPage("Sign in to Slack", subtitleText, body, SERVICE_LABEL));
873
+ });
874
+ app.post("/oauth/v2/authorize/callback", async (c) => {
875
+ const body = await c.req.parseBody();
876
+ const userId = bodyStr(body.user_id);
877
+ const redirect_uri = bodyStr(body.redirect_uri);
878
+ const scope = bodyStr(body.scope);
879
+ const state = bodyStr(body.state);
880
+ const client_id = bodyStr(body.client_id);
881
+ const code = randomBytes2(20).toString("hex");
882
+ getPendingCodes(store).set(code, {
883
+ userId,
884
+ scope,
885
+ redirectUri: redirect_uri,
886
+ clientId: client_id,
887
+ created_at: Date.now()
888
+ });
889
+ debug("slack.oauth", `[Slack callback] code=${code.slice(0, 8)}... user=${userId}`);
890
+ const url = new URL(redirect_uri);
891
+ url.searchParams.set("code", code);
892
+ if (state) url.searchParams.set("state", state);
893
+ return c.redirect(url.toString(), 302);
894
+ });
895
+ app.post("/api/oauth.v2.access", async (c) => {
896
+ const contentType = c.req.header("Content-Type") ?? "";
897
+ const rawText = await c.req.text();
898
+ let body;
899
+ if (contentType.includes("application/json")) {
900
+ try {
901
+ body = JSON.parse(rawText);
902
+ } catch {
903
+ body = {};
904
+ }
905
+ } else {
906
+ body = Object.fromEntries(new URLSearchParams(rawText));
907
+ }
908
+ const code = typeof body.code === "string" ? body.code : "";
909
+ const client_id = typeof body.client_id === "string" ? body.client_id : "";
910
+ const client_secret = typeof body.client_secret === "string" ? body.client_secret : "";
911
+ const appsConfigured = ss().oauthApps.all().length > 0;
912
+ if (appsConfigured) {
913
+ const oauthApp = ss().oauthApps.findOneBy("client_id", client_id);
914
+ if (!oauthApp) {
915
+ return c.json({ ok: false, error: "invalid_client_id" });
916
+ }
917
+ if (!constantTimeSecretEqual(client_secret, oauthApp.client_secret)) {
918
+ return c.json({ ok: false, error: "invalid_client_id" });
919
+ }
920
+ }
921
+ const pendingMap = getPendingCodes(store);
922
+ const pending = pendingMap.get(code);
923
+ if (!pending) {
924
+ return c.json({ ok: false, error: "invalid_code" });
925
+ }
926
+ if (isPendingCodeExpired(pending)) {
927
+ pendingMap.delete(code);
928
+ return c.json({ ok: false, error: "invalid_code" });
929
+ }
930
+ pendingMap.delete(code);
931
+ const user = ss().users.findOneBy("user_id", pending.userId);
932
+ if (!user) {
933
+ return c.json({ ok: false, error: "invalid_code" });
934
+ }
935
+ const accessToken = "xoxb-" + randomBytes2(20).toString("base64url");
936
+ const team = ss().teams.all()[0];
937
+ if (tokenMap) {
938
+ const scopes = pending.scope ? pending.scope.split(/[,\s]+/).filter(Boolean) : [];
939
+ tokenMap.set(accessToken, { login: user.user_id, id: user.id, scopes });
940
+ }
941
+ debug("slack.oauth", `[Slack token] issued token for ${user.name}`);
942
+ return c.json({
943
+ ok: true,
944
+ access_token: accessToken,
945
+ token_type: "bot",
946
+ scope: pending.scope || "chat:write,channels:read",
947
+ bot_user_id: user.user_id,
948
+ app_id: client_id,
949
+ team: {
950
+ id: team?.team_id ?? "T000000001",
951
+ name: team?.name ?? "Emulate"
952
+ },
953
+ authed_user: {
954
+ id: user.user_id
955
+ }
956
+ });
957
+ });
958
+ }
959
+
960
+ // src/routes/webhooks.ts
961
+ function webhookRoutes(ctx) {
962
+ const { app, store, webhooks } = ctx;
963
+ const ss = () => getSlackStore(store);
964
+ app.post("/services/:teamId/:botId/:token", async (c) => {
965
+ const contentType = c.req.header("Content-Type") ?? "";
966
+ const rawText = await c.req.text();
967
+ let body;
968
+ if (contentType.includes("application/json")) {
969
+ try {
970
+ body = JSON.parse(rawText);
971
+ } catch {
972
+ return c.text("invalid_payload", 400);
973
+ }
974
+ } else {
975
+ const params = new URLSearchParams(rawText);
976
+ const payload = params.get("payload");
977
+ if (payload) {
978
+ try {
979
+ body = JSON.parse(payload);
980
+ } catch {
981
+ return c.text("invalid_payload", 400);
982
+ }
983
+ } else {
984
+ body = {};
985
+ }
986
+ }
987
+ const text = typeof body.text === "string" ? body.text : "";
988
+ const channelName = typeof body.channel === "string" ? body.channel : "";
989
+ const threadTs = typeof body.thread_ts === "string" ? body.thread_ts : void 0;
990
+ if (!text && !body.blocks && !body.attachments) {
991
+ return c.text("no_text", 400);
992
+ }
993
+ const webhook = ss().incomingWebhooks.all().find(
994
+ (w) => w.token === c.req.param("token")
995
+ );
996
+ let targetChannel = channelName ? ss().channels.findOneBy("name", channelName) ?? ss().channels.findOneBy("channel_id", channelName) : null;
997
+ if (!targetChannel && webhook) {
998
+ targetChannel = ss().channels.findOneBy("name", webhook.default_channel) ?? ss().channels.findOneBy("channel_id", webhook.default_channel);
999
+ }
1000
+ if (!targetChannel) {
1001
+ targetChannel = ss().channels.findOneBy("name", "general");
1002
+ }
1003
+ if (!targetChannel) {
1004
+ return c.text("channel_not_found", 404);
1005
+ }
1006
+ const ts = generateTs();
1007
+ const botId = c.req.param("botId");
1008
+ ss().messages.insert({
1009
+ ts,
1010
+ channel_id: targetChannel.channel_id,
1011
+ user: botId,
1012
+ text: text || "(rich message)",
1013
+ type: "message",
1014
+ subtype: "bot_message",
1015
+ thread_ts: threadTs,
1016
+ reply_count: 0,
1017
+ reply_users: [],
1018
+ reactions: []
1019
+ });
1020
+ await webhooks.dispatch("message", {
1021
+ type: "event_callback",
1022
+ event: {
1023
+ type: "message",
1024
+ subtype: "bot_message",
1025
+ channel: targetChannel.channel_id,
1026
+ bot_id: botId,
1027
+ text: text || "(rich message)",
1028
+ ts,
1029
+ thread_ts: threadTs
1030
+ }
1031
+ });
1032
+ return c.text("ok");
1033
+ });
1034
+ }
1035
+
1036
+ // src/routes/inspector.ts
1037
+ var SERVICE_LABEL2 = "Slack";
1038
+ function timeAgo(isoDate) {
1039
+ const seconds = Math.floor((Date.now() - new Date(isoDate).getTime()) / 1e3);
1040
+ if (seconds < 60) return "just now";
1041
+ if (seconds < 3600) return `${Math.floor(seconds / 60)}m ago`;
1042
+ if (seconds < 86400) return `${Math.floor(seconds / 3600)}h ago`;
1043
+ return `${Math.floor(seconds / 86400)}d ago`;
1044
+ }
1045
+ function renderReactions(reactions) {
1046
+ if (!reactions || reactions.length === 0) return "";
1047
+ const badges = reactions.map((r) => `<span class="badge badge-granted">:${escapeHtml(r.name)}: ${r.count}</span>`).join(" ");
1048
+ return `<div style="margin-top:4px">${badges}</div>`;
1049
+ }
1050
+ function renderMessage(msg, users) {
1051
+ const displayName = users.get(msg.user) ?? msg.user;
1052
+ const isBot = msg.subtype === "bot_message";
1053
+ const letter = isBot ? "B" : (displayName[0] ?? "?").toUpperCase();
1054
+ const threadBadge = msg.reply_count > 0 ? ` <span class="badge badge-requested">${msg.reply_count} ${msg.reply_count === 1 ? "reply" : "replies"}</span>` : "";
1055
+ const threadIndicator = msg.thread_ts && msg.thread_ts !== msg.ts ? `<span class="badge badge-denied">thread</span> ` : "";
1056
+ return `<div class="org-row">
1057
+ <span class="org-icon">${escapeHtml(letter)}</span>
1058
+ <span class="org-name">${escapeHtml(displayName)}${isBot ? ' <span class="badge badge-granted">bot</span>' : ""}</span>
1059
+ <span class="user-meta" style="margin-left:auto">${timeAgo(msg.created_at)}</span>
1060
+ </div>
1061
+ <div class="info-text">${threadIndicator}${escapeHtml(msg.text)}${threadBadge}</div>
1062
+ ${renderReactions(msg.reactions)}`;
1063
+ }
1064
+ function renderChannelSidebar(channels, activeId) {
1065
+ return channels.map((ch) => {
1066
+ const active = ch.channel_id === activeId ? ' class="active"' : "";
1067
+ const prefix = ch.is_private ? "\u{1F512} " : "# ";
1068
+ return `<a href="/?channel=${escapeHtml(ch.channel_id)}"${active}>${prefix}${escapeHtml(ch.name)}</a>`;
1069
+ }).join("\n");
1070
+ }
1071
+ function inspectorRoutes(ctx) {
1072
+ const { app, store } = ctx;
1073
+ const ss = () => getSlackStore(store);
1074
+ app.get("/", (c) => {
1075
+ const channels = ss().channels.all().filter((ch) => !ch.is_archived);
1076
+ const team = ss().teams.all()[0];
1077
+ if (channels.length === 0) {
1078
+ return c.html(renderSettingsPage(
1079
+ "Slack Inspector",
1080
+ "<p class='empty'>No channels</p>",
1081
+ "<p class='empty'>No channels in the emulator store.</p>",
1082
+ SERVICE_LABEL2
1083
+ ));
1084
+ }
1085
+ const requestedChannel = c.req.query("channel") ?? "";
1086
+ const activeChannel = channels.find((ch) => ch.channel_id === requestedChannel) ?? channels[0];
1087
+ const userMap = /* @__PURE__ */ new Map();
1088
+ for (const u of ss().users.all()) {
1089
+ userMap.set(u.user_id, u.name);
1090
+ userMap.set(u.name, u.name);
1091
+ }
1092
+ for (const b of ss().bots.all()) {
1093
+ userMap.set(b.bot_id, b.name);
1094
+ }
1095
+ const messages = ss().messages.findBy("channel_id", activeChannel.channel_id).sort((a, b) => b.ts > a.ts ? 1 : -1).slice(0, 50);
1096
+ const sidebar = renderChannelSidebar(channels, activeChannel.channel_id);
1097
+ const messageHtml = messages.length === 0 ? '<p class="empty">No messages yet. Post one with chat.postMessage or an incoming webhook.</p>' : messages.map((m) => renderMessage(m, userMap)).join("\n<div style='height:8px'></div>\n");
1098
+ const stats = `${ss().users.all().length} users, ${channels.length} channels, ${ss().messages.all().length} messages`;
1099
+ const bodyHtml = `
1100
+ <div class="s-card">
1101
+ <div class="s-card-header">
1102
+ <div class="s-icon">#</div>
1103
+ <div>
1104
+ <div class="s-title">${escapeHtml(activeChannel.name)}</div>
1105
+ <div class="s-subtitle">${escapeHtml(activeChannel.topic.value || "No topic set")} - ${activeChannel.num_members} members</div>
1106
+ </div>
1107
+ </div>
1108
+ <div class="section-heading">
1109
+ Messages
1110
+ <span class="user-meta">${stats}</span>
1111
+ </div>
1112
+ ${messageHtml}
1113
+ </div>`;
1114
+ return c.html(renderSettingsPage(
1115
+ `${team?.name ?? "Slack"} - Message Inspector`,
1116
+ sidebar,
1117
+ bodyHtml,
1118
+ SERVICE_LABEL2
1119
+ ));
1120
+ });
1121
+ }
1122
+
1123
+ // src/index.ts
1124
+ function seedDefaults(store, _baseUrl) {
1125
+ const ss = getSlackStore(store);
1126
+ const teamId = "T000000001";
1127
+ ss.teams.insert({
1128
+ team_id: teamId,
1129
+ name: "Emulate",
1130
+ domain: "emulate"
1131
+ });
1132
+ const userId = "U000000001";
1133
+ ss.users.insert({
1134
+ user_id: userId,
1135
+ team_id: teamId,
1136
+ name: "admin",
1137
+ real_name: "Admin User",
1138
+ email: "admin@emulate.dev",
1139
+ is_admin: true,
1140
+ is_bot: false,
1141
+ deleted: false,
1142
+ profile: {
1143
+ display_name: "admin",
1144
+ real_name: "Admin User",
1145
+ email: "admin@emulate.dev",
1146
+ image_48: "",
1147
+ image_192: ""
1148
+ }
1149
+ });
1150
+ ss.channels.insert({
1151
+ channel_id: "C000000001",
1152
+ team_id: teamId,
1153
+ name: "general",
1154
+ is_channel: true,
1155
+ is_private: false,
1156
+ is_archived: false,
1157
+ topic: { value: "General discussion", creator: userId, last_set: Math.floor(Date.now() / 1e3) },
1158
+ purpose: { value: "A place for general discussion", creator: userId, last_set: Math.floor(Date.now() / 1e3) },
1159
+ members: [userId],
1160
+ creator: userId,
1161
+ num_members: 1
1162
+ });
1163
+ ss.channels.insert({
1164
+ channel_id: "C000000002",
1165
+ team_id: teamId,
1166
+ name: "random",
1167
+ is_channel: true,
1168
+ is_private: false,
1169
+ is_archived: false,
1170
+ topic: { value: "Random stuff", creator: userId, last_set: Math.floor(Date.now() / 1e3) },
1171
+ purpose: { value: "A place for non-work-related chatter", creator: userId, last_set: Math.floor(Date.now() / 1e3) },
1172
+ members: [userId],
1173
+ creator: userId,
1174
+ num_members: 1
1175
+ });
1176
+ ss.incomingWebhooks.insert({
1177
+ token: "X000000001",
1178
+ team_id: teamId,
1179
+ bot_id: "B000000001",
1180
+ default_channel: "general",
1181
+ label: "Default Webhook",
1182
+ url: `/services/${teamId}/B000000001/X000000001`
1183
+ });
1184
+ }
1185
+ function seedFromConfig(store, _baseUrl, config) {
1186
+ const ss = getSlackStore(store);
1187
+ if (config.team) {
1188
+ const existing = ss.teams.all()[0];
1189
+ if (existing) {
1190
+ ss.teams.update(existing.id, {
1191
+ name: config.team.name ?? existing.name,
1192
+ domain: config.team.domain ?? existing.domain
1193
+ });
1194
+ }
1195
+ }
1196
+ const team = ss.teams.all()[0];
1197
+ const teamId = team?.team_id ?? "T000000001";
1198
+ if (config.users) {
1199
+ for (const u of config.users) {
1200
+ const existing = ss.users.all().find((eu) => eu.name === u.name);
1201
+ if (existing) continue;
1202
+ const userId = generateSlackId("U");
1203
+ const email = u.email ?? `${u.name}@emulate.dev`;
1204
+ ss.users.insert({
1205
+ user_id: userId,
1206
+ team_id: teamId,
1207
+ name: u.name,
1208
+ real_name: u.real_name ?? u.name,
1209
+ email,
1210
+ is_admin: u.is_admin ?? false,
1211
+ is_bot: false,
1212
+ deleted: false,
1213
+ profile: {
1214
+ display_name: u.name,
1215
+ real_name: u.real_name ?? u.name,
1216
+ email,
1217
+ image_48: "",
1218
+ image_192: ""
1219
+ }
1220
+ });
1221
+ }
1222
+ }
1223
+ if (config.channels) {
1224
+ for (const ch of config.channels) {
1225
+ const existing = ss.channels.findOneBy("name", ch.name);
1226
+ if (existing) continue;
1227
+ const creator = ss.users.all()[0]?.user_id ?? "U000000001";
1228
+ const now = Math.floor(Date.now() / 1e3);
1229
+ const isPrivate = ch.is_private ?? false;
1230
+ ss.channels.insert({
1231
+ channel_id: generateSlackId("C"),
1232
+ team_id: teamId,
1233
+ name: ch.name,
1234
+ is_channel: !isPrivate,
1235
+ is_private: isPrivate,
1236
+ is_archived: false,
1237
+ topic: { value: ch.topic ?? "", creator, last_set: now },
1238
+ purpose: { value: ch.purpose ?? "", creator, last_set: now },
1239
+ members: ss.users.all().map((u) => u.user_id),
1240
+ creator,
1241
+ num_members: ss.users.all().length
1242
+ });
1243
+ }
1244
+ }
1245
+ if (config.bots) {
1246
+ for (const b of config.bots) {
1247
+ const existing = ss.bots.all().find((eb) => eb.name === b.name);
1248
+ if (existing) continue;
1249
+ ss.bots.insert({
1250
+ bot_id: generateSlackId("B"),
1251
+ name: b.name,
1252
+ deleted: false,
1253
+ icons: { image_48: "" }
1254
+ });
1255
+ }
1256
+ }
1257
+ if (config.oauth_apps) {
1258
+ for (const oa of config.oauth_apps) {
1259
+ const existing = ss.oauthApps.findOneBy("client_id", oa.client_id);
1260
+ if (existing) continue;
1261
+ ss.oauthApps.insert({
1262
+ client_id: oa.client_id,
1263
+ client_secret: oa.client_secret,
1264
+ name: oa.name,
1265
+ redirect_uris: oa.redirect_uris
1266
+ });
1267
+ }
1268
+ }
1269
+ if (config.incoming_webhooks) {
1270
+ const firstBot = ss.bots.all()[0];
1271
+ const botId = firstBot?.bot_id ?? "B000000001";
1272
+ for (const wh of config.incoming_webhooks) {
1273
+ const token = generateSlackId("X");
1274
+ ss.incomingWebhooks.insert({
1275
+ token,
1276
+ team_id: teamId,
1277
+ bot_id: botId,
1278
+ default_channel: wh.channel,
1279
+ label: wh.label ?? wh.channel,
1280
+ url: `/services/${teamId}/${botId}/${token}`
1281
+ });
1282
+ }
1283
+ }
1284
+ if (config.signing_secret) {
1285
+ store.setData("slack.signing_secret", config.signing_secret);
1286
+ }
1287
+ }
1288
+ var slackPlugin = {
1289
+ name: "slack",
1290
+ register(app, store, webhooks, baseUrl, tokenMap) {
1291
+ const ctx = { app, store, webhooks, baseUrl, tokenMap };
1292
+ authRoutes(ctx);
1293
+ chatRoutes(ctx);
1294
+ conversationsRoutes(ctx);
1295
+ usersRoutes(ctx);
1296
+ reactionsRoutes(ctx);
1297
+ teamRoutes(ctx);
1298
+ oauthRoutes(ctx);
1299
+ webhookRoutes(ctx);
1300
+ inspectorRoutes(ctx);
1301
+ },
1302
+ seed(store, baseUrl) {
1303
+ seedDefaults(store, baseUrl);
1304
+ }
1305
+ };
1306
+ var index_default = slackPlugin;
1307
+ export {
1308
+ index_default as default,
1309
+ getSlackStore,
1310
+ seedFromConfig,
1311
+ slackPlugin
1312
+ };
1313
+ //# sourceMappingURL=index.js.map