@opengeni/api-router 0.16.5 → 0.20.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.js +1 -1
- package/dist/{chunk-FGNCK7HE.js → chunk-AEVD7E2F.js} +2579 -141
- package/dist/chunk-AEVD7E2F.js.map +1 -0
- package/dist/codex-realtime.d.ts +43 -0
- package/dist/gateway-realtime.d.ts +24 -0
- package/dist/index.js +4 -3
- package/dist/index.js.map +1 -1
- package/dist/integrations/google-drive.d.ts +16 -1
- package/dist/integrations/slack-bot.d.ts +122 -0
- package/dist/integrations/slack-interactions.d.ts +5 -0
- package/dist/mcp/documents.d.ts +3 -3
- package/dist/mcp/server.d.ts +1 -1
- package/dist/session-realtime-context.d.ts +19 -0
- package/dist/workspace-state-projection.d.ts +29 -1
- package/package.json +12 -12
- package/src/app.ts +33 -2
- package/src/codex-realtime.ts +371 -0
- package/src/gateway-realtime.ts +143 -0
- package/src/index.ts +3 -2
- package/src/integrations/google-drive.ts +399 -24
- package/src/integrations/slack-bot.ts +536 -0
- package/src/integrations/slack-interactions.ts +467 -2
- package/src/mcp/documents.ts +49 -27
- package/src/routes/connections.ts +63 -11
- package/src/routes/documents.ts +134 -43
- package/src/routes/sessions.ts +622 -11
- package/src/routes/workspace-instruction-policies.ts +22 -0
- package/src/routes/workspace-state.ts +46 -1
- package/src/routes/workspaces.ts +56 -1
- package/src/session-realtime-context.ts +134 -0
- package/src/workspace-state-projection.ts +203 -4
- package/dist/chunk-FGNCK7HE.js.map +0 -1
|
@@ -46,6 +46,18 @@ const SLACK_TIMEOUT_MS = 10_000;
|
|
|
46
46
|
const MAX_CHANNEL_PAGE = 200;
|
|
47
47
|
const MAX_HISTORY_PAGE = 100;
|
|
48
48
|
const MAX_THREAD_PAGE = 100;
|
|
49
|
+
const MAX_REACTION_CONTEXT_MESSAGES = 15;
|
|
50
|
+
const MAX_REACTION_CONTEXT_PAGES = 8;
|
|
51
|
+
const MAX_REACTION_CONTEXT_SEEN_MESSAGES =
|
|
52
|
+
MAX_REACTION_CONTEXT_MESSAGES * MAX_REACTION_CONTEXT_PAGES;
|
|
53
|
+
// Leave headroom for PostgreSQL jsonb's canonical text spacing under the
|
|
54
|
+
// database's independent 128 KiB CHECK constraint.
|
|
55
|
+
const MAX_REACTION_CONTEXT_CHECKPOINT_BYTES = 120 * 1024;
|
|
56
|
+
const MAX_REACTION_CONTEXT_CHECKPOINT_AGE_MS = 24 * 60 * 60_000;
|
|
57
|
+
const MAX_REACTION_CONTEXT_CHECKPOINT_CLOCK_SKEW_MS = 5 * 60_000;
|
|
58
|
+
const MAX_REACTION_CONTEXT_CHECKPOINT_FILE_LABEL_CHARS = 1_500;
|
|
59
|
+
const MAX_REACTION_CONTEXT_CHECKPOINT_FILES = 16;
|
|
60
|
+
const SLACK_REACTION_CONTEXT_CHECKPOINT_VERSION = 1;
|
|
49
61
|
const MAX_USER_PAGE = 200;
|
|
50
62
|
const MAX_FILE_PAGE = 200;
|
|
51
63
|
const MAX_FILE_CURSOR_LENGTH = 1_024;
|
|
@@ -152,6 +164,45 @@ type SlackBotContext = {
|
|
|
152
164
|
scheduledTaskId?: string | null;
|
|
153
165
|
};
|
|
154
166
|
|
|
167
|
+
export type SlackReactionContextCheckpointBinding = {
|
|
168
|
+
inboxId: string;
|
|
169
|
+
accountId: string;
|
|
170
|
+
workspaceId: string;
|
|
171
|
+
connectionId: string;
|
|
172
|
+
providerEventId: string;
|
|
173
|
+
providerMessageId: string;
|
|
174
|
+
slackTeamId: string;
|
|
175
|
+
slackChannelId: string;
|
|
176
|
+
slackMessageTs: string;
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
type SlackReactionCheckpointMessage = {
|
|
180
|
+
timestamp: string;
|
|
181
|
+
userId: string;
|
|
182
|
+
botId: string;
|
|
183
|
+
threadTimestamp: string;
|
|
184
|
+
text: string;
|
|
185
|
+
files: Array<{ id: string; label: string }>;
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
type SlackReactionContextCheckpointUnsigned = {
|
|
189
|
+
version: typeof SLACK_REACTION_CONTEXT_CHECKPOINT_VERSION;
|
|
190
|
+
binding: SlackReactionContextCheckpointBinding;
|
|
191
|
+
state: {
|
|
192
|
+
createdAtMs: number;
|
|
193
|
+
pageCount: number;
|
|
194
|
+
nextCursor: string;
|
|
195
|
+
seenCursors: string[];
|
|
196
|
+
seenMessageTimestamps: string[];
|
|
197
|
+
threadTimestamp: string | null;
|
|
198
|
+
messages: SlackReactionCheckpointMessage[];
|
|
199
|
+
};
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
export type SlackReactionContextCheckpoint = SlackReactionContextCheckpointUnsigned & {
|
|
203
|
+
signature: string;
|
|
204
|
+
};
|
|
205
|
+
|
|
155
206
|
export class SlackBotProviderError extends Error {
|
|
156
207
|
constructor(
|
|
157
208
|
readonly code: string,
|
|
@@ -395,6 +446,117 @@ export class OpenGeniSlackBotClient {
|
|
|
395
446
|
});
|
|
396
447
|
}
|
|
397
448
|
|
|
449
|
+
async reactionMessageContext(input: {
|
|
450
|
+
channelId: string;
|
|
451
|
+
messageTimestamp: string;
|
|
452
|
+
checkpoint: unknown | null;
|
|
453
|
+
checkpointBinding: SlackReactionContextCheckpointBinding;
|
|
454
|
+
saveCheckpoint: (checkpoint: SlackReactionContextCheckpoint) => Promise<void>;
|
|
455
|
+
}) {
|
|
456
|
+
return await this.withAudit("thread_replies.read", async (headers) => {
|
|
457
|
+
const checkpointKey = environmentsEncryptionKeyBytes(this.settings);
|
|
458
|
+
if (!checkpointKey) throw new Error("connection encryption is not configured");
|
|
459
|
+
assertSlackReactionCheckpointBinding(
|
|
460
|
+
input.checkpointBinding,
|
|
461
|
+
this.context,
|
|
462
|
+
this.connection.id,
|
|
463
|
+
this.metadata.slackTeamId,
|
|
464
|
+
input.channelId,
|
|
465
|
+
input.messageTimestamp,
|
|
466
|
+
);
|
|
467
|
+
const restored = input.checkpoint
|
|
468
|
+
? parseSlackReactionContextCheckpoint(
|
|
469
|
+
input.checkpoint,
|
|
470
|
+
input.checkpointBinding,
|
|
471
|
+
checkpointKey,
|
|
472
|
+
)
|
|
473
|
+
: null;
|
|
474
|
+
const info = await this.requireMemberChannel(headers, input.channelId);
|
|
475
|
+
if (info.isShared || info.isExternallyShared || info.isOrgShared) {
|
|
476
|
+
throw new SlackBotProviderError("slack_connect_unsupported");
|
|
477
|
+
}
|
|
478
|
+
const messages: ReturnType<typeof projectMessage>[] = restored
|
|
479
|
+
? restored.state.messages.map(projectSlackReactionCheckpointMessage)
|
|
480
|
+
: [];
|
|
481
|
+
const seenMessageTimestamps = new Set(restored?.state.seenMessageTimestamps ?? []);
|
|
482
|
+
const seenCursors = new Set(restored?.state.seenCursors ?? []);
|
|
483
|
+
let cursor: string | null = restored?.state.nextCursor ?? null;
|
|
484
|
+
let nextCursor: string | null = cursor;
|
|
485
|
+
let threadTimestamp: string | null = restored?.state.threadTimestamp ?? null;
|
|
486
|
+
let reactedMessage: ReturnType<typeof projectMessage> | null = null;
|
|
487
|
+
const checkpointCreatedAtMs = restored?.state.createdAtMs ?? Date.now();
|
|
488
|
+
const firstPage = restored?.state.pageCount ?? 0;
|
|
489
|
+
|
|
490
|
+
for (let page = firstPage; page < MAX_REACTION_CONTEXT_PAGES; page += 1) {
|
|
491
|
+
const payload = await this.call(headers, "conversations.replies", {
|
|
492
|
+
channel: input.channelId,
|
|
493
|
+
// Slack accepts either the parent timestamp or a message timestamp from
|
|
494
|
+
// inside the thread and returns the containing thread.
|
|
495
|
+
ts: input.messageTimestamp,
|
|
496
|
+
limit: String(MAX_REACTION_CONTEXT_MESSAGES),
|
|
497
|
+
...(cursor ? { cursor } : {}),
|
|
498
|
+
});
|
|
499
|
+
const pageMessages = slackArray(payload.messages)
|
|
500
|
+
.map(projectMessage)
|
|
501
|
+
.filter((message) => message.timestamp.length > 0);
|
|
502
|
+
const first = pageMessages[0];
|
|
503
|
+
threadTimestamp ??= first?.threadTimestamp || first?.timestamp || null;
|
|
504
|
+
for (const message of pageMessages) {
|
|
505
|
+
if (seenMessageTimestamps.has(message.timestamp)) continue;
|
|
506
|
+
seenMessageTimestamps.add(message.timestamp);
|
|
507
|
+
messages.push(message);
|
|
508
|
+
}
|
|
509
|
+
reactedMessage =
|
|
510
|
+
reactedMessage ??
|
|
511
|
+
pageMessages.find((message) => message.timestamp === input.messageTimestamp) ??
|
|
512
|
+
null;
|
|
513
|
+
nextCursor = responseCursor(payload);
|
|
514
|
+
if (reactedMessage || !nextCursor) break;
|
|
515
|
+
if (seenCursors.has(nextCursor)) {
|
|
516
|
+
throw new SlackBotProviderError("reaction_pagination_invalid");
|
|
517
|
+
}
|
|
518
|
+
seenCursors.add(nextCursor);
|
|
519
|
+
const pageCount = page + 1;
|
|
520
|
+
if (pageCount >= MAX_REACTION_CONTEXT_PAGES) {
|
|
521
|
+
throw new SlackBotProviderError("reaction_pagination_exhausted");
|
|
522
|
+
}
|
|
523
|
+
const retainedMessages = selectSlackReactionCheckpointMessages(messages);
|
|
524
|
+
messages.splice(0, messages.length, ...retainedMessages);
|
|
525
|
+
await input.saveCheckpoint(
|
|
526
|
+
createSlackReactionContextCheckpoint(
|
|
527
|
+
input.checkpointBinding,
|
|
528
|
+
{
|
|
529
|
+
createdAtMs: checkpointCreatedAtMs,
|
|
530
|
+
pageCount,
|
|
531
|
+
nextCursor,
|
|
532
|
+
seenCursors: [...seenCursors],
|
|
533
|
+
seenMessageTimestamps: [...seenMessageTimestamps],
|
|
534
|
+
threadTimestamp,
|
|
535
|
+
messages: retainedMessages.map(slackReactionCheckpointMessage),
|
|
536
|
+
},
|
|
537
|
+
checkpointKey,
|
|
538
|
+
),
|
|
539
|
+
);
|
|
540
|
+
cursor = nextCursor;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
if (!reactedMessage || !threadTimestamp) {
|
|
544
|
+
throw new SlackBotProviderError("message_not_found");
|
|
545
|
+
}
|
|
546
|
+
const boundedMessages = selectSlackReactionContextMessages(
|
|
547
|
+
messages,
|
|
548
|
+
reactedMessage.timestamp,
|
|
549
|
+
);
|
|
550
|
+
return {
|
|
551
|
+
channel: info,
|
|
552
|
+
threadTimestamp,
|
|
553
|
+
reactedMessage,
|
|
554
|
+
messages: boundedMessages,
|
|
555
|
+
truncated: nextCursor !== null || seenMessageTimestamps.size > boundedMessages.length,
|
|
556
|
+
};
|
|
557
|
+
});
|
|
558
|
+
}
|
|
559
|
+
|
|
398
560
|
async listUsers(input: { limit?: number; cursor?: string } = {}) {
|
|
399
561
|
return await this.withAudit("users.list", async (headers) => {
|
|
400
562
|
const payload = await this.call(headers, "users.list", {
|
|
@@ -1263,6 +1425,9 @@ function projectChannel(value: unknown) {
|
|
|
1263
1425
|
isMember: channel.is_member === true,
|
|
1264
1426
|
isDirectMessage: channel.is_im === true,
|
|
1265
1427
|
isArchived: channel.is_archived === true,
|
|
1428
|
+
isShared: channel.is_shared === true,
|
|
1429
|
+
isExternallyShared: channel.is_ext_shared === true,
|
|
1430
|
+
isOrgShared: channel.is_org_shared === true,
|
|
1266
1431
|
topic: boundedSlackString(slackRecord(channel.topic)?.value, 1_024),
|
|
1267
1432
|
purpose: boundedSlackString(slackRecord(channel.purpose)?.value, 1_024),
|
|
1268
1433
|
numMembers:
|
|
@@ -1286,6 +1451,377 @@ function projectMessage(value: unknown) {
|
|
|
1286
1451
|
};
|
|
1287
1452
|
}
|
|
1288
1453
|
|
|
1454
|
+
function assertSlackReactionCheckpointBinding(
|
|
1455
|
+
binding: SlackReactionContextCheckpointBinding,
|
|
1456
|
+
context: SlackBotContext,
|
|
1457
|
+
connectionId: string,
|
|
1458
|
+
slackTeamId: string,
|
|
1459
|
+
channelId: string,
|
|
1460
|
+
messageTimestamp: string,
|
|
1461
|
+
): void {
|
|
1462
|
+
if (
|
|
1463
|
+
binding.accountId !== context.accountId ||
|
|
1464
|
+
binding.workspaceId !== context.workspaceId ||
|
|
1465
|
+
binding.connectionId !== connectionId ||
|
|
1466
|
+
binding.slackTeamId !== slackTeamId ||
|
|
1467
|
+
binding.slackChannelId !== channelId ||
|
|
1468
|
+
binding.slackMessageTs !== messageTimestamp
|
|
1469
|
+
) {
|
|
1470
|
+
throw new SlackBotProviderError("reaction_checkpoint_invalid");
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
function createSlackReactionContextCheckpoint(
|
|
1475
|
+
binding: SlackReactionContextCheckpointBinding,
|
|
1476
|
+
state: SlackReactionContextCheckpointUnsigned["state"],
|
|
1477
|
+
key: Uint8Array,
|
|
1478
|
+
): SlackReactionContextCheckpoint {
|
|
1479
|
+
const unsigned: SlackReactionContextCheckpointUnsigned = {
|
|
1480
|
+
version: SLACK_REACTION_CONTEXT_CHECKPOINT_VERSION,
|
|
1481
|
+
binding: { ...binding },
|
|
1482
|
+
state: {
|
|
1483
|
+
createdAtMs: state.createdAtMs,
|
|
1484
|
+
pageCount: state.pageCount,
|
|
1485
|
+
nextCursor: state.nextCursor,
|
|
1486
|
+
seenCursors: [...state.seenCursors],
|
|
1487
|
+
seenMessageTimestamps: [...state.seenMessageTimestamps],
|
|
1488
|
+
threadTimestamp: state.threadTimestamp,
|
|
1489
|
+
messages: state.messages.map((message) => ({
|
|
1490
|
+
...message,
|
|
1491
|
+
files: message.files.map((file) => ({ ...file })),
|
|
1492
|
+
})),
|
|
1493
|
+
},
|
|
1494
|
+
};
|
|
1495
|
+
const checkpoint: SlackReactionContextCheckpoint = {
|
|
1496
|
+
...unsigned,
|
|
1497
|
+
signature: slackReactionContextCheckpointSignature(unsigned, key),
|
|
1498
|
+
};
|
|
1499
|
+
if (
|
|
1500
|
+
Buffer.byteLength(JSON.stringify(checkpoint), "utf8") > MAX_REACTION_CONTEXT_CHECKPOINT_BYTES
|
|
1501
|
+
) {
|
|
1502
|
+
throw new SlackBotProviderError("reaction_checkpoint_too_large");
|
|
1503
|
+
}
|
|
1504
|
+
return checkpoint;
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
function parseSlackReactionContextCheckpoint(
|
|
1508
|
+
value: unknown,
|
|
1509
|
+
expectedBinding: SlackReactionContextCheckpointBinding,
|
|
1510
|
+
key: Uint8Array,
|
|
1511
|
+
nowMs = Date.now(),
|
|
1512
|
+
): SlackReactionContextCheckpoint {
|
|
1513
|
+
const checkpoint = slackRecord(value);
|
|
1514
|
+
if (
|
|
1515
|
+
!checkpoint ||
|
|
1516
|
+
!hasExactSlackCheckpointKeys(checkpoint, ["binding", "signature", "state", "version"]) ||
|
|
1517
|
+
Buffer.byteLength(JSON.stringify(checkpoint), "utf8") > MAX_REACTION_CONTEXT_CHECKPOINT_BYTES ||
|
|
1518
|
+
checkpoint.version !== SLACK_REACTION_CONTEXT_CHECKPOINT_VERSION
|
|
1519
|
+
) {
|
|
1520
|
+
throw new SlackBotProviderError("reaction_checkpoint_invalid");
|
|
1521
|
+
}
|
|
1522
|
+
const bindingValue = slackRecord(checkpoint.binding);
|
|
1523
|
+
const stateValue = slackRecord(checkpoint.state);
|
|
1524
|
+
const signature = exactSlackCheckpointString(checkpoint.signature, 64);
|
|
1525
|
+
if (
|
|
1526
|
+
!bindingValue ||
|
|
1527
|
+
!stateValue ||
|
|
1528
|
+
!signature ||
|
|
1529
|
+
!/^[0-9a-f]{64}$/.test(signature) ||
|
|
1530
|
+
!hasExactSlackCheckpointKeys(bindingValue, [
|
|
1531
|
+
"accountId",
|
|
1532
|
+
"connectionId",
|
|
1533
|
+
"inboxId",
|
|
1534
|
+
"providerEventId",
|
|
1535
|
+
"providerMessageId",
|
|
1536
|
+
"slackChannelId",
|
|
1537
|
+
"slackMessageTs",
|
|
1538
|
+
"slackTeamId",
|
|
1539
|
+
"workspaceId",
|
|
1540
|
+
]) ||
|
|
1541
|
+
!hasExactSlackCheckpointKeys(stateValue, [
|
|
1542
|
+
"createdAtMs",
|
|
1543
|
+
"messages",
|
|
1544
|
+
"nextCursor",
|
|
1545
|
+
"pageCount",
|
|
1546
|
+
"seenCursors",
|
|
1547
|
+
"seenMessageTimestamps",
|
|
1548
|
+
"threadTimestamp",
|
|
1549
|
+
])
|
|
1550
|
+
) {
|
|
1551
|
+
throw new SlackBotProviderError("reaction_checkpoint_invalid");
|
|
1552
|
+
}
|
|
1553
|
+
const binding: SlackReactionContextCheckpointBinding = {
|
|
1554
|
+
inboxId: requiredSlackCheckpointString(bindingValue.inboxId, 64),
|
|
1555
|
+
accountId: requiredSlackCheckpointString(bindingValue.accountId, 64),
|
|
1556
|
+
workspaceId: requiredSlackCheckpointString(bindingValue.workspaceId, 64),
|
|
1557
|
+
connectionId: requiredSlackCheckpointString(bindingValue.connectionId, 64),
|
|
1558
|
+
providerEventId: requiredSlackCheckpointString(bindingValue.providerEventId, 256),
|
|
1559
|
+
providerMessageId: requiredSlackCheckpointString(bindingValue.providerMessageId, 256),
|
|
1560
|
+
slackTeamId: requiredSlackCheckpointString(bindingValue.slackTeamId, 64),
|
|
1561
|
+
slackChannelId: requiredSlackCheckpointString(bindingValue.slackChannelId, 64),
|
|
1562
|
+
slackMessageTs: requiredSlackCheckpointString(bindingValue.slackMessageTs, 64),
|
|
1563
|
+
};
|
|
1564
|
+
if (!slackReactionCheckpointBindingMatches(binding, expectedBinding)) {
|
|
1565
|
+
throw new SlackBotProviderError("reaction_checkpoint_invalid");
|
|
1566
|
+
}
|
|
1567
|
+
const createdAtMs = stateValue.createdAtMs;
|
|
1568
|
+
const pageCount = stateValue.pageCount;
|
|
1569
|
+
const nextCursor = exactSlackCheckpointString(stateValue.nextCursor, 1_024);
|
|
1570
|
+
const threadTimestamp =
|
|
1571
|
+
stateValue.threadTimestamp === null
|
|
1572
|
+
? null
|
|
1573
|
+
: exactSlackCheckpointString(stateValue.threadTimestamp, 64);
|
|
1574
|
+
if (
|
|
1575
|
+
typeof createdAtMs !== "number" ||
|
|
1576
|
+
!Number.isSafeInteger(createdAtMs) ||
|
|
1577
|
+
createdAtMs > nowMs + MAX_REACTION_CONTEXT_CHECKPOINT_CLOCK_SKEW_MS ||
|
|
1578
|
+
createdAtMs < nowMs - MAX_REACTION_CONTEXT_CHECKPOINT_AGE_MS ||
|
|
1579
|
+
typeof pageCount !== "number" ||
|
|
1580
|
+
!Number.isSafeInteger(pageCount) ||
|
|
1581
|
+
pageCount < 1 ||
|
|
1582
|
+
pageCount >= MAX_REACTION_CONTEXT_PAGES ||
|
|
1583
|
+
!nextCursor ||
|
|
1584
|
+
(threadTimestamp === "" && stateValue.threadTimestamp !== null) ||
|
|
1585
|
+
!Array.isArray(stateValue.seenCursors) ||
|
|
1586
|
+
!Array.isArray(stateValue.seenMessageTimestamps) ||
|
|
1587
|
+
!Array.isArray(stateValue.messages)
|
|
1588
|
+
) {
|
|
1589
|
+
throw new SlackBotProviderError("reaction_checkpoint_invalid");
|
|
1590
|
+
}
|
|
1591
|
+
const seenCursors = stateValue.seenCursors.map((cursor) =>
|
|
1592
|
+
requiredSlackCheckpointString(cursor, 1_024),
|
|
1593
|
+
);
|
|
1594
|
+
const seenMessageTimestamps = stateValue.seenMessageTimestamps.map((timestamp) =>
|
|
1595
|
+
requiredSlackCheckpointString(timestamp, 64),
|
|
1596
|
+
);
|
|
1597
|
+
if (
|
|
1598
|
+
seenCursors.length !== pageCount ||
|
|
1599
|
+
seenCursors.length > MAX_REACTION_CONTEXT_PAGES ||
|
|
1600
|
+
new Set(seenCursors).size !== seenCursors.length ||
|
|
1601
|
+
seenCursors.at(-1) !== nextCursor ||
|
|
1602
|
+
seenMessageTimestamps.length > MAX_REACTION_CONTEXT_SEEN_MESSAGES ||
|
|
1603
|
+
new Set(seenMessageTimestamps).size !== seenMessageTimestamps.length ||
|
|
1604
|
+
seenMessageTimestamps.includes(expectedBinding.slackMessageTs) ||
|
|
1605
|
+
stateValue.messages.length > MAX_REACTION_CONTEXT_MESSAGES
|
|
1606
|
+
) {
|
|
1607
|
+
throw new SlackBotProviderError("reaction_checkpoint_invalid");
|
|
1608
|
+
}
|
|
1609
|
+
const messages = stateValue.messages.map(parseSlackReactionCheckpointMessage);
|
|
1610
|
+
const seenTimestampIndexes = messages.map((message) =>
|
|
1611
|
+
seenMessageTimestamps.indexOf(message.timestamp),
|
|
1612
|
+
);
|
|
1613
|
+
if (
|
|
1614
|
+
(seenMessageTimestamps.length > 0 && messages.length === 0) ||
|
|
1615
|
+
(messages.length > 0 && threadTimestamp === null) ||
|
|
1616
|
+
(messages.length > 0 && messages[0]!.timestamp !== seenMessageTimestamps[0]) ||
|
|
1617
|
+
seenTimestampIndexes.some(
|
|
1618
|
+
(index, position) =>
|
|
1619
|
+
index < 0 || (position > 0 && index <= seenTimestampIndexes[position - 1]!),
|
|
1620
|
+
)
|
|
1621
|
+
) {
|
|
1622
|
+
throw new SlackBotProviderError("reaction_checkpoint_invalid");
|
|
1623
|
+
}
|
|
1624
|
+
const unsigned: SlackReactionContextCheckpointUnsigned = {
|
|
1625
|
+
version: SLACK_REACTION_CONTEXT_CHECKPOINT_VERSION,
|
|
1626
|
+
binding,
|
|
1627
|
+
state: {
|
|
1628
|
+
createdAtMs,
|
|
1629
|
+
pageCount,
|
|
1630
|
+
nextCursor,
|
|
1631
|
+
seenCursors,
|
|
1632
|
+
seenMessageTimestamps,
|
|
1633
|
+
threadTimestamp,
|
|
1634
|
+
messages,
|
|
1635
|
+
},
|
|
1636
|
+
};
|
|
1637
|
+
const expectedSignature = slackReactionContextCheckpointSignature(unsigned, key);
|
|
1638
|
+
const actualBytes = Buffer.from(signature, "utf8");
|
|
1639
|
+
const expectedBytes = Buffer.from(expectedSignature, "utf8");
|
|
1640
|
+
if (actualBytes.length !== expectedBytes.length || !timingSafeEqual(actualBytes, expectedBytes)) {
|
|
1641
|
+
throw new SlackBotProviderError("reaction_checkpoint_invalid");
|
|
1642
|
+
}
|
|
1643
|
+
return { ...unsigned, signature };
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
function slackReactionContextCheckpointSignature(
|
|
1647
|
+
checkpoint: SlackReactionContextCheckpointUnsigned,
|
|
1648
|
+
key: Uint8Array,
|
|
1649
|
+
): string {
|
|
1650
|
+
return createHmac("sha256", key).update(JSON.stringify(checkpoint)).digest("hex");
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1653
|
+
function slackReactionCheckpointBindingMatches(
|
|
1654
|
+
left: SlackReactionContextCheckpointBinding,
|
|
1655
|
+
right: SlackReactionContextCheckpointBinding,
|
|
1656
|
+
): boolean {
|
|
1657
|
+
return (
|
|
1658
|
+
left.inboxId === right.inboxId &&
|
|
1659
|
+
left.accountId === right.accountId &&
|
|
1660
|
+
left.workspaceId === right.workspaceId &&
|
|
1661
|
+
left.connectionId === right.connectionId &&
|
|
1662
|
+
left.providerEventId === right.providerEventId &&
|
|
1663
|
+
left.providerMessageId === right.providerMessageId &&
|
|
1664
|
+
left.slackTeamId === right.slackTeamId &&
|
|
1665
|
+
left.slackChannelId === right.slackChannelId &&
|
|
1666
|
+
left.slackMessageTs === right.slackMessageTs
|
|
1667
|
+
);
|
|
1668
|
+
}
|
|
1669
|
+
|
|
1670
|
+
function parseSlackReactionCheckpointMessage(value: unknown): SlackReactionCheckpointMessage {
|
|
1671
|
+
const message = slackRecord(value);
|
|
1672
|
+
if (
|
|
1673
|
+
!message ||
|
|
1674
|
+
!hasExactSlackCheckpointKeys(message, [
|
|
1675
|
+
"botId",
|
|
1676
|
+
"files",
|
|
1677
|
+
"text",
|
|
1678
|
+
"threadTimestamp",
|
|
1679
|
+
"timestamp",
|
|
1680
|
+
"userId",
|
|
1681
|
+
]) ||
|
|
1682
|
+
!Array.isArray(message.files) ||
|
|
1683
|
+
message.files.length > MAX_REACTION_CONTEXT_CHECKPOINT_FILES ||
|
|
1684
|
+
typeof message.timestamp !== "string" ||
|
|
1685
|
+
message.timestamp.length < 1 ||
|
|
1686
|
+
message.timestamp.length > 64 ||
|
|
1687
|
+
typeof message.userId !== "string" ||
|
|
1688
|
+
message.userId.length > 64 ||
|
|
1689
|
+
typeof message.botId !== "string" ||
|
|
1690
|
+
message.botId.length > 64 ||
|
|
1691
|
+
typeof message.threadTimestamp !== "string" ||
|
|
1692
|
+
message.threadTimestamp.length > 64 ||
|
|
1693
|
+
typeof message.text !== "string" ||
|
|
1694
|
+
message.text.length > MAX_PROJECTED_TEXT
|
|
1695
|
+
) {
|
|
1696
|
+
throw new SlackBotProviderError("reaction_checkpoint_invalid");
|
|
1697
|
+
}
|
|
1698
|
+
const files = message.files.map((candidate) => {
|
|
1699
|
+
const file = slackRecord(candidate);
|
|
1700
|
+
if (!file || !hasExactSlackCheckpointKeys(file, ["id", "label"])) {
|
|
1701
|
+
throw new SlackBotProviderError("reaction_checkpoint_invalid");
|
|
1702
|
+
}
|
|
1703
|
+
return {
|
|
1704
|
+
id: requiredSlackCheckpointString(file.id, 64),
|
|
1705
|
+
label: requiredSlackCheckpointString(file.label, 512),
|
|
1706
|
+
};
|
|
1707
|
+
});
|
|
1708
|
+
let fileLabelChars = 0;
|
|
1709
|
+
for (const file of files) {
|
|
1710
|
+
fileLabelChars += file.label.length + (fileLabelChars > 0 ? 2 : 0);
|
|
1711
|
+
}
|
|
1712
|
+
if (fileLabelChars > MAX_REACTION_CONTEXT_CHECKPOINT_FILE_LABEL_CHARS) {
|
|
1713
|
+
throw new SlackBotProviderError("reaction_checkpoint_invalid");
|
|
1714
|
+
}
|
|
1715
|
+
return {
|
|
1716
|
+
timestamp: message.timestamp,
|
|
1717
|
+
userId: message.userId,
|
|
1718
|
+
botId: message.botId,
|
|
1719
|
+
threadTimestamp: message.threadTimestamp,
|
|
1720
|
+
text: message.text,
|
|
1721
|
+
files,
|
|
1722
|
+
};
|
|
1723
|
+
}
|
|
1724
|
+
|
|
1725
|
+
function slackReactionCheckpointMessage(
|
|
1726
|
+
message: ReturnType<typeof projectMessage>,
|
|
1727
|
+
): SlackReactionCheckpointMessage {
|
|
1728
|
+
const files: SlackReactionCheckpointMessage["files"] = [];
|
|
1729
|
+
let fileLabelChars = 0;
|
|
1730
|
+
for (const file of message.files) {
|
|
1731
|
+
const label = file.title || file.name || file.id;
|
|
1732
|
+
if (!label) continue;
|
|
1733
|
+
const addedChars = label.length + (files.length > 0 ? 2 : 0);
|
|
1734
|
+
if (
|
|
1735
|
+
files.length >= MAX_REACTION_CONTEXT_CHECKPOINT_FILES ||
|
|
1736
|
+
fileLabelChars + addedChars > MAX_REACTION_CONTEXT_CHECKPOINT_FILE_LABEL_CHARS
|
|
1737
|
+
) {
|
|
1738
|
+
break;
|
|
1739
|
+
}
|
|
1740
|
+
files.push({ id: file.id, label });
|
|
1741
|
+
fileLabelChars += addedChars;
|
|
1742
|
+
}
|
|
1743
|
+
return {
|
|
1744
|
+
timestamp: message.timestamp,
|
|
1745
|
+
userId: message.userId,
|
|
1746
|
+
botId: message.botId,
|
|
1747
|
+
threadTimestamp: message.threadTimestamp,
|
|
1748
|
+
text: message.text,
|
|
1749
|
+
files,
|
|
1750
|
+
};
|
|
1751
|
+
}
|
|
1752
|
+
|
|
1753
|
+
function projectSlackReactionCheckpointMessage(
|
|
1754
|
+
message: SlackReactionCheckpointMessage,
|
|
1755
|
+
): ReturnType<typeof projectMessage> {
|
|
1756
|
+
return {
|
|
1757
|
+
timestamp: message.timestamp,
|
|
1758
|
+
userId: message.userId,
|
|
1759
|
+
botId: message.botId,
|
|
1760
|
+
threadTimestamp: message.threadTimestamp,
|
|
1761
|
+
text: message.text,
|
|
1762
|
+
files: message.files.map((file) => ({
|
|
1763
|
+
id: file.id,
|
|
1764
|
+
name: "",
|
|
1765
|
+
title: file.label,
|
|
1766
|
+
mimetype: "",
|
|
1767
|
+
filetype: "",
|
|
1768
|
+
mode: "",
|
|
1769
|
+
size: null,
|
|
1770
|
+
originatingHuddleId: "",
|
|
1771
|
+
huddleTranscriptFileId: "",
|
|
1772
|
+
})),
|
|
1773
|
+
};
|
|
1774
|
+
}
|
|
1775
|
+
|
|
1776
|
+
function selectSlackReactionCheckpointMessages(
|
|
1777
|
+
messages: ReturnType<typeof projectMessage>[],
|
|
1778
|
+
): ReturnType<typeof projectMessage>[] {
|
|
1779
|
+
if (messages.length <= MAX_REACTION_CONTEXT_MESSAGES) return [...messages];
|
|
1780
|
+
return [messages[0]!, ...messages.slice(-(MAX_REACTION_CONTEXT_MESSAGES - 1))];
|
|
1781
|
+
}
|
|
1782
|
+
|
|
1783
|
+
function hasExactSlackCheckpointKeys(value: Record<string, unknown>, expected: string[]): boolean {
|
|
1784
|
+
return Object.keys(value).sort().join(",") === [...expected].sort().join(",");
|
|
1785
|
+
}
|
|
1786
|
+
|
|
1787
|
+
function exactSlackCheckpointString(value: unknown, max: number): string {
|
|
1788
|
+
return typeof value === "string" && value.length <= max ? value : "";
|
|
1789
|
+
}
|
|
1790
|
+
|
|
1791
|
+
function requiredSlackCheckpointString(value: unknown, max: number): string {
|
|
1792
|
+
const result = exactSlackCheckpointString(value, max);
|
|
1793
|
+
if (!result) throw new SlackBotProviderError("reaction_checkpoint_invalid");
|
|
1794
|
+
return result;
|
|
1795
|
+
}
|
|
1796
|
+
|
|
1797
|
+
function selectSlackReactionContextMessages(
|
|
1798
|
+
messages: ReturnType<typeof projectMessage>[],
|
|
1799
|
+
reactedTimestamp: string,
|
|
1800
|
+
) {
|
|
1801
|
+
if (messages.length <= MAX_REACTION_CONTEXT_MESSAGES) return messages;
|
|
1802
|
+
const reactedIndex = messages.findIndex((message) => message.timestamp === reactedTimestamp);
|
|
1803
|
+
if (reactedIndex < 0) return [];
|
|
1804
|
+
|
|
1805
|
+
const selected = new Set<number>([0, reactedIndex]);
|
|
1806
|
+
for (
|
|
1807
|
+
let distance = 1;
|
|
1808
|
+
selected.size < MAX_REACTION_CONTEXT_MESSAGES && distance < messages.length;
|
|
1809
|
+
distance += 1
|
|
1810
|
+
) {
|
|
1811
|
+
const before = reactedIndex - distance;
|
|
1812
|
+
const after = reactedIndex + distance;
|
|
1813
|
+
if (before > 0) selected.add(before);
|
|
1814
|
+
if (selected.size < MAX_REACTION_CONTEXT_MESSAGES && after < messages.length) {
|
|
1815
|
+
selected.add(after);
|
|
1816
|
+
}
|
|
1817
|
+
}
|
|
1818
|
+
for (let index = 0; selected.size < MAX_REACTION_CONTEXT_MESSAGES; index += 1) {
|
|
1819
|
+
if (index >= messages.length) break;
|
|
1820
|
+
selected.add(index);
|
|
1821
|
+
}
|
|
1822
|
+
return [...selected].sort((left, right) => left - right).map((index) => messages[index]!);
|
|
1823
|
+
}
|
|
1824
|
+
|
|
1289
1825
|
function projectFile(value: unknown) {
|
|
1290
1826
|
const file = slackRecord(value);
|
|
1291
1827
|
const id = slackString(file?.id);
|