@verdict-ai/api 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +9 -0
- package/README.md +21 -2
- package/dist/app.d.ts +94 -0
- package/dist/app.js +120 -0
- package/dist/app.js.map +1 -0
- package/dist/github.d.ts +15 -0
- package/dist/github.js +40 -0
- package/dist/github.js.map +1 -0
- package/dist/hub-pages.d.ts +24 -0
- package/dist/hub-pages.js +119 -0
- package/dist/hub-pages.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/openapi.d.ts +463 -0
- package/dist/openapi.js +58 -0
- package/dist/openapi.js.map +1 -0
- package/dist/present.d.ts +110 -0
- package/dist/present.js +10 -0
- package/dist/present.js.map +1 -0
- package/dist/queue-cloudflare.d.ts +17 -0
- package/dist/queue-cloudflare.js +11 -0
- package/dist/queue-cloudflare.js.map +1 -0
- package/dist/rate-limit.d.ts +44 -0
- package/dist/rate-limit.js +40 -0
- package/dist/rate-limit.js.map +1 -0
- package/dist/routes/admin.d.ts +4 -0
- package/dist/routes/admin.js +81 -0
- package/dist/routes/admin.js.map +1 -0
- package/dist/routes/batches.d.ts +4 -0
- package/dist/routes/batches.js +27 -0
- package/dist/routes/batches.js.map +1 -0
- package/dist/routes/evaluations.d.ts +4 -0
- package/dist/routes/evaluations.js +104 -0
- package/dist/routes/evaluations.js.map +1 -0
- package/dist/routes/hub.d.ts +12 -0
- package/dist/routes/hub.js +66 -0
- package/dist/routes/hub.js.map +1 -0
- package/dist/routes/project-versions.d.ts +4 -0
- package/dist/routes/project-versions.js +56 -0
- package/dist/routes/project-versions.js.map +1 -0
- package/dist/routes/projects.d.ts +4 -0
- package/dist/routes/projects.js +91 -0
- package/dist/routes/projects.js.map +1 -0
- package/dist/routes/promotions.d.ts +4 -0
- package/dist/routes/promotions.js +72 -0
- package/dist/routes/promotions.js.map +1 -0
- package/dist/routes/public-hub.d.ts +41 -0
- package/dist/routes/public-hub.js +119 -0
- package/dist/routes/public-hub.js.map +1 -0
- package/dist/routes/publications.d.ts +4 -0
- package/dist/routes/publications.js +79 -0
- package/dist/routes/publications.js.map +1 -0
- package/dist/routes/reports.d.ts +4 -0
- package/dist/routes/reports.js +84 -0
- package/dist/routes/reports.js.map +1 -0
- package/dist/routes/rubrics.d.ts +4 -0
- package/dist/routes/rubrics.js +50 -0
- package/dist/routes/rubrics.js.map +1 -0
- package/dist/routes/shared.d.ts +42 -0
- package/dist/routes/shared.js +94 -0
- package/dist/routes/shared.js.map +1 -0
- package/dist/server.d.ts +19 -0
- package/dist/server.js +69 -0
- package/dist/server.js.map +1 -0
- package/dist/worker.d.ts +58 -0
- package/dist/worker.js +81 -0
- package/dist/worker.js.map +1 -0
- package/package.json +69 -3
- package/wrangler.example.toml +53 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Request limits for the hub's open doors (docs/design/hub.md H6): sign-in, publishing, and public reads. A limiter
|
|
3
|
+
* answers whether one more request under a key is allowed. The in-memory limiter counts per process (per isolate on
|
|
4
|
+
* Workers) and is a floor; on Cloudflare, bind the Workers Rate Limiting API to share counts across isolates.
|
|
5
|
+
*/
|
|
6
|
+
import type { Context } from "hono";
|
|
7
|
+
import { HTTPException } from "hono/http-exception";
|
|
8
|
+
export interface RateLimiter {
|
|
9
|
+
limit(key: string): Promise<boolean> | boolean;
|
|
10
|
+
}
|
|
11
|
+
/** `login`: per client address. `publish` (preview included): per account. `read`: public JSON and pages, per client address. */
|
|
12
|
+
export interface HubLimits {
|
|
13
|
+
login?: RateLimiter;
|
|
14
|
+
publish?: RateLimiter;
|
|
15
|
+
read?: RateLimiter;
|
|
16
|
+
}
|
|
17
|
+
export interface Rule {
|
|
18
|
+
limit: number;
|
|
19
|
+
windowMs: number;
|
|
20
|
+
}
|
|
21
|
+
export declare const DEFAULT_HUB_RULES: Record<keyof HubLimits, Rule>;
|
|
22
|
+
/** Fixed-window counter. Old windows are dropped as the map grows, so memory stays bounded by active keys. */
|
|
23
|
+
export declare function memoryLimiter({ limit, windowMs }: Rule, now?: () => number): RateLimiter;
|
|
24
|
+
export declare function defaultHubLimits(now?: () => number): Required<HubLimits>;
|
|
25
|
+
/** Cloudflare's Workers Rate Limiting binding (`[[ratelimits]]` in wrangler.toml). */
|
|
26
|
+
export interface CloudflareRateLimit {
|
|
27
|
+
limit(options: {
|
|
28
|
+
key: string;
|
|
29
|
+
}): Promise<{
|
|
30
|
+
success: boolean;
|
|
31
|
+
}>;
|
|
32
|
+
}
|
|
33
|
+
export declare const cloudflareLimiter: (binding: CloudflareRateLimit) => RateLimiter;
|
|
34
|
+
/**
|
|
35
|
+
* The client a request comes from. Only a header the platform sets is trusted (Cloudflare's `cf-connecting-ip`), or the
|
|
36
|
+
* socket address the Node server passes in; X-Forwarded-For is anyone's to write.
|
|
37
|
+
*/
|
|
38
|
+
export type ClientKey = (c: Context) => string;
|
|
39
|
+
export declare const defaultClientKey: ClientKey;
|
|
40
|
+
/** Answered as 429 with Retry-After by the app's error handler. */
|
|
41
|
+
export declare class RateLimitedError extends HTTPException {
|
|
42
|
+
constructor(what: string);
|
|
43
|
+
}
|
|
44
|
+
export declare function enforce(limiter: RateLimiter | undefined, key: string, what: string): Promise<void>;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { HTTPException } from "hono/http-exception";
|
|
2
|
+
export const DEFAULT_HUB_RULES = {
|
|
3
|
+
login: { limit: 10, windowMs: 60_000 },
|
|
4
|
+
publish: { limit: 30, windowMs: 60_000 },
|
|
5
|
+
read: { limit: 300, windowMs: 60_000 },
|
|
6
|
+
};
|
|
7
|
+
/** Fixed-window counter. Old windows are dropped as the map grows, so memory stays bounded by active keys. */
|
|
8
|
+
export function memoryLimiter({ limit, windowMs }, now = Date.now) {
|
|
9
|
+
const windows = new Map();
|
|
10
|
+
return {
|
|
11
|
+
limit(key) {
|
|
12
|
+
const t = now();
|
|
13
|
+
if (windows.size > 10_000)
|
|
14
|
+
for (const [k, w] of windows)
|
|
15
|
+
if (t - w.start >= windowMs)
|
|
16
|
+
windows.delete(k);
|
|
17
|
+
const w = windows.get(key);
|
|
18
|
+
if (!w || t - w.start >= windowMs) {
|
|
19
|
+
windows.set(key, { start: t, count: 1 });
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
w.count++;
|
|
23
|
+
return w.count <= limit;
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
export function defaultHubLimits(now) {
|
|
28
|
+
return { login: memoryLimiter(DEFAULT_HUB_RULES.login, now), publish: memoryLimiter(DEFAULT_HUB_RULES.publish, now), read: memoryLimiter(DEFAULT_HUB_RULES.read, now) };
|
|
29
|
+
}
|
|
30
|
+
export const cloudflareLimiter = (binding) => ({ limit: async (key) => (await binding.limit({ key })).success });
|
|
31
|
+
export const defaultClientKey = (c) => c.req.header("cf-connecting-ip") ?? "anonymous";
|
|
32
|
+
/** Answered as 429 with Retry-After by the app's error handler. */
|
|
33
|
+
export class RateLimitedError extends HTTPException {
|
|
34
|
+
constructor(what) { super(429, { message: `too many ${what} requests; try again in a minute` }); }
|
|
35
|
+
}
|
|
36
|
+
export async function enforce(limiter, key, what) {
|
|
37
|
+
if (limiter && !(await limiter.limit(key)))
|
|
38
|
+
throw new RateLimitedError(what);
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=rate-limit.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rate-limit.js","sourceRoot":"","sources":["../src/rate-limit.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAQpD,MAAM,CAAC,MAAM,iBAAiB,GAAkC;IAC9D,KAAK,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE;IACtC,OAAO,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE;IACxC,IAAI,EAAE,EAAE,KAAK,EAAE,GAAG,EAAE,QAAQ,EAAE,MAAM,EAAE;CACvC,CAAC;AAEF,8GAA8G;AAC9G,MAAM,UAAU,aAAa,CAAC,EAAE,KAAK,EAAE,QAAQ,EAAQ,EAAE,MAAoB,IAAI,CAAC,GAAG;IACnF,MAAM,OAAO,GAAG,IAAI,GAAG,EAA4C,CAAC;IACpE,OAAO;QACL,KAAK,CAAC,GAAG;YACP,MAAM,CAAC,GAAG,GAAG,EAAE,CAAC;YAChB,IAAI,OAAO,CAAC,IAAI,GAAG,MAAM;gBAAE,KAAK,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,OAAO;oBAAE,IAAI,CAAC,GAAG,CAAC,CAAC,KAAK,IAAI,QAAQ;wBAAE,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;YACxG,MAAM,CAAC,GAAG,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;YAC3B,IAAI,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,KAAK,IAAI,QAAQ,EAAE,CAAC;gBAAC,OAAO,CAAC,GAAG,CAAC,GAAG,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC,CAAC;gBAAC,OAAO,IAAI,CAAC;YAAC,CAAC;YAC7F,CAAC,CAAC,KAAK,EAAE,CAAC;YACV,OAAO,CAAC,CAAC,KAAK,IAAI,KAAK,CAAC;QAC1B,CAAC;KACF,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,GAAkB;IACjD,OAAO,EAAE,KAAK,EAAE,aAAa,CAAC,iBAAiB,CAAC,KAAK,EAAE,GAAG,CAAC,EAAE,OAAO,EAAE,aAAa,CAAC,iBAAiB,CAAC,OAAO,EAAE,GAAG,CAAC,EAAE,IAAI,EAAE,aAAa,CAAC,iBAAiB,CAAC,IAAI,EAAE,GAAG,CAAC,EAAE,CAAC;AAC1K,CAAC;AAKD,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,OAA4B,EAAe,EAAE,CAAC,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,GAAG,EAAE,EAAE,CAAC,CAAC,MAAM,OAAO,CAAC,KAAK,CAAC,EAAE,GAAG,EAAE,CAAC,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC;AAOnJ,MAAM,CAAC,MAAM,gBAAgB,GAAc,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,kBAAkB,CAAC,IAAI,WAAW,CAAC;AAElG,mEAAmE;AACnE,MAAM,OAAO,gBAAiB,SAAQ,aAAa;IACjD,YAAY,IAAY,IAAI,KAAK,CAAC,GAAG,EAAE,EAAE,OAAO,EAAE,YAAY,IAAI,kCAAkC,EAAE,CAAC,CAAC,CAAC,CAAC;CAC3G;AAED,MAAM,CAAC,KAAK,UAAU,OAAO,CAAC,OAAgC,EAAE,GAAW,EAAE,IAAY;IACvF,IAAI,OAAO,IAAI,CAAC,CAAC,MAAM,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QAAE,MAAM,IAAI,gBAAgB,CAAC,IAAI,CAAC,CAAC;AAC/E,CAAC"}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import type { AppOptions, Env } from "../app.js";
|
|
3
|
+
/** Registers admin operations; handler failures propagate to the transport error boundary. */
|
|
4
|
+
export declare function registerAdmin(app: Hono<Env>, { engine, keys, adminKey, now }: AppOptions): void;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Defines admin request handling at the REST boundary.
|
|
3
|
+
* Tenant context flows to every engine/store operation; validation failures use the shared HTTP error mapper.
|
|
4
|
+
*/
|
|
5
|
+
import { hasPublications, NotFoundError, ValidationError } from "@verdict-ai/core";
|
|
6
|
+
import { HTTPException } from "hono/http-exception";
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
/** Registers admin operations; handler failures propagate to the transport error boundary. */
|
|
9
|
+
export function registerAdmin(app, { engine, keys, adminKey, now = () => Date.now() }) {
|
|
10
|
+
app.use("/v1/admin/*", async (c, next) => {
|
|
11
|
+
if (!adminKey)
|
|
12
|
+
throw new HTTPException(404, { message: "admin API disabled (set VERDICT_ADMIN_KEY)" });
|
|
13
|
+
const header = c.req.header("authorization")?.replace(/^Bearer\s+/i, "");
|
|
14
|
+
if (header !== adminKey)
|
|
15
|
+
throw new HTTPException(401, { message: "invalid admin key" });
|
|
16
|
+
await next();
|
|
17
|
+
});
|
|
18
|
+
registerAdminKeys(app, keys);
|
|
19
|
+
registerTakedowns(app, engine, now);
|
|
20
|
+
}
|
|
21
|
+
/* ---------- admin: API keys ---------- */
|
|
22
|
+
function registerAdminKeys(app, keys) {
|
|
23
|
+
const manager = () => {
|
|
24
|
+
if (!keys)
|
|
25
|
+
throw new HTTPException(404, { message: "key management needs a database-backed key store" });
|
|
26
|
+
return keys;
|
|
27
|
+
};
|
|
28
|
+
app.post("/v1/admin/keys", async (c) => {
|
|
29
|
+
const body = z.object({ tenantId: z.string().min(1), name: z.string().min(1) }).parse(await c.req.json());
|
|
30
|
+
return c.json(await manager().createKey(body.tenantId, body.name), 201);
|
|
31
|
+
});
|
|
32
|
+
app.get("/v1/admin/keys", async (c) => {
|
|
33
|
+
const tenantId = c.req.query("tenant");
|
|
34
|
+
if (!tenantId)
|
|
35
|
+
throw new ValidationError("query param `tenant` is required");
|
|
36
|
+
return c.json({ data: await manager().listKeys(tenantId) });
|
|
37
|
+
});
|
|
38
|
+
app.delete("/v1/admin/keys/:id", async (c) => {
|
|
39
|
+
if (!(await manager().revokeKey(c.req.param("id"))))
|
|
40
|
+
throw new NotFoundError(`key ${c.req.param("id")} not found`);
|
|
41
|
+
return c.body(null, 204);
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
/*
|
|
45
|
+
* ---------- admin: hub takedowns (docs/design/hub.md H6) ----------
|
|
46
|
+
* Operators can take any publication down, with a reason that is shown publicly, and put back only what they took down:
|
|
47
|
+
* an author's own withdrawal is the author's to keep.
|
|
48
|
+
*/
|
|
49
|
+
function registerTakedowns(app, engine, now) {
|
|
50
|
+
const store = () => {
|
|
51
|
+
if (!hasPublications(engine.store))
|
|
52
|
+
throw new HTTPException(404, { message: "this server's store cannot keep publications" });
|
|
53
|
+
return engine.store;
|
|
54
|
+
};
|
|
55
|
+
const find = async (id) => {
|
|
56
|
+
const p = await store().getPublication(id);
|
|
57
|
+
if (!p)
|
|
58
|
+
throw new NotFoundError(`publication ${id} not found`);
|
|
59
|
+
return p;
|
|
60
|
+
};
|
|
61
|
+
app.get("/v1/admin/publications", async (c) => {
|
|
62
|
+
const q = z.object({ q: z.string().optional(), owner: z.string().optional(), slug: z.string().optional(), limit: z.coerce.number().int().min(1).max(500).default(100) }).parse(c.req.query());
|
|
63
|
+
return c.json({ data: await store().listPublications({ ...q, includeWithdrawn: true }) });
|
|
64
|
+
});
|
|
65
|
+
app.post("/v1/admin/publications/:id/takedown", async (c) => {
|
|
66
|
+
const { reason } = z.object({ reason: z.string().trim().min(1).max(500) }).parse(await c.req.json());
|
|
67
|
+
const p = await find(c.req.param("id")), at = new Date(now()).toISOString();
|
|
68
|
+
const next = { ...p, withdrawnAt: p.withdrawnAt ?? at, takedown: { reason, at } };
|
|
69
|
+
await store().putPublication(next);
|
|
70
|
+
return c.json({ publication: next });
|
|
71
|
+
});
|
|
72
|
+
app.post("/v1/admin/publications/:id/restore", async (c) => {
|
|
73
|
+
const p = await find(c.req.param("id"));
|
|
74
|
+
if (!p.takedown)
|
|
75
|
+
throw new HTTPException(409, { message: `${p.ref} at ${p.version.id} was not taken down${p.withdrawnAt ? "; its author withdrew it" : ""}` });
|
|
76
|
+
const { takedown: _takedown, withdrawnAt: _withdrawnAt, ...restored } = p;
|
|
77
|
+
await store().putPublication(restored);
|
|
78
|
+
return c.json({ publication: restored });
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
//# sourceMappingURL=admin.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"admin.js","sourceRoot":"","sources":["../../src/routes/admin.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tenant context flows to every engine/store operation; validation failures use the shared HTTP error mapper.
|
|
3
|
+
*/
|
|
4
|
+
import { Rubric, Subject } from "@verdict-ai/core";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { present } from "../present.js";
|
|
7
|
+
import { readIdempotencyKey, tenant } from "./shared.js";
|
|
8
|
+
/** Registers batches operations; handler failures propagate to the transport error boundary. */
|
|
9
|
+
export function registerBatches(app, { engine }) {
|
|
10
|
+
/* ---------- batches (leaderboards) ---------- */
|
|
11
|
+
app.post("/v1/batches", async (c) => {
|
|
12
|
+
const raw = z.record(z.string(), z.unknown()).parse(await c.req.json());
|
|
13
|
+
const idempotencyKey = readIdempotencyKey(c.req.raw);
|
|
14
|
+
const body = z.object({ rubric: z.union([z.string(), Rubric]), subjects: z.array(Subject).min(1), metadata: z.record(z.string(), z.unknown()).optional(), webhook: z.string().url().optional() }).parse(raw);
|
|
15
|
+
const opts = { tenantId: tenant(c), metadata: body.metadata, webhook: body.webhook };
|
|
16
|
+
// why: preserve the submitted body for conflict detection instead of hashing schema defaults.
|
|
17
|
+
const fingerprintBody = Object.fromEntries(["rubric", "subjects", "metadata", "webhook"].filter(k => k in raw).map(k => [k, raw[k]]));
|
|
18
|
+
const result = idempotencyKey === undefined
|
|
19
|
+
? { ...await engine.evaluateBatch(body.rubric, body.subjects, opts), replayed: false }
|
|
20
|
+
: await engine.evaluateBatchIdempotent(body.rubric, body.subjects, { ...opts, idempotencyKey, fingerprintBody });
|
|
21
|
+
const { evaluations, ranking, replayed } = result;
|
|
22
|
+
if (replayed)
|
|
23
|
+
c.header("Idempotency-Replayed", "true");
|
|
24
|
+
return c.json({ ranking, evaluations: evaluations.map((e) => present(e, { ratings: false })) }, replayed ? 200 : 201);
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=batches.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"batches.js","sourceRoot":"","sources":["../../src/routes/batches.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAEnD,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,OAAO,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AACxC,OAAO,EAAE,kBAAkB,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAEzD,gGAAgG;AAChG,MAAM,UAAU,eAAe,CAAC,GAAc,EAAE,EAAE,MAAM,EAAc;IACpE,kDAAkD;IAClD,GAAG,CAAC,IAAI,CAAC,aAAa,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE;QAClC,MAAM,GAAG,GAAG,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC;QACxE,MAAM,cAAc,GAAG,kBAAkB,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QACrD,MAAM,IAAI,GAAG,CAAC,CAAC,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,EAAE,EAAE,MAAM,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,QAAQ,EAAE,EAAE,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QAC7M,MAAM,IAAI,GAAG,EAAE,QAAQ,EAAE,MAAM,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,IAAI,CAAC,QAAQ,EAAE,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE,CAAC;QACrF,8FAA8F;QAC9F,MAAM,eAAe,GAAG,MAAM,CAAC,WAAW,CAAC,CAAC,QAAQ,EAAE,UAAU,EAAE,UAAU,EAAE,SAAS,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QACtI,MAAM,MAAM,GAAG,cAAc,KAAK,SAAS;YACzC,CAAC,CAAC,EAAE,GAAG,MAAM,MAAM,CAAC,aAAa,CAAC,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,EAAE,QAAQ,EAAE,KAAK,EAAE;YACtF,CAAC,CAAC,MAAM,MAAM,CAAC,uBAAuB,CAAC,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,QAAQ,EAAE,EAAE,GAAG,IAAI,EAAE,cAAc,EAAE,eAAe,EAAE,CAAC,CAAC;QACnH,MAAM,EAAE,WAAW,EAAE,OAAO,EAAE,QAAQ,EAAE,GAAG,MAAM,CAAC;QAClD,IAAI,QAAQ;YAAE,CAAC,CAAC,MAAM,CAAC,sBAAsB,EAAE,MAAM,CAAC,CAAC;QACvD,OAAO,CAAC,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,WAAW,EAAE,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,CAAC,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC,CAAC,EAAE,EAAE,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;IACxH,CAAC,CAAC,CAAC;AAEL,CAAC"}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import type { AppOptions, Env } from "../app.js";
|
|
3
|
+
/** Registers evaluations operations; handler failures propagate to the transport error boundary. */
|
|
4
|
+
export declare function registerEvaluations(app: Hono<Env>, { engine, allowAsync, background }: AppOptions): void;
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tenant context flows to every engine/store operation; validation failures use the shared HTTP error mapper.
|
|
3
|
+
*/
|
|
4
|
+
import { Aggregation, NotFoundError, Rubric, Subject, ValidationError } from "@verdict-ai/core";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { present } from "../present.js";
|
|
7
|
+
import { readIdempotencyKey, tenant } from "./shared.js";
|
|
8
|
+
// why: the inbox is bounded independently of ordinary listings so pending tasks remain useful without unbounded reads.
|
|
9
|
+
const TASK_INBOX_LIMIT = 500;
|
|
10
|
+
/** Registers evaluations operations; handler failures propagate to the transport error boundary. */
|
|
11
|
+
export function registerEvaluations(app, { engine, allowAsync, background }) {
|
|
12
|
+
/* ---------- evaluations ---------- */
|
|
13
|
+
app.post("/v1/evaluations", async (c) => {
|
|
14
|
+
const parsed = await parseEvaluateBody(c);
|
|
15
|
+
const { body } = parsed;
|
|
16
|
+
if (body.async && allowAsync)
|
|
17
|
+
return respondAsync(c, engine, parsed, background);
|
|
18
|
+
return respondSync(c, engine, parsed);
|
|
19
|
+
});
|
|
20
|
+
app.get("/v1/evaluations", async (c) => {
|
|
21
|
+
const list = await engine.store.listEvaluations({ tenantId: tenant(c), rubricId: c.req.query("rubric"), status: c.req.query("status"), limit: Number(c.req.query("limit") ?? 50) });
|
|
22
|
+
return c.json({ data: list.map((e) => present(e, { ratings: false })) });
|
|
23
|
+
});
|
|
24
|
+
app.get("/v1/evaluations/:id", async (c) => {
|
|
25
|
+
const ev = await engine.store.getEvaluation(c.req.param("id"), tenant(c));
|
|
26
|
+
if (!ev)
|
|
27
|
+
throw new NotFoundError(`evaluation ${c.req.param("id")} not found`);
|
|
28
|
+
return c.json(present(ev, { evidence: c.req.query("evidence") === "true" }));
|
|
29
|
+
});
|
|
30
|
+
app.post("/v1/evaluations/:id/retry", async (c) => {
|
|
31
|
+
const ev = await engine.retryEvaluation(c.req.param("id"), tenant(c));
|
|
32
|
+
return c.json({ id: ev.id, status: "queued", url: `/v1/evaluations/${ev.id}` }, 202);
|
|
33
|
+
});
|
|
34
|
+
const ScoreBody = z.object({ criterionId: z.string(), raterId: z.string(), score: z.union([z.number(), z.string()]), rationale: z.string().optional(), confidence: z.number().min(0).max(1).optional() });
|
|
35
|
+
app.post("/v1/evaluations/:id/scores", async (c) => {
|
|
36
|
+
const body = ScoreBody.parse(await c.req.json());
|
|
37
|
+
return c.json(present(await engine.submitScore(c.req.param("id"), body, tenant(c))));
|
|
38
|
+
});
|
|
39
|
+
/** Human task inbox: everything waiting on a rater. */
|
|
40
|
+
app.get("/v1/tasks", async (c) => {
|
|
41
|
+
const rater = c.req.query("rater");
|
|
42
|
+
const list = await engine.store.listEvaluations({ tenantId: tenant(c), status: "pending_human", limit: TASK_INBOX_LIMIT });
|
|
43
|
+
const tasks = list.flatMap((e) => e.pendingTasks.filter((t) => !rater || (t.raterId ? t.raterId === rater : !t.allowedRaters || t.allowedRaters.includes(rater))).map((t) => ({ evaluationId: e.id, rubric: e.rubric.name, subject: e.subject.name, ...t })));
|
|
44
|
+
return c.json({ data: tasks });
|
|
45
|
+
});
|
|
46
|
+
app.post("/v1/evaluations/:id/aggregate", async (c) => {
|
|
47
|
+
const body = z.object({ aggregation: Aggregation.partial(), persist: z.boolean().default(false) }).parse(await c.req.json());
|
|
48
|
+
return c.json(present(await engine.reaggregate(c.req.param("id"), body.aggregation, tenant(c), body.persist)));
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
const EvaluateBody = z.object({
|
|
52
|
+
rubric: z.union([z.string(), Rubric]),
|
|
53
|
+
subject: Subject.optional(),
|
|
54
|
+
/** Shorthand: inputs at top level instead of subject.inputs. */
|
|
55
|
+
inputs: Subject.shape.inputs.optional(),
|
|
56
|
+
name: z.string().optional(),
|
|
57
|
+
metadata: z.record(z.string(), z.unknown()).optional(),
|
|
58
|
+
webhook: z.string().url().optional(),
|
|
59
|
+
async: z.boolean().optional(),
|
|
60
|
+
/** Include full evidence text in the response. */
|
|
61
|
+
includeEvidence: z.boolean().optional(),
|
|
62
|
+
});
|
|
63
|
+
async function parseEvaluateBody(c) {
|
|
64
|
+
const raw = z.record(z.string(), z.unknown()).parse(await c.req.json());
|
|
65
|
+
const body = EvaluateBody.parse(raw);
|
|
66
|
+
const idempotencyKey = readIdempotencyKey(c.req.raw);
|
|
67
|
+
// why: transport-only flags do not change evaluation identity; hash only scoring inputs and delivery metadata.
|
|
68
|
+
const fingerprintBody = Object.fromEntries(["rubric", "subject", "inputs", "name", "metadata", "webhook"].filter(k => k in raw).map(k => [k, raw[k]]));
|
|
69
|
+
const subject = body.subject ?? { name: body.name, inputs: body.inputs ?? [] };
|
|
70
|
+
if (!subject.inputs?.length)
|
|
71
|
+
throw new ValidationError("provide `subject.inputs` or `inputs`");
|
|
72
|
+
const req = { rubric: body.rubric, subject, tenantId: tenant(c), metadata: body.metadata, webhook: body.webhook };
|
|
73
|
+
return { body, req, idempotencyKey, fingerprintBody };
|
|
74
|
+
}
|
|
75
|
+
async function respondAsync(c, engine, { req, idempotencyKey, fingerprintBody }, background) {
|
|
76
|
+
// why: durable queues own retries; the background fallback is for deployments without a queue binding.
|
|
77
|
+
if (engine.queue) {
|
|
78
|
+
const result = idempotencyKey === undefined
|
|
79
|
+
? { evaluation: await engine.enqueueEvaluation(req), replayed: false }
|
|
80
|
+
: await engine.enqueueEvaluationIdempotent({ ...req, idempotencyKey, fingerprintBody });
|
|
81
|
+
// why: a replay returns the existing resource with 200 instead of claiming a new job was accepted.
|
|
82
|
+
if (result.replayed)
|
|
83
|
+
c.header("Idempotency-Replayed", "true");
|
|
84
|
+
return c.json({ id: result.evaluation.id, status: result.evaluation.status, url: `/v1/evaluations/${result.evaluation.id}` }, result.replayed ? 200 : 202);
|
|
85
|
+
}
|
|
86
|
+
{
|
|
87
|
+
const result = await engine.evaluateBackground({ ...req, idempotencyKey, fingerprintBody }, background ?? (work => { void work.catch(error => console.error("Background evaluation failed", error)); }));
|
|
88
|
+
if (result.replayed)
|
|
89
|
+
c.header("Idempotency-Replayed", "true");
|
|
90
|
+
return c.json({ id: result.evaluation.id, status: result.replayed ? result.evaluation.status : "queued", url: `/v1/evaluations/${result.evaluation.id}` }, result.replayed ? 200 : 202);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
async function respondSync(c, engine, { req, idempotencyKey, fingerprintBody, body }) {
|
|
94
|
+
const includeEvidence = body.includeEvidence;
|
|
95
|
+
if (idempotencyKey !== undefined) {
|
|
96
|
+
const { evaluation, replayed } = await engine.evaluateIdempotent({ ...req, idempotencyKey, fingerprintBody });
|
|
97
|
+
if (replayed)
|
|
98
|
+
c.header("Idempotency-Replayed", "true");
|
|
99
|
+
return c.json(present(evaluation, { evidence: includeEvidence }), replayed ? 200 : evaluation.status === "failed" ? 422 : 201);
|
|
100
|
+
}
|
|
101
|
+
const ev = await engine.evaluate(req);
|
|
102
|
+
return c.json(present(ev, { evidence: includeEvidence }), ev.status === "failed" ? 422 : 201);
|
|
103
|
+
}
|
|
104
|
+
//# sourceMappingURL=evaluations.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"evaluations.js","sourceRoot":"","sources":["../../src/routes/evaluations.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import type { AppOptions, Env } from "../app.js";
|
|
3
|
+
/** Paths that answer without an API key. */
|
|
4
|
+
export declare const PUBLIC_HUB_PATHS: Set<string>;
|
|
5
|
+
/** The tenant of a GitHub account. Numeric ids survive renamed logins. */
|
|
6
|
+
export declare const githubTenant: (githubId: number) => string;
|
|
7
|
+
export declare function registerHub(app: Hono<Env>, { engine, keys, github, now, limits, clientKey, signIn }: AppOptions): void;
|
|
8
|
+
/**
|
|
9
|
+
* Who a tenant publishes as. With GitHub sign-in configured, only signed-in GitHub accounts publish, under their
|
|
10
|
+
* login; without it (a self-hosted team server), a tenant publishes under its own id, an open server as "local".
|
|
11
|
+
*/
|
|
12
|
+
export declare function publisherFor(opts: Pick<AppOptions, "engine" | "github">): (tenantId: string | undefined) => Promise<string | undefined>;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hub sign-in (docs/design/hub.md H2). `GET /v1/hub/config` and `POST /v1/hub/login` are public: the CLI learns the
|
|
3
|
+
* GitHub client id, runs the device flow, and trades the GitHub token for a hub API key bound to the GitHub account.
|
|
4
|
+
* `GET /v1/hub/me` and `DELETE /v1/hub/keys/:id` are authenticated.
|
|
5
|
+
*/
|
|
6
|
+
import { hasPublishers } from "@verdict-ai/core";
|
|
7
|
+
import { HTTPException } from "hono/http-exception";
|
|
8
|
+
import { z } from "zod";
|
|
9
|
+
import { verifyGithubToken } from "../github.js";
|
|
10
|
+
import { defaultClientKey, enforce } from "../rate-limit.js";
|
|
11
|
+
import { tenant } from "./shared.js";
|
|
12
|
+
/** Paths that answer without an API key. */
|
|
13
|
+
export const PUBLIC_HUB_PATHS = new Set(["/v1/hub/config", "/v1/hub/login"]);
|
|
14
|
+
/** The tenant of a GitHub account. Numeric ids survive renamed logins. */
|
|
15
|
+
export const githubTenant = (githubId) => `gh:${githubId}`;
|
|
16
|
+
export function registerHub(app, { engine, keys, github, now = () => Date.now(), limits, clientKey = defaultClientKey, signIn }) {
|
|
17
|
+
const publishers = () => {
|
|
18
|
+
if (!hasPublishers(engine.store))
|
|
19
|
+
throw new HTTPException(501, { message: "this server's store cannot keep publishers" });
|
|
20
|
+
return engine.store;
|
|
21
|
+
};
|
|
22
|
+
app.get("/v1/hub/config", (c) => c.json({ github: github ? { clientId: github.clientId } : null, publishing: github ? "github" : "tenant", ...(signIn ? { signIn } : {}) }));
|
|
23
|
+
app.post("/v1/hub/login", async (c) => {
|
|
24
|
+
if (!github)
|
|
25
|
+
throw new HTTPException(501, { message: "GitHub sign-in is not configured on this server (GITHUB_CLIENT_ID)" });
|
|
26
|
+
if (!keys)
|
|
27
|
+
throw new HTTPException(501, { message: "this server cannot issue API keys (needs a database store)" });
|
|
28
|
+
// Each sign-in asks GitHub and mints a key: limit it per client before doing either.
|
|
29
|
+
await enforce(limits?.login, clientKey(c), "sign-in");
|
|
30
|
+
const { githubToken } = z.object({ githubToken: z.string().min(1).max(512) }).parse(await c.req.json());
|
|
31
|
+
const account = await verifyGithubToken(githubToken, github);
|
|
32
|
+
const tenantId = githubTenant(account.id), at = new Date(now()).toISOString();
|
|
33
|
+
const existing = await publishers().getPublisher(tenantId);
|
|
34
|
+
await publishers().putPublisher({ tenantId, githubId: account.id, login: account.login, createdAt: existing?.createdAt ?? at, updatedAt: at });
|
|
35
|
+
const key = await keys.createKey(tenantId, `hub sign-in (${account.login})`);
|
|
36
|
+
// The GitHub token is not stored anywhere; the hub key is shown once, here.
|
|
37
|
+
return c.json({ apiKey: key.key, keyId: key.id, handle: account.login, tenantId }, 201);
|
|
38
|
+
});
|
|
39
|
+
app.get("/v1/hub/me", async (c) => {
|
|
40
|
+
const tenantId = tenant(c);
|
|
41
|
+
const publisher = tenantId && hasPublishers(engine.store) ? await engine.store.getPublisher(tenantId) : undefined;
|
|
42
|
+
return c.json({ tenantId: tenantId ?? null, handle: publisher?.login ?? (github ? null : tenantId ?? "local"), github: publisher ? { id: publisher.githubId, login: publisher.login } : null });
|
|
43
|
+
});
|
|
44
|
+
app.delete("/v1/hub/keys/:id", async (c) => {
|
|
45
|
+
if (!keys)
|
|
46
|
+
throw new HTTPException(501, { message: "this server cannot manage API keys" });
|
|
47
|
+
const tenantId = tenant(c);
|
|
48
|
+
if (!tenantId || !(await keys.revokeKey(c.req.param("id"), tenantId)))
|
|
49
|
+
throw new HTTPException(404, { message: "no such key for this account" });
|
|
50
|
+
return c.body(null, 204);
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Who a tenant publishes as. With GitHub sign-in configured, only signed-in GitHub accounts publish, under their
|
|
55
|
+
* login; without it (a self-hosted team server), a tenant publishes under its own id, an open server as "local".
|
|
56
|
+
*/
|
|
57
|
+
export function publisherFor(opts) {
|
|
58
|
+
return async (tenantId) => {
|
|
59
|
+
if (!opts.github)
|
|
60
|
+
return tenantId ?? "local";
|
|
61
|
+
if (!tenantId || !hasPublishers(opts.engine.store))
|
|
62
|
+
return undefined;
|
|
63
|
+
return (await opts.engine.store.getPublisher(tenantId))?.login;
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
//# sourceMappingURL=hub.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hub.js","sourceRoot":"","sources":["../../src/routes/hub.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,EAAE,aAAa,EAAuB,MAAM,kBAAkB,CAAC;AAEtE,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,OAAO,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AACjD,OAAO,EAAE,gBAAgB,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAC7D,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAErC,4CAA4C;AAC5C,MAAM,CAAC,MAAM,gBAAgB,GAAG,IAAI,GAAG,CAAC,CAAC,gBAAgB,EAAE,eAAe,CAAC,CAAC,CAAC;AAE7E,0EAA0E;AAC1E,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,QAAgB,EAAE,EAAE,CAAC,MAAM,QAAQ,EAAE,CAAC;AAEnE,MAAM,UAAU,WAAW,CAAC,GAAc,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,GAAG,GAAG,EAAE,CAAC,IAAI,CAAC,GAAG,EAAE,EAAE,MAAM,EAAE,SAAS,GAAG,gBAAgB,EAAE,MAAM,EAAc;IACpJ,MAAM,UAAU,GAAG,GAAmB,EAAE;QACtC,IAAI,CAAC,aAAa,CAAC,MAAM,CAAC,KAAK,CAAC;YAAE,MAAM,IAAI,aAAa,CAAC,GAAG,EAAE,EAAE,OAAO,EAAE,4CAA4C,EAAE,CAAC,CAAC;QAC1H,OAAO,MAAM,CAAC,KAAK,CAAC;IACtB,CAAC,CAAC;IAEF,GAAG,CAAC,GAAG,CAAC,gBAAgB,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,MAAM,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,UAAU,EAAE,MAAM,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,QAAQ,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;IAE7K,GAAG,CAAC,IAAI,CAAC,eAAe,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE;QACpC,IAAI,CAAC,MAAM;YAAE,MAAM,IAAI,aAAa,CAAC,GAAG,EAAE,EAAE,OAAO,EAAE,oEAAoE,EAAE,CAAC,CAAC;QAC7H,IAAI,CAAC,IAAI;YAAE,MAAM,IAAI,aAAa,CAAC,GAAG,EAAE,EAAE,OAAO,EAAE,4DAA4D,EAAE,CAAC,CAAC;QACnH,qFAAqF;QACrF,MAAM,OAAO,CAAC,MAAM,EAAE,KAAK,EAAE,SAAS,CAAC,CAAC,CAAC,EAAE,SAAS,CAAC,CAAC;QACtD,MAAM,EAAE,WAAW,EAAE,GAAG,CAAC,CAAC,MAAM,CAAC,EAAE,WAAW,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC;QACxG,MAAM,OAAO,GAAG,MAAM,iBAAiB,CAAC,WAAW,EAAE,MAAM,CAAC,CAAC;QAC7D,MAAM,QAAQ,GAAG,YAAY,CAAC,OAAO,CAAC,EAAE,CAAC,EAAE,EAAE,GAAG,IAAI,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC,WAAW,EAAE,CAAC;QAC9E,MAAM,QAAQ,GAAG,MAAM,UAAU,EAAE,CAAC,YAAY,CAAC,QAAQ,CAAC,CAAC;QAC3D,MAAM,UAAU,EAAE,CAAC,YAAY,CAAC,EAAE,QAAQ,EAAE,QAAQ,EAAE,OAAO,CAAC,EAAE,EAAE,KAAK,EAAE,OAAO,CAAC,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,SAAS,IAAI,EAAE,EAAE,SAAS,EAAE,EAAE,EAAE,CAAC,CAAC;QAC/I,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,SAAS,CAAC,QAAQ,EAAE,gBAAgB,OAAO,CAAC,KAAK,GAAG,CAAC,CAAC;QAC7E,4EAA4E;QAC5E,OAAO,CAAC,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,GAAG,CAAC,GAAG,EAAE,KAAK,EAAE,GAAG,CAAC,EAAE,EAAE,MAAM,EAAE,OAAO,CAAC,KAAK,EAAE,QAAQ,EAAE,EAAE,GAAG,CAAC,CAAC;IAC1F,CAAC,CAAC,CAAC;IAEH,GAAG,CAAC,GAAG,CAAC,YAAY,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE;QAChC,MAAM,QAAQ,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC;QAC3B,MAAM,SAAS,GAAG,QAAQ,IAAI,aAAa,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,MAAM,CAAC,KAAK,CAAC,YAAY,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;QAClH,OAAO,CAAC,CAAC,IAAI,CAAC,EAAE,QAAQ,EAAE,QAAQ,IAAI,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,KAAK,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,QAAQ,IAAI,OAAO,CAAC,EAAE,MAAM,EAAE,SAAS,CAAC,CAAC,CAAC,EAAE,EAAE,EAAE,SAAS,CAAC,QAAQ,EAAE,KAAK,EAAE,SAAS,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;IAClM,CAAC,CAAC,CAAC;IAEH,GAAG,CAAC,MAAM,CAAC,kBAAkB,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE;QACzC,IAAI,CAAC,IAAI;YAAE,MAAM,IAAI,aAAa,CAAC,GAAG,EAAE,EAAE,OAAO,EAAE,oCAAoC,EAAE,CAAC,CAAC;QAC3F,MAAM,QAAQ,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC;QAC3B,IAAI,CAAC,QAAQ,IAAI,CAAC,CAAC,MAAM,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,QAAQ,CAAC,CAAC;YAAE,MAAM,IAAI,aAAa,CAAC,GAAG,EAAE,EAAE,OAAO,EAAE,8BAA8B,EAAE,CAAC,CAAC;QACjJ,OAAO,CAAC,CAAC,IAAI,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;IAC3B,CAAC,CAAC,CAAC;AACL,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,YAAY,CAAC,IAA2C;IACtE,OAAO,KAAK,EAAE,QAA4B,EAA+B,EAAE;QACzE,IAAI,CAAC,IAAI,CAAC,MAAM;YAAE,OAAO,QAAQ,IAAI,OAAO,CAAC;QAC7C,IAAI,CAAC,QAAQ,IAAI,CAAC,aAAa,CAAC,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC;YAAE,OAAO,SAAS,CAAC;QACrE,OAAO,CAAC,MAAM,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,YAAY,CAAC,QAAQ,CAAC,CAAC,EAAE,KAAK,CAAC;IACjE,CAAC,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Immutable rubric versions kept in a project (docs/design/control-plane.md §4). A version is stored under the name
|
|
3
|
+
* services ask for; the server freezes the uploaded rubric again and refuses an id that does not match its content.
|
|
4
|
+
*/
|
|
5
|
+
import { CONTROL_LIST_LIMIT, CONTROL_VERSION_BYTES, controlName, NotFoundError, projectVersion, VersionUpload } from "@verdict-ai/core";
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
import { ApiError, bare, controlContext, projectNotFound, readJson } from "./shared.js";
|
|
8
|
+
/** A version as lists show it: everything but the rubric document. */
|
|
9
|
+
function listed(v) {
|
|
10
|
+
const { rubric: _rubric, ...rest } = bare(v);
|
|
11
|
+
return rest;
|
|
12
|
+
}
|
|
13
|
+
/** Registers version routes. */
|
|
14
|
+
export function registerProjectVersions(app, options) {
|
|
15
|
+
const { store, scope, allow, project, now } = controlContext(options);
|
|
16
|
+
const base = "/v1/projects/:project/rubrics";
|
|
17
|
+
const missing = (name, id, projectName) => new NotFoundError(`version ${id} of ${name} not found in project ${projectName}`);
|
|
18
|
+
app.post(`${base}/:name/versions`, async (c) => {
|
|
19
|
+
const s = scope(c), name = c.req.param("name");
|
|
20
|
+
await allow(c, "version.put", { project: s.project, name });
|
|
21
|
+
const upload = VersionUpload.parse(await readJson(c.req.raw, CONTROL_VERSION_BYTES));
|
|
22
|
+
const result = await store().putVersion(await projectVersion(upload, s, controlName(name, "rubric"), now));
|
|
23
|
+
if (result === "no_project")
|
|
24
|
+
throw projectNotFound(s.project);
|
|
25
|
+
return c.json({ version: bare(result.version), created: result.created }, result.created ? 201 : 200);
|
|
26
|
+
});
|
|
27
|
+
app.get(base, async (c) => {
|
|
28
|
+
await allow(c, "version.read", { project: c.req.param("project") });
|
|
29
|
+
return c.json({ data: await store().listRubricNames(await project(c)) });
|
|
30
|
+
});
|
|
31
|
+
app.get(`${base}/:name/versions`, async (c) => {
|
|
32
|
+
const name = c.req.param("name");
|
|
33
|
+
await allow(c, "version.read", { project: c.req.param("project"), name });
|
|
34
|
+
const { limit } = z.object({ limit: z.coerce.number().int().min(1).max(CONTROL_LIST_LIMIT).default(CONTROL_LIST_LIMIT) }).parse(c.req.query());
|
|
35
|
+
return c.json({ data: (await store().listVersions(await project(c), { name, limit })).map(listed) });
|
|
36
|
+
});
|
|
37
|
+
app.get(`${base}/:name/versions/:id`, async (c) => {
|
|
38
|
+
const s = scope(c), name = c.req.param("name"), id = c.req.param("id");
|
|
39
|
+
await allow(c, "version.read", { project: s.project, name, versionId: id });
|
|
40
|
+
const version = await store().getVersion(s, name, id);
|
|
41
|
+
if (!version)
|
|
42
|
+
throw missing(name, id, s.project);
|
|
43
|
+
return c.json(bare(version));
|
|
44
|
+
});
|
|
45
|
+
app.delete(`${base}/:name/versions/:id`, async (c) => {
|
|
46
|
+
const s = scope(c), name = c.req.param("name"), id = c.req.param("id");
|
|
47
|
+
await allow(c, "version.delete", { project: s.project, name, versionId: id });
|
|
48
|
+
const result = await store().deleteVersion(s, name, id);
|
|
49
|
+
if (result === "not_found")
|
|
50
|
+
throw missing(name, id, s.project);
|
|
51
|
+
if (result === "in_use")
|
|
52
|
+
throw new ApiError(409, "in_use", `an environment points at ${id}; promote another version of ${name} or remove the pointer first`);
|
|
53
|
+
return c.body(null, 204);
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
//# sourceMappingURL=project-versions.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"project-versions.js","sourceRoot":"","sources":["../../src/routes/project-versions.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Projects and environments of the control plane (docs/design/control-plane.md §3, §5.3). Authenticated and
|
|
3
|
+
* tenant-scoped; every handler asks `allow` before it touches the store.
|
|
4
|
+
*/
|
|
5
|
+
import { ConfirmRequest, fingerprint, NameRequest, NotFoundError, ValidationError } from "@verdict-ai/core";
|
|
6
|
+
import { HTTPException } from "hono/http-exception";
|
|
7
|
+
import { ApiError, bare, controlContext, projectNotFound, readJson, tenant } from "./shared.js";
|
|
8
|
+
/** Deleting takes the name twice: once in the URL, once in the body. */
|
|
9
|
+
async function confirmed(req, name, what) {
|
|
10
|
+
const { confirm } = ConfirmRequest.parse(await readJson(req));
|
|
11
|
+
if (confirm !== name)
|
|
12
|
+
throw new ValidationError(`to delete ${what} ${name}, send { "confirm": "${name}" }`);
|
|
13
|
+
}
|
|
14
|
+
/** The document a pointer names. A pointed-at version cannot be deleted, so a miss means the pointer just moved. */
|
|
15
|
+
async function rubricOf(store, scope, pointer) {
|
|
16
|
+
const version = await store.getVersion(scope, pointer.name, pointer.versionId);
|
|
17
|
+
if (!version)
|
|
18
|
+
throw new HTTPException(503, { message: `${pointer.name} changed while it was being read; ask again` });
|
|
19
|
+
return version.rubric;
|
|
20
|
+
}
|
|
21
|
+
/** Registers project and environment routes. */
|
|
22
|
+
export function registerProjects(app, options) {
|
|
23
|
+
const { store, allow, at } = controlContext(options);
|
|
24
|
+
app.post("/v1/projects", async (c) => {
|
|
25
|
+
const { name } = NameRequest.parse(await readJson(c.req.raw));
|
|
26
|
+
await allow(c, "project.create", { project: name });
|
|
27
|
+
const tenantId = tenant(c), project = { ...(tenantId === undefined ? {} : { tenantId }), name, createdAt: at() };
|
|
28
|
+
if (!(await store().createProject(project)))
|
|
29
|
+
throw new ApiError(409, "already_exists", `project ${name} already exists`);
|
|
30
|
+
return c.json(bare(project), 201);
|
|
31
|
+
});
|
|
32
|
+
app.get("/v1/projects", async (c) => {
|
|
33
|
+
await allow(c, "project.read");
|
|
34
|
+
return c.json({ data: (await store().listProjects(tenant(c))).map(bare) });
|
|
35
|
+
});
|
|
36
|
+
app.delete("/v1/projects/:project", async (c) => {
|
|
37
|
+
const name = c.req.param("project");
|
|
38
|
+
await allow(c, "project.delete", { project: name });
|
|
39
|
+
await confirmed(c.req.raw, name, "project");
|
|
40
|
+
if (!(await store().deleteProject(tenant(c), name)))
|
|
41
|
+
throw projectNotFound(name);
|
|
42
|
+
return c.body(null, 204);
|
|
43
|
+
});
|
|
44
|
+
registerEnvironments(app, options);
|
|
45
|
+
}
|
|
46
|
+
function registerEnvironments(app, options) {
|
|
47
|
+
const { store, scope, allow, project, at } = controlContext(options);
|
|
48
|
+
app.post("/v1/projects/:project/environments", async (c) => {
|
|
49
|
+
const { name } = NameRequest.parse(await readJson(c.req.raw));
|
|
50
|
+
const s = scope(c);
|
|
51
|
+
await allow(c, "environment.create", { project: s.project, environment: name });
|
|
52
|
+
const environment = { ...s, name, createdAt: at() };
|
|
53
|
+
const result = await store().createEnvironment(environment);
|
|
54
|
+
if (result === "no_project")
|
|
55
|
+
throw projectNotFound(s.project);
|
|
56
|
+
if (result === "exists")
|
|
57
|
+
throw new ApiError(409, "already_exists", `environment ${name} already exists in project ${s.project}`);
|
|
58
|
+
return c.json(bare(environment), 201);
|
|
59
|
+
});
|
|
60
|
+
app.get("/v1/projects/:project/environments", async (c) => {
|
|
61
|
+
await allow(c, "environment.read", { project: c.req.param("project") });
|
|
62
|
+
return c.json({ data: (await store().listEnvironments(await project(c))).map(bare) });
|
|
63
|
+
});
|
|
64
|
+
// What a service polls: `?include=rubrics` returns the documents too, so one answer holds everything an environment
|
|
65
|
+
// runs. Versions never change, so the pointers alone identify the answer: an unchanged environment answers 304.
|
|
66
|
+
app.get("/v1/projects/:project/environments/:environment", async (c) => {
|
|
67
|
+
const s = scope(c), name = c.req.param("environment"), withRubrics = c.req.query("include") === "rubrics";
|
|
68
|
+
await allow(c, "environment.read", { project: s.project, environment: name });
|
|
69
|
+
const environment = await store().getEnvironment(s, name);
|
|
70
|
+
if (!environment)
|
|
71
|
+
throw new NotFoundError(`environment ${name} not found in project ${s.project}`);
|
|
72
|
+
const pointers = (await store().listPointers(s, name)).map((p) => ({ name: p.name, versionId: p.versionId, updatedAt: p.updatedAt }));
|
|
73
|
+
const etag = `"${await fingerprint([environment.createdAt, withRubrics, pointers.map((p) => [p.name, p.versionId])])}"`;
|
|
74
|
+
c.header("ETag", etag);
|
|
75
|
+
c.header("Cache-Control", "private, no-cache");
|
|
76
|
+
if (c.req.header("if-none-match") === etag)
|
|
77
|
+
return c.body(null, 304);
|
|
78
|
+
if (!withRubrics)
|
|
79
|
+
return c.json({ ...bare(environment), pointers });
|
|
80
|
+
return c.json({ ...bare(environment), pointers: await Promise.all(pointers.map(async (p) => ({ ...p, rubric: await rubricOf(store(), s, p) }))) });
|
|
81
|
+
});
|
|
82
|
+
app.delete("/v1/projects/:project/environments/:environment", async (c) => {
|
|
83
|
+
const s = scope(c), name = c.req.param("environment");
|
|
84
|
+
await allow(c, "environment.delete", { project: s.project, environment: name });
|
|
85
|
+
await confirmed(c.req.raw, name, "environment");
|
|
86
|
+
if (!(await store().deleteEnvironment(s, name)))
|
|
87
|
+
throw new NotFoundError(`environment ${name} not found in project ${s.project}`);
|
|
88
|
+
return c.body(null, 204);
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
//# sourceMappingURL=projects.js.map
|