mirror of
https://github.com/eliasstepanik/core.git
synced 2026-01-11 16:48:27 +00:00
First cut: integrations
This commit is contained in:
parent
293927fa06
commit
12453954b7
@ -1,13 +1,10 @@
|
||||
import { RiGithubFill } from "@remixicon/react";
|
||||
import { Button } from "./button";
|
||||
import { Separator } from "./separator";
|
||||
import { useLocation } from "@remix-run/react";
|
||||
|
||||
const PAGE_TITLES: Record<string, string> = {
|
||||
"/home/dashboard": "Memory graph",
|
||||
"/home/chat": "Chat",
|
||||
"/home/api": "API",
|
||||
"/home/logs": "Logs",
|
||||
"/home/integrations": "Integrations",
|
||||
"/home/activity": "Activity",
|
||||
};
|
||||
|
||||
function getHeaderTitle(pathname: string): string {
|
||||
@ -29,18 +26,6 @@ export function SiteHeader() {
|
||||
<header className="flex h-(--header-height) shrink-0 items-center gap-2 border-b transition-[width,height] ease-linear group-has-data-[collapsible=icon]/sidebar-wrapper:h-(--header-height)">
|
||||
<div className="flex w-full items-center gap-1 px-4 lg:gap-2">
|
||||
<h1 className="text-base">{title}</h1>
|
||||
<div className="ml-auto flex items-center gap-2">
|
||||
<Button variant="ghost" size="sm" className="hidden sm:flex">
|
||||
<a
|
||||
href="https://github.com/redplanethq/core"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
className="dark:text-foreground"
|
||||
>
|
||||
<RiGithubFill size={20} />
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
|
||||
32
apps/webapp/app/routes/api.v1.oauth._index.tsx
Normal file
32
apps/webapp/app/routes/api.v1.oauth._index.tsx
Normal file
@ -0,0 +1,32 @@
|
||||
import { json } from "@remix-run/node";
|
||||
import { createActionApiRoute } from "~/services/routeBuilders/apiBuilder.server";
|
||||
import { OAuthBodySchema } from "~/services/oauth/oauth-utils.server";
|
||||
|
||||
import { getRedirectURL } from "~/services/oauth/oauth.server";
|
||||
import { getWorkspaceByUser } from "~/models/workspace.server";
|
||||
|
||||
// This route handles the OAuth redirect URL generation, similar to the NestJS controller
|
||||
const { action, loader } = createActionApiRoute(
|
||||
{
|
||||
body: OAuthBodySchema,
|
||||
allowJWT: true,
|
||||
authorization: {
|
||||
action: "oauth",
|
||||
},
|
||||
corsStrategy: "all",
|
||||
},
|
||||
async ({ body, authentication, request }) => {
|
||||
const workspace = await getWorkspaceByUser(authentication.userId);
|
||||
|
||||
// Call the service to get the redirect URL
|
||||
const redirectURL = await getRedirectURL(
|
||||
body,
|
||||
authentication.userId,
|
||||
workspace?.id,
|
||||
);
|
||||
|
||||
return json(redirectURL);
|
||||
},
|
||||
);
|
||||
|
||||
export { action, loader };
|
||||
21
apps/webapp/app/routes/api.v1.oauth.callback.tsx
Normal file
21
apps/webapp/app/routes/api.v1.oauth.callback.tsx
Normal file
@ -0,0 +1,21 @@
|
||||
import { createActionApiRoute } from "~/services/routeBuilders/apiBuilder.server";
|
||||
import { callbackHandler } from "~/services/oauth/oauth.server";
|
||||
import type { CallbackParams } from "~/services/oauth/oauth-utils.server";
|
||||
|
||||
// This route handles the OAuth callback, similar to the NestJS controller
|
||||
const { loader } = createActionApiRoute(
|
||||
{
|
||||
allowJWT: false,
|
||||
corsStrategy: "all",
|
||||
},
|
||||
async ({ request }) => {
|
||||
const url = new URL(request.url);
|
||||
const params: CallbackParams = {};
|
||||
for (const [key, value] of url.searchParams.entries()) {
|
||||
params[key] = value;
|
||||
}
|
||||
return await callbackHandler(params, request);
|
||||
},
|
||||
);
|
||||
|
||||
export { loader };
|
||||
@ -1,213 +1,213 @@
|
||||
import { json } from "@remix-run/node";
|
||||
import { z } from "zod";
|
||||
import { createActionApiRoute } from "~/services/routeBuilders/apiBuilder.server";
|
||||
import { getUserQueue, type IngestBodyRequest } from "~/lib/ingest.server";
|
||||
import { prisma } from "~/db.server";
|
||||
import { logger } from "~/services/logger.service";
|
||||
import { IngestionStatus, type Prisma } from "@core/database";
|
||||
// import { json } from "@remix-run/node";
|
||||
// import { z } from "zod";
|
||||
// import { createActionApiRoute } from "~/services/routeBuilders/apiBuilder.server";
|
||||
// import { getUserQueue, type IngestBodyRequest } from "~/lib/ingest.server";
|
||||
// import { prisma } from "~/db.server";
|
||||
// import { logger } from "~/services/logger.service";
|
||||
// import { IngestionStatus, type Prisma } from "@core/database";
|
||||
|
||||
const ReingestionBodyRequest = z.object({
|
||||
userId: z.string().optional(),
|
||||
spaceId: z.string().optional(),
|
||||
dryRun: z.boolean().optional().default(false),
|
||||
});
|
||||
// const ReingestionBodyRequest = z.object({
|
||||
// userId: z.string().optional(),
|
||||
// spaceId: z.string().optional(),
|
||||
// dryRun: z.boolean().optional().default(false),
|
||||
// });
|
||||
|
||||
type ReingestionRequest = z.infer<typeof ReingestionBodyRequest>;
|
||||
// type ReingestionRequest = z.infer<typeof ReingestionBodyRequest>;
|
||||
|
||||
async function getCompletedIngestionsByUser(userId?: string, spaceId?: string) {
|
||||
const whereClause: Prisma.IngestionQueueWhereInput = {
|
||||
status: IngestionStatus.COMPLETED,
|
||||
};
|
||||
// async function getCompletedIngestionsByUser(userId?: string, spaceId?: string) {
|
||||
// const whereClause: Prisma.IngestionQueueWhereInput = {
|
||||
// status: IngestionStatus.COMPLETED,
|
||||
// };
|
||||
|
||||
if (userId) {
|
||||
whereClause.workspace = {
|
||||
userId: userId,
|
||||
};
|
||||
}
|
||||
// if (userId) {
|
||||
// whereClause.workspace = {
|
||||
// userId: userId,
|
||||
// };
|
||||
// }
|
||||
|
||||
if (spaceId) {
|
||||
whereClause.spaceId = spaceId;
|
||||
}
|
||||
// if (spaceId) {
|
||||
// whereClause.spaceId = spaceId;
|
||||
// }
|
||||
|
||||
const ingestions = await prisma.ingestionQueue.findMany({
|
||||
where: whereClause,
|
||||
include: {
|
||||
workspace: {
|
||||
include: {
|
||||
user: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: [
|
||||
{ createdAt: 'asc' }, // Maintain temporal order
|
||||
],
|
||||
});
|
||||
// const ingestions = await prisma.ingestionQueue.findMany({
|
||||
// where: whereClause,
|
||||
// include: {
|
||||
// workspace: {
|
||||
// include: {
|
||||
// user: true,
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// orderBy: [
|
||||
// { createdAt: 'asc' }, // Maintain temporal order
|
||||
// ],
|
||||
// });
|
||||
|
||||
return ingestions;
|
||||
}
|
||||
// return ingestions;
|
||||
// }
|
||||
|
||||
async function getAllUsers() {
|
||||
const users = await prisma.user.findMany({
|
||||
include: {
|
||||
Workspace: true,
|
||||
},
|
||||
});
|
||||
return users.filter(user => user.Workspace); // Only users with workspaces
|
||||
}
|
||||
// async function getAllUsers() {
|
||||
// const users = await prisma.user.findMany({
|
||||
// include: {
|
||||
// Workspace: true,
|
||||
// },
|
||||
// });
|
||||
// return users.filter(user => user.Workspace); // Only users with workspaces
|
||||
// }
|
||||
|
||||
async function reingestionForUser(userId: string, spaceId?: string, dryRun = false) {
|
||||
const ingestions = await getCompletedIngestionsByUser(userId, spaceId);
|
||||
|
||||
logger.info(`Found ${ingestions.length} completed ingestions for user ${userId}${spaceId ? ` in space ${spaceId}` : ''}`);
|
||||
// async function reingestionForUser(userId: string, spaceId?: string, dryRun = false) {
|
||||
// const ingestions = await getCompletedIngestionsByUser(userId, spaceId);
|
||||
|
||||
if (dryRun) {
|
||||
return {
|
||||
userId,
|
||||
ingestionCount: ingestions.length,
|
||||
ingestions: ingestions.map(ing => ({
|
||||
id: ing.id,
|
||||
createdAt: ing.createdAt,
|
||||
spaceId: ing.spaceId,
|
||||
data: {
|
||||
episodeBody: (ing.data as any)?.episodeBody?.substring(0, 100) +
|
||||
((ing.data as any)?.episodeBody?.length > 100 ? '...' : ''),
|
||||
source: (ing.data as any)?.source,
|
||||
referenceTime: (ing.data as any)?.referenceTime,
|
||||
},
|
||||
})),
|
||||
};
|
||||
}
|
||||
// logger.info(`Found ${ingestions.length} completed ingestions for user ${userId}${spaceId ? ` in space ${spaceId}` : ''}`);
|
||||
|
||||
// Queue ingestions in temporal order (already sorted by createdAt ASC)
|
||||
const queuedJobs = [];
|
||||
const ingestionQueue = getUserQueue(userId);
|
||||
for (const ingestion of ingestions) {
|
||||
try {
|
||||
// Parse the original data and add reingestion metadata
|
||||
const originalData = ingestion.data as z.infer<typeof IngestBodyRequest>;
|
||||
|
||||
const reingestionData = {
|
||||
...originalData,
|
||||
source: `reingest-${originalData.source}`,
|
||||
metadata: {
|
||||
...originalData.metadata,
|
||||
isReingestion: true,
|
||||
originalIngestionId: ingestion.id,
|
||||
},
|
||||
};
|
||||
// if (dryRun) {
|
||||
// return {
|
||||
// userId,
|
||||
// ingestionCount: ingestions.length,
|
||||
// ingestions: ingestions.map(ing => ({
|
||||
// id: ing.id,
|
||||
// createdAt: ing.createdAt,
|
||||
// spaceId: ing.spaceId,
|
||||
// data: {
|
||||
// episodeBody: (ing.data as any)?.episodeBody?.substring(0, 100) +
|
||||
// ((ing.data as any)?.episodeBody?.length > 100 ? '...' : ''),
|
||||
// source: (ing.data as any)?.source,
|
||||
// referenceTime: (ing.data as any)?.referenceTime,
|
||||
// },
|
||||
// })),
|
||||
// };
|
||||
// }
|
||||
|
||||
const jobDetails = await ingestionQueue.add(
|
||||
`ingest-user-${userId}`,
|
||||
{
|
||||
queueId: ingestion.id,
|
||||
spaceId: ingestion.spaceId,
|
||||
userId: userId,
|
||||
body: ingestion.data,
|
||||
},
|
||||
{
|
||||
jobId: `${userId}-${Date.now()}`,
|
||||
},
|
||||
);
|
||||
// // Queue ingestions in temporal order (already sorted by createdAt ASC)
|
||||
// const queuedJobs = [];
|
||||
// const ingestionQueue = getUserQueue(userId);
|
||||
// for (const ingestion of ingestions) {
|
||||
// try {
|
||||
// // Parse the original data and add reingestion metadata
|
||||
// const originalData = ingestion.data as z.infer<typeof IngestBodyRequest>;
|
||||
|
||||
queuedJobs.push({id: jobDetails.id});
|
||||
} catch (error) {
|
||||
logger.error(`Failed to queue ingestion ${ingestion.id} for user ${userId}:`, {error});
|
||||
}
|
||||
}
|
||||
// const reingestionData = {
|
||||
// ...originalData,
|
||||
// source: `reingest-${originalData.source}`,
|
||||
// metadata: {
|
||||
// ...originalData.metadata,
|
||||
// isReingestion: true,
|
||||
// originalIngestionId: ingestion.id,
|
||||
// },
|
||||
// };
|
||||
|
||||
return {
|
||||
userId,
|
||||
ingestionCount: ingestions.length,
|
||||
queuedJobsCount: queuedJobs.length,
|
||||
queuedJobs,
|
||||
};
|
||||
}
|
||||
// const jobDetails = await ingestionQueue.add(
|
||||
// `ingest-user-${userId}`,
|
||||
// {
|
||||
// queueId: ingestion.id,
|
||||
// spaceId: ingestion.spaceId,
|
||||
// userId: userId,
|
||||
// body: ingestion.data,
|
||||
// },
|
||||
// {
|
||||
// jobId: `${userId}-${Date.now()}`,
|
||||
// },
|
||||
// );
|
||||
|
||||
const { action, loader } = createActionApiRoute(
|
||||
{
|
||||
body: ReingestionBodyRequest,
|
||||
allowJWT: true,
|
||||
authorization: {
|
||||
action: "reingest",
|
||||
},
|
||||
corsStrategy: "all",
|
||||
},
|
||||
async ({ body, authentication }) => {
|
||||
const { userId, spaceId, dryRun } = body;
|
||||
// queuedJobs.push({id: jobDetails.id});
|
||||
// } catch (error) {
|
||||
// logger.error(`Failed to queue ingestion ${ingestion.id} for user ${userId}:`, {error});
|
||||
// }
|
||||
// }
|
||||
|
||||
try {
|
||||
// Check if the user is an admin
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: authentication.userId }
|
||||
});
|
||||
// return {
|
||||
// userId,
|
||||
// ingestionCount: ingestions.length,
|
||||
// queuedJobsCount: queuedJobs.length,
|
||||
// queuedJobs,
|
||||
// };
|
||||
// }
|
||||
|
||||
if (!user || user.admin !== true) {
|
||||
logger.warn("Unauthorized reingest attempt", {
|
||||
requestUserId: authentication.userId,
|
||||
});
|
||||
return json(
|
||||
{
|
||||
success: false,
|
||||
error: "Unauthorized: Only admin users can perform reingestion"
|
||||
},
|
||||
{ status: 403 }
|
||||
);
|
||||
}
|
||||
if (userId) {
|
||||
// Reingest for specific user
|
||||
const result = await reingestionForUser(userId, spaceId, dryRun);
|
||||
return json({
|
||||
success: true,
|
||||
type: "single_user",
|
||||
result,
|
||||
});
|
||||
} else {
|
||||
// Reingest for all users
|
||||
const users = await getAllUsers();
|
||||
const results = [];
|
||||
// const { action, loader } = createActionApiRoute(
|
||||
// {
|
||||
// body: ReingestionBodyRequest,
|
||||
// allowJWT: true,
|
||||
// authorization: {
|
||||
// action: "reingest",
|
||||
// },
|
||||
// corsStrategy: "all",
|
||||
// },
|
||||
// async ({ body, authentication }) => {
|
||||
// const { userId, spaceId, dryRun } = body;
|
||||
|
||||
logger.info(`Starting reingestion for ${users.length} users`);
|
||||
// try {
|
||||
// // Check if the user is an admin
|
||||
// const user = await prisma.user.findUnique({
|
||||
// where: { id: authentication.userId }
|
||||
// });
|
||||
|
||||
for (const user of users) {
|
||||
try {
|
||||
const result = await reingestionForUser(user.id, spaceId, dryRun);
|
||||
results.push(result);
|
||||
|
||||
if (!dryRun) {
|
||||
// Add small delay between users to prevent overwhelming the system
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(`Failed to reingest for user ${user.id}:`, {error});
|
||||
results.push({
|
||||
userId: user.id,
|
||||
error: error instanceof Error ? error.message : "Unknown error",
|
||||
});
|
||||
}
|
||||
}
|
||||
// if (!user || user.admin !== true) {
|
||||
// logger.warn("Unauthorized reingest attempt", {
|
||||
// requestUserId: authentication.userId,
|
||||
// });
|
||||
// return json(
|
||||
// {
|
||||
// success: false,
|
||||
// error: "Unauthorized: Only admin users can perform reingestion"
|
||||
// },
|
||||
// { status: 403 }
|
||||
// );
|
||||
// }
|
||||
// if (userId) {
|
||||
// // Reingest for specific user
|
||||
// const result = await reingestionForUser(userId, spaceId, dryRun);
|
||||
// return json({
|
||||
// success: true,
|
||||
// type: "single_user",
|
||||
// result,
|
||||
// });
|
||||
// } else {
|
||||
// // Reingest for all users
|
||||
// const users = await getAllUsers();
|
||||
// const results = [];
|
||||
|
||||
return json({
|
||||
success: true,
|
||||
type: "all_users",
|
||||
totalUsers: users.length,
|
||||
results,
|
||||
summary: {
|
||||
totalIngestions: results.reduce((sum, r) => sum, 0),
|
||||
totalQueuedJobs: results.reduce((sum, r) => sum, 0),
|
||||
},
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error("Reingestion failed:", {error});
|
||||
return json(
|
||||
{
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : "Unknown error",
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
// logger.info(`Starting reingestion for ${users.length} users`);
|
||||
|
||||
export { action, loader };
|
||||
// for (const user of users) {
|
||||
// try {
|
||||
// const result = await reingestionForUser(user.id, spaceId, dryRun);
|
||||
// results.push(result);
|
||||
|
||||
// if (!dryRun) {
|
||||
// // Add small delay between users to prevent overwhelming the system
|
||||
// await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
// }
|
||||
// } catch (error) {
|
||||
// logger.error(`Failed to reingest for user ${user.id}:`, {error});
|
||||
// results.push({
|
||||
// userId: user.id,
|
||||
// error: error instanceof Error ? error.message : "Unknown error",
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
|
||||
// return json({
|
||||
// success: true,
|
||||
// type: "all_users",
|
||||
// totalUsers: users.length,
|
||||
// results,
|
||||
// summary: {
|
||||
// totalIngestions: results.reduce((sum, r) => sum, 0),
|
||||
// totalQueuedJobs: results.reduce((sum, r) => sum, 0),
|
||||
// },
|
||||
// });
|
||||
// }
|
||||
// } catch (error) {
|
||||
// logger.error("Reingestion failed:", {error});
|
||||
// return json(
|
||||
// {
|
||||
// success: false,
|
||||
// error: error instanceof Error ? error.message : "Unknown error",
|
||||
// },
|
||||
// { status: 500 }
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
// );
|
||||
|
||||
// export { action, loader };
|
||||
|
||||
@ -86,7 +86,7 @@ export default function API() {
|
||||
const [name, setName] = useState("");
|
||||
|
||||
const onSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
|
||||
fetcher.submit({ name }, { method: "POST", action: "/home/api" });
|
||||
fetcher.submit({ name }, { method: "POST", action: "/settings/api" });
|
||||
setOpen(false);
|
||||
setShowToken(true);
|
||||
};
|
||||
|
||||
@ -85,7 +85,7 @@ export async function authenticateApiKeyWithFailure(
|
||||
}
|
||||
|
||||
export function isSecretApiKey(key: string) {
|
||||
return key.startsWith("tr_");
|
||||
return key.startsWith("rc_");
|
||||
}
|
||||
|
||||
export function getApiKeyFromRequest(request: Request) {
|
||||
|
||||
@ -5,9 +5,7 @@ import {
|
||||
type MailTransportOptions,
|
||||
} from "emails";
|
||||
|
||||
import { redirect } from "remix-typedjson";
|
||||
import { env } from "~/env.server";
|
||||
import type { AuthUser } from "./authUser";
|
||||
|
||||
import { logger } from "./logger.service";
|
||||
import { singleton } from "~/utils/singleton";
|
||||
|
||||
88
apps/webapp/app/services/integration.server.ts
Normal file
88
apps/webapp/app/services/integration.server.ts
Normal file
@ -0,0 +1,88 @@
|
||||
import { tasks } from "@trigger.dev/sdk/v3";
|
||||
|
||||
import { getOrCreatePersonalAccessToken } from "./personalAccessToken.server";
|
||||
import { logger } from "./logger.service";
|
||||
import { type integrationRun } from "~/trigger/integrations/integration-run";
|
||||
|
||||
import type { IntegrationDefinitionV2 } from "@core/database";
|
||||
|
||||
/**
|
||||
* Prepares the parameters for triggering an integration.
|
||||
* If userId is provided, gets or creates a personal access token for the user.
|
||||
*/
|
||||
async function prepareIntegrationTrigger(
|
||||
integrationDefinition: IntegrationDefinitionV2,
|
||||
userId?: string,
|
||||
workspaceId?: string,
|
||||
) {
|
||||
logger.info(`Loading integration ${integrationDefinition.slug}`);
|
||||
|
||||
let pat = "";
|
||||
if (userId) {
|
||||
// Use the integration slug as the token name for uniqueness
|
||||
const tokenResult = await getOrCreatePersonalAccessToken({
|
||||
name: integrationDefinition.slug ?? "integration",
|
||||
userId,
|
||||
});
|
||||
pat = tokenResult.token ?? "";
|
||||
}
|
||||
|
||||
return {
|
||||
integrationDefinition,
|
||||
pat,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggers an integration run asynchronously.
|
||||
*/
|
||||
export async function runIntegrationTriggerAsync(
|
||||
integrationDefinition: IntegrationDefinitionV2,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
event: any,
|
||||
userId?: string,
|
||||
workspaceId?: string,
|
||||
) {
|
||||
const params = await prepareIntegrationTrigger(
|
||||
integrationDefinition,
|
||||
userId,
|
||||
workspaceId,
|
||||
);
|
||||
return await tasks.trigger<typeof integrationRun>("integration-run", {
|
||||
...params,
|
||||
event,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggers an integration run and waits for completion.
|
||||
*/
|
||||
export async function runIntegrationTrigger(
|
||||
integrationDefinition: IntegrationDefinitionV2,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
event: any,
|
||||
userId?: string,
|
||||
workspaceId?: string,
|
||||
) {
|
||||
const params = await prepareIntegrationTrigger(
|
||||
integrationDefinition,
|
||||
userId,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
const response = await tasks.triggerAndPoll<typeof integrationRun>(
|
||||
"integration-run",
|
||||
{
|
||||
...params,
|
||||
integrationAccount: event.integrationAccount,
|
||||
event: event.event,
|
||||
eventBody: event.eventBody,
|
||||
},
|
||||
);
|
||||
|
||||
if (response.status === "COMPLETED") {
|
||||
return response.output;
|
||||
}
|
||||
|
||||
throw new Error(`Integration trigger failed with status: ${response.status}`);
|
||||
}
|
||||
24
apps/webapp/app/services/integrationDefinition.server.ts
Normal file
24
apps/webapp/app/services/integrationDefinition.server.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import { prisma } from "~/db.server";
|
||||
|
||||
/**
|
||||
* Get all integration definitions available to a workspace.
|
||||
* Returns both global (workspaceId: null) and workspace-specific definitions.
|
||||
*/
|
||||
export async function getIntegrationDefinitions(workspaceId: string) {
|
||||
return prisma.integrationDefinitionV2.findMany({
|
||||
where: {
|
||||
OR: [{ workspaceId: null }, { workspaceId }],
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a single integration definition by its ID.
|
||||
*/
|
||||
export async function getIntegrationDefinitionWithId(
|
||||
integrationDefinitionId: string,
|
||||
) {
|
||||
return prisma.integrationDefinitionV2.findUnique({
|
||||
where: { id: integrationDefinitionId },
|
||||
});
|
||||
}
|
||||
@ -1,8 +1,6 @@
|
||||
import { openai } from "@ai-sdk/openai";
|
||||
import { type CoreMessage, embed } from "ai";
|
||||
import {
|
||||
EpisodeType,
|
||||
LLMModelEnum,
|
||||
type AddEpisodeParams,
|
||||
type EntityNode,
|
||||
type EpisodicNode,
|
||||
|
||||
155
apps/webapp/app/services/oauth/oauth-utils.server.ts
Normal file
155
apps/webapp/app/services/oauth/oauth-utils.server.ts
Normal file
@ -0,0 +1,155 @@
|
||||
import { type OAuth2Params } from "@core/types";
|
||||
import { IsBoolean, IsString } from "class-validator";
|
||||
import type { IntegrationDefinitionV2 } from "@core/database";
|
||||
import { z } from "zod";
|
||||
|
||||
export interface RedirectURLParams {
|
||||
workspaceSlug: string;
|
||||
integrationOAuthAppName: string;
|
||||
config: string;
|
||||
}
|
||||
|
||||
export interface SessionRecord {
|
||||
integrationDefinitionId: string;
|
||||
config: OAuth2Params;
|
||||
redirectURL: string;
|
||||
workspaceId: string;
|
||||
accountIdentifier?: string;
|
||||
integrationKeys?: string;
|
||||
personal: boolean;
|
||||
userId?: string;
|
||||
}
|
||||
|
||||
export class OAuthBodyInterface {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
config?: any;
|
||||
|
||||
@IsString()
|
||||
redirectURL: string;
|
||||
|
||||
@IsBoolean()
|
||||
personal: boolean = false;
|
||||
|
||||
@IsString()
|
||||
integrationDefinitionId: string;
|
||||
}
|
||||
|
||||
export const OAuthBodySchema = z.object({
|
||||
config: z.any().optional(),
|
||||
redirectURL: z.string(),
|
||||
personal: z.boolean().default(false),
|
||||
integrationDefinitionId: z.string(),
|
||||
});
|
||||
|
||||
export type CallbackParams = Record<string, string>;
|
||||
|
||||
export interface ProviderConfig {
|
||||
client_id: string;
|
||||
client_secret: string;
|
||||
scopes: string;
|
||||
}
|
||||
|
||||
const enum ProviderAuthModes {
|
||||
"OAuth2" = "OAuth2",
|
||||
}
|
||||
|
||||
export interface ProviderTemplate extends OAuth2Params {
|
||||
auth_mode: ProviderAuthModes;
|
||||
}
|
||||
|
||||
export enum OAuthAuthorizationMethod {
|
||||
BODY = "body",
|
||||
HEADER = "header",
|
||||
}
|
||||
|
||||
export enum OAuthBodyFormat {
|
||||
FORM = "form",
|
||||
JSON = "json",
|
||||
}
|
||||
|
||||
export interface ProviderTemplateOAuth2 extends ProviderTemplate {
|
||||
auth_mode: ProviderAuthModes.OAuth2;
|
||||
|
||||
disable_pkce?: boolean; // Defaults to false (=PKCE used) if not provided
|
||||
|
||||
token_params?: {
|
||||
grant_type?: "authorization_code" | "client_credentials";
|
||||
};
|
||||
|
||||
refresh_params?: {
|
||||
grant_type: "refresh_token";
|
||||
};
|
||||
|
||||
authorization_method?: OAuthAuthorizationMethod;
|
||||
body_format?: OAuthBodyFormat;
|
||||
|
||||
refresh_url?: string;
|
||||
|
||||
token_request_auth_method?: "basic";
|
||||
}
|
||||
|
||||
/**
|
||||
* A helper function to interpolate a string.
|
||||
* interpolateString('Hello ${name} of ${age} years", {name: 'Tester', age: 234}) -> returns 'Hello Tester of age 234 years'
|
||||
*
|
||||
* @remarks
|
||||
* Copied from https://stackoverflow.com/a/1408373/250880
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export function interpolateString(str: string, replacers: Record<string, any>) {
|
||||
return str.replace(/\${([^{}]*)}/g, (a, b) => {
|
||||
const r = replacers[b];
|
||||
return typeof r === "string" || typeof r === "number" ? (r as string) : a; // Typecast needed to make TypeScript happy
|
||||
});
|
||||
}
|
||||
|
||||
export function getSimpleOAuth2ClientConfig(
|
||||
providerConfig: ProviderConfig,
|
||||
template: ProviderTemplate,
|
||||
connectionConfig: OAuth2Params,
|
||||
) {
|
||||
const tokenUrl = new URL(
|
||||
interpolateString(template.token_url, connectionConfig),
|
||||
);
|
||||
const authorizeUrl = new URL(
|
||||
interpolateString(template.authorization_url, connectionConfig),
|
||||
);
|
||||
const headers = { "User-Agent": "Sol" };
|
||||
|
||||
const authConfig = template as ProviderTemplateOAuth2;
|
||||
|
||||
return {
|
||||
client: {
|
||||
id: providerConfig.client_id,
|
||||
secret: providerConfig.client_secret,
|
||||
},
|
||||
auth: {
|
||||
tokenHost: tokenUrl.origin,
|
||||
tokenPath: tokenUrl.pathname,
|
||||
authorizeHost: authorizeUrl.origin,
|
||||
authorizePath: authorizeUrl.pathname,
|
||||
},
|
||||
http: { headers },
|
||||
options: {
|
||||
authorizationMethod:
|
||||
authConfig.authorization_method || OAuthAuthorizationMethod.BODY,
|
||||
bodyFormat: authConfig.body_format || OAuthBodyFormat.FORM,
|
||||
scopeSeparator: template.scope_separator || " ",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export async function getTemplate(
|
||||
integrationDefinition: IntegrationDefinitionV2,
|
||||
): Promise<ProviderTemplate> {
|
||||
const spec = integrationDefinition.spec as any;
|
||||
const template: ProviderTemplate = spec.auth.OAuth2 as ProviderTemplate;
|
||||
|
||||
if (!template) {
|
||||
throw new Error(
|
||||
`This extension doesn't support OAuth. Reach out to us if you need support for this extension`,
|
||||
);
|
||||
}
|
||||
|
||||
return template;
|
||||
}
|
||||
245
apps/webapp/app/services/oauth/oauth.server.ts
Normal file
245
apps/webapp/app/services/oauth/oauth.server.ts
Normal file
@ -0,0 +1,245 @@
|
||||
import { IntegrationPayloadEventType, type OAuth2Params } from "@core/types";
|
||||
import * as simpleOauth2 from "simple-oauth2";
|
||||
import { tasks } from "@trigger.dev/sdk/v3";
|
||||
import {
|
||||
getSimpleOAuth2ClientConfig,
|
||||
getTemplate,
|
||||
type OAuthBodyInterface,
|
||||
type ProviderTemplateOAuth2,
|
||||
type SessionRecord,
|
||||
} from "./oauth-utils.server";
|
||||
import { getIntegrationDefinitionWithId } from "../integrationDefinition.server";
|
||||
import { type scheduler } from "~/trigger/integrations/scheduler";
|
||||
import { logger } from "../logger.service";
|
||||
import { runIntegrationTrigger } from "../integration.server";
|
||||
import type { IntegrationDefinitionV2 } from "@core/database";
|
||||
import { env } from "~/env.server";
|
||||
|
||||
// Use process.env for config in Remix
|
||||
const CALLBACK_URL = process.env.OAUTH_CALLBACK_URL ?? "";
|
||||
|
||||
// Session store (in-memory, for single server)
|
||||
const session: Record<string, SessionRecord> = {};
|
||||
|
||||
export type CallbackParams = Record<string, string>;
|
||||
|
||||
// Remix-style callback handler
|
||||
// Accepts a Remix LoaderFunctionArgs-like object: { request }
|
||||
export async function callbackHandler(
|
||||
params: CallbackParams,
|
||||
request: Request,
|
||||
) {
|
||||
if (!params.state) {
|
||||
throw new Error("No state found");
|
||||
}
|
||||
|
||||
const sessionRecord = session[params.state];
|
||||
|
||||
// Delete the session once it's used
|
||||
delete session[params.state];
|
||||
|
||||
if (!sessionRecord) {
|
||||
throw new Error("No session found");
|
||||
}
|
||||
|
||||
const integrationDefinition = await getIntegrationDefinitionWithId(
|
||||
sessionRecord.integrationDefinitionId,
|
||||
);
|
||||
|
||||
const template = (await getTemplate(
|
||||
integrationDefinition as IntegrationDefinitionV2,
|
||||
)) as ProviderTemplateOAuth2;
|
||||
|
||||
if (integrationDefinition === null) {
|
||||
const errorMessage = "No matching integration definition found";
|
||||
return new Response(null, {
|
||||
status: 302,
|
||||
headers: {
|
||||
Location: `${sessionRecord.redirectURL}?success=false&error=${encodeURIComponent(
|
||||
errorMessage,
|
||||
)}`,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
let additionalTokenParams: Record<string, string> = {};
|
||||
if (template.token_params !== undefined) {
|
||||
const deepCopy = JSON.parse(JSON.stringify(template.token_params));
|
||||
additionalTokenParams = deepCopy;
|
||||
}
|
||||
|
||||
if (template.refresh_params) {
|
||||
additionalTokenParams = template.refresh_params;
|
||||
}
|
||||
|
||||
const headers: Record<string, string> = {};
|
||||
|
||||
const integrationConfig = integrationDefinition.config as any;
|
||||
const integrationSpec = integrationDefinition.spec as any;
|
||||
|
||||
if (template.token_request_auth_method === "basic") {
|
||||
headers["Authorization"] = `Basic ${Buffer.from(
|
||||
`${integrationConfig?.clientId}:${integrationConfig.clientSecret}`,
|
||||
).toString("base64")}`;
|
||||
}
|
||||
|
||||
const accountIdentifier = sessionRecord.accountIdentifier
|
||||
? `&accountIdentifier=${encodeURIComponent(sessionRecord.accountIdentifier)}`
|
||||
: "";
|
||||
const integrationKeys = sessionRecord.integrationKeys
|
||||
? `&integrationKeys=${encodeURIComponent(sessionRecord.integrationKeys)}`
|
||||
: "";
|
||||
|
||||
try {
|
||||
const scopes = (integrationSpec.auth.OAuth2 as OAuth2Params)
|
||||
.scopes as string[];
|
||||
|
||||
const simpleOAuthClient = new simpleOauth2.AuthorizationCode(
|
||||
getSimpleOAuth2ClientConfig(
|
||||
{
|
||||
client_id: integrationConfig.clientId,
|
||||
client_secret: integrationConfig.clientSecret,
|
||||
scopes: scopes.join(","),
|
||||
},
|
||||
template,
|
||||
sessionRecord.config,
|
||||
),
|
||||
);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const tokensResponse: any = await simpleOAuthClient.getToken(
|
||||
{
|
||||
code: params.code as string,
|
||||
redirect_uri: CALLBACK_URL,
|
||||
...additionalTokenParams,
|
||||
},
|
||||
{
|
||||
headers,
|
||||
},
|
||||
);
|
||||
|
||||
const integrationAccount = await runIntegrationTrigger(
|
||||
integrationDefinition,
|
||||
{
|
||||
event: IntegrationPayloadEventType.INTEGRATION_ACCOUNT_CREATED,
|
||||
eventBody: {
|
||||
oauthResponse: tokensResponse.token,
|
||||
oauthParams: {
|
||||
...params,
|
||||
redirect_uri: CALLBACK_URL,
|
||||
},
|
||||
integrationDefinition,
|
||||
},
|
||||
},
|
||||
sessionRecord.userId,
|
||||
sessionRecord.workspaceId,
|
||||
);
|
||||
|
||||
await tasks.trigger<typeof scheduler>("scheduler", {
|
||||
integrationAccountId: integrationAccount.id,
|
||||
});
|
||||
|
||||
return new Response(null, {
|
||||
status: 302,
|
||||
headers: {
|
||||
Location: `${sessionRecord.redirectURL}?success=true&integrationName=${encodeURIComponent(
|
||||
integrationDefinition.name,
|
||||
)}${accountIdentifier}${integrationKeys}`,
|
||||
},
|
||||
});
|
||||
} catch (e: any) {
|
||||
logger.error(e);
|
||||
|
||||
return new Response(null, {
|
||||
status: 302,
|
||||
headers: {
|
||||
Location: `${sessionRecord.redirectURL}?success=false&error=${encodeURIComponent(
|
||||
e.message,
|
||||
)}${accountIdentifier}${integrationKeys}`,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function getRedirectURL(
|
||||
oAuthBody: OAuthBodyInterface,
|
||||
userId: string,
|
||||
workspaceId?: string,
|
||||
specificScopes?: string,
|
||||
) {
|
||||
const { integrationDefinitionId, personal } = oAuthBody;
|
||||
|
||||
const redirectURL = `${env.APP_ORIGIN}/integrations`;
|
||||
|
||||
logger.info(
|
||||
`We got OAuth request for ${workspaceId}: ${integrationDefinitionId}`,
|
||||
);
|
||||
|
||||
const integrationDefinition = await getIntegrationDefinitionWithId(
|
||||
integrationDefinitionId,
|
||||
);
|
||||
|
||||
if (!integrationDefinition) {
|
||||
throw new Error("No integration definition ");
|
||||
}
|
||||
|
||||
const spec = integrationDefinition.spec as any;
|
||||
const externalConfig = spec.auth.OAuth2 as OAuth2Params;
|
||||
const template = await getTemplate(integrationDefinition);
|
||||
|
||||
const scopesString =
|
||||
specificScopes || (externalConfig.scopes as string[]).join(",");
|
||||
const additionalAuthParams = template.authorization_params || {};
|
||||
|
||||
const integrationConfig = integrationDefinition.config as any;
|
||||
|
||||
try {
|
||||
const simpleOAuthClient = new simpleOauth2.AuthorizationCode(
|
||||
getSimpleOAuth2ClientConfig(
|
||||
{
|
||||
client_id: integrationConfig.clientId,
|
||||
client_secret: integrationConfig.clientSecret,
|
||||
scopes: scopesString,
|
||||
},
|
||||
template,
|
||||
externalConfig,
|
||||
),
|
||||
);
|
||||
|
||||
const uniqueId = Date.now().toString(36);
|
||||
session[uniqueId] = {
|
||||
integrationDefinitionId: integrationDefinition.id,
|
||||
redirectURL,
|
||||
workspaceId: workspaceId as string,
|
||||
config: externalConfig,
|
||||
userId,
|
||||
personal,
|
||||
};
|
||||
|
||||
const scopes = [
|
||||
...scopesString.split(","),
|
||||
...(template.default_scopes || []),
|
||||
];
|
||||
|
||||
const scopeIdentifier = externalConfig.scope_identifier ?? "scope";
|
||||
|
||||
const authorizationUri = simpleOAuthClient.authorizeURL({
|
||||
redirect_uri: CALLBACK_URL,
|
||||
[scopeIdentifier]: scopes.join(template.scope_separator || " "),
|
||||
state: uniqueId,
|
||||
...additionalAuthParams,
|
||||
});
|
||||
|
||||
logger.debug(
|
||||
`OAuth 2.0 for ${integrationDefinition.name} - redirecting to: ${authorizationUri}`,
|
||||
);
|
||||
|
||||
return {
|
||||
status: 200,
|
||||
redirectURL: authorizationUri,
|
||||
};
|
||||
} catch (e: any) {
|
||||
logger.warn(e);
|
||||
throw new Error(e.message);
|
||||
}
|
||||
}
|
||||
@ -268,6 +268,58 @@ export async function createPersonalAccessTokenFromAuthorizationCode(
|
||||
return token;
|
||||
}
|
||||
|
||||
/** Get or create a PersonalAccessToken for the given name and userId.
|
||||
* If one exists (not revoked), return it (without the unencrypted token).
|
||||
* If not, create a new one and return it (with the unencrypted token).
|
||||
* We only ever return the unencrypted token once, on creation.
|
||||
*/
|
||||
export async function getOrCreatePersonalAccessToken({
|
||||
name,
|
||||
userId,
|
||||
}: CreatePersonalAccessTokenOptions) {
|
||||
// Try to find an existing, non-revoked token
|
||||
const existing = await prisma.personalAccessToken.findFirst({
|
||||
where: {
|
||||
name,
|
||||
userId,
|
||||
revokedAt: null,
|
||||
},
|
||||
});
|
||||
|
||||
if (existing) {
|
||||
// Do not return the unencrypted token if it already exists
|
||||
return {
|
||||
id: existing.id,
|
||||
name: existing.name,
|
||||
userId: existing.userId,
|
||||
obfuscatedToken: existing.obfuscatedToken,
|
||||
// token is not returned
|
||||
};
|
||||
}
|
||||
|
||||
// Create a new token
|
||||
const token = createToken();
|
||||
const encryptedToken = encryptToken(token);
|
||||
|
||||
const personalAccessToken = await prisma.personalAccessToken.create({
|
||||
data: {
|
||||
name,
|
||||
userId,
|
||||
encryptedToken,
|
||||
obfuscatedToken: obfuscateToken(token),
|
||||
hashedToken: hashToken(token),
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
id: personalAccessToken.id,
|
||||
name,
|
||||
userId,
|
||||
token,
|
||||
obfuscatedToken: personalAccessToken.obfuscatedToken,
|
||||
};
|
||||
}
|
||||
|
||||
/** Created a new PersonalAccessToken, and return the token. We only ever return the unencrypted token once. */
|
||||
export async function createPersonalAccessToken({
|
||||
name,
|
||||
@ -306,7 +358,7 @@ function createToken() {
|
||||
return `${tokenPrefix}${tokenGenerator()}`;
|
||||
}
|
||||
|
||||
/** Obfuscates all but the first and last 4 characters of the token, so it looks like tr_pat_bhbd•••••••••••••••••••fd4a */
|
||||
/** Obfuscates all but the first and last 4 characters of the token, so it looks like rc_pat_bhbd•••••••••••••••••••fd4a */
|
||||
function obfuscateToken(token: string) {
|
||||
const withoutPrefix = token.replace(tokenPrefix, "");
|
||||
const obfuscated = `${withoutPrefix.slice(0, 4)}${"•".repeat(18)}${withoutPrefix.slice(-4)}`;
|
||||
|
||||
@ -397,6 +397,8 @@ export function createActionApiRoute<
|
||||
maxContentLength,
|
||||
} = options;
|
||||
|
||||
console.log(options);
|
||||
|
||||
async function loader({ request, params }: LoaderFunctionArgs) {
|
||||
if (corsStrategy !== "none" && request.method.toUpperCase() === "OPTIONS") {
|
||||
return apiCors(request, json({}));
|
||||
|
||||
@ -60,6 +60,8 @@
|
||||
"clsx": "^2.1.1",
|
||||
"compression": "^1.7.4",
|
||||
"cross-env": "^7.0.3",
|
||||
"class-transformer": "0.5.1",
|
||||
"class-validator": "0.14.1",
|
||||
"d3": "^7.9.0",
|
||||
"date-fns": "^4.1.0",
|
||||
"dayjs": "^1.11.10",
|
||||
@ -92,6 +94,7 @@
|
||||
"remix-utils": "^7.7.0",
|
||||
"sdk": "link:@modelcontextprotocol/sdk",
|
||||
"sigma": "^3.0.2",
|
||||
"simple-oauth2": "^5.1.0",
|
||||
"tailwind-merge": "^2.6.0",
|
||||
"tailwind-scrollbar-hide": "^2.0.0",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
@ -113,6 +116,7 @@
|
||||
"@types/d3": "^7.4.3",
|
||||
"@types/express": "^4.17.13",
|
||||
"@types/morgan": "^1.9.3",
|
||||
"@types/simple-oauth2": "^5.0.7",
|
||||
"@types/react": "^18.2.20",
|
||||
"@types/react-dom": "^18.2.7",
|
||||
"@typescript-eslint/eslint-plugin": "^6.7.4",
|
||||
|
||||
@ -25,6 +25,7 @@
|
||||
"skipLibCheck": true,
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"strictPropertyInitialization": false,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"~/*": ["./app/*"],
|
||||
|
||||
21
core/types/package.json
Normal file
21
core/types/package.json
Normal file
@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "@echo/core-types",
|
||||
"version": "1.0.0",
|
||||
"description": "Core types for Echo integrations",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"dev": "tsc --watch"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.0.0"
|
||||
},
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./dist/index.js",
|
||||
"require": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts"
|
||||
}
|
||||
}
|
||||
}
|
||||
1
core/types/src/index.ts
Normal file
1
core/types/src/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './integration';
|
||||
64
core/types/src/integration.ts
Normal file
64
core/types/src/integration.ts
Normal file
@ -0,0 +1,64 @@
|
||||
export enum IntegrationEventType {
|
||||
/**
|
||||
* Setting up or creating an integration account
|
||||
*/
|
||||
SETUP = "setup",
|
||||
|
||||
/**
|
||||
* Processing incoming data from the integration
|
||||
*/
|
||||
PROCESS = "process",
|
||||
|
||||
/**
|
||||
* Identifying which account a webhook belongs to
|
||||
*/
|
||||
IDENTIFY = "identify",
|
||||
|
||||
/**
|
||||
* Scheduled synchronization of data
|
||||
*/
|
||||
SYNC = "sync",
|
||||
}
|
||||
|
||||
export interface IntegrationEventPayload {
|
||||
event: IntegrationEventType;
|
||||
[x: string]: any;
|
||||
}
|
||||
|
||||
export interface Spec {
|
||||
name: string;
|
||||
key: string;
|
||||
description: string;
|
||||
icon: string;
|
||||
mcp?: {
|
||||
command: string;
|
||||
args: string[];
|
||||
env: Record<string, string>;
|
||||
};
|
||||
auth?: {
|
||||
OAuth2?: {
|
||||
token_url: string;
|
||||
authorization_url: string;
|
||||
scopes: string[];
|
||||
scope_identifier?: string;
|
||||
scope_separator?: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export interface Config {
|
||||
access_token: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export interface Identifier {
|
||||
id: string;
|
||||
type?: string;
|
||||
}
|
||||
|
||||
export type MessageType = 'spec' | 'data' | 'identifier';
|
||||
|
||||
export interface Message {
|
||||
type: MessageType;
|
||||
data: any;
|
||||
}
|
||||
18
core/types/tsconfig.json
Normal file
18
core/types/tsconfig.json
Normal file
@ -0,0 +1,18 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"module": "ESNext",
|
||||
"lib": ["ES2020"],
|
||||
"declaration": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"moduleResolution": "node",
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true
|
||||
},
|
||||
"include": ["src/**/*", "*.ts"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
2
integrations/slack/.gitignore
vendored
Normal file
2
integrations/slack/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
bin
|
||||
node_modules
|
||||
22
integrations/slack/.prettierrc
Normal file
22
integrations/slack/.prettierrc
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"arrowParens": "always",
|
||||
"tabWidth": 2,
|
||||
"useTabs": false,
|
||||
"semi": true,
|
||||
"bracketSpacing": true,
|
||||
"jsxBracketSameLine": false,
|
||||
"requirePragma": false,
|
||||
"proseWrap": "preserve",
|
||||
"singleQuote": true,
|
||||
"formatOnSave": true,
|
||||
"trailingComma": "all",
|
||||
"printWidth": 100,
|
||||
"overrides": [
|
||||
{
|
||||
"files": ".prettierrc",
|
||||
"options": {
|
||||
"parser": "json"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
81
integrations/slack/eslint.config.js
Normal file
81
integrations/slack/eslint.config.js
Normal file
@ -0,0 +1,81 @@
|
||||
const eslint = require('@eslint/js');
|
||||
const tseslint = require('typescript-eslint');
|
||||
const jestPlugin = require('eslint-plugin-jest');
|
||||
const importPlugin = require('eslint-plugin-import');
|
||||
const prettierPlugin = require('eslint-plugin-prettier');
|
||||
const unusedImportsPlugin = require('eslint-plugin-unused-imports');
|
||||
|
||||
module.exports = [
|
||||
eslint.configs.recommended,
|
||||
...tseslint.configs.recommended,
|
||||
{
|
||||
files: ['**/*.{js,jsx,ts,tsx}'],
|
||||
plugins: {
|
||||
jest: jestPlugin,
|
||||
import: importPlugin,
|
||||
prettier: prettierPlugin,
|
||||
'unused-imports': unusedImportsPlugin,
|
||||
},
|
||||
languageOptions: {
|
||||
ecmaVersion: 2020,
|
||||
sourceType: 'module',
|
||||
parser: tseslint.parser,
|
||||
parserOptions: {
|
||||
ecmaFeatures: {
|
||||
jsx: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
curly: 'warn',
|
||||
'dot-location': 'warn',
|
||||
eqeqeq: 'error',
|
||||
'prettier/prettier': 'warn',
|
||||
'unused-imports/no-unused-imports': 'warn',
|
||||
'no-else-return': 'warn',
|
||||
'no-lonely-if': 'warn',
|
||||
'no-inner-declarations': 'off',
|
||||
'no-unused-vars': 'off',
|
||||
'no-useless-computed-key': 'warn',
|
||||
'no-useless-return': 'warn',
|
||||
'no-var': 'warn',
|
||||
'object-shorthand': ['warn', 'always'],
|
||||
'prefer-arrow-callback': 'warn',
|
||||
'prefer-const': 'warn',
|
||||
'prefer-destructuring': ['warn', { AssignmentExpression: { array: true } }],
|
||||
'prefer-object-spread': 'warn',
|
||||
'prefer-template': 'warn',
|
||||
'spaced-comment': ['warn', 'always', { markers: ['/'] }],
|
||||
yoda: 'warn',
|
||||
'import/order': [
|
||||
'warn',
|
||||
{
|
||||
'newlines-between': 'always',
|
||||
groups: ['type', 'builtin', 'external', 'internal', ['parent', 'sibling'], 'index'],
|
||||
pathGroupsExcludedImportTypes: ['builtin'],
|
||||
pathGroups: [],
|
||||
alphabetize: {
|
||||
order: 'asc',
|
||||
caseInsensitive: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
'@typescript-eslint/array-type': ['warn', { default: 'array-simple' }],
|
||||
'@typescript-eslint/ban-ts-comment': [
|
||||
'warn',
|
||||
{
|
||||
'ts-expect-error': 'allow-with-description',
|
||||
},
|
||||
],
|
||||
'@typescript-eslint/consistent-indexed-object-style': ['warn', 'record'],
|
||||
'@typescript-eslint/consistent-type-definitions': ['warn', 'interface'],
|
||||
'@typescript-eslint/no-unused-vars': 'warn',
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['scripts/**/*'],
|
||||
rules: {
|
||||
'@typescript-eslint/no-var-requires': 'off',
|
||||
},
|
||||
},
|
||||
];
|
||||
71
integrations/slack/package.json
Normal file
71
integrations/slack/package.json
Normal file
@ -0,0 +1,71 @@
|
||||
{
|
||||
"name": "@sol/slack",
|
||||
"version": "0.1.2",
|
||||
"description": "slack extension for Sol",
|
||||
"main": "./bin/index.js",
|
||||
"module": "./bin/index.mjs",
|
||||
"type": "module",
|
||||
"files": [
|
||||
"slack",
|
||||
"bin"
|
||||
],
|
||||
"bin": {
|
||||
"slack": "./bin/index.js"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "rimraf bin && npx tsup",
|
||||
"lint": "eslint --ext js,ts,tsx backend/ frontend/ --fix",
|
||||
"prettier": "prettier --config .prettierrc --write ."
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/preset-typescript": "^7.26.0",
|
||||
"@rollup/plugin-commonjs": "^28.0.1",
|
||||
"@rollup/plugin-json": "^6.1.0",
|
||||
"@rollup/plugin-node-resolve": "^15.3.0",
|
||||
"@rollup/plugin-replace": "^5.0.7",
|
||||
"@types/node": "^18.0.20",
|
||||
"eslint": "^9.24.0",
|
||||
"eslint-config-prettier": "^10.1.2",
|
||||
"eslint-import-resolver-alias": "^1.1.2",
|
||||
"eslint-plugin-import": "^2.31.0",
|
||||
"eslint-plugin-jest": "^27.9.0",
|
||||
"eslint-plugin-prettier": "^5.2.1",
|
||||
"eslint-plugin-unused-imports": "^2.0.0",
|
||||
"prettier": "^3.4.2",
|
||||
"rimraf": "^3.0.2",
|
||||
"rollup": "^4.28.1",
|
||||
"rollup-plugin-node-polyfills": "^0.2.1",
|
||||
"rollup-plugin-terser": "^7.0.2",
|
||||
"rollup-plugin-typescript2": "^0.34.1",
|
||||
"tslib": "^2.8.1",
|
||||
"typescript": "^4.7.2",
|
||||
"tsup": "^8.0.1",
|
||||
"ncc": "0.3.6"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.7.9",
|
||||
"commander": "^12.0.0",
|
||||
"openai": "^4.0.0",
|
||||
"react-query": "^3.39.3",
|
||||
"@echo/core-types": "workspace:*"
|
||||
}
|
||||
}
|
||||
5101
integrations/slack/pnpm-lock.yaml
generated
Normal file
5101
integrations/slack/pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
41
integrations/slack/spec.json
Normal file
41
integrations/slack/spec.json
Normal file
@ -0,0 +1,41 @@
|
||||
{
|
||||
"name": "Slack extension",
|
||||
"key": "slack",
|
||||
"description": "Connect your workspace to Slack. Run your workflows from slack bookmarks",
|
||||
"icon": "slack",
|
||||
"mcp": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "@modelcontextprotocol/server-slack"],
|
||||
"env": {
|
||||
"SLACK_BOT_TOKEN": "${config:access_token}",
|
||||
"SLACK_TEAM_ID": "${config:team_id}",
|
||||
"SLACK_CHANNEL_IDS": "${config:channel_ids}"
|
||||
}
|
||||
},
|
||||
"auth": {
|
||||
"OAuth2": {
|
||||
"token_url": "https://slack.com/api/oauth.v2.access",
|
||||
"authorization_url": "https://slack.com/oauth/v2/authorize",
|
||||
"scopes": [
|
||||
"stars:read",
|
||||
"team:read",
|
||||
"stars:write",
|
||||
"users:read",
|
||||
"channels:read",
|
||||
"groups:read",
|
||||
"im:read",
|
||||
"im:history",
|
||||
"mpim:read",
|
||||
"mpim:write",
|
||||
"mpim:history",
|
||||
"channels:history",
|
||||
"chat:write",
|
||||
"reactions:read",
|
||||
"reactions:write",
|
||||
"users.profile:read"
|
||||
],
|
||||
"scope_identifier": "user_scope",
|
||||
"scope_separator": ","
|
||||
}
|
||||
}
|
||||
}
|
||||
27
integrations/slack/src/account-create.ts
Normal file
27
integrations/slack/src/account-create.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import axios from 'axios';
|
||||
|
||||
export async function integrationCreate(
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
data: any,
|
||||
integrationDefinition: any,
|
||||
) {
|
||||
const { oauthResponse } = data;
|
||||
const integrationConfiguration = {
|
||||
access_token: oauthResponse.authed_user.access_token,
|
||||
teamId: oauthResponse.team.id,
|
||||
teamName: oauthResponse.team.name,
|
||||
userId: oauthResponse.authed_user.id,
|
||||
scope: oauthResponse.authed_user.scope,
|
||||
};
|
||||
|
||||
const payload = {
|
||||
settings: {},
|
||||
accountId: integrationConfiguration.userId,
|
||||
config: integrationConfiguration,
|
||||
integrationDefinitionId: integrationDefinition.id,
|
||||
};
|
||||
|
||||
const integrationAccount = (await axios.post(`/api/v1/integration_account`, payload)).data;
|
||||
|
||||
return integrationAccount;
|
||||
}
|
||||
186
integrations/slack/src/common/IntegrationCLI.ts
Normal file
186
integrations/slack/src/common/IntegrationCLI.ts
Normal file
@ -0,0 +1,186 @@
|
||||
import { Command } from 'commander';
|
||||
import {
|
||||
IntegrationEventPayload,
|
||||
Spec,
|
||||
Config,
|
||||
Identifier,
|
||||
Message
|
||||
} from '@echo/core-types';
|
||||
|
||||
export abstract class IntegrationCLI {
|
||||
protected program: Command;
|
||||
protected integrationName: string;
|
||||
protected version: string;
|
||||
|
||||
constructor(integrationName: string, version: string = '1.0.0') {
|
||||
this.integrationName = integrationName;
|
||||
this.version = version;
|
||||
this.program = new Command();
|
||||
this.setupProgram();
|
||||
}
|
||||
|
||||
private setupProgram(): void {
|
||||
this.program
|
||||
.name(`${this.integrationName}-integration`)
|
||||
.description(`${this.integrationName} integration CLI`)
|
||||
.version(this.version);
|
||||
|
||||
this.setupSpecCommand();
|
||||
this.setupAccountCommands();
|
||||
this.setupDataCommands();
|
||||
this.setupSyncCommand();
|
||||
}
|
||||
|
||||
private setupAccountCommands(): void {
|
||||
const accountCmd = this.program
|
||||
.command('account')
|
||||
.description(`Manage ${this.integrationName} integration accounts`);
|
||||
|
||||
accountCmd
|
||||
.command('create')
|
||||
.description(`Create a new ${this.integrationName} integration account`)
|
||||
.requiredOption('--oauth-response <response>', 'OAuth response JSON')
|
||||
.action(async (options) => {
|
||||
try {
|
||||
const oauthResponse = JSON.parse(options.oauthResponse);
|
||||
const integrationDefinition = JSON.parse(options.integrationDefinition);
|
||||
|
||||
const result = await this.handleEvent({
|
||||
event: 'INTEGRATION_ACCOUNT_CREATED',
|
||||
eventBody: { oauthResponse },
|
||||
integrationDefinition,
|
||||
});
|
||||
|
||||
console.log('Account created successfully:', JSON.stringify(result, null, 2));
|
||||
} catch (error) {
|
||||
console.error('Error creating account:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private setupDataCommands(): void {
|
||||
this.program
|
||||
.command('process')
|
||||
.description(`Process ${this.integrationName} integration data`)
|
||||
.requiredOption('--event-data <data>', 'Event data JSON')
|
||||
.requiredOption('--config <config>', 'Integration configuration JSON')
|
||||
.action(async (options) => {
|
||||
try {
|
||||
const eventData = JSON.parse(options.eventData);
|
||||
const config = JSON.parse(options.config);
|
||||
|
||||
const result = await this.handleEvent({
|
||||
event: 'PROCESS',
|
||||
eventBody: { eventData },
|
||||
config,
|
||||
});
|
||||
|
||||
const message: Message = {
|
||||
type: 'data',
|
||||
data: result
|
||||
};
|
||||
console.log(JSON.stringify(message, null, 2));
|
||||
} catch (error) {
|
||||
console.error('Error processing data:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
this.program
|
||||
.command('identify')
|
||||
.description('Identify webhook account')
|
||||
.requiredOption('--webhook-data <data>', 'Webhook data JSON')
|
||||
.action(async (options) => {
|
||||
try {
|
||||
const webhookData = JSON.parse(options.webhookData);
|
||||
|
||||
const result = await this.handleEvent({
|
||||
event: 'IDENTIFY',
|
||||
eventBody: webhookData,
|
||||
});
|
||||
|
||||
const message: Message = {
|
||||
type: 'identifier',
|
||||
data: result
|
||||
};
|
||||
console.log(JSON.stringify(message, null, 2));
|
||||
} catch (error) {
|
||||
console.error('Error identifying account:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private setupSpecCommand(): void {
|
||||
this.program
|
||||
.command('spec')
|
||||
.description('Get integration specification')
|
||||
.action(async () => {
|
||||
try {
|
||||
const spec = await this.getSpec();
|
||||
const message: Message = {
|
||||
type: 'spec',
|
||||
data: spec
|
||||
};
|
||||
console.log(JSON.stringify(message, null, 2));
|
||||
} catch (error) {
|
||||
console.error('Error getting spec:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private setupSyncCommand(): void {
|
||||
this.program
|
||||
.command('sync')
|
||||
.description('Perform scheduled sync')
|
||||
.requiredOption('--config <config>', 'Integration configuration JSON')
|
||||
.action(async (options) => {
|
||||
try {
|
||||
const config = JSON.parse(options.config);
|
||||
|
||||
const result = await this.handleEvent({
|
||||
event: 'SYNC',
|
||||
eventBody: {},
|
||||
config,
|
||||
});
|
||||
|
||||
const message: Message = {
|
||||
type: 'data',
|
||||
data: result
|
||||
};
|
||||
console.log(JSON.stringify(message, null, 2));
|
||||
} catch (error) {
|
||||
console.error('Error during sync:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstract method that must be implemented by each integration
|
||||
* This method should handle the integration-specific logic for each event type
|
||||
*/
|
||||
protected abstract handleEvent(eventPayload: IntegrationEventPayload): Promise<any>;
|
||||
|
||||
/**
|
||||
* Abstract method that must be implemented by each integration
|
||||
* This method should return the integration specification
|
||||
*/
|
||||
protected abstract getSpec(): Promise<Spec>;
|
||||
|
||||
/**
|
||||
* Parse and execute the CLI commands
|
||||
*/
|
||||
public parse(): void {
|
||||
this.program.parse();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the commander program instance for additional customization
|
||||
*/
|
||||
public getProgram(): Command {
|
||||
return this.program;
|
||||
}
|
||||
}
|
||||
65
integrations/slack/src/common/README.md
Normal file
65
integrations/slack/src/common/README.md
Normal file
@ -0,0 +1,65 @@
|
||||
# IntegrationCLI Base Class
|
||||
|
||||
This is a common CLI base class that can be moved to the SDK and used by all integrations.
|
||||
|
||||
## Usage
|
||||
|
||||
### 1. Create your integration-specific CLI class:
|
||||
|
||||
```typescript
|
||||
import { IntegrationCLI, IntegrationEventPayload } from './common/IntegrationCLI';
|
||||
|
||||
export class MyIntegrationCLI extends IntegrationCLI {
|
||||
constructor() {
|
||||
super('my-integration', '1.0.0');
|
||||
}
|
||||
|
||||
protected async handleEvent(eventPayload: IntegrationEventPayload): Promise<any> {
|
||||
// Your integration-specific logic here
|
||||
return await processMyIntegrationEvent(eventPayload);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Create your CLI entry point:
|
||||
|
||||
```typescript
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { MyIntegrationCLI } from './MyIntegrationCLI';
|
||||
|
||||
const cli = new MyIntegrationCLI();
|
||||
cli.parse();
|
||||
```
|
||||
|
||||
### 3. Update your package.json:
|
||||
|
||||
```json
|
||||
{
|
||||
"bin": {
|
||||
"my-integration": "./dist/cli.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"commander": "^12.0.0"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Available Commands
|
||||
|
||||
The base class provides these commands automatically:
|
||||
|
||||
- `account create --oauth-response <json> --integration-definition <json>`
|
||||
- `account delete --account-id <id>`
|
||||
- `process --event-data <json> --integration-account <json>`
|
||||
- `identify --webhook-data <json>`
|
||||
- `sync --integration-account <json>`
|
||||
|
||||
## Moving to SDK
|
||||
|
||||
To move this to the SDK:
|
||||
|
||||
1. Move `IntegrationCLI.ts` to `@redplanethq/sol-sdk/src/cli/`
|
||||
2. Export it from the SDK's index
|
||||
3. Update imports in integrations to use the SDK version
|
||||
4. Add commander as a dependency to the SDK
|
||||
131
integrations/slack/src/create-activity.ts
Normal file
131
integrations/slack/src/create-activity.ts
Normal file
@ -0,0 +1,131 @@
|
||||
// import { IntegrationAccount } from '@redplanethq/sol-sdk';
|
||||
import axios from 'axios';
|
||||
|
||||
import { getUserDetails } from './utils';
|
||||
|
||||
async function getMessage(accessToken: string, channel: string, ts: string) {
|
||||
const result = await axios.get('https://slack.com/api/conversations.history', {
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
params: {
|
||||
channel,
|
||||
latest: ts,
|
||||
inclusive: true,
|
||||
limit: 1,
|
||||
},
|
||||
});
|
||||
|
||||
return result.data.messages?.[0];
|
||||
}
|
||||
|
||||
async function getConversationInfo(accessToken: string, channel: string) {
|
||||
const result = await axios.get('https://slack.com/api/conversations.info', {
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
},
|
||||
params: {
|
||||
channel,
|
||||
},
|
||||
});
|
||||
|
||||
return result.data.channel;
|
||||
}
|
||||
|
||||
export const createActivityEvent = async (
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
eventBody: any,
|
||||
config: any,
|
||||
) => {
|
||||
const { eventData } = eventBody;
|
||||
if (eventData.event.type === 'message' && eventData.event.channel === 'D06UAK42494') {
|
||||
const event = eventData.event;
|
||||
|
||||
if (!config) {
|
||||
throw new Error('Integration configuration not found');
|
||||
}
|
||||
|
||||
const accessToken = config.access_token;
|
||||
|
||||
const text = `DM with Sigma channel Content: '${event.text}'`;
|
||||
|
||||
const permalinkResponse = await axios.get(
|
||||
`https://slack.com/api/chat.getPermalink?channel=${event.channel}&message_ts=${event.ts}`,
|
||||
{
|
||||
headers: { Authorization: `Bearer ${accessToken}` },
|
||||
},
|
||||
);
|
||||
|
||||
const activity = {
|
||||
sourceURL: permalinkResponse.data.permalink,
|
||||
text,
|
||||
integrationAccountId: config.integrationAccountId,
|
||||
taskId: null,
|
||||
};
|
||||
|
||||
await axios.post('/api/v1/activity', activity);
|
||||
}
|
||||
|
||||
if (eventData.event.type === 'reaction_added' && eventData.event.reaction === 'eyes') {
|
||||
const event = eventData.event;
|
||||
|
||||
if (!config) {
|
||||
throw new Error('Integration configuration not found');
|
||||
}
|
||||
|
||||
const accessToken = config.access_token;
|
||||
const channel = event.item.channel;
|
||||
const ts = event.item.ts;
|
||||
|
||||
const eventMessage = await getMessage(accessToken, channel, ts);
|
||||
const mentionedUsers = getMentionUsers(eventMessage.text);
|
||||
|
||||
const [userDetails, conversationInfo] = await Promise.all([
|
||||
getUserDetails([eventMessage.user, ...mentionedUsers], config.access_token),
|
||||
getConversationInfo(accessToken, channel),
|
||||
]);
|
||||
|
||||
const userIdMap = new Map(userDetails.map((user) => [user.id, user]));
|
||||
|
||||
const eventMessageText = eventMessage.text.replace(/<@U\w+>/g, (match: string) => {
|
||||
const userId = match.replace(/<@|>/g, '');
|
||||
const user = userIdMap.get(userId);
|
||||
return user ? `@${user.real_name}|${userId}` : match;
|
||||
});
|
||||
|
||||
let conversationContext;
|
||||
if (conversationInfo.is_im) {
|
||||
const dmUser = userIdMap.get(conversationInfo.user);
|
||||
conversationContext = `direct message with ${dmUser?.real_name}(${conversationInfo.user})`;
|
||||
} else if (conversationInfo.is_group) {
|
||||
conversationContext = `private channel ${conversationInfo.name}(${conversationInfo.id})`;
|
||||
} else {
|
||||
conversationContext = `channel ${conversationInfo.name}(${conversationInfo.id})`;
|
||||
}
|
||||
|
||||
const text = `Message from user ${userIdMap.get(eventMessage.user)?.real_name}(${eventMessage.user}) in ${conversationContext} at ${eventMessage.ts}. Content: '${eventMessageText}'`;
|
||||
|
||||
const permalinkResponse = await axios.get(
|
||||
`https://slack.com/api/chat.getPermalink?channel=${channel}&message_ts=${ts}`,
|
||||
{
|
||||
headers: { Authorization: `Bearer ${accessToken}` },
|
||||
},
|
||||
);
|
||||
|
||||
const activity = {
|
||||
sourceURL: permalinkResponse.data.permalink,
|
||||
text,
|
||||
integrationAccountId: config.integrationAccountId,
|
||||
taskId: null,
|
||||
};
|
||||
|
||||
await axios.post('/api/v1/activity', activity);
|
||||
}
|
||||
return { message: `Processed activity from slack` };
|
||||
};
|
||||
|
||||
function getMentionUsers(message: string): string[] {
|
||||
const mentionUsers = message.matchAll(/<@U\w+>/g);
|
||||
return Array.from(mentionUsers).map((match) => match[0].replace(/<@|>/g, ''));
|
||||
}
|
||||
91
integrations/slack/src/index.ts
Normal file
91
integrations/slack/src/index.ts
Normal file
@ -0,0 +1,91 @@
|
||||
// import { IntegrationPayloadEventType } from '@redplanethq/sol-sdk';
|
||||
|
||||
import { integrationCreate } from './account-create';
|
||||
import { createActivityEvent } from './create-activity';
|
||||
import { IntegrationCLI } from './common/IntegrationCLI';
|
||||
import { IntegrationEventPayload, Spec } from '@echo/core-types';
|
||||
|
||||
export async function run(eventPayload: IntegrationEventPayload) {
|
||||
switch (eventPayload.event) {
|
||||
case 'SETUP':
|
||||
return await integrationCreate(eventPayload.eventBody, eventPayload.integrationDefinition);
|
||||
|
||||
case 'IDENTIFY':
|
||||
return eventPayload.eventBody.event.user;
|
||||
|
||||
case 'PROCESS':
|
||||
return createActivityEvent(eventPayload.eventBody, eventPayload.config);
|
||||
|
||||
case 'SYNC':
|
||||
return { message: 'Scheduled sync completed successfully' };
|
||||
|
||||
default:
|
||||
return {
|
||||
message: `The event payload type is ${eventPayload.event}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// CLI implementation that extends the base class
|
||||
class SlackCLI extends IntegrationCLI {
|
||||
constructor() {
|
||||
super('slack', '1.0.0');
|
||||
}
|
||||
|
||||
protected async handleEvent(eventPayload: IntegrationEventPayload): Promise<any> {
|
||||
return await run(eventPayload);
|
||||
}
|
||||
|
||||
protected async getSpec(): Promise<Spec> {
|
||||
return {
|
||||
name: "Slack extension",
|
||||
key: "slack",
|
||||
description: "Connect your workspace to Slack. Run your workflows from slack bookmarks",
|
||||
icon: "slack",
|
||||
mcp: {
|
||||
command: "npx",
|
||||
args: ["-y", "@modelcontextprotocol/server-slack"],
|
||||
env: {
|
||||
"SLACK_BOT_TOKEN": "${config:access_token}",
|
||||
"SLACK_TEAM_ID": "${config:team_id}",
|
||||
"SLACK_CHANNEL_IDS": "${config:channel_ids}"
|
||||
}
|
||||
},
|
||||
auth: {
|
||||
OAuth2: {
|
||||
token_url: "https://slack.com/api/oauth.v2.access",
|
||||
authorization_url: "https://slack.com/oauth/v2/authorize",
|
||||
scopes: [
|
||||
"stars:read",
|
||||
"team:read",
|
||||
"stars:write",
|
||||
"users:read",
|
||||
"channels:read",
|
||||
"groups:read",
|
||||
"im:read",
|
||||
"im:history",
|
||||
"mpim:read",
|
||||
"mpim:write",
|
||||
"mpim:history",
|
||||
"channels:history",
|
||||
"chat:write",
|
||||
"reactions:read",
|
||||
"reactions:write",
|
||||
"users.profile:read"
|
||||
],
|
||||
scope_identifier: "user_scope",
|
||||
scope_separator: ","
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Define a main function and invoke it directly.
|
||||
// This works after bundling to JS and running with `node index.js`.
|
||||
function main() {
|
||||
const slackCLI = new SlackCLI();
|
||||
slackCLI.parse();
|
||||
}
|
||||
|
||||
main();
|
||||
32
integrations/slack/src/utils.ts
Normal file
32
integrations/slack/src/utils.ts
Normal file
@ -0,0 +1,32 @@
|
||||
import axios from 'axios';
|
||||
|
||||
export interface ActivityCreate {
|
||||
url: string;
|
||||
text: string;
|
||||
sourceId: string;
|
||||
sourceURL: string;
|
||||
integrationAccountId: string;
|
||||
}
|
||||
|
||||
export async function getSlackTeamInfo(slackTeamId: string, accessToken: string) {
|
||||
const response = await axios.get(`https://slack.com/api/team.info?team=${slackTeamId}`, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json; charset=utf-8',
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
},
|
||||
});
|
||||
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export async function getUserDetails(userIds: string[], accessToken: string) {
|
||||
return await Promise.all(
|
||||
userIds.map(async (userId) => {
|
||||
const userResponse = await axios.get(`https://slack.com/api/users.info?user=${userId}`, {
|
||||
headers: { Authorization: `Bearer ${accessToken}` },
|
||||
});
|
||||
|
||||
return userResponse.data.user;
|
||||
}),
|
||||
);
|
||||
}
|
||||
38
integrations/slack/tsconfig.json
Normal file
38
integrations/slack/tsconfig.json
Normal file
@ -0,0 +1,38 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"baseUrl": "frontend",
|
||||
"allowJs": false,
|
||||
|
||||
"allowSyntheticDefaultImports": true,
|
||||
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
|
||||
"isolatedModules": true,
|
||||
"strictNullChecks": true,
|
||||
"removeComments": true,
|
||||
"preserveConstEnums": true,
|
||||
"sourceMap": true,
|
||||
"noUnusedParameters": true,
|
||||
"noUnusedLocals": true,
|
||||
"noImplicitReturns": true,
|
||||
"noImplicitThis": true,
|
||||
"noImplicitAny": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"useUnknownInCatchVariables": false
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "build", "dist", "scripts", "acceptance-tests", "webpack", "jest"],
|
||||
"types": ["typePatches"]
|
||||
}
|
||||
20
integrations/slack/tsup.config.ts
Normal file
20
integrations/slack/tsup.config.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import { defineConfig } from 'tsup';
|
||||
import { dependencies } from './package.json';
|
||||
|
||||
export default defineConfig({
|
||||
entry: ['src/index.ts'],
|
||||
format: ['cjs'], // or esm if you're using that
|
||||
bundle: true,
|
||||
target: 'node16',
|
||||
outDir: 'bin',
|
||||
splitting: false,
|
||||
shims: true,
|
||||
clean: true,
|
||||
name: 'slack',
|
||||
platform: 'node',
|
||||
legacyOutput: false,
|
||||
noExternal: Object.keys(dependencies || {}), // ⬅️ bundle all deps
|
||||
treeshake: {
|
||||
preset: 'recommended',
|
||||
},
|
||||
});
|
||||
@ -0,0 +1,5 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "User" ADD COLUMN "preferences" JSONB;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "Workspace" ADD COLUMN "preferences" JSONB;
|
||||
@ -60,7 +60,7 @@ model Conversation {
|
||||
workspace Workspace? @relation(fields: [workspaceId], references: [id])
|
||||
workspaceId String?
|
||||
|
||||
status String @default("pending") // Can be "pending", "running", "completed", "failed", "need_attension"
|
||||
status String @default("pending") // Can be "pending", "running", "completed", "failed", "need_attention"
|
||||
|
||||
ConversationHistory ConversationHistory[]
|
||||
}
|
||||
|
||||
@ -5,7 +5,9 @@
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"dependencies": {
|
||||
"@prisma/client": "5.4.1"
|
||||
"@prisma/client": "5.4.1",
|
||||
"class-transformer": "0.5.1",
|
||||
"class-validator": "0.14.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"prisma": "5.4.1",
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
export * from "./llm";
|
||||
export * from "./graph";
|
||||
export * from "./conversation-execution-step";
|
||||
export * from "./oauth";
|
||||
export * from "./integration";
|
||||
|
||||
68
packages/types/src/integration.ts
Normal file
68
packages/types/src/integration.ts
Normal file
@ -0,0 +1,68 @@
|
||||
export enum IntegrationPayloadEventType {
|
||||
/**
|
||||
* When a webhook is received, this event is triggered to identify which integration
|
||||
* account the webhook belongs to
|
||||
*/
|
||||
IDENTIFY_WEBHOOK_ACCOUNT = "identify_webhook_account",
|
||||
|
||||
/**
|
||||
* Lifecycle events for integration accounts
|
||||
*/
|
||||
INTEGRATION_ACCOUNT_CREATED = "integration_account_created",
|
||||
|
||||
/**
|
||||
* When data is received from the integration source (e.g. new Slack message)
|
||||
*/
|
||||
INTEGRATION_DATA_RECEIVED = "integration_data_received",
|
||||
|
||||
/**
|
||||
* For integrations without webhook support, this event is triggered at the
|
||||
* configured frequency to sync data
|
||||
*/
|
||||
SCHEDULED_SYNC = "scheduled_sync",
|
||||
}
|
||||
|
||||
export interface IntegrationEventPayload {
|
||||
event: IntegrationPayloadEventType;
|
||||
[x: string]: any;
|
||||
}
|
||||
|
||||
export interface Activity {
|
||||
id: string;
|
||||
type: string;
|
||||
timestamp: string;
|
||||
data: any;
|
||||
}
|
||||
|
||||
export interface IntegrationAccountConfig {
|
||||
access_token: string;
|
||||
team_id?: string;
|
||||
channel_ids?: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export interface IntegrationAccountIdentifier {
|
||||
identifier: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
export interface IntegrationAccountSettings {
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export type MessageType =
|
||||
| "Spec"
|
||||
| "Activity"
|
||||
| "IntegrationAccountConfig"
|
||||
| "IntegrationAccountIdentifier"
|
||||
| "IntegrationAccountSettings";
|
||||
|
||||
export interface Message {
|
||||
type: MessageType;
|
||||
data:
|
||||
| Spec
|
||||
| Activity
|
||||
| IntegrationAccountConfig
|
||||
| IntegrationAccountIdentifier
|
||||
| IntegrationAccountSettings;
|
||||
}
|
||||
1
packages/types/src/oauth/index.ts
Normal file
1
packages/types/src/oauth/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from "./params";
|
||||
25
packages/types/src/oauth/params.ts
Normal file
25
packages/types/src/oauth/params.ts
Normal file
@ -0,0 +1,25 @@
|
||||
export class OAuth2Params {
|
||||
authorization_url: string;
|
||||
authorization_params?: Record<string, string>;
|
||||
default_scopes?: string[];
|
||||
scope_separator?: string;
|
||||
scope_identifier?: string;
|
||||
token_url: string;
|
||||
token_params?: Record<string, string>;
|
||||
redirect_uri_metadata?: string[];
|
||||
token_response_metadata?: string[];
|
||||
token_expiration_buffer?: number; // In seconds.
|
||||
scopes?: string[];
|
||||
}
|
||||
|
||||
export type AuthType = "OAuth2" | "APIKey";
|
||||
|
||||
export class APIKeyParams {
|
||||
"header_name": string;
|
||||
"format": string;
|
||||
}
|
||||
|
||||
export class Spec {
|
||||
auth: Record<string, OAuth2Params | APIKeyParams>;
|
||||
other_data?: any;
|
||||
}
|
||||
@ -12,7 +12,10 @@
|
||||
"noEmit": false,
|
||||
"outDir": "./dist",
|
||||
"declaration": true,
|
||||
"declarationDir": "./dist"
|
||||
"declarationDir": "./dist",
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"strictPropertyInitialization": false
|
||||
},
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
|
||||
173
pnpm-lock.yaml
generated
173
pnpm-lock.yaml
generated
@ -165,6 +165,12 @@ importers:
|
||||
bullmq:
|
||||
specifier: ^5.53.2
|
||||
version: 5.53.2
|
||||
class-transformer:
|
||||
specifier: 0.5.1
|
||||
version: 0.5.1
|
||||
class-validator:
|
||||
specifier: 0.14.1
|
||||
version: 0.14.1
|
||||
class-variance-authority:
|
||||
specifier: ^0.7.1
|
||||
version: 0.7.1
|
||||
@ -273,6 +279,9 @@ importers:
|
||||
sigma:
|
||||
specifier: ^3.0.2
|
||||
version: 3.0.2(graphology-types@0.24.8)
|
||||
simple-oauth2:
|
||||
specifier: ^5.1.0
|
||||
version: 5.1.0
|
||||
tailwind-merge:
|
||||
specifier: ^2.6.0
|
||||
version: 2.6.0
|
||||
@ -337,6 +346,9 @@ importers:
|
||||
'@types/react-dom':
|
||||
specifier: ^18.2.7
|
||||
version: 18.3.7(@types/react@18.2.69)
|
||||
'@types/simple-oauth2':
|
||||
specifier: ^5.0.7
|
||||
version: 5.0.7
|
||||
'@typescript-eslint/eslint-plugin':
|
||||
specifier: ^6.7.4
|
||||
version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1)(typescript@5.8.3)
|
||||
@ -410,6 +422,12 @@ importers:
|
||||
specifier: ^4.2.1
|
||||
version: 4.3.2(typescript@5.8.3)(vite@6.3.5(@types/node@24.0.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.42.0)(yaml@2.8.0))
|
||||
|
||||
core/types:
|
||||
devDependencies:
|
||||
typescript:
|
||||
specifier: ^5.0.0
|
||||
version: 5.8.3
|
||||
|
||||
packages/database:
|
||||
dependencies:
|
||||
'@prisma/client':
|
||||
@ -468,6 +486,12 @@ importers:
|
||||
'@prisma/client':
|
||||
specifier: 5.4.1
|
||||
version: 5.4.1(prisma@5.4.1)
|
||||
class-transformer:
|
||||
specifier: 0.5.1
|
||||
version: 0.5.1
|
||||
class-validator:
|
||||
specifier: 0.14.1
|
||||
version: 0.14.1
|
||||
devDependencies:
|
||||
esbuild:
|
||||
specifier: ^0.25.5
|
||||
@ -1533,6 +1557,24 @@ packages:
|
||||
engines: {node: '>=6'}
|
||||
hasBin: true
|
||||
|
||||
'@hapi/boom@10.0.1':
|
||||
resolution: {integrity: sha512-ERcCZaEjdH3OgSJlyjVk8pHIFeus91CjKP3v+MpgBNp5IvGzP2l/bRiD78nqYcKPaZdbKkK5vDBVPd2ohHBlsA==}
|
||||
|
||||
'@hapi/bourne@3.0.0':
|
||||
resolution: {integrity: sha512-Waj1cwPXJDucOib4a3bAISsKJVb15MKi9IvmTI/7ssVEm6sywXGjVJDhl6/umt1pK1ZS7PacXU3A1PmFKHEZ2w==}
|
||||
|
||||
'@hapi/hoek@11.0.7':
|
||||
resolution: {integrity: sha512-HV5undWkKzcB4RZUusqOpcgxOaq6VOAH7zhhIr2g3G8NF/MlFO75SjOr2NfuSx0Mh40+1FqCkagKLJRykUWoFQ==}
|
||||
|
||||
'@hapi/hoek@9.3.0':
|
||||
resolution: {integrity: sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==}
|
||||
|
||||
'@hapi/topo@5.1.0':
|
||||
resolution: {integrity: sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==}
|
||||
|
||||
'@hapi/wreck@18.1.0':
|
||||
resolution: {integrity: sha512-0z6ZRCmFEfV/MQqkQomJ7sl/hyxvcZM7LtuVqN3vdAO4vM9eBbowl0kaqQj9EJJQab+3Uuh1GxbGIBFy4NfJ4w==}
|
||||
|
||||
'@humanwhocodes/config-array@0.13.0':
|
||||
resolution: {integrity: sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==}
|
||||
engines: {node: '>=10.10.0'}
|
||||
@ -3172,6 +3214,15 @@ packages:
|
||||
'@selderee/plugin-htmlparser2@0.11.0':
|
||||
resolution: {integrity: sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==}
|
||||
|
||||
'@sideway/address@4.1.5':
|
||||
resolution: {integrity: sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==}
|
||||
|
||||
'@sideway/formula@3.0.1':
|
||||
resolution: {integrity: sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==}
|
||||
|
||||
'@sideway/pinpoint@2.0.0':
|
||||
resolution: {integrity: sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==}
|
||||
|
||||
'@sindresorhus/merge-streams@4.0.0':
|
||||
resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==}
|
||||
engines: {node: '>=18'}
|
||||
@ -3799,9 +3850,15 @@ packages:
|
||||
'@types/shimmer@1.2.0':
|
||||
resolution: {integrity: sha512-UE7oxhQLLd9gub6JKIAhDq06T0F6FnztwMNRvYgjeQSBeMc1ZG/tA47EwfduvkuQS8apbkM/lpLpWsaCeYsXVg==}
|
||||
|
||||
'@types/simple-oauth2@5.0.7':
|
||||
resolution: {integrity: sha512-8JbWVJbiTSBQP/7eiyGKyXWAqp3dKQZpaA+pdW16FCi32ujkzRMG8JfjoAzdWt6W8U591ZNdHcPtP2D7ILTKuA==}
|
||||
|
||||
'@types/unist@2.0.11':
|
||||
resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==}
|
||||
|
||||
'@types/validator@13.15.2':
|
||||
resolution: {integrity: sha512-y7pa/oEJJ4iGYBxOpfAKn5b9+xuihvzDVnC/OSvlVnGxVg0pOqmjiMafiJ1KVNQEaPZf9HsEp5icEwGg8uIe5Q==}
|
||||
|
||||
'@types/webpack@5.28.5':
|
||||
resolution: {integrity: sha512-wR87cgvxj3p6D0Crt1r5avwqffqPXUkNlnQ1mjU93G7gCuFjufZR4I6j8cz5g1F1tTYpfOOFvly+cmIQwL9wvw==}
|
||||
|
||||
@ -4495,6 +4552,12 @@ packages:
|
||||
cjs-module-lexer@1.4.3:
|
||||
resolution: {integrity: sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==}
|
||||
|
||||
class-transformer@0.5.1:
|
||||
resolution: {integrity: sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==}
|
||||
|
||||
class-validator@0.14.1:
|
||||
resolution: {integrity: sha512-2VEG9JICxIqTpoK1eMzZqaV+u/EiwEJkMGzTrZf6sU/fwsnOITVgYJ8yojSy6CaXtO9V0Cc6ZQZ8h8m4UBuLwQ==}
|
||||
|
||||
class-variance-authority@0.7.1:
|
||||
resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==}
|
||||
|
||||
@ -6197,6 +6260,9 @@ packages:
|
||||
resolution: {integrity: sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==}
|
||||
hasBin: true
|
||||
|
||||
joi@17.13.3:
|
||||
resolution: {integrity: sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA==}
|
||||
|
||||
jose@5.10.0:
|
||||
resolution: {integrity: sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==}
|
||||
|
||||
@ -6299,6 +6365,9 @@ packages:
|
||||
resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
|
||||
libphonenumber-js@1.12.9:
|
||||
resolution: {integrity: sha512-VWwAdNeJgN7jFOD+wN4qx83DTPMVPPAUyx9/TUkBXKLiNkuWWk6anV0439tgdtwaJDrEdqkvdN22iA6J4bUCZg==}
|
||||
|
||||
lightningcss-darwin-arm64@1.30.1:
|
||||
resolution: {integrity: sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
@ -8028,6 +8097,9 @@ packages:
|
||||
resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
|
||||
engines: {node: '>=14'}
|
||||
|
||||
simple-oauth2@5.1.0:
|
||||
resolution: {integrity: sha512-gWDa38Ccm4MwlG5U7AlcJxPv3lvr80dU7ARJWrGdgvOKyzSj1gr3GBPN1rABTedAYvC/LsGYoFuFxwDBPtGEbw==}
|
||||
|
||||
simple-swizzle@0.2.2:
|
||||
resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==}
|
||||
|
||||
@ -8728,6 +8800,10 @@ packages:
|
||||
resolution: {integrity: sha512-OljLrQ9SQdOUqTaQxqL5dEfZWrXExyyWsozYlAWFawPVNuD83igl7uJD2RTkNMbniIYgt8l81eCJGIdQF7avLQ==}
|
||||
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
|
||||
|
||||
validator@13.15.15:
|
||||
resolution: {integrity: sha512-BgWVbCI72aIQy937xbawcs+hrVaN/CZ2UwutgaJ36hGqRrLNM+f5LUT/YPRbo8IV/ASeFzXszezV+y2+rq3l8A==}
|
||||
engines: {node: '>= 0.10'}
|
||||
|
||||
vary@1.1.2:
|
||||
resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==}
|
||||
engines: {node: '>= 0.8'}
|
||||
@ -10250,6 +10326,26 @@ snapshots:
|
||||
protobufjs: 7.5.3
|
||||
yargs: 17.7.2
|
||||
|
||||
'@hapi/boom@10.0.1':
|
||||
dependencies:
|
||||
'@hapi/hoek': 11.0.7
|
||||
|
||||
'@hapi/bourne@3.0.0': {}
|
||||
|
||||
'@hapi/hoek@11.0.7': {}
|
||||
|
||||
'@hapi/hoek@9.3.0': {}
|
||||
|
||||
'@hapi/topo@5.1.0':
|
||||
dependencies:
|
||||
'@hapi/hoek': 9.3.0
|
||||
|
||||
'@hapi/wreck@18.1.0':
|
||||
dependencies:
|
||||
'@hapi/boom': 10.0.1
|
||||
'@hapi/bourne': 3.0.0
|
||||
'@hapi/hoek': 11.0.7
|
||||
|
||||
'@humanwhocodes/config-array@0.13.0':
|
||||
dependencies:
|
||||
'@humanwhocodes/object-schema': 2.0.3
|
||||
@ -10728,7 +10824,7 @@ snapshots:
|
||||
react-dom: 18.3.1(react@18.3.1)
|
||||
optionalDependencies:
|
||||
'@types/react': 18.2.47
|
||||
'@types/react-dom': 18.3.7(@types/react@18.2.47)
|
||||
'@types/react-dom': 18.3.7(@types/react@18.2.69)
|
||||
|
||||
'@radix-ui/react-arrow@1.1.7(@types/react-dom@18.3.7(@types/react@18.2.69))(@types/react@18.2.69)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
|
||||
dependencies:
|
||||
@ -10782,7 +10878,7 @@ snapshots:
|
||||
react-dom: 18.3.1(react@18.3.1)
|
||||
optionalDependencies:
|
||||
'@types/react': 18.2.47
|
||||
'@types/react-dom': 18.3.7(@types/react@18.2.47)
|
||||
'@types/react-dom': 18.3.7(@types/react@18.2.69)
|
||||
|
||||
'@radix-ui/react-collapsible@1.1.11(@types/react-dom@18.3.7(@types/react@18.2.69))(@types/react@18.2.69)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
|
||||
dependencies:
|
||||
@ -10810,7 +10906,7 @@ snapshots:
|
||||
react-dom: 18.3.1(react@18.3.1)
|
||||
optionalDependencies:
|
||||
'@types/react': 18.2.47
|
||||
'@types/react-dom': 18.3.7(@types/react@18.2.47)
|
||||
'@types/react-dom': 18.3.7(@types/react@18.2.69)
|
||||
|
||||
'@radix-ui/react-collection@1.1.7(@types/react-dom@18.3.7(@types/react@18.2.69))(@types/react@18.2.69)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
|
||||
dependencies:
|
||||
@ -10900,7 +10996,7 @@ snapshots:
|
||||
react-dom: 18.3.1(react@18.3.1)
|
||||
optionalDependencies:
|
||||
'@types/react': 18.2.47
|
||||
'@types/react-dom': 18.3.7(@types/react@18.2.47)
|
||||
'@types/react-dom': 18.3.7(@types/react@18.2.69)
|
||||
|
||||
'@radix-ui/react-dismissable-layer@1.1.10(@types/react-dom@18.3.7(@types/react@18.2.69))(@types/react@18.2.69)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
|
||||
dependencies:
|
||||
@ -10951,7 +11047,7 @@ snapshots:
|
||||
react-dom: 18.3.1(react@18.3.1)
|
||||
optionalDependencies:
|
||||
'@types/react': 18.2.47
|
||||
'@types/react-dom': 18.3.7(@types/react@18.2.47)
|
||||
'@types/react-dom': 18.3.7(@types/react@18.2.69)
|
||||
|
||||
'@radix-ui/react-focus-scope@1.1.7(@types/react-dom@18.3.7(@types/react@18.2.69))(@types/react@18.2.69)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
|
||||
dependencies:
|
||||
@ -11038,7 +11134,7 @@ snapshots:
|
||||
react-remove-scroll: 2.5.7(@types/react@18.2.47)(react@18.3.1)
|
||||
optionalDependencies:
|
||||
'@types/react': 18.2.47
|
||||
'@types/react-dom': 18.3.7(@types/react@18.2.47)
|
||||
'@types/react-dom': 18.3.7(@types/react@18.2.69)
|
||||
|
||||
'@radix-ui/react-popover@1.1.14(@types/react-dom@18.3.7(@types/react@18.2.69))(@types/react@18.2.69)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
|
||||
dependencies:
|
||||
@ -11079,7 +11175,7 @@ snapshots:
|
||||
react-dom: 18.3.1(react@18.3.1)
|
||||
optionalDependencies:
|
||||
'@types/react': 18.2.47
|
||||
'@types/react-dom': 18.3.7(@types/react@18.2.47)
|
||||
'@types/react-dom': 18.3.7(@types/react@18.2.69)
|
||||
|
||||
'@radix-ui/react-popper@1.2.7(@types/react-dom@18.3.7(@types/react@18.2.69))(@types/react@18.2.69)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
|
||||
dependencies:
|
||||
@ -11107,7 +11203,7 @@ snapshots:
|
||||
react-dom: 18.3.1(react@18.3.1)
|
||||
optionalDependencies:
|
||||
'@types/react': 18.2.47
|
||||
'@types/react-dom': 18.3.7(@types/react@18.2.47)
|
||||
'@types/react-dom': 18.3.7(@types/react@18.2.69)
|
||||
|
||||
'@radix-ui/react-portal@1.1.9(@types/react-dom@18.3.7(@types/react@18.2.69))(@types/react@18.2.69)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
|
||||
dependencies:
|
||||
@ -11127,7 +11223,7 @@ snapshots:
|
||||
react-dom: 18.3.1(react@18.3.1)
|
||||
optionalDependencies:
|
||||
'@types/react': 18.2.47
|
||||
'@types/react-dom': 18.3.7(@types/react@18.2.47)
|
||||
'@types/react-dom': 18.3.7(@types/react@18.2.69)
|
||||
|
||||
'@radix-ui/react-presence@1.1.4(@types/react-dom@18.3.7(@types/react@18.2.69))(@types/react@18.2.69)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
|
||||
dependencies:
|
||||
@ -11146,7 +11242,7 @@ snapshots:
|
||||
react-dom: 18.3.1(react@18.3.1)
|
||||
optionalDependencies:
|
||||
'@types/react': 18.2.47
|
||||
'@types/react-dom': 18.3.7(@types/react@18.2.47)
|
||||
'@types/react-dom': 18.3.7(@types/react@18.2.69)
|
||||
|
||||
'@radix-ui/react-primitive@2.1.3(@types/react-dom@18.3.7(@types/react@18.2.69))(@types/react@18.2.69)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
|
||||
dependencies:
|
||||
@ -11172,7 +11268,7 @@ snapshots:
|
||||
react-dom: 18.3.1(react@18.3.1)
|
||||
optionalDependencies:
|
||||
'@types/react': 18.2.47
|
||||
'@types/react-dom': 18.3.7(@types/react@18.2.47)
|
||||
'@types/react-dom': 18.3.7(@types/react@18.2.69)
|
||||
|
||||
'@radix-ui/react-roving-focus@1.1.10(@types/react-dom@18.3.7(@types/react@18.2.69))(@types/react@18.2.69)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
|
||||
dependencies:
|
||||
@ -11332,7 +11428,7 @@ snapshots:
|
||||
react-dom: 18.3.1(react@18.3.1)
|
||||
optionalDependencies:
|
||||
'@types/react': 18.2.47
|
||||
'@types/react-dom': 18.3.7(@types/react@18.2.47)
|
||||
'@types/react-dom': 18.3.7(@types/react@18.2.69)
|
||||
|
||||
'@radix-ui/react-toggle@1.1.0(@types/react-dom@18.3.7(@types/react@18.2.47))(@types/react@18.2.47)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
|
||||
dependencies:
|
||||
@ -11343,7 +11439,7 @@ snapshots:
|
||||
react-dom: 18.3.1(react@18.3.1)
|
||||
optionalDependencies:
|
||||
'@types/react': 18.2.47
|
||||
'@types/react-dom': 18.3.7(@types/react@18.2.47)
|
||||
'@types/react-dom': 18.3.7(@types/react@18.2.69)
|
||||
|
||||
'@radix-ui/react-tooltip@1.1.1(@types/react-dom@18.3.7(@types/react@18.2.47))(@types/react@18.2.47)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
|
||||
dependencies:
|
||||
@ -11363,7 +11459,7 @@ snapshots:
|
||||
react-dom: 18.3.1(react@18.3.1)
|
||||
optionalDependencies:
|
||||
'@types/react': 18.2.47
|
||||
'@types/react-dom': 18.3.7(@types/react@18.2.47)
|
||||
'@types/react-dom': 18.3.7(@types/react@18.2.69)
|
||||
|
||||
'@radix-ui/react-tooltip@1.2.7(@types/react-dom@18.3.7(@types/react@18.2.69))(@types/react@18.2.69)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
|
||||
dependencies:
|
||||
@ -11493,7 +11589,7 @@ snapshots:
|
||||
react-dom: 18.3.1(react@18.3.1)
|
||||
optionalDependencies:
|
||||
'@types/react': 18.2.47
|
||||
'@types/react-dom': 18.3.7(@types/react@18.2.47)
|
||||
'@types/react-dom': 18.3.7(@types/react@18.2.69)
|
||||
|
||||
'@radix-ui/react-visually-hidden@1.2.3(@types/react-dom@18.3.7(@types/react@18.2.69))(@types/react@18.2.69)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
|
||||
dependencies:
|
||||
@ -11990,6 +12086,14 @@ snapshots:
|
||||
domhandler: 5.0.3
|
||||
selderee: 0.11.0
|
||||
|
||||
'@sideway/address@4.1.5':
|
||||
dependencies:
|
||||
'@hapi/hoek': 9.3.0
|
||||
|
||||
'@sideway/formula@3.0.1': {}
|
||||
|
||||
'@sideway/pinpoint@2.0.0': {}
|
||||
|
||||
'@sindresorhus/merge-streams@4.0.0': {}
|
||||
|
||||
'@smithy/abort-controller@4.0.4':
|
||||
@ -12748,10 +12852,6 @@ snapshots:
|
||||
|
||||
'@types/range-parser@1.2.7': {}
|
||||
|
||||
'@types/react-dom@18.3.7(@types/react@18.2.47)':
|
||||
dependencies:
|
||||
'@types/react': 18.2.47
|
||||
|
||||
'@types/react-dom@18.3.7(@types/react@18.2.69)':
|
||||
dependencies:
|
||||
'@types/react': 18.2.69
|
||||
@ -12785,8 +12885,12 @@ snapshots:
|
||||
|
||||
'@types/shimmer@1.2.0': {}
|
||||
|
||||
'@types/simple-oauth2@5.0.7': {}
|
||||
|
||||
'@types/unist@2.0.11': {}
|
||||
|
||||
'@types/validator@13.15.2': {}
|
||||
|
||||
'@types/webpack@5.28.5(@swc/core@1.3.101(@swc/helpers@0.5.2))(esbuild@0.19.11)':
|
||||
dependencies:
|
||||
'@types/node': 24.0.0
|
||||
@ -13633,6 +13737,14 @@ snapshots:
|
||||
|
||||
cjs-module-lexer@1.4.3: {}
|
||||
|
||||
class-transformer@0.5.1: {}
|
||||
|
||||
class-validator@0.14.1:
|
||||
dependencies:
|
||||
'@types/validator': 13.15.2
|
||||
libphonenumber-js: 1.12.9
|
||||
validator: 13.15.15
|
||||
|
||||
class-variance-authority@0.7.1:
|
||||
dependencies:
|
||||
clsx: 2.1.1
|
||||
@ -15671,6 +15783,14 @@ snapshots:
|
||||
|
||||
jiti@2.4.2: {}
|
||||
|
||||
joi@17.13.3:
|
||||
dependencies:
|
||||
'@hapi/hoek': 9.3.0
|
||||
'@hapi/topo': 5.1.0
|
||||
'@sideway/address': 4.1.5
|
||||
'@sideway/formula': 3.0.1
|
||||
'@sideway/pinpoint': 2.0.0
|
||||
|
||||
jose@5.10.0: {}
|
||||
|
||||
js-beautify@1.15.4:
|
||||
@ -15762,6 +15882,8 @@ snapshots:
|
||||
prelude-ls: 1.2.1
|
||||
type-check: 0.4.0
|
||||
|
||||
libphonenumber-js@1.12.9: {}
|
||||
|
||||
lightningcss-darwin-arm64@1.30.1:
|
||||
optional: true
|
||||
|
||||
@ -17164,7 +17286,7 @@ snapshots:
|
||||
'@radix-ui/react-tooltip': 1.1.1(@types/react-dom@18.3.7(@types/react@18.2.47))(@types/react@18.2.47)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
'@swc/core': 1.3.101(@swc/helpers@0.5.2)
|
||||
'@types/react': 18.2.47
|
||||
'@types/react-dom': 18.3.7(@types/react@18.2.47)
|
||||
'@types/react-dom': 18.3.7(@types/react@18.2.69)
|
||||
'@types/webpack': 5.28.5(@swc/core@1.3.101(@swc/helpers@0.5.2))(esbuild@0.19.11)
|
||||
autoprefixer: 10.4.14(postcss@8.4.38)
|
||||
chalk: 4.1.2
|
||||
@ -17735,6 +17857,15 @@ snapshots:
|
||||
|
||||
signal-exit@4.1.0: {}
|
||||
|
||||
simple-oauth2@5.1.0:
|
||||
dependencies:
|
||||
'@hapi/hoek': 11.0.7
|
||||
'@hapi/wreck': 18.1.0
|
||||
debug: 4.4.1
|
||||
joi: 17.13.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
simple-swizzle@0.2.2:
|
||||
dependencies:
|
||||
is-arrayish: 0.3.2
|
||||
@ -18553,6 +18684,8 @@ snapshots:
|
||||
|
||||
validate-npm-package-name@5.0.1: {}
|
||||
|
||||
validator@13.15.15: {}
|
||||
|
||||
vary@1.1.2: {}
|
||||
|
||||
vfile-message@3.1.4:
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
packages:
|
||||
- "apps/*"
|
||||
- "packages/*"
|
||||
- "core/*"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user