@worker-protocol/conformance 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/LICENSE +201 -0
- package/NOTICE +9 -0
- package/dist/attribution.d.ts +13 -0
- package/dist/attribution.js +27 -0
- package/dist/checks/actions.d.ts +24 -0
- package/dist/checks/actions.js +345 -0
- package/dist/checks/activity.d.ts +18 -0
- package/dist/checks/activity.js +64 -0
- package/dist/checks/alerts.d.ts +16 -0
- package/dist/checks/alerts.js +84 -0
- package/dist/checks/arranged.d.ts +27 -0
- package/dist/checks/arranged.js +232 -0
- package/dist/checks/descriptor.d.ts +34 -0
- package/dist/checks/descriptor.js +179 -0
- package/dist/checks/endpoints.d.ts +17 -0
- package/dist/checks/endpoints.js +139 -0
- package/dist/checks/events.d.ts +16 -0
- package/dist/checks/events.js +58 -0
- package/dist/checks/health.d.ts +13 -0
- package/dist/checks/health.js +85 -0
- package/dist/checks/metrics.d.ts +14 -0
- package/dist/checks/metrics.js +422 -0
- package/dist/checks/nudges.d.ts +20 -0
- package/dist/checks/nudges.js +77 -0
- package/dist/checks/surfaces.d.ts +14 -0
- package/dist/checks/surfaces.js +249 -0
- package/dist/checks/tasks.d.ts +23 -0
- package/dist/checks/tasks.js +171 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +120 -0
- package/dist/index.d.ts +95 -0
- package/dist/index.js +128 -0
- package/dist/report.d.ts +87 -0
- package/dist/report.js +71 -0
- package/dist/transcript.d.ts +44 -0
- package/dist/transcript.js +49 -0
- package/package.json +51 -0
- package/rules.json +1242 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { EDITION } from "@worker-protocol/schemas";
|
|
4
|
+
import { checkActions } from "./checks/actions.js";
|
|
5
|
+
import { checkActivity } from "./checks/activity.js";
|
|
6
|
+
import { checkAlerts } from "./checks/alerts.js";
|
|
7
|
+
import { checkArranged } from "./checks/arranged.js";
|
|
8
|
+
import { readDescriptor } from "./checks/descriptor.js";
|
|
9
|
+
import { judgeTranscript } from "./checks/endpoints.js";
|
|
10
|
+
import { checkEvents } from "./checks/events.js";
|
|
11
|
+
import { checkHealth } from "./checks/health.js";
|
|
12
|
+
import { checkMetrics } from "./checks/metrics.js";
|
|
13
|
+
import { checkNudges } from "./checks/nudges.js";
|
|
14
|
+
import { callSurfaces } from "./checks/surfaces.js";
|
|
15
|
+
import { checkTasks } from "./checks/tasks.js";
|
|
16
|
+
import { unclaimed } from "./report.js";
|
|
17
|
+
import { transcript } from "./transcript.js";
|
|
18
|
+
export { tally } from "./report.js";
|
|
19
|
+
/** Generated from `spec/` by `src/generate-rules.ts` and committed beside the source. */
|
|
20
|
+
export async function universe() {
|
|
21
|
+
const path = join(import.meta.dirname, "..", "rules.json");
|
|
22
|
+
return JSON.parse(await readFile(path, "utf8"));
|
|
23
|
+
}
|
|
24
|
+
export async function verify(options) {
|
|
25
|
+
const { rules: all, codes, attribution } = await universe();
|
|
26
|
+
const byId = new Map(all.map((rule) => [rule.id, rule]));
|
|
27
|
+
const tape = transcript(options.fetch ?? globalThis.fetch, options.credential);
|
|
28
|
+
const descriptor = await readDescriptor(options.baseUrl, byId, attribution, tape);
|
|
29
|
+
// DESC-25: a verifier that does not hold the declared edition's MAJOR verifies NOTHING and
|
|
30
|
+
// reports that it is older than the Worker — rather than failing a Worker for a surface added
|
|
31
|
+
// after this tool was built. The ordering DESC-23 fixes is what lets it say `older` rather than
|
|
32
|
+
// merely `unrecognised`, and that is the difference between telling an operator to upgrade the
|
|
33
|
+
// verifier and leaving the Worker under suspicion for what is the reader's problem.
|
|
34
|
+
const declaredMajor = descriptor.document?.edition.split(".")[0];
|
|
35
|
+
if (declaredMajor !== undefined && declaredMajor !== EDITION.split(".")[0]) {
|
|
36
|
+
return {
|
|
37
|
+
baseUrl: options.baseUrl,
|
|
38
|
+
edition: descriptor.document?.edition ?? null,
|
|
39
|
+
verifierEdition: EDITION,
|
|
40
|
+
older: true,
|
|
41
|
+
results: all.map((rule) => ({
|
|
42
|
+
rule,
|
|
43
|
+
verdict: "notExercised",
|
|
44
|
+
detail: `this verifier holds edition ${EDITION} and the Worker declares ${descriptor.document?.edition}`,
|
|
45
|
+
})),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
const results = [...descriptor.results];
|
|
49
|
+
// Addresses a Capability declares INSIDE its own entry rather than beside it — `configure`'s
|
|
50
|
+
// reading address is the first. ENDP-1 judges what the verifier called against what the
|
|
51
|
+
// Descriptor declared, so an address it could not see would read as the Worker's fault.
|
|
52
|
+
const nested = [];
|
|
53
|
+
/** The address a Capability declared, resolved by `readDescriptor` per DESC-12. */
|
|
54
|
+
const surface = (name) => descriptor.surfaces.find((one) => one.capability === name)?.url ?? null;
|
|
55
|
+
if (descriptor.document !== null && descriptor.url !== null) {
|
|
56
|
+
results.push(...(await callSurfaces(descriptor.surfaces, descriptor.url,
|
|
57
|
+
// ENDP-6 is probed on every write this protocol has, and being wrong on a write is what
|
|
58
|
+
// the rule is for: a Worker that refuses an unanswerable version on a read and performs
|
|
59
|
+
// one here has done the thing it exists to prevent.
|
|
60
|
+
[surface("actions"), surface("nudges")].filter((url) => url !== null), byId, tape, options.credential, options.mayPerform === true)));
|
|
61
|
+
// TASK-32 is an agreement between two entries rather than a shape inside one, so the tasks
|
|
62
|
+
// check is handed the `actions` entry itself: it judges the name an entry points at AND the
|
|
63
|
+
// input that Action declares, and neither is reachable from inside `tasks`.
|
|
64
|
+
const accepts = descriptor.document.capabilities.actions?.accepts ?? {};
|
|
65
|
+
// Tasks before Actions, and the order is not a preference: an Action that answers a Task
|
|
66
|
+
// resolves its condition (TASK-15), so a Worker whose Tasks are read after its Actions are
|
|
67
|
+
// performed may have none left to read. Every check here is a GET and changes nothing.
|
|
68
|
+
const listed = await checkTasks(descriptor.document.capabilities.tasks, surface("tasks"), Object.keys(descriptor.document.skills ?? {}), accepts, byId, attribution, tape);
|
|
69
|
+
results.push(...listed.results);
|
|
70
|
+
nested.push(...listed.addresses);
|
|
71
|
+
const performed = await checkActions(descriptor.document.capabilities.actions, surface("actions"), descriptor.url, byId, attribution, tape, options.mayPerform === true, options.arrangement ?? {});
|
|
72
|
+
results.push(...performed.results);
|
|
73
|
+
nested.push(...performed.addresses);
|
|
74
|
+
results.push(...(await checkMetrics(descriptor.document.capabilities.metrics, surface("metrics"), byId, attribution, tape)));
|
|
75
|
+
// `events` has no address by design (DESC-22), so this check sends nothing and takes no
|
|
76
|
+
// transcript. It is the only Capability a verifier judges entirely from the Descriptor.
|
|
77
|
+
results.push(...checkEvents(descriptor.document.capabilities.events, byId, attribution));
|
|
78
|
+
results.push(...(await checkAlerts(descriptor.document.capabilities.alerts, surface("alerts"), Object.keys(accepts), byId, attribution, tape)));
|
|
79
|
+
results.push(...(await checkActivity(descriptor.document.capabilities.activity, surface("activity"), byId, attribution, tape)));
|
|
80
|
+
results.push(...(await checkHealth(descriptor.document.capabilities.health, surface("health"), byId, attribution, tape)));
|
|
81
|
+
// Last of the Capability checks, because it is the only one that sends the Worker somewhere: a
|
|
82
|
+
// nudge it accepts has it read a Task list. Everything above it is a read.
|
|
83
|
+
results.push(...(await checkNudges(descriptor.document.capabilities.nudges, surface("nudges"), Object.keys(descriptor.document.skills ?? {}), byId, attribution, tape, options.mayPerform === true)));
|
|
84
|
+
}
|
|
85
|
+
if (descriptor.document !== null) {
|
|
86
|
+
const configure = descriptor.document.capabilities.actions?.accepts?.configure?.readAddress;
|
|
87
|
+
results.push(...(await checkArranged({
|
|
88
|
+
descriptorUrl: descriptor.url,
|
|
89
|
+
alertsUrl: surface("alerts"),
|
|
90
|
+
activityUrl: surface("activity"),
|
|
91
|
+
tasksUrl: surface("tasks"),
|
|
92
|
+
settingsUrl: configure === undefined || descriptor.url === null
|
|
93
|
+
? null
|
|
94
|
+
: new URL(configure, descriptor.url).toString(),
|
|
95
|
+
workerId: descriptor.document.id,
|
|
96
|
+
eventTypes: Object.keys(descriptor.document.capabilities.events
|
|
97
|
+
?.publishes ?? {}),
|
|
98
|
+
}, {
|
|
99
|
+
...(options.arrangement ?? {}),
|
|
100
|
+
healthUrl: surface("health") ?? undefined,
|
|
101
|
+
actionsUrl: surface("actions") ?? undefined,
|
|
102
|
+
}, options.mayPerform === true, byId, tape)));
|
|
103
|
+
}
|
|
104
|
+
// Last, and over everything the run provoked. ENDP-26 is a statement about a set of responses
|
|
105
|
+
// rather than about one, so it cannot be asked until there are no more to come.
|
|
106
|
+
const declared = new Set([
|
|
107
|
+
...(descriptor.url === null ? [] : [descriptor.url]),
|
|
108
|
+
...descriptor.surfaces.map((s) => s.url),
|
|
109
|
+
...nested,
|
|
110
|
+
]);
|
|
111
|
+
results.push(...judgeTranscript(tape.exchanges, codes, declared, byId));
|
|
112
|
+
// Every rule gets a verdict, never only the ones a check claimed. A report covering 23 rules of
|
|
113
|
+
// 102, all green, tells an operator the Worker was checked against the protocol — and the reader
|
|
114
|
+
// concludes more than was established, which is the fault this whole directory is written
|
|
115
|
+
// against. `unclaimed` is what says which kind of silence each remaining rule is.
|
|
116
|
+
const claimed = new Set(results.map((result) => result.rule.id));
|
|
117
|
+
const complete = [
|
|
118
|
+
...results,
|
|
119
|
+
...all.filter((rule) => !claimed.has(rule.id)).map(unclaimed),
|
|
120
|
+
];
|
|
121
|
+
complete.sort((a, b) => a.rule.id.localeCompare(b.rule.id, "en", { numeric: true }));
|
|
122
|
+
return {
|
|
123
|
+
baseUrl: options.baseUrl,
|
|
124
|
+
edition: descriptor.document?.edition ?? null,
|
|
125
|
+
verifierEdition: EDITION,
|
|
126
|
+
results: complete,
|
|
127
|
+
};
|
|
128
|
+
}
|
package/dist/report.d.ts
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The verdict vocabulary, which `conformance/README.md` states and this file encodes.
|
|
3
|
+
*
|
|
4
|
+
* Nothing here decides anything. Every verdict corresponds to a sentence in that document, and the
|
|
5
|
+
* one property worth protecting in code is the one the prose spends a paragraph on: `notExercised`
|
|
6
|
+
* and `unverified` are separate, because they read alike on a page and are opposite facts.
|
|
7
|
+
*/
|
|
8
|
+
/** A rule as `packages/conformance/rules.json` records it, generated from `spec/`. */
|
|
9
|
+
export type Rule = {
|
|
10
|
+
id: string;
|
|
11
|
+
/** The `spec/` file that defines it, so a report can group by subject. */
|
|
12
|
+
file: string;
|
|
13
|
+
/** `spec/README.md`: a contract, or advice this specification has standing to give. */
|
|
14
|
+
class: "required" | "recommended";
|
|
15
|
+
/** `conformance/verifiability.md`: what a check can observe. */
|
|
16
|
+
reach: "W" | "H" | "P" | "N" | "—";
|
|
17
|
+
};
|
|
18
|
+
export type Verdict =
|
|
19
|
+
/** The check ran and the Worker satisfied it. */
|
|
20
|
+
"passes"
|
|
21
|
+
/**
|
|
22
|
+
* The check ran and the Worker did not satisfy it. A `recommended` rule is still reported with
|
|
23
|
+
* this verdict; it is the rule's class, printed beside it, that keeps a report from reading as a
|
|
24
|
+
* verdict where the specification has no standing to give one.
|
|
25
|
+
*/
|
|
26
|
+
| "fails"
|
|
27
|
+
/**
|
|
28
|
+
* A check exists and this run did not reach it: the Worker declares no such Capability, or the
|
|
29
|
+
* check needs a Worker arranged to be observed and this one is not.
|
|
30
|
+
*/
|
|
31
|
+
| "notExercised"
|
|
32
|
+
/** The rule's subject is the Worker and no party outside it can observe a violation. */
|
|
33
|
+
| "unverified"
|
|
34
|
+
/** The rule binds a verifier, a Tower, a consumer, an issuer or this specification. */
|
|
35
|
+
| "otherSubject";
|
|
36
|
+
export type Result = {
|
|
37
|
+
rule: Rule;
|
|
38
|
+
verdict: Verdict;
|
|
39
|
+
/** Why, in one line. Required for every verdict but `passes`, where the rule says it already. */
|
|
40
|
+
detail?: string;
|
|
41
|
+
};
|
|
42
|
+
export type Report = {
|
|
43
|
+
/** The base URL the Worker was enrolled as, exactly as the caller gave it. */
|
|
44
|
+
baseUrl: string;
|
|
45
|
+
/** The edition the Descriptor declared, or null where none could be read. */
|
|
46
|
+
edition: string | null;
|
|
47
|
+
/**
|
|
48
|
+
* The edition this verifier holds.
|
|
49
|
+
*
|
|
50
|
+
* DESC-25 binds a verifier rather than a Worker, and publishing an edition is what made it ours
|
|
51
|
+
* to obey: a tool that does not hold the declared MAJOR verifies nothing and says it is the one
|
|
52
|
+
* that is behind. A report that left this out would leave a reader unable to tell a Worker that
|
|
53
|
+
* failed from a verifier that could not read it.
|
|
54
|
+
*/
|
|
55
|
+
verifierEdition: string;
|
|
56
|
+
/** Set where DESC-25 stopped the run: this verifier is older than the Worker. */
|
|
57
|
+
older?: true;
|
|
58
|
+
results: Result[];
|
|
59
|
+
};
|
|
60
|
+
/**
|
|
61
|
+
* The verdict a rule gets when no check claimed it.
|
|
62
|
+
*
|
|
63
|
+
* This is the whole reason a report covers every rule rather than only the ones it exercised. A
|
|
64
|
+
* rule whose reach says a Worker could be observed and that nothing observed is `notExercised` —
|
|
65
|
+
* a gap somebody can close. One that binds another party, or that nothing can ever see, is not a
|
|
66
|
+
* gap and says so under its own name.
|
|
67
|
+
*/
|
|
68
|
+
export declare const unclaimed: (rule: Rule) => Result;
|
|
69
|
+
/** Counts by verdict, for the one line a report ends with. */
|
|
70
|
+
export declare const tally: (results: Result[]) => Record<Verdict, number>;
|
|
71
|
+
/**
|
|
72
|
+
* Where a check module accumulates its verdicts.
|
|
73
|
+
*
|
|
74
|
+
* Every one of them was opening with the same seven lines — an array, a `say` that looks the rule
|
|
75
|
+
* up and drops what this verifier does not hold, and an `allExcept` that answers for the rest of
|
|
76
|
+
* its claims. None of it decides anything: which id, which verdict and which reason are the
|
|
77
|
+
* module's, and are still written there, beside the rule each one argues for.
|
|
78
|
+
*
|
|
79
|
+
* `say` drops an id the universe does not carry rather than throwing, and that is deliberate: a
|
|
80
|
+
* verifier holding an older `rules.json` than the module was written against reports what it can
|
|
81
|
+
* and stays silent about what it cannot, which is DESC-25's posture one level down.
|
|
82
|
+
*/
|
|
83
|
+
export declare function verdicts(rules: Map<string, Rule>, claims: readonly string[]): {
|
|
84
|
+
results: Result[];
|
|
85
|
+
say: (id: string, verdict: Verdict, detail?: string) => void;
|
|
86
|
+
allExcept: (verdict: Verdict, why: string, except?: string[]) => void;
|
|
87
|
+
};
|
package/dist/report.js
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The verdict vocabulary, which `conformance/README.md` states and this file encodes.
|
|
3
|
+
*
|
|
4
|
+
* Nothing here decides anything. Every verdict corresponds to a sentence in that document, and the
|
|
5
|
+
* one property worth protecting in code is the one the prose spends a paragraph on: `notExercised`
|
|
6
|
+
* and `unverified` are separate, because they read alike on a page and are opposite facts.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* The verdict a rule gets when no check claimed it.
|
|
10
|
+
*
|
|
11
|
+
* This is the whole reason a report covers every rule rather than only the ones it exercised. A
|
|
12
|
+
* rule whose reach says a Worker could be observed and that nothing observed is `notExercised` —
|
|
13
|
+
* a gap somebody can close. One that binds another party, or that nothing can ever see, is not a
|
|
14
|
+
* gap and says so under its own name.
|
|
15
|
+
*/
|
|
16
|
+
export const unclaimed = (rule) => {
|
|
17
|
+
switch (rule.reach) {
|
|
18
|
+
case "P":
|
|
19
|
+
return { rule, verdict: "otherSubject", detail: "the subject of this rule is not a Worker" };
|
|
20
|
+
case "N":
|
|
21
|
+
return { rule, verdict: "unverified", detail: "no party outside the Worker can observe it" };
|
|
22
|
+
case "—":
|
|
23
|
+
return {
|
|
24
|
+
rule,
|
|
25
|
+
verdict: "notExercised",
|
|
26
|
+
detail: "the surface it is about belongs to a spec/ file that is still open",
|
|
27
|
+
};
|
|
28
|
+
default:
|
|
29
|
+
return { rule, verdict: "notExercised", detail: "no check in this verifier claims it yet" };
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
/** Counts by verdict, for the one line a report ends with. */
|
|
33
|
+
export const tally = (results) => {
|
|
34
|
+
const counts = {
|
|
35
|
+
passes: 0,
|
|
36
|
+
fails: 0,
|
|
37
|
+
notExercised: 0,
|
|
38
|
+
unverified: 0,
|
|
39
|
+
otherSubject: 0,
|
|
40
|
+
};
|
|
41
|
+
for (const result of results)
|
|
42
|
+
counts[result.verdict] += 1;
|
|
43
|
+
return counts;
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* Where a check module accumulates its verdicts.
|
|
47
|
+
*
|
|
48
|
+
* Every one of them was opening with the same seven lines — an array, a `say` that looks the rule
|
|
49
|
+
* up and drops what this verifier does not hold, and an `allExcept` that answers for the rest of
|
|
50
|
+
* its claims. None of it decides anything: which id, which verdict and which reason are the
|
|
51
|
+
* module's, and are still written there, beside the rule each one argues for.
|
|
52
|
+
*
|
|
53
|
+
* `say` drops an id the universe does not carry rather than throwing, and that is deliberate: a
|
|
54
|
+
* verifier holding an older `rules.json` than the module was written against reports what it can
|
|
55
|
+
* and stays silent about what it cannot, which is DESC-25's posture one level down.
|
|
56
|
+
*/
|
|
57
|
+
export function verdicts(rules, claims) {
|
|
58
|
+
const results = [];
|
|
59
|
+
const say = (id, verdict, detail) => {
|
|
60
|
+
const rule = rules.get(id);
|
|
61
|
+
if (rule)
|
|
62
|
+
results.push({ rule, verdict, detail });
|
|
63
|
+
};
|
|
64
|
+
/** The claims this module has not already spoken for, all at one verdict and one reason. */
|
|
65
|
+
const allExcept = (verdict, why, except = []) => {
|
|
66
|
+
for (const id of claims)
|
|
67
|
+
if (!except.includes(id))
|
|
68
|
+
say(id, verdict, why);
|
|
69
|
+
};
|
|
70
|
+
return { results, say, allExcept };
|
|
71
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every exchange the verifier had with the Worker, kept so that the cross-cutting rules can be
|
|
3
|
+
* judged over all of them.
|
|
4
|
+
*
|
|
5
|
+
* Several rules in [endpoints](../../../spec/endpoints.md) are about *every* response rather than
|
|
6
|
+
* about one surface — ENDP-5 puts two headers on all of them, ENDP-26 forbids one code arriving
|
|
7
|
+
* under two statuses, which is a statement no single response can break. A verifier that checked
|
|
8
|
+
* those inside each surface's own check would be asking a question it could not answer, so the
|
|
9
|
+
* exchanges are collected first and the cross-cutting checks run last, over the whole record.
|
|
10
|
+
*/
|
|
11
|
+
export type Exchange = {
|
|
12
|
+
url: string;
|
|
13
|
+
method: string;
|
|
14
|
+
status: number;
|
|
15
|
+
headers: Headers;
|
|
16
|
+
/** The raw body, so that a check can report what failed to parse rather than that it did. */
|
|
17
|
+
body: string;
|
|
18
|
+
/** The parsed body, or null where it is not JSON. */
|
|
19
|
+
json: unknown;
|
|
20
|
+
/** What the verifier was doing, for a report that has to say why a response was provoked. */
|
|
21
|
+
intent: string;
|
|
22
|
+
/**
|
|
23
|
+
* Whether the verifier knows this request is wrong in a way that will not change.
|
|
24
|
+
*
|
|
25
|
+
* ENDP-11 forbids a `5xx` for a condition that will not change, and its witness is ordinarily
|
|
26
|
+
* out of reach: nothing outside a Worker can tell a transient fault from a permanent one. But a
|
|
27
|
+
* verifier holds one fact nobody else does — it knows which of its own requests were deliberately
|
|
28
|
+
* and permanently wrong, because it made them that way. An Action no entry declares will never
|
|
29
|
+
* exist; a filter no surface knows will never be recognised; a credential never issued will
|
|
30
|
+
* never be accepted. A `5xx` to any of those is the rule broken, with no arrangement needed.
|
|
31
|
+
*/
|
|
32
|
+
permanent: boolean;
|
|
33
|
+
};
|
|
34
|
+
export type Transcript = {
|
|
35
|
+
exchanges: Exchange[];
|
|
36
|
+
/** Performs a request, records it, and returns it. A network failure throws, as fetch does. */
|
|
37
|
+
send: (url: string, intent: string, init?: RequestInit & {
|
|
38
|
+
permanent?: boolean;
|
|
39
|
+
}) => Promise<Exchange>;
|
|
40
|
+
};
|
|
41
|
+
export type Sender = typeof globalThis.fetch;
|
|
42
|
+
export declare function transcript(send: Sender, credential?: string): Transcript;
|
|
43
|
+
/** `application/json; charset=utf-8` and `application/json` are the same media type (ENDP-4). */
|
|
44
|
+
export declare const isJson: (headers: Headers) => boolean;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every exchange the verifier had with the Worker, kept so that the cross-cutting rules can be
|
|
3
|
+
* judged over all of them.
|
|
4
|
+
*
|
|
5
|
+
* Several rules in [endpoints](../../../spec/endpoints.md) are about *every* response rather than
|
|
6
|
+
* about one surface — ENDP-5 puts two headers on all of them, ENDP-26 forbids one code arriving
|
|
7
|
+
* under two statuses, which is a statement no single response can break. A verifier that checked
|
|
8
|
+
* those inside each surface's own check would be asking a question it could not answer, so the
|
|
9
|
+
* exchanges are collected first and the cross-cutting checks run last, over the whole record.
|
|
10
|
+
*/
|
|
11
|
+
export function transcript(send, credential) {
|
|
12
|
+
const exchanges = [];
|
|
13
|
+
return {
|
|
14
|
+
exchanges,
|
|
15
|
+
async send(url, intent, init = {}) {
|
|
16
|
+
// REG-3: a credential is presented as `Authorization: Bearer <token>`, and this protocol
|
|
17
|
+
// fixes nothing else about it. A caller that presented it anywhere else would produce a call
|
|
18
|
+
// that cannot complete however good either party's intentions are.
|
|
19
|
+
const headers = new Headers(init.headers);
|
|
20
|
+
if (credential !== undefined && !headers.has("authorization")) {
|
|
21
|
+
headers.set("authorization", `Bearer ${credential}`);
|
|
22
|
+
}
|
|
23
|
+
const { permanent = false, ...request } = init;
|
|
24
|
+
const response = await send(url, { ...request, headers, redirect: "manual" });
|
|
25
|
+
const body = await response.text();
|
|
26
|
+
let json = null;
|
|
27
|
+
try {
|
|
28
|
+
json = JSON.parse(body);
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
json = null;
|
|
32
|
+
}
|
|
33
|
+
const exchange = {
|
|
34
|
+
url,
|
|
35
|
+
method: init.method ?? "GET",
|
|
36
|
+
status: response.status,
|
|
37
|
+
headers: response.headers,
|
|
38
|
+
body,
|
|
39
|
+
json,
|
|
40
|
+
intent,
|
|
41
|
+
permanent,
|
|
42
|
+
};
|
|
43
|
+
exchanges.push(exchange);
|
|
44
|
+
return exchange;
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
/** `application/json; charset=utf-8` and `application/json` are the same media type (ENDP-4). */
|
|
49
|
+
export const isJson = (headers) => (headers.get("content-type") ?? "").split(";")[0].trim().toLowerCase() === "application/json";
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@worker-protocol/conformance",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"workerProtocolEdition": "0.1",
|
|
5
|
+
"description": "Point it at a Worker's base URL, get a report of what it complies with",
|
|
6
|
+
"license": "Apache-2.0",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/rowing-tech/worker-protocol.git",
|
|
10
|
+
"directory": "packages/conformance"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://github.com/rowing-tech/worker-protocol#readme",
|
|
13
|
+
"bugs": "https://github.com/rowing-tech/worker-protocol/issues",
|
|
14
|
+
"publishConfig": {
|
|
15
|
+
"access": "public"
|
|
16
|
+
},
|
|
17
|
+
"type": "module",
|
|
18
|
+
"main": "./dist/index.js",
|
|
19
|
+
"types": "./dist/index.d.ts",
|
|
20
|
+
"exports": {
|
|
21
|
+
".": {
|
|
22
|
+
"types": "./dist/index.d.ts",
|
|
23
|
+
"default": "./dist/index.js"
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"bin": {
|
|
27
|
+
"worker-protocol-conformance": "./dist/cli.js"
|
|
28
|
+
},
|
|
29
|
+
"files": [
|
|
30
|
+
"dist",
|
|
31
|
+
"rules.json",
|
|
32
|
+
"LICENSE",
|
|
33
|
+
"NOTICE"
|
|
34
|
+
],
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"@worker-protocol/schemas": "0.1.0",
|
|
37
|
+
"zod": "^4.5.4"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@types/node": "26.5.1",
|
|
41
|
+
"@worker-protocol/client": "0.1.0",
|
|
42
|
+
"typescript": "7.0.2",
|
|
43
|
+
"vitest": "5.0.0",
|
|
44
|
+
"wrangler": "4.131.1"
|
|
45
|
+
},
|
|
46
|
+
"scripts": {
|
|
47
|
+
"typecheck": "tsc -p tsconfig.json",
|
|
48
|
+
"build": "tsc -p tsconfig.build.json",
|
|
49
|
+
"test": "vitest run"
|
|
50
|
+
}
|
|
51
|
+
}
|