@weatherboard/annotation 0.4.0 → 0.7.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/ADOPTING.md +147 -10
- package/COMPATIBILITY.md +29 -0
- package/README.md +84 -10
- package/dist/anchor.cjs +1 -1
- package/dist/anchor.cjs.map +2 -2
- package/dist/anchor.mjs.map +2 -2
- package/dist/arming.d.cts +18 -21
- package/dist/arming.d.mts +18 -21
- package/dist/arming.d.ts +18 -21
- package/dist/bootstrap-runtime.d.cts +30 -0
- package/dist/bootstrap-runtime.d.mts +30 -0
- package/dist/bootstrap-runtime.d.ts +30 -0
- package/dist/bootstrap.cjs +2 -0
- package/dist/bootstrap.cjs.map +7 -0
- package/dist/bootstrap.d.cts +8 -0
- package/dist/bootstrap.d.mts +8 -0
- package/dist/bootstrap.d.ts +8 -0
- package/dist/bootstrap.mjs +2 -0
- package/dist/bootstrap.mjs.map +7 -0
- package/dist/host.cjs +1 -1
- package/dist/host.cjs.map +3 -3
- package/dist/host.mjs +1 -1
- package/dist/host.mjs.map +3 -3
- package/dist/index.cjs +4 -4
- package/dist/index.cjs.map +4 -4
- package/dist/index.d.cts +10 -4
- package/dist/index.d.mts +10 -4
- package/dist/index.d.ts +10 -4
- package/dist/index.mjs +4 -4
- package/dist/index.mjs.map +4 -4
- package/dist/markers.cjs +2 -2
- package/dist/markers.cjs.map +2 -2
- package/dist/markers.mjs +2 -2
- package/dist/markers.mjs.map +2 -2
- package/dist/page-capture.cjs +2 -0
- package/dist/page-capture.cjs.map +7 -0
- package/dist/page-capture.d.cts +115 -0
- package/dist/page-capture.d.mts +115 -0
- package/dist/page-capture.d.ts +115 -0
- package/dist/page-capture.mjs +2 -0
- package/dist/page-capture.mjs.map +7 -0
- package/dist/protocol.cjs +1 -1
- package/dist/protocol.cjs.map +3 -3
- package/dist/protocol.d.cts +19 -0
- package/dist/protocol.d.mts +19 -0
- package/dist/protocol.d.ts +19 -0
- package/dist/protocol.mjs +1 -1
- package/dist/protocol.mjs.map +3 -3
- package/dist/read.cjs +1 -1
- package/dist/read.cjs.map +2 -2
- package/dist/read.mjs +1 -1
- package/dist/read.mjs.map +2 -2
- package/dist/screenshot.d.cts +56 -7
- package/dist/screenshot.d.mts +56 -7
- package/dist/screenshot.d.ts +56 -7
- package/dist/selection.cjs +5 -0
- package/dist/selection.cjs.map +7 -0
- package/dist/selection.d.cts +27 -0
- package/dist/selection.d.mts +27 -0
- package/dist/selection.d.ts +27 -0
- package/dist/selection.mjs +5 -0
- package/dist/selection.mjs.map +7 -0
- package/dist/session.d.cts +17 -0
- package/dist/session.d.mts +17 -0
- package/dist/session.d.ts +17 -0
- package/dist/shortcut.d.cts +52 -0
- package/dist/shortcut.d.mts +52 -0
- package/dist/shortcut.d.ts +52 -0
- package/dist/toolbar.d.cts +13 -0
- package/dist/toolbar.d.mts +13 -0
- package/dist/toolbar.d.ts +13 -0
- package/dist/transport.d.cts +10 -0
- package/dist/transport.d.mts +10 -0
- package/dist/transport.d.ts +10 -0
- package/package.json +45 -10
package/ADOPTING.md
CHANGED
|
@@ -41,8 +41,12 @@ npm i @weatherboard/annotation
|
|
|
41
41
|
npm i -D @weatherboard/annotation-next
|
|
42
42
|
```
|
|
43
43
|
|
|
44
|
-
The runtime package has **
|
|
45
|
-
|
|
44
|
+
The runtime package has **one dependency**, `modern-screenshot` (MIT), and ships
|
|
45
|
+
in your app. It is the approved exception to this package's no-dependencies
|
|
46
|
+
rule (A-2385): it renders the viewport screenshot, and it is loaded with a
|
|
47
|
+
dynamic `import()` at capture time only, so your bundler splits it into its own
|
|
48
|
+
chunk and an armed page that is never clicked never fetches it. The transform
|
|
49
|
+
runs only in your build.
|
|
46
50
|
|
|
47
51
|
**What breaks:** `-D` on the second one is not cosmetic. `annotation-next`
|
|
48
52
|
depends on a Babel parser, and npm installs a package's `dependencies` whichever
|
|
@@ -98,23 +102,40 @@ pointed at production anyway.
|
|
|
98
102
|
|
|
99
103
|
```ts
|
|
100
104
|
// middleware.ts
|
|
101
|
-
import {
|
|
105
|
+
import {
|
|
106
|
+
ANNOTATION_ARMED_COOKIE,
|
|
107
|
+
ANNOTATION_ARMED_COOKIES,
|
|
108
|
+
ANNOTATION_TOKEN_PARAMS,
|
|
109
|
+
readAnnotationToken,
|
|
110
|
+
} from "@weatherboard/annotation/protocol";
|
|
102
111
|
|
|
103
112
|
const presented = readAnnotationToken(request.nextUrl.searchParams);
|
|
104
113
|
const expected = process.env.ANNOTATION_TOKEN;
|
|
114
|
+
const attempted = ANNOTATION_TOKEN_PARAMS.some((name) => request.nextUrl.searchParams.has(name));
|
|
115
|
+
const overHttps = request.nextUrl.protocol === "https:";
|
|
105
116
|
|
|
106
117
|
if (expected && presented && presented.length === expected.length && presented === expected) {
|
|
107
|
-
const overHttps = request.nextUrl.protocol === "https:";
|
|
108
118
|
response.cookies.set(ANNOTATION_ARMED_COOKIE, "1", {
|
|
109
119
|
httpOnly: false, // the client bundle is what reads it
|
|
110
120
|
sameSite: overHttps ? "none" : "lax",
|
|
111
121
|
secure: overHttps,
|
|
112
122
|
path: "/",
|
|
113
123
|
});
|
|
124
|
+
} else if (attempted) {
|
|
125
|
+
for (const name of ANNOTATION_ARMED_COOKIES) {
|
|
126
|
+
response.cookies.set(name, "", {
|
|
127
|
+
httpOnly: false,
|
|
128
|
+
sameSite: overHttps ? "none" : "lax",
|
|
129
|
+
secure: overHttps,
|
|
130
|
+
path: "/",
|
|
131
|
+
maxAge: 0,
|
|
132
|
+
expires: new Date(0),
|
|
133
|
+
});
|
|
134
|
+
}
|
|
114
135
|
}
|
|
115
136
|
```
|
|
116
137
|
|
|
117
|
-
|
|
138
|
+
Five details, each of which has cost somebody a day:
|
|
118
139
|
|
|
119
140
|
- **The cookie holds `1`, never the token.** It records that the token was
|
|
120
141
|
presented once, so navigation inside the frame stays armed without the secret
|
|
@@ -127,6 +148,10 @@ Four details, each of which has cost somebody a day:
|
|
|
127
148
|
- **…but `None` is only honoured with `Secure`**, and `Secure` is dropped over
|
|
128
149
|
plain http — so a local `next dev` would set a cookie the browser silently
|
|
129
150
|
discards. Fall back to `Lax`, which is same-origin anyway.
|
|
151
|
+
- **A fresh rejected token expires both cookie spellings.** A browser may still
|
|
152
|
+
carry a valid cookie from an earlier link. Leaving it in place turns the new
|
|
153
|
+
rejection into an accepted launch and lets the old capture/read/run context
|
|
154
|
+
survive under a different review link.
|
|
130
155
|
|
|
131
156
|
### First-party, the cookie is an ordinary one — use `armedCookie()` (A-241)
|
|
132
157
|
|
|
@@ -140,14 +165,65 @@ Do not re-derive the attributes. `@weatherboard/annotation/protocol` exports the
|
|
|
140
165
|
cookie itself, so the two halves cannot disagree:
|
|
141
166
|
|
|
142
167
|
```ts
|
|
143
|
-
import {
|
|
168
|
+
import {
|
|
169
|
+
armedCookie,
|
|
170
|
+
clearedCookie,
|
|
171
|
+
ANNOTATION_ARMED_COOKIES,
|
|
172
|
+
ANNOTATION_TOKEN_PARAMS,
|
|
173
|
+
readAnnotationToken,
|
|
174
|
+
} from "@weatherboard/annotation/protocol";
|
|
144
175
|
|
|
145
|
-
|
|
146
|
-
|
|
176
|
+
const presented = readAnnotationToken(request.nextUrl.searchParams);
|
|
177
|
+
const attempted = ANNOTATION_TOKEN_PARAMS.some((name) => request.nextUrl.searchParams.has(name));
|
|
178
|
+
if (presented && presented === process.env.ANNOTATION_TOKEN) {
|
|
179
|
+
response.headers.append("set-cookie", armedCookie());
|
|
180
|
+
} else if (attempted) {
|
|
181
|
+
for (const name of ANNOTATION_ARMED_COOKIES) {
|
|
182
|
+
response.headers.append("set-cookie", clearedCookie(name));
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
Put the package bootstrap in the first raw script in `<head>`, before styles,
|
|
188
|
+
analytics and hydration:
|
|
189
|
+
|
|
190
|
+
```tsx
|
|
191
|
+
import { annotationBootstrapScript } from "@weatherboard/annotation/bootstrap";
|
|
192
|
+
|
|
193
|
+
<script dangerouslySetInnerHTML={{ __html: annotationBootstrapScript() }} />
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
The script synchronously strips `ai_annotate`, its legacy spelling,
|
|
197
|
+
`ai_capture`, `ai_read` and `ai_run`. When middleware accepted the arming link,
|
|
198
|
+
it keeps capture/read/run in this tab's `sessionStorage`, so same-tab routing
|
|
199
|
+
and a hard reload remain armed after the URL is clean. When middleware rejected
|
|
200
|
+
the link, it persists only `{ status: "rejected", capture: "present" | "missing" }`.
|
|
201
|
+
No credential from an unarmed link is stored.
|
|
202
|
+
|
|
203
|
+
The client mount can consume that signal once and report a generic failure:
|
|
204
|
+
|
|
205
|
+
```ts
|
|
206
|
+
import {
|
|
207
|
+
consumeAnnotationAttempt,
|
|
208
|
+
reportAnnotationStartupFailure,
|
|
209
|
+
} from "@weatherboard/annotation/bootstrap";
|
|
210
|
+
|
|
211
|
+
const attempt = consumeAnnotationAttempt();
|
|
212
|
+
if (attempt?.status === "rejected") {
|
|
213
|
+
return reportAnnotationStartupFailure("rejected-arming");
|
|
214
|
+
}
|
|
215
|
+
if (attempt?.capture === "missing") {
|
|
216
|
+
return reportAnnotationStartupFailure("missing-capture");
|
|
217
|
+
}
|
|
147
218
|
```
|
|
148
219
|
|
|
149
|
-
|
|
150
|
-
|
|
220
|
+
The reporter owns one host, `#ai-annotation-startup-status`, and returns a
|
|
221
|
+
cleanup function. Import and mount failures use `"import-failed"` and
|
|
222
|
+
`"mount-failed"`. All copy is package-owned and contains no token or product
|
|
223
|
+
name.
|
|
224
|
+
|
|
225
|
+
It takes no value argument on purpose — the middleware's arming cookie holds a
|
|
226
|
+
marker, never the token. The page half calls
|
|
151
227
|
`readArming()`, which reads that cookie, strips the tokens out of the URL with
|
|
152
228
|
`replaceState`, and hands the capture token to `httpTransport`.
|
|
153
229
|
|
|
@@ -268,6 +344,32 @@ dynamic import costs a chunk fetch, so the first few milliseconds of an armed
|
|
|
268
344
|
page are genuinely unobserved. That is the trade. What you must *not* do is defer
|
|
269
345
|
the collector to click time, which observes nothing at all.
|
|
270
346
|
|
|
347
|
+
### The keyboard shortcut — install it before the armed check (A-2384)
|
|
348
|
+
|
|
349
|
+
The mount above returns on an unarmed page before importing anything, so a key
|
|
350
|
+
listener inside `mountAnnotation` cannot hear the chord there. Install the
|
|
351
|
+
pre-arm listener from `./bootstrap` **above** that return, on every page:
|
|
352
|
+
|
|
353
|
+
```ts
|
|
354
|
+
import { DEFAULT_HOTKEY, installAnnotationShortcut } from "@weatherboard/annotation/bootstrap";
|
|
355
|
+
|
|
356
|
+
useEffect(() => installAnnotationShortcut({
|
|
357
|
+
build,
|
|
358
|
+
armUrl: process.env.NEXT_PUBLIC_ANNOTATION_ARM_URL!, // https://<studio>/annotate/arm?product=<id>&target=staging
|
|
359
|
+
hotkey: DEFAULT_HOTKEY,
|
|
360
|
+
}), []);
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
and pass the same `hotkey` to `mountAnnotation`. There is no default `armUrl`,
|
|
364
|
+
for the reason step 5 gives for origins. Studio's route does the signed-in
|
|
365
|
+
reviewer check and refuses anything but the registered staging target; your
|
|
366
|
+
middleware still decides arming from the token it is sent back with. A
|
|
367
|
+
production build installs no listener.
|
|
368
|
+
|
|
369
|
+
**What breaks:** installed below the `isAnnotationArmed` return, the chord
|
|
370
|
+
works only on a page that is already armed, which looks like a shortcut that
|
|
371
|
+
"sometimes" works.
|
|
372
|
+
|
|
271
373
|
## 7. `parentOrigin` is one address, not a list
|
|
272
374
|
|
|
273
375
|
`postMessage`'s second argument is **not an allowlist**. It is an address, and
|
|
@@ -490,3 +592,38 @@ misattributed.
|
|
|
490
592
|
Every step above is a thing that, when absent, produces no error, no warning and
|
|
491
593
|
no log line — only an absence. When you are debugging, prefer the check that
|
|
492
594
|
distinguishes causes over the check that confirms your guess.
|
|
595
|
+
|
|
596
|
+
## Host-owned contextual feedback
|
|
597
|
+
|
|
598
|
+
`@weatherboard/annotation/selection` exposes `relativePoint`,
|
|
599
|
+
`captureTextSelection`, `resolveTextSelection` and `prepareTextSelections` without
|
|
600
|
+
arming the developer SDK, mounting UI or sending network requests. Production
|
|
601
|
+
hosts such as Pitch own authentication, capture intent, target persistence and
|
|
602
|
+
permission-scoped reads. Import this subpath directly; importing the root SDK
|
|
603
|
+
still observes its existing production restrictions.
|
|
604
|
+
|
|
605
|
+
Mark authored text containers with `data-annotation-text="host-field-path"`.
|
|
606
|
+
Capture returns UTF-16 offsets, exact quote and bounded prefix/suffix using CRLF
|
|
607
|
+
normalisation only. A field path is evidence qualified by a host content revision,
|
|
608
|
+
not a permanent identity. Pass whether content is unchanged when resolving;
|
|
609
|
+
edited content requires a unique quote/context match. Missing or ambiguous
|
|
610
|
+
locations must remain reviewable as unplaced feedback. `prepareTextSelections`
|
|
611
|
+
shares one bounded text-map snapshot across annotations in a layout frame; discard
|
|
612
|
+
it after content or layout changes. Neither helper reads application controls as
|
|
613
|
+
authored text or persists DOM nodes.
|
|
614
|
+
|
|
615
|
+
## After adopting: prove the capture end to end on staging
|
|
616
|
+
|
|
617
|
+
A package release cannot prove its own capture path, because that needs a host
|
|
618
|
+
that consumes the published version. After a product adopts it on staging:
|
|
619
|
+
|
|
620
|
+
1. Arm at the Studio arm endpoint, press the chord, and file one capture with
|
|
621
|
+
its viewport screenshot.
|
|
622
|
+
2. Confirm the capture reached ingest with the screenshot attached, and that the
|
|
623
|
+
filed issue carries the image.
|
|
624
|
+
3. For the annotate-to-merge loop, the product's registry row must let
|
|
625
|
+
`workflowAutomationDecision` say yes for that origin: its preview host,
|
|
626
|
+
image ref and base SHA. Then confirm one `annotation_workflows` row and a
|
|
627
|
+
sandbox run that opens a pull request.
|
|
628
|
+
4. Record the capture id, the issue, the run id and the pull request on the
|
|
629
|
+
adoption issue.
|
package/COMPATIBILITY.md
CHANGED
|
@@ -95,3 +95,32 @@ whoever drops it, in the same change, and not by whoever finds out.
|
|
|
95
95
|
|
|
96
96
|
`0.x` while the wire protocol is still moving. **1.0 is the statement that a
|
|
97
97
|
hundred consumers can rely on it**, not a way to look finished.
|
|
98
|
+
|
|
99
|
+
### Additive selection subpath (A-391)
|
|
100
|
+
|
|
101
|
+
The workspace adds `./selection` with ESM, CommonJS and TypeScript exports.
|
|
102
|
+
Existing SDK wire versions and production arming rules are unchanged. Pitch uses
|
|
103
|
+
the workspace source; external consumers need a subsequently published package
|
|
104
|
+
version containing this entry point. The tarball smoke test verifies its exports;
|
|
105
|
+
this feature does not itself publish an npm release.
|
|
106
|
+
|
|
107
|
+
### In-app shortcut and cross-tab arming (A-2384)
|
|
108
|
+
|
|
109
|
+
Version 0.7.0 adds `hotkey` to `mountAnnotation` and `installAnnotationShortcut`
|
|
110
|
+
to `./bootstrap`. Both are opt-in: absent `hotkey` means what it meant before, an
|
|
111
|
+
immediately visible toolbar. Passing a chord (`DEFAULT_HOTKEY` is
|
|
112
|
+
`Mod+Shift+Period`) starts an armed toolbar dormant and toggles it. The
|
|
113
|
+
bootstrap listener re-arms through Studio's `/annotate/arm` when no fresh
|
|
114
|
+
mounted toolbar claims the chord, and carries an open intent in session storage
|
|
115
|
+
so the next armed mount opens. `showResult` attaches a dormant toolbar.
|
|
116
|
+
`readArming` copies accepted capture and read tokens into a secure, expiring
|
|
117
|
+
first-party cookie; a new tab uses it when its session storage has no token.
|
|
118
|
+
The arming marker remains the middleware's decision, and the console still
|
|
119
|
+
verifies tokens at its endpoint.
|
|
120
|
+
|
|
121
|
+
### Additive bootstrap subpath and hosted-mode metadata (A-1375)
|
|
122
|
+
|
|
123
|
+
Version 0.6.0 adds `./bootstrap` for synchronous credential stripping,
|
|
124
|
+
per-tab continuation and token-free startup status. It also adds the exact
|
|
125
|
+
`x-weatherboard-annotation-mode: first-party` constants to `./protocol`. The
|
|
126
|
+
wire payload version and all existing names remain unchanged.
|
package/README.md
CHANGED
|
@@ -11,8 +11,11 @@ sentence. What comes back is not a screenshot and a guess — it is the file and
|
|
|
11
11
|
line that rendered that element, the ancestors that led to it, the console
|
|
12
12
|
errors and failed requests from that session, and the build the page came from.
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
One runtime dependency, by exception: `modern-screenshot` (MIT), which renders
|
|
15
|
+
the viewport into the screenshot a capture carries. It is loaded with a dynamic
|
|
16
|
+
`import()` on the first capture only, so a page that is never annotated
|
|
17
|
+
downloads none of it. Nothing else: it runs inside your production site, so it
|
|
18
|
+
has no business adding to your supply chain.
|
|
16
19
|
|
|
17
20
|
## Install
|
|
18
21
|
|
|
@@ -20,23 +23,37 @@ adding to your supply chain.
|
|
|
20
23
|
npm i @weatherboard/annotation
|
|
21
24
|
```
|
|
22
25
|
|
|
23
|
-
##
|
|
26
|
+
## Eight entry points
|
|
24
27
|
|
|
25
28
|
They are split by **who imports them**, not by topic.
|
|
26
29
|
|
|
27
30
|
| Import | Runs where | For |
|
|
28
31
|
|---|---|---|
|
|
29
32
|
| `@weatherboard/annotation` | The browser | The page SDK — overlay, target resolution, state collection, screenshots |
|
|
33
|
+
| `@weatherboard/annotation/bootstrap` | The browser, before hydration | Immediate URL stripping, arming reads and token-free startup status |
|
|
30
34
|
| `@weatherboard/annotation/protocol` | Anywhere, no DOM | The wire names and the two readers. This is what a middleware imports |
|
|
31
35
|
| `@weatherboard/annotation/host` | The reviewing console | The parent half of the channel |
|
|
32
36
|
| `@weatherboard/annotation/anchor` | The browser | `resolveAnchor` — where a capture's marker goes, or why it goes nowhere |
|
|
33
37
|
| `@weatherboard/annotation/read` | The browser | `createAnnotationReader` — the captures already made on this page |
|
|
34
38
|
| `@weatherboard/annotation/markers` | The browser | The marker layer and its panel |
|
|
39
|
+
| `@weatherboard/annotation/selection` | The browser | Authored-content text/point selection and placement helpers |
|
|
35
40
|
|
|
36
|
-
The
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
is.
|
|
41
|
+
The final four remain separate because products can choose capture, marker
|
|
42
|
+
rendering and authored-content selection independently. A product that merely
|
|
43
|
+
captures should not pay for readers, marker UI or selection helpers. They are
|
|
44
|
+
subpaths rather than a tree-shaking promise for the same reason `/protocol` is.
|
|
45
|
+
|
|
46
|
+
`/bootstrap` is also a subpath rather than a tree-shaking promise. Put
|
|
47
|
+
`annotationBootstrapScript()` in the first raw script in `<head>`, then keep the
|
|
48
|
+
root entry behind a dynamic import. A normal visitor pays for URL and cookie
|
|
49
|
+
reads without downloading the toolbar or state collector.
|
|
50
|
+
|
|
51
|
+
Package-authored browser UI keeps its styles zero-dependency too. Declare
|
|
52
|
+
colour, spacing and radius literals once as local `--ai-*` custom properties,
|
|
53
|
+
then consume them with `var()` in the same style string. Do not import a
|
|
54
|
+
product's design tokens: a toolbar or startup notice runs inside somebody
|
|
55
|
+
else's page, where those names and values are not ours. New runtime surfaces
|
|
56
|
+
must pass `pnpm gyde:check`; do not add their findings to the allowance ledger.
|
|
40
57
|
|
|
41
58
|
`/protocol` is a subpath rather than a tree-shaking promise on purpose: an edge
|
|
42
59
|
middleware is often bundled as CJS, and CJS does not shake. Importing the root
|
|
@@ -44,6 +61,52 @@ there would drag the overlay into a bundle that has no `document`.
|
|
|
44
61
|
|
|
45
62
|
## The page half
|
|
46
63
|
|
|
64
|
+
### The in-app shortcut (A-2384)
|
|
65
|
+
|
|
66
|
+
The shortcut has two halves, because a host loads the toolbar only on an armed
|
|
67
|
+
page (ADOPTING.md, step 6) and an ordinary staging page has nothing loaded to
|
|
68
|
+
hear a key.
|
|
69
|
+
|
|
70
|
+
1. **Pre-arm, on every page.** From the small `./bootstrap` entry point, next to
|
|
71
|
+
the bootstrap script:
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
import { DEFAULT_HOTKEY, installAnnotationShortcut } from "@weatherboard/annotation/bootstrap";
|
|
75
|
+
|
|
76
|
+
installAnnotationShortcut({
|
|
77
|
+
build, // the same BuildInfo the mount gets; production installs nothing
|
|
78
|
+
armUrl: process.env.NEXT_PUBLIC_ANNOTATION_ARM_URL, // Studio's /annotate/arm?product=<id>&target=staging
|
|
79
|
+
hotkey: DEFAULT_HOTKEY,
|
|
80
|
+
});
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
2. **The mount, on an armed page.** Pass the middleware-backed `readArming()`
|
|
84
|
+
result and the same `hotkey` to `mountAnnotation()`.
|
|
85
|
+
|
|
86
|
+
With a fresh token the mounted toolbar claims the chord and toggles itself.
|
|
87
|
+
Otherwise — nothing mounted, no token, a token within five minutes of expiry,
|
|
88
|
+
or a session ended with Done — the chord is unclaimed, and the pre-arm listener
|
|
89
|
+
writes a credential-free open intent to session storage and navigates the tab
|
|
90
|
+
to `armUrl` with a `return` path from which every annotation parameter has
|
|
91
|
+
been removed. Studio checks the signed-in reviewer, mints the tokens and sends
|
|
92
|
+
the tab back; the host's middleware arms on that request, the bootstrap script
|
|
93
|
+
strips the query, and the mount opens the toolbar once because the intent is
|
|
94
|
+
under ten minutes old. One chord arms and opens. `e2e/shortcut.spec.ts` runs
|
|
95
|
+
that loop in Chromium, through to a capture with a screenshot.
|
|
96
|
+
|
|
97
|
+
The hotkey is opt-in: with no `hotkey` (absent or `null`) the toolbar shows
|
|
98
|
+
immediately, as every mount before 0.7.0 did. `DEFAULT_HOTKEY` is
|
|
99
|
+
`Mod+Shift+Period`, and `Mod` accepts Command or Control. The chord works while
|
|
100
|
+
a form field has focus. A result shown by `showResult` always reveals the
|
|
101
|
+
toolbar.
|
|
102
|
+
|
|
103
|
+
`readArming` strips credentials from the deep link, keeps them in session
|
|
104
|
+
storage, and copies an accepted token pair to an expiring
|
|
105
|
+
`Secure; SameSite=Lax; Path=/` cookie so a new tab can recover them. That
|
|
106
|
+
cookie is sent on product-origin requests, so product request logs must exclude
|
|
107
|
+
its value. The package reads the signed token's expiry without verifying the
|
|
108
|
+
signature; the console verifies it when used.
|
|
109
|
+
|
|
47
110
|
```ts
|
|
48
111
|
import { createAnnotationSdk, isAnnotationArmed, readAnnotationToken } from "@weatherboard/annotation";
|
|
49
112
|
|
|
@@ -63,11 +126,21 @@ right is yours, because only you hold the secret.
|
|
|
63
126
|
|
|
64
127
|
```ts
|
|
65
128
|
// middleware.ts
|
|
66
|
-
import {
|
|
129
|
+
import {
|
|
130
|
+
ANNOTATION_ARMED_COOKIE,
|
|
131
|
+
ANNOTATION_ARMED_COOKIES,
|
|
132
|
+
ANNOTATION_TOKEN_PARAMS,
|
|
133
|
+
readAnnotationToken,
|
|
134
|
+
} from "@weatherboard/annotation/protocol";
|
|
67
135
|
|
|
68
136
|
const token = readAnnotationToken(request.nextUrl.searchParams);
|
|
69
|
-
|
|
137
|
+
const attempted = ANNOTATION_TOKEN_PARAMS.some((name) => request.nextUrl.searchParams.has(name));
|
|
138
|
+
if (token !== null && token === process.env.ANNOTATION_TOKEN) {
|
|
70
139
|
response.cookies.set(ANNOTATION_ARMED_COOKIE, "1", { httpOnly: false, sameSite: "lax" });
|
|
140
|
+
} else if (attempted) {
|
|
141
|
+
for (const name of ANNOTATION_ARMED_COOKIES) {
|
|
142
|
+
response.cookies.set(name, "", { path: "/", maxAge: 0, expires: new Date(0) });
|
|
143
|
+
}
|
|
71
144
|
}
|
|
72
145
|
```
|
|
73
146
|
|
|
@@ -75,7 +148,8 @@ if (token !== undefined && token === process.env.ANNOTATION_TOKEN) {
|
|
|
75
148
|
for an absent *or empty* parameter. An empty token compared with `===` against
|
|
76
149
|
your secret is a rejection; an empty token treated as present is an app armed
|
|
77
150
|
with the empty string, which fails later and reads like a bad secret rather than
|
|
78
|
-
a missing one.
|
|
151
|
+
a missing one. A fresh rejected attempt must expire **both** supported cookie
|
|
152
|
+
spellings so an already-armed browser cannot reuse its previous session.
|
|
79
153
|
|
|
80
154
|
## Source refs
|
|
81
155
|
|
package/dist/anchor.cjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
"use strict";var f=Object.defineProperty;var x=Object.getOwnPropertyDescriptor;var E=Object.getOwnPropertyNames;var
|
|
1
|
+
"use strict";var f=Object.defineProperty;var x=Object.getOwnPropertyDescriptor;var E=Object.getOwnPropertyNames;var b=Object.prototype.hasOwnProperty;var T=(e,n)=>{for(var t in n)f(e,t,{get:n[t],enumerable:!0})},R=(e,n,t,o)=>{if(n&&typeof n=="object"||typeof n=="function")for(let r of E(n))!b.call(e,r)&&r!==t&&f(e,r,{get:()=>n[r],enumerable:!(o=x(n,r))||o.enumerable});return e};var y=e=>R(f({},"__esModule",{value:!0}),e);var O={};T(O,{anchorFromTarget:()=>S,resolveAnchor:()=>A,tallyAnchors:()=>_});module.exports=y(O);var u="data-src";function g(e){return e.selection==="region"?"region":"element"}var m=8;function s(e){if(!e)return null;let n=e.lastIndexOf(":");if(n<=0)return null;let t=e.lastIndexOf(":",n-1);if(t<=0)return null;let o=e.slice(0,t),r=Number(e.slice(t+1,n)),i=Number(e.slice(n+1));return!o||!Number.isFinite(r)||!Number.isFinite(i)?null:{file:o,line:r,column:i}}function d(e,n=m){let t=[],o=e;for(;o&&t.length<n;){let r=s(o.getAttribute(u));if(r){let i=t[t.length-1];i&&i.file===r.file&&i.line===r.line||t.push(r)}o=o.parentElement}return t}function h(e){let n=e;for(;n;){if(s(n.getAttribute(u)))return n;n=n.parentElement}return null}function l(e,n){return e.file!==n.file||e.line!==n.line?!1:e.column===void 0||n.column===void 0?!0:e.column===n.column}function k(e,n){if(e.length===0)return!1;if(e.length<m){if(n.length!==e.length)return!1}else if(n.length<e.length)return!1;return e.every((t,o)=>{let r=n[o];return r!==void 0&&l(t,r)})}function N(e,n){let t={file:e.file,line:e.line};e.column!==void 0&&(t.column=e.column);let o=n.querySelectorAll(`[${u}]`),r=[];for(let i=0;i<o.length;i++){let a=o[i],c=s(a.getAttribute(u));if(!c||!l(t,c))continue;let p=s(h(a.parentElement)?.getAttribute(u)??null);p&&l(p,c)||r.push(a)}return r}function A(e,n){switch(e.kind){case"route":{if(!e.path)return"none";let t=n.location?.pathname;return!t||t!==e.path?"none":n.body??"none"}case"dom-path":{if(!e.path)return"none";let t;try{t=n.querySelectorAll(e.path)}catch{return"none"}return t.length===0?"none":t.length>1?"ambiguous":t[0]}case"source-ref":{if(e.chain.length===0)return"none";let t=N(e,n).filter(o=>k(e.chain,d(o)));return t.length===0?"none":t.length>1?"ambiguous":t[0]}default:return"none"}}function S(e,n={}){let t=()=>n.path===void 0?{kind:"route"}:{kind:"route",path:n.path};if(g(e)==="region")return t();let o=e.sourceChain??[],r=o[0],i=e.source??r;if(i&&r&&l(i,r)){let a={kind:"source-ref",file:i.file,line:i.line,chain:o};return i.column!==void 0&&(a.column=i.column),n.ordinal!==void 0&&(a.ordinal=n.ordinal),a}return e.domPath?{kind:"dom-path",path:e.domPath}:t()}function _(e,n){let t={total:e.length,placed:0,ambiguous:0,unresolved:0};for(let o of e){let r=A(o,n);r==="ambiguous"?t.ambiguous+=1:r==="none"?t.unresolved+=1:t.placed+=1}return t}
|
|
2
2
|
//# sourceMappingURL=anchor.cjs.map
|