First cut: integrations

This commit is contained in:
Harshith Mullapudi 2025-07-08 09:36:25 +05:30
parent 293927fa06
commit 12453954b7
44 changed files with 7096 additions and 239 deletions

View File

@ -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>
);

View 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 };

View 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 };

View File

@ -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 };

View File

@ -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);
};

View File

@ -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) {

View File

@ -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";

View 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}`);
}

View 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 },
});
}

View File

@ -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,

View 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;
}

View 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);
}
}

View File

@ -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)}`;

View File

@ -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({}));

View File

@ -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",

View File

@ -25,6 +25,7 @@
"skipLibCheck": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"strictPropertyInitialization": false,
"baseUrl": ".",
"paths": {
"~/*": ["./app/*"],

21
core/types/package.json Normal file
View 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
View File

@ -0,0 +1 @@
export * from './integration';

View 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
View 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
View File

@ -0,0 +1,2 @@
bin
node_modules

View 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"
}
}
]
}

View 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',
},
},
];

View 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

File diff suppressed because it is too large Load Diff

View 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": ","
}
}
}

View 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;
}

View 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;
}
}

View 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

View 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, ''));
}

View 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();

View 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;
}),
);
}

View 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"]
}

View 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',
},
});

View File

@ -0,0 +1,5 @@
-- AlterTable
ALTER TABLE "User" ADD COLUMN "preferences" JSONB;
-- AlterTable
ALTER TABLE "Workspace" ADD COLUMN "preferences" JSONB;

View File

@ -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[]
}

View File

@ -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",

View File

@ -1,3 +1,5 @@
export * from "./llm";
export * from "./graph";
export * from "./conversation-execution-step";
export * from "./oauth";
export * from "./integration";

View 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;
}

View File

@ -0,0 +1 @@
export * from "./params";

View 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;
}

View File

@ -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
View File

@ -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:

View File

@ -1,3 +1,4 @@
packages:
- "apps/*"
- "packages/*"
- "core/*"