feat(skills): GitHub PAT support for private skill repos + delete by source
- Add optional authToken to skill import for GitHub private repos
- Store PAT as encrypted company secret (skill-pat:{skillId})
- Thread auth token through ghFetch, fetchText, fetchJson, and all GitHub resolution functions
- Add PATCH /companies/:companyId/skills/:skillId/auth for managing PAT per skill
- Add DELETE /companies/:companyId/skills/by-source for bulk deleting skills from a repo
- Preserve sourceAuthSecretId across skill re-imports/updates
- UI: Add PAT input field in import form for GitHub URLs
- UI: Add SkillAuthSection with ShieldCheck icon for viewing/updating/removing PAT
- UI: Add trash icon next to source label for delete-by-source
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,7 @@ import {
|
||||
companySkillCreateSchema,
|
||||
companySkillFileUpdateSchema,
|
||||
companySkillImportSchema,
|
||||
companySkillUpdateAuthSchema,
|
||||
companySkillProjectScanRequestSchema,
|
||||
} from "@paperclipai/shared";
|
||||
import { trackSkillImported } from "@paperclipai/shared/telemetry";
|
||||
@@ -194,7 +195,8 @@ export function companySkillRoutes(db: Db) {
|
||||
const companyId = req.params.companyId as string;
|
||||
await assertCanMutateCompanySkills(req, companyId);
|
||||
const source = String(req.body.source ?? "");
|
||||
const result = await svc.importFromSource(companyId, source);
|
||||
const authToken = typeof req.body.authToken === "string" ? req.body.authToken.trim() : undefined;
|
||||
const result = await svc.importFromSource(companyId, source, authToken || undefined);
|
||||
|
||||
const actor = getActorInfo(req);
|
||||
await logActivity(db, {
|
||||
@@ -260,6 +262,36 @@ export function companySkillRoutes(db: Db) {
|
||||
},
|
||||
);
|
||||
|
||||
router.delete("/companies/:companyId/skills/by-source", async (req, res) => {
|
||||
const companyId = req.params.companyId as string;
|
||||
const sourceLocator = String(req.query.source ?? "").trim();
|
||||
if (!sourceLocator) {
|
||||
res.status(400).json({ error: "source query parameter is required" });
|
||||
return;
|
||||
}
|
||||
await assertCanMutateCompanySkills(req, companyId);
|
||||
const deleted = await svc.deleteBySource(companyId, sourceLocator);
|
||||
|
||||
const actor = getActorInfo(req);
|
||||
await logActivity(db, {
|
||||
companyId,
|
||||
actorType: actor.actorType,
|
||||
actorId: actor.actorId,
|
||||
agentId: actor.agentId,
|
||||
runId: actor.runId,
|
||||
action: "company.skills_source_deleted",
|
||||
entityType: "company",
|
||||
entityId: companyId,
|
||||
details: {
|
||||
sourceLocator,
|
||||
deletedCount: deleted.length,
|
||||
deletedSlugs: deleted.map((s) => s.slug),
|
||||
},
|
||||
});
|
||||
|
||||
res.json({ deleted });
|
||||
});
|
||||
|
||||
router.delete("/companies/:companyId/skills/:skillId", async (req, res) => {
|
||||
const companyId = req.params.companyId as string;
|
||||
const skillId = req.params.skillId as string;
|
||||
@@ -318,5 +350,38 @@ export function companySkillRoutes(db: Db) {
|
||||
res.json(result);
|
||||
});
|
||||
|
||||
router.patch(
|
||||
"/companies/:companyId/skills/:skillId/auth",
|
||||
validate(companySkillUpdateAuthSchema),
|
||||
async (req, res) => {
|
||||
const companyId = req.params.companyId as string;
|
||||
const skillId = req.params.skillId as string;
|
||||
await assertCanMutateCompanySkills(req, companyId);
|
||||
const authToken = req.body.authToken as string | null;
|
||||
const result = await svc.updateSkillAuth(companyId, skillId, authToken);
|
||||
if (!result) {
|
||||
res.status(404).json({ error: "Skill not found" });
|
||||
return;
|
||||
}
|
||||
|
||||
const actor = getActorInfo(req);
|
||||
await logActivity(db, {
|
||||
companyId,
|
||||
actorType: actor.actorType,
|
||||
actorId: actor.actorId,
|
||||
agentId: actor.agentId,
|
||||
runId: actor.runId,
|
||||
action: authToken ? "company.skill_auth_updated" : "company.skill_auth_removed",
|
||||
entityType: "company_skill",
|
||||
entityId: result.id,
|
||||
details: {
|
||||
slug: result.slug,
|
||||
},
|
||||
});
|
||||
|
||||
res.json(result);
|
||||
},
|
||||
);
|
||||
|
||||
return router;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user