Skip to content
Closed
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
5 changes: 5 additions & 0 deletions .changeset/gateway-delete-world.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@resciencelab/agent-world-network": minor
---

Add DELETE /world/:worldId admin endpoint to the gateway for deregistering worlds, with optional GATEWAY_ADMIN_KEY bearer token auth
44 changes: 43 additions & 1 deletion gateway/server.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
* GET /health — health check
* GET /worlds — list discovered world:* agents on AWN network
* GET /agents — list all known AWN agents
* GET /world/:worldId — info about a specific world
* GET /world/:worldId — info about a specific world
* DELETE /world/:worldId — deregister a world (admin, requires GATEWAY_ADMIN_KEY bearer token if set)
* GET /peer/ping — peer liveness
* GET /peer/peers — known peers exchange
* POST /peer/announce — world server registration
Expand Down Expand Up @@ -498,6 +499,47 @@ export async function createGatewayApp(opts = {}) {
};
});

app.delete("/world/:worldId", {
schema: {
summary: "Deregister a world (admin)",
operationId: "deleteWorld",
tags: ["gateway"],
params: {
type: "object",
required: ["worldId"],
properties: { worldId: { type: "string" } },
},
response: {
200: {
type: "object",
required: ["ok", "removed"],
properties: { ok: { type: "boolean" }, removed: { type: "integer" } },
},
403: { $ref: "Error#" },
404: { $ref: "Error#" },
},
},
}, async (req, reply) => {
const adminKey = process.env.GATEWAY_ADMIN_KEY;
if (adminKey) {
const auth = req.headers["authorization"] ?? "";
if (auth !== `Bearer ${adminKey}`) {
return reply.code(403).send({ error: "Forbidden" });
}
}
const { worldId } = req.params;
const worlds = findByCapability(`world:${worldId}`);
if (!worlds.length) return reply.code(404).send({ error: "World not found" });
let removed = 0;
for (const w of worlds) {
registry.delete(w.agentId);
removed++;
}
_registryModifiedAt = Date.now();
console.log(`[gateway] Deregistered world:${worldId} (${removed} agent(s) removed)`);
return { ok: true, removed };
});

app.get("/ws", { websocket: true }, (socket, req) => {
const worldId = new URL(req.url, "http://x").searchParams.get("world");
if (!worldId) {
Expand Down
62 changes: 62 additions & 0 deletions test/gateway-world-record.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -83,4 +83,66 @@ describe("Gateway /world/:worldId", () => {
assert.equal(r2.publicKey, kp2.publicKey)
assert.notEqual(r1.publicKey, r2.publicKey)
})

it("DELETE /world/:worldId returns 404 for unknown world", async () => {
const resp = await app.inject({ method: "DELETE", url: "/world/nonexistent-delete" })
assert.equal(resp.statusCode, 404)
})

it("DELETE /world/:worldId removes a known world", async () => {
const kp = makeKeypair()
const worldId = "delete-me"

await announce(kp, worldId)
const before = await app.inject({ method: "GET", url: `/world/${worldId}` })
assert.equal(before.statusCode, 200)

const del = await app.inject({ method: "DELETE", url: `/world/${worldId}` })
assert.equal(del.statusCode, 200)
const body = JSON.parse(del.body)
assert.equal(body.ok, true)
assert.equal(body.removed, 1)

const after = await app.inject({ method: "GET", url: `/world/${worldId}` })
assert.equal(after.statusCode, 404)
})

it("DELETE /world/:worldId returns 403 when GATEWAY_ADMIN_KEY is set and token is missing", async () => {
const kp = makeKeypair()
const worldId = "protected-world"
await announce(kp, worldId)

const prev = process.env.GATEWAY_ADMIN_KEY
process.env.GATEWAY_ADMIN_KEY = "secret-test-key"
try {
const resp = await app.inject({ method: "DELETE", url: `/world/${worldId}` })
assert.equal(resp.statusCode, 403)
} finally {
if (prev === undefined) delete process.env.GATEWAY_ADMIN_KEY
else process.env.GATEWAY_ADMIN_KEY = prev
}
})

it("DELETE /world/:worldId succeeds with correct GATEWAY_ADMIN_KEY bearer token", async () => {
const kp = makeKeypair()
const worldId = "protected-world-2"
await announce(kp, worldId)

const prev = process.env.GATEWAY_ADMIN_KEY
process.env.GATEWAY_ADMIN_KEY = "secret-test-key"
try {
const resp = await app.inject({
method: "DELETE",
url: `/world/${worldId}`,
headers: { authorization: "Bearer secret-test-key" },
})
assert.equal(resp.statusCode, 200)
const body = JSON.parse(resp.body)
assert.equal(body.ok, true)
assert.equal(body.removed, 1)
} finally {
if (prev === undefined) delete process.env.GATEWAY_ADMIN_KEY
else process.env.GATEWAY_ADMIN_KEY = prev
}
})
})