imprint-mcp 0.6.0 → 0.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/package.json +4 -2
- package/prompts/audit-agent.md +4 -2
- package/prompts/auth-compile-agent.md +149 -158
- package/prompts/build-planning.md +14 -9
- package/prompts/compile-agent.md +46 -23
- package/prompts/live-verifier-agent.md +101 -0
- package/prompts/playbook-compilation.md +8 -1
- package/prompts/tool-candidate-detection.md +9 -29
- package/src/cli.ts +83 -1
- package/src/imprint/audit.ts +219 -20
- package/src/imprint/auth-compile-agent.ts +215 -155
- package/src/imprint/auth-compile-tools.ts +300 -118
- package/src/imprint/auth-verifier.ts +167 -180
- package/src/imprint/backend-cache.ts +18 -11
- package/src/imprint/backend-ladder.ts +225 -177
- package/src/imprint/build-plan.ts +3 -8
- package/src/imprint/cdp-browser-fetch.ts +930 -83
- package/src/imprint/claude-cli-compile.ts +46 -20
- package/src/imprint/codex-cli-compile.ts +436 -139
- package/src/imprint/compile-agent-types.ts +15 -10
- package/src/imprint/compile-agent.ts +88 -28
- package/src/imprint/compile-tools.ts +228 -39
- package/src/imprint/compile-verification.ts +187 -0
- package/src/imprint/compile.ts +71 -2
- package/src/imprint/credential-store.ts +57 -44
- package/src/imprint/emit.ts +3 -1
- package/src/imprint/freeform-redact.ts +27 -6
- package/src/imprint/live-verifier.ts +1665 -0
- package/src/imprint/llm.ts +22 -3
- package/src/imprint/login.ts +107 -25
- package/src/imprint/mcp-compile-server.ts +447 -114
- package/src/imprint/mcp-live-verifier-server.ts +359 -0
- package/src/imprint/mcp-server.ts +116 -35
- package/src/imprint/probe-backends.ts +88 -7
- package/src/imprint/recording-request.ts +36 -0
- package/src/imprint/request-capture.ts +40 -17
- package/src/imprint/runtime.ts +490 -337
- package/src/imprint/teach-state.ts +36 -2
- package/src/imprint/teach.ts +302 -146
- package/src/imprint/tool-candidates.ts +8 -10
- package/src/imprint/tool-loader.ts +12 -4
- package/src/imprint/types.ts +137 -68
|
@@ -1,13 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* - the claude-cli / codex-cli path (mcp-compile-server.ts in auth mode)
|
|
6
|
-
*
|
|
7
|
-
* Keeping these in one module guarantees the agent sees an identical toolset
|
|
8
|
-
* and identical external verification regardless of which provider drives it.
|
|
2
|
+
* Recording tools and contract-level verification shared by every auth compile
|
|
3
|
+
* provider. Site behavior is decided by the compile agent and encoded in the
|
|
4
|
+
* auth action program, not inferred here.
|
|
9
5
|
*/
|
|
10
6
|
|
|
7
|
+
import { createHash } from 'node:crypto';
|
|
11
8
|
import { existsSync, readFileSync } from 'node:fs';
|
|
12
9
|
import { join as pathJoin } from 'node:path';
|
|
13
10
|
import type { AgentTool } from './agent.ts';
|
|
@@ -18,50 +15,38 @@ import {
|
|
|
18
15
|
buildReadResponseBodyTool,
|
|
19
16
|
buildReadSessionSummaryTool,
|
|
20
17
|
buildRunBashTool,
|
|
18
|
+
buildSearchRequestsTool,
|
|
21
19
|
buildWriteFileTool,
|
|
22
20
|
} from './compile-tools.ts';
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
25
|
-
import type
|
|
21
|
+
import { recordedRequestMatchesWorkflow } from './recording-request.ts';
|
|
22
|
+
import { captureHeader, captureValueMatches, jsonpath } from './request-capture.ts';
|
|
23
|
+
import { type Session, type Workflow, WorkflowSchema } from './types.ts';
|
|
26
24
|
|
|
27
25
|
type TeachCredentials = { site: string; values: Record<string, string> };
|
|
28
26
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
* server is launched in auth mode, and the agent gets the auth tool list +
|
|
32
|
-
* initial prompt. */
|
|
27
|
+
export const AUTH_VERIFICATION_ATTEMPT_SENTINEL = '.auth-verification-attempt.json';
|
|
28
|
+
|
|
33
29
|
export interface AuthCliCompileMode {
|
|
34
|
-
/** Site slug — the MCP server loads credentials from the store for it. */
|
|
35
30
|
site: string;
|
|
36
|
-
/** JSON-serialized AuthToolPlan, passed to the MCP server. */
|
|
37
31
|
authPlanJson: string;
|
|
38
|
-
/** Short tool names to pre-approve (the driver prefixes them per provider). */
|
|
39
32
|
allowedTools: readonly string[];
|
|
40
|
-
/** The initial user message handed to the agent on turn 1. */
|
|
41
33
|
initialPrompt: string;
|
|
42
34
|
}
|
|
43
35
|
|
|
44
|
-
/** Short names of every tool the auth compile agent may call (excluding the
|
|
45
|
-
* lifecycle done/give_up). Used by the claude-cli path to build --allowedTools.
|
|
46
|
-
* The agent SHAPES from the recording with the read/write tools and never logs
|
|
47
|
-
* in itself; live login happens only via the checkpoint tools (run_verification
|
|
48
|
-
* / prompt_user / wait_for_cooldown), which the orchestrator executes. */
|
|
49
36
|
export const AUTH_COMPILE_TOOL_NAMES = [
|
|
50
37
|
'read_session_summary',
|
|
38
|
+
'search_requests',
|
|
51
39
|
'read_request',
|
|
52
40
|
'read_response_body',
|
|
53
41
|
'write_file',
|
|
54
42
|
'read_file',
|
|
55
43
|
'run_bash',
|
|
56
44
|
'run_verification',
|
|
45
|
+
'inspect_verification_page',
|
|
57
46
|
'prompt_user',
|
|
58
47
|
'wait_for_cooldown',
|
|
59
48
|
] as const;
|
|
60
49
|
|
|
61
|
-
/** Assemble the auth compile SHAPING toolset (read/write only — no live login).
|
|
62
|
-
* The checkpoint tools (run_verification/prompt_user/wait_for_cooldown) and
|
|
63
|
-
* done()/give_up() are appended by the driver (mcp-compile-server in auth mode)
|
|
64
|
-
* because they are orchestrator-mediated, not executed in this process. */
|
|
65
50
|
export function buildAuthCompileTools(
|
|
66
51
|
session: Session,
|
|
67
52
|
toolDir: string,
|
|
@@ -71,139 +56,336 @@ export function buildAuthCompileTools(
|
|
|
71
56
|
const context: CompileToolContext = { teachCredentials };
|
|
72
57
|
return [
|
|
73
58
|
buildReadSessionSummaryTool(session, context),
|
|
59
|
+
buildSearchRequestsTool(session),
|
|
74
60
|
buildReadRequestTool(session),
|
|
75
61
|
buildReadResponseBodyTool(session),
|
|
76
|
-
// Auth runs on cdp-replay only (a real headed browser that replays the
|
|
77
|
-
// recorded requests in-page) — there is no playbook rung in the auth path, so
|
|
78
|
-
// the agent emits workflow.json only. No playbook.yaml in the write allowlist.
|
|
79
62
|
buildWriteFileTool(toolDir),
|
|
80
63
|
buildReadFileTool(toolDir),
|
|
81
64
|
buildRunBashTool(toolDir),
|
|
82
65
|
];
|
|
83
66
|
}
|
|
84
67
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
* declare a matching `authConfig.sessionCapture` so the login persists it —
|
|
96
|
-
* otherwise the data tool's contracted auth header can never resolve at runtime. */
|
|
68
|
+
export function authWorkflowPreflightFailures(
|
|
69
|
+
toolDir: string,
|
|
70
|
+
session?: Session,
|
|
71
|
+
requiredCredentialNames: readonly string[] = [],
|
|
72
|
+
): string[] {
|
|
73
|
+
const workflowPath = pathJoin(toolDir, 'workflow.json');
|
|
74
|
+
if (!existsSync(workflowPath)) return ['workflow.json does not exist'];
|
|
75
|
+
return parseWorkflow(workflowPath, requiredCredentialNames, session).failures;
|
|
76
|
+
}
|
|
77
|
+
|
|
97
78
|
export function authExternalVerification(
|
|
98
79
|
toolDir: string,
|
|
99
80
|
requiredSessionCaptures: Array<{ name: string; usedAs?: string }> = [],
|
|
81
|
+
options: { requireLiveAttempt?: boolean; requiredCredentialNames?: readonly string[] } = {},
|
|
100
82
|
): string[] {
|
|
101
|
-
const failures: string[] = [];
|
|
102
83
|
const workflowPath = pathJoin(toolDir, 'workflow.json');
|
|
84
|
+
if (!existsSync(workflowPath)) return ['workflow.json does not exist'];
|
|
103
85
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
86
|
+
const parsed = parseWorkflow(workflowPath, options.requiredCredentialNames);
|
|
87
|
+
if (!parsed.workflow) return parsed.failures;
|
|
88
|
+
const failures = [...parsed.failures];
|
|
89
|
+
const workflow = parsed.workflow;
|
|
90
|
+
const persisted = new Set(workflow.authConfig?.persist ?? []);
|
|
91
|
+
|
|
92
|
+
const missingContracts = requiredSessionCaptures.filter((capture) => {
|
|
93
|
+
const target = (capture.usedAs ?? '').toLowerCase();
|
|
94
|
+
return (
|
|
95
|
+
target.startsWith('header:') &&
|
|
96
|
+
target !== 'header:cookie' &&
|
|
97
|
+
target !== 'header:set-cookie' &&
|
|
98
|
+
!persisted.has(capture.name)
|
|
99
|
+
);
|
|
100
|
+
});
|
|
101
|
+
if (missingContracts.length > 0) {
|
|
102
|
+
failures.push(
|
|
103
|
+
`authConfig.persist must include downstream credential capture(s): ${missingContracts
|
|
104
|
+
.map((capture) => capture.name)
|
|
105
|
+
.join(', ')}`,
|
|
106
|
+
);
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
+
if (options.requireLiveAttempt) {
|
|
110
|
+
failures.push(...liveAttemptFailures(toolDir, workflow));
|
|
111
|
+
}
|
|
112
|
+
return failures;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function parseWorkflow(
|
|
116
|
+
path: string,
|
|
117
|
+
requiredCredentialNames: readonly string[] = [],
|
|
118
|
+
session?: Session,
|
|
119
|
+
): { workflow?: Workflow; failures: string[] } {
|
|
109
120
|
let raw: unknown;
|
|
110
121
|
try {
|
|
111
|
-
raw = JSON.parse(readFileSync(
|
|
122
|
+
raw = JSON.parse(readFileSync(path, 'utf8'));
|
|
112
123
|
} catch (err) {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
124
|
+
return {
|
|
125
|
+
failures: [
|
|
126
|
+
`workflow.json is not valid JSON: ${err instanceof Error ? err.message : String(err)}`,
|
|
127
|
+
],
|
|
128
|
+
};
|
|
117
129
|
}
|
|
118
130
|
|
|
119
131
|
const parsed = WorkflowSchema.safeParse(raw);
|
|
120
132
|
if (!parsed.success) {
|
|
121
|
-
|
|
133
|
+
return {
|
|
134
|
+
failures: [`workflow.json does not match WorkflowSchema: ${parsed.error.message}`],
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
return {
|
|
138
|
+
workflow: parsed.data,
|
|
139
|
+
failures: authProgramFailures(parsed.data, requiredCredentialNames, session),
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function authProgramFailures(
|
|
144
|
+
workflow: Workflow,
|
|
145
|
+
requiredCredentialNames: readonly string[] = [],
|
|
146
|
+
session?: Session,
|
|
147
|
+
): string[] {
|
|
148
|
+
const failures: string[] = [];
|
|
149
|
+
if (workflow.toolKind !== 'authenticate') {
|
|
150
|
+
failures.push(`workflow.toolKind must be "authenticate"`);
|
|
122
151
|
return failures;
|
|
123
152
|
}
|
|
153
|
+
const config = workflow.authConfig;
|
|
154
|
+
if (!config) return ['workflow.authConfig is missing'];
|
|
124
155
|
|
|
125
|
-
const
|
|
156
|
+
const reachableRequestIndexes = new Set(
|
|
157
|
+
Object.values(config.actions).flatMap((action) => action.steps.map((step) => step.request)),
|
|
158
|
+
);
|
|
159
|
+
const executable = JSON.stringify({
|
|
160
|
+
bootstrap: workflow.bootstrap,
|
|
161
|
+
requests: workflow.requests.filter((_, index) => reachableRequestIndexes.has(index)),
|
|
162
|
+
});
|
|
163
|
+
for (const name of requiredCredentialNames) {
|
|
164
|
+
if (!executable.includes(`\${credential.${name}}`)) {
|
|
165
|
+
failures.push(
|
|
166
|
+
`workflow must use planned credential ${JSON.stringify(name)} as \${credential.${name}} in an executable request or bootstrap`,
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
if (!config.actions[config.entry]) {
|
|
171
|
+
failures.push(`authConfig.entry references unknown action ${JSON.stringify(config.entry)}`);
|
|
172
|
+
}
|
|
126
173
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
);
|
|
174
|
+
const actionParameter = workflow.parameters.find((parameter) => parameter.name === 'action');
|
|
175
|
+
const actionNames = Object.keys(config.actions);
|
|
176
|
+
if (!actionParameter) {
|
|
177
|
+
failures.push('workflow.parameters must declare the auth action selector');
|
|
178
|
+
} else {
|
|
179
|
+
const choices = new Set(actionParameter.choices ?? []);
|
|
180
|
+
const mismatchedChoices =
|
|
181
|
+
choices.size !== actionNames.length || actionNames.some((name) => !choices.has(name));
|
|
182
|
+
if (
|
|
183
|
+
actionParameter.type !== 'string' ||
|
|
184
|
+
actionParameter.default !== config.entry ||
|
|
185
|
+
mismatchedChoices
|
|
186
|
+
) {
|
|
187
|
+
failures.push(
|
|
188
|
+
'workflow action parameter must be a string whose default is authConfig.entry and whose choices exactly match authConfig.actions',
|
|
189
|
+
);
|
|
190
|
+
}
|
|
131
191
|
}
|
|
132
192
|
|
|
133
|
-
|
|
134
|
-
|
|
193
|
+
const parameters = new Set(workflow.parameters.map((parameter) => parameter.name));
|
|
194
|
+
const captures = new Set<string>();
|
|
195
|
+
const captureOwners = new Map<
|
|
196
|
+
string,
|
|
197
|
+
{
|
|
198
|
+
request: Workflow['requests'][number];
|
|
199
|
+
capture: NonNullable<Workflow['requests'][number]['captures']>[number];
|
|
200
|
+
}
|
|
201
|
+
>();
|
|
202
|
+
for (const request of workflow.requests) {
|
|
203
|
+
for (const capture of request.captures ?? []) {
|
|
204
|
+
if (captures.has(capture.name)) {
|
|
205
|
+
failures.push(
|
|
206
|
+
`capture name ${JSON.stringify(capture.name)} must be unique across the auth workflow`,
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
captures.add(capture.name);
|
|
210
|
+
captureOwners.set(capture.name, { request, capture });
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
for (const action of Object.values(config.actions)) {
|
|
214
|
+
for (const step of action.steps) {
|
|
215
|
+
if (step.repeat) {
|
|
216
|
+
if (captures.has(step.repeat.until.name)) {
|
|
217
|
+
failures.push(
|
|
218
|
+
`capture name ${JSON.stringify(step.repeat.until.name)} must be unique across the auth workflow`,
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
captures.add(step.repeat.until.name);
|
|
222
|
+
const request = workflow.requests[step.request];
|
|
223
|
+
if (request) {
|
|
224
|
+
captureOwners.set(step.repeat.until.name, { request, capture: step.repeat.until });
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
135
228
|
}
|
|
136
229
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
// Push completes by polling an endpoint until a recording-grounded terminal
|
|
146
|
-
// (pollTerminal) resolves; without an endpoint there's nothing to poll.
|
|
147
|
-
if (!authConfig.pollEndpoint) {
|
|
148
|
-
failures.push("authConfig.twoFactorType is 'push' but authConfig.pollEndpoint is missing");
|
|
230
|
+
let hasSuccess = false;
|
|
231
|
+
for (const [name, action] of Object.entries(config.actions)) {
|
|
232
|
+
for (const parameter of action.parameters) {
|
|
233
|
+
if (!parameters.has(parameter)) {
|
|
234
|
+
failures.push(
|
|
235
|
+
`action ${JSON.stringify(name)} references unknown parameter ${JSON.stringify(parameter)}`,
|
|
236
|
+
);
|
|
237
|
+
}
|
|
149
238
|
}
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
239
|
+
for (const [stepIndex, step] of action.steps.entries()) {
|
|
240
|
+
if (!workflow.requests[step.request]) {
|
|
241
|
+
failures.push(
|
|
242
|
+
`action ${JSON.stringify(name)} step ${stepIndex} references missing request ${step.request}`,
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
if (step.onError === 'retry' && !step.repeat) {
|
|
246
|
+
failures.push(
|
|
247
|
+
`action ${JSON.stringify(name)} step ${stepIndex} uses onError="retry" without repeat bounds`,
|
|
248
|
+
);
|
|
249
|
+
}
|
|
154
250
|
}
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
// slice(0, initiateCount) — NOT `initiateCount || undefined`: when the count is
|
|
162
|
-
// 0/unset there are NO initiate-phase requests, so `slice(0, 0)` must yield an
|
|
163
|
-
// empty set. `|| undefined` would make this `slice(0, undefined)` = ALL requests,
|
|
164
|
-
// counting completion-phase captures as "covered" and defeating the check.
|
|
165
|
-
for (const req of workflow.requests.slice(0, initiateCount)) {
|
|
166
|
-
for (const cap of req.captures ?? []) initiateCaptureNames.add(cap.name);
|
|
251
|
+
for (const evidence of action.outcome.evidence) {
|
|
252
|
+
if (!captures.has(evidence)) {
|
|
253
|
+
failures.push(
|
|
254
|
+
`action ${JSON.stringify(name)} references unknown evidence capture ${JSON.stringify(evidence)}`,
|
|
255
|
+
);
|
|
256
|
+
}
|
|
167
257
|
}
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
258
|
+
if (action.outcome.type === 'pause') {
|
|
259
|
+
if (!config.actions[action.outcome.next]) {
|
|
260
|
+
failures.push(
|
|
261
|
+
`action ${JSON.stringify(name)} pauses to unknown action ${JSON.stringify(action.outcome.next)}`,
|
|
262
|
+
);
|
|
173
263
|
}
|
|
264
|
+
for (const carried of action.outcome.carry) {
|
|
265
|
+
if (!captures.has(carried)) {
|
|
266
|
+
failures.push(
|
|
267
|
+
`action ${JSON.stringify(name)} carries unknown capture ${JSON.stringify(carried)}`,
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
} else {
|
|
272
|
+
hasSuccess = true;
|
|
174
273
|
}
|
|
175
|
-
|
|
176
|
-
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (!hasSuccess) failures.push('authConfig.actions has no success outcome');
|
|
277
|
+
for (const name of config.persist) {
|
|
278
|
+
if (!captures.has(name)) {
|
|
279
|
+
failures.push(`authConfig.persist references unknown capture ${JSON.stringify(name)}`);
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
const owner = captureOwners.get(name);
|
|
283
|
+
if (!owner) continue;
|
|
284
|
+
const { request, capture } = owner;
|
|
285
|
+
if (request.recordingRequestSeq === undefined) {
|
|
177
286
|
failures.push(
|
|
178
|
-
`
|
|
287
|
+
`persisted capture ${JSON.stringify(name)} must declare recordingRequestSeq on its producing request`,
|
|
288
|
+
);
|
|
289
|
+
} else if (session) {
|
|
290
|
+
const recorded = session.requests.find(
|
|
291
|
+
(candidate) => candidate.seq === request.recordingRequestSeq,
|
|
179
292
|
);
|
|
293
|
+
if (!recorded) {
|
|
294
|
+
failures.push(
|
|
295
|
+
`persisted capture ${JSON.stringify(name)} references missing recorded request seq ${request.recordingRequestSeq}`,
|
|
296
|
+
);
|
|
297
|
+
} else if (!recordedRequestMatchesWorkflow(recorded, request)) {
|
|
298
|
+
failures.push(
|
|
299
|
+
`persisted capture ${JSON.stringify(name)} recordingRequestSeq ${request.recordingRequestSeq} does not match its workflow request`,
|
|
300
|
+
);
|
|
301
|
+
} else {
|
|
302
|
+
if (!recordedResponseProducesCapture(recorded, capture)) {
|
|
303
|
+
failures.push(
|
|
304
|
+
`persisted capture ${JSON.stringify(name)} is not produced by recorded request seq ${request.recordingRequestSeq}`,
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
180
308
|
}
|
|
181
309
|
}
|
|
310
|
+
if (!reachableSuccess(config.entry, config.actions)) {
|
|
311
|
+
failures.push('authConfig.entry cannot reach a success outcome');
|
|
312
|
+
}
|
|
313
|
+
return failures;
|
|
314
|
+
}
|
|
182
315
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
);
|
|
316
|
+
function recordedResponseProducesCapture(
|
|
317
|
+
request: Session['requests'][number],
|
|
318
|
+
capture: NonNullable<Workflow['requests'][number]['captures']>[number],
|
|
319
|
+
): boolean {
|
|
320
|
+
const response = request.response;
|
|
321
|
+
if (!response || capture.source === 'cookie') return false;
|
|
322
|
+
const body = response.body ?? '';
|
|
323
|
+
let value: unknown;
|
|
324
|
+
if (capture.source === 'json') {
|
|
325
|
+
try {
|
|
326
|
+
value = jsonpath(JSON.parse(body), capture.path);
|
|
327
|
+
} catch {
|
|
328
|
+
return false;
|
|
329
|
+
}
|
|
330
|
+
} else if (capture.source === 'response_header') {
|
|
331
|
+
value =
|
|
332
|
+
capture.header.toLowerCase() === 'x-imprint-final-url'
|
|
333
|
+
? request.url
|
|
334
|
+
: captureHeader(new Headers(response.headers), capture.header, capture.mode);
|
|
335
|
+
} else {
|
|
336
|
+
try {
|
|
337
|
+
value = new RegExp(capture.pattern).exec(body)?.[capture.group ?? 1];
|
|
338
|
+
} catch {
|
|
339
|
+
return false;
|
|
205
340
|
}
|
|
206
341
|
}
|
|
342
|
+
return captureValueMatches(value, capture.equals);
|
|
343
|
+
}
|
|
207
344
|
|
|
208
|
-
|
|
345
|
+
function reachableSuccess(
|
|
346
|
+
entry: string,
|
|
347
|
+
actions: NonNullable<Workflow['authConfig']>['actions'],
|
|
348
|
+
): boolean {
|
|
349
|
+
const seen = new Set<string>();
|
|
350
|
+
let current: string | undefined = entry;
|
|
351
|
+
while (current && !seen.has(current)) {
|
|
352
|
+
seen.add(current);
|
|
353
|
+
const action: (typeof actions)[string] | undefined = actions[current];
|
|
354
|
+
if (!action) return false;
|
|
355
|
+
if (action.outcome.type === 'success') return true;
|
|
356
|
+
current = action.outcome.next;
|
|
357
|
+
}
|
|
358
|
+
return false;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
export function authWorkflowHash(workflow: Workflow): string {
|
|
362
|
+
return createHash('sha256').update(JSON.stringify(workflow)).digest('hex');
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function liveAttemptFailures(toolDir: string, workflow: Workflow): string[] {
|
|
366
|
+
const path = pathJoin(toolDir, AUTH_VERIFICATION_ATTEMPT_SENTINEL);
|
|
367
|
+
if (!existsSync(path)) {
|
|
368
|
+
return ['no live auth verification attempt was recorded; call run_verification before done'];
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
let attempt: { action?: unknown; ok?: unknown; workflowHash?: unknown };
|
|
372
|
+
try {
|
|
373
|
+
attempt = JSON.parse(readFileSync(path, 'utf8')) as typeof attempt;
|
|
374
|
+
} catch (err) {
|
|
375
|
+
return [
|
|
376
|
+
`live auth verification record is invalid: ${err instanceof Error ? err.message : String(err)}`,
|
|
377
|
+
];
|
|
378
|
+
}
|
|
379
|
+
if (attempt.ok !== true || typeof attempt.action !== 'string') {
|
|
380
|
+
return ['the most recent live auth verification did not succeed'];
|
|
381
|
+
}
|
|
382
|
+
if (attempt.workflowHash !== authWorkflowHash(workflow)) {
|
|
383
|
+
return ['workflow.json changed after the most recent successful live auth verification'];
|
|
384
|
+
}
|
|
385
|
+
const action = workflow.authConfig?.actions[attempt.action];
|
|
386
|
+
return action?.outcome.type === 'success'
|
|
387
|
+
? []
|
|
388
|
+
: [
|
|
389
|
+
`the most recent successful live action ${JSON.stringify(attempt.action)} does not declare a success outcome`,
|
|
390
|
+
];
|
|
209
391
|
}
|