@openpicker/protocol 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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Usertour
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,36 @@
1
+ # @openpicker/protocol
2
+
3
+ The open [openpicker](https://openpicker.dev) postMessage protocol: the wire types, constants, and
4
+ selector helpers that the browser extension and any client share. Website:
5
+ [openpicker.dev](https://openpicker.dev) · Docs: [docs.openpicker.dev](https://docs.openpicker.dev)
6
+
7
+ Most integrations should use [`@openpicker/sdk`](https://www.npmjs.com/package/@openpicker/sdk),
8
+ which wraps this protocol in a friendly client. Depend on this package directly only when building
9
+ your own client (another SDK, a different extension, a non-browser bridge) against the same wire
10
+ format.
11
+
12
+ ```bash
13
+ npm install @openpicker/protocol
14
+ ```
15
+
16
+ ```ts
17
+ import {
18
+ CHANNEL,
19
+ PROTOCOL_VERSION,
20
+ isEnvelope,
21
+ matchesSelectorConfig,
22
+ type PickParams,
23
+ type SelectorConfig,
24
+ } from "@openpicker/protocol"
25
+ ```
26
+
27
+ This package ships:
28
+
29
+ - **Constants** — `CHANNEL`, `PROTOCOL_VERSION`.
30
+ - **Envelopes** — `RequestEnvelope`, `ResponseEnvelope`, `EventEnvelope`, and the `isEnvelope` guard.
31
+ - **Methods** — `MethodMap` plus the params/result types (`PickParams`, `PickResult`, `SelectorConfig`, …).
32
+ - **Errors** — `ProtocolError`, `ErrorCode`.
33
+ - **Selector helpers** — `tokenizeSelector`, `matchesSelectorConfig`, and their token types.
34
+
35
+ See the [protocol spec](https://github.com/usertour/openpicker/blob/main/PROTOCOL.md) for the full
36
+ wire format.
package/dist/index.cjs ADDED
@@ -0,0 +1,153 @@
1
+ 'use strict';
2
+
3
+ // src/constants.ts
4
+ var CHANNEL = "openpicker";
5
+ var PROTOCOL_VERSION = 1;
6
+
7
+ // src/envelope.ts
8
+ function isEnvelope(value) {
9
+ if (typeof value !== "object" || value === null) return false;
10
+ const v = value;
11
+ return v.channel === CHANNEL && typeof v.kind === "string";
12
+ }
13
+
14
+ // src/selectorTokens.ts
15
+ var WORD = /[\w-]/;
16
+ function tokenizeSelector(selector) {
17
+ const tokens = [];
18
+ const n = selector.length;
19
+ let i = 0;
20
+ const push = (text, type) => {
21
+ if (text) tokens.push({ text, type });
22
+ };
23
+ const runOfWord = (from) => {
24
+ let j = from;
25
+ while (j < n && WORD.test(selector.charAt(j))) j++;
26
+ return j;
27
+ };
28
+ while (i < n) {
29
+ const c = selector.charAt(i);
30
+ if (/\s/.test(c)) {
31
+ let j = i + 1;
32
+ while (j < n && /\s/.test(selector.charAt(j))) j++;
33
+ push(selector.slice(i, j), "combinator");
34
+ i = j;
35
+ continue;
36
+ }
37
+ if (c === ">" || c === "+" || c === "~") {
38
+ push(c, "combinator");
39
+ i++;
40
+ continue;
41
+ }
42
+ if (c === "#") {
43
+ const j = runOfWord(i + 1);
44
+ push(selector.slice(i, j), "id");
45
+ i = j;
46
+ continue;
47
+ }
48
+ if (c === ".") {
49
+ const j = runOfWord(i + 1);
50
+ push(selector.slice(i, j), "class");
51
+ i = j;
52
+ continue;
53
+ }
54
+ if (c === ":") {
55
+ let j = i + 1;
56
+ if (selector.charAt(j) === ":") j++;
57
+ j = runOfWord(j);
58
+ if (selector.charAt(j) === "(") {
59
+ let depth = 0;
60
+ while (j < n) {
61
+ const ch = selector.charAt(j);
62
+ j++;
63
+ if (ch === "(") depth++;
64
+ else if (ch === ")") {
65
+ depth--;
66
+ if (depth === 0) break;
67
+ }
68
+ }
69
+ }
70
+ push(selector.slice(i, j), "pseudo");
71
+ i = j;
72
+ continue;
73
+ }
74
+ if (c === "[") {
75
+ push("[", "punctuation");
76
+ i++;
77
+ i = (() => {
78
+ const nameEnd = runOfWord(i);
79
+ push(selector.slice(i, nameEnd), "attrName");
80
+ let k = nameEnd;
81
+ while (k < n && selector.charAt(k) !== "]") {
82
+ const ch = selector.charAt(k);
83
+ if (ch === '"' || ch === "'") {
84
+ let q = k + 1;
85
+ while (q < n && selector.charAt(q) !== ch) q++;
86
+ if (q < n) q++;
87
+ push(selector.slice(k, q), "attrValue");
88
+ k = q;
89
+ } else if (/[\s~|^$*=]/.test(ch)) {
90
+ push(ch, "punctuation");
91
+ k++;
92
+ } else {
93
+ let v = k;
94
+ while (v < n && !/[\]\s=~|^$*'"]/.test(selector.charAt(v))) v++;
95
+ push(selector.slice(k, v), "attrValue");
96
+ k = v;
97
+ }
98
+ }
99
+ return k;
100
+ })();
101
+ if (i < n && selector.charAt(i) === "]") {
102
+ push("]", "punctuation");
103
+ i++;
104
+ }
105
+ continue;
106
+ }
107
+ if (c === "*") {
108
+ push("*", "tag");
109
+ i++;
110
+ continue;
111
+ }
112
+ if (WORD.test(c)) {
113
+ const j = runOfWord(i);
114
+ push(selector.slice(i, j), "tag");
115
+ i = j;
116
+ continue;
117
+ }
118
+ push(c, "punctuation");
119
+ i++;
120
+ }
121
+ return tokens;
122
+ }
123
+ function anchorAllowsName(name, anchor) {
124
+ if (!anchor) return true;
125
+ if (anchor.enabled === false) return false;
126
+ try {
127
+ if (anchor.ignore && new RegExp(anchor.ignore).test(name)) return false;
128
+ if (anchor.allow) return new RegExp(anchor.allow).test(name);
129
+ } catch {
130
+ return true;
131
+ }
132
+ return true;
133
+ }
134
+ function matchesSelectorConfig(selector, config) {
135
+ for (const tok of tokenizeSelector(selector)) {
136
+ if (tok.type === "id") {
137
+ if (!anchorAllowsName(tok.text.replace(/^#/, ""), config.id)) return false;
138
+ } else if (tok.type === "class") {
139
+ if (!anchorAllowsName(tok.text.replace(/^\./, ""), config.class)) return false;
140
+ } else if (tok.type === "tag") {
141
+ if (tok.text !== "*" && !anchorAllowsName(tok.text, config.tag)) return false;
142
+ } else if (tok.type === "attrName") {
143
+ if (!anchorAllowsName(tok.text, config.attr)) return false;
144
+ }
145
+ }
146
+ return true;
147
+ }
148
+
149
+ exports.CHANNEL = CHANNEL;
150
+ exports.PROTOCOL_VERSION = PROTOCOL_VERSION;
151
+ exports.isEnvelope = isEnvelope;
152
+ exports.matchesSelectorConfig = matchesSelectorConfig;
153
+ exports.tokenizeSelector = tokenizeSelector;
@@ -0,0 +1,228 @@
1
+ /**
2
+ * Fixed discriminator carried on every openpicker message. Receivers ignore any
3
+ * message whose `channel` is not exactly this value.
4
+ */
5
+ declare const CHANNEL: "openpicker";
6
+ type Channel = typeof CHANNEL;
7
+ /** Protocol major version implemented by this package. See PROTOCOL.md §9. */
8
+ declare const PROTOCOL_VERSION: 1;
9
+ /** The three message kinds that share the envelope. */
10
+ type MessageKind = "req" | "res" | "evt";
11
+
12
+ /** Stable error identifiers returned on a failed response. See PROTOCOL.md §8. */
13
+ type ErrorCode = "extension_not_installed" | "unsupported_protocol" | "consent_denied" | "cancelled" | "invalid_params" | "unsupported" | "timeout" | "internal_error";
14
+ /** Error payload carried on a failed response envelope (`ok: false`). */
15
+ interface ProtocolError {
16
+ code: ErrorCode;
17
+ message: string;
18
+ data?: unknown;
19
+ }
20
+
21
+ /**
22
+ * What the returned screenshot covers. See DESIGN.md §5b.
23
+ * - "none": no screenshot.
24
+ * - "element": cropped to the selected element.
25
+ * - "viewport": the full visible viewport.
26
+ * ("fullpage" is reserved for the future.)
27
+ */
28
+ type ScreenshotMode = "none" | "element" | "viewport";
29
+ /** A regular-expression source string. Compiled with `new RegExp(...)` — never eval'd. */
30
+ type RegexSource = string;
31
+ /**
32
+ * Rules for one anchor type (id / class / attr / tag) when building a selector.
33
+ * This mirrors the in-picker gear settings; passed via {@link PickParams.selector}
34
+ * it pre-fills them. For `attr`, `allow`/`ignore` match the attribute NAME.
35
+ */
36
+ interface SelectorAnchorConfig {
37
+ /** Whether this anchor type may be used at all. Default: true. */
38
+ enabled?: boolean;
39
+ /**
40
+ * Only names matching this regex may be used. Omitted/empty = openpicker's
41
+ * built-in "stable name" heuristics (skips ember/radix ids, hashed CSS-in-JS /
42
+ * CSS-module classes, etc.).
43
+ */
44
+ allow?: RegexSource;
45
+ /** Names matching this regex are never used (applied on top of `allow`). */
46
+ ignore?: RegexSource;
47
+ }
48
+ /** Per-dimension selector-generation rules. See {@link SelectorAnchorConfig}. */
49
+ interface SelectorConfig {
50
+ id?: SelectorAnchorConfig;
51
+ class?: SelectorAnchorConfig;
52
+ attr?: SelectorAnchorConfig;
53
+ tag?: SelectorAnchorConfig;
54
+ }
55
+ interface PingParams {
56
+ /** Display-only application name, surfaced in the consent prompt. Never trusted. */
57
+ appName?: string;
58
+ }
59
+ interface PingResult {
60
+ /** The extension's own version (from its manifest). */
61
+ extensionVersion: string;
62
+ /** Protocol majors the extension supports. */
63
+ protocolVersions: number[];
64
+ /** Feature flags so the SDK can degrade gracefully. */
65
+ capabilities: string[];
66
+ }
67
+ interface PickParams {
68
+ /** Request resolution of elements inside iframes (may be reported unsupported in v1). */
69
+ iframe?: boolean;
70
+ /**
71
+ * Screenshot to include in the result. A {@link ScreenshotMode}, or a boolean for
72
+ * compatibility (`true` → "element", `false` → "none"). Defaults to "none".
73
+ */
74
+ screenshot?: ScreenshotMode | boolean;
75
+ /**
76
+ * The URL to pick in. The extension opens it in a tab, the user picks there, and
77
+ * the result is routed back to the calling tab (cross-tab picking). Required: an
78
+ * extension earns its keep by crossing the tab/origin boundary — a page can
79
+ * already script its own DOM, so same-tab picking is not an SDK capability (only
80
+ * the toolbar offers it, for humans). Requires the "openUrl" capability.
81
+ */
82
+ url: string;
83
+ /**
84
+ * Optional opaque identifier for "which task" this pick is for. Only used to
85
+ * decide whether a follow-up cross-tab pick reuses the existing target tab or
86
+ * opens a new one (equality compare; never interpreted). See DESIGN.md §5d.
87
+ */
88
+ key?: string;
89
+ /** Display-only application name, surfaced in the consent prompt. Never trusted. */
90
+ appName?: string;
91
+ /**
92
+ * Initial selector-generation rules for this pick (also the values shown in the
93
+ * gear). Composed (AND) with the user's saved rules — both can only narrow.
94
+ * Omitted dimensions use openpicker's defaults.
95
+ */
96
+ selector?: SelectorConfig;
97
+ /** Open the gear settings read-only (the user can see them but not change). Default: false. */
98
+ lockSelectorSettings?: boolean;
99
+ /** Make the selector field read-only (no hand-editing). Default: false. */
100
+ lockSelectorEdit?: boolean;
101
+ /** Only allow confirming (OK) when the selector matches exactly one element. Default: false. */
102
+ requireUniqueMatch?: boolean;
103
+ }
104
+ interface PickedElement {
105
+ tag: string;
106
+ id?: string;
107
+ classes?: string[];
108
+ text?: string;
109
+ attributes?: Record<string, string>;
110
+ }
111
+ interface PickResult {
112
+ /** The chosen CSS selector. */
113
+ selector: string;
114
+ /** How many elements the selector currently matches. */
115
+ matchCount: number;
116
+ /** A summary of the selected element. */
117
+ element: PickedElement;
118
+ /** Present only when `screenshot` was requested. */
119
+ screenshot?: string;
120
+ }
121
+ type CancelParams = Record<string, never>;
122
+ type CancelResult = Record<string, never>;
123
+ interface HighlightParams {
124
+ selector: string;
125
+ }
126
+ interface HighlightResult {
127
+ matchCount: number;
128
+ }
129
+ type ClearHighlightParams = Record<string, never>;
130
+ type ClearHighlightResult = Record<string, never>;
131
+ type ActivateSelfParams = Record<string, never>;
132
+ type ActivateSelfResult = Record<string, never>;
133
+ type IsTargetOpenParams = Record<string, never>;
134
+ interface IsTargetOpenResult {
135
+ /** Whether a cross-tab target tab opened by this source is still open. */
136
+ open: boolean;
137
+ }
138
+ /** Maps each method name to its request params and response result. */
139
+ interface MethodMap {
140
+ ping: {
141
+ params: PingParams;
142
+ result: PingResult;
143
+ };
144
+ pick: {
145
+ params: PickParams;
146
+ result: PickResult;
147
+ };
148
+ cancel: {
149
+ params: CancelParams;
150
+ result: CancelResult;
151
+ };
152
+ highlight: {
153
+ params: HighlightParams;
154
+ result: HighlightResult;
155
+ };
156
+ clearHighlight: {
157
+ params: ClearHighlightParams;
158
+ result: ClearHighlightResult;
159
+ };
160
+ /** Bring the calling tab to the foreground (it can only focus itself). */
161
+ activateSelf: {
162
+ params: ActivateSelfParams;
163
+ result: ActivateSelfResult;
164
+ };
165
+ /** Report whether the cross-tab target opened by this source is still open. */
166
+ isTargetOpen: {
167
+ params: IsTargetOpenParams;
168
+ result: IsTargetOpenResult;
169
+ };
170
+ }
171
+ type MethodName = keyof MethodMap;
172
+
173
+ /** A method call: SDK → extension. See PROTOCOL.md §3. */
174
+ interface RequestEnvelope<M extends MethodName = MethodName> {
175
+ channel: Channel;
176
+ v: number;
177
+ kind: "req";
178
+ id: string;
179
+ method: M;
180
+ params: MethodMap[M]["params"];
181
+ }
182
+ /** A reply to a request: extension → SDK. `id` echoes the request. */
183
+ interface ResponseEnvelope<M extends MethodName = MethodName> {
184
+ channel: Channel;
185
+ v: number;
186
+ kind: "res";
187
+ id: string;
188
+ ok: boolean;
189
+ result?: MethodMap[M]["result"];
190
+ error?: ProtocolError;
191
+ }
192
+ /** Reserved notification names (none required in v1). See PROTOCOL.md §6.6. */
193
+ type EventName = "hoverChange" | "consentChange";
194
+ /** A fire-and-forget notification, not correlated to a single request. */
195
+ interface EventEnvelope {
196
+ channel: Channel;
197
+ v: number;
198
+ kind: "evt";
199
+ event: EventName;
200
+ data: unknown;
201
+ }
202
+ type Envelope = RequestEnvelope | ResponseEnvelope | EventEnvelope;
203
+ /** Narrowing guard: is this an openpicker envelope at all? */
204
+ declare function isEnvelope(value: unknown): value is Envelope;
205
+
206
+ /**
207
+ * Tokenize a CSS selector for syntax highlighting and for validating a selector
208
+ * against a {@link SelectorConfig}. Best-effort and resilient: anything
209
+ * unrecognized falls through as "punctuation", so an in-progress or invalid
210
+ * selector still tokenizes without throwing. Invariant: the concatenated token text
211
+ * always equals the input exactly. Lives in the protocol package so both the
212
+ * extension (highlighting) and the SDK (validation) can share it.
213
+ */
214
+ type SelectorTokenType = "tag" | "id" | "class" | "attrName" | "attrValue" | "pseudo" | "combinator" | "punctuation";
215
+ interface SelectorToken {
216
+ text: string;
217
+ type: SelectorTokenType;
218
+ }
219
+ declare function tokenizeSelector(selector: string): SelectorToken[];
220
+ /**
221
+ * Whether a CSS selector only uses anchors permitted by a {@link SelectorConfig} —
222
+ * the SDK-side check a developer runs on the returned selector (which the user may
223
+ * have hand-edited). Combinators, pseudo-classes, attribute values, and the
224
+ * universal `*` are unconstrained. A selector touching no constrained anchors passes.
225
+ */
226
+ declare function matchesSelectorConfig(selector: string, config: SelectorConfig): boolean;
227
+
228
+ export { type ActivateSelfParams, type ActivateSelfResult, CHANNEL, type CancelParams, type CancelResult, type Channel, type ClearHighlightParams, type ClearHighlightResult, type Envelope, type ErrorCode, type EventEnvelope, type EventName, type HighlightParams, type HighlightResult, type IsTargetOpenParams, type IsTargetOpenResult, type MessageKind, type MethodMap, type MethodName, PROTOCOL_VERSION, type PickParams, type PickResult, type PickedElement, type PingParams, type PingResult, type ProtocolError, type RegexSource, type RequestEnvelope, type ResponseEnvelope, type ScreenshotMode, type SelectorAnchorConfig, type SelectorConfig, type SelectorToken, type SelectorTokenType, isEnvelope, matchesSelectorConfig, tokenizeSelector };
@@ -0,0 +1,228 @@
1
+ /**
2
+ * Fixed discriminator carried on every openpicker message. Receivers ignore any
3
+ * message whose `channel` is not exactly this value.
4
+ */
5
+ declare const CHANNEL: "openpicker";
6
+ type Channel = typeof CHANNEL;
7
+ /** Protocol major version implemented by this package. See PROTOCOL.md §9. */
8
+ declare const PROTOCOL_VERSION: 1;
9
+ /** The three message kinds that share the envelope. */
10
+ type MessageKind = "req" | "res" | "evt";
11
+
12
+ /** Stable error identifiers returned on a failed response. See PROTOCOL.md §8. */
13
+ type ErrorCode = "extension_not_installed" | "unsupported_protocol" | "consent_denied" | "cancelled" | "invalid_params" | "unsupported" | "timeout" | "internal_error";
14
+ /** Error payload carried on a failed response envelope (`ok: false`). */
15
+ interface ProtocolError {
16
+ code: ErrorCode;
17
+ message: string;
18
+ data?: unknown;
19
+ }
20
+
21
+ /**
22
+ * What the returned screenshot covers. See DESIGN.md §5b.
23
+ * - "none": no screenshot.
24
+ * - "element": cropped to the selected element.
25
+ * - "viewport": the full visible viewport.
26
+ * ("fullpage" is reserved for the future.)
27
+ */
28
+ type ScreenshotMode = "none" | "element" | "viewport";
29
+ /** A regular-expression source string. Compiled with `new RegExp(...)` — never eval'd. */
30
+ type RegexSource = string;
31
+ /**
32
+ * Rules for one anchor type (id / class / attr / tag) when building a selector.
33
+ * This mirrors the in-picker gear settings; passed via {@link PickParams.selector}
34
+ * it pre-fills them. For `attr`, `allow`/`ignore` match the attribute NAME.
35
+ */
36
+ interface SelectorAnchorConfig {
37
+ /** Whether this anchor type may be used at all. Default: true. */
38
+ enabled?: boolean;
39
+ /**
40
+ * Only names matching this regex may be used. Omitted/empty = openpicker's
41
+ * built-in "stable name" heuristics (skips ember/radix ids, hashed CSS-in-JS /
42
+ * CSS-module classes, etc.).
43
+ */
44
+ allow?: RegexSource;
45
+ /** Names matching this regex are never used (applied on top of `allow`). */
46
+ ignore?: RegexSource;
47
+ }
48
+ /** Per-dimension selector-generation rules. See {@link SelectorAnchorConfig}. */
49
+ interface SelectorConfig {
50
+ id?: SelectorAnchorConfig;
51
+ class?: SelectorAnchorConfig;
52
+ attr?: SelectorAnchorConfig;
53
+ tag?: SelectorAnchorConfig;
54
+ }
55
+ interface PingParams {
56
+ /** Display-only application name, surfaced in the consent prompt. Never trusted. */
57
+ appName?: string;
58
+ }
59
+ interface PingResult {
60
+ /** The extension's own version (from its manifest). */
61
+ extensionVersion: string;
62
+ /** Protocol majors the extension supports. */
63
+ protocolVersions: number[];
64
+ /** Feature flags so the SDK can degrade gracefully. */
65
+ capabilities: string[];
66
+ }
67
+ interface PickParams {
68
+ /** Request resolution of elements inside iframes (may be reported unsupported in v1). */
69
+ iframe?: boolean;
70
+ /**
71
+ * Screenshot to include in the result. A {@link ScreenshotMode}, or a boolean for
72
+ * compatibility (`true` → "element", `false` → "none"). Defaults to "none".
73
+ */
74
+ screenshot?: ScreenshotMode | boolean;
75
+ /**
76
+ * The URL to pick in. The extension opens it in a tab, the user picks there, and
77
+ * the result is routed back to the calling tab (cross-tab picking). Required: an
78
+ * extension earns its keep by crossing the tab/origin boundary — a page can
79
+ * already script its own DOM, so same-tab picking is not an SDK capability (only
80
+ * the toolbar offers it, for humans). Requires the "openUrl" capability.
81
+ */
82
+ url: string;
83
+ /**
84
+ * Optional opaque identifier for "which task" this pick is for. Only used to
85
+ * decide whether a follow-up cross-tab pick reuses the existing target tab or
86
+ * opens a new one (equality compare; never interpreted). See DESIGN.md §5d.
87
+ */
88
+ key?: string;
89
+ /** Display-only application name, surfaced in the consent prompt. Never trusted. */
90
+ appName?: string;
91
+ /**
92
+ * Initial selector-generation rules for this pick (also the values shown in the
93
+ * gear). Composed (AND) with the user's saved rules — both can only narrow.
94
+ * Omitted dimensions use openpicker's defaults.
95
+ */
96
+ selector?: SelectorConfig;
97
+ /** Open the gear settings read-only (the user can see them but not change). Default: false. */
98
+ lockSelectorSettings?: boolean;
99
+ /** Make the selector field read-only (no hand-editing). Default: false. */
100
+ lockSelectorEdit?: boolean;
101
+ /** Only allow confirming (OK) when the selector matches exactly one element. Default: false. */
102
+ requireUniqueMatch?: boolean;
103
+ }
104
+ interface PickedElement {
105
+ tag: string;
106
+ id?: string;
107
+ classes?: string[];
108
+ text?: string;
109
+ attributes?: Record<string, string>;
110
+ }
111
+ interface PickResult {
112
+ /** The chosen CSS selector. */
113
+ selector: string;
114
+ /** How many elements the selector currently matches. */
115
+ matchCount: number;
116
+ /** A summary of the selected element. */
117
+ element: PickedElement;
118
+ /** Present only when `screenshot` was requested. */
119
+ screenshot?: string;
120
+ }
121
+ type CancelParams = Record<string, never>;
122
+ type CancelResult = Record<string, never>;
123
+ interface HighlightParams {
124
+ selector: string;
125
+ }
126
+ interface HighlightResult {
127
+ matchCount: number;
128
+ }
129
+ type ClearHighlightParams = Record<string, never>;
130
+ type ClearHighlightResult = Record<string, never>;
131
+ type ActivateSelfParams = Record<string, never>;
132
+ type ActivateSelfResult = Record<string, never>;
133
+ type IsTargetOpenParams = Record<string, never>;
134
+ interface IsTargetOpenResult {
135
+ /** Whether a cross-tab target tab opened by this source is still open. */
136
+ open: boolean;
137
+ }
138
+ /** Maps each method name to its request params and response result. */
139
+ interface MethodMap {
140
+ ping: {
141
+ params: PingParams;
142
+ result: PingResult;
143
+ };
144
+ pick: {
145
+ params: PickParams;
146
+ result: PickResult;
147
+ };
148
+ cancel: {
149
+ params: CancelParams;
150
+ result: CancelResult;
151
+ };
152
+ highlight: {
153
+ params: HighlightParams;
154
+ result: HighlightResult;
155
+ };
156
+ clearHighlight: {
157
+ params: ClearHighlightParams;
158
+ result: ClearHighlightResult;
159
+ };
160
+ /** Bring the calling tab to the foreground (it can only focus itself). */
161
+ activateSelf: {
162
+ params: ActivateSelfParams;
163
+ result: ActivateSelfResult;
164
+ };
165
+ /** Report whether the cross-tab target opened by this source is still open. */
166
+ isTargetOpen: {
167
+ params: IsTargetOpenParams;
168
+ result: IsTargetOpenResult;
169
+ };
170
+ }
171
+ type MethodName = keyof MethodMap;
172
+
173
+ /** A method call: SDK → extension. See PROTOCOL.md §3. */
174
+ interface RequestEnvelope<M extends MethodName = MethodName> {
175
+ channel: Channel;
176
+ v: number;
177
+ kind: "req";
178
+ id: string;
179
+ method: M;
180
+ params: MethodMap[M]["params"];
181
+ }
182
+ /** A reply to a request: extension → SDK. `id` echoes the request. */
183
+ interface ResponseEnvelope<M extends MethodName = MethodName> {
184
+ channel: Channel;
185
+ v: number;
186
+ kind: "res";
187
+ id: string;
188
+ ok: boolean;
189
+ result?: MethodMap[M]["result"];
190
+ error?: ProtocolError;
191
+ }
192
+ /** Reserved notification names (none required in v1). See PROTOCOL.md §6.6. */
193
+ type EventName = "hoverChange" | "consentChange";
194
+ /** A fire-and-forget notification, not correlated to a single request. */
195
+ interface EventEnvelope {
196
+ channel: Channel;
197
+ v: number;
198
+ kind: "evt";
199
+ event: EventName;
200
+ data: unknown;
201
+ }
202
+ type Envelope = RequestEnvelope | ResponseEnvelope | EventEnvelope;
203
+ /** Narrowing guard: is this an openpicker envelope at all? */
204
+ declare function isEnvelope(value: unknown): value is Envelope;
205
+
206
+ /**
207
+ * Tokenize a CSS selector for syntax highlighting and for validating a selector
208
+ * against a {@link SelectorConfig}. Best-effort and resilient: anything
209
+ * unrecognized falls through as "punctuation", so an in-progress or invalid
210
+ * selector still tokenizes without throwing. Invariant: the concatenated token text
211
+ * always equals the input exactly. Lives in the protocol package so both the
212
+ * extension (highlighting) and the SDK (validation) can share it.
213
+ */
214
+ type SelectorTokenType = "tag" | "id" | "class" | "attrName" | "attrValue" | "pseudo" | "combinator" | "punctuation";
215
+ interface SelectorToken {
216
+ text: string;
217
+ type: SelectorTokenType;
218
+ }
219
+ declare function tokenizeSelector(selector: string): SelectorToken[];
220
+ /**
221
+ * Whether a CSS selector only uses anchors permitted by a {@link SelectorConfig} —
222
+ * the SDK-side check a developer runs on the returned selector (which the user may
223
+ * have hand-edited). Combinators, pseudo-classes, attribute values, and the
224
+ * universal `*` are unconstrained. A selector touching no constrained anchors passes.
225
+ */
226
+ declare function matchesSelectorConfig(selector: string, config: SelectorConfig): boolean;
227
+
228
+ export { type ActivateSelfParams, type ActivateSelfResult, CHANNEL, type CancelParams, type CancelResult, type Channel, type ClearHighlightParams, type ClearHighlightResult, type Envelope, type ErrorCode, type EventEnvelope, type EventName, type HighlightParams, type HighlightResult, type IsTargetOpenParams, type IsTargetOpenResult, type MessageKind, type MethodMap, type MethodName, PROTOCOL_VERSION, type PickParams, type PickResult, type PickedElement, type PingParams, type PingResult, type ProtocolError, type RegexSource, type RequestEnvelope, type ResponseEnvelope, type ScreenshotMode, type SelectorAnchorConfig, type SelectorConfig, type SelectorToken, type SelectorTokenType, isEnvelope, matchesSelectorConfig, tokenizeSelector };
package/dist/index.js ADDED
@@ -0,0 +1,147 @@
1
+ // src/constants.ts
2
+ var CHANNEL = "openpicker";
3
+ var PROTOCOL_VERSION = 1;
4
+
5
+ // src/envelope.ts
6
+ function isEnvelope(value) {
7
+ if (typeof value !== "object" || value === null) return false;
8
+ const v = value;
9
+ return v.channel === CHANNEL && typeof v.kind === "string";
10
+ }
11
+
12
+ // src/selectorTokens.ts
13
+ var WORD = /[\w-]/;
14
+ function tokenizeSelector(selector) {
15
+ const tokens = [];
16
+ const n = selector.length;
17
+ let i = 0;
18
+ const push = (text, type) => {
19
+ if (text) tokens.push({ text, type });
20
+ };
21
+ const runOfWord = (from) => {
22
+ let j = from;
23
+ while (j < n && WORD.test(selector.charAt(j))) j++;
24
+ return j;
25
+ };
26
+ while (i < n) {
27
+ const c = selector.charAt(i);
28
+ if (/\s/.test(c)) {
29
+ let j = i + 1;
30
+ while (j < n && /\s/.test(selector.charAt(j))) j++;
31
+ push(selector.slice(i, j), "combinator");
32
+ i = j;
33
+ continue;
34
+ }
35
+ if (c === ">" || c === "+" || c === "~") {
36
+ push(c, "combinator");
37
+ i++;
38
+ continue;
39
+ }
40
+ if (c === "#") {
41
+ const j = runOfWord(i + 1);
42
+ push(selector.slice(i, j), "id");
43
+ i = j;
44
+ continue;
45
+ }
46
+ if (c === ".") {
47
+ const j = runOfWord(i + 1);
48
+ push(selector.slice(i, j), "class");
49
+ i = j;
50
+ continue;
51
+ }
52
+ if (c === ":") {
53
+ let j = i + 1;
54
+ if (selector.charAt(j) === ":") j++;
55
+ j = runOfWord(j);
56
+ if (selector.charAt(j) === "(") {
57
+ let depth = 0;
58
+ while (j < n) {
59
+ const ch = selector.charAt(j);
60
+ j++;
61
+ if (ch === "(") depth++;
62
+ else if (ch === ")") {
63
+ depth--;
64
+ if (depth === 0) break;
65
+ }
66
+ }
67
+ }
68
+ push(selector.slice(i, j), "pseudo");
69
+ i = j;
70
+ continue;
71
+ }
72
+ if (c === "[") {
73
+ push("[", "punctuation");
74
+ i++;
75
+ i = (() => {
76
+ const nameEnd = runOfWord(i);
77
+ push(selector.slice(i, nameEnd), "attrName");
78
+ let k = nameEnd;
79
+ while (k < n && selector.charAt(k) !== "]") {
80
+ const ch = selector.charAt(k);
81
+ if (ch === '"' || ch === "'") {
82
+ let q = k + 1;
83
+ while (q < n && selector.charAt(q) !== ch) q++;
84
+ if (q < n) q++;
85
+ push(selector.slice(k, q), "attrValue");
86
+ k = q;
87
+ } else if (/[\s~|^$*=]/.test(ch)) {
88
+ push(ch, "punctuation");
89
+ k++;
90
+ } else {
91
+ let v = k;
92
+ while (v < n && !/[\]\s=~|^$*'"]/.test(selector.charAt(v))) v++;
93
+ push(selector.slice(k, v), "attrValue");
94
+ k = v;
95
+ }
96
+ }
97
+ return k;
98
+ })();
99
+ if (i < n && selector.charAt(i) === "]") {
100
+ push("]", "punctuation");
101
+ i++;
102
+ }
103
+ continue;
104
+ }
105
+ if (c === "*") {
106
+ push("*", "tag");
107
+ i++;
108
+ continue;
109
+ }
110
+ if (WORD.test(c)) {
111
+ const j = runOfWord(i);
112
+ push(selector.slice(i, j), "tag");
113
+ i = j;
114
+ continue;
115
+ }
116
+ push(c, "punctuation");
117
+ i++;
118
+ }
119
+ return tokens;
120
+ }
121
+ function anchorAllowsName(name, anchor) {
122
+ if (!anchor) return true;
123
+ if (anchor.enabled === false) return false;
124
+ try {
125
+ if (anchor.ignore && new RegExp(anchor.ignore).test(name)) return false;
126
+ if (anchor.allow) return new RegExp(anchor.allow).test(name);
127
+ } catch {
128
+ return true;
129
+ }
130
+ return true;
131
+ }
132
+ function matchesSelectorConfig(selector, config) {
133
+ for (const tok of tokenizeSelector(selector)) {
134
+ if (tok.type === "id") {
135
+ if (!anchorAllowsName(tok.text.replace(/^#/, ""), config.id)) return false;
136
+ } else if (tok.type === "class") {
137
+ if (!anchorAllowsName(tok.text.replace(/^\./, ""), config.class)) return false;
138
+ } else if (tok.type === "tag") {
139
+ if (tok.text !== "*" && !anchorAllowsName(tok.text, config.tag)) return false;
140
+ } else if (tok.type === "attrName") {
141
+ if (!anchorAllowsName(tok.text, config.attr)) return false;
142
+ }
143
+ }
144
+ return true;
145
+ }
146
+
147
+ export { CHANNEL, PROTOCOL_VERSION, isEnvelope, matchesSelectorConfig, tokenizeSelector };
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "@openpicker/protocol",
3
+ "version": "0.1.0",
4
+ "description": "Open postMessage protocol for the openpicker CSS element picker — wire types, constants, and selector helpers.",
5
+ "license": "MIT",
6
+ "author": "Usertour (https://www.usertour.io)",
7
+ "homepage": "https://openpicker.dev",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/usertour/openpicker.git",
11
+ "directory": "packages/protocol"
12
+ },
13
+ "bugs": {
14
+ "url": "https://github.com/usertour/openpicker/issues"
15
+ },
16
+ "keywords": [
17
+ "openpicker",
18
+ "protocol",
19
+ "postmessage",
20
+ "element-picker",
21
+ "css-selector",
22
+ "browser-extension"
23
+ ],
24
+ "type": "module",
25
+ "exports": {
26
+ ".": {
27
+ "types": "./dist/index.d.ts",
28
+ "import": "./dist/index.js",
29
+ "require": "./dist/index.cjs"
30
+ }
31
+ },
32
+ "types": "./dist/index.d.ts",
33
+ "sideEffects": false,
34
+ "files": [
35
+ "dist"
36
+ ],
37
+ "publishConfig": {
38
+ "access": "public",
39
+ "registry": "https://registry.npmjs.org/"
40
+ },
41
+ "devDependencies": {
42
+ "tsup": "^8.5.0",
43
+ "typescript": "^5.9.3"
44
+ },
45
+ "scripts": {
46
+ "build": "tsup",
47
+ "dev": "tsup --watch",
48
+ "typecheck": "tsc --noEmit"
49
+ },
50
+ "main": "./dist/index.cjs",
51
+ "module": "./dist/index.js"
52
+ }