@opengeni/db 0.16.2 → 0.18.1
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/{chunk-4RUK5CON.js → chunk-T6RSJT6C.js} +20 -2
- package/dist/chunk-T6RSJT6C.js.map +1 -0
- package/dist/{chunk-HZD7KVAR.js → chunk-YKWJ7QJ2.js} +369 -17
- package/dist/chunk-YKWJ7QJ2.js.map +1 -0
- package/dist/index.d.ts +225 -0
- package/dist/index.js +3735 -2488
- package/dist/index.js.map +1 -1
- package/dist/provision-roles.js +1 -1
- package/dist/runtime-posture.d.ts +3 -3
- package/dist/schema.d.ts +2631 -749
- package/dist/schema.js +15 -1
- package/dist/workspace-artifacts.d.ts +62 -0
- package/drizzle/0141_social_connection_credentials.sql +8 -0
- package/drizzle/0149_workspace_artifacts.sql +348 -0
- package/drizzle/0150_slack_task_interactions.sql +344 -0
- package/drizzle/0151_slack_delivery_backoff.sql +134 -0
- package/package.json +3 -3
- package/src/index.ts +1217 -12
- package/src/runtime-posture.ts +19 -1
- package/src/schema.ts +409 -16
- package/src/workspace-artifacts.ts +751 -0
- package/dist/chunk-4RUK5CON.js.map +0 -1
- package/dist/chunk-HZD7KVAR.js.map +0 -1
package/src/index.ts
CHANGED
|
@@ -153,6 +153,7 @@ import {
|
|
|
153
153
|
asc,
|
|
154
154
|
desc,
|
|
155
155
|
eq,
|
|
156
|
+
getTableColumns,
|
|
156
157
|
gt,
|
|
157
158
|
gte,
|
|
158
159
|
inArray,
|
|
@@ -1190,6 +1191,8 @@ export const allWorkspacePermissions: Permission[] = [
|
|
|
1190
1191
|
"goals:manage",
|
|
1191
1192
|
"enrollments:read",
|
|
1192
1193
|
"enrollments:manage",
|
|
1194
|
+
"artifacts:read",
|
|
1195
|
+
"artifacts:publish",
|
|
1193
1196
|
];
|
|
1194
1197
|
|
|
1195
1198
|
export const allAccountPermissions: Permission[] = [
|
|
@@ -3379,10 +3382,31 @@ export type CreateSocialConnectionInput = {
|
|
|
3379
3382
|
status: SocialConnectionStatus;
|
|
3380
3383
|
scopes?: string[];
|
|
3381
3384
|
credentialRef?: string | null;
|
|
3385
|
+
credentialEncrypted?: string | null;
|
|
3382
3386
|
tokenMetadata?: Record<string, unknown>;
|
|
3383
3387
|
metadata?: Record<string, unknown>;
|
|
3384
3388
|
};
|
|
3385
3389
|
|
|
3390
|
+
export type UpsertSocialOAuthConnectionInput = {
|
|
3391
|
+
accountId: string;
|
|
3392
|
+
workspaceId: string;
|
|
3393
|
+
provider: SocialProvider;
|
|
3394
|
+
accountHandle: string;
|
|
3395
|
+
accountName?: string | null;
|
|
3396
|
+
externalAccountId?: string | null;
|
|
3397
|
+
scopes: string[];
|
|
3398
|
+
credentialEncrypted: string;
|
|
3399
|
+
tokenMetadata?: Record<string, unknown>;
|
|
3400
|
+
};
|
|
3401
|
+
|
|
3402
|
+
export type UpdateSocialConnectionCredentialInput = {
|
|
3403
|
+
workspaceId: string;
|
|
3404
|
+
connectionId: string;
|
|
3405
|
+
credentialEncrypted?: string | null;
|
|
3406
|
+
status?: SocialConnectionStatus;
|
|
3407
|
+
tokenMetadata?: Record<string, unknown>;
|
|
3408
|
+
};
|
|
3409
|
+
|
|
3386
3410
|
export type CreateSocialPostInput = {
|
|
3387
3411
|
accountId: string;
|
|
3388
3412
|
workspaceId: string;
|
|
@@ -5083,6 +5107,21 @@ function connectionExactSubject(subjectId?: string | null): SQL {
|
|
|
5083
5107
|
: isNull(schema.connections.subjectId);
|
|
5084
5108
|
}
|
|
5085
5109
|
|
|
5110
|
+
function personalSlackCanonicalConnectionOrder(): SQL[] {
|
|
5111
|
+
return [
|
|
5112
|
+
sql`case ${schema.connections.status}
|
|
5113
|
+
when 'active' then 0
|
|
5114
|
+
when 'needs_reauth' then 1
|
|
5115
|
+
when 'error' then 2
|
|
5116
|
+
when 'revoked' then 3
|
|
5117
|
+
else 4
|
|
5118
|
+
end`,
|
|
5119
|
+
desc(schema.connections.updatedAt),
|
|
5120
|
+
desc(schema.connections.createdAt),
|
|
5121
|
+
desc(schema.connections.id),
|
|
5122
|
+
];
|
|
5123
|
+
}
|
|
5124
|
+
|
|
5086
5125
|
async function withConnectionSubjectRls<T>(
|
|
5087
5126
|
db: Database,
|
|
5088
5127
|
workspaceId: string,
|
|
@@ -5297,6 +5336,941 @@ export async function revokeConnection(
|
|
|
5297
5336
|
);
|
|
5298
5337
|
}
|
|
5299
5338
|
|
|
5339
|
+
export type SlackInstallationRoute = {
|
|
5340
|
+
accountId: string;
|
|
5341
|
+
workspaceId: string;
|
|
5342
|
+
connectionId: string;
|
|
5343
|
+
botId: string;
|
|
5344
|
+
botUserId: string;
|
|
5345
|
+
};
|
|
5346
|
+
|
|
5347
|
+
export type SlackBotUserLink = {
|
|
5348
|
+
id: string;
|
|
5349
|
+
accountId: string;
|
|
5350
|
+
workspaceId: string;
|
|
5351
|
+
connectionId: string;
|
|
5352
|
+
slackTeamId: string;
|
|
5353
|
+
slackUserId: string;
|
|
5354
|
+
subjectId: string;
|
|
5355
|
+
linkedBySubjectId: string;
|
|
5356
|
+
createdAt: Date;
|
|
5357
|
+
updatedAt: Date;
|
|
5358
|
+
};
|
|
5359
|
+
|
|
5360
|
+
export type SlackInteractionTriggerKind =
|
|
5361
|
+
| "app_mention"
|
|
5362
|
+
| "dm"
|
|
5363
|
+
| "slash_command"
|
|
5364
|
+
| "message_shortcut"
|
|
5365
|
+
| "thread_reply";
|
|
5366
|
+
|
|
5367
|
+
export type SlackInteractionInboxEntry = {
|
|
5368
|
+
id: string;
|
|
5369
|
+
accountId: string;
|
|
5370
|
+
workspaceId: string;
|
|
5371
|
+
connectionId: string;
|
|
5372
|
+
providerEventId: string;
|
|
5373
|
+
providerMessageId: string;
|
|
5374
|
+
slackTeamId: string;
|
|
5375
|
+
slackUserId: string;
|
|
5376
|
+
slackChannelId: string;
|
|
5377
|
+
slackMessageTs: string;
|
|
5378
|
+
slackThreadTs: string | null;
|
|
5379
|
+
triggerKind: SlackInteractionTriggerKind;
|
|
5380
|
+
text: string;
|
|
5381
|
+
status: "pending" | "processing" | "processed" | "failed";
|
|
5382
|
+
claimHolderId: string | null;
|
|
5383
|
+
claimExpiresAt: Date | null;
|
|
5384
|
+
attemptCount: number;
|
|
5385
|
+
retryAt: Date | null;
|
|
5386
|
+
lastErrorCode: string | null;
|
|
5387
|
+
processedAt: Date | null;
|
|
5388
|
+
createdAt: Date;
|
|
5389
|
+
updatedAt: Date;
|
|
5390
|
+
};
|
|
5391
|
+
|
|
5392
|
+
export type SlackInteraction = {
|
|
5393
|
+
id: string;
|
|
5394
|
+
accountId: string;
|
|
5395
|
+
workspaceId: string;
|
|
5396
|
+
connectionId: string;
|
|
5397
|
+
slackTeamId: string;
|
|
5398
|
+
slackChannelId: string;
|
|
5399
|
+
slackThreadTs: string;
|
|
5400
|
+
routeKey: string;
|
|
5401
|
+
triggeringProviderEventId: string;
|
|
5402
|
+
owningSubjectId: string;
|
|
5403
|
+
visibility: "private" | "workspace";
|
|
5404
|
+
sessionReservationId: string;
|
|
5405
|
+
sessionId: string | null;
|
|
5406
|
+
lastDeliveredSessionEventSequence: number;
|
|
5407
|
+
deliveryClaimHolderId: string | null;
|
|
5408
|
+
deliveryClaimExpiresAt: Date | null;
|
|
5409
|
+
deliveryAttemptCount: number;
|
|
5410
|
+
deliveryRetryAt: Date | null;
|
|
5411
|
+
deliveryLastErrorCode: string | null;
|
|
5412
|
+
ackSlackMessageTs: string | null;
|
|
5413
|
+
progressCount: number;
|
|
5414
|
+
terminalDeliveryState: "open" | "completed" | "failed" | "cancelled" | "blocked";
|
|
5415
|
+
createdAt: Date;
|
|
5416
|
+
updatedAt: Date;
|
|
5417
|
+
};
|
|
5418
|
+
|
|
5419
|
+
export type SlackInteractionProgressDelivery = {
|
|
5420
|
+
id: string;
|
|
5421
|
+
accountId: string;
|
|
5422
|
+
workspaceId: string;
|
|
5423
|
+
interactionId: string;
|
|
5424
|
+
sessionEventSequence: number;
|
|
5425
|
+
slot: number;
|
|
5426
|
+
operationId: string;
|
|
5427
|
+
createdAt: Date;
|
|
5428
|
+
};
|
|
5429
|
+
|
|
5430
|
+
export async function resolveSlackInstallationRoute(
|
|
5431
|
+
db: Database,
|
|
5432
|
+
slackTeamId: string,
|
|
5433
|
+
): Promise<SlackInstallationRoute | null> {
|
|
5434
|
+
const rows = await db.execute<{
|
|
5435
|
+
account_id: string;
|
|
5436
|
+
workspace_id: string;
|
|
5437
|
+
connection_id: string;
|
|
5438
|
+
bot_id: string;
|
|
5439
|
+
bot_user_id: string;
|
|
5440
|
+
}>(sql`select * from opengeni_private.resolve_slack_installation(${slackTeamId})`);
|
|
5441
|
+
const row = rows[0];
|
|
5442
|
+
return row
|
|
5443
|
+
? {
|
|
5444
|
+
accountId: row.account_id,
|
|
5445
|
+
workspaceId: row.workspace_id,
|
|
5446
|
+
connectionId: row.connection_id,
|
|
5447
|
+
botId: row.bot_id,
|
|
5448
|
+
botUserId: row.bot_user_id,
|
|
5449
|
+
}
|
|
5450
|
+
: null;
|
|
5451
|
+
}
|
|
5452
|
+
|
|
5453
|
+
export async function saveSlackBotUserLink(
|
|
5454
|
+
db: Database,
|
|
5455
|
+
input: Omit<SlackBotUserLink, "id" | "createdAt" | "updatedAt">,
|
|
5456
|
+
): Promise<SlackBotUserLink> {
|
|
5457
|
+
return await withWorkspaceRls(db, input.workspaceId, async (scopedDb) => {
|
|
5458
|
+
const [row] = await scopedDb
|
|
5459
|
+
.insert(schema.slackBotUserLinks)
|
|
5460
|
+
.values(input)
|
|
5461
|
+
.onConflictDoUpdate({
|
|
5462
|
+
target: [schema.slackBotUserLinks.connectionId, schema.slackBotUserLinks.slackUserId],
|
|
5463
|
+
set: {
|
|
5464
|
+
accountId: input.accountId,
|
|
5465
|
+
workspaceId: input.workspaceId,
|
|
5466
|
+
slackTeamId: input.slackTeamId,
|
|
5467
|
+
subjectId: input.subjectId,
|
|
5468
|
+
linkedBySubjectId: input.linkedBySubjectId,
|
|
5469
|
+
updatedAt: sql`now()`,
|
|
5470
|
+
},
|
|
5471
|
+
})
|
|
5472
|
+
.returning();
|
|
5473
|
+
if (!row) throw new Error("Slack identity link write returned no row");
|
|
5474
|
+
return mapSlackBotUserLink(row);
|
|
5475
|
+
});
|
|
5476
|
+
}
|
|
5477
|
+
|
|
5478
|
+
export async function getSlackBotUserLink(
|
|
5479
|
+
db: Database,
|
|
5480
|
+
workspaceId: string,
|
|
5481
|
+
connectionId: string,
|
|
5482
|
+
slackUserId: string,
|
|
5483
|
+
): Promise<SlackBotUserLink | null> {
|
|
5484
|
+
return await withWorkspaceRls(db, workspaceId, async (scopedDb) => {
|
|
5485
|
+
const [row] = await scopedDb
|
|
5486
|
+
.select()
|
|
5487
|
+
.from(schema.slackBotUserLinks)
|
|
5488
|
+
.where(
|
|
5489
|
+
and(
|
|
5490
|
+
eq(schema.slackBotUserLinks.workspaceId, workspaceId),
|
|
5491
|
+
eq(schema.slackBotUserLinks.connectionId, connectionId),
|
|
5492
|
+
eq(schema.slackBotUserLinks.slackUserId, slackUserId),
|
|
5493
|
+
),
|
|
5494
|
+
)
|
|
5495
|
+
.limit(1);
|
|
5496
|
+
return row ? mapSlackBotUserLink(row) : null;
|
|
5497
|
+
});
|
|
5498
|
+
}
|
|
5499
|
+
|
|
5500
|
+
export async function deleteSlackBotUserLink(
|
|
5501
|
+
db: Database,
|
|
5502
|
+
workspaceId: string,
|
|
5503
|
+
connectionId: string,
|
|
5504
|
+
slackUserId: string,
|
|
5505
|
+
): Promise<boolean> {
|
|
5506
|
+
return await withWorkspaceRls(db, workspaceId, async (scopedDb) => {
|
|
5507
|
+
const rows = await scopedDb
|
|
5508
|
+
.delete(schema.slackBotUserLinks)
|
|
5509
|
+
.where(
|
|
5510
|
+
and(
|
|
5511
|
+
eq(schema.slackBotUserLinks.workspaceId, workspaceId),
|
|
5512
|
+
eq(schema.slackBotUserLinks.connectionId, connectionId),
|
|
5513
|
+
eq(schema.slackBotUserLinks.slackUserId, slackUserId),
|
|
5514
|
+
),
|
|
5515
|
+
)
|
|
5516
|
+
.returning({ id: schema.slackBotUserLinks.id });
|
|
5517
|
+
return rows.length > 0;
|
|
5518
|
+
});
|
|
5519
|
+
}
|
|
5520
|
+
|
|
5521
|
+
export async function enqueueSlackInteractionInbox(
|
|
5522
|
+
db: Database,
|
|
5523
|
+
input: Omit<
|
|
5524
|
+
SlackInteractionInboxEntry,
|
|
5525
|
+
| "id"
|
|
5526
|
+
| "status"
|
|
5527
|
+
| "claimHolderId"
|
|
5528
|
+
| "claimExpiresAt"
|
|
5529
|
+
| "attemptCount"
|
|
5530
|
+
| "retryAt"
|
|
5531
|
+
| "lastErrorCode"
|
|
5532
|
+
| "processedAt"
|
|
5533
|
+
| "createdAt"
|
|
5534
|
+
| "updatedAt"
|
|
5535
|
+
>,
|
|
5536
|
+
): Promise<{ inserted: boolean; entry: SlackInteractionInboxEntry }> {
|
|
5537
|
+
return await withRlsContext(
|
|
5538
|
+
db,
|
|
5539
|
+
{ accountId: input.accountId, workspaceId: input.workspaceId },
|
|
5540
|
+
async (scopedDb) => {
|
|
5541
|
+
const [created] = await scopedDb
|
|
5542
|
+
.insert(schema.slackInteractionInbox)
|
|
5543
|
+
.values(input)
|
|
5544
|
+
.onConflictDoNothing()
|
|
5545
|
+
.returning();
|
|
5546
|
+
if (created) return { inserted: true, entry: mapSlackInteractionInbox(created) };
|
|
5547
|
+
const [existing] = await scopedDb
|
|
5548
|
+
.select()
|
|
5549
|
+
.from(schema.slackInteractionInbox)
|
|
5550
|
+
.where(
|
|
5551
|
+
and(
|
|
5552
|
+
eq(schema.slackInteractionInbox.connectionId, input.connectionId),
|
|
5553
|
+
or(
|
|
5554
|
+
eq(schema.slackInteractionInbox.providerEventId, input.providerEventId),
|
|
5555
|
+
eq(schema.slackInteractionInbox.providerMessageId, input.providerMessageId),
|
|
5556
|
+
),
|
|
5557
|
+
),
|
|
5558
|
+
)
|
|
5559
|
+
.limit(1);
|
|
5560
|
+
if (!existing) throw new Error("Slack inbox idempotency conflict could not be resolved");
|
|
5561
|
+
return { inserted: false, entry: mapSlackInteractionInbox(existing) };
|
|
5562
|
+
},
|
|
5563
|
+
);
|
|
5564
|
+
}
|
|
5565
|
+
|
|
5566
|
+
export async function claimSlackInteractionInbox(
|
|
5567
|
+
db: Database,
|
|
5568
|
+
claimHolderId: string,
|
|
5569
|
+
claimLeaseMs: number,
|
|
5570
|
+
): Promise<SlackInteractionInboxEntry | null> {
|
|
5571
|
+
const rows = await db.execute<typeof schema.slackInteractionInbox.$inferSelect>(
|
|
5572
|
+
sql`select * from opengeni_private.claim_slack_interaction_inbox(${claimHolderId}::uuid, ${claimLeaseMs})`,
|
|
5573
|
+
);
|
|
5574
|
+
return rows[0] ? mapSlackInteractionInbox(rows[0]) : null;
|
|
5575
|
+
}
|
|
5576
|
+
|
|
5577
|
+
export async function settleSlackInteractionInbox(
|
|
5578
|
+
db: Database,
|
|
5579
|
+
input: {
|
|
5580
|
+
entry: Pick<SlackInteractionInboxEntry, "id" | "accountId" | "workspaceId">;
|
|
5581
|
+
claimHolderId: string;
|
|
5582
|
+
outcome: "processed" | "failed";
|
|
5583
|
+
errorCode?: string | null;
|
|
5584
|
+
},
|
|
5585
|
+
): Promise<boolean> {
|
|
5586
|
+
return await withRlsContext(db, input.entry, async (scopedDb) => {
|
|
5587
|
+
const rows = await scopedDb
|
|
5588
|
+
.update(schema.slackInteractionInbox)
|
|
5589
|
+
.set({
|
|
5590
|
+
status: input.outcome,
|
|
5591
|
+
claimHolderId: null,
|
|
5592
|
+
claimExpiresAt: null,
|
|
5593
|
+
retryAt: null,
|
|
5594
|
+
processedAt: sql`now()`,
|
|
5595
|
+
lastErrorCode: input.errorCode ?? null,
|
|
5596
|
+
updatedAt: sql`now()`,
|
|
5597
|
+
})
|
|
5598
|
+
.where(
|
|
5599
|
+
and(
|
|
5600
|
+
eq(schema.slackInteractionInbox.id, input.entry.id),
|
|
5601
|
+
eq(schema.slackInteractionInbox.claimHolderId, input.claimHolderId),
|
|
5602
|
+
eq(schema.slackInteractionInbox.status, "processing"),
|
|
5603
|
+
),
|
|
5604
|
+
)
|
|
5605
|
+
.returning({ id: schema.slackInteractionInbox.id });
|
|
5606
|
+
return rows.length === 1;
|
|
5607
|
+
});
|
|
5608
|
+
}
|
|
5609
|
+
|
|
5610
|
+
export async function releaseSlackInteractionInbox(
|
|
5611
|
+
db: Database,
|
|
5612
|
+
input: {
|
|
5613
|
+
entry: Pick<SlackInteractionInboxEntry, "id" | "accountId" | "workspaceId">;
|
|
5614
|
+
claimHolderId: string;
|
|
5615
|
+
errorCode: string;
|
|
5616
|
+
retryAt: Date;
|
|
5617
|
+
},
|
|
5618
|
+
): Promise<boolean> {
|
|
5619
|
+
return await withRlsContext(db, input.entry, async (scopedDb) => {
|
|
5620
|
+
const rows = await scopedDb
|
|
5621
|
+
.update(schema.slackInteractionInbox)
|
|
5622
|
+
.set({
|
|
5623
|
+
status: "pending",
|
|
5624
|
+
claimHolderId: null,
|
|
5625
|
+
claimExpiresAt: null,
|
|
5626
|
+
retryAt: input.retryAt,
|
|
5627
|
+
lastErrorCode: input.errorCode,
|
|
5628
|
+
updatedAt: sql`now()`,
|
|
5629
|
+
})
|
|
5630
|
+
.where(
|
|
5631
|
+
and(
|
|
5632
|
+
eq(schema.slackInteractionInbox.id, input.entry.id),
|
|
5633
|
+
eq(schema.slackInteractionInbox.claimHolderId, input.claimHolderId),
|
|
5634
|
+
eq(schema.slackInteractionInbox.status, "processing"),
|
|
5635
|
+
),
|
|
5636
|
+
)
|
|
5637
|
+
.returning({ id: schema.slackInteractionInbox.id });
|
|
5638
|
+
return rows.length === 1;
|
|
5639
|
+
});
|
|
5640
|
+
}
|
|
5641
|
+
|
|
5642
|
+
export async function getOrCreateSlackInteraction(
|
|
5643
|
+
db: Database,
|
|
5644
|
+
input: Omit<
|
|
5645
|
+
SlackInteraction,
|
|
5646
|
+
| "id"
|
|
5647
|
+
| "sessionReservationId"
|
|
5648
|
+
| "sessionId"
|
|
5649
|
+
| "lastDeliveredSessionEventSequence"
|
|
5650
|
+
| "deliveryClaimHolderId"
|
|
5651
|
+
| "deliveryClaimExpiresAt"
|
|
5652
|
+
| "deliveryAttemptCount"
|
|
5653
|
+
| "deliveryRetryAt"
|
|
5654
|
+
| "deliveryLastErrorCode"
|
|
5655
|
+
| "ackSlackMessageTs"
|
|
5656
|
+
| "progressCount"
|
|
5657
|
+
| "terminalDeliveryState"
|
|
5658
|
+
| "createdAt"
|
|
5659
|
+
| "updatedAt"
|
|
5660
|
+
>,
|
|
5661
|
+
): Promise<{ created: boolean; interaction: SlackInteraction }> {
|
|
5662
|
+
return await withRlsContext(db, input, async (scopedDb) => {
|
|
5663
|
+
const [created] = await scopedDb
|
|
5664
|
+
.insert(schema.slackInteractions)
|
|
5665
|
+
.values(input)
|
|
5666
|
+
.onConflictDoNothing()
|
|
5667
|
+
.returning();
|
|
5668
|
+
if (created) return { created: true, interaction: mapSlackInteraction(created) };
|
|
5669
|
+
const [existing] = await scopedDb
|
|
5670
|
+
.select()
|
|
5671
|
+
.from(schema.slackInteractions)
|
|
5672
|
+
.where(
|
|
5673
|
+
and(
|
|
5674
|
+
eq(schema.slackInteractions.connectionId, input.connectionId),
|
|
5675
|
+
eq(schema.slackInteractions.routeKey, input.routeKey),
|
|
5676
|
+
),
|
|
5677
|
+
)
|
|
5678
|
+
.limit(1);
|
|
5679
|
+
if (!existing) throw new Error("Slack route idempotency conflict could not be resolved");
|
|
5680
|
+
return { created: false, interaction: mapSlackInteraction(existing) };
|
|
5681
|
+
});
|
|
5682
|
+
}
|
|
5683
|
+
|
|
5684
|
+
export async function getSlackInteractionByRoute(
|
|
5685
|
+
db: Database,
|
|
5686
|
+
workspaceId: string,
|
|
5687
|
+
connectionId: string,
|
|
5688
|
+
routeKey: string,
|
|
5689
|
+
): Promise<SlackInteraction | null> {
|
|
5690
|
+
return await withWorkspaceRls(db, workspaceId, async (scopedDb) => {
|
|
5691
|
+
const [row] = await scopedDb
|
|
5692
|
+
.select()
|
|
5693
|
+
.from(schema.slackInteractions)
|
|
5694
|
+
.where(
|
|
5695
|
+
and(
|
|
5696
|
+
eq(schema.slackInteractions.workspaceId, workspaceId),
|
|
5697
|
+
eq(schema.slackInteractions.connectionId, connectionId),
|
|
5698
|
+
eq(schema.slackInteractions.routeKey, routeKey),
|
|
5699
|
+
),
|
|
5700
|
+
)
|
|
5701
|
+
.limit(1);
|
|
5702
|
+
return row ? mapSlackInteraction(row) : null;
|
|
5703
|
+
});
|
|
5704
|
+
}
|
|
5705
|
+
|
|
5706
|
+
export async function getSlackInteractionSessionAccess(
|
|
5707
|
+
db: Database,
|
|
5708
|
+
workspaceId: string,
|
|
5709
|
+
rootSessionId: string,
|
|
5710
|
+
): Promise<Pick<SlackInteraction, "owningSubjectId" | "visibility"> | null> {
|
|
5711
|
+
return await withWorkspaceRls(db, workspaceId, async (scopedDb) => {
|
|
5712
|
+
const [row] = await scopedDb
|
|
5713
|
+
.select({
|
|
5714
|
+
owningSubjectId: schema.slackInteractions.owningSubjectId,
|
|
5715
|
+
visibility: schema.slackInteractions.visibility,
|
|
5716
|
+
})
|
|
5717
|
+
.from(schema.slackInteractions)
|
|
5718
|
+
.where(
|
|
5719
|
+
and(
|
|
5720
|
+
eq(schema.slackInteractions.workspaceId, workspaceId),
|
|
5721
|
+
eq(schema.slackInteractions.sessionReservationId, rootSessionId),
|
|
5722
|
+
),
|
|
5723
|
+
)
|
|
5724
|
+
.limit(1);
|
|
5725
|
+
return row ?? null;
|
|
5726
|
+
});
|
|
5727
|
+
}
|
|
5728
|
+
|
|
5729
|
+
export async function getSlackInteractionSessionAccessForSession(
|
|
5730
|
+
db: Database,
|
|
5731
|
+
input: {
|
|
5732
|
+
accountId: string;
|
|
5733
|
+
workspaceId: string;
|
|
5734
|
+
sessionId: string;
|
|
5735
|
+
},
|
|
5736
|
+
): Promise<
|
|
5737
|
+
(Pick<SlackInteraction, "owningSubjectId" | "visibility"> & { rootSessionId: string }) | null
|
|
5738
|
+
> {
|
|
5739
|
+
return await withRlsContext(db, input, async (scopedDb) => {
|
|
5740
|
+
const rows = await scopedDb.execute<{
|
|
5741
|
+
rootSessionId: string;
|
|
5742
|
+
parentSessionId: string | null;
|
|
5743
|
+
depth: number;
|
|
5744
|
+
cycle: boolean;
|
|
5745
|
+
owningSubjectId: string | null;
|
|
5746
|
+
visibility: SlackInteraction["visibility"] | null;
|
|
5747
|
+
}>(sql`
|
|
5748
|
+
with recursive lineage(id, parent_session_id, depth, path, cycle) as (
|
|
5749
|
+
select
|
|
5750
|
+
${schema.sessions.id},
|
|
5751
|
+
${schema.sessions.parentSessionId},
|
|
5752
|
+
0,
|
|
5753
|
+
array[${schema.sessions.id}],
|
|
5754
|
+
false
|
|
5755
|
+
from ${schema.sessions}
|
|
5756
|
+
where ${schema.sessions.accountId} = ${input.accountId}
|
|
5757
|
+
and ${schema.sessions.workspaceId} = ${input.workspaceId}
|
|
5758
|
+
and ${schema.sessions.id} = ${input.sessionId}
|
|
5759
|
+
union all
|
|
5760
|
+
select
|
|
5761
|
+
parent.id,
|
|
5762
|
+
parent.parent_session_id,
|
|
5763
|
+
lineage.depth + 1,
|
|
5764
|
+
lineage.path || parent.id,
|
|
5765
|
+
parent.id = any(lineage.path)
|
|
5766
|
+
from ${schema.sessions} parent
|
|
5767
|
+
join lineage on lineage.parent_session_id = parent.id
|
|
5768
|
+
where parent.account_id = ${input.accountId}
|
|
5769
|
+
and parent.workspace_id = ${input.workspaceId}
|
|
5770
|
+
and not lineage.cycle
|
|
5771
|
+
and lineage.depth < 64
|
|
5772
|
+
), root as (
|
|
5773
|
+
select id, parent_session_id, depth, cycle
|
|
5774
|
+
from lineage
|
|
5775
|
+
order by depth desc
|
|
5776
|
+
limit 1
|
|
5777
|
+
)
|
|
5778
|
+
select
|
|
5779
|
+
root.id as "rootSessionId",
|
|
5780
|
+
root.parent_session_id as "parentSessionId",
|
|
5781
|
+
root.depth,
|
|
5782
|
+
root.cycle,
|
|
5783
|
+
interaction.owning_subject_id as "owningSubjectId",
|
|
5784
|
+
interaction.visibility
|
|
5785
|
+
from root
|
|
5786
|
+
left join ${schema.slackInteractions} interaction
|
|
5787
|
+
on interaction.account_id = ${input.accountId}
|
|
5788
|
+
and interaction.workspace_id = ${input.workspaceId}
|
|
5789
|
+
and interaction.session_reservation_id = root.id
|
|
5790
|
+
`);
|
|
5791
|
+
const root = rows[0];
|
|
5792
|
+
if (!root) return null;
|
|
5793
|
+
if (root.cycle || root.parentSessionId !== null || Number(root.depth) >= 64) {
|
|
5794
|
+
throw new Error(`session lineage for ${input.sessionId} has no valid workspace root`);
|
|
5795
|
+
}
|
|
5796
|
+
if (!root.owningSubjectId || !root.visibility) return null;
|
|
5797
|
+
return {
|
|
5798
|
+
rootSessionId: root.rootSessionId,
|
|
5799
|
+
owningSubjectId: root.owningSubjectId,
|
|
5800
|
+
visibility: root.visibility,
|
|
5801
|
+
};
|
|
5802
|
+
});
|
|
5803
|
+
}
|
|
5804
|
+
|
|
5805
|
+
export async function bindSlackInteractionSession(
|
|
5806
|
+
db: Database,
|
|
5807
|
+
input: Pick<SlackInteraction, "id" | "accountId" | "workspaceId" | "owningSubjectId"> & {
|
|
5808
|
+
sessionId: string;
|
|
5809
|
+
},
|
|
5810
|
+
): Promise<SlackInteraction | null> {
|
|
5811
|
+
return await withRlsContext(db, input, async (scopedDb) => {
|
|
5812
|
+
const [row] = await scopedDb
|
|
5813
|
+
.update(schema.slackInteractions)
|
|
5814
|
+
.set({ sessionId: input.sessionId, updatedAt: sql`now()` })
|
|
5815
|
+
.where(
|
|
5816
|
+
and(
|
|
5817
|
+
eq(schema.slackInteractions.id, input.id),
|
|
5818
|
+
eq(schema.slackInteractions.owningSubjectId, input.owningSubjectId),
|
|
5819
|
+
eq(schema.slackInteractions.sessionReservationId, input.sessionId),
|
|
5820
|
+
isNull(schema.slackInteractions.sessionId),
|
|
5821
|
+
),
|
|
5822
|
+
)
|
|
5823
|
+
.returning();
|
|
5824
|
+
if (row) return mapSlackInteraction(row);
|
|
5825
|
+
const [existing] = await scopedDb
|
|
5826
|
+
.select()
|
|
5827
|
+
.from(schema.slackInteractions)
|
|
5828
|
+
.where(eq(schema.slackInteractions.id, input.id))
|
|
5829
|
+
.limit(1);
|
|
5830
|
+
return existing?.sessionId === input.sessionId ? mapSlackInteraction(existing) : null;
|
|
5831
|
+
});
|
|
5832
|
+
}
|
|
5833
|
+
|
|
5834
|
+
export type SlackInteractionProgressClaim =
|
|
5835
|
+
| {
|
|
5836
|
+
kind: "claimed";
|
|
5837
|
+
created: boolean;
|
|
5838
|
+
progressCount: number;
|
|
5839
|
+
delivery: SlackInteractionProgressDelivery;
|
|
5840
|
+
}
|
|
5841
|
+
| { kind: "limit_reached"; progressCount: number }
|
|
5842
|
+
| { kind: "not_owned" };
|
|
5843
|
+
|
|
5844
|
+
/**
|
|
5845
|
+
* Reserve one globally bounded progress slot before any Slack provider call.
|
|
5846
|
+
* The interaction row lock serializes replicas and expired delivery claim
|
|
5847
|
+
* successors; the event row preserves one operation UUID across every retry.
|
|
5848
|
+
*/
|
|
5849
|
+
export async function claimSlackInteractionProgressDelivery(
|
|
5850
|
+
db: Database,
|
|
5851
|
+
input: {
|
|
5852
|
+
accountId: string;
|
|
5853
|
+
workspaceId: string;
|
|
5854
|
+
interactionId: string;
|
|
5855
|
+
claimHolderId: string;
|
|
5856
|
+
sessionEventSequence: number;
|
|
5857
|
+
maxProgress: number;
|
|
5858
|
+
},
|
|
5859
|
+
): Promise<SlackInteractionProgressClaim> {
|
|
5860
|
+
if (
|
|
5861
|
+
!Number.isSafeInteger(input.sessionEventSequence) ||
|
|
5862
|
+
input.sessionEventSequence < 1 ||
|
|
5863
|
+
!Number.isSafeInteger(input.maxProgress) ||
|
|
5864
|
+
input.maxProgress < 1 ||
|
|
5865
|
+
input.maxProgress > 3
|
|
5866
|
+
) {
|
|
5867
|
+
throw new RangeError("invalid Slack progress delivery claim bounds");
|
|
5868
|
+
}
|
|
5869
|
+
return await withRlsContext(db, input, async (scopedDb) => {
|
|
5870
|
+
const [interaction] = await scopedDb
|
|
5871
|
+
.select({
|
|
5872
|
+
progressCount: schema.slackInteractions.progressCount,
|
|
5873
|
+
deliveryClaimHolderId: schema.slackInteractions.deliveryClaimHolderId,
|
|
5874
|
+
terminalDeliveryState: schema.slackInteractions.terminalDeliveryState,
|
|
5875
|
+
})
|
|
5876
|
+
.from(schema.slackInteractions)
|
|
5877
|
+
.where(
|
|
5878
|
+
and(
|
|
5879
|
+
eq(schema.slackInteractions.id, input.interactionId),
|
|
5880
|
+
eq(schema.slackInteractions.accountId, input.accountId),
|
|
5881
|
+
eq(schema.slackInteractions.workspaceId, input.workspaceId),
|
|
5882
|
+
),
|
|
5883
|
+
)
|
|
5884
|
+
.for("update")
|
|
5885
|
+
.limit(1);
|
|
5886
|
+
if (
|
|
5887
|
+
!interaction ||
|
|
5888
|
+
interaction.deliveryClaimHolderId !== input.claimHolderId ||
|
|
5889
|
+
interaction.terminalDeliveryState !== "open"
|
|
5890
|
+
) {
|
|
5891
|
+
return { kind: "not_owned" };
|
|
5892
|
+
}
|
|
5893
|
+
|
|
5894
|
+
const [existing] = await scopedDb
|
|
5895
|
+
.select()
|
|
5896
|
+
.from(schema.slackInteractionProgressDeliveries)
|
|
5897
|
+
.where(
|
|
5898
|
+
and(
|
|
5899
|
+
eq(schema.slackInteractionProgressDeliveries.interactionId, input.interactionId),
|
|
5900
|
+
eq(
|
|
5901
|
+
schema.slackInteractionProgressDeliveries.sessionEventSequence,
|
|
5902
|
+
input.sessionEventSequence,
|
|
5903
|
+
),
|
|
5904
|
+
),
|
|
5905
|
+
)
|
|
5906
|
+
.limit(1);
|
|
5907
|
+
if (existing) {
|
|
5908
|
+
return {
|
|
5909
|
+
kind: "claimed",
|
|
5910
|
+
created: false,
|
|
5911
|
+
progressCount: interaction.progressCount,
|
|
5912
|
+
delivery: mapSlackInteractionProgressDelivery(existing),
|
|
5913
|
+
};
|
|
5914
|
+
}
|
|
5915
|
+
if (interaction.progressCount >= input.maxProgress) {
|
|
5916
|
+
return { kind: "limit_reached", progressCount: interaction.progressCount };
|
|
5917
|
+
}
|
|
5918
|
+
|
|
5919
|
+
const slot = interaction.progressCount + 1;
|
|
5920
|
+
const [delivery] = await scopedDb
|
|
5921
|
+
.insert(schema.slackInteractionProgressDeliveries)
|
|
5922
|
+
.values({
|
|
5923
|
+
accountId: input.accountId,
|
|
5924
|
+
workspaceId: input.workspaceId,
|
|
5925
|
+
interactionId: input.interactionId,
|
|
5926
|
+
sessionEventSequence: input.sessionEventSequence,
|
|
5927
|
+
slot,
|
|
5928
|
+
operationId: crypto.randomUUID(),
|
|
5929
|
+
})
|
|
5930
|
+
.returning();
|
|
5931
|
+
if (!delivery) throw new Error("Slack progress delivery claim returned no row");
|
|
5932
|
+
const [updated] = await scopedDb
|
|
5933
|
+
.update(schema.slackInteractions)
|
|
5934
|
+
.set({ progressCount: slot, updatedAt: sql`now()` })
|
|
5935
|
+
.where(
|
|
5936
|
+
and(
|
|
5937
|
+
eq(schema.slackInteractions.id, input.interactionId),
|
|
5938
|
+
eq(schema.slackInteractions.deliveryClaimHolderId, input.claimHolderId),
|
|
5939
|
+
eq(schema.slackInteractions.progressCount, interaction.progressCount),
|
|
5940
|
+
),
|
|
5941
|
+
)
|
|
5942
|
+
.returning({ progressCount: schema.slackInteractions.progressCount });
|
|
5943
|
+
if (!updated) throw new Error("Slack progress delivery lost its durable interaction claim");
|
|
5944
|
+
return {
|
|
5945
|
+
kind: "claimed",
|
|
5946
|
+
created: true,
|
|
5947
|
+
progressCount: updated.progressCount,
|
|
5948
|
+
delivery: mapSlackInteractionProgressDelivery(delivery),
|
|
5949
|
+
};
|
|
5950
|
+
});
|
|
5951
|
+
}
|
|
5952
|
+
|
|
5953
|
+
export async function rekeySlackInteractionRoute(
|
|
5954
|
+
db: Database,
|
|
5955
|
+
input: Pick<SlackInteraction, "id" | "accountId" | "workspaceId"> & {
|
|
5956
|
+
routeKey: string;
|
|
5957
|
+
slackThreadTs: string;
|
|
5958
|
+
ackSlackMessageTs: string;
|
|
5959
|
+
},
|
|
5960
|
+
): Promise<SlackInteraction | null> {
|
|
5961
|
+
return await withRlsContext(db, input, async (scopedDb) => {
|
|
5962
|
+
const [row] = await scopedDb
|
|
5963
|
+
.update(schema.slackInteractions)
|
|
5964
|
+
.set({
|
|
5965
|
+
routeKey: input.routeKey,
|
|
5966
|
+
slackThreadTs: input.slackThreadTs,
|
|
5967
|
+
ackSlackMessageTs: input.ackSlackMessageTs,
|
|
5968
|
+
updatedAt: sql`now()`,
|
|
5969
|
+
})
|
|
5970
|
+
.where(eq(schema.slackInteractions.id, input.id))
|
|
5971
|
+
.returning();
|
|
5972
|
+
return row ? mapSlackInteraction(row) : null;
|
|
5973
|
+
});
|
|
5974
|
+
}
|
|
5975
|
+
|
|
5976
|
+
export async function claimSlackInteractionDelivery(
|
|
5977
|
+
db: Database,
|
|
5978
|
+
claimHolderId: string,
|
|
5979
|
+
claimLeaseMs: number,
|
|
5980
|
+
): Promise<SlackInteraction | null> {
|
|
5981
|
+
const rows = await db.execute<typeof schema.slackInteractions.$inferSelect>(
|
|
5982
|
+
sql`select * from opengeni_private.claim_slack_interaction_delivery(${claimHolderId}::uuid, ${claimLeaseMs})`,
|
|
5983
|
+
);
|
|
5984
|
+
return rows[0] ? mapSlackInteraction(rows[0]) : null;
|
|
5985
|
+
}
|
|
5986
|
+
|
|
5987
|
+
export async function reopenSlackInteractionDelivery(
|
|
5988
|
+
db: Database,
|
|
5989
|
+
input: Pick<SlackInteraction, "id" | "accountId" | "workspaceId">,
|
|
5990
|
+
): Promise<boolean> {
|
|
5991
|
+
return await withRlsContext(db, input, async (scopedDb) => {
|
|
5992
|
+
const rows = await scopedDb
|
|
5993
|
+
.update(schema.slackInteractions)
|
|
5994
|
+
.set({
|
|
5995
|
+
terminalDeliveryState: "open",
|
|
5996
|
+
deliveryAttemptCount: 0,
|
|
5997
|
+
deliveryRetryAt: null,
|
|
5998
|
+
deliveryLastErrorCode: null,
|
|
5999
|
+
updatedAt: sql`now()`,
|
|
6000
|
+
})
|
|
6001
|
+
.where(eq(schema.slackInteractions.id, input.id))
|
|
6002
|
+
.returning({ id: schema.slackInteractions.id });
|
|
6003
|
+
return rows.length === 1;
|
|
6004
|
+
});
|
|
6005
|
+
}
|
|
6006
|
+
|
|
6007
|
+
export async function advanceSlackInteractionDelivery(
|
|
6008
|
+
db: Database,
|
|
6009
|
+
input: Pick<SlackInteraction, "id" | "accountId" | "workspaceId"> & {
|
|
6010
|
+
claimHolderId: string;
|
|
6011
|
+
sequence: number;
|
|
6012
|
+
ackSlackMessageTs?: string | null;
|
|
6013
|
+
},
|
|
6014
|
+
): Promise<boolean> {
|
|
6015
|
+
return await withRlsContext(db, input, async (scopedDb) => {
|
|
6016
|
+
const rows = await scopedDb
|
|
6017
|
+
.update(schema.slackInteractions)
|
|
6018
|
+
.set({
|
|
6019
|
+
lastDeliveredSessionEventSequence: input.sequence,
|
|
6020
|
+
...(input.ackSlackMessageTs !== undefined
|
|
6021
|
+
? { ackSlackMessageTs: input.ackSlackMessageTs }
|
|
6022
|
+
: {}),
|
|
6023
|
+
deliveryAttemptCount: 0,
|
|
6024
|
+
deliveryRetryAt: null,
|
|
6025
|
+
deliveryLastErrorCode: null,
|
|
6026
|
+
updatedAt: sql`now()`,
|
|
6027
|
+
})
|
|
6028
|
+
.where(
|
|
6029
|
+
and(
|
|
6030
|
+
eq(schema.slackInteractions.id, input.id),
|
|
6031
|
+
eq(schema.slackInteractions.deliveryClaimHolderId, input.claimHolderId),
|
|
6032
|
+
lte(schema.slackInteractions.lastDeliveredSessionEventSequence, input.sequence),
|
|
6033
|
+
),
|
|
6034
|
+
)
|
|
6035
|
+
.returning({ id: schema.slackInteractions.id });
|
|
6036
|
+
return rows.length === 1;
|
|
6037
|
+
});
|
|
6038
|
+
}
|
|
6039
|
+
|
|
6040
|
+
export async function releaseSlackInteractionDelivery(
|
|
6041
|
+
db: Database,
|
|
6042
|
+
input: Pick<SlackInteraction, "id" | "accountId" | "workspaceId"> & {
|
|
6043
|
+
claimHolderId: string;
|
|
6044
|
+
},
|
|
6045
|
+
): Promise<boolean> {
|
|
6046
|
+
return await withRlsContext(db, input, async (scopedDb) => {
|
|
6047
|
+
const rows = await scopedDb
|
|
6048
|
+
.update(schema.slackInteractions)
|
|
6049
|
+
.set({
|
|
6050
|
+
deliveryClaimHolderId: null,
|
|
6051
|
+
deliveryClaimExpiresAt: null,
|
|
6052
|
+
updatedAt: sql`now()`,
|
|
6053
|
+
})
|
|
6054
|
+
.where(
|
|
6055
|
+
and(
|
|
6056
|
+
eq(schema.slackInteractions.id, input.id),
|
|
6057
|
+
eq(schema.slackInteractions.deliveryClaimHolderId, input.claimHolderId),
|
|
6058
|
+
),
|
|
6059
|
+
)
|
|
6060
|
+
.returning({ id: schema.slackInteractions.id });
|
|
6061
|
+
return rows.length === 1;
|
|
6062
|
+
});
|
|
6063
|
+
}
|
|
6064
|
+
|
|
6065
|
+
export async function deferSlackInteractionDelivery(
|
|
6066
|
+
db: Database,
|
|
6067
|
+
input: Pick<SlackInteraction, "id" | "accountId" | "workspaceId"> & {
|
|
6068
|
+
claimHolderId: string;
|
|
6069
|
+
retryAt: Date;
|
|
6070
|
+
errorCode: string;
|
|
6071
|
+
},
|
|
6072
|
+
): Promise<boolean> {
|
|
6073
|
+
return await withRlsContext(db, input, async (scopedDb) => {
|
|
6074
|
+
const rows = await scopedDb
|
|
6075
|
+
.update(schema.slackInteractions)
|
|
6076
|
+
.set({
|
|
6077
|
+
deliveryClaimHolderId: null,
|
|
6078
|
+
deliveryClaimExpiresAt: null,
|
|
6079
|
+
deliveryRetryAt: input.retryAt,
|
|
6080
|
+
deliveryLastErrorCode: input.errorCode.slice(0, 128),
|
|
6081
|
+
updatedAt: sql`now()`,
|
|
6082
|
+
})
|
|
6083
|
+
.where(
|
|
6084
|
+
and(
|
|
6085
|
+
eq(schema.slackInteractions.id, input.id),
|
|
6086
|
+
eq(schema.slackInteractions.deliveryClaimHolderId, input.claimHolderId),
|
|
6087
|
+
),
|
|
6088
|
+
)
|
|
6089
|
+
.returning({ id: schema.slackInteractions.id });
|
|
6090
|
+
return rows.length === 1;
|
|
6091
|
+
});
|
|
6092
|
+
}
|
|
6093
|
+
|
|
6094
|
+
export async function closeSlackInteractionDelivery(
|
|
6095
|
+
db: Database,
|
|
6096
|
+
input: Pick<SlackInteraction, "id" | "accountId" | "workspaceId"> & {
|
|
6097
|
+
claimHolderId: string;
|
|
6098
|
+
sequence: number;
|
|
6099
|
+
state: Exclude<SlackInteraction["terminalDeliveryState"], "open">;
|
|
6100
|
+
errorCode?: string | null;
|
|
6101
|
+
},
|
|
6102
|
+
): Promise<boolean> {
|
|
6103
|
+
return await withRlsContext(db, input, async (scopedDb) => {
|
|
6104
|
+
const rows = await scopedDb
|
|
6105
|
+
.update(schema.slackInteractions)
|
|
6106
|
+
.set({
|
|
6107
|
+
lastDeliveredSessionEventSequence: input.sequence,
|
|
6108
|
+
terminalDeliveryState: input.state,
|
|
6109
|
+
deliveryClaimHolderId: null,
|
|
6110
|
+
deliveryClaimExpiresAt: null,
|
|
6111
|
+
deliveryRetryAt: null,
|
|
6112
|
+
deliveryLastErrorCode: input.errorCode?.slice(0, 128) ?? null,
|
|
6113
|
+
updatedAt: sql`now()`,
|
|
6114
|
+
})
|
|
6115
|
+
.where(
|
|
6116
|
+
and(
|
|
6117
|
+
eq(schema.slackInteractions.id, input.id),
|
|
6118
|
+
eq(schema.slackInteractions.deliveryClaimHolderId, input.claimHolderId),
|
|
6119
|
+
),
|
|
6120
|
+
)
|
|
6121
|
+
.returning({ id: schema.slackInteractions.id });
|
|
6122
|
+
return rows.length === 1;
|
|
6123
|
+
});
|
|
6124
|
+
}
|
|
6125
|
+
|
|
6126
|
+
function mapSlackBotUserLink(row: typeof schema.slackBotUserLinks.$inferSelect): SlackBotUserLink {
|
|
6127
|
+
return row;
|
|
6128
|
+
}
|
|
6129
|
+
|
|
6130
|
+
function mapSlackInteractionInbox(
|
|
6131
|
+
row: typeof schema.slackInteractionInbox.$inferSelect | Record<string, unknown>,
|
|
6132
|
+
): SlackInteractionInboxEntry {
|
|
6133
|
+
return {
|
|
6134
|
+
id: slackRowString(row, "id", "id"),
|
|
6135
|
+
accountId: slackRowString(row, "accountId", "account_id"),
|
|
6136
|
+
workspaceId: slackRowString(row, "workspaceId", "workspace_id"),
|
|
6137
|
+
connectionId: slackRowString(row, "connectionId", "connection_id"),
|
|
6138
|
+
providerEventId: slackRowString(row, "providerEventId", "provider_event_id"),
|
|
6139
|
+
providerMessageId: slackRowString(row, "providerMessageId", "provider_message_id"),
|
|
6140
|
+
slackTeamId: slackRowString(row, "slackTeamId", "slack_team_id"),
|
|
6141
|
+
slackUserId: slackRowString(row, "slackUserId", "slack_user_id"),
|
|
6142
|
+
slackChannelId: slackRowString(row, "slackChannelId", "slack_channel_id"),
|
|
6143
|
+
slackMessageTs: slackRowString(row, "slackMessageTs", "slack_message_ts"),
|
|
6144
|
+
slackThreadTs: slackRowNullableString(row, "slackThreadTs", "slack_thread_ts"),
|
|
6145
|
+
triggerKind: slackRowString(row, "triggerKind", "trigger_kind") as SlackInteractionTriggerKind,
|
|
6146
|
+
text: slackRowString(row, "text", "text"),
|
|
6147
|
+
status: slackRowString(row, "status", "status") as SlackInteractionInboxEntry["status"],
|
|
6148
|
+
claimHolderId: slackRowNullableString(row, "claimHolderId", "claim_holder_id"),
|
|
6149
|
+
claimExpiresAt: slackRowNullableDate(row, "claimExpiresAt", "claim_expires_at"),
|
|
6150
|
+
attemptCount: slackRowNumber(row, "attemptCount", "attempt_count"),
|
|
6151
|
+
retryAt: slackRowNullableDate(row, "retryAt", "retry_at"),
|
|
6152
|
+
lastErrorCode: slackRowNullableString(row, "lastErrorCode", "last_error_code"),
|
|
6153
|
+
processedAt: slackRowNullableDate(row, "processedAt", "processed_at"),
|
|
6154
|
+
createdAt: slackRowDate(row, "createdAt", "created_at"),
|
|
6155
|
+
updatedAt: slackRowDate(row, "updatedAt", "updated_at"),
|
|
6156
|
+
};
|
|
6157
|
+
}
|
|
6158
|
+
|
|
6159
|
+
function mapSlackInteraction(
|
|
6160
|
+
row: typeof schema.slackInteractions.$inferSelect | Record<string, unknown>,
|
|
6161
|
+
): SlackInteraction {
|
|
6162
|
+
return {
|
|
6163
|
+
id: slackRowString(row, "id", "id"),
|
|
6164
|
+
accountId: slackRowString(row, "accountId", "account_id"),
|
|
6165
|
+
workspaceId: slackRowString(row, "workspaceId", "workspace_id"),
|
|
6166
|
+
connectionId: slackRowString(row, "connectionId", "connection_id"),
|
|
6167
|
+
slackTeamId: slackRowString(row, "slackTeamId", "slack_team_id"),
|
|
6168
|
+
slackChannelId: slackRowString(row, "slackChannelId", "slack_channel_id"),
|
|
6169
|
+
slackThreadTs: slackRowString(row, "slackThreadTs", "slack_thread_ts"),
|
|
6170
|
+
routeKey: slackRowString(row, "routeKey", "route_key"),
|
|
6171
|
+
triggeringProviderEventId: slackRowString(
|
|
6172
|
+
row,
|
|
6173
|
+
"triggeringProviderEventId",
|
|
6174
|
+
"triggering_provider_event_id",
|
|
6175
|
+
),
|
|
6176
|
+
owningSubjectId: slackRowString(row, "owningSubjectId", "owning_subject_id"),
|
|
6177
|
+
visibility: slackRowString(row, "visibility", "visibility") as SlackInteraction["visibility"],
|
|
6178
|
+
sessionReservationId: slackRowString(row, "sessionReservationId", "session_reservation_id"),
|
|
6179
|
+
sessionId: slackRowNullableString(row, "sessionId", "session_id"),
|
|
6180
|
+
lastDeliveredSessionEventSequence: slackRowNumber(
|
|
6181
|
+
row,
|
|
6182
|
+
"lastDeliveredSessionEventSequence",
|
|
6183
|
+
"last_delivered_session_event_sequence",
|
|
6184
|
+
),
|
|
6185
|
+
deliveryClaimHolderId: slackRowNullableString(
|
|
6186
|
+
row,
|
|
6187
|
+
"deliveryClaimHolderId",
|
|
6188
|
+
"delivery_claim_holder_id",
|
|
6189
|
+
),
|
|
6190
|
+
deliveryClaimExpiresAt: slackRowNullableDate(
|
|
6191
|
+
row,
|
|
6192
|
+
"deliveryClaimExpiresAt",
|
|
6193
|
+
"delivery_claim_expires_at",
|
|
6194
|
+
),
|
|
6195
|
+
deliveryAttemptCount: slackRowNumber(row, "deliveryAttemptCount", "delivery_attempt_count"),
|
|
6196
|
+
deliveryRetryAt: slackRowNullableDate(row, "deliveryRetryAt", "delivery_retry_at"),
|
|
6197
|
+
deliveryLastErrorCode: slackRowNullableString(
|
|
6198
|
+
row,
|
|
6199
|
+
"deliveryLastErrorCode",
|
|
6200
|
+
"delivery_last_error_code",
|
|
6201
|
+
),
|
|
6202
|
+
ackSlackMessageTs: slackRowNullableString(row, "ackSlackMessageTs", "ack_slack_message_ts"),
|
|
6203
|
+
progressCount: slackRowNumber(row, "progressCount", "progress_count"),
|
|
6204
|
+
terminalDeliveryState: slackRowString(
|
|
6205
|
+
row,
|
|
6206
|
+
"terminalDeliveryState",
|
|
6207
|
+
"terminal_delivery_state",
|
|
6208
|
+
) as SlackInteraction["terminalDeliveryState"],
|
|
6209
|
+
createdAt: slackRowDate(row, "createdAt", "created_at"),
|
|
6210
|
+
updatedAt: slackRowDate(row, "updatedAt", "updated_at"),
|
|
6211
|
+
};
|
|
6212
|
+
}
|
|
6213
|
+
|
|
6214
|
+
function mapSlackInteractionProgressDelivery(
|
|
6215
|
+
row: typeof schema.slackInteractionProgressDeliveries.$inferSelect | Record<string, unknown>,
|
|
6216
|
+
): SlackInteractionProgressDelivery {
|
|
6217
|
+
return {
|
|
6218
|
+
id: slackRowString(row, "id", "id"),
|
|
6219
|
+
accountId: slackRowString(row, "accountId", "account_id"),
|
|
6220
|
+
workspaceId: slackRowString(row, "workspaceId", "workspace_id"),
|
|
6221
|
+
interactionId: slackRowString(row, "interactionId", "interaction_id"),
|
|
6222
|
+
sessionEventSequence: slackRowNumber(row, "sessionEventSequence", "session_event_sequence"),
|
|
6223
|
+
slot: slackRowNumber(row, "slot", "slot"),
|
|
6224
|
+
operationId: slackRowString(row, "operationId", "operation_id"),
|
|
6225
|
+
createdAt: slackRowDate(row, "createdAt", "created_at"),
|
|
6226
|
+
};
|
|
6227
|
+
}
|
|
6228
|
+
|
|
6229
|
+
function slackRowValue(row: Record<string, unknown>, camelKey: string, snakeKey: string): unknown {
|
|
6230
|
+
return row[camelKey] ?? row[snakeKey];
|
|
6231
|
+
}
|
|
6232
|
+
|
|
6233
|
+
function slackRowString(row: Record<string, unknown>, camelKey: string, snakeKey: string): string {
|
|
6234
|
+
const value = slackRowValue(row, camelKey, snakeKey);
|
|
6235
|
+
if (typeof value !== "string") throw new Error(`Slack row omitted ${snakeKey}`);
|
|
6236
|
+
return value;
|
|
6237
|
+
}
|
|
6238
|
+
|
|
6239
|
+
function slackRowNullableString(
|
|
6240
|
+
row: Record<string, unknown>,
|
|
6241
|
+
camelKey: string,
|
|
6242
|
+
snakeKey: string,
|
|
6243
|
+
): string | null {
|
|
6244
|
+
const value = slackRowValue(row, camelKey, snakeKey);
|
|
6245
|
+
if (value === null || value === undefined) return null;
|
|
6246
|
+
if (typeof value !== "string") throw new Error(`Slack row malformed ${snakeKey}`);
|
|
6247
|
+
return value;
|
|
6248
|
+
}
|
|
6249
|
+
|
|
6250
|
+
function slackRowNumber(row: Record<string, unknown>, camelKey: string, snakeKey: string): number {
|
|
6251
|
+
const value = slackRowValue(row, camelKey, snakeKey);
|
|
6252
|
+
const parsed = typeof value === "number" ? value : Number(value);
|
|
6253
|
+
if (!Number.isSafeInteger(parsed)) throw new Error(`Slack row malformed ${snakeKey}`);
|
|
6254
|
+
return parsed;
|
|
6255
|
+
}
|
|
6256
|
+
|
|
6257
|
+
function slackRowDate(row: Record<string, unknown>, camelKey: string, snakeKey: string): Date {
|
|
6258
|
+
const value = slackRowValue(row, camelKey, snakeKey);
|
|
6259
|
+
const parsed = value instanceof Date ? value : new Date(String(value));
|
|
6260
|
+
if (Number.isNaN(parsed.getTime())) throw new Error(`Slack row malformed ${snakeKey}`);
|
|
6261
|
+
return parsed;
|
|
6262
|
+
}
|
|
6263
|
+
|
|
6264
|
+
function slackRowNullableDate(
|
|
6265
|
+
row: Record<string, unknown>,
|
|
6266
|
+
camelKey: string,
|
|
6267
|
+
snakeKey: string,
|
|
6268
|
+
): Date | null {
|
|
6269
|
+
const value = slackRowValue(row, camelKey, snakeKey);
|
|
6270
|
+
if (value === null || value === undefined) return null;
|
|
6271
|
+
return slackRowDate(row, camelKey, snakeKey);
|
|
6272
|
+
}
|
|
6273
|
+
|
|
5300
6274
|
export class SlackBotLifecycleSuccessAuditError extends Error {
|
|
5301
6275
|
constructor() {
|
|
5302
6276
|
super("OpenGeni Slack bot lifecycle success audit failed");
|
|
@@ -5889,7 +6863,10 @@ export async function claimSlackBotDeleteOperation(
|
|
|
5889
6863
|
})
|
|
5890
6864
|
.returning();
|
|
5891
6865
|
if (created) {
|
|
5892
|
-
return {
|
|
6866
|
+
return {
|
|
6867
|
+
kind: "claimed",
|
|
6868
|
+
operation: mapSlackBotDeleteOperation(created),
|
|
6869
|
+
} as const;
|
|
5893
6870
|
}
|
|
5894
6871
|
|
|
5895
6872
|
const [existing] = await tx
|
|
@@ -6030,7 +7007,11 @@ export async function releaseSlackBotDeleteOperationClaim(
|
|
|
6030
7007
|
}
|
|
6031
7008
|
|
|
6032
7009
|
export type CompleteSlackBotDeleteOperationResult =
|
|
6033
|
-
| {
|
|
7010
|
+
| {
|
|
7011
|
+
kind: "completed";
|
|
7012
|
+
operation: SlackBotDeleteOperation;
|
|
7013
|
+
newlyCompleted: boolean;
|
|
7014
|
+
}
|
|
6034
7015
|
| { kind: "not_found" | "not_owned" };
|
|
6035
7016
|
|
|
6036
7017
|
export async function completeSlackBotDeleteOperation(
|
|
@@ -6174,13 +7155,25 @@ export async function loadConnectionCredentialForBroker(
|
|
|
6174
7155
|
async (scopedDb) => {
|
|
6175
7156
|
// Prefer active rows: a revoke bumps updatedAt, so recency alone would let a
|
|
6176
7157
|
// freshly revoked connection shadow an active replacement for the provider.
|
|
7158
|
+
// UUID-free Personal Slack lookup additionally mirrors the UI/reconnect
|
|
7159
|
+
// canonical rule through createdAt and immutable UUID tie-breakers. Migration
|
|
7160
|
+
// 0132 can stamp legacy duplicates with the same updatedAt in one transaction.
|
|
7161
|
+
const personalSlackSubjectLookup =
|
|
7162
|
+
!input.connectionId &&
|
|
7163
|
+
input.allowSubjectOwned === true &&
|
|
7164
|
+
input.providerDomain === "slack.com" &&
|
|
7165
|
+
input.kind === "oauth2";
|
|
6177
7166
|
const [row] = await scopedDb
|
|
6178
7167
|
.select()
|
|
6179
7168
|
.from(schema.connections)
|
|
6180
7169
|
.where(and(...conditions))
|
|
6181
7170
|
.orderBy(
|
|
6182
|
-
|
|
6183
|
-
|
|
7171
|
+
...(personalSlackSubjectLookup
|
|
7172
|
+
? personalSlackCanonicalConnectionOrder()
|
|
7173
|
+
: [
|
|
7174
|
+
desc(sql`(${schema.connections.status} = 'active')`),
|
|
7175
|
+
desc(schema.connections.updatedAt),
|
|
7176
|
+
]),
|
|
6184
7177
|
)
|
|
6185
7178
|
.limit(1);
|
|
6186
7179
|
if (!row) {
|
|
@@ -7710,6 +8703,7 @@ export async function createSocialConnection(
|
|
|
7710
8703
|
status: input.status,
|
|
7711
8704
|
scopes: input.scopes ?? [],
|
|
7712
8705
|
credentialRef: input.credentialRef ?? null,
|
|
8706
|
+
credentialEncrypted: input.credentialEncrypted ?? null,
|
|
7713
8707
|
tokenMetadata: input.tokenMetadata ?? {},
|
|
7714
8708
|
metadata: input.metadata ?? {},
|
|
7715
8709
|
})
|
|
@@ -7722,6 +8716,118 @@ export async function createSocialConnection(
|
|
|
7722
8716
|
);
|
|
7723
8717
|
}
|
|
7724
8718
|
|
|
8719
|
+
/**
|
|
8720
|
+
* OAuth-callback persistence: reconnecting the same provider account replaces
|
|
8721
|
+
* the stored credential and revives the connection instead of failing on the
|
|
8722
|
+
* (workspace, provider, handle) unique index.
|
|
8723
|
+
*/
|
|
8724
|
+
export async function upsertSocialOAuthConnection(
|
|
8725
|
+
db: Database,
|
|
8726
|
+
input: UpsertSocialOAuthConnectionInput,
|
|
8727
|
+
): Promise<SocialConnection> {
|
|
8728
|
+
return await withRlsContext(
|
|
8729
|
+
db,
|
|
8730
|
+
{ accountId: input.accountId, workspaceId: input.workspaceId },
|
|
8731
|
+
async (scopedDb) => {
|
|
8732
|
+
const [row] = await scopedDb
|
|
8733
|
+
.insert(schema.socialConnections)
|
|
8734
|
+
.values({
|
|
8735
|
+
accountId: input.accountId,
|
|
8736
|
+
workspaceId: input.workspaceId,
|
|
8737
|
+
provider: input.provider,
|
|
8738
|
+
accountHandle: input.accountHandle,
|
|
8739
|
+
accountName: input.accountName ?? null,
|
|
8740
|
+
externalAccountId: input.externalAccountId ?? null,
|
|
8741
|
+
status: "connected",
|
|
8742
|
+
scopes: input.scopes,
|
|
8743
|
+
credentialEncrypted: input.credentialEncrypted,
|
|
8744
|
+
tokenMetadata: input.tokenMetadata ?? {},
|
|
8745
|
+
})
|
|
8746
|
+
.onConflictDoUpdate({
|
|
8747
|
+
target: [
|
|
8748
|
+
schema.socialConnections.workspaceId,
|
|
8749
|
+
schema.socialConnections.provider,
|
|
8750
|
+
schema.socialConnections.accountHandle,
|
|
8751
|
+
],
|
|
8752
|
+
set: {
|
|
8753
|
+
accountName: input.accountName ?? null,
|
|
8754
|
+
externalAccountId: input.externalAccountId ?? null,
|
|
8755
|
+
status: "connected",
|
|
8756
|
+
scopes: input.scopes,
|
|
8757
|
+
credentialEncrypted: input.credentialEncrypted,
|
|
8758
|
+
tokenMetadata: input.tokenMetadata ?? {},
|
|
8759
|
+
updatedAt: new Date(),
|
|
8760
|
+
},
|
|
8761
|
+
})
|
|
8762
|
+
.returning();
|
|
8763
|
+
if (!row) {
|
|
8764
|
+
throw new Error("Failed to upsert social connection");
|
|
8765
|
+
}
|
|
8766
|
+
return mapSocialConnection(row);
|
|
8767
|
+
},
|
|
8768
|
+
);
|
|
8769
|
+
}
|
|
8770
|
+
|
|
8771
|
+
/**
|
|
8772
|
+
* Host-side credential read for the social API client. Deliberately not part
|
|
8773
|
+
* of mapSocialConnection so the encrypted bundle never rides along on list or
|
|
8774
|
+
* MCP responses.
|
|
8775
|
+
*/
|
|
8776
|
+
export async function loadSocialConnectionCredential(
|
|
8777
|
+
db: Database,
|
|
8778
|
+
workspaceId: string,
|
|
8779
|
+
connectionId: string,
|
|
8780
|
+
): Promise<{ connection: SocialConnection; credentialEncrypted: string | null } | null> {
|
|
8781
|
+
return await withWorkspaceRls(db, workspaceId, async (scopedDb) => {
|
|
8782
|
+
const [row] = await scopedDb
|
|
8783
|
+
.select()
|
|
8784
|
+
.from(schema.socialConnections)
|
|
8785
|
+
.where(
|
|
8786
|
+
and(
|
|
8787
|
+
eq(schema.socialConnections.workspaceId, workspaceId),
|
|
8788
|
+
eq(schema.socialConnections.id, connectionId),
|
|
8789
|
+
),
|
|
8790
|
+
)
|
|
8791
|
+
.limit(1);
|
|
8792
|
+
if (!row) {
|
|
8793
|
+
return null;
|
|
8794
|
+
}
|
|
8795
|
+
return { connection: mapSocialConnection(row), credentialEncrypted: row.credentialEncrypted };
|
|
8796
|
+
});
|
|
8797
|
+
}
|
|
8798
|
+
|
|
8799
|
+
export async function updateSocialConnectionCredential(
|
|
8800
|
+
db: Database,
|
|
8801
|
+
input: UpdateSocialConnectionCredentialInput,
|
|
8802
|
+
): Promise<SocialConnection | null> {
|
|
8803
|
+
return await withWorkspaceRls(db, input.workspaceId, async (scopedDb) => {
|
|
8804
|
+
const [row] = await scopedDb
|
|
8805
|
+
.update(schema.socialConnections)
|
|
8806
|
+
.set({
|
|
8807
|
+
...(input.credentialEncrypted !== undefined
|
|
8808
|
+
? { credentialEncrypted: input.credentialEncrypted }
|
|
8809
|
+
: {}),
|
|
8810
|
+
...(input.status !== undefined ? { status: input.status } : {}),
|
|
8811
|
+
...(input.tokenMetadata !== undefined ? { tokenMetadata: input.tokenMetadata } : {}),
|
|
8812
|
+
updatedAt: new Date(),
|
|
8813
|
+
})
|
|
8814
|
+
.where(
|
|
8815
|
+
and(
|
|
8816
|
+
eq(schema.socialConnections.workspaceId, input.workspaceId),
|
|
8817
|
+
eq(schema.socialConnections.id, input.connectionId),
|
|
8818
|
+
),
|
|
8819
|
+
)
|
|
8820
|
+
.returning();
|
|
8821
|
+
return row ? mapSocialConnection(row) : null;
|
|
8822
|
+
});
|
|
8823
|
+
}
|
|
8824
|
+
|
|
8825
|
+
// List/get never select credential_encrypted (same posture as the broker
|
|
8826
|
+
// `connections` helpers): the ciphertext must not ride into API-process memory
|
|
8827
|
+
// on every list, where one added debug log or row spread would expose it.
|
|
8828
|
+
const { credentialEncrypted: _socialCredentialColumn, ...socialConnectionPublicColumns } =
|
|
8829
|
+
getTableColumns(schema.socialConnections);
|
|
8830
|
+
|
|
7725
8831
|
export async function listSocialConnections(
|
|
7726
8832
|
db: Database,
|
|
7727
8833
|
workspaceId: string,
|
|
@@ -7729,7 +8835,7 @@ export async function listSocialConnections(
|
|
|
7729
8835
|
): Promise<SocialConnection[]> {
|
|
7730
8836
|
return await withWorkspaceRls(db, workspaceId, async (scopedDb) => {
|
|
7731
8837
|
const rows = await scopedDb
|
|
7732
|
-
.select()
|
|
8838
|
+
.select(socialConnectionPublicColumns)
|
|
7733
8839
|
.from(schema.socialConnections)
|
|
7734
8840
|
.where(eq(schema.socialConnections.workspaceId, workspaceId))
|
|
7735
8841
|
.orderBy(desc(schema.socialConnections.createdAt))
|
|
@@ -7745,7 +8851,7 @@ export async function getSocialConnection(
|
|
|
7745
8851
|
): Promise<SocialConnection | null> {
|
|
7746
8852
|
return await withWorkspaceRls(db, workspaceId, async (scopedDb) => {
|
|
7747
8853
|
const [row] = await scopedDb
|
|
7748
|
-
.select()
|
|
8854
|
+
.select(socialConnectionPublicColumns)
|
|
7749
8855
|
.from(schema.socialConnections)
|
|
7750
8856
|
.where(
|
|
7751
8857
|
and(
|
|
@@ -7807,6 +8913,70 @@ export async function createSocialPost(
|
|
|
7807
8913
|
);
|
|
7808
8914
|
}
|
|
7809
8915
|
|
|
8916
|
+
/**
|
|
8917
|
+
* Idempotent bulk ingest for provider sync: rows already present under the
|
|
8918
|
+
* (workspace, connection, external post id) unique index are skipped, so a
|
|
8919
|
+
* scheduled re-sync never duplicates or fails.
|
|
8920
|
+
*/
|
|
8921
|
+
export async function recordSyncedSocialPosts(
|
|
8922
|
+
db: Database,
|
|
8923
|
+
input: {
|
|
8924
|
+
accountId: string;
|
|
8925
|
+
workspaceId: string;
|
|
8926
|
+
connectionId: string;
|
|
8927
|
+
posts: Array<{
|
|
8928
|
+
externalPostId: string;
|
|
8929
|
+
url?: string | null;
|
|
8930
|
+
authorHandle?: string | null;
|
|
8931
|
+
text: string;
|
|
8932
|
+
publishedAt: Date;
|
|
8933
|
+
metrics?: Record<string, number>;
|
|
8934
|
+
raw?: Record<string, unknown>;
|
|
8935
|
+
}>;
|
|
8936
|
+
},
|
|
8937
|
+
): Promise<{ inserted: number; skipped: number }> {
|
|
8938
|
+
if (input.posts.length === 0) {
|
|
8939
|
+
return { inserted: 0, skipped: 0 };
|
|
8940
|
+
}
|
|
8941
|
+
return await withRlsContext(
|
|
8942
|
+
db,
|
|
8943
|
+
{ accountId: input.accountId, workspaceId: input.workspaceId },
|
|
8944
|
+
async (scopedDb) => {
|
|
8945
|
+
const connection = await requireSocialConnection(
|
|
8946
|
+
scopedDb,
|
|
8947
|
+
input.workspaceId,
|
|
8948
|
+
input.connectionId,
|
|
8949
|
+
);
|
|
8950
|
+
const rows = await scopedDb
|
|
8951
|
+
.insert(schema.socialPosts)
|
|
8952
|
+
.values(
|
|
8953
|
+
input.posts.map((post) => ({
|
|
8954
|
+
accountId: input.accountId,
|
|
8955
|
+
workspaceId: input.workspaceId,
|
|
8956
|
+
connectionId: input.connectionId,
|
|
8957
|
+
provider: connection.provider,
|
|
8958
|
+
externalPostId: post.externalPostId,
|
|
8959
|
+
url: post.url ?? null,
|
|
8960
|
+
authorHandle: post.authorHandle ?? connection.accountHandle,
|
|
8961
|
+
text: post.text,
|
|
8962
|
+
publishedAt: post.publishedAt,
|
|
8963
|
+
metrics: post.metrics ?? {},
|
|
8964
|
+
raw: post.raw ?? {},
|
|
8965
|
+
})),
|
|
8966
|
+
)
|
|
8967
|
+
.onConflictDoNothing({
|
|
8968
|
+
target: [
|
|
8969
|
+
schema.socialPosts.workspaceId,
|
|
8970
|
+
schema.socialPosts.connectionId,
|
|
8971
|
+
schema.socialPosts.externalPostId,
|
|
8972
|
+
],
|
|
8973
|
+
})
|
|
8974
|
+
.returning({ id: schema.socialPosts.id });
|
|
8975
|
+
return { inserted: rows.length, skipped: input.posts.length - rows.length };
|
|
8976
|
+
},
|
|
8977
|
+
);
|
|
8978
|
+
}
|
|
8979
|
+
|
|
7810
8980
|
export async function listSocialPosts(
|
|
7811
8981
|
db: Database,
|
|
7812
8982
|
options: {
|
|
@@ -15680,9 +16850,21 @@ export async function sessionTreeStatsForSessions(
|
|
|
15680
16850
|
}
|
|
15681
16851
|
|
|
15682
16852
|
function sessionFilters(
|
|
15683
|
-
options: Pick<
|
|
16853
|
+
options: Pick<
|
|
16854
|
+
ListSessionsForSubjectOptions,
|
|
16855
|
+
"authorizationScope" | "parentSessionId" | "search" | "subjectId"
|
|
16856
|
+
>,
|
|
15684
16857
|
): SQL[] {
|
|
15685
|
-
const filters: SQL[] = [
|
|
16858
|
+
const filters: SQL[] = [
|
|
16859
|
+
sql`not exists (
|
|
16860
|
+
select 1
|
|
16861
|
+
from ${schema.slackInteractions} private_slack_interaction
|
|
16862
|
+
where private_slack_interaction.workspace_id = ${schema.sessions.workspaceId}
|
|
16863
|
+
and private_slack_interaction.session_reservation_id = ${schema.sessions.rootSessionId}
|
|
16864
|
+
and private_slack_interaction.visibility = 'private'
|
|
16865
|
+
and private_slack_interaction.owning_subject_id <> ${options.subjectId}
|
|
16866
|
+
)`,
|
|
16867
|
+
];
|
|
15686
16868
|
if (options.authorizationScope) {
|
|
15687
16869
|
filters.push(sessionAuthorizationScopeFilter(options.authorizationScope));
|
|
15688
16870
|
}
|
|
@@ -16295,7 +17477,20 @@ export async function getSessionForSubject(
|
|
|
16295
17477
|
eq(schema.sessionPins.sessionId, schema.sessions.id),
|
|
16296
17478
|
),
|
|
16297
17479
|
)
|
|
16298
|
-
.where(
|
|
17480
|
+
.where(
|
|
17481
|
+
and(
|
|
17482
|
+
eq(schema.sessions.workspaceId, workspaceId),
|
|
17483
|
+
eq(schema.sessions.id, sessionId),
|
|
17484
|
+
sql`not exists (
|
|
17485
|
+
select 1
|
|
17486
|
+
from ${schema.slackInteractions} private_slack_interaction
|
|
17487
|
+
where private_slack_interaction.workspace_id = ${schema.sessions.workspaceId}
|
|
17488
|
+
and private_slack_interaction.session_reservation_id = ${schema.sessions.rootSessionId}
|
|
17489
|
+
and private_slack_interaction.visibility = 'private'
|
|
17490
|
+
and private_slack_interaction.owning_subject_id <> ${subjectId}
|
|
17491
|
+
)`,
|
|
17492
|
+
),
|
|
17493
|
+
)
|
|
16299
17494
|
.limit(1);
|
|
16300
17495
|
if (!row) return null;
|
|
16301
17496
|
const mcpServers = await sessionMcpServerMetadataForSessions(scopedDb, workspaceId, [
|
|
@@ -19436,7 +20631,10 @@ export async function recordSkippedContextCompaction(
|
|
|
19436
20631
|
});
|
|
19437
20632
|
if (!fence.allowed) return { recorded: false as const, reason: fence.reason };
|
|
19438
20633
|
if (requirePendingRequest && !fence.session.compactRequested) {
|
|
19439
|
-
return {
|
|
20634
|
+
return {
|
|
20635
|
+
recorded: false as const,
|
|
20636
|
+
reason: "request_not_pending" as const,
|
|
20637
|
+
};
|
|
19440
20638
|
}
|
|
19441
20639
|
const inserted = await tx
|
|
19442
20640
|
.insert(schema.sessionEvents)
|
|
@@ -27566,7 +28764,11 @@ export type WorkspaceArchiveCaptureClaim = {
|
|
|
27566
28764
|
};
|
|
27567
28765
|
|
|
27568
28766
|
export type ClaimWorkspaceArchiveCaptureResult =
|
|
27569
|
-
| {
|
|
28767
|
+
| {
|
|
28768
|
+
status: "claimed";
|
|
28769
|
+
claim: WorkspaceArchiveCaptureClaim;
|
|
28770
|
+
lease: LeaseSnapshot;
|
|
28771
|
+
}
|
|
27570
28772
|
| {
|
|
27571
28773
|
status:
|
|
27572
28774
|
| "lease_fenced"
|
|
@@ -41223,7 +42425,9 @@ function mapKnowledgeMemory(row: typeof schema.knowledgeMemories.$inferSelect):
|
|
|
41223
42425
|
};
|
|
41224
42426
|
}
|
|
41225
42427
|
|
|
41226
|
-
function mapSocialConnection(
|
|
42428
|
+
function mapSocialConnection(
|
|
42429
|
+
row: Omit<typeof schema.socialConnections.$inferSelect, "credentialEncrypted">,
|
|
42430
|
+
): SocialConnection {
|
|
41227
42431
|
return {
|
|
41228
42432
|
id: row.id,
|
|
41229
42433
|
accountId: row.accountId,
|
|
@@ -41457,3 +42661,4 @@ function shortHash(value: string): string {
|
|
|
41457
42661
|
// evaluates under the index↔resolver module cycle.
|
|
41458
42662
|
export * from "./codex-token-resolver";
|
|
41459
42663
|
export * from "./connection-token-resolver";
|
|
42664
|
+
export * from "./workspace-artifacts";
|