@variance-authority/tribunal 0.1.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/CHANGELOG.md +100 -0
- package/LICENSE +21 -0
- package/README.md +506 -0
- package/dist/__fixtures__/review.d.ts +47 -0
- package/dist/__fixtures__/review.js +145 -0
- package/dist/__fixtures__/review.js.map +1 -0
- package/dist/bindings.d.ts +134 -0
- package/dist/bindings.js +88 -0
- package/dist/bindings.js.map +1 -0
- package/dist/changelog.d.ts +132 -0
- package/dist/changelog.js +129 -0
- package/dist/changelog.js.map +1 -0
- package/dist/history-queries.d.ts +82 -0
- package/dist/history-queries.js +200 -0
- package/dist/history-queries.js.map +1 -0
- package/dist/history-rows.d.ts +42 -0
- package/dist/history-rows.js +148 -0
- package/dist/history-rows.js.map +1 -0
- package/dist/history.d.ts +40 -0
- package/dist/history.js +186 -0
- package/dist/history.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -0
- package/dist/migration-steps.d.ts +32 -0
- package/dist/migration-steps.js +303 -0
- package/dist/migration-steps.js.map +1 -0
- package/dist/migrations.d.ts +58 -0
- package/dist/migrations.js +283 -0
- package/dist/migrations.js.map +1 -0
- package/dist/next.d.ts +94 -0
- package/dist/next.js +44 -0
- package/dist/next.js.map +1 -0
- package/dist/node/bin.d.ts +120 -0
- package/dist/node/bin.js +337 -0
- package/dist/node/bin.js.map +1 -0
- package/dist/node/bucket.d.ts +58 -0
- package/dist/node/bucket.js +142 -0
- package/dist/node/bucket.js.map +1 -0
- package/dist/node/database.d.ts +33 -0
- package/dist/node/database.js +157 -0
- package/dist/node/database.js.map +1 -0
- package/dist/node/index.d.ts +43 -0
- package/dist/node/index.js +43 -0
- package/dist/node/index.js.map +1 -0
- package/dist/node/serve.d.ts +81 -0
- package/dist/node/serve.js +193 -0
- package/dist/node/serve.js.map +1 -0
- package/dist/node/ui-assets.d.ts +47 -0
- package/dist/node/ui-assets.js +60 -0
- package/dist/node/ui-assets.js.map +1 -0
- package/dist/review-ingest.d.ts +24 -0
- package/dist/review-ingest.js +184 -0
- package/dist/review-ingest.js.map +1 -0
- package/dist/review-read.d.ts +87 -0
- package/dist/review-read.js +361 -0
- package/dist/review-read.js.map +1 -0
- package/dist/review-rows.d.ts +30 -0
- package/dist/review-rows.js +67 -0
- package/dist/review-rows.js.map +1 -0
- package/dist/review-types.d.ts +414 -0
- package/dist/review-types.js +2 -0
- package/dist/review-types.js.map +1 -0
- package/dist/review-write.d.ts +35 -0
- package/dist/review-write.js +86 -0
- package/dist/review-write.js.map +1 -0
- package/dist/review.d.ts +53 -0
- package/dist/review.fixtures.d.ts +47 -0
- package/dist/review.fixtures.js +145 -0
- package/dist/review.fixtures.js.map +1 -0
- package/dist/review.js +245 -0
- package/dist/review.js.map +1 -0
- package/dist/schema.d.ts +36 -0
- package/dist/schema.js +39 -0
- package/dist/schema.js.map +1 -0
- package/dist/store.d.ts +58 -0
- package/dist/store.js +278 -0
- package/dist/store.js.map +1 -0
- package/dist/testing.d.ts +38 -0
- package/dist/testing.js +92 -0
- package/dist/testing.js.map +1 -0
- package/dist/ui/__probe.d.ts +3 -0
- package/dist/ui/__probe.js +5 -0
- package/dist/ui/__probe.js.map +1 -0
- package/dist/ui/attribution.d.ts +46 -0
- package/dist/ui/attribution.js +79 -0
- package/dist/ui/attribution.js.map +1 -0
- package/dist/ui/because.d.ts +51 -0
- package/dist/ui/because.js +52 -0
- package/dist/ui/because.js.map +1 -0
- package/dist/ui/browser-entry.d.ts +2 -0
- package/dist/ui/browser-entry.js +34 -0
- package/dist/ui/browser-entry.js.map +1 -0
- package/dist/ui/builds.d.ts +43 -0
- package/dist/ui/builds.js +79 -0
- package/dist/ui/builds.js.map +1 -0
- package/dist/ui/carried.d.ts +73 -0
- package/dist/ui/carried.js +63 -0
- package/dist/ui/carried.js.map +1 -0
- package/dist/ui/cause.d.ts +73 -0
- package/dist/ui/cause.js +133 -0
- package/dist/ui/cause.js.map +1 -0
- package/dist/ui/change-story.d.ts +153 -0
- package/dist/ui/change-story.js +253 -0
- package/dist/ui/change-story.js.map +1 -0
- package/dist/ui/change.d.ts +51 -0
- package/dist/ui/change.js +125 -0
- package/dist/ui/change.js.map +1 -0
- package/dist/ui/client.d.ts +74 -0
- package/dist/ui/client.js +88 -0
- package/dist/ui/client.js.map +1 -0
- package/dist/ui/consumers.d.ts +80 -0
- package/dist/ui/consumers.js +125 -0
- package/dist/ui/consumers.js.map +1 -0
- package/dist/ui/control.d.ts +63 -0
- package/dist/ui/control.js +39 -0
- package/dist/ui/control.js.map +1 -0
- package/dist/ui/crossing.d.ts +55 -0
- package/dist/ui/crossing.js +74 -0
- package/dist/ui/crossing.js.map +1 -0
- package/dist/ui/declarations.d.ts +36 -0
- package/dist/ui/declarations.js +73 -0
- package/dist/ui/declarations.js.map +1 -0
- package/dist/ui/distance.d.ts +154 -0
- package/dist/ui/distance.js +180 -0
- package/dist/ui/distance.js.map +1 -0
- package/dist/ui/divergence.d.ts +40 -0
- package/dist/ui/divergence.js +176 -0
- package/dist/ui/divergence.js.map +1 -0
- package/dist/ui/docket.d.ts +39 -0
- package/dist/ui/docket.js +172 -0
- package/dist/ui/docket.js.map +1 -0
- package/dist/ui/findings.d.ts +42 -0
- package/dist/ui/findings.js +110 -0
- package/dist/ui/findings.js.map +1 -0
- package/dist/ui/foreseen.d.ts +55 -0
- package/dist/ui/foreseen.js +91 -0
- package/dist/ui/foreseen.js.map +1 -0
- package/dist/ui/glance.d.ts +101 -0
- package/dist/ui/glance.js +101 -0
- package/dist/ui/glance.js.map +1 -0
- package/dist/ui/grouping.d.ts +158 -0
- package/dist/ui/grouping.js +187 -0
- package/dist/ui/grouping.js.map +1 -0
- package/dist/ui/handed.d.ts +60 -0
- package/dist/ui/handed.js +122 -0
- package/dist/ui/handed.js.map +1 -0
- package/dist/ui/history.d.ts +88 -0
- package/dist/ui/history.js +170 -0
- package/dist/ui/history.js.map +1 -0
- package/dist/ui/holding.d.ts +84 -0
- package/dist/ui/holding.js +90 -0
- package/dist/ui/holding.js.map +1 -0
- package/dist/ui/impact.d.ts +31 -0
- package/dist/ui/impact.js +132 -0
- package/dist/ui/impact.js.map +1 -0
- package/dist/ui/index.d.ts +21 -0
- package/dist/ui/index.js +21 -0
- package/dist/ui/index.js.map +1 -0
- package/dist/ui/lead.d.ts +35 -0
- package/dist/ui/lead.js +34 -0
- package/dist/ui/lead.js.map +1 -0
- package/dist/ui/look.d.ts +109 -0
- package/dist/ui/look.js +248 -0
- package/dist/ui/look.js.map +1 -0
- package/dist/ui/mark.d.ts +11 -0
- package/dist/ui/mark.js +13 -0
- package/dist/ui/mark.js.map +1 -0
- package/dist/ui/moved.d.ts +121 -0
- package/dist/ui/moved.js +143 -0
- package/dist/ui/moved.js.map +1 -0
- package/dist/ui/navigation.d.ts +26 -0
- package/dist/ui/navigation.js +53 -0
- package/dist/ui/navigation.js.map +1 -0
- package/dist/ui/order.d.ts +113 -0
- package/dist/ui/order.js +226 -0
- package/dist/ui/order.js.map +1 -0
- package/dist/ui/origins.d.ts +42 -0
- package/dist/ui/origins.js +149 -0
- package/dist/ui/origins.js.map +1 -0
- package/dist/ui/outcome.d.ts +93 -0
- package/dist/ui/outcome.js +162 -0
- package/dist/ui/outcome.js.map +1 -0
- package/dist/ui/parted.d.ts +68 -0
- package/dist/ui/parted.js +101 -0
- package/dist/ui/parted.js.map +1 -0
- package/dist/ui/rail.d.ts +31 -0
- package/dist/ui/rail.js +21 -0
- package/dist/ui/rail.js.map +1 -0
- package/dist/ui/reach.d.ts +66 -0
- package/dist/ui/reach.js +185 -0
- package/dist/ui/reach.js.map +1 -0
- package/dist/ui/regions.d.ts +82 -0
- package/dist/ui/regions.js +67 -0
- package/dist/ui/regions.js.map +1 -0
- package/dist/ui/review.bundle.js +69 -0
- package/dist/ui/review.d.ts +97 -0
- package/dist/ui/review.js +39 -0
- package/dist/ui/review.js.map +1 -0
- package/dist/ui/root.d.ts +72 -0
- package/dist/ui/root.js +102 -0
- package/dist/ui/root.js.map +1 -0
- package/dist/ui/route.d.ts +95 -0
- package/dist/ui/route.js +114 -0
- package/dist/ui/route.js.map +1 -0
- package/dist/ui/run.d.ts +26 -0
- package/dist/ui/run.js +12 -0
- package/dist/ui/run.js.map +1 -0
- package/dist/ui/sense.d.ts +187 -0
- package/dist/ui/sense.js +222 -0
- package/dist/ui/sense.js.map +1 -0
- package/dist/ui/settled.d.ts +43 -0
- package/dist/ui/settled.js +59 -0
- package/dist/ui/settled.js.map +1 -0
- package/dist/ui/shell.d.ts +76 -0
- package/dist/ui/shell.js +65 -0
- package/dist/ui/shell.js.map +1 -0
- package/dist/ui/shift.d.ts +52 -0
- package/dist/ui/shift.js +98 -0
- package/dist/ui/shift.js.map +1 -0
- package/dist/ui/styles-docket.d.ts +18 -0
- package/dist/ui/styles-docket.js +281 -0
- package/dist/ui/styles-docket.js.map +1 -0
- package/dist/ui/styles-stage.d.ts +18 -0
- package/dist/ui/styles-stage.js +92 -0
- package/dist/ui/styles-stage.js.map +1 -0
- package/dist/ui/styles.d.ts +12 -0
- package/dist/ui/styles.js +482 -0
- package/dist/ui/styles.js.map +1 -0
- package/dist/ui/subject.d.ts +36 -0
- package/dist/ui/subject.js +138 -0
- package/dist/ui/subject.js.map +1 -0
- package/dist/ui/text.d.ts +129 -0
- package/dist/ui/text.js +164 -0
- package/dist/ui/text.js.map +1 -0
- package/dist/ui/variations.d.ts +37 -0
- package/dist/ui/variations.js +60 -0
- package/dist/ui/variations.js.map +1 -0
- package/dist/ui/viewer.d.ts +67 -0
- package/dist/ui/viewer.js +246 -0
- package/dist/ui/viewer.js.map +1 -0
- package/dist/worker-auth.d.ts +48 -0
- package/dist/worker-auth.js +74 -0
- package/dist/worker-auth.js.map +1 -0
- package/dist/worker-entry.d.ts +58 -0
- package/dist/worker-entry.js +36 -0
- package/dist/worker-entry.js.map +1 -0
- package/dist/worker-http.d.ts +39 -0
- package/dist/worker-http.js +111 -0
- package/dist/worker-http.js.map +1 -0
- package/dist/worker-input.d.ts +81 -0
- package/dist/worker-input.js +296 -0
- package/dist/worker-input.js.map +1 -0
- package/dist/worker.d.ts +91 -0
- package/dist/worker.js +287 -0
- package/dist/worker.js.map +1 -0
- package/mark.svg +30 -0
- package/migrations/0001_initial.sql +193 -0
- package/migrations/0002_changelog.sql +30 -0
- package/migrations/0003_accessibility.sql +8 -0
- package/migrations/0004_variations.sql +19 -0
- package/migrations/0005_reach.sql +26 -0
- package/migrations/0006_baseline-size.sql +7 -0
- package/migrations/0007_declarations.sql +7 -0
- package/migrations/0008_green-because.sql +7 -0
- package/migrations/0009_moved-bands.sql +6 -0
- package/migrations/0010_composition.sql +15 -0
- package/migrations/0011_movements.sql +21 -0
- package/migrations/0012_control-size.sql +6 -0
- package/package.json +110 -0
- package/wrangler.jsonc +45 -0
package/dist/worker.js
ADDED
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
import { APPROVALS_PATH, CHURN_PATH, CURRENT_PATH, FLAKINESS_PATH, LAST_CHANGED_PATH, OBSERVATIONS_PATH, REACH_PATH, VALUE_JOURNEY_PATH, } from '@variance-authority/history';
|
|
2
|
+
import { RasterStoreError, rasterFrom } from '@variance-authority/raster';
|
|
3
|
+
import { HistoryWriteConflict, churnFrom, currentFrom, flakinessFrom, journeyFrom, lastChangedFrom, reachFrom, } from '@variance-authority/server';
|
|
4
|
+
import { createD1Backend } from './history.js';
|
|
5
|
+
import { ReviewError, createReviewStore } from './review.js';
|
|
6
|
+
import { createBucketStore } from './store.js';
|
|
7
|
+
import { UNAUTHENTICATED, grant, refuseWeakTokens, requires } from './worker-auth.js';
|
|
8
|
+
import { BadRequest, Forbidden, MethodNotAllowed, asRecordBody, count, json, optional, requireMethod, required, string, } from './worker-http.js';
|
|
9
|
+
import { asApprovals, asBand, asBuildIngest, asCurrentRequest, asDecision, asIdentity, asKey, asRecordRequest, windowOf, } from './worker-input.js';
|
|
10
|
+
/**
|
|
11
|
+
* The baseline-store routes, restated.
|
|
12
|
+
*
|
|
13
|
+
* They are `@variance-authority/remote`'s and must stay byte-identical to them —
|
|
14
|
+
* a client and a server disagreeing about a URL is the class of failure that
|
|
15
|
+
* presents as a verdict. They are *restated* rather than imported because that
|
|
16
|
+
* package's entrypoint pulls in `node:http` to serve them, and a Worker bundle
|
|
17
|
+
* that drags a Node HTTP server in to read five string constants has acquired a
|
|
18
|
+
* dependency on a runtime it is not running on.
|
|
19
|
+
*
|
|
20
|
+
* The equality is a test, not a hope: `worker.test.ts` imports the constants from
|
|
21
|
+
* `remote` and asserts these against them, so drift fails the suite rather than a
|
|
22
|
+
* deployment.
|
|
23
|
+
*/
|
|
24
|
+
const BASELINE_FIND_PATH = '/baseline/find';
|
|
25
|
+
const BASELINE_DESCRIBE_PATH = '/baseline/describe';
|
|
26
|
+
const BASELINE_PUT_PATH = '/baseline/put';
|
|
27
|
+
const CACHE_FIND_PATH = '/cache/find';
|
|
28
|
+
const CACHE_PUT_PATH = '/cache/put';
|
|
29
|
+
/**
|
|
30
|
+
* Build the service over a database, a bucket and two tokens.
|
|
31
|
+
*
|
|
32
|
+
* Refuses a token under 16 characters and refuses two identical ones, here
|
|
33
|
+
* rather than on the first request: a deployment whose ingest token also
|
|
34
|
+
* promotes baselines is a misconfiguration that would otherwise be discovered by
|
|
35
|
+
* the build log that used it.
|
|
36
|
+
*/
|
|
37
|
+
export function createTribunal(options) {
|
|
38
|
+
refuseWeakTokens(options);
|
|
39
|
+
const baselines = createBucketStore(options);
|
|
40
|
+
const history = createD1Backend(options.db);
|
|
41
|
+
const review = createReviewStore(options);
|
|
42
|
+
const retentionDays = options.retentionDays ?? 30;
|
|
43
|
+
return {
|
|
44
|
+
async fetch(request) {
|
|
45
|
+
const granted = await grant(request, options);
|
|
46
|
+
if (granted === null) {
|
|
47
|
+
return json(401, { error: UNAUTHENTICATED }, { 'www-authenticate': 'Bearer' });
|
|
48
|
+
}
|
|
49
|
+
let url;
|
|
50
|
+
try {
|
|
51
|
+
url = new URL(request.url);
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return json(400, { error: `"${request.url}" is not a request URL` });
|
|
55
|
+
}
|
|
56
|
+
try {
|
|
57
|
+
return await route({ baselines, history, review, retentionDays }, granted, url, request);
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
if (error instanceof BadRequest)
|
|
61
|
+
return json(400, { error: error.message });
|
|
62
|
+
if (error instanceof Forbidden)
|
|
63
|
+
return json(403, { error: error.message });
|
|
64
|
+
if (error instanceof MethodNotAllowed) {
|
|
65
|
+
return json(405, { error: error.message }, { allow: error.allow });
|
|
66
|
+
}
|
|
67
|
+
if (error instanceof HistoryWriteConflict)
|
|
68
|
+
return json(409, { error: error.message });
|
|
69
|
+
if (error instanceof ReviewError)
|
|
70
|
+
return json(422, { error: error.message });
|
|
71
|
+
// Reported as a failure and never as an empty answer. Every client in
|
|
72
|
+
// this project turns a non-2xx into a thrown error precisely so that a
|
|
73
|
+
// broken service cannot become the sentence "no baseline" or "nothing has
|
|
74
|
+
// drifted".
|
|
75
|
+
return json(500, { error: error instanceof Error ? error.message : String(error) });
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
async function route(surfaces, granted, url, request) {
|
|
81
|
+
const path = url.pathname;
|
|
82
|
+
// ------------------------------------------------------------- baselines
|
|
83
|
+
if (path === BASELINE_FIND_PATH) {
|
|
84
|
+
requires(granted, 'ingest', path);
|
|
85
|
+
requireMethod(request, 'POST');
|
|
86
|
+
const body = await asRecordBody(request);
|
|
87
|
+
const found = await surfaces.baselines.find(asKey(body['key']), asIdentity(body['identity']));
|
|
88
|
+
return json(200, { found });
|
|
89
|
+
}
|
|
90
|
+
if (path === BASELINE_DESCRIBE_PATH) {
|
|
91
|
+
requires(granted, 'ingest', path);
|
|
92
|
+
requireMethod(request, 'POST');
|
|
93
|
+
const body = await asRecordBody(request);
|
|
94
|
+
const described = await surfaces.baselines.describe(asKey(body['key']), asIdentity(body['identity']));
|
|
95
|
+
return json(200, { described });
|
|
96
|
+
}
|
|
97
|
+
if (path === BASELINE_PUT_PATH) {
|
|
98
|
+
requires(granted, 'ingest', path);
|
|
99
|
+
requireMethod(request, 'POST');
|
|
100
|
+
const body = await asRecordBody(request);
|
|
101
|
+
const raster = rasterFrom(body['raster']);
|
|
102
|
+
if (raster === null)
|
|
103
|
+
throw new BadRequest('`raster` is not a raster');
|
|
104
|
+
await surfaces.baselines.put(asKey(body['key']), raster);
|
|
105
|
+
return json(200, { ok: true });
|
|
106
|
+
}
|
|
107
|
+
if (path === CACHE_FIND_PATH) {
|
|
108
|
+
requires(granted, 'ingest', path);
|
|
109
|
+
requireMethod(request, 'POST');
|
|
110
|
+
const body = await asRecordBody(request);
|
|
111
|
+
const digest = body['digest'];
|
|
112
|
+
if (typeof digest !== 'string')
|
|
113
|
+
throw new BadRequest('`digest` must be a string');
|
|
114
|
+
const raster = await surfaces.baselines.renderCache.get(digest, asIdentity(body['identity']));
|
|
115
|
+
return json(200, { raster });
|
|
116
|
+
}
|
|
117
|
+
if (path === CACHE_PUT_PATH) {
|
|
118
|
+
requires(granted, 'ingest', path);
|
|
119
|
+
requireMethod(request, 'POST');
|
|
120
|
+
const body = await asRecordBody(request);
|
|
121
|
+
const raster = rasterFrom(body['raster']);
|
|
122
|
+
if (raster === null)
|
|
123
|
+
throw new BadRequest('`raster` is not a raster');
|
|
124
|
+
await surfaces.baselines.renderCache.put(raster);
|
|
125
|
+
return json(200, { ok: true });
|
|
126
|
+
}
|
|
127
|
+
// --------------------------------------------------------------- history
|
|
128
|
+
const project = optional(url, 'project');
|
|
129
|
+
if (path === OBSERVATIONS_PATH) {
|
|
130
|
+
requires(granted, 'ingest', path);
|
|
131
|
+
requireMethod(request, 'POST');
|
|
132
|
+
const write = await asRecordRequest(request);
|
|
133
|
+
await surfaces.history.append(write.run, write.observations, write.tokens, write.instabilities);
|
|
134
|
+
// 204: the write left nothing to say, and the client treats any body on this
|
|
135
|
+
// route as a shape error rather than guessing at it.
|
|
136
|
+
return new Response(null, { status: 204 });
|
|
137
|
+
}
|
|
138
|
+
if (path === APPROVALS_PATH) {
|
|
139
|
+
requires(granted, 'ingest', path);
|
|
140
|
+
requireMethod(request, 'POST');
|
|
141
|
+
await surfaces.history.appendApprovals(await asApprovals(request));
|
|
142
|
+
return new Response(null, { status: 204 });
|
|
143
|
+
}
|
|
144
|
+
if (path === CURRENT_PATH) {
|
|
145
|
+
// A read, and still a POST, and still the ingest capability. Its argument is
|
|
146
|
+
// a subject list — three hundred subject ids in a query string is a 414 from
|
|
147
|
+
// a proxy nobody configured — and its caller is a run deciding what to write,
|
|
148
|
+
// not a person deciding what to ship (`CurrentRequest`).
|
|
149
|
+
requires(granted, 'ingest', path);
|
|
150
|
+
requireMethod(request, 'POST');
|
|
151
|
+
return json(200, await currentFrom(surfaces.history, project, await asCurrentRequest(request)));
|
|
152
|
+
}
|
|
153
|
+
if (path === LAST_CHANGED_PATH) {
|
|
154
|
+
readable(granted, path);
|
|
155
|
+
requireMethod(request, 'GET');
|
|
156
|
+
const band = optional(url, 'band');
|
|
157
|
+
return json(200, {
|
|
158
|
+
observation: await lastChangedFrom(surfaces.history, project, required(url, 'subject'), required(url, 'component'), band === undefined ? undefined : asBand(band, 'the `band` parameter')),
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
if (path === CHURN_PATH) {
|
|
162
|
+
readable(granted, path);
|
|
163
|
+
requireMethod(request, 'GET');
|
|
164
|
+
return json(200, await churnFrom(surfaces.history, project, required(url, 'component'), windowOf(url)));
|
|
165
|
+
}
|
|
166
|
+
if (path === VALUE_JOURNEY_PATH) {
|
|
167
|
+
readable(granted, path);
|
|
168
|
+
requireMethod(request, 'GET');
|
|
169
|
+
return json(200, await journeyFrom(surfaces.history, project, required(url, 'token'), windowOf(url)));
|
|
170
|
+
}
|
|
171
|
+
if (path === REACH_PATH) {
|
|
172
|
+
readable(granted, path);
|
|
173
|
+
requireMethod(request, 'GET');
|
|
174
|
+
return json(200, await reachFrom(surfaces.history, project, required(url, 'component'), windowOf(url)));
|
|
175
|
+
}
|
|
176
|
+
if (path === FLAKINESS_PATH) {
|
|
177
|
+
readable(granted, path);
|
|
178
|
+
requireMethod(request, 'GET');
|
|
179
|
+
return json(200, await flakinessFrom(surfaces.history, project, required(url, 'subject'), windowOf(url)));
|
|
180
|
+
}
|
|
181
|
+
// ---------------------------------------------------------------- review
|
|
182
|
+
if (path === '/review/builds' && request.method === 'POST') {
|
|
183
|
+
// The one review route the ingest token owns, and the only one it owns:
|
|
184
|
+
// posting a build is what CI does, and everything else on this surface is
|
|
185
|
+
// what a person does.
|
|
186
|
+
requires(granted, 'ingest', path);
|
|
187
|
+
await surfaces.review.ingest(await asBuildIngest(request));
|
|
188
|
+
return json(201, { ok: true });
|
|
189
|
+
}
|
|
190
|
+
if (path === '/review/builds') {
|
|
191
|
+
requires(granted, 'review', path);
|
|
192
|
+
requireMethod(request, 'GET');
|
|
193
|
+
const limit = optional(url, 'limit');
|
|
194
|
+
return json(200, { builds: await surfaces.review.builds(limit === undefined ? undefined : count(limit)) });
|
|
195
|
+
}
|
|
196
|
+
if (path === '/review/changelog') {
|
|
197
|
+
requires(granted, 'review', path);
|
|
198
|
+
requireMethod(request, 'GET');
|
|
199
|
+
const limit = optional(url, 'limit');
|
|
200
|
+
return json(200, await surfaces.review.changelog({
|
|
201
|
+
...(optional(url, 'component') !== undefined
|
|
202
|
+
? { component: required(url, 'component') }
|
|
203
|
+
: {}),
|
|
204
|
+
...(optional(url, 'subject') !== undefined ? { subject: required(url, 'subject') } : {}),
|
|
205
|
+
...(optional(url, 'since') !== undefined ? { since: required(url, 'since') } : {}),
|
|
206
|
+
...(limit !== undefined ? { limit: count(limit) } : {}),
|
|
207
|
+
}));
|
|
208
|
+
}
|
|
209
|
+
if (path === '/review/sweep') {
|
|
210
|
+
requires(granted, 'review', path);
|
|
211
|
+
requireMethod(request, 'POST');
|
|
212
|
+
const days = optional(url, 'days');
|
|
213
|
+
// Reported rather than performed silently. An operator who cannot see what a
|
|
214
|
+
// sweep removed cannot tell a working retention policy from one deleting a
|
|
215
|
+
// build a day.
|
|
216
|
+
return json(200, await surfaces.review.sweep(days === undefined ? surfaces.retentionDays : count(days)));
|
|
217
|
+
}
|
|
218
|
+
const build = /^\/review\/builds\/([^/]+)$/.exec(path);
|
|
219
|
+
if (build?.[1] !== undefined) {
|
|
220
|
+
requires(granted, 'review', path);
|
|
221
|
+
requireMethod(request, 'GET');
|
|
222
|
+
const detail = await surfaces.review.build(decodeURIComponent(build[1]));
|
|
223
|
+
return detail === null
|
|
224
|
+
? json(404, { error: `no build "${decodeURIComponent(build[1])}" in this project` })
|
|
225
|
+
: json(200, detail);
|
|
226
|
+
}
|
|
227
|
+
const decision = /^\/review\/builds\/([^/]+)\/subjects\/([^/]+)\/decision$/.exec(path);
|
|
228
|
+
if (decision?.[1] !== undefined && decision[2] !== undefined) {
|
|
229
|
+
requires(granted, 'review', path);
|
|
230
|
+
requireMethod(request, 'POST');
|
|
231
|
+
const body = await asRecordBody(request);
|
|
232
|
+
return json(200, await surfaces.review.decide({
|
|
233
|
+
build: decodeURIComponent(decision[1]),
|
|
234
|
+
subject: decodeURIComponent(decision[2]),
|
|
235
|
+
decision: asDecision(body['decision']),
|
|
236
|
+
by: string(body, 'by', 'the decision'),
|
|
237
|
+
...(typeof body['note'] === 'string' && body['note'] !== '' ? { note: body['note'] } : {}),
|
|
238
|
+
}));
|
|
239
|
+
}
|
|
240
|
+
const image = /^\/review\/builds\/([^/]+)\/subjects\/([^/]+)\/(before|after|diff)\.png$/.exec(path);
|
|
241
|
+
if (image?.[1] !== undefined && image[2] !== undefined && image[3] !== undefined) {
|
|
242
|
+
requires(granted, 'review', path);
|
|
243
|
+
requireMethod(request, 'GET');
|
|
244
|
+
const bytes = await surfaces.review.image(decodeURIComponent(image[1]), decodeURIComponent(image[2]), image[3]);
|
|
245
|
+
if (bytes === null)
|
|
246
|
+
return json(404, { error: 'this build kept no such image' });
|
|
247
|
+
return new Response(bytes, {
|
|
248
|
+
headers: {
|
|
249
|
+
'content-type': 'image/png',
|
|
250
|
+
// A build's images never change: the row is written once and the object
|
|
251
|
+
// with it. Anything a reviewer reloads is fetched again for nothing.
|
|
252
|
+
'cache-control': 'private, max-age=31536000, immutable',
|
|
253
|
+
},
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
return json(404, {
|
|
257
|
+
error: `no route for ${request.method} ${path}. This deployment answers the baseline routes ` +
|
|
258
|
+
`(${BASELINE_FIND_PATH}, ${BASELINE_DESCRIBE_PATH}, ${BASELINE_PUT_PATH}, ` +
|
|
259
|
+
`${CACHE_FIND_PATH}, ${CACHE_PUT_PATH}), the history routes (${OBSERVATIONS_PATH}, ` +
|
|
260
|
+
`${APPROVALS_PATH}, ${CURRENT_PATH}, ${LAST_CHANGED_PATH}, ${CHURN_PATH}, ` +
|
|
261
|
+
`${FLAKINESS_PATH}, ${VALUE_JOURNEY_PATH}, ${REACH_PATH}) and /review/builds and ` +
|
|
262
|
+
'/review/changelog. A path from a different API version is a client and a service that ' +
|
|
263
|
+
'disagree about a recorded shape',
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* The history routes that answer a question rather than record one.
|
|
268
|
+
*
|
|
269
|
+
* Either capability may ask. The split everywhere else in this file is *who is
|
|
270
|
+
* allowed to write*, and these five write nothing: churn, reach, flakiness, the
|
|
271
|
+
* value journey and the last change are derived from rows already recorded. A
|
|
272
|
+
* reviewer looking at a build needs exactly these to know whether the difference
|
|
273
|
+
* in front of them is the third this week or the first this year, and the browser
|
|
274
|
+
* that draws that page holds the review token — so refusing them here would mean
|
|
275
|
+
* a review surface that can approve a change it cannot put in context.
|
|
276
|
+
*
|
|
277
|
+
* It stays a rule with a name rather than an omitted check, because the next
|
|
278
|
+
* history route added is a write far more often than it is a read, and the
|
|
279
|
+
* default has to be the strict one.
|
|
280
|
+
*/
|
|
281
|
+
function readable(granted, path) {
|
|
282
|
+
if (granted !== 'ingest')
|
|
283
|
+
requires(granted, 'review', path);
|
|
284
|
+
}
|
|
285
|
+
/** Re-exported so a Worker entry can recognise a store failure without a second import. */
|
|
286
|
+
export { RasterStoreError };
|
|
287
|
+
//# sourceMappingURL=worker.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"worker.js","sourceRoot":"","sources":["../src/worker.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Digest } from '@variance-authority/core';\nimport {\n APPROVALS_PATH,\n CHURN_PATH,\n CURRENT_PATH,\n FLAKINESS_PATH,\n LAST_CHANGED_PATH,\n OBSERVATIONS_PATH,\n REACH_PATH,\n VALUE_JOURNEY_PATH,\n} from '@variance-authority/history';\nimport { RasterStoreError, rasterFrom } from '@variance-authority/raster';\nimport {\n HistoryWriteConflict,\n churnFrom,\n currentFrom,\n flakinessFrom,\n journeyFrom,\n lastChangedFrom,\n reachFrom,\n} from '@variance-authority/server';\nimport type { TribunalBindings } from './bindings.js';\nimport { createD1Backend } from './history.js';\nimport { ReviewError, createReviewStore } from './review.js';\nimport { createBucketStore } from './store.js';\nimport { UNAUTHENTICATED, grant, refuseWeakTokens, requires, type Granted } from './worker-auth.js';\nimport {\n BadRequest,\n Forbidden,\n MethodNotAllowed,\n asRecordBody,\n count,\n json,\n optional,\n requireMethod,\n required,\n string,\n} from './worker-http.js';\nimport {\n asApprovals,\n asBand,\n asBuildIngest,\n asCurrentRequest,\n asDecision,\n asIdentity,\n asKey,\n asRecordRequest,\n windowOf,\n} from './worker-input.js';\n\n/**\n * One `fetch` handler, three surfaces, and no outbound call.\n *\n * The operator writes the Worker entry and hands this their bindings; everything\n * below is the composition. Two of the three surfaces are protocols defined\n * elsewhere and re-served here, so a CLI configured with\n * `baselines.kind: \"remote\"` and a client built from\n * `@variance-authority/history/client` both reach this deployment unchanged.\n *\n * ## The Worker never calls anything\n *\n * No status check, no pull-request comment, no webhook, no telemetry. The\n * pipeline reports to the server and the server reports to nobody. That is the\n * project's standing constraint, and it is also what keeps this deployment free\n * of a credential for somebody else's system: there is nothing here to steal that\n * is useful anywhere but here.\n *\n * ## Two tokens, and what each one is for\n *\n * `ingest` is written into CI configuration and can write builds, baselines and\n * history. `review` belongs to people and can read the review surface and decide.\n * They are separate because deciding *promotes a baseline*, and one secret doing\n * both means anything that can read a CI log can approve a regression.\n *\n * They may not be equal, and construction refuses it. A deployment that set both\n * to the same value would satisfy every check in this file while having exactly\n * one secret, and nothing in a request would show it.\n *\n * ## Authentication happens before routing\n *\n * A caller holding neither token gets one sentence, whatever it asked for,\n * including for paths that do not exist. The alternative — 404 for unknown paths,\n * 401 for known ones — hands an unauthorised caller a map of the API, and on a\n * subject-scoped path it answers \"that subject exists\" to somebody holding\n * nothing.\n *\n * The *capability* check happens after routing, and deliberately: it can only be\n * reached by someone already holding a valid token, so telling them which of the\n * two a route wants reveals nothing they could not learn by trying.\n *\n * ## What is next door\n *\n * The token comparison and the capability rule are in\n * [`worker-auth.ts`](./worker-auth.ts); the refusals and the readers every route\n * validates in are in [`worker-http.ts`](./worker-http.ts) and\n * [`worker-input.ts`](./worker-input.ts). What is left here is the composition and\n * the routing table, which is the part an operator has to read.\n */\n\nexport interface TribunalOptions extends TribunalBindings {\n /** Scopes every row and object. One deployment, several repositories. */\n readonly project: string;\n /** Written into CI. Writes builds, baselines and history. At least 16 characters. */\n readonly ingestToken: string;\n /** Held by people. Reads the review surface and decides. At least 16 characters. */\n readonly reviewToken: string;\n /**\n * Days of builds to keep when `POST /review/sweep` is called.\n *\n * Applied on request rather than on a timer, because a Worker has no timer and\n * this package will not invent a cron the operator did not ask for. Wire it to a\n * scheduled trigger, or call it from a CI job, or never.\n */\n readonly retentionDays?: number;\n readonly now?: () => Date;\n}\n\n/**\n * The whole service, as one `fetch` handler.\n *\n * Deliberately nothing else. A handler is what a Worker exports, what a Next\n * route handler is, and what `node:http` can be adapted to in a dozen lines — so\n * the three deployments differ in their wiring and not in their router. It is\n * also the reason a `Tribunal` cannot be asked for its own tokens: a value with\n * one method has nothing to leak when it is logged.\n */\nexport interface Tribunal {\n fetch(request: Request): Promise<Response>;\n}\n\n/**\n * The baseline-store routes, restated.\n *\n * They are `@variance-authority/remote`'s and must stay byte-identical to them —\n * a client and a server disagreeing about a URL is the class of failure that\n * presents as a verdict. They are *restated* rather than imported because that\n * package's entrypoint pulls in `node:http` to serve them, and a Worker bundle\n * that drags a Node HTTP server in to read five string constants has acquired a\n * dependency on a runtime it is not running on.\n *\n * The equality is a test, not a hope: `worker.test.ts` imports the constants from\n * `remote` and asserts these against them, so drift fails the suite rather than a\n * deployment.\n */\nconst BASELINE_FIND_PATH = '/baseline/find';\nconst BASELINE_DESCRIBE_PATH = '/baseline/describe';\nconst BASELINE_PUT_PATH = '/baseline/put';\nconst CACHE_FIND_PATH = '/cache/find';\nconst CACHE_PUT_PATH = '/cache/put';\n\n/**\n * Build the service over a database, a bucket and two tokens.\n *\n * Refuses a token under 16 characters and refuses two identical ones, here\n * rather than on the first request: a deployment whose ingest token also\n * promotes baselines is a misconfiguration that would otherwise be discovered by\n * the build log that used it.\n */\nexport function createTribunal(options: TribunalOptions): Tribunal {\n refuseWeakTokens(options);\n\n const baselines = createBucketStore(options);\n const history = createD1Backend(options.db);\n const review = createReviewStore(options);\n const retentionDays = options.retentionDays ?? 30;\n\n return {\n async fetch(request: Request): Promise<Response> {\n const granted = await grant(request, options);\n if (granted === null) {\n return json(401, { error: UNAUTHENTICATED }, { 'www-authenticate': 'Bearer' });\n }\n\n let url: URL;\n try {\n url = new URL(request.url);\n } catch {\n return json(400, { error: `\"${request.url}\" is not a request URL` });\n }\n\n try {\n return await route({ baselines, history, review, retentionDays }, granted, url, request);\n } catch (error) {\n if (error instanceof BadRequest) return json(400, { error: error.message });\n if (error instanceof Forbidden) return json(403, { error: error.message });\n if (error instanceof MethodNotAllowed) {\n return json(405, { error: error.message }, { allow: error.allow });\n }\n if (error instanceof HistoryWriteConflict) return json(409, { error: error.message });\n if (error instanceof ReviewError) return json(422, { error: error.message });\n // Reported as a failure and never as an empty answer. Every client in\n // this project turns a non-2xx into a thrown error precisely so that a\n // broken service cannot become the sentence \"no baseline\" or \"nothing has\n // drifted\".\n return json(500, { error: error instanceof Error ? error.message : String(error) });\n }\n },\n };\n}\n\ninterface Surfaces {\n readonly baselines: ReturnType<typeof createBucketStore>;\n readonly history: ReturnType<typeof createD1Backend>;\n readonly review: ReturnType<typeof createReviewStore>;\n readonly retentionDays: number;\n}\n\nasync function route(\n surfaces: Surfaces,\n granted: Granted,\n url: URL,\n request: Request,\n): Promise<Response> {\n const path = url.pathname;\n\n // ------------------------------------------------------------- baselines\n if (path === BASELINE_FIND_PATH) {\n requires(granted, 'ingest', path);\n requireMethod(request, 'POST');\n const body = await asRecordBody(request);\n const found = await surfaces.baselines.find(asKey(body['key']), asIdentity(body['identity']));\n return json(200, { found });\n }\n\n if (path === BASELINE_DESCRIBE_PATH) {\n requires(granted, 'ingest', path);\n requireMethod(request, 'POST');\n const body = await asRecordBody(request);\n const described = await surfaces.baselines.describe(\n asKey(body['key']),\n asIdentity(body['identity']),\n );\n return json(200, { described });\n }\n\n if (path === BASELINE_PUT_PATH) {\n requires(granted, 'ingest', path);\n requireMethod(request, 'POST');\n const body = await asRecordBody(request);\n const raster = rasterFrom(body['raster']);\n if (raster === null) throw new BadRequest('`raster` is not a raster');\n await surfaces.baselines.put(asKey(body['key']), raster);\n return json(200, { ok: true });\n }\n\n if (path === CACHE_FIND_PATH) {\n requires(granted, 'ingest', path);\n requireMethod(request, 'POST');\n const body = await asRecordBody(request);\n const digest = body['digest'];\n if (typeof digest !== 'string') throw new BadRequest('`digest` must be a string');\n const raster = await surfaces.baselines.renderCache.get(\n digest as Digest,\n asIdentity(body['identity']),\n );\n return json(200, { raster });\n }\n\n if (path === CACHE_PUT_PATH) {\n requires(granted, 'ingest', path);\n requireMethod(request, 'POST');\n const body = await asRecordBody(request);\n const raster = rasterFrom(body['raster']);\n if (raster === null) throw new BadRequest('`raster` is not a raster');\n await surfaces.baselines.renderCache.put(raster);\n return json(200, { ok: true });\n }\n\n // --------------------------------------------------------------- history\n const project = optional(url, 'project');\n\n if (path === OBSERVATIONS_PATH) {\n requires(granted, 'ingest', path);\n requireMethod(request, 'POST');\n const write = await asRecordRequest(request);\n await surfaces.history.append(write.run, write.observations, write.tokens, write.instabilities);\n // 204: the write left nothing to say, and the client treats any body on this\n // route as a shape error rather than guessing at it.\n return new Response(null, { status: 204 });\n }\n\n if (path === APPROVALS_PATH) {\n requires(granted, 'ingest', path);\n requireMethod(request, 'POST');\n await surfaces.history.appendApprovals(await asApprovals(request));\n return new Response(null, { status: 204 });\n }\n\n if (path === CURRENT_PATH) {\n // A read, and still a POST, and still the ingest capability. Its argument is\n // a subject list — three hundred subject ids in a query string is a 414 from\n // a proxy nobody configured — and its caller is a run deciding what to write,\n // not a person deciding what to ship (`CurrentRequest`).\n requires(granted, 'ingest', path);\n requireMethod(request, 'POST');\n return json(200, await currentFrom(surfaces.history, project, await asCurrentRequest(request)));\n }\n\n if (path === LAST_CHANGED_PATH) {\n readable(granted, path);\n requireMethod(request, 'GET');\n const band = optional(url, 'band');\n return json(200, {\n observation: await lastChangedFrom(\n surfaces.history,\n project,\n required(url, 'subject'),\n required(url, 'component'),\n band === undefined ? undefined : asBand(band, 'the `band` parameter'),\n ),\n });\n }\n\n if (path === CHURN_PATH) {\n readable(granted, path);\n requireMethod(request, 'GET');\n return json(\n 200,\n await churnFrom(surfaces.history, project, required(url, 'component'), windowOf(url)),\n );\n }\n\n if (path === VALUE_JOURNEY_PATH) {\n readable(granted, path);\n requireMethod(request, 'GET');\n return json(\n 200,\n await journeyFrom(surfaces.history, project, required(url, 'token'), windowOf(url)),\n );\n }\n\n if (path === REACH_PATH) {\n readable(granted, path);\n requireMethod(request, 'GET');\n return json(\n 200,\n await reachFrom(surfaces.history, project, required(url, 'component'), windowOf(url)),\n );\n }\n\n if (path === FLAKINESS_PATH) {\n readable(granted, path);\n requireMethod(request, 'GET');\n return json(\n 200,\n await flakinessFrom(surfaces.history, project, required(url, 'subject'), windowOf(url)),\n );\n }\n\n // ---------------------------------------------------------------- review\n if (path === '/review/builds' && request.method === 'POST') {\n // The one review route the ingest token owns, and the only one it owns:\n // posting a build is what CI does, and everything else on this surface is\n // what a person does.\n requires(granted, 'ingest', path);\n await surfaces.review.ingest(await asBuildIngest(request));\n return json(201, { ok: true });\n }\n\n if (path === '/review/builds') {\n requires(granted, 'review', path);\n requireMethod(request, 'GET');\n const limit = optional(url, 'limit');\n return json(200, { builds: await surfaces.review.builds(limit === undefined ? undefined : count(limit)) });\n }\n\n if (path === '/review/changelog') {\n requires(granted, 'review', path);\n requireMethod(request, 'GET');\n const limit = optional(url, 'limit');\n return json(\n 200,\n await surfaces.review.changelog({\n ...(optional(url, 'component') !== undefined\n ? { component: required(url, 'component') }\n : {}),\n ...(optional(url, 'subject') !== undefined ? { subject: required(url, 'subject') } : {}),\n ...(optional(url, 'since') !== undefined ? { since: required(url, 'since') } : {}),\n ...(limit !== undefined ? { limit: count(limit) } : {}),\n }),\n );\n }\n\n if (path === '/review/sweep') {\n requires(granted, 'review', path);\n requireMethod(request, 'POST');\n const days = optional(url, 'days');\n // Reported rather than performed silently. An operator who cannot see what a\n // sweep removed cannot tell a working retention policy from one deleting a\n // build a day.\n return json(200, await surfaces.review.sweep(days === undefined ? surfaces.retentionDays : count(days)));\n }\n\n const build = /^\\/review\\/builds\\/([^/]+)$/.exec(path);\n if (build?.[1] !== undefined) {\n requires(granted, 'review', path);\n requireMethod(request, 'GET');\n const detail = await surfaces.review.build(decodeURIComponent(build[1]));\n return detail === null\n ? json(404, { error: `no build \"${decodeURIComponent(build[1])}\" in this project` })\n : json(200, detail);\n }\n\n const decision = /^\\/review\\/builds\\/([^/]+)\\/subjects\\/([^/]+)\\/decision$/.exec(path);\n if (decision?.[1] !== undefined && decision[2] !== undefined) {\n requires(granted, 'review', path);\n requireMethod(request, 'POST');\n const body = await asRecordBody(request);\n return json(\n 200,\n await surfaces.review.decide({\n build: decodeURIComponent(decision[1]),\n subject: decodeURIComponent(decision[2]),\n decision: asDecision(body['decision']),\n by: string(body, 'by', 'the decision'),\n ...(typeof body['note'] === 'string' && body['note'] !== '' ? { note: body['note'] } : {}),\n }),\n );\n }\n\n const image = /^\\/review\\/builds\\/([^/]+)\\/subjects\\/([^/]+)\\/(before|after|diff)\\.png$/.exec(path);\n if (image?.[1] !== undefined && image[2] !== undefined && image[3] !== undefined) {\n requires(granted, 'review', path);\n requireMethod(request, 'GET');\n const bytes = await surfaces.review.image(\n decodeURIComponent(image[1]),\n decodeURIComponent(image[2]),\n image[3] as 'before' | 'after' | 'diff',\n );\n if (bytes === null) return json(404, { error: 'this build kept no such image' });\n return new Response(bytes, {\n headers: {\n 'content-type': 'image/png',\n // A build's images never change: the row is written once and the object\n // with it. Anything a reviewer reloads is fetched again for nothing.\n 'cache-control': 'private, max-age=31536000, immutable',\n },\n });\n }\n\n return json(404, {\n error:\n `no route for ${request.method} ${path}. This deployment answers the baseline routes ` +\n `(${BASELINE_FIND_PATH}, ${BASELINE_DESCRIBE_PATH}, ${BASELINE_PUT_PATH}, ` +\n `${CACHE_FIND_PATH}, ${CACHE_PUT_PATH}), the history routes (${OBSERVATIONS_PATH}, ` +\n `${APPROVALS_PATH}, ${CURRENT_PATH}, ${LAST_CHANGED_PATH}, ${CHURN_PATH}, ` +\n `${FLAKINESS_PATH}, ${VALUE_JOURNEY_PATH}, ${REACH_PATH}) and /review/builds and ` +\n '/review/changelog. A path from a different API version is a client and a service that ' +\n 'disagree about a recorded shape',\n });\n}\n\n/**\n * The history routes that answer a question rather than record one.\n *\n * Either capability may ask. The split everywhere else in this file is *who is\n * allowed to write*, and these five write nothing: churn, reach, flakiness, the\n * value journey and the last change are derived from rows already recorded. A\n * reviewer looking at a build needs exactly these to know whether the difference\n * in front of them is the third this week or the first this year, and the browser\n * that draws that page holds the review token — so refusing them here would mean\n * a review surface that can approve a change it cannot put in context.\n *\n * It stays a rule with a name rather than an omitted check, because the next\n * history route added is a write far more often than it is a read, and the\n * default has to be the strict one.\n */\nfunction readable(granted: Granted, path: string): void {\n if (granted !== 'ingest') requires(granted, 'review', path);\n}\n\n/** Re-exported so a Worker entry can recognise a store failure without a second import. */\nexport { RasterStoreError };\n"]}
|
package/mark.svg
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 320" role="img" aria-labelledby="title desc">
|
|
2
|
+
<title id="title">Variance Authority mark</title>
|
|
3
|
+
<desc id="desc">Folded ribbon VA mark in charcoal and orange.</desc>
|
|
4
|
+
|
|
5
|
+
<defs>
|
|
6
|
+
<style>
|
|
7
|
+
.ribbon-dark {
|
|
8
|
+
fill: #24282A;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
@media (prefers-color-scheme: dark) {
|
|
12
|
+
.ribbon-dark {
|
|
13
|
+
fill: #F3F4F6;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
</style>
|
|
17
|
+
</defs>
|
|
18
|
+
|
|
19
|
+
<!-- Left descending ribbon -->
|
|
20
|
+
<path class="ribbon-dark" d="M64 54H159L256 266H160Z"/>
|
|
21
|
+
|
|
22
|
+
<!-- Right descending ribbon -->
|
|
23
|
+
<path class="ribbon-dark" d="M331 28H419L494 266H397Z"/>
|
|
24
|
+
|
|
25
|
+
<!-- Chosen / variant ribbon -->
|
|
26
|
+
<path d="M256 266L202 152L283 28H376L301 165Z" fill="#FF4A19"/>
|
|
27
|
+
|
|
28
|
+
<!-- Fold shadow -->
|
|
29
|
+
<path d="M202 152L256 266L301 165L264 103Z" fill="#D83A13" opacity="0.72"/>
|
|
30
|
+
</svg>
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
-- Generated from packages/tribunal/src/schema.ts by tools/tribunal-migrations.mjs.
|
|
2
|
+
-- Do not edit. `migrations.test.ts` fails when this file and the array disagree.
|
|
3
|
+
-- Schema version 3.
|
|
4
|
+
|
|
5
|
+
CREATE TABLE schema_version (
|
|
6
|
+
version INTEGER NOT NULL
|
|
7
|
+
) STRICT;
|
|
8
|
+
INSERT INTO schema_version (version) VALUES (3);
|
|
9
|
+
CREATE TABLE baselines (
|
|
10
|
+
project TEXT NOT NULL,
|
|
11
|
+
identity_digest TEXT NOT NULL,
|
|
12
|
+
subject TEXT NOT NULL,
|
|
13
|
+
label TEXT NOT NULL,
|
|
14
|
+
identity TEXT NOT NULL,
|
|
15
|
+
document_digest TEXT NOT NULL,
|
|
16
|
+
width INTEGER NOT NULL,
|
|
17
|
+
height INTEGER NOT NULL,
|
|
18
|
+
missing_fonts TEXT NOT NULL,
|
|
19
|
+
object_key TEXT NOT NULL,
|
|
20
|
+
at TEXT NOT NULL,
|
|
21
|
+
at_ms INTEGER NOT NULL,
|
|
22
|
+
PRIMARY KEY (project, identity_digest, subject, label)
|
|
23
|
+
) STRICT;
|
|
24
|
+
CREATE INDEX baselines_across_identities ON baselines (project, subject, label, at_ms DESC);
|
|
25
|
+
CREATE TABLE render_cache (
|
|
26
|
+
project TEXT NOT NULL,
|
|
27
|
+
identity_digest TEXT NOT NULL,
|
|
28
|
+
document_digest TEXT NOT NULL,
|
|
29
|
+
identity TEXT NOT NULL,
|
|
30
|
+
width INTEGER NOT NULL,
|
|
31
|
+
height INTEGER NOT NULL,
|
|
32
|
+
missing_fonts TEXT NOT NULL,
|
|
33
|
+
object_key TEXT NOT NULL,
|
|
34
|
+
at_ms INTEGER NOT NULL,
|
|
35
|
+
PRIMARY KEY (project, identity_digest, document_digest)
|
|
36
|
+
) STRICT;
|
|
37
|
+
CREATE TABLE builds (
|
|
38
|
+
project TEXT NOT NULL,
|
|
39
|
+
build TEXT NOT NULL,
|
|
40
|
+
"commit" TEXT NOT NULL,
|
|
41
|
+
branch TEXT,
|
|
42
|
+
intent TEXT,
|
|
43
|
+
at TEXT NOT NULL,
|
|
44
|
+
at_ms INTEGER NOT NULL,
|
|
45
|
+
identity TEXT NOT NULL,
|
|
46
|
+
identity_digest TEXT NOT NULL,
|
|
47
|
+
retention TEXT NOT NULL,
|
|
48
|
+
run_version INTEGER NOT NULL,
|
|
49
|
+
says_not_observed INTEGER NOT NULL,
|
|
50
|
+
PRIMARY KEY (project, build)
|
|
51
|
+
) STRICT;
|
|
52
|
+
CREATE INDEX builds_recent ON builds (project, at_ms DESC);
|
|
53
|
+
CREATE TABLE build_subjects (
|
|
54
|
+
project TEXT NOT NULL,
|
|
55
|
+
build TEXT NOT NULL,
|
|
56
|
+
subject TEXT NOT NULL,
|
|
57
|
+
verdict TEXT NOT NULL,
|
|
58
|
+
because TEXT NOT NULL,
|
|
59
|
+
changed_pixels INTEGER NOT NULL,
|
|
60
|
+
regions TEXT NOT NULL,
|
|
61
|
+
truncated TEXT,
|
|
62
|
+
missing_fonts TEXT,
|
|
63
|
+
findings TEXT,
|
|
64
|
+
before_key TEXT,
|
|
65
|
+
after_key TEXT,
|
|
66
|
+
diff_key TEXT,
|
|
67
|
+
candidate_document_digest TEXT,
|
|
68
|
+
candidate_width INTEGER,
|
|
69
|
+
candidate_height INTEGER,
|
|
70
|
+
candidate_missing_fonts TEXT,
|
|
71
|
+
PRIMARY KEY (project, build, subject)
|
|
72
|
+
) STRICT;
|
|
73
|
+
CREATE TABLE build_not_observed (
|
|
74
|
+
project TEXT NOT NULL,
|
|
75
|
+
build TEXT NOT NULL,
|
|
76
|
+
subject TEXT NOT NULL,
|
|
77
|
+
kind TEXT NOT NULL,
|
|
78
|
+
because TEXT NOT NULL,
|
|
79
|
+
PRIMARY KEY (project, build, subject)
|
|
80
|
+
) STRICT;
|
|
81
|
+
CREATE TABLE decisions (
|
|
82
|
+
seq INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
83
|
+
project TEXT NOT NULL,
|
|
84
|
+
build TEXT NOT NULL,
|
|
85
|
+
subject TEXT NOT NULL,
|
|
86
|
+
decision TEXT NOT NULL,
|
|
87
|
+
decided_by TEXT NOT NULL,
|
|
88
|
+
note TEXT,
|
|
89
|
+
at TEXT NOT NULL,
|
|
90
|
+
at_ms INTEGER NOT NULL
|
|
91
|
+
) STRICT;
|
|
92
|
+
CREATE INDEX decisions_latest ON decisions (project, build, subject, seq DESC);
|
|
93
|
+
CREATE TRIGGER decisions_are_append_only BEFORE UPDATE ON decisions BEGIN
|
|
94
|
+
SELECT RAISE(ABORT, 'decisions are append-only: a rewritten decision erases the review it replaced, and the earlier one is what makes the later one reviewable');
|
|
95
|
+
END;
|
|
96
|
+
CREATE TRIGGER decisions_are_permanent BEFORE DELETE ON decisions BEGIN
|
|
97
|
+
SELECT RAISE(ABORT, 'decisions are append-only: a deleted approval leaves a promoted baseline nobody can attribute to anyone');
|
|
98
|
+
END;
|
|
99
|
+
CREATE TABLE runs (
|
|
100
|
+
project TEXT NOT NULL,
|
|
101
|
+
run TEXT NOT NULL,
|
|
102
|
+
"commit" TEXT NOT NULL,
|
|
103
|
+
profile TEXT NOT NULL,
|
|
104
|
+
at TEXT NOT NULL,
|
|
105
|
+
at_ms INTEGER NOT NULL,
|
|
106
|
+
-- Nullable, and the null is load-bearing: a run that never said what it
|
|
107
|
+
-- examined must not be read as one that examined nothing, because that
|
|
108
|
+
-- number becomes the denominator of a flake rate.
|
|
109
|
+
swept INTEGER
|
|
110
|
+
) STRICT;
|
|
111
|
+
CREATE UNIQUE INDEX runs_identity ON runs (project, run, profile);
|
|
112
|
+
CREATE INDEX runs_window ON runs (project, at_ms);
|
|
113
|
+
CREATE TABLE observations (
|
|
114
|
+
project TEXT NOT NULL,
|
|
115
|
+
subject TEXT NOT NULL,
|
|
116
|
+
component TEXT NOT NULL,
|
|
117
|
+
band TEXT NOT NULL,
|
|
118
|
+
hash TEXT NOT NULL,
|
|
119
|
+
profile TEXT NOT NULL,
|
|
120
|
+
"commit" TEXT NOT NULL,
|
|
121
|
+
run TEXT NOT NULL,
|
|
122
|
+
at TEXT NOT NULL,
|
|
123
|
+
at_ms INTEGER NOT NULL,
|
|
124
|
+
accepted INTEGER NOT NULL,
|
|
125
|
+
file TEXT
|
|
126
|
+
) STRICT;
|
|
127
|
+
CREATE INDEX observations_area ON observations (project, subject, component, band, at_ms);
|
|
128
|
+
CREATE INDEX observations_component ON observations (project, component, at_ms);
|
|
129
|
+
CREATE INDEX observations_reach ON observations (project, component, subject, at_ms);
|
|
130
|
+
CREATE TABLE token_values (
|
|
131
|
+
project TEXT NOT NULL,
|
|
132
|
+
token TEXT NOT NULL,
|
|
133
|
+
value TEXT NOT NULL,
|
|
134
|
+
"commit" TEXT NOT NULL,
|
|
135
|
+
at TEXT NOT NULL,
|
|
136
|
+
at_ms INTEGER NOT NULL,
|
|
137
|
+
run TEXT NOT NULL,
|
|
138
|
+
accepted INTEGER NOT NULL
|
|
139
|
+
) STRICT;
|
|
140
|
+
CREATE INDEX token_values_journey ON token_values (project, token, at_ms);
|
|
141
|
+
CREATE TABLE instabilities (
|
|
142
|
+
project TEXT NOT NULL,
|
|
143
|
+
subject TEXT NOT NULL,
|
|
144
|
+
component TEXT,
|
|
145
|
+
band TEXT,
|
|
146
|
+
profile TEXT NOT NULL,
|
|
147
|
+
"commit" TEXT NOT NULL,
|
|
148
|
+
run TEXT NOT NULL,
|
|
149
|
+
at TEXT NOT NULL,
|
|
150
|
+
at_ms INTEGER NOT NULL,
|
|
151
|
+
absorbed_by TEXT
|
|
152
|
+
) STRICT;
|
|
153
|
+
CREATE INDEX instabilities_subject ON instabilities (project, subject, at_ms);
|
|
154
|
+
CREATE TABLE approvals (
|
|
155
|
+
project TEXT NOT NULL,
|
|
156
|
+
subject TEXT NOT NULL,
|
|
157
|
+
run TEXT NOT NULL,
|
|
158
|
+
at TEXT NOT NULL,
|
|
159
|
+
at_ms INTEGER NOT NULL,
|
|
160
|
+
approver TEXT
|
|
161
|
+
) STRICT;
|
|
162
|
+
CREATE UNIQUE INDEX approvals_identity ON approvals (project, subject, run);
|
|
163
|
+
CREATE INDEX approvals_window ON approvals (project, at_ms);
|
|
164
|
+
CREATE TRIGGER runs_are_append_only BEFORE UPDATE ON runs BEGIN
|
|
165
|
+
SELECT RAISE(ABORT, 'runs are append-only: a recorded run is a fact about a moment, and rewriting one changes a denominator somebody already read');
|
|
166
|
+
END;
|
|
167
|
+
CREATE TRIGGER runs_are_permanent BEFORE DELETE ON runs BEGIN
|
|
168
|
+
SELECT RAISE(ABORT, 'runs are append-only: deleting a quiet run inflates every rate computed over its window');
|
|
169
|
+
END;
|
|
170
|
+
CREATE TRIGGER observations_are_append_only BEFORE UPDATE ON observations BEGIN
|
|
171
|
+
SELECT RAISE(ABORT, 'observations are append-only: two hashes for one key are two rows, and an update is the merge this store exists to avoid');
|
|
172
|
+
END;
|
|
173
|
+
CREATE TRIGGER observations_are_permanent BEFORE DELETE ON observations BEGIN
|
|
174
|
+
SELECT RAISE(ABORT, 'observations are append-only: a deleted change is a change nobody can ever ask about again');
|
|
175
|
+
END;
|
|
176
|
+
CREATE TRIGGER token_values_are_append_only BEFORE UPDATE ON token_values BEGIN
|
|
177
|
+
SELECT RAISE(ABORT, 'token values are append-only: a rewritten value breaks the journey that was the reason for keeping it');
|
|
178
|
+
END;
|
|
179
|
+
CREATE TRIGGER token_values_are_permanent BEFORE DELETE ON token_values BEGIN
|
|
180
|
+
SELECT RAISE(ABORT, 'token values are append-only: a removed step turns a drift total into a lower bound with nothing saying so');
|
|
181
|
+
END;
|
|
182
|
+
CREATE TRIGGER instabilities_are_append_only BEFORE UPDATE ON instabilities BEGIN
|
|
183
|
+
SELECT RAISE(ABORT, 'instabilities are append-only: an occurrence is a fact about one run, and rewriting one changes a rate somebody already acted on');
|
|
184
|
+
END;
|
|
185
|
+
CREATE TRIGGER instabilities_are_permanent BEFORE DELETE ON instabilities BEGIN
|
|
186
|
+
SELECT RAISE(ABORT, 'instabilities are append-only: deleting an occurrence is how a flake that was fixed becomes a flake that never happened');
|
|
187
|
+
END;
|
|
188
|
+
CREATE TRIGGER approvals_are_append_only BEFORE UPDATE ON approvals BEGIN
|
|
189
|
+
SELECT RAISE(ABORT, 'approvals are append-only: a rewritten acceptance changes what a reviewer agreed to after they agreed to it');
|
|
190
|
+
END;
|
|
191
|
+
CREATE TRIGGER approvals_are_permanent BEFORE DELETE ON approvals BEGIN
|
|
192
|
+
SELECT RAISE(ABORT, 'approvals are append-only: a deleted acceptance turns a reviewed change back into an unreviewed one, and every drift total over it drops');
|
|
193
|
+
END;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
-- Generated from packages/tribunal/src/schema.ts by tools/tribunal-migrations.mjs.
|
|
2
|
+
-- Do not edit. `migrations.test.ts` fails when this file and the array disagree.
|
|
3
|
+
-- Schema version 4.
|
|
4
|
+
|
|
5
|
+
CREATE TABLE changelog (
|
|
6
|
+
seq INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
7
|
+
project TEXT NOT NULL,
|
|
8
|
+
build TEXT NOT NULL,
|
|
9
|
+
subject TEXT NOT NULL,
|
|
10
|
+
"commit" TEXT NOT NULL,
|
|
11
|
+
intent TEXT,
|
|
12
|
+
decided_by TEXT NOT NULL,
|
|
13
|
+
note TEXT,
|
|
14
|
+
-- The region list the build reported, frozen. Shapes are grouped when
|
|
15
|
+
-- somebody reads: approval here is per subject, so there is no batch at
|
|
16
|
+
-- write time to cluster, and a shape approved across three sessions should
|
|
17
|
+
-- still read as one change.
|
|
18
|
+
regions TEXT NOT NULL,
|
|
19
|
+
at TEXT NOT NULL,
|
|
20
|
+
at_ms INTEGER NOT NULL
|
|
21
|
+
) STRICT;
|
|
22
|
+
CREATE INDEX changelog_recent ON changelog (project, at_ms DESC);
|
|
23
|
+
CREATE INDEX changelog_subject ON changelog (project, subject, at_ms DESC);
|
|
24
|
+
CREATE TRIGGER changelog_is_append_only BEFORE UPDATE ON changelog BEGIN
|
|
25
|
+
SELECT RAISE(ABORT, 'the changelog is append-only: an edited explanation is an explanation of a baseline that was promoted for a different reason');
|
|
26
|
+
END;
|
|
27
|
+
CREATE TRIGGER changelog_is_permanent BEFORE DELETE ON changelog BEGIN
|
|
28
|
+
SELECT RAISE(ABORT, 'the changelog is append-only: a deleted entry leaves a baseline nobody can account for, which is the state this table exists to end');
|
|
29
|
+
END;
|
|
30
|
+
UPDATE schema_version SET version = 4;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
-- Generated from packages/tribunal/src/schema.ts by tools/tribunal-migrations.mjs.
|
|
2
|
+
-- Do not edit. `migrations.test.ts` fails when this file and the array disagree.
|
|
3
|
+
-- Schema version 5.
|
|
4
|
+
|
|
5
|
+
ALTER TABLE baselines ADD COLUMN accessibility TEXT;
|
|
6
|
+
ALTER TABLE build_subjects ADD COLUMN signals TEXT;
|
|
7
|
+
ALTER TABLE build_subjects ADD COLUMN candidate_accessibility TEXT;
|
|
8
|
+
UPDATE schema_version SET version = 5;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
-- Generated from packages/tribunal/src/schema.ts by tools/tribunal-migrations.mjs.
|
|
2
|
+
-- Do not edit. `migrations.test.ts` fails when this file and the array disagree.
|
|
3
|
+
-- Schema version 6.
|
|
4
|
+
|
|
5
|
+
CREATE TABLE build_variations (
|
|
6
|
+
project TEXT NOT NULL,
|
|
7
|
+
build TEXT NOT NULL,
|
|
8
|
+
subject TEXT NOT NULL,
|
|
9
|
+
parent TEXT,
|
|
10
|
+
identical INTEGER,
|
|
11
|
+
bands TEXT,
|
|
12
|
+
unobserved TEXT,
|
|
13
|
+
components TEXT,
|
|
14
|
+
digest TEXT,
|
|
15
|
+
how TEXT,
|
|
16
|
+
because TEXT NOT NULL,
|
|
17
|
+
PRIMARY KEY (project, build, subject)
|
|
18
|
+
) STRICT;
|
|
19
|
+
UPDATE schema_version SET version = 6;
|