@opengeni/api-router 0.21.9 → 0.21.11
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-BESOWOUQ.js → chunk-J4JS2F7L.js} +101 -31
- package/dist/chunk-J4JS2F7L.js.map +1 -0
- package/dist/index.js +1 -1
- package/dist/integrations/social-api.d.ts +1 -0
- package/dist/integrations/social-oauth.d.ts +2 -0
- package/package.json +11 -11
- package/src/integrations/social-api.ts +1 -1
- package/src/integrations/social-oauth.ts +21 -5
- package/src/mcp/server.ts +47 -13
- package/src/routes/capabilities.ts +4 -2
- package/src/routes/packs.ts +9 -3
- package/src/routes/social.ts +27 -6
- package/dist/chunk-BESOWOUQ.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -71,6 +71,7 @@ export declare function completeSocialOAuthCallback(deps: SocialOAuthDeps, input
|
|
|
71
71
|
export declare function freshSocialAccessToken(deps: SocialOAuthDeps, ref: {
|
|
72
72
|
workspaceId: string;
|
|
73
73
|
connectionId: string;
|
|
74
|
+
subjectId?: string | null;
|
|
74
75
|
}): Promise<{
|
|
75
76
|
connection: SocialConnection;
|
|
76
77
|
bundle: SocialCredentialBundle;
|
|
@@ -78,6 +79,7 @@ export declare function freshSocialAccessToken(deps: SocialOAuthDeps, ref: {
|
|
|
78
79
|
export declare function markNeedsReauth(deps: SocialOAuthDeps, ref: {
|
|
79
80
|
workspaceId: string;
|
|
80
81
|
connectionId: string;
|
|
82
|
+
subjectId?: string | null;
|
|
81
83
|
}): Promise<void>;
|
|
82
84
|
export declare function parseSocialCredentialBundle(raw: string): SocialCredentialBundle;
|
|
83
85
|
export declare function socialTokenNeedsRefresh(bundle: SocialCredentialBundle, now: Date): boolean;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opengeni/api-router",
|
|
3
|
-
"version": "0.21.
|
|
3
|
+
"version": "0.21.11",
|
|
4
4
|
"description": "OpenGeni HTTP surface: the Hono adapter/router (createApp), routes, MCP HTTP transport, and HTTP access adapters over @opengeni/core. An engine-distribution surface — its runtime closure includes engine-internal packages.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -44,17 +44,17 @@
|
|
|
44
44
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
45
45
|
"@opengeni/agent-proto": "^0.3.0",
|
|
46
46
|
"@opengeni/codex": "^0.2.10",
|
|
47
|
-
"@opengeni/config": "^0.10.
|
|
48
|
-
"@opengeni/contracts": "^0.38.
|
|
49
|
-
"@opengeni/core": "^0.20.
|
|
50
|
-
"@opengeni/db": "^0.27.
|
|
51
|
-
"@opengeni/documents": "^0.5.
|
|
52
|
-
"@opengeni/events": "^0.3.
|
|
53
|
-
"@opengeni/github": "^0.4.
|
|
47
|
+
"@opengeni/config": "^0.10.12",
|
|
48
|
+
"@opengeni/contracts": "^0.38.2",
|
|
49
|
+
"@opengeni/core": "^0.20.13",
|
|
50
|
+
"@opengeni/db": "^0.27.9",
|
|
51
|
+
"@opengeni/documents": "^0.5.5",
|
|
52
|
+
"@opengeni/events": "^0.3.75",
|
|
53
|
+
"@opengeni/github": "^0.4.28",
|
|
54
54
|
"@opengeni/network": "^0.2.0",
|
|
55
|
-
"@opengeni/observability": "^0.4.
|
|
56
|
-
"@opengeni/runtime": "^0.18.
|
|
57
|
-
"@opengeni/storage": "^0.2.
|
|
55
|
+
"@opengeni/observability": "^0.4.15",
|
|
56
|
+
"@opengeni/runtime": "^0.18.12",
|
|
57
|
+
"@opengeni/storage": "^0.2.65",
|
|
58
58
|
"@temporalio/client": "^1.17.0",
|
|
59
59
|
"better-auth": "^1.6.14",
|
|
60
60
|
"hono": "^4.12.18",
|
|
@@ -35,7 +35,7 @@ type SocialApiDeps = {
|
|
|
35
35
|
providerFetch?: SocialProviderFetch | undefined;
|
|
36
36
|
};
|
|
37
37
|
|
|
38
|
-
type ConnectionRef = { workspaceId: string; connectionId: string };
|
|
38
|
+
type ConnectionRef = { workspaceId: string; connectionId: string; subjectId?: string | null };
|
|
39
39
|
|
|
40
40
|
const MAX_LIVE_RESULTS = 50;
|
|
41
41
|
|
|
@@ -141,6 +141,7 @@ type SocialOAuthStatePayload = {
|
|
|
141
141
|
accountId: string;
|
|
142
142
|
workspaceId: string;
|
|
143
143
|
subjectId: string;
|
|
144
|
+
ownership: "workspace" | "personal";
|
|
144
145
|
provider: SocialOAuthProviderId;
|
|
145
146
|
scopes: string[];
|
|
146
147
|
encryptedPkceVerifier?: string;
|
|
@@ -190,6 +191,7 @@ export async function startSocialOAuth(
|
|
|
190
191
|
accountId: context.accountId,
|
|
191
192
|
workspaceId: context.workspaceId,
|
|
192
193
|
subjectId: context.subjectId,
|
|
194
|
+
ownership: context.payload.ownership,
|
|
193
195
|
provider: provider.id,
|
|
194
196
|
scopes,
|
|
195
197
|
...(verifier && key ? { encryptedPkceVerifier: encryptEnvironmentValue(key, verifier) } : {}),
|
|
@@ -276,7 +278,7 @@ export async function completeSocialOAuthCallback(
|
|
|
276
278
|
if (
|
|
277
279
|
!grant ||
|
|
278
280
|
grant.accountId !== state.accountId ||
|
|
279
|
-
!hasPermission(grant.permissions, "workspace:admin")
|
|
281
|
+
(state.ownership === "workspace" && !hasPermission(grant.permissions, "workspace:admin"))
|
|
280
282
|
) {
|
|
281
283
|
throw new HTTPException(403, {
|
|
282
284
|
message: "OAuth subject no longer has permission to connect social accounts",
|
|
@@ -314,6 +316,7 @@ export async function completeSocialOAuthCallback(
|
|
|
314
316
|
const connection = await upsertSocialOAuthConnection(db, {
|
|
315
317
|
accountId: state.accountId,
|
|
316
318
|
workspaceId: state.workspaceId,
|
|
319
|
+
subjectId: state.ownership === "personal" ? state.subjectId : null,
|
|
317
320
|
provider: provider.id,
|
|
318
321
|
accountHandle: identity.handle,
|
|
319
322
|
accountName: identity.name ?? null,
|
|
@@ -347,10 +350,15 @@ export async function completeSocialOAuthCallback(
|
|
|
347
350
|
*/
|
|
348
351
|
export async function freshSocialAccessToken(
|
|
349
352
|
deps: SocialOAuthDeps,
|
|
350
|
-
ref: { workspaceId: string; connectionId: string },
|
|
353
|
+
ref: { workspaceId: string; connectionId: string; subjectId?: string | null },
|
|
351
354
|
): Promise<{ connection: SocialConnection; bundle: SocialCredentialBundle }> {
|
|
352
355
|
const { db, settings } = deps;
|
|
353
|
-
const loaded = await loadSocialConnectionCredential(
|
|
356
|
+
const loaded = await loadSocialConnectionCredential(
|
|
357
|
+
db,
|
|
358
|
+
ref.workspaceId,
|
|
359
|
+
ref.connectionId,
|
|
360
|
+
ref.subjectId,
|
|
361
|
+
);
|
|
354
362
|
if (!loaded) {
|
|
355
363
|
throw new Error(`Social connection not found: ${ref.connectionId}`);
|
|
356
364
|
}
|
|
@@ -397,7 +405,12 @@ export async function freshSocialAccessToken(
|
|
|
397
405
|
// rotates refresh tokens per use, so the loser's token is already spent.
|
|
398
406
|
// Re-read before declaring the connection dead — if another writer
|
|
399
407
|
// persisted a newer bundle, use that instead of flipping needs_reauth.
|
|
400
|
-
const reloaded = await loadSocialConnectionCredential(
|
|
408
|
+
const reloaded = await loadSocialConnectionCredential(
|
|
409
|
+
db,
|
|
410
|
+
ref.workspaceId,
|
|
411
|
+
ref.connectionId,
|
|
412
|
+
ref.subjectId,
|
|
413
|
+
);
|
|
401
414
|
if (
|
|
402
415
|
reloaded?.credentialEncrypted &&
|
|
403
416
|
reloaded.credentialEncrypted !== loaded.credentialEncrypted
|
|
@@ -429,6 +442,7 @@ export async function freshSocialAccessToken(
|
|
|
429
442
|
(await updateSocialConnectionCredential(db, {
|
|
430
443
|
workspaceId: ref.workspaceId,
|
|
431
444
|
connectionId: ref.connectionId,
|
|
445
|
+
...(ref.subjectId !== undefined ? { subjectId: ref.subjectId } : {}),
|
|
432
446
|
credentialEncrypted: encryptEnvironmentValue(key, JSON.stringify(refreshed)),
|
|
433
447
|
status: "connected",
|
|
434
448
|
tokenMetadata: publicTokenMetadata(refreshed),
|
|
@@ -438,11 +452,12 @@ export async function freshSocialAccessToken(
|
|
|
438
452
|
|
|
439
453
|
export async function markNeedsReauth(
|
|
440
454
|
deps: SocialOAuthDeps,
|
|
441
|
-
ref: { workspaceId: string; connectionId: string },
|
|
455
|
+
ref: { workspaceId: string; connectionId: string; subjectId?: string | null },
|
|
442
456
|
): Promise<void> {
|
|
443
457
|
await updateSocialConnectionCredential(deps.db, {
|
|
444
458
|
workspaceId: ref.workspaceId,
|
|
445
459
|
connectionId: ref.connectionId,
|
|
460
|
+
...(ref.subjectId !== undefined ? { subjectId: ref.subjectId } : {}),
|
|
446
461
|
status: "needs_reauth",
|
|
447
462
|
});
|
|
448
463
|
}
|
|
@@ -671,6 +686,7 @@ function readSocialOAuthState(
|
|
|
671
686
|
accountId: requiredStateString(payload.accountId, "accountId"),
|
|
672
687
|
workspaceId: requiredStateString(payload.workspaceId, "workspaceId"),
|
|
673
688
|
subjectId: requiredStateString(payload.subjectId, "subjectId"),
|
|
689
|
+
ownership: payload.ownership === "personal" ? "personal" : "workspace",
|
|
674
690
|
provider,
|
|
675
691
|
scopes: Array.isArray(payload.scopes)
|
|
676
692
|
? payload.scopes.filter((scope): scope is string => typeof scope === "string")
|
package/src/mcp/server.ts
CHANGED
|
@@ -55,7 +55,6 @@ import {
|
|
|
55
55
|
listRigs,
|
|
56
56
|
listRigChangeMonitoringSummaries,
|
|
57
57
|
listRigVersionMonitoringSummaries,
|
|
58
|
-
listSocialConnections,
|
|
59
58
|
listSocialPosts,
|
|
60
59
|
recordAuditEvent,
|
|
61
60
|
recordSyncedSocialPosts,
|
|
@@ -99,6 +98,7 @@ import type { ToolAnnotations } from "@modelcontextprotocol/sdk/types.js";
|
|
|
99
98
|
import * as z4 from "zod/v4";
|
|
100
99
|
import {
|
|
101
100
|
hasPermission,
|
|
101
|
+
authorizedSocialConnectionsForGrant,
|
|
102
102
|
requireSessionAuthorization,
|
|
103
103
|
requireSessionAuthorizationListScope,
|
|
104
104
|
type ResolvedSessionAuthorization,
|
|
@@ -360,6 +360,20 @@ export function buildOpenGeniMcpServer(
|
|
|
360
360
|
});
|
|
361
361
|
const can = (permission: Permission) => hasPermission(grant.permissions, permission);
|
|
362
362
|
const toolspaceMode = options.toolspace != null;
|
|
363
|
+
let socialConnectionsPromise: ReturnType<typeof authorizedSocialConnectionsForGrant> | undefined;
|
|
364
|
+
const authorizedSocialConnections = () =>
|
|
365
|
+
(socialConnectionsPromise ??= authorizedSocialConnectionsForGrant({
|
|
366
|
+
db: deps.db,
|
|
367
|
+
grant,
|
|
368
|
+
limit: 500,
|
|
369
|
+
}));
|
|
370
|
+
const requireAuthorizedSocialConnection = async (connectionId: string) => {
|
|
371
|
+
const authority = (await authorizedSocialConnections()).find(
|
|
372
|
+
({ connection }) => connection.id === connectionId,
|
|
373
|
+
);
|
|
374
|
+
if (!authority) throw new Error(`Unknown or unavailable social connection: ${connectionId}`);
|
|
375
|
+
return authority;
|
|
376
|
+
};
|
|
363
377
|
|
|
364
378
|
// Session-scoped tools key off the worker-asserted sessionId claim (signed
|
|
365
379
|
// into the delegated token by the worker, never agent-controlled).
|
|
@@ -490,11 +504,9 @@ export function buildOpenGeniMcpServer(
|
|
|
490
504
|
},
|
|
491
505
|
async ({ limit }) =>
|
|
492
506
|
json({
|
|
493
|
-
connections: await
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
boundedMcpLimit(limit),
|
|
497
|
-
),
|
|
507
|
+
connections: (await authorizedSocialConnections())
|
|
508
|
+
.slice(0, boundedMcpLimit(limit))
|
|
509
|
+
.map(({ connection }) => connection),
|
|
498
510
|
}),
|
|
499
511
|
);
|
|
500
512
|
|
|
@@ -510,6 +522,15 @@ export function buildOpenGeniMcpServer(
|
|
|
510
522
|
},
|
|
511
523
|
},
|
|
512
524
|
async ({ connectionIds, since, windowHours, limit }) => {
|
|
525
|
+
const authorized = await authorizedSocialConnections();
|
|
526
|
+
const selected = connectionIds?.length
|
|
527
|
+
? connectionIds.map((id) => {
|
|
528
|
+
const match = authorized.find(({ connection }) => connection.id === id);
|
|
529
|
+
if (!match) throw new Error(`Unknown or unavailable social connection: ${id}`);
|
|
530
|
+
return match;
|
|
531
|
+
})
|
|
532
|
+
: authorized;
|
|
533
|
+
const personalSubjectId = selected.find(({ subjectId }) => subjectId)?.subjectId ?? null;
|
|
513
534
|
const sinceDate = since
|
|
514
535
|
? parseMcpDate(since, "since")
|
|
515
536
|
: new Date(Date.now() - (windowHours ?? 24) * 60 * 60 * 1000);
|
|
@@ -517,7 +538,8 @@ export function buildOpenGeniMcpServer(
|
|
|
517
538
|
since: sinceDate.toISOString(),
|
|
518
539
|
posts: await listSocialPosts(deps.db, {
|
|
519
540
|
workspaceId: grant.workspaceId,
|
|
520
|
-
|
|
541
|
+
subjectId: personalSubjectId,
|
|
542
|
+
connectionIds: selected.map(({ connection }) => connection.id),
|
|
521
543
|
since: sinceDate,
|
|
522
544
|
limit: boundedMcpLimit(limit),
|
|
523
545
|
}),
|
|
@@ -539,7 +561,8 @@ export function buildOpenGeniMcpServer(
|
|
|
539
561
|
},
|
|
540
562
|
},
|
|
541
563
|
async ({ connectionIds, documentBaseIds, since, windowHours, limit }) => {
|
|
542
|
-
const
|
|
564
|
+
const authorized = await authorizedSocialConnections();
|
|
565
|
+
const allConnections = authorized.map(({ connection }) => connection);
|
|
543
566
|
const selectedIds =
|
|
544
567
|
connectionIds && connectionIds.length > 0 ? new Set(connectionIds) : null;
|
|
545
568
|
const connections = selectedIds
|
|
@@ -559,6 +582,11 @@ export function buildOpenGeniMcpServer(
|
|
|
559
582
|
connections.length > 0
|
|
560
583
|
? await listSocialPosts(deps.db, {
|
|
561
584
|
workspaceId: grant.workspaceId,
|
|
585
|
+
subjectId:
|
|
586
|
+
authorized.find(
|
|
587
|
+
({ connection, subjectId }) =>
|
|
588
|
+
subjectId && connections.some((selected) => selected.id === connection.id),
|
|
589
|
+
)?.subjectId ?? null,
|
|
562
590
|
connectionIds: connections.map((connection) => connection.id),
|
|
563
591
|
since: sinceDate,
|
|
564
592
|
limit: boundedMcpLimit(limit),
|
|
@@ -597,9 +625,10 @@ export function buildOpenGeniMcpServer(
|
|
|
597
625
|
},
|
|
598
626
|
},
|
|
599
627
|
async ({ connectionId, query, subreddit, limit }) => {
|
|
628
|
+
const authority = await requireAuthorizedSocialConnection(connectionId);
|
|
600
629
|
const result = await socialSearchLive(
|
|
601
630
|
deps,
|
|
602
|
-
{ workspaceId: grant.workspaceId, connectionId },
|
|
631
|
+
{ workspaceId: grant.workspaceId, connectionId, subjectId: authority.subjectId },
|
|
603
632
|
{ query, subreddit, limit },
|
|
604
633
|
);
|
|
605
634
|
return json({ provider: result.connection.provider, posts: result.posts });
|
|
@@ -618,9 +647,10 @@ export function buildOpenGeniMcpServer(
|
|
|
618
647
|
},
|
|
619
648
|
},
|
|
620
649
|
async ({ connectionId, sinceId, limit }) => {
|
|
650
|
+
const authority = await requireAuthorizedSocialConnection(connectionId);
|
|
621
651
|
const result = await socialMentionsLive(
|
|
622
652
|
deps,
|
|
623
|
-
{ workspaceId: grant.workspaceId, connectionId },
|
|
653
|
+
{ workspaceId: grant.workspaceId, connectionId, subjectId: authority.subjectId },
|
|
624
654
|
{ sinceId, limit },
|
|
625
655
|
);
|
|
626
656
|
return json({ provider: result.connection.provider, posts: result.posts });
|
|
@@ -639,9 +669,10 @@ export function buildOpenGeniMcpServer(
|
|
|
639
669
|
},
|
|
640
670
|
},
|
|
641
671
|
async ({ connectionId, id, limit }) => {
|
|
672
|
+
const authority = await requireAuthorizedSocialConnection(connectionId);
|
|
642
673
|
const result = await socialThreadLive(
|
|
643
674
|
deps,
|
|
644
|
-
{ workspaceId: grant.workspaceId, connectionId },
|
|
675
|
+
{ workspaceId: grant.workspaceId, connectionId, subjectId: authority.subjectId },
|
|
645
676
|
{ id, limit },
|
|
646
677
|
);
|
|
647
678
|
return json({ provider: result.connection.provider, posts: result.posts });
|
|
@@ -664,9 +695,10 @@ export function buildOpenGeniMcpServer(
|
|
|
664
695
|
},
|
|
665
696
|
},
|
|
666
697
|
async ({ connectionId, limit }) => {
|
|
698
|
+
const authority = await requireAuthorizedSocialConnection(connectionId);
|
|
667
699
|
const result = await socialOwnPostsLive(
|
|
668
700
|
deps,
|
|
669
|
-
{ workspaceId: grant.workspaceId, connectionId },
|
|
701
|
+
{ workspaceId: grant.workspaceId, connectionId, subjectId: authority.subjectId },
|
|
670
702
|
{ limit },
|
|
671
703
|
);
|
|
672
704
|
// A post without a provider timestamp is skipped rather than recorded
|
|
@@ -677,6 +709,7 @@ export function buildOpenGeniMcpServer(
|
|
|
677
709
|
accountId: grant.accountId,
|
|
678
710
|
workspaceId: grant.workspaceId,
|
|
679
711
|
connectionId,
|
|
712
|
+
subjectId: authority.subjectId,
|
|
680
713
|
posts: datedPosts.map((post) => ({
|
|
681
714
|
externalPostId: post.id,
|
|
682
715
|
url: post.url,
|
|
@@ -708,9 +741,10 @@ export function buildOpenGeniMcpServer(
|
|
|
708
741
|
},
|
|
709
742
|
},
|
|
710
743
|
async ({ connectionId, inReplyToId, text }) => {
|
|
744
|
+
const authority = await requireAuthorizedSocialConnection(connectionId);
|
|
711
745
|
const result = await socialPostReply(
|
|
712
746
|
deps,
|
|
713
|
-
{ workspaceId: grant.workspaceId, connectionId },
|
|
747
|
+
{ workspaceId: grant.workspaceId, connectionId, subjectId: authority.subjectId },
|
|
714
748
|
{ inReplyToId, text },
|
|
715
749
|
);
|
|
716
750
|
// Outbound publishes leave a durable, secret-free receipt (house
|
|
@@ -22,9 +22,11 @@ export function registerCapabilityRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
22
22
|
|
|
23
23
|
app.get("/v1/workspaces/:workspaceId/capabilities", async (c) => {
|
|
24
24
|
const workspaceId = c.req.param("workspaceId");
|
|
25
|
-
await requireAccessGrant(c, deps, workspaceId, "workspace:read");
|
|
25
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "workspace:read");
|
|
26
26
|
return c.json(
|
|
27
|
-
CapabilityCatalogResponse.parse(
|
|
27
|
+
CapabilityCatalogResponse.parse(
|
|
28
|
+
await buildCapabilityCatalog({ db, workspaceId, settings, subjectId: grant.subjectId }),
|
|
29
|
+
),
|
|
28
30
|
);
|
|
29
31
|
});
|
|
30
32
|
|
package/src/routes/packs.ts
CHANGED
|
@@ -187,7 +187,12 @@ export function registerPackRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
187
187
|
action: "schedule:create",
|
|
188
188
|
quantity: 1,
|
|
189
189
|
});
|
|
190
|
-
const connections = await resolveSocialConnections(
|
|
190
|
+
const connections = await resolveSocialConnections(
|
|
191
|
+
db,
|
|
192
|
+
workspaceId,
|
|
193
|
+
payload.connectionIds,
|
|
194
|
+
grant.subjectId,
|
|
195
|
+
);
|
|
191
196
|
if (connections.length === 0) {
|
|
192
197
|
throw new HTTPException(422, {
|
|
193
198
|
message: "at least one connected social account is required",
|
|
@@ -254,20 +259,21 @@ async function resolveSocialConnections(
|
|
|
254
259
|
db: ApiRouteDeps["db"],
|
|
255
260
|
workspaceId: string,
|
|
256
261
|
connectionIds: string[],
|
|
262
|
+
subjectId: string,
|
|
257
263
|
): Promise<SocialConnection[]> {
|
|
258
264
|
const ids = [...new Set(connectionIds)];
|
|
259
265
|
const connections =
|
|
260
266
|
ids.length > 0
|
|
261
267
|
? await Promise.all(
|
|
262
268
|
ids.map(async (id) => {
|
|
263
|
-
const connection = await getSocialConnection(db, workspaceId, id);
|
|
269
|
+
const connection = await getSocialConnection(db, workspaceId, id, subjectId);
|
|
264
270
|
if (!connection) {
|
|
265
271
|
throw new HTTPException(422, { message: `unknown social connection: ${id}` });
|
|
266
272
|
}
|
|
267
273
|
return connection;
|
|
268
274
|
}),
|
|
269
275
|
)
|
|
270
|
-
: (await listSocialConnections(db, workspaceId, 500)).filter(
|
|
276
|
+
: (await listSocialConnections(db, workspaceId, 500, subjectId)).filter(
|
|
271
277
|
(connection) => connection.status === "connected",
|
|
272
278
|
);
|
|
273
279
|
const inactive = connections.find((connection) => connection.status !== "connected");
|
package/src/routes/social.ts
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
import {
|
|
8
8
|
createSocialConnection,
|
|
9
9
|
createSocialPost,
|
|
10
|
+
getSocialConnection,
|
|
10
11
|
listSocialConnections,
|
|
11
12
|
listSocialPosts,
|
|
12
13
|
updateSocialConnectionCredential,
|
|
@@ -24,8 +25,15 @@ export function registerSocialRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
24
25
|
|
|
25
26
|
app.get("/v1/workspaces/:workspaceId/social/connections", async (c) => {
|
|
26
27
|
const workspaceId = c.req.param("workspaceId");
|
|
27
|
-
await requireAccessGrant(c, deps, workspaceId, "workspace:read");
|
|
28
|
-
return c.json(
|
|
28
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "workspace:read");
|
|
29
|
+
return c.json(
|
|
30
|
+
await listSocialConnections(
|
|
31
|
+
db,
|
|
32
|
+
workspaceId,
|
|
33
|
+
boundedLimit(c.req.query("limit")),
|
|
34
|
+
grant.subjectId,
|
|
35
|
+
),
|
|
36
|
+
);
|
|
29
37
|
});
|
|
30
38
|
|
|
31
39
|
app.post("/v1/workspaces/:workspaceId/social/connections", async (c) => {
|
|
@@ -59,10 +67,16 @@ export function registerSocialRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
59
67
|
// reconnecting via the OAuth flow revives it.
|
|
60
68
|
app.delete("/v1/workspaces/:workspaceId/social/connections/:connectionId", async (c) => {
|
|
61
69
|
const workspaceId = c.req.param("workspaceId");
|
|
62
|
-
await requireAccessGrant(c, deps, workspaceId, "workspace:
|
|
70
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "workspace:read");
|
|
71
|
+
const connectionId = c.req.param("connectionId");
|
|
72
|
+
const existing = await getSocialConnection(db, workspaceId, connectionId, grant.subjectId);
|
|
73
|
+
if (!existing) throw new HTTPException(404, { message: "social connection not found" });
|
|
74
|
+
if (existing.ownership === "workspace")
|
|
75
|
+
await requireAccessGrant(c, deps, workspaceId, "workspace:admin");
|
|
63
76
|
const connection = await updateSocialConnectionCredential(db, {
|
|
64
77
|
workspaceId,
|
|
65
|
-
connectionId
|
|
78
|
+
connectionId,
|
|
79
|
+
subjectId: existing.ownership === "personal" ? grant.subjectId : null,
|
|
66
80
|
credentialEncrypted: null,
|
|
67
81
|
status: "disabled",
|
|
68
82
|
tokenMetadata: {},
|
|
@@ -78,7 +92,6 @@ export function registerSocialRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
78
92
|
// callback is unauthenticated (browser redirect) but bound by signed state.
|
|
79
93
|
app.post("/v1/workspaces/:workspaceId/social/oauth/start", async (c) => {
|
|
80
94
|
const workspaceId = c.req.param("workspaceId");
|
|
81
|
-
const grant = await requireAccessGrant(c, deps, workspaceId, "workspace:admin");
|
|
82
95
|
const parsed = SocialOAuthStartRequest.safeParse(await c.req.json());
|
|
83
96
|
if (!parsed.success) {
|
|
84
97
|
throw new HTTPException(400, {
|
|
@@ -86,6 +99,12 @@ export function registerSocialRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
86
99
|
});
|
|
87
100
|
}
|
|
88
101
|
const payload = parsed.data;
|
|
102
|
+
const grant = await requireAccessGrant(
|
|
103
|
+
c,
|
|
104
|
+
deps,
|
|
105
|
+
workspaceId,
|
|
106
|
+
payload.ownership === "workspace" ? "workspace:admin" : "workspace:read",
|
|
107
|
+
);
|
|
89
108
|
const result = await startSocialOAuth(
|
|
90
109
|
{ db, settings, observability },
|
|
91
110
|
{
|
|
@@ -114,7 +133,7 @@ export function registerSocialRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
114
133
|
|
|
115
134
|
app.get("/v1/workspaces/:workspaceId/social/posts", async (c) => {
|
|
116
135
|
const workspaceId = c.req.param("workspaceId");
|
|
117
|
-
await requireAccessGrant(c, deps, workspaceId, "workspace:read");
|
|
136
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "workspace:read");
|
|
118
137
|
const since = parseSince(c.req.query("since"));
|
|
119
138
|
const connectionIds = parseConnectionIds(
|
|
120
139
|
c.req.query("connectionIds") ?? c.req.query("connectionId"),
|
|
@@ -122,6 +141,7 @@ export function registerSocialRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
122
141
|
return c.json(
|
|
123
142
|
await listSocialPosts(db, {
|
|
124
143
|
workspaceId,
|
|
144
|
+
subjectId: grant.subjectId,
|
|
125
145
|
...(connectionIds?.length ? { connectionIds } : {}),
|
|
126
146
|
...(since ? { since } : {}),
|
|
127
147
|
limit: boundedLimit(c.req.query("limit")),
|
|
@@ -138,6 +158,7 @@ export function registerSocialRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
138
158
|
await createSocialPost(db, {
|
|
139
159
|
accountId: grant.accountId,
|
|
140
160
|
workspaceId,
|
|
161
|
+
subjectId: grant.subjectId,
|
|
141
162
|
connectionId: payload.connectionId,
|
|
142
163
|
externalPostId: payload.externalPostId ?? null,
|
|
143
164
|
url: payload.url ?? null,
|