Initial commit: AS4/411 directory and discovery service for Sankofa Marketplace
Some checks failed
CI / lint (push) Has been cancelled
CI / build (push) Has been cancelled

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
defiQUG
2026-02-08 08:44:20 -08:00
commit c24ae925cf
109 changed files with 7222 additions and 0 deletions

View File

@@ -0,0 +1,27 @@
{
"name": "@as4-411/api-rest",
"type": "module",
"version": "0.1.0",
"description": "REST API for as4-411 resolver and admin",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
"build": "tsc",
"start": "node dist/server.js",
"test": "node --test dist/**/*.test.js 2>/dev/null || true"
},
"dependencies": {
"@as4-411/core": "workspace:*",
"@as4-411/resolver": "workspace:*",
"@as4-411/storage": "workspace:*",
"express": "^4.18.2"
},
"devDependencies": {
"@types/express": "^4.17.21",
"@types/node": "^20.10.0",
"typescript": "^5.3.0"
},
"engines": {
"node": ">=18"
}
}

View File

@@ -0,0 +1,17 @@
import express from "express";
import { createResolverRouter } from "./routes/resolver.js";
import { createSystemRouter } from "./routes/system.js";
import { createAdminRouter } from "./routes/admin.js";
import type { Resolver } from "@as4-411/resolver";
import type { AdminStore } from "@as4-411/storage";
export function createApp(resolver: Resolver, adminStore?: AdminStore | null): express.Application {
const app = express();
app.use(express.json());
app.use(createResolverRouter(resolver));
app.use(createSystemRouter());
app.use(createAdminRouter(adminStore ?? null));
return app;
}

View File

@@ -0,0 +1,4 @@
export { createApp } from "./app.js";
export { createResolverRouter } from "./routes/resolver.js";
export { createSystemRouter } from "./routes/system.js";
export { createAdminRouter } from "./routes/admin.js";

View File

@@ -0,0 +1,276 @@
import { Router, type Request, type Response } from "express";
import type { AdminStore } from "@as4-411/storage";
import type {
Tenant,
Participant,
Identifier,
Endpoint,
CredentialRef,
Policy,
} from "@as4-411/core";
function randomId(): string {
return crypto.randomUUID();
}
/**
* Admin CRUD routes. If adminStore is not provided, returns 501.
* Paths match OpenAPI: /v1/admin/tenants, participants, identifiers, endpoints, credentials, policies.
*/
export function createAdminRouter(adminStore: AdminStore | null): Router {
const router = Router();
if (!adminStore) {
const notImplemented = (_req: Request, res: Response) => {
res.status(501).json({ error: "Admin API not implemented yet" });
};
router.get("/v1/admin/tenants", notImplemented);
router.post("/v1/admin/tenants", notImplemented);
router.get("/v1/admin/tenants/:tenantId", notImplemented);
router.put("/v1/admin/tenants/:tenantId", notImplemented);
router.delete("/v1/admin/tenants/:tenantId", notImplemented);
router.get("/v1/admin/participants", notImplemented);
router.post("/v1/admin/participants", notImplemented);
router.get("/v1/admin/participants/:participantId", notImplemented);
router.put("/v1/admin/participants/:participantId", notImplemented);
router.delete("/v1/admin/participants/:participantId", notImplemented);
router.get("/v1/admin/participants/:participantId/identifiers", notImplemented);
router.post("/v1/admin/participants/:participantId/identifiers", notImplemented);
router.get("/v1/admin/participants/:participantId/endpoints", notImplemented);
router.post("/v1/admin/participants/:participantId/endpoints", notImplemented);
router.get("/v1/admin/participants/:participantId/credentials", notImplemented);
router.post("/v1/admin/participants/:participantId/credentials", notImplemented);
router.get("/v1/admin/policies", notImplemented);
router.post("/v1/admin/policies", notImplemented);
router.get("/v1/admin/policies/:policyId", notImplemented);
router.put("/v1/admin/policies/:policyId", notImplemented);
router.delete("/v1/admin/policies/:policyId", notImplemented);
return router;
}
// Tenants
router.get("/v1/admin/tenants", async (_req: Request, res: Response) => {
try {
const list = await adminStore.listTenants();
res.json(list);
} catch (e) {
res.status(500).json({ error: String(e) });
}
});
router.post("/v1/admin/tenants", async (req: Request, res: Response) => {
try {
const body = req.body as Partial<Tenant>;
const id = body.id ?? randomId();
const name = body.name ?? "";
await adminStore.createTenant({ id, name });
res.status(201).json({ id, name });
} catch (e) {
res.status(400).json({ error: String(e) });
}
});
router.get("/v1/admin/tenants/:tenantId", async (req: Request, res: Response) => {
const t = await adminStore.getTenant(req.params.tenantId);
if (!t) return res.status(404).json({ error: "Not found" });
res.json(t);
});
router.put("/v1/admin/tenants/:tenantId", async (req: Request, res: Response) => {
const ok = await adminStore.updateTenant(req.params.tenantId, {
name: (req.body as { name?: string }).name ?? "",
});
if (!ok) return res.status(404).json({ error: "Not found" });
const t = await adminStore.getTenant(req.params.tenantId);
res.json(t);
});
router.delete("/v1/admin/tenants/:tenantId", async (req: Request, res: Response) => {
const ok = await adminStore.deleteTenant(req.params.tenantId);
if (!ok) return res.status(404).json({ error: "Not found" });
res.status(204).send();
});
// Participants
router.get("/v1/admin/participants", async (req: Request, res: Response) => {
try {
const tenantId = req.query.tenantId as string | undefined;
const list = await adminStore.listParticipants({ tenantId });
res.json(list);
} catch (e) {
res.status(500).json({ error: String(e) });
}
});
router.post("/v1/admin/participants", async (req: Request, res: Response) => {
try {
const body = req.body as Partial<Participant>;
const id = body.id ?? randomId();
const tenantId = body.tenantId ?? "";
const name = body.name ?? "";
await adminStore.createParticipant({ id, tenantId, name });
res.status(201).json({ id, tenantId, name });
} catch (e) {
res.status(400).json({ error: String(e) });
}
});
router.get("/v1/admin/participants/:participantId", async (req: Request, res: Response) => {
const p = await adminStore.getParticipant(req.params.participantId);
if (!p) return res.status(404).json({ error: "Not found" });
res.json(p);
});
router.put("/v1/admin/participants/:participantId", async (req: Request, res: Response) => {
const body = req.body as { name?: string };
const ok = await adminStore.updateParticipant(req.params.participantId, {
name: body.name ?? "",
});
if (!ok) return res.status(404).json({ error: "Not found" });
const p = await adminStore.getParticipant(req.params.participantId);
res.json(p);
});
router.delete("/v1/admin/participants/:participantId", async (req: Request, res: Response) => {
const ok = await adminStore.deleteParticipant(req.params.participantId);
if (!ok) return res.status(404).json({ error: "Not found" });
res.status(204).send();
});
// Identifiers (nested under participant)
router.get("/v1/admin/participants/:participantId/identifiers", async (req: Request, res: Response) => {
const list = await adminStore.getIdentifiersByParticipantId(req.params.participantId);
res.json(list);
});
router.post("/v1/admin/participants/:participantId/identifiers", async (req: Request, res: Response) => {
try {
const body = req.body as Partial<Identifier>;
const id = body.id ?? randomId();
const participantId = req.params.participantId;
const identifier: Identifier = {
id,
participantId,
identifier_type: body.identifier_type ?? "",
value: body.value ?? "",
scope: body.scope,
priority: body.priority ?? 0,
verified_at: body.verified_at,
};
await adminStore.createIdentifier(identifier);
res.status(201).json(identifier);
} catch (e) {
res.status(400).json({ error: String(e) });
}
});
// Endpoints
router.get("/v1/admin/participants/:participantId/endpoints", async (req: Request, res: Response) => {
const list = await adminStore.getEndpointsByParticipantId(req.params.participantId);
res.json(list);
});
router.post("/v1/admin/participants/:participantId/endpoints", async (req: Request, res: Response) => {
try {
const body = req.body as Partial<Endpoint>;
const id = body.id ?? randomId();
const participantId = req.params.participantId;
const endpoint: Endpoint = {
id,
participantId,
protocol: body.protocol ?? "",
address: body.address ?? "",
profile: body.profile,
priority: body.priority ?? 0,
status: body.status ?? "active",
};
await adminStore.createEndpoint(endpoint);
res.status(201).json(endpoint);
} catch (e) {
res.status(400).json({ error: String(e) });
}
});
// Credentials
router.get("/v1/admin/participants/:participantId/credentials", async (req: Request, res: Response) => {
const list = await adminStore.getCredentialsByParticipantId(req.params.participantId);
res.json(list);
});
router.post("/v1/admin/participants/:participantId/credentials", async (req: Request, res: Response) => {
try {
const body = req.body as Partial<CredentialRef>;
const id = body.id ?? randomId();
const participantId = req.params.participantId;
const credential: CredentialRef = {
id,
participantId,
credential_type: body.credential_type ?? "tls",
vault_ref: body.vault_ref ?? "",
fingerprint: body.fingerprint,
valid_from: body.valid_from,
valid_to: body.valid_to,
};
await adminStore.createCredential(credential);
res.status(201).json(credential);
} catch (e) {
res.status(400).json({ error: String(e) });
}
});
// Policies
router.get("/v1/admin/policies", async (req: Request, res: Response) => {
try {
const tenantId = req.query.tenantId as string | undefined;
const list = await adminStore.listPolicies({ tenantId });
res.json(list);
} catch (e) {
res.status(500).json({ error: String(e) });
}
});
router.post("/v1/admin/policies", async (req: Request, res: Response) => {
try {
const body = req.body as Partial<Policy>;
const id = body.id ?? randomId();
const tenantId = body.tenantId ?? "";
const policy: Policy = {
id,
tenantId,
rule_json: body.rule_json ?? {},
effect: body.effect ?? "allow",
priority: body.priority ?? 0,
};
await adminStore.createPolicy(policy);
res.status(201).json(policy);
} catch (e) {
res.status(400).json({ error: String(e) });
}
});
router.get("/v1/admin/policies/:policyId", async (req: Request, res: Response) => {
const p = await adminStore.getPolicy(req.params.policyId);
if (!p) return res.status(404).json({ error: "Not found" });
res.json(p);
});
router.put("/v1/admin/policies/:policyId", async (req: Request, res: Response) => {
const body = req.body as Partial<Policy>;
const ok = await adminStore.updatePolicy(req.params.policyId, {
rule_json: body.rule_json ?? {},
effect: body.effect ?? "allow",
priority: body.priority ?? 0,
});
if (!ok) return res.status(404).json({ error: "Not found" });
const p = await adminStore.getPolicy(req.params.policyId);
res.json(p);
});
router.delete("/v1/admin/policies/:policyId", async (req: Request, res: Response) => {
const ok = await adminStore.deletePolicy(req.params.policyId);
if (!ok) return res.status(404).json({ error: "Not found" });
res.status(204).send();
});
return router;
}

View File

@@ -0,0 +1,46 @@
import { Router, type Request, type Response } from "express";
import type { Resolver } from "@as4-411/resolver";
import type { ResolveRequest } from "@as4-411/core";
export function createResolverRouter(resolver: Resolver): Router {
const router = Router();
router.post("/v1/resolve", async (req: Request, res: Response) => {
try {
const body = req.body as ResolveRequest;
if (!body?.identifiers?.length) {
res.status(400).json({ error: "identifiers required and must be non-empty" });
return;
}
const result = await resolver.resolve(body);
res.json(result);
} catch (err) {
res.status(503).json({
error: "Resolution failed",
message: err instanceof Error ? err.message : String(err),
});
}
});
router.post("/v1/bulk-resolve", async (req: Request, res: Response) => {
try {
const { requests } = req.body as { requests?: ResolveRequest[] };
if (!Array.isArray(requests)) {
res.status(400).json({ error: "requests array required" });
return;
}
const results = await Promise.all(requests.map((r) => resolver.resolve(r)));
res.json({
results,
traceId: crypto.randomUUID(),
});
} catch (err) {
res.status(503).json({
error: "Bulk resolution failed",
message: err instanceof Error ? err.message : String(err),
});
}
});
return router;
}

View File

@@ -0,0 +1,20 @@
import { Router, type Request, type Response } from "express";
export function createSystemRouter(): Router {
const router = Router();
router.get("/v1/health", (_req: Request, res: Response) => {
res.json({
status: "ok",
version: "0.1.0",
checks: {},
});
});
router.get("/v1/metrics", (_req: Request, res: Response) => {
res.setHeader("Content-Type", "text/plain; charset=utf-8");
res.send("# as4-411 metrics (placeholder)\n");
});
return router;
}

View File

@@ -0,0 +1,15 @@
import { createApp } from "./app.js";
import { Resolver } from "@as4-411/resolver";
import { InMemoryDirectoryStore } from "@as4-411/storage";
import { InMemoryResolveCache } from "@as4-411/resolver";
const store = new InMemoryDirectoryStore();
const cache = new InMemoryResolveCache();
const resolver = new Resolver({ store, cache });
const app = createApp(resolver, store);
const port = Number(process.env.PORT) || 4110;
app.listen(port, () => {
console.log(`as4-411 API listening on http://localhost:${port}`);
});

View File

@@ -0,0 +1,16 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"outDir": "dist",
"rootDir": "src",
"strict": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"skipLibCheck": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}