Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
117 changes: 63 additions & 54 deletions data/events.json
Original file line number Diff line number Diff line change
@@ -1,82 +1,91 @@
[
{
"id": "evt_20260212_workshop_memory",
"title": "Agent Memory Patterns in Practice",
"description": "A hands-on workshop on building resilient short-term and long-term memory layers for agents.",
"id": "evt_20260118_workshop_memory_loops",
"title": "Memory Loops Workshop",
"description": "A practical workshop for building durable memory layers and retrieval flows in agent systems.",
"type": "workshop",
"date": "2026-02-12T18:00:00.000Z",
"url": "https://foragents.dev/events/memory-patterns",
"date": "2026-01-18T18:00:00.000Z",
"url": "https://foragents.dev/events/memory-loops-workshop",
"location": "Online",
"createdAt": "2026-02-01T18:00:00.000Z"
"attendeeCount": 36,
"maxAttendees": 60,
"rsvps": ["kai_ops", "nova_builds", "echo_lab"],
"createdAt": "2026-01-05T16:00:00.000Z"
},
{
"id": "evt_20260214_meetup_vancouver",
"id": "evt_20260130_meetup_vancouver_agents",
"title": "Vancouver Agent Builders Meetup",
"description": "Community meetup for local builders sharing active agent projects and deployment lessons.",
"description": "Local meetup for demos, architecture swaps, and lessons from shipping autonomous workflows.",
"type": "meetup",
"date": "2026-02-14T02:00:00.000Z",
"url": "https://foragents.dev/events/vancouver-meetup",
"date": "2026-01-30T02:00:00.000Z",
"location": "Vancouver, BC",
"createdAt": "2026-02-01T18:05:00.000Z"
"attendeeCount": 44,
"maxAttendees": 80,
"rsvps": ["northstar", "agent_rye", "raven_core"],
"createdAt": "2026-01-08T10:20:00.000Z"
},
{
"id": "evt_20260218_workshop_observability",
"title": "Agent Observability Workshop",
"description": "Learn logging, traces, and replay strategies for debugging production agent workflows.",
"type": "workshop",
"date": "2026-02-18T19:00:00.000Z",
"url": "https://foragents.dev/events/observability-workshop",
"id": "evt_20260213_webinar_guardrails",
"title": "Webinar: Guardrails for Tool-Using Agents",
"description": "Live webinar covering policy checks, action constraints, and safety telemetry for production agents.",
"type": "webinar",
"date": "2026-02-13T19:00:00.000Z",
"url": "https://foragents.dev/events/guardrails-webinar",
"location": "Online",
"createdAt": "2026-02-01T18:10:00.000Z"
"attendeeCount": 112,
"maxAttendees": 150,
"rsvps": ["delta_ops", "signal_agent", "aperture-ai"],
"createdAt": "2026-01-20T12:40:00.000Z"
},
{
"id": "evt_20260222_hackathon_weekend",
"title": "Weekend Agent Build Hackathon",
"description": "A 2-day hackathon to ship agent tools, workflows, and integrations with community mentors.",
"id": "evt_20260222_hackathon_weekender",
"title": "Agent Weekender Hackathon",
"description": "A two-day hackathon to ship tools, MCP integrations, and workflow automations with mentor support.",
"type": "hackathon",
"date": "2026-02-22T17:00:00.000Z",
"url": "https://foragents.dev/events/weekend-hackathon",
"url": "https://foragents.dev/events/weekender-hackathon",
"location": "Online",
"createdAt": "2026-02-01T18:15:00.000Z"
},
{
"id": "evt_20260301_meetup_nyc",
"title": "NYC Agents + MCP Meetup",
"description": "Meetup focused on MCP integrations, local demos, and networking with agent engineers.",
"type": "meetup",
"date": "2026-03-01T23:30:00.000Z",
"url": "https://foragents.dev/events/nyc-mcp-meetup",
"location": "New York, NY",
"createdAt": "2026-02-01T18:20:00.000Z"
"attendeeCount": 71,
"maxAttendees": 120,
"rsvps": ["patchbot", "luna_stack", "vectorforge"],
"createdAt": "2026-01-22T09:15:00.000Z"
},
{
"id": "evt_20260305_workshop_security",
"title": "Secure Agent Actions Workshop",
"description": "Practical controls for prompt-injection defense, tool allowlists, and safe action boundaries.",
"type": "workshop",
"date": "2026-03-05T20:00:00.000Z",
"url": "https://foragents.dev/events/security-workshop",
"id": "evt_20260302_launch_registry2",
"title": "forAgents Registry v2 Launch",
"description": "Launch event for Registry v2 with live demos, migration walkthroughs, and Q&A.",
"type": "launch",
"date": "2026-03-02T20:00:00.000Z",
"url": "https://foragents.dev/events/registry-v2-launch",
"location": "Online",
"createdAt": "2026-02-01T18:25:00.000Z"
"attendeeCount": 58,
"maxAttendees": 200,
"rsvps": ["lattice", "orbit_agent", "stackpilot"],
"createdAt": "2026-01-25T14:00:00.000Z"
},
{
"id": "evt_20260310_meetup_sf",
"title": "San Francisco Agent Operators Meetup",
"description": "In-person meetup for teams operating autonomous systems in production.",
"id": "evt_20260312_meetup_nyc_runtime",
"title": "NYC Runtime Operators Meetup",
"description": "In-person meetup focused on observability and ops patterns for autonomous systems.",
"type": "meetup",
"date": "2026-03-10T01:00:00.000Z",
"url": "https://foragents.dev/events/sf-operators-meetup",
"location": "San Francisco, CA",
"createdAt": "2026-02-01T18:30:00.000Z"
"date": "2026-03-12T23:30:00.000Z",
"location": "New York, NY",
"attendeeCount": 39,
"maxAttendees": 75,
"rsvps": ["zenflow", "agent_atlas", "dockyard"],
"createdAt": "2026-01-30T11:00:00.000Z"
},
{
"id": "evt_20260320_hackathon_global",
"title": "Global Agent Hackathon Spring 2026",
"description": "Global hackathon for agent-native products with mentors, tracks, and showcase judging.",
"type": "hackathon",
"date": "2026-03-20T16:00:00.000Z",
"url": "https://foragents.dev/events/global-hackathon-spring",
"id": "evt_20260328_workshop_eval_studio",
"title": "Workshop: Eval Studio for Agent Quality",
"description": "Hands-on lab for creating benchmark suites, failure replay loops, and release confidence checks.",
"type": "workshop",
"date": "2026-03-28T18:30:00.000Z",
"url": "https://foragents.dev/events/eval-studio-workshop",
"location": "Online",
"createdAt": "2026-02-01T18:35:00.000Z"
"attendeeCount": 24,
"maxAttendees": 60,
"rsvps": ["sparrow", "matrix_ops", "linebreaker"],
"createdAt": "2026-02-02T08:05:00.000Z"
}
]
152 changes: 152 additions & 0 deletions src/app/api/events/[id]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
import { NextRequest, NextResponse } from "next/server";
import {
checkRateLimit,
getClientIp,
rateLimitResponse,
readJsonWithLimit,
} from "@/lib/requestLimits";
import { readEventsFile, toPublicEvent, writeEventsFile } from "@/lib/eventsStore";

export const runtime = "nodejs";

const MAX_BODY_BYTES = 8_000;

type RsvpInput = {
agentHandle: string;
};

function validateRsvpPayload(
body: Record<string, unknown>
): { ok: true; value: RsvpInput } | { ok: false; errors: string[] } {
const errors: string[] = [];

const agentHandleRaw =
typeof body.agentHandle === "string" ? body.agentHandle.trim().toLowerCase() : "";

if (!agentHandleRaw) {
errors.push("agentHandle is required");
}

if (agentHandleRaw.length > 50) {
errors.push("agentHandle must be 50 characters or fewer");
}

if (!/^[a-z0-9._-]+$/.test(agentHandleRaw)) {
errors.push("agentHandle may only contain lowercase letters, numbers, dots, underscores, and hyphens");
}

if (errors.length > 0) {
return { ok: false, errors };
}

return {
ok: true,
value: {
agentHandle: agentHandleRaw,
},
};
}

export async function GET(
request: NextRequest,
context: { params: Promise<{ id: string }> }
) {
const ip = getClientIp(request);
const rl = checkRateLimit(`events-id:get:${ip}`, { windowMs: 60_000, max: 120 });
if (!rl.ok) return rateLimitResponse(rl.retryAfterSec);

const { id } = await context.params;
const events = await readEventsFile();
const event = events.find((entry) => entry.id === id);

if (!event) {
return NextResponse.json({ error: "Event not found" }, { status: 404 });
}

return NextResponse.json(toPublicEvent(event), {
headers: {
"Cache-Control": "no-store",
},
});
}

export async function POST(
request: NextRequest,
context: { params: Promise<{ id: string }> }
) {
const ip = getClientIp(request);
const rl = checkRateLimit(`events-id:post:${ip}`, { windowMs: 60_000, max: 50 });
if (!rl.ok) return rateLimitResponse(rl.retryAfterSec);

const { id } = await context.params;

try {
const body = await readJsonWithLimit(request, MAX_BODY_BYTES);
const validation = validateRsvpPayload(body);

if (!validation.ok) {
return NextResponse.json(
{ error: "Validation failed", details: validation.errors },
{ status: 400 }
);
}

const events = await readEventsFile();
const eventIndex = events.findIndex((entry) => entry.id === id);

if (eventIndex === -1) {
return NextResponse.json({ error: "Event not found" }, { status: 404 });
}

const event = events[eventIndex];
const handle = validation.value.agentHandle;

if (event.rsvps.includes(handle)) {
return NextResponse.json(
{
event: toPublicEvent(event),
status: "already_rsvped",
},
{ status: 200 }
);
}

if (event.attendeeCount >= event.maxAttendees) {
return NextResponse.json({ error: "Event is at capacity" }, { status: 409 });
}

const updatedEvent = {
...event,
rsvps: [...event.rsvps, handle],
attendeeCount: Math.min(event.maxAttendees, event.attendeeCount + 1),
};

events[eventIndex] = updatedEvent;
await writeEventsFile(events);

return NextResponse.json(
{
event: toPublicEvent(updatedEvent),
status: "rsvped",
},
{ status: 200 }
);
} catch (err: unknown) {
const status =
typeof err === "object" &&
err !== null &&
"status" in err &&
typeof (err as { status?: unknown }).status === "number"
? ((err as { status: number }).status ?? 400)
: 400;

if (status === 413) {
return NextResponse.json({ error: "Request body too large" }, { status: 413 });
}

return NextResponse.json(
{ error: "Invalid request body. Expected JSON." },
{ status: 400 }
);
}
}
32 changes: 23 additions & 9 deletions src/app/api/events/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
makeEventId,
readEventsFile,
sortEventsByDate,
toPublicEvent,
writeEventsFile,
} from "@/lib/eventsStore";

Expand All @@ -28,9 +29,9 @@ type CreateEventInput = {
location?: string;
};

function validateEventPayload(body: Record<string, unknown>):
| { ok: true; value: CreateEventInput }
| { ok: false; errors: string[] } {
function validateEventPayload(
body: Record<string, unknown>
): { ok: true; value: CreateEventInput } | { ok: false; errors: string[] } {
const errors: string[] = [];

const title = typeof body.title === "string" ? body.title.trim() : "";
Expand All @@ -47,13 +48,17 @@ function validateEventPayload(body: Record<string, unknown>):
if (description.length > 2_000) errors.push("description must be under 2,000 characters");

if (!isEventType(typeRaw)) {
errors.push("type must be one of: workshop, meetup, hackathon");
errors.push("type must be one of: workshop, meetup, hackathon, webinar, launch");
}

if (!dateRaw || Number.isNaN(Date.parse(dateRaw))) {
errors.push("date must be a valid ISO date");
}

if (!url && !location) {
errors.push("either location or url is required");
}

if (url) {
if (url.length > 500) {
errors.push("url must be under 500 characters");
Expand Down Expand Up @@ -114,7 +119,7 @@ export async function GET(request: NextRequest) {

return NextResponse.json(
{
events: sorted,
events: sorted.map(toPublicEvent),
total: sorted.length,
},
{
Expand Down Expand Up @@ -145,16 +150,25 @@ export async function POST(request: NextRequest) {

const newEvent: CommunityEvent = {
id: makeEventId(),
attendeeCount: 0,
maxAttendees: 100,
rsvps: [],
createdAt: new Date().toISOString(),
...validation.value,
};

await writeEventsFile([...current, newEvent]);

return NextResponse.json(newEvent, { status: 201 });
} catch (err) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const status = typeof (err as any)?.status === "number" ? (err as any).status : 400;
return NextResponse.json(toPublicEvent(newEvent), { status: 201 });
} catch (err: unknown) {
const status =
typeof err === "object" &&
err !== null &&
"status" in err &&
typeof (err as { status?: unknown }).status === "number"
? ((err as { status: number }).status ?? 400)
: 400;

if (status === 413) {
return NextResponse.json({ error: "Request body too large" }, { status: 413 });
}
Expand Down
Loading
Loading