@verdict-ai/api 0.1.0 → 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/dist/app.d.ts +50 -4
- package/dist/app.js +63 -11
- package/dist/app.js.map +1 -1
- 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 +2 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/openapi.d.ts +355 -0
- package/dist/openapi.js +35 -4
- package/dist/openapi.js.map +1 -1
- package/dist/present.d.ts +3 -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 +1 -1
- package/dist/routes/admin.js +54 -8
- package/dist/routes/admin.js.map +1 -1
- 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/shared.d.ts +34 -0
- package/dist/routes/shared.js +73 -1
- package/dist/routes/shared.js.map +1 -1
- package/dist/server.js +12 -4
- package/dist/server.js.map +1 -1
- package/dist/worker.d.ts +20 -2
- package/dist/worker.js +23 -5
- package/dist/worker.js.map +1 -1
- package/package.json +3 -3
- package/wrangler.example.toml +18 -1
|
@@ -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":"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"}
|
|
@@ -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
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"projects.js","sourceRoot":"","sources":["../../src/routes/projects.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pointer changes and their history (docs/design/control-plane.md §5). A promotion names the version the caller saw;
|
|
3
|
+
* the store applies all of a request's changes or none, and a stale request is answered with where the pointers are.
|
|
4
|
+
*/
|
|
5
|
+
import { CONTROL_LIST_LIMIT, NotFoundError, planRollback, PromotionRequest, promotionEvents, RollbackRequest, } from "@verdict-ai/core";
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
import { ApiError, bare, controlContext, readJson } from "./shared.js";
|
|
8
|
+
const HistoryQuery = z.object({
|
|
9
|
+
environment: z.string().optional(), name: z.string().optional(),
|
|
10
|
+
limit: z.coerce.number().int().min(1).max(CONTROL_LIST_LIMIT).default(CONTROL_LIST_LIMIT),
|
|
11
|
+
});
|
|
12
|
+
const noEnvironment = (ctx) => new NotFoundError(`environment ${ctx.environment} not found in project ${ctx.scope.project}`);
|
|
13
|
+
/** Where the named pointers are now, for the answer to a request that saw them elsewhere. */
|
|
14
|
+
async function current(store, scope, environment, names) {
|
|
15
|
+
const pointers = await store.listPointers(scope, environment);
|
|
16
|
+
return names.map((name) => ({ name, versionId: pointers.find((p) => p.name === name)?.versionId ?? null }));
|
|
17
|
+
}
|
|
18
|
+
/** The error for a batch the store refused. */
|
|
19
|
+
async function refusal(store, reason, events, ctx) {
|
|
20
|
+
if (reason === "no_environment")
|
|
21
|
+
return noEnvironment(ctx);
|
|
22
|
+
if (reason === "unknown_version") {
|
|
23
|
+
for (const e of events) {
|
|
24
|
+
if (e.to !== null && !(await store.getVersion(ctx.scope, e.name, e.to)))
|
|
25
|
+
return new ApiError(400, "unknown_version", `version ${e.to} is not kept under ${e.name} in project ${ctx.scope.project}; upload it first`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
const now = await current(store, ctx.scope, ctx.environment, events.map((e) => e.name));
|
|
29
|
+
return new ApiError(409, "pointer_conflict", `${ctx.environment} changed since you read it; nothing was applied`, { current: now });
|
|
30
|
+
}
|
|
31
|
+
/** Registers promotion, rollback and history routes. */
|
|
32
|
+
export function registerPromotions(app, options) {
|
|
33
|
+
const { store, scope, allow, project, at } = controlContext(options);
|
|
34
|
+
const base = "/v1/projects/:project/environments/:environment";
|
|
35
|
+
app.post(`${base}/promotions`, async (c) => {
|
|
36
|
+
const request = PromotionRequest.parse(await readJson(c.req.raw));
|
|
37
|
+
const ctx = { scope: scope(c), environment: c.req.param("environment"), actor: c.get("actor"), at: at() };
|
|
38
|
+
for (const change of request.changes) {
|
|
39
|
+
await allow(c, change.versionId === null ? "pointer.unset" : "pointer.promote", { project: ctx.scope.project, environment: ctx.environment, name: change.name, versionId: change.versionId });
|
|
40
|
+
}
|
|
41
|
+
const events = promotionEvents(request, ctx);
|
|
42
|
+
const result = await store().applyPromotions(events);
|
|
43
|
+
if (!result.ok)
|
|
44
|
+
throw await refusal(store(), result.reason, events, ctx);
|
|
45
|
+
return c.json({ events: events.map(bare) });
|
|
46
|
+
});
|
|
47
|
+
app.post(`${base}/rollbacks`, async (c) => {
|
|
48
|
+
const request = RollbackRequest.parse(await readJson(c.req.raw));
|
|
49
|
+
const ctx = { scope: scope(c), environment: c.req.param("environment"), actor: c.get("actor"), at: at() };
|
|
50
|
+
await allow(c, "pointer.rollback", { project: ctx.scope.project, environment: ctx.environment, name: request.name });
|
|
51
|
+
const plan = await planRollback(store(), request, ctx);
|
|
52
|
+
if (!plan.ok) {
|
|
53
|
+
if (plan.reason === "no_environment")
|
|
54
|
+
throw noEnvironment(ctx);
|
|
55
|
+
if (plan.reason === "nothing_to_roll_back")
|
|
56
|
+
throw new ApiError(409, "nothing_to_roll_back", `${request.name} in ${ctx.environment} has no earlier version on record`);
|
|
57
|
+
throw new ApiError(409, "pointer_conflict", `${ctx.environment} changed since you read it; nothing was applied`, { current: [{ name: request.name, versionId: plan.current }] });
|
|
58
|
+
}
|
|
59
|
+
const result = await store().applyPromotions([plan.event]);
|
|
60
|
+
if (!result.ok && result.reason === "unknown_version")
|
|
61
|
+
throw new ApiError(409, "nothing_to_roll_back", `the earlier version ${plan.event.to} of ${request.name} was deleted`);
|
|
62
|
+
if (!result.ok)
|
|
63
|
+
throw await refusal(store(), result.reason, [plan.event], ctx);
|
|
64
|
+
return c.json({ event: bare(plan.event) });
|
|
65
|
+
});
|
|
66
|
+
app.get("/v1/projects/:project/history", async (c) => {
|
|
67
|
+
const q = HistoryQuery.parse(c.req.query());
|
|
68
|
+
await allow(c, "history.read", { project: c.req.param("project"), environment: q.environment, name: q.name });
|
|
69
|
+
return c.json({ data: (await store().listHistory(await project(c), q)).map(bare) });
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
//# sourceMappingURL=promotions.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"promotions.js","sourceRoot":"","sources":["../../src/routes/promotions.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public hub reads (docs/design/hub.md §9, §16.3): search, a card's versions and one publication, as JSON under
|
|
3
|
+
* `/v1/public/*` and as pages under `/hub`. No API key. These are the only store reads that are not tenant-scoped, and
|
|
4
|
+
* they read only the publications collection, through `publicView`, which drops the publishing tenant.
|
|
5
|
+
*/
|
|
6
|
+
import { type Publication, type PublicationStore, type PublicPublication, type PublicVersionEntry } from "@verdict-ai/core";
|
|
7
|
+
import { Hono } from "hono";
|
|
8
|
+
import { z } from "zod";
|
|
9
|
+
import type { AppOptions, Env } from "../app.js";
|
|
10
|
+
/** Public paths start with these; the tenant middleware lets them through without a key. */
|
|
11
|
+
export declare const isPublicHubPath: (path: string) => boolean;
|
|
12
|
+
declare const Search: z.ZodObject<{
|
|
13
|
+
q: z.ZodOptional<z.ZodString>;
|
|
14
|
+
domain: z.ZodOptional<z.ZodString>;
|
|
15
|
+
locale: z.ZodOptional<z.ZodString>;
|
|
16
|
+
type: z.ZodOptional<z.ZodEnum<{
|
|
17
|
+
choice: "choice";
|
|
18
|
+
score: "score";
|
|
19
|
+
noul: "noul";
|
|
20
|
+
}>>;
|
|
21
|
+
owner: z.ZodOptional<z.ZodString>;
|
|
22
|
+
limit: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
|
|
23
|
+
}, z.core.$strip>;
|
|
24
|
+
export type PublicSearch = z.infer<typeof Search>;
|
|
25
|
+
/** A withdrawn version answers 410: it existed and was taken down, which is different from never existing. */
|
|
26
|
+
export declare class WithdrawnError extends Error {
|
|
27
|
+
publication: Publication;
|
|
28
|
+
constructor(publication: Publication);
|
|
29
|
+
}
|
|
30
|
+
export declare function registerPublicHub(app: Hono<Env>, { engine, limits, clientKey }: Pick<AppOptions, "engine" | "limits" | "clientKey">): void;
|
|
31
|
+
/** The reads themselves, kept apart from HTTP so the JSON routes and the pages answer the same way. */
|
|
32
|
+
export declare function publicHub(store: () => PublicationStore): {
|
|
33
|
+
search(q: PublicSearch): Promise<Publication[]>;
|
|
34
|
+
/** A card at `versionId`, or at its newest live version, with every version it has had. */
|
|
35
|
+
card(owner: string, slug: string, versionId?: string): Promise<{
|
|
36
|
+
publication: PublicPublication;
|
|
37
|
+
versions: PublicVersionEntry[];
|
|
38
|
+
}>;
|
|
39
|
+
publication(id: string): Promise<PublicPublication>;
|
|
40
|
+
};
|
|
41
|
+
export {};
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public hub reads (docs/design/hub.md §9, §16.3): search, a card's versions and one publication, as JSON under
|
|
3
|
+
* `/v1/public/*` and as pages under `/hub`. No API key. These are the only store reads that are not tenant-scoped, and
|
|
4
|
+
* they read only the publications collection, through `publicView`, which drops the publishing tenant.
|
|
5
|
+
*/
|
|
6
|
+
import { hasPublications, latestPerRef, listingOf, NotFoundError, publicView, versionEntry, withdrawalNote, } from "@verdict-ai/core";
|
|
7
|
+
import { HTTPException } from "hono/http-exception";
|
|
8
|
+
import { z } from "zod";
|
|
9
|
+
import { cardPage, goneCardPage, missingPage, searchPage } from "../hub-pages.js";
|
|
10
|
+
import { defaultClientKey, enforce } from "../rate-limit.js";
|
|
11
|
+
/** Public paths start with these; the tenant middleware lets them through without a key. */
|
|
12
|
+
export const isPublicHubPath = (path) => path.startsWith("/v1/public/") || path === "/hub" || path.startsWith("/hub/");
|
|
13
|
+
const SEARCH_LIMIT = 50;
|
|
14
|
+
/** Enough rows to find the newest version of each card a page shows. */
|
|
15
|
+
const SCAN_LIMIT = 500;
|
|
16
|
+
const Search = z.object({
|
|
17
|
+
q: z.string().max(200).optional(), domain: z.string().max(60).optional(), locale: z.string().max(35).optional(),
|
|
18
|
+
type: z.enum(["score", "noul", "choice"]).optional(), owner: z.string().max(64).optional(),
|
|
19
|
+
limit: z.coerce.number().int().min(1).max(SEARCH_LIMIT).default(SEARCH_LIMIT),
|
|
20
|
+
});
|
|
21
|
+
/** A withdrawn version answers 410: it existed and was taken down, which is different from never existing. */
|
|
22
|
+
export class WithdrawnError extends Error {
|
|
23
|
+
publication;
|
|
24
|
+
constructor(publication) {
|
|
25
|
+
super(withdrawalNote(publication));
|
|
26
|
+
this.publication = publication;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
export function registerPublicHub(app, { engine, limits, clientKey = defaultClientKey }) {
|
|
30
|
+
const store = () => {
|
|
31
|
+
if (!hasPublications(engine.store))
|
|
32
|
+
throw new HTTPException(501, { message: "this server's store cannot keep publications" });
|
|
33
|
+
return engine.store;
|
|
34
|
+
};
|
|
35
|
+
const hub = publicHub(store);
|
|
36
|
+
// One check per request: in Hono "/hub/*" also matches "/hub", so the paths are tested here rather than as patterns.
|
|
37
|
+
app.use(async (c, next) => {
|
|
38
|
+
if (isPublicHubPath(c.req.path))
|
|
39
|
+
await enforce(limits?.read, clientKey(c), "hub read");
|
|
40
|
+
await next();
|
|
41
|
+
});
|
|
42
|
+
app.get("/v1/public/rubrics", async (c) => c.json({ data: (await hub.search(Search.parse(c.req.query()))).map(listingOf) }));
|
|
43
|
+
app.get("/v1/public/rubrics/:owner/:slug", async (c) => {
|
|
44
|
+
try {
|
|
45
|
+
return c.json(await hub.card(c.req.param("owner"), c.req.param("slug"), c.req.query("version")));
|
|
46
|
+
}
|
|
47
|
+
catch (e) {
|
|
48
|
+
return withdrawnJson(c, e);
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
app.get("/v1/public/publications/:id", async (c) => {
|
|
52
|
+
try {
|
|
53
|
+
return c.json(await hub.publication(c.req.param("id")));
|
|
54
|
+
}
|
|
55
|
+
catch (e) {
|
|
56
|
+
return withdrawnJson(c, e);
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
app.get("/hub", async (c) => {
|
|
60
|
+
const parsed = Search.safeParse(c.req.query());
|
|
61
|
+
const query = parsed.success ? parsed.data : Search.parse({});
|
|
62
|
+
return html(c, searchPage({ query, results: (await hub.search(query)).map(listingOf) }));
|
|
63
|
+
});
|
|
64
|
+
app.get("/hub/:owner/:slug", async (c) => {
|
|
65
|
+
const origin = new URL(c.req.url).origin;
|
|
66
|
+
try {
|
|
67
|
+
return html(c, cardPage({ origin, ...(await hub.card(c.req.param("owner"), c.req.param("slug"), c.req.query("version"))) }));
|
|
68
|
+
}
|
|
69
|
+
catch (e) {
|
|
70
|
+
if (e instanceof WithdrawnError)
|
|
71
|
+
return html(c, goneCardPage(e.publication, e.message), 410);
|
|
72
|
+
if (e instanceof NotFoundError)
|
|
73
|
+
return html(c, missingPage(e.message), 404);
|
|
74
|
+
throw e;
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
/** The reads themselves, kept apart from HTTP so the JSON routes and the pages answer the same way. */
|
|
79
|
+
export function publicHub(store) {
|
|
80
|
+
return {
|
|
81
|
+
async search(q) {
|
|
82
|
+
const query = { q: q.q || undefined, domain: q.domain || undefined, locale: q.locale || undefined, judgmentType: q.type, owner: q.owner || undefined, limit: SCAN_LIMIT };
|
|
83
|
+
return latestPerRef(await store().listPublications(query)).slice(0, q.limit);
|
|
84
|
+
},
|
|
85
|
+
/** A card at `versionId`, or at its newest live version, with every version it has had. */
|
|
86
|
+
async card(owner, slug, versionId) {
|
|
87
|
+
const all = await store().listPublications({ owner, slug, includeWithdrawn: true, limit: SCAN_LIMIT });
|
|
88
|
+
if (!all.length)
|
|
89
|
+
throw new NotFoundError(`no card ${owner}/${slug} on this hub`);
|
|
90
|
+
const chosen = versionId ? all.find((p) => p.version.id === versionId) : all.find((p) => !p.withdrawnAt) ?? all[0];
|
|
91
|
+
if (!chosen)
|
|
92
|
+
throw new NotFoundError(`${owner}/${slug} has no version ${versionId}`);
|
|
93
|
+
if (chosen.withdrawnAt)
|
|
94
|
+
throw new WithdrawnError(chosen);
|
|
95
|
+
return { publication: publicView(chosen), versions: all.map(versionEntry) };
|
|
96
|
+
},
|
|
97
|
+
async publication(id) {
|
|
98
|
+
const p = await store().getPublication(id);
|
|
99
|
+
if (!p)
|
|
100
|
+
throw new NotFoundError(`publication ${id} not found`);
|
|
101
|
+
if (p.withdrawnAt)
|
|
102
|
+
throw new WithdrawnError(p);
|
|
103
|
+
return publicView(p);
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
function withdrawnJson(c, e) {
|
|
108
|
+
if (!(e instanceof WithdrawnError))
|
|
109
|
+
throw e;
|
|
110
|
+
return c.json({ error: { type: e.publication.takedown ? "taken_down" : "withdrawn", message: e.message, withdrawnAt: e.publication.withdrawnAt } }, 410);
|
|
111
|
+
}
|
|
112
|
+
/** Pages carry no scripts and load nothing from elsewhere: everything shown is author-written text, escaped. */
|
|
113
|
+
function html(c, body, status = 200) {
|
|
114
|
+
c.header("content-security-policy", "default-src 'none'; style-src 'unsafe-inline'; base-uri 'none'; form-action 'self'; frame-ancestors 'none'");
|
|
115
|
+
c.header("x-content-type-options", "nosniff");
|
|
116
|
+
c.header("referrer-policy", "no-referrer");
|
|
117
|
+
return c.html(body, status);
|
|
118
|
+
}
|
|
119
|
+
//# sourceMappingURL=public-hub.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"public-hub.js","sourceRoot":"","sources":["../../src/routes/public-hub.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import type { AppOptions, Env } from "../app.js";
|
|
3
|
+
/** Registers publication routes; requests are validated by `buildPublication`, never trusted as documents. */
|
|
4
|
+
export declare function registerPublications(app: Hono<Env>, { engine, publisherOf, now, limits }: AppOptions): void;
|