imprint-mcp 0.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.
Files changed (97) hide show
  1. package/CHANGELOG.md +168 -0
  2. package/LICENSE +21 -0
  3. package/README.md +322 -0
  4. package/examples/discoverandgo/README.md +57 -0
  5. package/examples/discoverandgo/book_discoverandgo_museum_pass/cron.json +8 -0
  6. package/examples/discoverandgo/book_discoverandgo_museum_pass/index.ts +89 -0
  7. package/examples/discoverandgo/book_discoverandgo_museum_pass/workflow.json +39 -0
  8. package/examples/echo/README.md +37 -0
  9. package/examples/echo/echo_test/index.ts +31 -0
  10. package/examples/google-flights/search_google_flights/index.ts +101 -0
  11. package/examples/google-flights/search_google_flights/parser.test.ts +140 -0
  12. package/examples/google-flights/search_google_flights/parser.ts +189 -0
  13. package/examples/google-flights/search_google_flights/playbook.yaml +130 -0
  14. package/examples/google-flights/search_google_flights/workflow.json +48 -0
  15. package/examples/google-hotels/search_google_hotels/index.ts +194 -0
  16. package/examples/google-hotels/search_google_hotels/parser.test.ts +168 -0
  17. package/examples/google-hotels/search_google_hotels/parser.ts +330 -0
  18. package/examples/google-hotels/search_google_hotels/playbook.yaml +125 -0
  19. package/examples/google-hotels/search_google_hotels/workflow.json +111 -0
  20. package/examples/namecheap-domains/search_namecheap_domains/index.ts +144 -0
  21. package/examples/namecheap-domains/search_namecheap_domains/parser.ts +380 -0
  22. package/examples/namecheap-domains/search_namecheap_domains/playbook.yaml +50 -0
  23. package/examples/namecheap-domains/search_namecheap_domains/request-transform.ts +136 -0
  24. package/examples/namecheap-domains/search_namecheap_domains/workflow.json +97 -0
  25. package/examples/southwest/README.md +81 -0
  26. package/examples/southwest/search_southwest_flights/backends.json +23 -0
  27. package/examples/southwest/search_southwest_flights/cron.json +19 -0
  28. package/examples/southwest/search_southwest_flights/index.ts +110 -0
  29. package/examples/southwest/search_southwest_flights/playbook.yaml +46 -0
  30. package/examples/southwest/search_southwest_flights/workflow.json +54 -0
  31. package/package.json +78 -0
  32. package/prompts/compile-agent.md +580 -0
  33. package/prompts/intent-detection.md +198 -0
  34. package/prompts/playbook-compilation.md +279 -0
  35. package/prompts/request-triage.md +74 -0
  36. package/prompts/tool-candidate-detection.md +104 -0
  37. package/src/cli.ts +1287 -0
  38. package/src/imprint/agent.ts +468 -0
  39. package/src/imprint/app-api-hosts.ts +53 -0
  40. package/src/imprint/backend-ladder.ts +568 -0
  41. package/src/imprint/check.ts +136 -0
  42. package/src/imprint/chromium.ts +211 -0
  43. package/src/imprint/claude-cli-compile.ts +640 -0
  44. package/src/imprint/cli-credential.ts +394 -0
  45. package/src/imprint/codex-cli-compile.ts +712 -0
  46. package/src/imprint/compile-agent-types.ts +40 -0
  47. package/src/imprint/compile-agent.ts +404 -0
  48. package/src/imprint/compile-tools.ts +1389 -0
  49. package/src/imprint/compile.ts +720 -0
  50. package/src/imprint/cookie-jar.ts +246 -0
  51. package/src/imprint/credential-bundle.ts +195 -0
  52. package/src/imprint/credential-extract.ts +290 -0
  53. package/src/imprint/credential-store.ts +707 -0
  54. package/src/imprint/cron.ts +312 -0
  55. package/src/imprint/doctor.ts +223 -0
  56. package/src/imprint/emit.ts +154 -0
  57. package/src/imprint/etld.ts +134 -0
  58. package/src/imprint/freeform-redact.ts +216 -0
  59. package/src/imprint/inject-listener.ts +137 -0
  60. package/src/imprint/install.ts +795 -0
  61. package/src/imprint/integrations.ts +385 -0
  62. package/src/imprint/is-compiled.ts +2 -0
  63. package/src/imprint/json-path.ts +100 -0
  64. package/src/imprint/llm.ts +998 -0
  65. package/src/imprint/load-json.ts +54 -0
  66. package/src/imprint/log.ts +33 -0
  67. package/src/imprint/login.ts +166 -0
  68. package/src/imprint/mcp-compile-server.ts +282 -0
  69. package/src/imprint/mcp-maintenance.ts +1790 -0
  70. package/src/imprint/mcp-server.ts +350 -0
  71. package/src/imprint/multi-progress.ts +69 -0
  72. package/src/imprint/notify.ts +155 -0
  73. package/src/imprint/paths.ts +64 -0
  74. package/src/imprint/playbook-parser.ts +21 -0
  75. package/src/imprint/playbook-runner.ts +465 -0
  76. package/src/imprint/probe-backends.ts +251 -0
  77. package/src/imprint/progress.ts +28 -0
  78. package/src/imprint/record.ts +470 -0
  79. package/src/imprint/redact.ts +550 -0
  80. package/src/imprint/replay-capture.ts +387 -0
  81. package/src/imprint/request-context.ts +66 -0
  82. package/src/imprint/runtime-link.ts +73 -0
  83. package/src/imprint/runtime.ts +942 -0
  84. package/src/imprint/sensitive-keys.ts +156 -0
  85. package/src/imprint/session-diff.ts +409 -0
  86. package/src/imprint/session-merge.ts +198 -0
  87. package/src/imprint/session-writer.ts +149 -0
  88. package/src/imprint/sites.ts +27 -0
  89. package/src/imprint/stealth-fetch.ts +434 -0
  90. package/src/imprint/teach-state.ts +235 -0
  91. package/src/imprint/teach.ts +2120 -0
  92. package/src/imprint/tool-candidates.ts +423 -0
  93. package/src/imprint/tool-loader.ts +186 -0
  94. package/src/imprint/tool-selection.ts +70 -0
  95. package/src/imprint/tracing.ts +508 -0
  96. package/src/imprint/types.ts +472 -0
  97. package/src/imprint/version.ts +21 -0
@@ -0,0 +1,134 @@
1
+ /** Registrable-domain extraction (eTLD+1) for hostname filtering.
2
+ *
3
+ * Naive `split('.').slice(-2)` is wrong for multi-part public suffixes:
4
+ * api.example.co.uk → "co.uk" (wrong; should be "example.co.uk")
5
+ * which then over-matches every other .co.uk hostname.
6
+ *
7
+ * We're not pulling in the full Mozilla Public Suffix List — too much
8
+ * weight for a CLI tool. Instead, a small allow-list of the common
9
+ * multi-part suffixes covers ~all real-world cases. If we ever record
10
+ * against an exotic ccTLD we'll add it here. */
11
+ const MULTI_PART_SUFFIXES = new Set([
12
+ // United Kingdom
13
+ 'co.uk',
14
+ 'org.uk',
15
+ 'me.uk',
16
+ 'ltd.uk',
17
+ 'plc.uk',
18
+ 'net.uk',
19
+ 'sch.uk',
20
+ 'ac.uk',
21
+ 'gov.uk',
22
+ 'nhs.uk',
23
+ // Australia
24
+ 'com.au',
25
+ 'net.au',
26
+ 'org.au',
27
+ 'edu.au',
28
+ 'gov.au',
29
+ 'asn.au',
30
+ 'id.au',
31
+ // Japan
32
+ 'co.jp',
33
+ 'ne.jp',
34
+ 'or.jp',
35
+ 'ac.jp',
36
+ 'ad.jp',
37
+ 'go.jp',
38
+ 'gr.jp',
39
+ // Brazil
40
+ 'com.br',
41
+ 'net.br',
42
+ 'org.br',
43
+ 'gov.br',
44
+ 'edu.br',
45
+ 'mil.br',
46
+ // South Africa
47
+ 'co.za',
48
+ 'ac.za',
49
+ 'gov.za',
50
+ 'org.za',
51
+ 'net.za',
52
+ // Mexico
53
+ 'com.mx',
54
+ 'gob.mx',
55
+ 'org.mx',
56
+ 'edu.mx',
57
+ // India
58
+ 'co.in',
59
+ 'gov.in',
60
+ 'ac.in',
61
+ 'org.in',
62
+ 'net.in',
63
+ 'edu.in',
64
+ // South Korea
65
+ 'co.kr',
66
+ 'ne.kr',
67
+ 'or.kr',
68
+ 'go.kr',
69
+ 'ac.kr',
70
+ // China
71
+ 'com.cn',
72
+ 'net.cn',
73
+ 'org.cn',
74
+ 'gov.cn',
75
+ 'edu.cn',
76
+ 'ac.cn',
77
+ // Hong Kong
78
+ 'com.hk',
79
+ 'org.hk',
80
+ 'gov.hk',
81
+ 'edu.hk',
82
+ 'net.hk',
83
+ // New Zealand
84
+ 'co.nz',
85
+ 'org.nz',
86
+ 'net.nz',
87
+ 'govt.nz',
88
+ 'ac.nz',
89
+ // Singapore
90
+ 'com.sg',
91
+ 'org.sg',
92
+ 'gov.sg',
93
+ 'edu.sg',
94
+ 'net.sg',
95
+ // Israel
96
+ 'co.il',
97
+ 'org.il',
98
+ 'gov.il',
99
+ 'ac.il',
100
+ 'net.il',
101
+ // Argentina
102
+ 'com.ar',
103
+ 'gov.ar',
104
+ 'edu.ar',
105
+ 'org.ar',
106
+ ]);
107
+
108
+ /** Return the registrable domain (eTLD+1) of a hostname.
109
+ * Examples:
110
+ * api.example.com → example.com
111
+ * api.example.co.uk → example.co.uk
112
+ * example.com → example.com
113
+ * localhost → localhost
114
+ * 192.168.1.1 → 192.168.1.1 (no further reduction) */
115
+ export function registrableDomain(hostname: string): string {
116
+ // IPs and bare hosts pass through unchanged.
117
+ if (hostname.length === 0) return hostname;
118
+ if (/^\d+\.\d+\.\d+\.\d+$/.test(hostname)) return hostname;
119
+
120
+ const parts = hostname.split('.');
121
+ if (parts.length <= 2) return hostname;
122
+
123
+ const lastTwo = parts.slice(-2).join('.');
124
+ if (MULTI_PART_SUFFIXES.has(lastTwo) && parts.length >= 3) {
125
+ return parts.slice(-3).join('.');
126
+ }
127
+ return parts.slice(-2).join('.');
128
+ }
129
+
130
+ /** True when `hostname` is the registrable domain `root` or a subdomain
131
+ * of it. Used by request-filtering and cookie-scoping. */
132
+ export function isSameRegistrableDomain(hostname: string, root: string): boolean {
133
+ return hostname === root || hostname.endsWith(`.${root}`);
134
+ }
@@ -0,0 +1,216 @@
1
+ import { Policies, type PolicyName, createRedactum } from 'redactum';
2
+
3
+ const FREEFORM_POLICIES: PolicyName[] = [
4
+ Policies.EMAIL_ADDRESS,
5
+ Policies.PHONE_NUMBER_US,
6
+ Policies.PHONE_NUMBER_INTERNATIONAL,
7
+ Policies.PHONE_NUMBER_UK,
8
+ Policies.PHONE_NUMBER_CANADIAN,
9
+ Policies.SSN,
10
+ Policies.CREDIT_CARD,
11
+ Policies.CREDIT_CARD_WITH_SEPARATORS,
12
+ Policies.CREDIT_CARD_CVV,
13
+ Policies.URL_WITH_CREDENTIALS,
14
+ Policies.JWT_TOKEN,
15
+ Policies.BASIC_AUTH_HEADER,
16
+ Policies.BEARER_TOKEN_HEADER,
17
+ Policies.API_KEY_HEADER,
18
+ Policies.SESSION_ID_COOKIE,
19
+ Policies.API_KEY_GENERIC,
20
+ Policies.OPENAI_API_KEY,
21
+ Policies.ANTHROPIC_API_KEY,
22
+ Policies.GOOGLE_API_KEY,
23
+ Policies.GCP_API_KEY,
24
+ Policies.GITHUB_TOKEN,
25
+ Policies.GITHUB_FINE_GRAINED_TOKEN,
26
+ Policies.GITLAB_TOKEN,
27
+ Policies.BITBUCKET_TOKEN,
28
+ Policies.AWS_ACCESS_KEY,
29
+ Policies.AWS_SECRET_KEY,
30
+ Policies.AWS_SESSION_TOKEN,
31
+ Policies.AZURE_STORAGE_CONNECTION_STRING,
32
+ Policies.DIGITALOCEAN_TOKEN,
33
+ Policies.HEROKU_API_KEY,
34
+ Policies.RAILWAY_TOKEN,
35
+ Policies.CLOUDFLARE_API_TOKEN,
36
+ Policies.DOCKER_HUB_TOKEN,
37
+ Policies.DOCKER_REGISTRY_TOKEN,
38
+ Policies.NPM_TOKEN,
39
+ Policies.PYPI_TOKEN,
40
+ Policies.RUBYGEMS_API_KEY,
41
+ Policies.QUAY_IO_TOKEN,
42
+ Policies.JFROG_ARTIFACTORY_TOKEN,
43
+ Policies.NEXUS_REPOSITORY_TOKEN,
44
+ Policies.SLACK_WEBHOOK,
45
+ Policies.DISCORD_WEBHOOK,
46
+ Policies.WEBHOOK_URL,
47
+ Policies.SLACK_TOKEN,
48
+ Policies.DISCORD_TOKEN,
49
+ Policies.TWILIO_AUTH_TOKEN,
50
+ Policies.TWILIO_API_KEY,
51
+ Policies.SENDGRID_API_KEY,
52
+ Policies.STRIPE_KEY,
53
+ Policies.MAILGUN_API_KEY,
54
+ Policies.MAILCHIMP_API_KEY,
55
+ Policies.MONGODB_CONNECTION_STRING,
56
+ Policies.POSTGRESQL_CONNECTION_STRING,
57
+ Policies.MYSQL_CONNECTION_STRING,
58
+ Policies.REDIS_CONNECTION_STRING,
59
+ Policies.ELASTICSEARCH_URL,
60
+ Policies.RABBITMQ_CONNECTION_STRING,
61
+ Policies.KAFKA_CONNECTION_STRING,
62
+ Policies.CASSANDRA_CONNECTION_STRING,
63
+ Policies.DATABASE_CONNECTION_STRING,
64
+ Policies.DATABASE_URL,
65
+ Policies.LDAP_CONNECTION_STRING,
66
+ Policies.JDBC_CONNECTION_STRING,
67
+ Policies.SMTP_CONNECTION_STRING,
68
+ Policies.SSH_PRIVATE_KEY,
69
+ Policies.RSA_PRIVATE_KEY,
70
+ Policies.EC_PRIVATE_KEY,
71
+ Policies.OPENSSH_PRIVATE_KEY,
72
+ Policies.GENERIC_PRIVATE_KEY,
73
+ Policies.PGP_PRIVATE_KEY,
74
+ Policies.PASSWORD_ASSIGNMENT,
75
+ Policies.ENVIRONMENT_VARIABLE_SECRET,
76
+ Policies.GENERIC_PASSWORD,
77
+ Policies.GENERIC_TOKEN,
78
+ Policies.GENERIC_CREDENTIAL,
79
+ Policies.GENERIC_SECRET,
80
+ Policies.OAUTH_CLIENT_SECRET,
81
+ Policies.OAUTH_REFRESH_TOKEN,
82
+ Policies.OAUTH_ACCESS_TOKEN,
83
+ Policies.OKTA_API_TOKEN,
84
+ Policies.AUTH0_API_TOKEN,
85
+ Policies.KEYCLOAK_CLIENT_SECRET,
86
+ Policies.JENKINS_TOKEN,
87
+ Policies.CIRCLECI_TOKEN,
88
+ Policies.TRAVIS_CI_TOKEN,
89
+ Policies.GITLAB_CI_TOKEN,
90
+ Policies.AZURE_DEVOPS_TOKEN,
91
+ Policies.BITBUCKET_TOKEN_ALT,
92
+ Policies.SENTRY_DSN,
93
+ Policies.NEW_RELIC_LICENSE_KEY,
94
+ Policies.DATADOG_API_KEY,
95
+ Policies.PAGERDUTY_INTEGRATION_KEY,
96
+ Policies.GRAFANA_API_KEY,
97
+ Policies.SPLUNK_HEC_TOKEN,
98
+ Policies.BUGSNAG_API_KEY,
99
+ Policies.ROLLBAR_ACCESS_TOKEN,
100
+ Policies.AIRBRAKE_API_KEY,
101
+ Policies.LOGDNA_INGESTION_KEY,
102
+ Policies.LOGGLY_TOKEN,
103
+ Policies.PAPERTRAIL_TOKEN,
104
+ Policies.TERRAFORM_CLOUD_TOKEN,
105
+ Policies.HASHICORP_VAULT_TOKEN,
106
+ Policies.AWS_SECRETS_MANAGER_ARN,
107
+ Policies.AZURE_KEY_VAULT_SECRET,
108
+ Policies.GCP_SECRET_MANAGER,
109
+ Policies.CONSUL_TOKEN,
110
+ Policies.RANCHER_TOKEN,
111
+ Policies.AGE_SECRET_KEY,
112
+ Policies.MASTER_KEY,
113
+ ];
114
+
115
+ const REDACTION_HINT_RE =
116
+ /@|\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.|\b\d{3}[-.\s]\d{2}[-.\s]\d{4}\b|\b\d{3}[-.\s]\d{3}[-.\s]\d{4}\b|\b\d{4}[-\s]\d{4}[-\s]\d{4}[-\s]\d{4}\b|api[_-]?key|auth|bearer|card|cookie|credential|key|password|postgres|mysql|mongodb|redis|secret|session|sk-|token|-----BEGIN/i;
117
+
118
+ const PROTECTED_PATTERNS = [
119
+ /\[REDACTED:\d+\]/g,
120
+ /\$\{credential\.[A-Za-z0-9_.-]+\}/g,
121
+ /\b[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\b/gi,
122
+ /\b[0-9a-f]{40}\b/gi,
123
+ ];
124
+
125
+ const REDACTOR = createRedactum({
126
+ policies: FREEFORM_POLICIES,
127
+ replacement: () => '[REDACTED]',
128
+ });
129
+ const CACHE_MAX = 512;
130
+ const cache = new Map<string, FreeformRedaction>();
131
+
132
+ interface FreeformRedaction {
133
+ redacted: string;
134
+ redactionsCount: number;
135
+ }
136
+
137
+ interface Range {
138
+ start: number;
139
+ end: number;
140
+ }
141
+
142
+ export function redactFreeformText(text: string): FreeformRedaction {
143
+ if (!hasFreeformRedactionHint(text)) {
144
+ return { redacted: text, redactionsCount: 0 };
145
+ }
146
+
147
+ const protectedRanges = collectProtectedRanges(text);
148
+ if (protectedRanges.length === 0) {
149
+ return redactUnprotectedText(text);
150
+ }
151
+
152
+ let redacted = '';
153
+ let cursor = 0;
154
+ let redactionsCount = 0;
155
+ for (const range of protectedRanges) {
156
+ const segment = text.slice(cursor, range.start);
157
+ const segmentResult = redactUnprotectedText(segment);
158
+ redacted += segmentResult.redacted;
159
+ redactionsCount += segmentResult.redactionsCount;
160
+ redacted += text.slice(range.start, range.end);
161
+ cursor = range.end;
162
+ }
163
+
164
+ const tail = redactUnprotectedText(text.slice(cursor));
165
+ redacted += tail.redacted;
166
+ redactionsCount += tail.redactionsCount;
167
+
168
+ return { redacted, redactionsCount };
169
+ }
170
+
171
+ function redactUnprotectedText(text: string): FreeformRedaction {
172
+ if (text.length === 0 || !hasFreeformRedactionHint(text)) {
173
+ return { redacted: text, redactionsCount: 0 };
174
+ }
175
+ const cached = cache.get(text);
176
+ if (cached) return cached;
177
+
178
+ const result = REDACTOR.redactum(text);
179
+ const redaction = {
180
+ redacted: result.redactedText,
181
+ redactionsCount: result.stats.totalFindings,
182
+ };
183
+ cache.set(text, redaction);
184
+ if (cache.size > CACHE_MAX) {
185
+ const oldest = cache.keys().next().value;
186
+ if (oldest !== undefined) cache.delete(oldest);
187
+ }
188
+ return redaction;
189
+ }
190
+
191
+ export function hasFreeformRedactionHint(text: string): boolean {
192
+ return REDACTION_HINT_RE.test(text);
193
+ }
194
+
195
+ function collectProtectedRanges(text: string): Range[] {
196
+ const ranges: Range[] = [];
197
+ for (const pattern of PROTECTED_PATTERNS) {
198
+ pattern.lastIndex = 0;
199
+ for (const match of text.matchAll(pattern)) {
200
+ if (match.index === undefined) continue;
201
+ ranges.push({ start: match.index, end: match.index + match[0].length });
202
+ }
203
+ }
204
+ ranges.sort((a, b) => a.start - b.start || b.end - a.end);
205
+
206
+ const merged: Range[] = [];
207
+ for (const range of ranges) {
208
+ const previous = merged.at(-1);
209
+ if (!previous || range.start > previous.end) {
210
+ merged.push({ ...range });
211
+ } else if (range.end > previous.end) {
212
+ previous.end = range.end;
213
+ }
214
+ }
215
+ return merged;
216
+ }
@@ -0,0 +1,137 @@
1
+ /**
2
+ * Injected per-page (Page.addScriptToEvaluateOnNewDocument). Passive
3
+ * capture-phase listeners emit sentinel-prefixed console.log lines that
4
+ * the recorder picks up via Runtime.consoleAPICalled. Sentinel
5
+ * `[IMPRINT]` is exact-match — keep stable.
6
+ */
7
+
8
+ export const INJECTED_LISTENER_SOURCE = `
9
+ (function imprintInjector() {
10
+ if (window.__imprint_injected__) return;
11
+ window.__imprint_injected__ = true;
12
+
13
+ const SENTINEL = '[IMPRINT]';
14
+ const MAX_VAL = 200;
15
+
16
+ function safeStr(v) {
17
+ try {
18
+ if (v == null) return null;
19
+ const s = String(v);
20
+ return s.length > MAX_VAL ? s.slice(0, MAX_VAL) + '…' : s;
21
+ } catch (e) { return null; }
22
+ }
23
+
24
+ function selectorFor(el) {
25
+ try {
26
+ if (!el || !el.tagName) return null;
27
+ if (el.id) return '#' + el.id;
28
+ const parts = [];
29
+ let node = el;
30
+ let depth = 0;
31
+ while (node && node.nodeType === 1 && depth < 5) {
32
+ let part = node.tagName.toLowerCase();
33
+ if (node.className && typeof node.className === 'string') {
34
+ const cls = node.className.trim().split(/\\s+/).slice(0, 2).join('.');
35
+ if (cls) part += '.' + cls;
36
+ }
37
+ parts.unshift(part);
38
+ node = node.parentElement;
39
+ depth++;
40
+ }
41
+ return parts.join(' > ');
42
+ } catch (e) { return null; }
43
+ }
44
+
45
+ function describe(el) {
46
+ try {
47
+ if (!el || !el.tagName) return {};
48
+ return {
49
+ tag: el.tagName.toLowerCase(),
50
+ id: el.id || null,
51
+ name: el.getAttribute && el.getAttribute('name') || null,
52
+ type: el.getAttribute && el.getAttribute('type') || null,
53
+ text: safeStr((el.textContent || '').trim()),
54
+ ariaLabel: el.getAttribute && el.getAttribute('aria-label') || null,
55
+ href: el.tagName === 'A' ? el.getAttribute('href') : null,
56
+ selector: selectorFor(el),
57
+ };
58
+ } catch (e) { return {}; }
59
+ }
60
+
61
+ function emit(type, payload) {
62
+ try {
63
+ console.log(SENTINEL, type, JSON.stringify(payload));
64
+ } catch (e) { /* ignore */ }
65
+ }
66
+
67
+ function onClick(ev) {
68
+ try {
69
+ const tgt = ev.target;
70
+ emit('click', describe(tgt));
71
+ } catch (e) { /* ignore */ }
72
+ }
73
+
74
+ function onInput(ev) {
75
+ try {
76
+ const tgt = ev.target;
77
+ // For inputs we capture the field name + a TRUNCATED value preview.
78
+ // Sensitive fields (type=password) get value redacted.
79
+ const desc = describe(tgt);
80
+ if (desc.type === 'password') {
81
+ desc.value = '[redacted password]';
82
+ } else if (tgt && 'value' in tgt) {
83
+ desc.value = safeStr(tgt.value);
84
+ }
85
+ emit('input', desc);
86
+ } catch (e) { /* ignore */ }
87
+ }
88
+
89
+ function onChange(ev) {
90
+ try {
91
+ const tgt = ev.target;
92
+ const desc = describe(tgt);
93
+ if (desc.type === 'password') {
94
+ desc.value = '[redacted password]';
95
+ } else if (tgt && 'value' in tgt) {
96
+ desc.value = safeStr(tgt.value);
97
+ }
98
+ emit('change', desc);
99
+ } catch (e) { /* ignore */ }
100
+ }
101
+
102
+ function onSubmit(ev) {
103
+ try {
104
+ const form = ev.target;
105
+ const fields = [];
106
+ if (form && form.elements) {
107
+ for (let i = 0; i < form.elements.length; i++) {
108
+ const el = form.elements[i];
109
+ if (!el || !el.name) continue;
110
+ let value = null;
111
+ if (el.type === 'password') {
112
+ value = '[redacted]';
113
+ } else if ('value' in el) {
114
+ value = safeStr(el.value);
115
+ }
116
+ fields.push({ name: el.name, type: el.type || null, value: value });
117
+ }
118
+ }
119
+ emit('submit', {
120
+ selector: selectorFor(form),
121
+ action: form && form.getAttribute && form.getAttribute('action') || null,
122
+ method: form && form.getAttribute && form.getAttribute('method') || 'get',
123
+ fields: fields,
124
+ });
125
+ } catch (e) { /* ignore */ }
126
+ }
127
+
128
+ // Capture phase = true so we see the event before the site has a chance to
129
+ // stopPropagation it.
130
+ document.addEventListener('click', onClick, true);
131
+ document.addEventListener('input', onInput, true);
132
+ document.addEventListener('change', onChange, true);
133
+ document.addEventListener('submit', onSubmit, true);
134
+ })();
135
+ `;
136
+
137
+ export const IMPRINT_SENTINEL = '[IMPRINT]';