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,290 @@
1
+ /**
2
+ * Credential extraction over a recorded session.
3
+ *
4
+ * Detects login form submissions in raw HTTP request bodies (form-urlencoded
5
+ * or JSON) and pairs each password-like field with the most likely
6
+ * username/email field in the same body. Surfaces the values + their byte
7
+ * locations so the redaction pass can rewrite them to `${credential.NAME}`
8
+ * placeholders BEFORE the LLM sees the session.
9
+ *
10
+ * The DOM event stream is consulted as a confirmation signal — passwords are
11
+ * already client-side-masked there by inject-listener.ts, but the username
12
+ * value is visible and lets us confirm which form was the login form.
13
+ */
14
+
15
+ import { isSensitiveCredentialKey, normalizeKey } from './sensitive-keys.ts';
16
+ import type { CapturedEvent, CapturedRequest, Session } from './types.ts';
17
+
18
+ /** Field-name patterns we'll treat as the username/email partner of a
19
+ * password field. Ordered by preference: emails first, then user-ish
20
+ * identifiers. */
21
+ const USERNAME_KEY_RE =
22
+ /^(user(?:name|id)?|email(?:address)?|login(?:id)?|account|patron(?:number|id)?)$/i;
23
+
24
+ /** Where, within a request, a redactable value lives. */
25
+ export type ReplacementLocation =
26
+ | { kind: 'body-form'; key: string }
27
+ | { kind: 'body-json'; path: string[] };
28
+
29
+ export interface Replacement {
30
+ /** Index into session.requests. */
31
+ requestSeq: number;
32
+ /** Where exactly in the request body. */
33
+ location: ReplacementLocation;
34
+ /** The exact substring we'll overwrite. */
35
+ originalValue: string;
36
+ /** What we'll replace it with — e.g. `${credential.username}`. */
37
+ placeholder: string;
38
+ }
39
+
40
+ export interface CredentialFinding {
41
+ kind: 'login-pair';
42
+ /** `username` for form-login pairs by default. Re-namable by the user. */
43
+ usernameName: string;
44
+ passwordName: string;
45
+ usernameValue: string;
46
+ passwordValue: string;
47
+ /** Where these values live (used by the redactor, also surfaced to the
48
+ * user so they can verify the right form was detected). */
49
+ requestSeq: number;
50
+ /** Brief request label like `POST /api/security/v4/security/token`. */
51
+ requestLabel: string;
52
+ /** Whether the username appears in form-submit DOM events too (high signal). */
53
+ confirmedByDom: boolean;
54
+ }
55
+
56
+ interface ExtractionResult {
57
+ findings: CredentialFinding[];
58
+ replacements: Replacement[];
59
+ }
60
+
61
+ /** Top-level entry point. */
62
+ export function extractCredentials(session: Session): ExtractionResult {
63
+ const findings: CredentialFinding[] = [];
64
+ const replacements: Replacement[] = [];
65
+ const usernamesInDom = collectFormSubmitUsernames(session.events);
66
+
67
+ for (const req of session.requests) {
68
+ if (!req.body) continue;
69
+ const ct = (req.headers['content-type'] ?? req.headers['Content-Type'] ?? '').toLowerCase();
70
+ const found = ct.includes('json')
71
+ ? findInJsonBody(req)
72
+ : ct.includes('urlencoded') || req.body.includes('=')
73
+ ? findInFormBody(req)
74
+ : null;
75
+ if (!found) continue;
76
+
77
+ const confirmedByDom = usernamesInDom.has(found.usernameValue);
78
+ findings.push({
79
+ kind: 'login-pair',
80
+ usernameName: 'username',
81
+ passwordName: 'password',
82
+ usernameValue: found.usernameValue,
83
+ passwordValue: found.passwordValue,
84
+ requestSeq: req.seq,
85
+ requestLabel: `${req.method} ${shortUrl(req.url)}`,
86
+ confirmedByDom,
87
+ });
88
+
89
+ replacements.push(
90
+ {
91
+ requestSeq: req.seq,
92
+ location: found.usernameLocation,
93
+ originalValue: found.usernameValue,
94
+ placeholder: '${credential.username}',
95
+ },
96
+ {
97
+ requestSeq: req.seq,
98
+ location: found.passwordLocation,
99
+ originalValue: found.passwordValue,
100
+ placeholder: '${credential.password}',
101
+ },
102
+ );
103
+ }
104
+
105
+ return { findings, replacements };
106
+ }
107
+
108
+ interface BodyFinding {
109
+ usernameValue: string;
110
+ passwordValue: string;
111
+ usernameLocation: ReplacementLocation;
112
+ passwordLocation: ReplacementLocation;
113
+ }
114
+
115
+ function findInFormBody(req: CapturedRequest): BodyFinding | null {
116
+ if (!req.body) return null;
117
+ const pairs = parseFormBody(req.body);
118
+ let usernameKey: string | null = null;
119
+ let usernameValue: string | null = null;
120
+ let passwordKey: string | null = null;
121
+ let passwordValue: string | null = null;
122
+
123
+ // First pass: find a sensitive (password-like) key.
124
+ for (const { key, value } of pairs) {
125
+ if (isSensitiveCredentialKey(key) && value.length > 0) {
126
+ passwordKey = key;
127
+ passwordValue = value;
128
+ break;
129
+ }
130
+ }
131
+ if (passwordKey === null || passwordValue === null) return null;
132
+
133
+ // Second pass: find a username-like key.
134
+ for (const { key, value } of pairs) {
135
+ if (USERNAME_KEY_RE.test(normalizeKey(key)) && value.length > 0) {
136
+ usernameKey = key;
137
+ usernameValue = value;
138
+ break;
139
+ }
140
+ }
141
+ if (usernameKey === null || usernameValue === null) return null;
142
+
143
+ return {
144
+ usernameValue,
145
+ passwordValue,
146
+ usernameLocation: { kind: 'body-form', key: usernameKey },
147
+ passwordLocation: { kind: 'body-form', key: passwordKey },
148
+ };
149
+ }
150
+
151
+ function findInJsonBody(req: CapturedRequest): BodyFinding | null {
152
+ if (!req.body) return null;
153
+ let parsed: unknown;
154
+ try {
155
+ parsed = JSON.parse(req.body);
156
+ } catch {
157
+ return null;
158
+ }
159
+ if (!parsed || typeof parsed !== 'object') return null;
160
+
161
+ const pwdHit = findFirstByPredicate(parsed, isSensitiveCredentialKey);
162
+ if (!pwdHit) return null;
163
+ if (typeof pwdHit.value !== 'string' || pwdHit.value.length === 0) return null;
164
+
165
+ // Look for a username-like key; prefer one in the same parent object.
166
+ const userHit = findFirstByPredicate(
167
+ parsed,
168
+ (k) => USERNAME_KEY_RE.test(normalizeKey(k)),
169
+ pwdHit.parent,
170
+ );
171
+ if (!userHit || typeof userHit.value !== 'string' || userHit.value.length === 0) return null;
172
+
173
+ return {
174
+ usernameValue: userHit.value,
175
+ passwordValue: pwdHit.value,
176
+ usernameLocation: { kind: 'body-json', path: userHit.path },
177
+ passwordLocation: { kind: 'body-json', path: pwdHit.path },
178
+ };
179
+ }
180
+
181
+ interface JsonHit {
182
+ key: string;
183
+ value: unknown;
184
+ path: string[];
185
+ // Reference identity of the parent object — used to prefer same-parent matches.
186
+ // biome-ignore lint/suspicious/noExplicitAny: opaque parent ref
187
+ parent: any;
188
+ }
189
+
190
+ function findFirstByPredicate(
191
+ root: unknown,
192
+ predicate: (key: string) => boolean,
193
+ preferredParent?: unknown,
194
+ ): JsonHit | null {
195
+ // BFS. If `preferredParent` is set, we run twice: first restricted to
196
+ // children of that parent, then anywhere.
197
+ if (preferredParent) {
198
+ const r1 = bfsFindUnder(preferredParent, predicate);
199
+ if (r1) return r1;
200
+ }
201
+ return bfsFind(root, predicate);
202
+ }
203
+
204
+ function bfsFindUnder(parent: unknown, predicate: (key: string) => boolean): JsonHit | null {
205
+ if (!parent || typeof parent !== 'object') return null;
206
+ for (const [k, v] of Object.entries(parent)) {
207
+ if (predicate(k)) return { key: k, value: v, path: [k], parent };
208
+ }
209
+ return null;
210
+ }
211
+
212
+ function bfsFind(root: unknown, predicate: (key: string) => boolean): JsonHit | null {
213
+ const queue: Array<{ node: unknown; path: string[] }> = [{ node: root, path: [] }];
214
+ while (queue.length > 0) {
215
+ const item = queue.shift();
216
+ if (!item) break;
217
+ const { node, path } = item;
218
+ if (Array.isArray(node)) {
219
+ for (let i = 0; i < node.length; i++) {
220
+ queue.push({ node: node[i], path: [...path, String(i)] });
221
+ }
222
+ } else if (node && typeof node === 'object') {
223
+ for (const [k, v] of Object.entries(node)) {
224
+ if (predicate(k)) return { key: k, value: v, path: [...path, k], parent: node };
225
+ queue.push({ node: v, path: [...path, k] });
226
+ }
227
+ }
228
+ }
229
+ return null;
230
+ }
231
+
232
+ function collectFormSubmitUsernames(events: CapturedEvent[]): Set<string> {
233
+ const out = new Set<string>();
234
+ for (const ev of events) {
235
+ if (ev.type !== 'submit') continue;
236
+ try {
237
+ const detail = JSON.parse(ev.detail) as {
238
+ fields?: Array<{ name?: string; type?: string; value?: string }>;
239
+ };
240
+ for (const f of detail.fields ?? []) {
241
+ if (
242
+ f.name &&
243
+ f.value &&
244
+ f.type !== 'password' &&
245
+ USERNAME_KEY_RE.test(normalizeKey(f.name))
246
+ ) {
247
+ out.add(f.value);
248
+ }
249
+ }
250
+ } catch {
251
+ // ignore malformed details
252
+ }
253
+ }
254
+ return out;
255
+ }
256
+
257
+ /** Parse `a=1&b=2` into pairs, URL-decoding both sides. Best-effort: bad
258
+ * pairs get skipped. */
259
+ export function parseFormBody(body: string): Array<{ key: string; value: string }> {
260
+ const out: Array<{ key: string; value: string }> = [];
261
+ for (const pair of body.split('&')) {
262
+ const eq = pair.indexOf('=');
263
+ if (eq === -1) continue;
264
+ const rawK = pair.slice(0, eq);
265
+ const rawV = pair.slice(eq + 1);
266
+ let k: string;
267
+ let v: string;
268
+ try {
269
+ k = decodeURIComponent(rawK);
270
+ } catch {
271
+ k = rawK;
272
+ }
273
+ try {
274
+ v = decodeURIComponent(rawV);
275
+ } catch {
276
+ v = rawV;
277
+ }
278
+ out.push({ key: k, value: v });
279
+ }
280
+ return out;
281
+ }
282
+
283
+ function shortUrl(url: string): string {
284
+ try {
285
+ const u = new URL(url);
286
+ return `${u.pathname}${u.search ? '?…' : ''}`.slice(0, 80);
287
+ } catch {
288
+ return url.slice(0, 80);
289
+ }
290
+ }