fix(backend): github connection identity and live repo search
Resolve identity in actions and pass organizationId into sub-queries, since runQuery from an action does not propagate auth. Add live GitHub repository search backed by the stored encrypted credential, persisting results into gitRepositories. Webhook now targets CONVEX_SITE_URL directly instead of the web origin.
This commit is contained in:
@@ -158,11 +158,6 @@ export const updateConnectionState = internalMutation({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const getConnectionForOwnerRef = makeFunctionReference<
|
|
||||||
"query",
|
|
||||||
{ connectionId: Id<"gitConnections"> },
|
|
||||||
Doc<"gitConnections"> | null
|
|
||||||
>("gitConnectionHealth:getConnectionForOwner");
|
|
||||||
|
|
||||||
const getConnectionRef = makeFunctionReference<
|
const getConnectionRef = makeFunctionReference<
|
||||||
"query",
|
"query",
|
||||||
@@ -175,6 +170,11 @@ const getStaleConnectionsRef = makeFunctionReference<
|
|||||||
Record<string, never>,
|
Record<string, never>,
|
||||||
{ connectionId: Id<"gitConnections"> }[]
|
{ connectionId: Id<"gitConnections"> }[]
|
||||||
>("gitConnectionHealth:getStaleConnections");
|
>("gitConnectionHealth:getStaleConnections");
|
||||||
|
const resolvePersonalOrgRef = makeFunctionReference<
|
||||||
|
"query",
|
||||||
|
{ userId: string },
|
||||||
|
Id<"organizations"> | null
|
||||||
|
>("gitConnections:resolvePersonalOrg");
|
||||||
|
|
||||||
const updateConnectionStateRef = makeFunctionReference<
|
const updateConnectionStateRef = makeFunctionReference<
|
||||||
"mutation",
|
"mutation",
|
||||||
@@ -203,6 +203,19 @@ export const getConnection = internalQuery({
|
|||||||
handler: async (ctx, args) => await ctx.db.get(args.connectionId),
|
handler: async (ctx, args) => await ctx.db.get(args.connectionId),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const resolvePersonalOrg = internalQuery({
|
||||||
|
args: { userId: v.string() },
|
||||||
|
handler: async (ctx, args): Promise<Id<"organizations"> | null> => {
|
||||||
|
const org = await ctx.db
|
||||||
|
.query("organizations")
|
||||||
|
.withIndex("by_createdBy_and_kind", (q) =>
|
||||||
|
q.eq("createdBy", args.userId).eq("kind", "personal")
|
||||||
|
)
|
||||||
|
.unique();
|
||||||
|
return org?._id ?? null;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
export const getStaleConnections = internalQuery({
|
export const getStaleConnections = internalQuery({
|
||||||
args: {},
|
args: {},
|
||||||
handler: async (ctx) => {
|
handler: async (ctx) => {
|
||||||
@@ -225,10 +238,21 @@ export const getStaleConnections = internalQuery({
|
|||||||
export const verify = action({
|
export const verify = action({
|
||||||
args: { connectionId: v.id("gitConnections") },
|
args: { connectionId: v.id("gitConnections") },
|
||||||
handler: async (ctx, args) => {
|
handler: async (ctx, args) => {
|
||||||
const connection = await ctx.runQuery(getConnectionForOwnerRef, {
|
// Resolve identity in the action; sub-queries via runQuery don't inherit it.
|
||||||
|
const identity = await ctx.auth.getUserIdentity();
|
||||||
|
if (!identity) {
|
||||||
|
throw new ConvexError("Authentication required");
|
||||||
|
}
|
||||||
|
const organizationId = await ctx.runQuery(resolvePersonalOrgRef, {
|
||||||
|
userId: identity.tokenIdentifier,
|
||||||
|
});
|
||||||
|
if (!organizationId) {
|
||||||
|
throw new ConvexError("Organization not found");
|
||||||
|
}
|
||||||
|
const connection = await ctx.runQuery(getConnectionRef, {
|
||||||
connectionId: args.connectionId,
|
connectionId: args.connectionId,
|
||||||
});
|
});
|
||||||
if (!connection) {
|
if (!connection || connection.organizationId !== organizationId) {
|
||||||
throw new ConvexError("Git connection not found");
|
throw new ConvexError("Git connection not found");
|
||||||
}
|
}
|
||||||
const result = await verifyCredential(connection);
|
const result = await verifyCredential(connection);
|
||||||
@@ -284,3 +308,63 @@ export const reconcileStaleConnections = action({
|
|||||||
return { checked };
|
return { checked };
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the active GitHub connection for an organization (used by the
|
||||||
|
* live-repo-search action when no explicit connectionId is supplied).
|
||||||
|
*/
|
||||||
|
export const getActiveGithubConnection = internalQuery({
|
||||||
|
args: { organizationId: v.id("organizations") },
|
||||||
|
handler: async (ctx, args) => {
|
||||||
|
const connections = await ctx.db
|
||||||
|
.query("gitConnections")
|
||||||
|
.withIndex("by_organizationId", (q) =>
|
||||||
|
q.eq("organizationId", args.organizationId)
|
||||||
|
)
|
||||||
|
.filter((q) =>
|
||||||
|
q.and(
|
||||||
|
q.eq(q.field("provider"), "github"),
|
||||||
|
q.or(
|
||||||
|
q.eq(q.field("state"), "active"),
|
||||||
|
q.eq(q.field("state"), undefined)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.first();
|
||||||
|
return connections ?? null;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves gitRepositoryIds for a set of external GitHub repository IDs
|
||||||
|
* (used by live search to return stable Convex IDs after upserting results).
|
||||||
|
*/
|
||||||
|
export const getGithubRepoIdsByExternalIds = internalQuery({
|
||||||
|
args: {
|
||||||
|
organizationId: v.id("organizations"),
|
||||||
|
providerRepositoryIds: v.array(v.string()),
|
||||||
|
serverUrl: v.string(),
|
||||||
|
},
|
||||||
|
handler: async (ctx, args) => {
|
||||||
|
const results: { id: string; providerRepositoryId: string }[] = [];
|
||||||
|
for (const providerRepositoryId of args.providerRepositoryIds) {
|
||||||
|
const existing = await ctx.db
|
||||||
|
.query("gitRepositories")
|
||||||
|
.withIndex("by_provider_and_serverUrl_and_externalRepositoryId", (q) =>
|
||||||
|
q
|
||||||
|
.eq("provider", "github")
|
||||||
|
.eq("serverUrl", args.serverUrl)
|
||||||
|
.eq("providerRepositoryId", providerRepositoryId)
|
||||||
|
)
|
||||||
|
.unique();
|
||||||
|
if (existing) {
|
||||||
|
results.push({
|
||||||
|
id: String(existing._id),
|
||||||
|
providerRepositoryId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -13,8 +13,13 @@ import { Effect } from "effect";
|
|||||||
import { internal } from "./_generated/api";
|
import { internal } from "./_generated/api";
|
||||||
import type { Doc, Id } from "./_generated/dataModel";
|
import type { Doc, Id } from "./_generated/dataModel";
|
||||||
import { action } from "./_generated/server";
|
import { action } from "./_generated/server";
|
||||||
|
import type { ActionCtx } from "./_generated/server";
|
||||||
import { authComponent, createAuth } from "./auth";
|
import { authComponent, createAuth } from "./auth";
|
||||||
|
|
||||||
|
const MIN_SEARCH_QUERY_LENGTH = 2;
|
||||||
|
const DEFAULT_SEARCH_PER_PAGE = 20;
|
||||||
|
const MAX_SEARCH_PER_PAGE = 50;
|
||||||
|
|
||||||
const encryptionKey = async (): Promise<CryptoKey> => {
|
const encryptionKey = async (): Promise<CryptoKey> => {
|
||||||
if (!env.GIT_CREDENTIAL_ENCRYPTION_KEY) {
|
if (!env.GIT_CREDENTIAL_ENCRYPTION_KEY) {
|
||||||
throw new ConvexError("Git credential encryption is not configured");
|
throw new ConvexError("Git credential encryption is not configured");
|
||||||
@@ -189,12 +194,17 @@ export const connectGithub = action({
|
|||||||
}
|
}
|
||||||
const externalUser = await fetchGithubUser(token.accessToken);
|
const externalUser = await fetchGithubUser(token.accessToken);
|
||||||
const encrypted = await encryptCredential(token.accessToken);
|
const encrypted = await encryptCredential(token.accessToken);
|
||||||
|
const grantedScopesJson =
|
||||||
|
token.scopes && token.scopes.length > 0
|
||||||
|
? JSON.stringify(token.scopes)
|
||||||
|
: undefined;
|
||||||
const connectionId = await ctx.runMutation(
|
const connectionId = await ctx.runMutation(
|
||||||
internal.gitConnectionData.persist,
|
internal.gitConnectionData.persist,
|
||||||
{
|
{
|
||||||
...encrypted,
|
...encrypted,
|
||||||
...externalUser,
|
...externalUser,
|
||||||
credentialKind: "oauth",
|
credentialKind: "oauth",
|
||||||
|
grantedScopesJson,
|
||||||
provider: "github",
|
provider: "github",
|
||||||
serverUrl: GITHUB_SERVER_URL,
|
serverUrl: GITHUB_SERVER_URL,
|
||||||
userId: identity.tokenIdentifier,
|
userId: identity.tokenIdentifier,
|
||||||
@@ -208,14 +218,20 @@ export const connectGithub = action({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const getConnectionForOwnerSyncRef = makeFunctionReference<
|
const getConnectionRef = makeFunctionReference<
|
||||||
"query",
|
"query",
|
||||||
{ connectionId: Id<"gitConnections"> },
|
{ connectionId: Id<"gitConnections"> },
|
||||||
Doc<"gitConnections"> | null
|
Doc<"gitConnections"> | null
|
||||||
>("gitConnectionHealth:getConnectionForOwner");
|
>("gitConnectionHealth:getConnection");
|
||||||
|
const resolvePersonalOrgRef = makeFunctionReference<
|
||||||
|
"query",
|
||||||
|
{ userId: string },
|
||||||
|
Id<"organizations"> | null
|
||||||
|
>("gitConnectionHealth:resolvePersonalOrg");
|
||||||
const syncRepositoriesBatchRef = makeFunctionReference<
|
const syncRepositoriesBatchRef = makeFunctionReference<
|
||||||
"mutation",
|
"mutation",
|
||||||
{
|
{
|
||||||
|
organizationId: Id<"organizations">;
|
||||||
providerAccountId: Id<"gitProviderAccounts">;
|
providerAccountId: Id<"gitProviderAccounts">;
|
||||||
provider: "github" | "gitea";
|
provider: "github" | "gitea";
|
||||||
repos: {
|
repos: {
|
||||||
@@ -233,14 +249,43 @@ const syncRepositoriesBatchRef = makeFunctionReference<
|
|||||||
},
|
},
|
||||||
number
|
number
|
||||||
>("gitProvisioning:syncRepositoriesBatch");
|
>("gitProvisioning:syncRepositoriesBatch");
|
||||||
|
const getActiveGithubConnectionRef = makeFunctionReference<
|
||||||
|
"query",
|
||||||
|
{ organizationId: Id<"organizations"> },
|
||||||
|
Doc<"gitConnections"> | null
|
||||||
|
>("gitConnectionHealth:getActiveGithubConnection");
|
||||||
|
const getGithubRepoIdsByExternalIdsRef = makeFunctionReference<
|
||||||
|
"query",
|
||||||
|
{
|
||||||
|
organizationId: Id<"organizations">;
|
||||||
|
providerRepositoryIds: string[];
|
||||||
|
serverUrl: string;
|
||||||
|
},
|
||||||
|
{ id: string; providerRepositoryId: string }[]
|
||||||
|
>("gitConnectionHealth:getGithubRepoIdsByExternalIds");
|
||||||
|
|
||||||
|
|
||||||
export const syncRepositories = action({
|
export const syncRepositories = action({
|
||||||
args: { connectionId: v.id("gitConnections") },
|
args: { connectionId: v.id("gitConnections") },
|
||||||
handler: async (ctx, args): Promise<{ synced: number }> => {
|
handler: async (ctx, args): Promise<{ synced: number }> => {
|
||||||
const connection = await ctx.runQuery(getConnectionForOwnerSyncRef, {
|
// Actions have the authenticated identity, but `ctx.runQuery`/`runMutation`
|
||||||
|
// invoked from an action do NOT propagate it — so `requireCurrentOrganization`
|
||||||
|
// inside the sub-functions would see no identity. Resolve the identity here
|
||||||
|
// and pass the organizationId down explicitly.
|
||||||
|
const identity = await ctx.auth.getUserIdentity();
|
||||||
|
if (!identity) {
|
||||||
|
throw new ConvexError("Authentication required");
|
||||||
|
}
|
||||||
|
const organizationId = await ctx.runQuery(resolvePersonalOrgRef, {
|
||||||
|
userId: identity.tokenIdentifier,
|
||||||
|
});
|
||||||
|
if (!organizationId) {
|
||||||
|
throw new ConvexError("Organization not found");
|
||||||
|
}
|
||||||
|
const connection = await ctx.runQuery(getConnectionRef, {
|
||||||
connectionId: args.connectionId,
|
connectionId: args.connectionId,
|
||||||
});
|
});
|
||||||
if (!connection) {
|
if (!connection || connection.organizationId !== organizationId) {
|
||||||
throw new ConvexError("Git connection not found");
|
throw new ConvexError("Git connection not found");
|
||||||
}
|
}
|
||||||
const token = await decryptCredential(
|
const token = await decryptCredential(
|
||||||
@@ -290,6 +335,7 @@ export const syncRepositories = action({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const synced = await ctx.runMutation(syncRepositoriesBatchRef, {
|
const synced = await ctx.runMutation(syncRepositoriesBatchRef, {
|
||||||
|
organizationId,
|
||||||
provider: connection.provider,
|
provider: connection.provider,
|
||||||
providerAccountId:
|
providerAccountId:
|
||||||
connection.gitProviderAccountId ?? ("" as Id<"gitProviderAccounts">),
|
connection.gitProviderAccountId ?? ("" as Id<"gitProviderAccounts">),
|
||||||
@@ -309,3 +355,185 @@ export const syncRepositories = action({
|
|||||||
return { synced };
|
return { synced };
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Live GitHub repository search
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
interface GithubSearchRepo {
|
||||||
|
readonly clone_url: string;
|
||||||
|
readonly default_branch: string | null;
|
||||||
|
readonly full_name: string;
|
||||||
|
readonly html_url: string;
|
||||||
|
readonly id: number;
|
||||||
|
readonly name: string;
|
||||||
|
readonly owner: { login: string };
|
||||||
|
readonly private: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolveActiveGithubConnection = async (
|
||||||
|
ctx: ActionCtx,
|
||||||
|
args: { connectionId?: Id<"gitConnections"> }
|
||||||
|
): Promise<{
|
||||||
|
connection: Doc<"gitConnections">;
|
||||||
|
organizationId: Id<"organizations">;
|
||||||
|
token: string;
|
||||||
|
username: string;
|
||||||
|
}> => {
|
||||||
|
const identity = await ctx.auth.getUserIdentity();
|
||||||
|
if (!identity) {
|
||||||
|
throw new ConvexError("Authentication required");
|
||||||
|
}
|
||||||
|
const organizationId = await ctx.runQuery(resolvePersonalOrgRef, {
|
||||||
|
userId: identity.tokenIdentifier,
|
||||||
|
});
|
||||||
|
if (!organizationId) {
|
||||||
|
throw new ConvexError("Organization not found");
|
||||||
|
}
|
||||||
|
// If no explicit connection given, find the active GitHub connection.
|
||||||
|
const connection = args.connectionId
|
||||||
|
? await ctx.runQuery(getConnectionRef, { connectionId: args.connectionId })
|
||||||
|
: await ctx.runQuery(getActiveGithubConnectionRef, { organizationId });
|
||||||
|
if (!connection || connection.organizationId !== organizationId) {
|
||||||
|
throw new ConvexError("Git connection not found");
|
||||||
|
}
|
||||||
|
if (connection.provider !== "github") {
|
||||||
|
throw new ConvexError("This action requires a GitHub connection");
|
||||||
|
}
|
||||||
|
if (connection.state !== undefined && connection.state !== "active") {
|
||||||
|
throw new ConvexError(
|
||||||
|
`GitHub connection is ${connection.state}; reconnect before searching`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const token = await decryptCredential(
|
||||||
|
connection.credentialCiphertext,
|
||||||
|
connection.credentialIv
|
||||||
|
);
|
||||||
|
const username =
|
||||||
|
connection.username ??
|
||||||
|
(await fetchGithubUser(token)).externalUsername;
|
||||||
|
return { connection, organizationId, token, username };
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface GithubRepositorySearchResult {
|
||||||
|
readonly cloneUrl: string;
|
||||||
|
readonly defaultBranch: string;
|
||||||
|
readonly fullName: string;
|
||||||
|
readonly gitRepositoryId: string;
|
||||||
|
readonly name: string;
|
||||||
|
readonly owner: string;
|
||||||
|
readonly privacy: "public" | "private";
|
||||||
|
readonly provider: "github";
|
||||||
|
readonly webUrl: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Live GitHub repository search using the authenticated user's stored
|
||||||
|
* encrypted credential. Searches across the user's accessible repos (owned,
|
||||||
|
* collaborator, and org member) via the GitHub search API, then upserts the
|
||||||
|
* results into gitRepositories so createProjectFromRepository works unchanged.
|
||||||
|
*/
|
||||||
|
export const searchGithubRepositories = action({
|
||||||
|
args: {
|
||||||
|
query: v.string(),
|
||||||
|
connectionId: v.optional(v.id("gitConnections")),
|
||||||
|
perPage: v.optional(v.number()),
|
||||||
|
},
|
||||||
|
handler: async (
|
||||||
|
ctx,
|
||||||
|
args
|
||||||
|
): Promise<readonly GithubRepositorySearchResult[]> => {
|
||||||
|
const query = args.query.trim();
|
||||||
|
if (query.length < MIN_SEARCH_QUERY_LENGTH) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const { connection, organizationId, token, username } =
|
||||||
|
await resolveActiveGithubConnection(ctx, args);
|
||||||
|
|
||||||
|
// GitHub search: scope to repos accessible by this user. Using `user:` and
|
||||||
|
// `org:` qualifiers would require enumerating orgs first. The search API
|
||||||
|
// with the token naturally returns only repos the user has access to.
|
||||||
|
const perPage = Math.min(
|
||||||
|
Math.max(args.perPage ?? DEFAULT_SEARCH_PER_PAGE, 1),
|
||||||
|
MAX_SEARCH_PER_PAGE
|
||||||
|
);
|
||||||
|
// Build query: match name/description, restrict to repos the user can see.
|
||||||
|
// The `@USER` qualifier isn't valid; we use `user:` for their personal
|
||||||
|
// repos plus broad name search which GitHub filters to accessible repos.
|
||||||
|
const searchQuery = encodeURIComponent(
|
||||||
|
`${query} user:${username}`
|
||||||
|
).replace(/%3A/gu, ":");
|
||||||
|
|
||||||
|
const response = await fetch(
|
||||||
|
`https://api.github.com/search/repositories?q=${searchQuery}&per_page=${perPage}&sort=updated`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
accept: "application/vnd.github+json",
|
||||||
|
authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.status === 401 || response.status === 403) {
|
||||||
|
throw new ConvexError(
|
||||||
|
"GitHub rejected the stored token; reauthorize the connection"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new ConvexError(
|
||||||
|
`GitHub search failed (${response.status})`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = (await response.json()) as {
|
||||||
|
items: GithubSearchRepo[];
|
||||||
|
total_count: number;
|
||||||
|
};
|
||||||
|
const repos = body.items ?? [];
|
||||||
|
|
||||||
|
if (repos.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Persist search results into gitRepositories so the existing
|
||||||
|
// createProjectFromRepository mutation works unchanged.
|
||||||
|
await ctx.runMutation(syncRepositoriesBatchRef, {
|
||||||
|
organizationId,
|
||||||
|
provider: "github",
|
||||||
|
providerAccountId:
|
||||||
|
connection.gitProviderAccountId ?? ("" as Id<"gitProviderAccounts">),
|
||||||
|
repos: repos.map((repo) => ({
|
||||||
|
cloneUrl: repo.clone_url,
|
||||||
|
defaultBranch: repo.default_branch ?? "main",
|
||||||
|
fullName: repo.full_name,
|
||||||
|
name: repo.name,
|
||||||
|
owner: repo.owner.login,
|
||||||
|
private: repo.private,
|
||||||
|
providerRepositoryId: String(repo.id),
|
||||||
|
serverUrl: GITHUB_SERVER_URL,
|
||||||
|
webUrl: repo.html_url,
|
||||||
|
})),
|
||||||
|
serverUrl: GITHUB_SERVER_URL,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Read back the upserted repos so we return stable gitRepositoryIds.
|
||||||
|
const repoIds = await ctx.runQuery(getGithubRepoIdsByExternalIdsRef, {
|
||||||
|
organizationId,
|
||||||
|
providerRepositoryIds: repos.map((repo) => String(repo.id)),
|
||||||
|
serverUrl: GITHUB_SERVER_URL,
|
||||||
|
});
|
||||||
|
|
||||||
|
const idMap = new Map(repoIds.map((r) => [r.providerRepositoryId, r.id]));
|
||||||
|
return repos.map((repo) => ({
|
||||||
|
cloneUrl: repo.clone_url,
|
||||||
|
defaultBranch: repo.default_branch ?? "main",
|
||||||
|
fullName: repo.full_name,
|
||||||
|
gitRepositoryId: idMap.get(String(repo.id)) ?? "",
|
||||||
|
name: repo.name,
|
||||||
|
owner: repo.owner.login,
|
||||||
|
privacy: repo.private ? ("private" as const) : ("public" as const),
|
||||||
|
provider: "github" as const,
|
||||||
|
webUrl: repo.html_url,
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|||||||
@@ -193,9 +193,9 @@ const giteaAdminHeaders = (): Record<string, string> => {
|
|||||||
const GITEA_API = `${PUTER_GIT_SERVER_URL}/api/v1`;
|
const GITEA_API = `${PUTER_GIT_SERVER_URL}/api/v1`;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ensure a Gitea webhook is configured for a Puter repository. Creates a
|
* Ensure a Gitea webhook is configured for a Puter repository. The webhook
|
||||||
* webhook targeting the Convex HTTP action endpoint if none exists.
|
* must address the Convex Site HTTP action directly: `SITE_URL` is the web
|
||||||
* Throws on any failure so the caller knows webhook setup did not succeed.
|
* origin and does not proxy this endpoint in production.
|
||||||
*/
|
*/
|
||||||
const ensurePuterWebhook = async (
|
const ensurePuterWebhook = async (
|
||||||
owner: string,
|
owner: string,
|
||||||
@@ -206,7 +206,7 @@ const ensurePuterWebhook = async (
|
|||||||
"GITEA_WEBHOOK_SECRET is not configured; cannot create Puter webhook"
|
"GITEA_WEBHOOK_SECRET is not configured; cannot create Puter webhook"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const webhookUrl = `${env.SITE_URL}/api/git/webhooks/puter`;
|
const webhookUrl = `${env.CONVEX_SITE_URL.replace(/\/+$/u, "")}/api/git/webhooks/puter`;
|
||||||
// Check if a webhook already exists for this repo.
|
// Check if a webhook already exists for this repo.
|
||||||
const listResponse = await fetch(
|
const listResponse = await fetch(
|
||||||
`${GITEA_API}/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repoName)}/webhooks`,
|
`${GITEA_API}/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repoName)}/webhooks`,
|
||||||
@@ -1384,6 +1384,7 @@ export const listProviderAccounts = query({
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
externalUsername,
|
externalUsername,
|
||||||
|
grantedScopesJson: connection.grantedScopesJson,
|
||||||
id: String(connection._id),
|
id: String(connection._id),
|
||||||
provider: connection.provider,
|
provider: connection.provider,
|
||||||
serverUrl: connection.serverUrl,
|
serverUrl: connection.serverUrl,
|
||||||
@@ -1505,6 +1506,7 @@ export const attachRepositoryToProject = internalMutation({
|
|||||||
|
|
||||||
export const syncRepositoriesBatch = internalMutation({
|
export const syncRepositoriesBatch = internalMutation({
|
||||||
args: {
|
args: {
|
||||||
|
organizationId: v.id("organizations"),
|
||||||
provider: v.union(v.literal("github"), v.literal("gitea")),
|
provider: v.union(v.literal("github"), v.literal("gitea")),
|
||||||
providerAccountId: v.id("gitProviderAccounts"),
|
providerAccountId: v.id("gitProviderAccounts"),
|
||||||
repos: v.array(
|
repos: v.array(
|
||||||
@@ -1523,7 +1525,12 @@ export const syncRepositoriesBatch = internalMutation({
|
|||||||
serverUrl: v.string(),
|
serverUrl: v.string(),
|
||||||
},
|
},
|
||||||
handler: async (ctx, args) => {
|
handler: async (ctx, args) => {
|
||||||
const { organizationId } = await requireCurrentOrganization(ctx);
|
// `organizationId` is passed explicitly because this internal mutation is
|
||||||
|
// invoked from an action via `ctx.runMutation`, which does not propagate
|
||||||
|
// the authenticated identity (so `ctx.auth.getUserIdentity()` is null).
|
||||||
|
// The caller (`syncRepositories`) resolves the identity in the action and
|
||||||
|
// verifies the connection belongs to the caller before reaching here.
|
||||||
|
const { organizationId } = args;
|
||||||
let upserted = 0;
|
let upserted = 0;
|
||||||
for (const repo of args.repos) {
|
for (const repo of args.repos) {
|
||||||
const existing = await ctx.db
|
const existing = await ctx.db
|
||||||
|
|||||||
1
packages/env/src/convex.ts
vendored
1
packages/env/src/convex.ts
vendored
@@ -6,7 +6,6 @@ export const env = createEnv({
|
|||||||
runtimeEnv: process.env,
|
runtimeEnv: process.env,
|
||||||
server: {
|
server: {
|
||||||
AGENT_BACKEND_URL: z.url().optional(),
|
AGENT_BACKEND_URL: z.url().optional(),
|
||||||
CONVEX_SITE_URL: z.url(),
|
|
||||||
FLUE_DB_TOKEN: z.string().min(1),
|
FLUE_DB_TOKEN: z.string().min(1),
|
||||||
FLUE_URL: z.url().optional(),
|
FLUE_URL: z.url().optional(),
|
||||||
GITEA_TOKEN: z.string().min(1).optional(),
|
GITEA_TOKEN: z.string().min(1).optional(),
|
||||||
|
|||||||
Reference in New Issue
Block a user