@opengeni/db 0.17.1 → 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-TLAC622R.js → chunk-YKWJ7QJ2.js} +11 -1
- package/dist/chunk-YKWJ7QJ2.js.map +1 -0
- package/dist/index.d.ts +70 -2
- package/dist/index.js +176 -6
- package/dist/index.js.map +1 -1
- package/dist/schema.d.ts +87 -2
- package/dist/schema.js +1 -1
- package/drizzle/0141_social_connection_credentials.sql +8 -0
- package/drizzle/0151_slack_delivery_backoff.sql +134 -0
- package/package.json +3 -3
- package/src/index.ts +294 -6
- package/src/schema.ts +10 -0
- package/dist/chunk-TLAC622R.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,
|
|
@@ -3381,10 +3382,31 @@ export type CreateSocialConnectionInput = {
|
|
|
3381
3382
|
status: SocialConnectionStatus;
|
|
3382
3383
|
scopes?: string[];
|
|
3383
3384
|
credentialRef?: string | null;
|
|
3385
|
+
credentialEncrypted?: string | null;
|
|
3384
3386
|
tokenMetadata?: Record<string, unknown>;
|
|
3385
3387
|
metadata?: Record<string, unknown>;
|
|
3386
3388
|
};
|
|
3387
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
|
+
|
|
3388
3410
|
export type CreateSocialPostInput = {
|
|
3389
3411
|
accountId: string;
|
|
3390
3412
|
workspaceId: string;
|
|
@@ -5085,6 +5107,21 @@ function connectionExactSubject(subjectId?: string | null): SQL {
|
|
|
5085
5107
|
: isNull(schema.connections.subjectId);
|
|
5086
5108
|
}
|
|
5087
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
|
+
|
|
5088
5125
|
async function withConnectionSubjectRls<T>(
|
|
5089
5126
|
db: Database,
|
|
5090
5127
|
workspaceId: string,
|
|
@@ -5345,6 +5382,7 @@ export type SlackInteractionInboxEntry = {
|
|
|
5345
5382
|
claimHolderId: string | null;
|
|
5346
5383
|
claimExpiresAt: Date | null;
|
|
5347
5384
|
attemptCount: number;
|
|
5385
|
+
retryAt: Date | null;
|
|
5348
5386
|
lastErrorCode: string | null;
|
|
5349
5387
|
processedAt: Date | null;
|
|
5350
5388
|
createdAt: Date;
|
|
@@ -5368,6 +5406,9 @@ export type SlackInteraction = {
|
|
|
5368
5406
|
lastDeliveredSessionEventSequence: number;
|
|
5369
5407
|
deliveryClaimHolderId: string | null;
|
|
5370
5408
|
deliveryClaimExpiresAt: Date | null;
|
|
5409
|
+
deliveryAttemptCount: number;
|
|
5410
|
+
deliveryRetryAt: Date | null;
|
|
5411
|
+
deliveryLastErrorCode: string | null;
|
|
5371
5412
|
ackSlackMessageTs: string | null;
|
|
5372
5413
|
progressCount: number;
|
|
5373
5414
|
terminalDeliveryState: "open" | "completed" | "failed" | "cancelled" | "blocked";
|
|
@@ -5486,6 +5527,7 @@ export async function enqueueSlackInteractionInbox(
|
|
|
5486
5527
|
| "claimHolderId"
|
|
5487
5528
|
| "claimExpiresAt"
|
|
5488
5529
|
| "attemptCount"
|
|
5530
|
+
| "retryAt"
|
|
5489
5531
|
| "lastErrorCode"
|
|
5490
5532
|
| "processedAt"
|
|
5491
5533
|
| "createdAt"
|
|
@@ -5548,6 +5590,7 @@ export async function settleSlackInteractionInbox(
|
|
|
5548
5590
|
status: input.outcome,
|
|
5549
5591
|
claimHolderId: null,
|
|
5550
5592
|
claimExpiresAt: null,
|
|
5593
|
+
retryAt: null,
|
|
5551
5594
|
processedAt: sql`now()`,
|
|
5552
5595
|
lastErrorCode: input.errorCode ?? null,
|
|
5553
5596
|
updatedAt: sql`now()`,
|
|
@@ -5570,6 +5613,7 @@ export async function releaseSlackInteractionInbox(
|
|
|
5570
5613
|
entry: Pick<SlackInteractionInboxEntry, "id" | "accountId" | "workspaceId">;
|
|
5571
5614
|
claimHolderId: string;
|
|
5572
5615
|
errorCode: string;
|
|
5616
|
+
retryAt: Date;
|
|
5573
5617
|
},
|
|
5574
5618
|
): Promise<boolean> {
|
|
5575
5619
|
return await withRlsContext(db, input.entry, async (scopedDb) => {
|
|
@@ -5579,6 +5623,7 @@ export async function releaseSlackInteractionInbox(
|
|
|
5579
5623
|
status: "pending",
|
|
5580
5624
|
claimHolderId: null,
|
|
5581
5625
|
claimExpiresAt: null,
|
|
5626
|
+
retryAt: input.retryAt,
|
|
5582
5627
|
lastErrorCode: input.errorCode,
|
|
5583
5628
|
updatedAt: sql`now()`,
|
|
5584
5629
|
})
|
|
@@ -5604,6 +5649,9 @@ export async function getOrCreateSlackInteraction(
|
|
|
5604
5649
|
| "lastDeliveredSessionEventSequence"
|
|
5605
5650
|
| "deliveryClaimHolderId"
|
|
5606
5651
|
| "deliveryClaimExpiresAt"
|
|
5652
|
+
| "deliveryAttemptCount"
|
|
5653
|
+
| "deliveryRetryAt"
|
|
5654
|
+
| "deliveryLastErrorCode"
|
|
5607
5655
|
| "ackSlackMessageTs"
|
|
5608
5656
|
| "progressCount"
|
|
5609
5657
|
| "terminalDeliveryState"
|
|
@@ -5943,7 +5991,13 @@ export async function reopenSlackInteractionDelivery(
|
|
|
5943
5991
|
return await withRlsContext(db, input, async (scopedDb) => {
|
|
5944
5992
|
const rows = await scopedDb
|
|
5945
5993
|
.update(schema.slackInteractions)
|
|
5946
|
-
.set({
|
|
5994
|
+
.set({
|
|
5995
|
+
terminalDeliveryState: "open",
|
|
5996
|
+
deliveryAttemptCount: 0,
|
|
5997
|
+
deliveryRetryAt: null,
|
|
5998
|
+
deliveryLastErrorCode: null,
|
|
5999
|
+
updatedAt: sql`now()`,
|
|
6000
|
+
})
|
|
5947
6001
|
.where(eq(schema.slackInteractions.id, input.id))
|
|
5948
6002
|
.returning({ id: schema.slackInteractions.id });
|
|
5949
6003
|
return rows.length === 1;
|
|
@@ -5966,6 +6020,9 @@ export async function advanceSlackInteractionDelivery(
|
|
|
5966
6020
|
...(input.ackSlackMessageTs !== undefined
|
|
5967
6021
|
? { ackSlackMessageTs: input.ackSlackMessageTs }
|
|
5968
6022
|
: {}),
|
|
6023
|
+
deliveryAttemptCount: 0,
|
|
6024
|
+
deliveryRetryAt: null,
|
|
6025
|
+
deliveryLastErrorCode: null,
|
|
5969
6026
|
updatedAt: sql`now()`,
|
|
5970
6027
|
})
|
|
5971
6028
|
.where(
|
|
@@ -6005,12 +6062,42 @@ export async function releaseSlackInteractionDelivery(
|
|
|
6005
6062
|
});
|
|
6006
6063
|
}
|
|
6007
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
|
+
|
|
6008
6094
|
export async function closeSlackInteractionDelivery(
|
|
6009
6095
|
db: Database,
|
|
6010
6096
|
input: Pick<SlackInteraction, "id" | "accountId" | "workspaceId"> & {
|
|
6011
6097
|
claimHolderId: string;
|
|
6012
6098
|
sequence: number;
|
|
6013
6099
|
state: Exclude<SlackInteraction["terminalDeliveryState"], "open">;
|
|
6100
|
+
errorCode?: string | null;
|
|
6014
6101
|
},
|
|
6015
6102
|
): Promise<boolean> {
|
|
6016
6103
|
return await withRlsContext(db, input, async (scopedDb) => {
|
|
@@ -6021,6 +6108,8 @@ export async function closeSlackInteractionDelivery(
|
|
|
6021
6108
|
terminalDeliveryState: input.state,
|
|
6022
6109
|
deliveryClaimHolderId: null,
|
|
6023
6110
|
deliveryClaimExpiresAt: null,
|
|
6111
|
+
deliveryRetryAt: null,
|
|
6112
|
+
deliveryLastErrorCode: input.errorCode?.slice(0, 128) ?? null,
|
|
6024
6113
|
updatedAt: sql`now()`,
|
|
6025
6114
|
})
|
|
6026
6115
|
.where(
|
|
@@ -6059,6 +6148,7 @@ function mapSlackInteractionInbox(
|
|
|
6059
6148
|
claimHolderId: slackRowNullableString(row, "claimHolderId", "claim_holder_id"),
|
|
6060
6149
|
claimExpiresAt: slackRowNullableDate(row, "claimExpiresAt", "claim_expires_at"),
|
|
6061
6150
|
attemptCount: slackRowNumber(row, "attemptCount", "attempt_count"),
|
|
6151
|
+
retryAt: slackRowNullableDate(row, "retryAt", "retry_at"),
|
|
6062
6152
|
lastErrorCode: slackRowNullableString(row, "lastErrorCode", "last_error_code"),
|
|
6063
6153
|
processedAt: slackRowNullableDate(row, "processedAt", "processed_at"),
|
|
6064
6154
|
createdAt: slackRowDate(row, "createdAt", "created_at"),
|
|
@@ -6102,6 +6192,13 @@ function mapSlackInteraction(
|
|
|
6102
6192
|
"deliveryClaimExpiresAt",
|
|
6103
6193
|
"delivery_claim_expires_at",
|
|
6104
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
|
+
),
|
|
6105
6202
|
ackSlackMessageTs: slackRowNullableString(row, "ackSlackMessageTs", "ack_slack_message_ts"),
|
|
6106
6203
|
progressCount: slackRowNumber(row, "progressCount", "progress_count"),
|
|
6107
6204
|
terminalDeliveryState: slackRowString(
|
|
@@ -7058,13 +7155,25 @@ export async function loadConnectionCredentialForBroker(
|
|
|
7058
7155
|
async (scopedDb) => {
|
|
7059
7156
|
// Prefer active rows: a revoke bumps updatedAt, so recency alone would let a
|
|
7060
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";
|
|
7061
7166
|
const [row] = await scopedDb
|
|
7062
7167
|
.select()
|
|
7063
7168
|
.from(schema.connections)
|
|
7064
7169
|
.where(and(...conditions))
|
|
7065
7170
|
.orderBy(
|
|
7066
|
-
|
|
7067
|
-
|
|
7171
|
+
...(personalSlackSubjectLookup
|
|
7172
|
+
? personalSlackCanonicalConnectionOrder()
|
|
7173
|
+
: [
|
|
7174
|
+
desc(sql`(${schema.connections.status} = 'active')`),
|
|
7175
|
+
desc(schema.connections.updatedAt),
|
|
7176
|
+
]),
|
|
7068
7177
|
)
|
|
7069
7178
|
.limit(1);
|
|
7070
7179
|
if (!row) {
|
|
@@ -8594,6 +8703,7 @@ export async function createSocialConnection(
|
|
|
8594
8703
|
status: input.status,
|
|
8595
8704
|
scopes: input.scopes ?? [],
|
|
8596
8705
|
credentialRef: input.credentialRef ?? null,
|
|
8706
|
+
credentialEncrypted: input.credentialEncrypted ?? null,
|
|
8597
8707
|
tokenMetadata: input.tokenMetadata ?? {},
|
|
8598
8708
|
metadata: input.metadata ?? {},
|
|
8599
8709
|
})
|
|
@@ -8606,6 +8716,118 @@ export async function createSocialConnection(
|
|
|
8606
8716
|
);
|
|
8607
8717
|
}
|
|
8608
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
|
+
|
|
8609
8831
|
export async function listSocialConnections(
|
|
8610
8832
|
db: Database,
|
|
8611
8833
|
workspaceId: string,
|
|
@@ -8613,7 +8835,7 @@ export async function listSocialConnections(
|
|
|
8613
8835
|
): Promise<SocialConnection[]> {
|
|
8614
8836
|
return await withWorkspaceRls(db, workspaceId, async (scopedDb) => {
|
|
8615
8837
|
const rows = await scopedDb
|
|
8616
|
-
.select()
|
|
8838
|
+
.select(socialConnectionPublicColumns)
|
|
8617
8839
|
.from(schema.socialConnections)
|
|
8618
8840
|
.where(eq(schema.socialConnections.workspaceId, workspaceId))
|
|
8619
8841
|
.orderBy(desc(schema.socialConnections.createdAt))
|
|
@@ -8629,7 +8851,7 @@ export async function getSocialConnection(
|
|
|
8629
8851
|
): Promise<SocialConnection | null> {
|
|
8630
8852
|
return await withWorkspaceRls(db, workspaceId, async (scopedDb) => {
|
|
8631
8853
|
const [row] = await scopedDb
|
|
8632
|
-
.select()
|
|
8854
|
+
.select(socialConnectionPublicColumns)
|
|
8633
8855
|
.from(schema.socialConnections)
|
|
8634
8856
|
.where(
|
|
8635
8857
|
and(
|
|
@@ -8691,6 +8913,70 @@ export async function createSocialPost(
|
|
|
8691
8913
|
);
|
|
8692
8914
|
}
|
|
8693
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
|
+
|
|
8694
8980
|
export async function listSocialPosts(
|
|
8695
8981
|
db: Database,
|
|
8696
8982
|
options: {
|
|
@@ -42139,7 +42425,9 @@ function mapKnowledgeMemory(row: typeof schema.knowledgeMemories.$inferSelect):
|
|
|
42139
42425
|
};
|
|
42140
42426
|
}
|
|
42141
42427
|
|
|
42142
|
-
function mapSocialConnection(
|
|
42428
|
+
function mapSocialConnection(
|
|
42429
|
+
row: Omit<typeof schema.socialConnections.$inferSelect, "credentialEncrypted">,
|
|
42430
|
+
): SocialConnection {
|
|
42143
42431
|
return {
|
|
42144
42432
|
id: row.id,
|
|
42145
42433
|
accountId: row.accountId,
|
package/src/schema.ts
CHANGED
|
@@ -776,6 +776,7 @@ export const slackInteractionInbox = pgTable(
|
|
|
776
776
|
claimHolderId: uuid("claim_holder_id"),
|
|
777
777
|
claimExpiresAt: timestamp("claim_expires_at", { withTimezone: true }),
|
|
778
778
|
attemptCount: integer("attempt_count").notNull().default(0),
|
|
779
|
+
retryAt: timestamp("retry_at", { withTimezone: true }),
|
|
779
780
|
lastErrorCode: text("last_error_code"),
|
|
780
781
|
processedAt: timestamp("processed_at", { withTimezone: true }),
|
|
781
782
|
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
|
@@ -792,6 +793,7 @@ export const slackInteractionInbox = pgTable(
|
|
|
792
793
|
),
|
|
793
794
|
pending: index("slack_interaction_inbox_pending_idx").on(
|
|
794
795
|
table.status,
|
|
796
|
+
table.retryAt,
|
|
795
797
|
table.createdAt,
|
|
796
798
|
table.id,
|
|
797
799
|
),
|
|
@@ -829,6 +831,9 @@ export const slackInteractions = pgTable(
|
|
|
829
831
|
deliveryClaimExpiresAt: timestamp("delivery_claim_expires_at", {
|
|
830
832
|
withTimezone: true,
|
|
831
833
|
}),
|
|
834
|
+
deliveryAttemptCount: integer("delivery_attempt_count").notNull().default(0),
|
|
835
|
+
deliveryRetryAt: timestamp("delivery_retry_at", { withTimezone: true }),
|
|
836
|
+
deliveryLastErrorCode: text("delivery_last_error_code"),
|
|
832
837
|
ackSlackMessageTs: text("ack_slack_message_ts"),
|
|
833
838
|
progressCount: integer("progress_count").notNull().default(0),
|
|
834
839
|
terminalDeliveryState: text("terminal_delivery_state")
|
|
@@ -854,6 +859,7 @@ export const slackInteractions = pgTable(
|
|
|
854
859
|
.where(sql`${table.sessionId} is not null`),
|
|
855
860
|
delivery: index("slack_interactions_delivery_idx").on(
|
|
856
861
|
table.terminalDeliveryState,
|
|
862
|
+
table.deliveryRetryAt,
|
|
857
863
|
table.updatedAt,
|
|
858
864
|
table.id,
|
|
859
865
|
),
|
|
@@ -5119,6 +5125,10 @@ export const socialConnections = pgTable(
|
|
|
5119
5125
|
status: text("status").notNull().default("connected"),
|
|
5120
5126
|
scopes: jsonb("scopes").$type<string[]>().notNull().default([]),
|
|
5121
5127
|
credentialRef: text("credential_ref"),
|
|
5128
|
+
// AES-256-GCM envelope (environment-crypto v1 format) holding the OAuth
|
|
5129
|
+
// token bundle. Never exposed through contracts or MCP tools; only the
|
|
5130
|
+
// host-side social API client decrypts it.
|
|
5131
|
+
credentialEncrypted: text("credential_encrypted"),
|
|
5122
5132
|
tokenMetadata: jsonb("token_metadata").$type<Record<string, unknown>>().notNull().default({}),
|
|
5123
5133
|
metadata: jsonb("metadata").$type<Record<string, unknown>>().notNull().default({}),
|
|
5124
5134
|
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|