From 0bfdb2285e72652566560232aa84a6f6208a55e9 Mon Sep 17 00:00:00 2001 From: gitadmin Date: Sat, 2 May 2026 14:13:14 +0000 Subject: [PATCH] feat: co-steward invite/list/remove endpoints, co_steward_emails schema --- routes/steward.ts | 77 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) diff --git a/routes/steward.ts b/routes/steward.ts index c1cc666..09e0ef2 100644 --- a/routes/steward.ts +++ b/routes/steward.ts @@ -61,6 +61,9 @@ export async function initStewardDash() { `); // Also add to registry for forward-compat (new stewards via web form) await pool.query(`ALTER TABLE agentify_subject_registry ADD COLUMN IF NOT EXISTS dashboard_token TEXT`).catch(() => {}); + // Co-steward collaboration: array of invited co-steward emails + await pool.query(`ALTER TABLE agentify_subject_registry ADD COLUMN IF NOT EXISTS co_steward_emails TEXT[] DEFAULT '{}'`).catch(() => {}); + await pool.query(`ALTER TABLE personaforge_subjects ADD COLUMN IF NOT EXISTS co_steward_emails TEXT[] DEFAULT '{}'`).catch(() => {}); console.log("[StewardDash] Schema columns ready"); } @@ -1248,6 +1251,80 @@ Then give your verdict on a new line: "Verdict: Approve", "Verdict: Flag", or "V const dashUrl = `https://agentify.help/steward/${slug}?token=${token}`; res.json({ slug, dashboard_token: token, dashboard_url: dashUrl }); }); + + // ── POST /api/steward/:slug/invite-co-steward ──────────────────────────── + // Invite a collaborator email to co-steward this agent. + // They gain access via the /my-agents portfolio magic-link flow. + app.post("/api/steward/:slug/invite-co-steward", express.json({ limit: "1mb" }), async (req: Request, res: Response) => { + if (!isAgentify(req)) return res.status(404).json({ error: "Not found" }); + const { slug } = req.params; + const token = req.query.token as string | undefined; + const key = (req.headers["x-admin-key"] as string) || (req.query.admin_key as string); + if (key !== ADMIN_KEY && !(await validateStewardToken(slug, token))) { + return res.status(403).json({ error: "Steward token or admin key required" }); + } + const { email } = req.body ?? {}; + if (!email || !email.includes("@")) return res.status(400).json({ error: "Valid email required" }); + const normalized = email.toLowerCase().trim(); + + await pool.query( + `UPDATE agentify_subject_registry + SET co_steward_emails = array_append(COALESCE(co_steward_emails, '{}'), $2) + WHERE subject_slug = $1 + AND NOT ($2 = ANY(COALESCE(co_steward_emails, '{}')))`, + [slug, normalized] + ).catch(() => {}); + await pool.query( + `UPDATE personaforge_subjects + SET co_steward_emails = array_append(COALESCE(co_steward_emails, '{}'), $2) + WHERE slug = $1 + AND NOT ($2 = ANY(COALESCE(co_steward_emails, '{}')))`, + [slug, normalized] + ).catch(() => {}); + + res.json({ ok: true, slug, co_steward_email: normalized, + note: "Co-steward added. They can now use /my-agents with their email to receive a portal access link." }); + }); + + // ── GET /api/steward/:slug/co-stewards ─────────────────────────────────── + app.get("/api/steward/:slug/co-stewards", async (req: Request, res: Response) => { + if (!isAgentify(req)) return res.status(404).json({ error: "Not found" }); + const { slug } = req.params; + const token = req.query.token as string | undefined; + const key = (req.headers["x-admin-key"] as string) || (req.query.admin_key as string); + if (key !== ADMIN_KEY && !(await validateStewardToken(slug, token))) { + return res.status(403).json({ error: "Steward token or admin key required" }); + } + const { rows } = await pool.query( + `SELECT co_steward_emails FROM agentify_subject_registry WHERE subject_slug = $1`, [slug] + ).catch(() => ({ rows: [] })); + res.json({ slug, co_steward_emails: rows[0]?.co_steward_emails || [] }); + }); + + // ── DELETE /api/steward/:slug/co-stewards/:email ───────────────────────── + app.delete("/api/steward/:slug/co-stewards/:email", async (req: Request, res: Response) => { + if (!isAgentify(req)) return res.status(404).json({ error: "Not found" }); + const { slug, email } = req.params; + const token = req.query.token as string | undefined; + const key = (req.headers["x-admin-key"] as string) || (req.query.admin_key as string); + if (key !== ADMIN_KEY && !(await validateStewardToken(slug, token))) { + return res.status(403).json({ error: "Steward token or admin key required" }); + } + const normalized = decodeURIComponent(email).toLowerCase().trim(); + await pool.query( + `UPDATE agentify_subject_registry + SET co_steward_emails = array_remove(COALESCE(co_steward_emails, '{}'), $2) + WHERE subject_slug = $1`, + [slug, normalized] + ).catch(() => {}); + await pool.query( + `UPDATE personaforge_subjects + SET co_steward_emails = array_remove(COALESCE(co_steward_emails, '{}'), $2) + WHERE slug = $1`, + [slug, normalized] + ).catch(() => {}); + res.json({ ok: true, slug, removed: normalized }); + }); } // ── Approved-only corpus build (async) ───────────────────────────────────────