@weatherboard/annotation 0.2.0 → 0.2.2

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.
@@ -87,6 +87,52 @@ export interface SendReceipt {
87
87
  * outcome the receipt is already designed to survive.
88
88
  */
89
89
  export declare function safeIssueUrl(value: unknown): string | undefined;
90
+ /**
91
+ * A refusal sentence from the network that is safe to put in front of a
92
+ * reviewer (A-285).
93
+ *
94
+ * `safeIssueUrl`'s argument, applied to prose. The ingest composes refusals a
95
+ * human can act on — a link issued for a different deployment, an empty
96
+ * comment — and until this slice the transport threw `HTTP 403` and discarded
97
+ * every one of them, so **not one of those sentences had ever reached a
98
+ * human.**
99
+ *
100
+ * The value is still untrusted, and it ends up as `textContent` in the
101
+ * CUSTOMER'S OWN PAGE. `textContent` is not an injection seam the way `href`
102
+ * is, so the risk here is not script execution — it is a hostile or broken
103
+ * ingest writing a screenful of control characters, ANSI escapes or a whole
104
+ * JSON document into a toast the reviewer cannot dismiss. So: strings only,
105
+ * control characters dropped, whitespace collapsed, and a hard ceiling.
106
+ *
107
+ * Anything that fails is dropped rather than repaired, and the caller falls
108
+ * back to the HTTP status — which is what every refusal rendered as before
109
+ * this existed, so the floor cannot get worse.
110
+ */
111
+ export declare function safeRefusalReason(value: unknown): string | undefined;
112
+ /**
113
+ * The error a refused capture throws, carrying the reason as a FIELD.
114
+ *
115
+ * The reason is in the `message` too, prefixed for a log. A renderer that had
116
+ * to recover it by string-slicing that message would be parsing our own prose,
117
+ * which breaks the first time somebody edits the prefix — so the sentence a
118
+ * human should see is a property, and `refusalReason` is the one way to read
119
+ * it.
120
+ */
121
+ export interface IngestRefusal extends Error {
122
+ /** The ingest's own words, already through `safeRefusalReason`. */
123
+ reason?: string;
124
+ /** The status, always — it is the fallback and it is never absent. */
125
+ status?: number;
126
+ }
127
+ /**
128
+ * The reviewer-facing sentence for a failed capture, or `undefined`.
129
+ *
130
+ * SANITISED AGAIN, deliberately, exactly as `safeIssueUrl` is called at both
131
+ * ends: this reads an `EgressOutcome.error` from WHATEVER transport produced
132
+ * it, including one a consumer wrote, which never passed through
133
+ * `httpTransport`. One function, so the two ends cannot disagree.
134
+ */
135
+ export declare function refusalReason(error: unknown): string | undefined;
90
136
  export interface AnnotationTransport {
91
137
  /** For diagnostics and for tests; never sent anywhere. */
92
138
  readonly name: string;
@@ -87,6 +87,52 @@ export interface SendReceipt {
87
87
  * outcome the receipt is already designed to survive.
88
88
  */
89
89
  export declare function safeIssueUrl(value: unknown): string | undefined;
90
+ /**
91
+ * A refusal sentence from the network that is safe to put in front of a
92
+ * reviewer (A-285).
93
+ *
94
+ * `safeIssueUrl`'s argument, applied to prose. The ingest composes refusals a
95
+ * human can act on — a link issued for a different deployment, an empty
96
+ * comment — and until this slice the transport threw `HTTP 403` and discarded
97
+ * every one of them, so **not one of those sentences had ever reached a
98
+ * human.**
99
+ *
100
+ * The value is still untrusted, and it ends up as `textContent` in the
101
+ * CUSTOMER'S OWN PAGE. `textContent` is not an injection seam the way `href`
102
+ * is, so the risk here is not script execution — it is a hostile or broken
103
+ * ingest writing a screenful of control characters, ANSI escapes or a whole
104
+ * JSON document into a toast the reviewer cannot dismiss. So: strings only,
105
+ * control characters dropped, whitespace collapsed, and a hard ceiling.
106
+ *
107
+ * Anything that fails is dropped rather than repaired, and the caller falls
108
+ * back to the HTTP status — which is what every refusal rendered as before
109
+ * this existed, so the floor cannot get worse.
110
+ */
111
+ export declare function safeRefusalReason(value: unknown): string | undefined;
112
+ /**
113
+ * The error a refused capture throws, carrying the reason as a FIELD.
114
+ *
115
+ * The reason is in the `message` too, prefixed for a log. A renderer that had
116
+ * to recover it by string-slicing that message would be parsing our own prose,
117
+ * which breaks the first time somebody edits the prefix — so the sentence a
118
+ * human should see is a property, and `refusalReason` is the one way to read
119
+ * it.
120
+ */
121
+ export interface IngestRefusal extends Error {
122
+ /** The ingest's own words, already through `safeRefusalReason`. */
123
+ reason?: string;
124
+ /** The status, always — it is the fallback and it is never absent. */
125
+ status?: number;
126
+ }
127
+ /**
128
+ * The reviewer-facing sentence for a failed capture, or `undefined`.
129
+ *
130
+ * SANITISED AGAIN, deliberately, exactly as `safeIssueUrl` is called at both
131
+ * ends: this reads an `EgressOutcome.error` from WHATEVER transport produced
132
+ * it, including one a consumer wrote, which never passed through
133
+ * `httpTransport`. One function, so the two ends cannot disagree.
134
+ */
135
+ export declare function refusalReason(error: unknown): string | undefined;
90
136
  export interface AnnotationTransport {
91
137
  /** For diagnostics and for tests; never sent anywhere. */
92
138
  readonly name: string;
@@ -87,6 +87,52 @@ export interface SendReceipt {
87
87
  * outcome the receipt is already designed to survive.
88
88
  */
89
89
  export declare function safeIssueUrl(value: unknown): string | undefined;
90
+ /**
91
+ * A refusal sentence from the network that is safe to put in front of a
92
+ * reviewer (A-285).
93
+ *
94
+ * `safeIssueUrl`'s argument, applied to prose. The ingest composes refusals a
95
+ * human can act on — a link issued for a different deployment, an empty
96
+ * comment — and until this slice the transport threw `HTTP 403` and discarded
97
+ * every one of them, so **not one of those sentences had ever reached a
98
+ * human.**
99
+ *
100
+ * The value is still untrusted, and it ends up as `textContent` in the
101
+ * CUSTOMER'S OWN PAGE. `textContent` is not an injection seam the way `href`
102
+ * is, so the risk here is not script execution — it is a hostile or broken
103
+ * ingest writing a screenful of control characters, ANSI escapes or a whole
104
+ * JSON document into a toast the reviewer cannot dismiss. So: strings only,
105
+ * control characters dropped, whitespace collapsed, and a hard ceiling.
106
+ *
107
+ * Anything that fails is dropped rather than repaired, and the caller falls
108
+ * back to the HTTP status — which is what every refusal rendered as before
109
+ * this existed, so the floor cannot get worse.
110
+ */
111
+ export declare function safeRefusalReason(value: unknown): string | undefined;
112
+ /**
113
+ * The error a refused capture throws, carrying the reason as a FIELD.
114
+ *
115
+ * The reason is in the `message` too, prefixed for a log. A renderer that had
116
+ * to recover it by string-slicing that message would be parsing our own prose,
117
+ * which breaks the first time somebody edits the prefix — so the sentence a
118
+ * human should see is a property, and `refusalReason` is the one way to read
119
+ * it.
120
+ */
121
+ export interface IngestRefusal extends Error {
122
+ /** The ingest's own words, already through `safeRefusalReason`. */
123
+ reason?: string;
124
+ /** The status, always — it is the fallback and it is never absent. */
125
+ status?: number;
126
+ }
127
+ /**
128
+ * The reviewer-facing sentence for a failed capture, or `undefined`.
129
+ *
130
+ * SANITISED AGAIN, deliberately, exactly as `safeIssueUrl` is called at both
131
+ * ends: this reads an `EgressOutcome.error` from WHATEVER transport produced
132
+ * it, including one a consumer wrote, which never passed through
133
+ * `httpTransport`. One function, so the two ends cannot disagree.
134
+ */
135
+ export declare function refusalReason(error: unknown): string | undefined;
90
136
  export interface AnnotationTransport {
91
137
  /** For diagnostics and for tests; never sent anywhere. */
92
138
  readonly name: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@weatherboard/annotation",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "Click an element in a running app, get the source location that rendered it.",
5
5
  "keywords": [
6
6
  "annotation",
@@ -67,10 +67,10 @@
67
67
  "LICENSE"
68
68
  ],
69
69
  "//sizeBudget": "Gzipped bytes per published entry point, checked by `build.mjs` on every build and therefore in CI. A budget is here rather than in a separate config because the number is part of the package's contract: this code runs on every page of every managed product, and 'it grew a bit each time' is how a 4KB SDK becomes a 40KB one with nobody having decided. Raising a number is allowed; doing it in the same commit as the growth, where a reviewer sees both, is the point.",
70
- "//sizeBudget-index": "A-241 adds arming: 8700 -> 9400 for index and 700 -> 850 for protocol. MEASURED by rebuilding, four numbers. Baseline at A-240's merge commit fceb183: index 8562, protocol 646 - the two figures the previous note recorded, re-measured rather than trusted. As shipped: index 9281, protocol 788. And with the ./arming re-export block deleted from src/index.ts but everything else present: 8913. SO readArming AND ITS TYPES ARE 368 OF THE 719, AND 351 ARE NOT: 142 of that is protocol's own growth (armedCookie, clearedCookie, ANNOTATION_ARMED_VALUE, REVIEW_RUN_PARAM), which index pays too because it re-exports protocol whole, and the rest is isAnnotatableBuild with its ANNOTATABLE_ENVS allowlist, the two-gate expression in mountAnnotation, and Done's teardown. That 351 is the number worth knowing: a consumer who never calls readArming still pays it, because the kill-switch is not optional and must not be tree-shakeable. TWO REVIEW CYCLES MOVED IT AND BOTH COSTS ARE MEASURED: the fail-closed allowlist +27 (9239 -> 9266) and Done's teardown +15 (9266 -> 9281). HEADROOM IS PER ENTRY POINT AND THE TWO ARE STATED SEPARATELY, because one figure for both was false here once already: index 9400/9281 = 1.28%, against A-240's 8700/8562 = 1.61%, so tighter and DELIBERATELY NOT TOPPED UP - the budget's job is to make the next growth a decision, and quietly restoring slack each time is how it stops doing that; protocol 850/788 = 7.87%, against its previous 700/646 = 8.36%. protocol runs looser in PROPORTION only because it is small - 62 bytes is 7.87% there and would be 0.67% of index. The argument for the toolbar's own 2553 bytes is in docs/decisions/a-240-annotation-toolbar-size.md, extended there for this slice rather than restated here.",
70
+ "//sizeBudget-index": "A-241 adds arming: 8700 -> 9400 for index and 700 -> 850 for protocol. MEASURED by rebuilding, four numbers. Baseline at A-240's merge commit fceb183: index 8562, protocol 646 - the two figures the previous note recorded, re-measured rather than trusted. As shipped: index 9281, protocol 788. And with the ./arming re-export block deleted from src/index.ts but everything else present: 8913. SO readArming AND ITS TYPES ARE 368 OF THE 719, AND 351 ARE NOT: 142 of that is protocol's own growth (armedCookie, clearedCookie, ANNOTATION_ARMED_VALUE, REVIEW_RUN_PARAM), which index pays too because it re-exports protocol whole, and the rest is isAnnotatableBuild with its ANNOTATABLE_ENVS allowlist, the two-gate expression in mountAnnotation, and Done's teardown. That 351 is the number worth knowing: a consumer who never calls readArming still pays it, because the kill-switch is not optional and must not be tree-shakeable. TWO REVIEW CYCLES MOVED IT AND BOTH COSTS ARE MEASURED: the fail-closed allowlist +27 (9239 -> 9266) and Done's teardown +15 (9266 -> 9281). HEADROOM IS PER ENTRY POINT AND THE TWO ARE STATED SEPARATELY, because one figure for both was false here once already: index 9400/9281 = 1.28%, against A-240's 8700/8562 = 1.61%, so tighter and DELIBERATELY NOT TOPPED UP - the budget's job is to make the next growth a decision, and quietly restoring slack each time is how it stops doing that; protocol 850/788 = 7.87%, against its previous 700/646 = 8.36%. protocol runs looser in PROPORTION only because it is small - 62 bytes is 7.87% there and would be 0.67% of index. The argument for the toolbar's own 2553 bytes is in docs/decisions/a-240-annotation-toolbar-size.md, extended there for this slice rather than restated here. A-254 MOVED IT AGAIN AND THE BUDGET IS DELIBERATELY NOT RAISED: index 9281 -> 9300, +19 gzipped bytes for the armed-before-persist early return in readArming, re-measured by running build.mjs before and after. Headroom is now 9400/9300 = 1.06%. That is tight on purpose - 19 bytes bought the end of an unarmed page storing an attacker-supplied capture token, and the next growth after it should be a decision somebody makes rather than slack somebody spends. A-285 MOVED IT AGAIN AND THE BUDGET IS RAISED, WITH THE COST SPLIT AND MEASURED: index 9300 -> 9471, +171 gzipped bytes, by rebuilding three times. 9300 is the baseline at this branch point, re-measured rather than trusted. 9426 with transport.ts alone - safeRefusalReason, the IngestRefusal fields and the body read on the refusal branch, 126 bytes. 9471 with the toolbar rendering it and index re-exporting the two functions, a further 45. THAT 171 IS WHAT IT COSTS FOR A REFUSAL TO REACH A HUMAN AT ALL: before it, every sentence the ingest composed - the link issued for another deployment, the empty comment - was thrown away by the only client that could show it, and every failure read 'That did not send'. 9550 rather than 9500: 9500 would leave 29 bytes, which is a budget that fails on the next comment somebody writes rather than on the next decision somebody makes. 9550/9471 = 0.83%, against A-254's 9400/9300 = 1.06% - so still tighter than it was, and deliberately not topped up.",
71
71
  "sizeBudget": {
72
72
  "protocol": 850,
73
- "index": 9400,
73
+ "index": 9550,
74
74
  "host": 660
75
75
  },
76
76
  "engines": {