@bitvea/feedback-cli 1.1.0 → 1.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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @bitvea/feedback-cli
2
2
 
3
- The installer for [BitVea Feedback](https://github.com/bitvea/feedback-toolbar): it installs the toolbar into an app, and it registers a deployment as a project.
3
+ The installer for [BitVea Feedback](https://github.com/bitvea/feedback-toolbar): it installs the toolbar into an app, it registers a deployment as a project, and it runs the local-mode receiver.
4
4
 
5
5
  You are not expected to type this package's name.
6
6
  The command everybody uses is the toolbar's:
@@ -20,6 +20,11 @@ Re-running it changes nothing, and a run that cannot find a root layout changes
20
20
  It is what the `prebuild` line runs, and it is a command of its own because a script-tag install has no build to hook.
21
21
  `--optional` reports a failure and exits 0, so a registration problem can never fail somebody's deploy.
22
22
 
23
+ `dev` is the receiver for the toolbar's local mode.
24
+ It listens on `127.0.0.1:4477` and nowhere wider, answers only pages on this machine and origins named with `--allow-origin`, and writes each annotation to `.bitvea/feedback/<id>.json` with its screenshot beside it as `<id>.png` and a regenerated `inbox.md` for a person or a coding agent to read.
25
+ It needs no account and no key, and it sends nothing anywhere.
26
+ `--port` and `--dir` move the port and the repository it writes into.
27
+
23
28
  `help` and `--version` print usage and the versions in play.
24
29
 
25
30
  ## Which toolbar version does `init` pin?
@@ -0,0 +1,9 @@
1
+ export interface RunDevOptions {
2
+ /**
3
+ * Resolves when the receiver should stop. Defaults to the first SIGINT or
4
+ * SIGTERM, which is what a person pressing Ctrl-C sends.
5
+ */
6
+ until?: Promise<void>;
7
+ }
8
+ export declare function runDev(argv: string[], options?: RunDevOptions): Promise<number>;
9
+ //# sourceMappingURL=dev.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dev.d.ts","sourceRoot":"","sources":["../../src/commands/dev.ts"],"names":[],"mappings":"AAsBA,MAAM,WAAW,aAAa;IAC5B;;;OAGG;IACH,KAAK,CAAC,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC;CACvB;AAsBD,wBAAsB,MAAM,CAC1B,IAAI,EAAE,MAAM,EAAE,EACd,OAAO,GAAE,aAAkB,GAC1B,OAAO,CAAC,MAAM,CAAC,CAmEjB"}
@@ -0,0 +1,97 @@
1
+ // `npx @bitvea/feedback-toolbar dev`: the loopback receiver, in the foreground
2
+ // until Ctrl-C.
3
+ //
4
+ // This is the terminal half: flags in, a banner out, one line per annotation,
5
+ // and an exit code. Everything a test needs to drive is in ../dev/server.ts,
6
+ // which this only starts and stops.
7
+ //
8
+ // `node:util`'s `parseArgs` rather than the hand-rolled reader `init` and
9
+ // `register` share, because `--allow-origin` repeats and because a typo in a
10
+ // flag here should be refused rather than ignored: a mistyped
11
+ // `--allow-orgin https://preview.example.com` that silently allowed nothing
12
+ // would read as the toolbar being broken.
13
+ import { resolve } from "node:path";
14
+ import { parseArgs } from "node:util";
15
+ import { parseAllowedOrigin } from "../dev/origins.js";
16
+ import { DEV_DEFAULT_PORT, DEV_HOST, startDevReceiver, } from "../dev/server.js";
17
+ function untilSignalled() {
18
+ return new Promise((done) => {
19
+ const stop = () => {
20
+ process.off("SIGINT", stop);
21
+ process.off("SIGTERM", stop);
22
+ done();
23
+ };
24
+ process.on("SIGINT", stop);
25
+ process.on("SIGTERM", stop);
26
+ });
27
+ }
28
+ function parsePort(raw) {
29
+ if (raw === undefined)
30
+ return DEV_DEFAULT_PORT;
31
+ if (!/^\d{1,5}$/.test(raw) || Number(raw) > 65535) {
32
+ throw new Error(`--port ${JSON.stringify(raw)} is not a port number.`);
33
+ }
34
+ return Number(raw);
35
+ }
36
+ export async function runDev(argv, options = {}) {
37
+ let port;
38
+ let root;
39
+ let allowOrigins;
40
+ try {
41
+ const { values } = parseArgs({
42
+ args: argv.slice(1),
43
+ options: {
44
+ port: { type: "string" },
45
+ dir: { type: "string" },
46
+ "allow-origin": { type: "string", multiple: true },
47
+ },
48
+ strict: true,
49
+ allowPositionals: false,
50
+ });
51
+ port = parsePort(values.port);
52
+ root = resolve(values.dir ?? process.cwd());
53
+ allowOrigins = (values["allow-origin"] ?? []).map(parseAllowedOrigin);
54
+ }
55
+ catch (err) {
56
+ console.error(`Could not start: ${err instanceof Error ? err.message : String(err)}\n` +
57
+ "Run `npx @bitvea/feedback-toolbar help` for the flags.");
58
+ return 1;
59
+ }
60
+ let receiver;
61
+ try {
62
+ receiver = await startDevReceiver({
63
+ root,
64
+ port,
65
+ allowOrigins,
66
+ log: (line) => console.log(line),
67
+ warn: (line) => console.warn(line),
68
+ });
69
+ }
70
+ catch (err) {
71
+ const code = err.code;
72
+ console.error(code === "EADDRINUSE"
73
+ ? `Could not start: ${DEV_HOST}:${port} is already in use. ` +
74
+ "Is another `dev` receiver running? Stop it, or pass --port and " +
75
+ "point the page's data-sink-url at the same port."
76
+ : `Could not start: ${err instanceof Error ? err.message : String(err)}`);
77
+ return 1;
78
+ }
79
+ const lines = [
80
+ `BitVea Feedback local mode: listening on ${receiver.origin} (this machine only).`,
81
+ `Annotations are written to ${receiver.dir}`,
82
+ "Read inbox.md there, or point your coding agent at it.",
83
+ ];
84
+ if (receiver.port !== DEV_DEFAULT_PORT) {
85
+ lines.push(`The toolbar looks on port ${DEV_DEFAULT_PORT} by default; add ` +
86
+ `data-sink-url="${receiver.sinkUrl}" (or the sinkUrl prop) to the page.`);
87
+ }
88
+ if (allowOrigins.length > 0) {
89
+ lines.push(`Also accepting annotations from: ${allowOrigins.join(", ")}`);
90
+ }
91
+ lines.push("Nothing is sent anywhere else. Press Ctrl-C to stop.");
92
+ console.log(lines.join("\n"));
93
+ await (options.until ?? untilSignalled());
94
+ await receiver.close();
95
+ return 0;
96
+ }
97
+ //# sourceMappingURL=dev.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dev.js","sourceRoot":"","sources":["../../src/commands/dev.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,94 @@
1
+ /** The contract version. A version this receiver does not know is refused. */
2
+ export declare const LOCAL_ANNOTATION_VERSION: 1;
3
+ /** Copy of `FEEDBACK_BODY_MAX` in `packages/types/src/feedback.ts`. */
4
+ export declare const ANNOTATION_TEXT_MAX = 5000;
5
+ /** Copy of `SCREENSHOT_DATA_URL_MAX` in `packages/types/src/feedback.ts`. */
6
+ export declare const SCREENSHOT_DATA_URL_MAX = 4000000;
7
+ /**
8
+ * The largest request body the receiver reads, in bytes.
9
+ *
10
+ * Copy of `LOCAL_ANNOTATION_MAX_BYTES` in the toolbar's contract: the largest
11
+ * picture the toolbar will send plus headroom for the words. Refusing less
12
+ * than this would refuse screenshots the toolbar considers ordinary; the
13
+ * toolbar reads a 413 as "the picture was too big" and retries with the text.
14
+ */
15
+ export declare const LOCAL_ANNOTATION_MAX_BYTES: number;
16
+ /** How many owners the component chain may carry, nearest first. */
17
+ export declare const COMPONENT_OWNERS_MAX = 10;
18
+ /**
19
+ * An annotation id. It is also the file name on disk, so this pattern is the
20
+ * whole of the path-traversal defence: no dot, no slash, nothing else.
21
+ */
22
+ export declare const ANNOTATION_ID_PATTERN: RegExp;
23
+ /** The only picture a sink is sent: a PNG, inline. */
24
+ export declare const PNG_DATA_URL_PREFIX = "data:image/png;base64,";
25
+ export type LocalAnnotationKind = "pin" | "note";
26
+ export interface LocalAnnotationBox {
27
+ x: number;
28
+ y: number;
29
+ width: number;
30
+ height: number;
31
+ }
32
+ export interface LocalAnnotationPin {
33
+ offsetX: number;
34
+ offsetY: number;
35
+ box: LocalAnnotationBox | null;
36
+ }
37
+ export interface LocalAnnotationComponent {
38
+ name: string;
39
+ owners: string[];
40
+ }
41
+ export interface LocalAnnotation {
42
+ v: typeof LOCAL_ANNOTATION_VERSION;
43
+ id: string;
44
+ kind: LocalAnnotationKind;
45
+ text: string;
46
+ url: string;
47
+ selector: string | null;
48
+ pin: LocalAnnotationPin | null;
49
+ viewport: {
50
+ width: number;
51
+ height: number;
52
+ devicePixelRatio: number;
53
+ };
54
+ timestamp: string;
55
+ userAgent: string;
56
+ /**
57
+ * A PNG data URL on the way in; on disk, the sibling file's name; in a list
58
+ * response, the root-relative path this receiver serves it from.
59
+ */
60
+ screenshot: string | null;
61
+ component?: LocalAnnotationComponent;
62
+ }
63
+ /**
64
+ * A C0 control character or DEL.
65
+ *
66
+ * Refused in every one-line field, because those fields end up on one line of
67
+ * `inbox.md` and of the receiver's terminal, and a newline or an ESC there is
68
+ * a way to write markup or escape sequences that read as the receiver's own.
69
+ * `new URL()` is no check on it: the WHATWG parser silently STRIPS tabs and
70
+ * newlines before parsing, so the raw string validates with them inside.
71
+ */
72
+ export declare function isControlCharacter(code: number): boolean;
73
+ /**
74
+ * `value` with every control character replaced, except the ones in `keep`.
75
+ * For the two places the receiver writes somebody else's prose: the terminal
76
+ * and `inbox.md`.
77
+ */
78
+ export declare function replaceControlCharacters(value: string, replacement: string, keep?: string): string;
79
+ /**
80
+ * Every problem with `value` as an incoming annotation, or an empty list when
81
+ * it is one. The same rules, in the same order, as the toolbar's
82
+ * `validateLocalAnnotation(value, "send")`.
83
+ */
84
+ export declare function validateAnnotation(value: unknown): string[];
85
+ /**
86
+ * The PNG a validated screenshot data URL carries, or null when the bytes are
87
+ * not a PNG.
88
+ *
89
+ * The prefix alone is a claim, not a fact: this is about to become a file in
90
+ * somebody's repository with a `.png` name, so the bytes are checked for the
91
+ * signature rather than trusted to match the label.
92
+ */
93
+ export declare function decodePngDataUrl(dataUrl: string): Buffer | null;
94
+ //# sourceMappingURL=contract.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"contract.d.ts","sourceRoot":"","sources":["../../src/dev/contract.ts"],"names":[],"mappings":"AAuBA,8EAA8E;AAC9E,eAAO,MAAM,wBAAwB,EAAG,CAAU,CAAC;AAEnD,uEAAuE;AACvE,eAAO,MAAM,mBAAmB,OAAO,CAAC;AAExC,6EAA6E;AAC7E,eAAO,MAAM,uBAAuB,UAAY,CAAC;AAEjD;;;;;;;GAOG;AACH,eAAO,MAAM,0BAA0B,QAAoC,CAAC;AAE5E,oEAAoE;AACpE,eAAO,MAAM,oBAAoB,KAAK,CAAC;AAEvC;;;GAGG;AACH,eAAO,MAAM,qBAAqB,QAA0B,CAAC;AAE7D,sDAAsD;AACtD,eAAO,MAAM,mBAAmB,2BAA2B,CAAC;AAE5D,MAAM,MAAM,mBAAmB,GAAG,KAAK,GAAG,MAAM,CAAC;AAEjD,MAAM,WAAW,kBAAkB;IACjC,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;IACV,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,kBAAkB;IACjC,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,GAAG,EAAE,kBAAkB,GAAG,IAAI,CAAC;CAChC;AAED,MAAM,WAAW,wBAAwB;IACvC,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED,MAAM,WAAW,eAAe;IAC9B,CAAC,EAAE,OAAO,wBAAwB,CAAC;IACnC,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,mBAAmB,CAAC;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,GAAG,EAAE,kBAAkB,GAAG,IAAI,CAAC;IAC/B,QAAQ,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,gBAAgB,EAAE,MAAM,CAAA;KAAE,CAAC;IACtE,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB;;;OAGG;IACH,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,SAAS,CAAC,EAAE,wBAAwB,CAAC;CACtC;AA6BD;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAExD;AASD;;;;GAIG;AACH,wBAAgB,wBAAwB,CACtC,KAAK,EAAE,MAAM,EACb,WAAW,EAAE,MAAM,EACnB,IAAI,SAAK,GACR,MAAM,CASR;AAeD;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,EAAE,CAuI3D;AAOD;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAW/D"}
@@ -0,0 +1,260 @@
1
+ // The local-mode annotation, as the `dev` receiver accepts it.
2
+ //
3
+ // This is the RECEIVING copy of a contract whose sending copy lives in
4
+ // `packages/toolbar/src/local/contract.ts`. Two copies rather than one import,
5
+ // and both halves of that are forced rather than chosen:
6
+ //
7
+ // - this package may not import the toolbar, because the toolbar depends on
8
+ // this package and never the reverse (CLAUDE.md section 1); and
9
+ // - this package has ZERO runtime dependencies, which its publishable check
10
+ // enforces, so the Zod schema a receiver would normally reach for is not
11
+ // available either. The validator is written out by hand, exactly as the
12
+ // browser's is, for the opposite reason: the browser bundle refuses Zod.
13
+ //
14
+ // What holds the two together is the version field, which is what a receiver
15
+ // branches on the day the shape changes, and `contract.test.ts` here, which
16
+ // feeds this validator the same shapes the toolbar's own tests assert it
17
+ // produces. The numbers below are copies too, and each names its original.
18
+ //
19
+ // STRICT ABOUT UNKNOWN FIELDS, on purpose and on both ends: the toolbar's
20
+ // promise is that an annotation carries only what the page itself holds, and a
21
+ // receiver that quietly accepted an extra field would be the half of the
22
+ // contract that stopped noticing when that promise broke.
23
+ /** The contract version. A version this receiver does not know is refused. */
24
+ export const LOCAL_ANNOTATION_VERSION = 1;
25
+ /** Copy of `FEEDBACK_BODY_MAX` in `packages/types/src/feedback.ts`. */
26
+ export const ANNOTATION_TEXT_MAX = 5000;
27
+ /** Copy of `SCREENSHOT_DATA_URL_MAX` in `packages/types/src/feedback.ts`. */
28
+ export const SCREENSHOT_DATA_URL_MAX = 4_000_000;
29
+ /**
30
+ * The largest request body the receiver reads, in bytes.
31
+ *
32
+ * Copy of `LOCAL_ANNOTATION_MAX_BYTES` in the toolbar's contract: the largest
33
+ * picture the toolbar will send plus headroom for the words. Refusing less
34
+ * than this would refuse screenshots the toolbar considers ordinary; the
35
+ * toolbar reads a 413 as "the picture was too big" and retries with the text.
36
+ */
37
+ export const LOCAL_ANNOTATION_MAX_BYTES = SCREENSHOT_DATA_URL_MAX + 500_000;
38
+ /** How many owners the component chain may carry, nearest first. */
39
+ export const COMPONENT_OWNERS_MAX = 10;
40
+ /**
41
+ * An annotation id. It is also the file name on disk, so this pattern is the
42
+ * whole of the path-traversal defence: no dot, no slash, nothing else.
43
+ */
44
+ export const ANNOTATION_ID_PATTERN = /^[A-Za-z0-9_-]{1,64}$/;
45
+ /** The only picture a sink is sent: a PNG, inline. */
46
+ export const PNG_DATA_URL_PREFIX = "data:image/png;base64,";
47
+ const KNOWN_KEYS = new Set([
48
+ "v",
49
+ "id",
50
+ "kind",
51
+ "text",
52
+ "url",
53
+ "selector",
54
+ "pin",
55
+ "viewport",
56
+ "timestamp",
57
+ "userAgent",
58
+ "screenshot",
59
+ "component",
60
+ ]);
61
+ function isRecord(value) {
62
+ return typeof value === "object" && value !== null && !Array.isArray(value);
63
+ }
64
+ function isFiniteNumber(value) {
65
+ return typeof value === "number" && Number.isFinite(value);
66
+ }
67
+ function isFraction(value) {
68
+ return isFiniteNumber(value) && value >= 0 && value <= 1;
69
+ }
70
+ /**
71
+ * A C0 control character or DEL.
72
+ *
73
+ * Refused in every one-line field, because those fields end up on one line of
74
+ * `inbox.md` and of the receiver's terminal, and a newline or an ESC there is
75
+ * a way to write markup or escape sequences that read as the receiver's own.
76
+ * `new URL()` is no check on it: the WHATWG parser silently STRIPS tabs and
77
+ * newlines before parsing, so the raw string validates with them inside.
78
+ */
79
+ export function isControlCharacter(code) {
80
+ return code <= 0x1f || code === 0x7f;
81
+ }
82
+ function isOneLine(value) {
83
+ for (let i = 0; i < value.length; i += 1) {
84
+ if (isControlCharacter(value.charCodeAt(i)))
85
+ return false;
86
+ }
87
+ return true;
88
+ }
89
+ /**
90
+ * `value` with every control character replaced, except the ones in `keep`.
91
+ * For the two places the receiver writes somebody else's prose: the terminal
92
+ * and `inbox.md`.
93
+ */
94
+ export function replaceControlCharacters(value, replacement, keep = "") {
95
+ let out = "";
96
+ for (const char of value) {
97
+ out +=
98
+ isControlCharacter(char.charCodeAt(0)) && !keep.includes(char)
99
+ ? replacement
100
+ : char;
101
+ }
102
+ return out;
103
+ }
104
+ function isHttpUrl(value) {
105
+ if (typeof value !== "string" || value.length === 0 || value.length > 4096) {
106
+ return false;
107
+ }
108
+ if (!isOneLine(value))
109
+ return false;
110
+ try {
111
+ const url = new URL(value);
112
+ return url.protocol === "http:" || url.protocol === "https:";
113
+ }
114
+ catch {
115
+ return false;
116
+ }
117
+ }
118
+ /**
119
+ * Every problem with `value` as an incoming annotation, or an empty list when
120
+ * it is one. The same rules, in the same order, as the toolbar's
121
+ * `validateLocalAnnotation(value, "send")`.
122
+ */
123
+ export function validateAnnotation(value) {
124
+ const errors = [];
125
+ if (!isRecord(value))
126
+ return ["annotation: must be an object"];
127
+ for (const key of Object.keys(value)) {
128
+ if (!KNOWN_KEYS.has(key))
129
+ errors.push(`${key}: unknown field`);
130
+ }
131
+ if (value["v"] !== LOCAL_ANNOTATION_VERSION) {
132
+ errors.push(`v: must be ${LOCAL_ANNOTATION_VERSION}`);
133
+ }
134
+ const id = value["id"];
135
+ if (typeof id !== "string" || !ANNOTATION_ID_PATTERN.test(id)) {
136
+ errors.push("id: must be 1-64 characters of A-Z, a-z, 0-9, _ or -");
137
+ }
138
+ const kind = value["kind"];
139
+ if (kind !== "pin" && kind !== "note") {
140
+ errors.push('kind: must be "pin" or "note"');
141
+ }
142
+ const text = value["text"];
143
+ if (typeof text !== "string" ||
144
+ text.trim().length === 0 ||
145
+ text.length > ANNOTATION_TEXT_MAX) {
146
+ errors.push(`text: must be 1-${ANNOTATION_TEXT_MAX} characters`);
147
+ }
148
+ if (!isHttpUrl(value["url"]))
149
+ errors.push("url: must be an http(s) URL");
150
+ const selector = value["selector"];
151
+ const pin = value["pin"];
152
+ if (selector === null) {
153
+ if (pin !== null)
154
+ errors.push("pin: must be null when selector is null");
155
+ if (kind === "pin")
156
+ errors.push("selector: a pin must carry a selector");
157
+ }
158
+ else if (typeof selector !== "string" ||
159
+ selector.length === 0 ||
160
+ selector.length > 2000 ||
161
+ !isOneLine(selector)) {
162
+ errors.push("selector: must be a 1-2000 character string or null");
163
+ }
164
+ else {
165
+ if (kind === "note")
166
+ errors.push("selector: a note must not carry one");
167
+ if (!isRecord(pin)) {
168
+ errors.push("pin: must be an object when selector is set");
169
+ }
170
+ else {
171
+ if (!isFraction(pin["offsetX"]) || !isFraction(pin["offsetY"])) {
172
+ errors.push("pin: offsetX and offsetY must be between 0 and 1");
173
+ }
174
+ const box = pin["box"];
175
+ if (box !== null &&
176
+ !(isRecord(box) &&
177
+ isFiniteNumber(box["x"]) &&
178
+ isFiniteNumber(box["y"]) &&
179
+ isFiniteNumber(box["width"]) &&
180
+ box["width"] >= 0 &&
181
+ isFiniteNumber(box["height"]) &&
182
+ box["height"] >= 0)) {
183
+ errors.push("pin.box: must be null or {x, y, width, height}");
184
+ }
185
+ }
186
+ }
187
+ const viewport = value["viewport"];
188
+ if (!(isRecord(viewport) &&
189
+ isFiniteNumber(viewport["width"]) &&
190
+ viewport["width"] > 0 &&
191
+ isFiniteNumber(viewport["height"]) &&
192
+ viewport["height"] > 0 &&
193
+ isFiniteNumber(viewport["devicePixelRatio"]) &&
194
+ viewport["devicePixelRatio"] > 0)) {
195
+ errors.push("viewport: must be {width, height, devicePixelRatio}");
196
+ }
197
+ const timestamp = value["timestamp"];
198
+ if (typeof timestamp !== "string" ||
199
+ !isOneLine(timestamp) ||
200
+ Number.isNaN(Date.parse(timestamp))) {
201
+ errors.push("timestamp: must be an ISO 8601 date");
202
+ }
203
+ const userAgent = value["userAgent"];
204
+ if (typeof userAgent !== "string" ||
205
+ userAgent.length > 1000 ||
206
+ !isOneLine(userAgent)) {
207
+ errors.push("userAgent: must be a string of at most 1000 characters");
208
+ }
209
+ const screenshot = value["screenshot"];
210
+ if (screenshot !== null &&
211
+ (typeof screenshot !== "string" ||
212
+ !screenshot.startsWith(PNG_DATA_URL_PREFIX) ||
213
+ screenshot.length > SCREENSHOT_DATA_URL_MAX)) {
214
+ errors.push("screenshot: must be null or a PNG data URL");
215
+ }
216
+ const component = value["component"];
217
+ if (component !== undefined) {
218
+ const owners = isRecord(component) ? component["owners"] : undefined;
219
+ if (!(isRecord(component) &&
220
+ typeof component["name"] === "string" &&
221
+ component["name"].length > 0 &&
222
+ component["name"].length <= 200 &&
223
+ isOneLine(component["name"]) &&
224
+ Array.isArray(owners) &&
225
+ owners.length <= COMPONENT_OWNERS_MAX &&
226
+ owners.every((owner) => typeof owner === "string" &&
227
+ owner.length > 0 &&
228
+ owner.length <= 200 &&
229
+ isOneLine(owner)))) {
230
+ errors.push("component: must be {name, owners[]}");
231
+ }
232
+ }
233
+ return errors;
234
+ }
235
+ /** The eight bytes every PNG starts with. */
236
+ const PNG_SIGNATURE = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
237
+ const BASE64 = /^[A-Za-z0-9+/]*={0,2}$/;
238
+ /**
239
+ * The PNG a validated screenshot data URL carries, or null when the bytes are
240
+ * not a PNG.
241
+ *
242
+ * The prefix alone is a claim, not a fact: this is about to become a file in
243
+ * somebody's repository with a `.png` name, so the bytes are checked for the
244
+ * signature rather than trusted to match the label.
245
+ */
246
+ export function decodePngDataUrl(dataUrl) {
247
+ if (!dataUrl.startsWith(PNG_DATA_URL_PREFIX))
248
+ return null;
249
+ const payload = dataUrl.slice(PNG_DATA_URL_PREFIX.length);
250
+ if (payload.length === 0 || payload.length % 4 !== 0 || !BASE64.test(payload)) {
251
+ return null;
252
+ }
253
+ const bytes = Buffer.from(payload, "base64");
254
+ if (bytes.length < PNG_SIGNATURE.length)
255
+ return null;
256
+ return bytes.subarray(0, PNG_SIGNATURE.length).equals(PNG_SIGNATURE)
257
+ ? bytes
258
+ : null;
259
+ }
260
+ //# sourceMappingURL=contract.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"contract.js","sourceRoot":"","sources":["../../src/dev/contract.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,17 @@
1
+ /** Whether a URL hostname (as `new URL().hostname` spells it) is this machine. */
2
+ export declare function isLoopbackHostname(hostname: string): boolean;
3
+ /**
4
+ * Normalise an origin the developer typed for --allow-origin, or throw a
5
+ * message that says what was wrong with it.
6
+ *
7
+ * An origin is a scheme, a host and a port and nothing else: a path would be
8
+ * silently ignored by the comparison below, which is how somebody believes they
9
+ * allowed one page and in fact allowed a whole site. `*` is refused by name,
10
+ * because this receiver writes files into a repository.
11
+ */
12
+ export declare function parseAllowedOrigin(raw: string): string;
13
+ /** Whether a request's `Origin` header names a page allowed to send here. */
14
+ export declare function isAllowedOrigin(origin: string, extra: ReadonlySet<string>): boolean;
15
+ /** Whether a request's `Host` header addresses this machine. */
16
+ export declare function isLoopbackHostHeader(host: string | undefined): boolean;
17
+ //# sourceMappingURL=origins.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"origins.d.ts","sourceRoot":"","sources":["../../src/dev/origins.ts"],"names":[],"mappings":"AAoBA,kFAAkF;AAClF,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAE5D;AAED;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CA8BtD;AAED,6EAA6E;AAC7E,wBAAgB,eAAe,CAC7B,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,WAAW,CAAC,MAAM,CAAC,GACzB,OAAO,CAaT;AAED,gEAAgE;AAChE,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAStE"}
@@ -0,0 +1,90 @@
1
+ // Who may talk to the `dev` receiver, asked two ways.
2
+ //
3
+ // A process listening on a loopback port is reachable from EVERY page open in
4
+ // the developer's browser, not only from the app they are building: any site
5
+ // can `fetch("http://127.0.0.1:4477/...")`. So binding to 127.0.0.1 keeps the
6
+ // room out and does nothing about the tabs. These two checks are what does.
7
+ //
8
+ // - ORIGIN is the page asking. It must be this machine (localhost,
9
+ // 127.0.0.1 or [::1], any port and either scheme) or one the developer
10
+ // named with --allow-origin. Everything else is refused before a byte of
11
+ // the body is read, and gets no CORS headers, so a browser will not hand
12
+ // the page the answer either.
13
+ // - HOST is the name the request was addressed to. It must be a loopback
14
+ // name. That closes DNS rebinding: a hostile page that re-points its own
15
+ // domain at 127.0.0.1 becomes same-origin with this receiver and sends no
16
+ // Origin on a GET, but it still addresses the request to its own domain,
17
+ // and that is the name this check refuses.
18
+ const LOOPBACK_HOSTNAMES = new Set(["localhost", "127.0.0.1", "[::1]"]);
19
+ /** Whether a URL hostname (as `new URL().hostname` spells it) is this machine. */
20
+ export function isLoopbackHostname(hostname) {
21
+ return LOOPBACK_HOSTNAMES.has(hostname.toLowerCase().replace(/\.$/, ""));
22
+ }
23
+ /**
24
+ * Normalise an origin the developer typed for --allow-origin, or throw a
25
+ * message that says what was wrong with it.
26
+ *
27
+ * An origin is a scheme, a host and a port and nothing else: a path would be
28
+ * silently ignored by the comparison below, which is how somebody believes they
29
+ * allowed one page and in fact allowed a whole site. `*` is refused by name,
30
+ * because this receiver writes files into a repository.
31
+ */
32
+ export function parseAllowedOrigin(raw) {
33
+ const value = raw.trim();
34
+ if (value === "*" || value === "null") {
35
+ throw new Error(`--allow-origin ${value} is refused: name the one origin that may send ` +
36
+ "annotations, such as https://preview.example.com.");
37
+ }
38
+ let url;
39
+ try {
40
+ url = new URL(value);
41
+ }
42
+ catch {
43
+ throw new Error(`--allow-origin ${JSON.stringify(raw)} is not a URL.`);
44
+ }
45
+ if (url.protocol !== "https:" && url.protocol !== "http:") {
46
+ throw new Error(`--allow-origin ${JSON.stringify(raw)} must be http(s).`);
47
+ }
48
+ if (url.username !== "" ||
49
+ url.password !== "" ||
50
+ (url.pathname !== "/" && url.pathname !== "") ||
51
+ url.search !== "" ||
52
+ url.hash !== "") {
53
+ throw new Error(`--allow-origin ${JSON.stringify(raw)} must be an origin only ` +
54
+ `(scheme, host and port), such as ${url.origin}.`);
55
+ }
56
+ return url.origin;
57
+ }
58
+ /** Whether a request's `Origin` header names a page allowed to send here. */
59
+ export function isAllowedOrigin(origin, extra) {
60
+ if (extra.has(origin))
61
+ return true;
62
+ let url;
63
+ try {
64
+ url = new URL(origin);
65
+ }
66
+ catch {
67
+ return false;
68
+ }
69
+ // `new URL("http://x").origin` round-trips a real Origin header exactly; a
70
+ // header with a path or credentials in it is not one a browser sent.
71
+ if (url.origin !== origin)
72
+ return false;
73
+ if (url.protocol !== "http:" && url.protocol !== "https:")
74
+ return false;
75
+ return isLoopbackHostname(url.hostname);
76
+ }
77
+ /** Whether a request's `Host` header addresses this machine. */
78
+ export function isLoopbackHostHeader(host) {
79
+ if (!host || /[@/\\?#\s]/.test(host))
80
+ return false;
81
+ let url;
82
+ try {
83
+ url = new URL(`http://${host}`);
84
+ }
85
+ catch {
86
+ return false;
87
+ }
88
+ return isLoopbackHostname(url.hostname);
89
+ }
90
+ //# sourceMappingURL=origins.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"origins.js","sourceRoot":"","sources":["../../src/dev/origins.ts"],"names":[],"mappings":"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"}