autokap 1.8.6 → 1.8.8
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/dist/action-verifier.d.ts +6 -0
- package/dist/action-verifier.js +30 -17
- package/dist/browser.d.ts +59 -0
- package/dist/browser.js +259 -0
- package/dist/cli-config.js +7 -12
- package/dist/cli-contract.d.ts +5 -9
- package/dist/cli-contract.js +11 -38
- package/dist/cli-runner.d.ts +0 -1
- package/dist/cli-runner.js +74 -59
- package/dist/cli.js +7 -7
- package/dist/clip-capture-loop.d.ts +28 -7
- package/dist/clip-capture-loop.js +102 -19
- package/dist/engine-version.d.ts +24 -0
- package/dist/engine-version.js +25 -0
- package/dist/execution-schema.d.ts +22 -0
- package/dist/execution-schema.js +59 -8
- package/dist/execution-types.d.ts +116 -0
- package/dist/opcode-runner.d.ts +8 -1
- package/dist/opcode-runner.js +120 -29
- package/dist/postcondition.d.ts +18 -3
- package/dist/postcondition.js +75 -27
- package/dist/program-hash.d.ts +11 -0
- package/dist/program-hash.js +28 -0
- package/dist/program-migrations.d.ts +31 -0
- package/dist/program-migrations.js +93 -0
- package/dist/program-signing.d.ts +11 -0
- package/dist/program-signing.js +1 -0
- package/dist/recovery-chain.js +8 -11
- package/dist/scenario-cookie.d.ts +36 -0
- package/dist/scenario-cookie.js +62 -0
- package/dist/security.d.ts +21 -0
- package/dist/security.js +46 -8
- package/dist/server-credit-usage.d.ts +1 -1
- package/dist/version.d.ts +1 -0
- package/dist/version.js +1 -0
- package/dist/video-narration-schema.d.ts +3 -0
- package/dist/video-narration-schema.js +3 -0
- package/dist/wait-contract.d.ts +104 -0
- package/dist/wait-contract.js +144 -0
- package/dist/web-playwright-local.d.ts +9 -1
- package/dist/web-playwright-local.js +0 -0
- package/package.json +2 -2
- package/readme.md +9 -15
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Capture Agent — Program FORM migrations (migrate-on-read)
|
|
3
|
+
*
|
|
4
|
+
* Old presets are stored at whatever `programSchemaVersion` (FORM) was current
|
|
5
|
+
* when they were authored. `upgradeProgram` runs a chain of pure
|
|
6
|
+
* `migrate_vN→vN+1` functions to bring any stored program up to the current
|
|
7
|
+
* form BEFORE strict schema validation, so the runner only ever sees one shape.
|
|
8
|
+
*
|
|
9
|
+
* Properties of this layer (decisions locked in AUT-242):
|
|
10
|
+
* - Compat forever: the chain is kept indefinitely; no support window.
|
|
11
|
+
* - Migrate-on-read only: programs are NEVER rewritten back to storage. The
|
|
12
|
+
* stored form changes only when the generator recompiles (create/modify).
|
|
13
|
+
* - Pure + idempotent: a program already at the current form is a no-op.
|
|
14
|
+
*
|
|
15
|
+
* This module is intentionally free of Node-only imports so it can be pulled
|
|
16
|
+
* into the schema validation chain on any runtime. Content hashing
|
|
17
|
+
* (`node:crypto`) lives in program-hash.ts.
|
|
18
|
+
*/
|
|
19
|
+
import { CURRENT_PROGRAM_SCHEMA_VERSION } from './engine-version.js';
|
|
20
|
+
/** Canonical video capture/delivery resolution (1920×1080 @1×). */
|
|
21
|
+
const VIDEO_RESOLUTION = { width: 1920, height: 1080 };
|
|
22
|
+
function isRecord(value) {
|
|
23
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Reads the FORM version a raw (pre-migration) program was stored at.
|
|
27
|
+
* Absent / non-finite ⇒ 0 (the oldest form). Used to stamp run provenance
|
|
28
|
+
* (`program_schema_version_origin`) before `upgradeProgram` bumps it.
|
|
29
|
+
*/
|
|
30
|
+
export function readOriginSchemaVersion(raw) {
|
|
31
|
+
if (isRecord(raw) &&
|
|
32
|
+
typeof raw.programSchemaVersion === 'number' &&
|
|
33
|
+
Number.isFinite(raw.programSchemaVersion)) {
|
|
34
|
+
return raw.programSchemaVersion;
|
|
35
|
+
}
|
|
36
|
+
return 0;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* v0 → v1: fold the legacy video capture-resolution normalization (formerly the
|
|
40
|
+
* runtime `normalizeVideoCaptureProgram` in cli-runner.ts) into the program
|
|
41
|
+
* FORM. Legacy video presets carrying viewport=2560×1440 / DPR=1.3333 /
|
|
42
|
+
* captureResolution=2560×1440 are normalized to 1920×1080 @1×. Non-video
|
|
43
|
+
* programs pass through unchanged. Defensive on malformed shapes — anything it
|
|
44
|
+
* can't normalize is left for the strict schema to reject with a clean error.
|
|
45
|
+
*/
|
|
46
|
+
function migrate_0_to_1(prog) {
|
|
47
|
+
if (prog.mediaMode !== 'video')
|
|
48
|
+
return prog;
|
|
49
|
+
const artifactPlan = isRecord(prog.artifactPlan) ? prog.artifactPlan : undefined;
|
|
50
|
+
const format = artifactPlan && isRecord(artifactPlan.format) ? artifactPlan.format : {};
|
|
51
|
+
const variants = Array.isArray(prog.variants)
|
|
52
|
+
? prog.variants.map((v) => isRecord(v) ? { ...v, viewport: { ...VIDEO_RESOLUTION }, deviceScaleFactor: 1 } : v)
|
|
53
|
+
: prog.variants;
|
|
54
|
+
return {
|
|
55
|
+
...prog,
|
|
56
|
+
outputScale: 1,
|
|
57
|
+
variants,
|
|
58
|
+
artifactPlan: {
|
|
59
|
+
...(artifactPlan ?? {}),
|
|
60
|
+
format: {
|
|
61
|
+
...format,
|
|
62
|
+
captureResolution: { ...VIDEO_RESOLUTION },
|
|
63
|
+
deliveryResolution: { ...VIDEO_RESOLUTION },
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
/** Ordered FORM migrations. `MIGRATIONS[n]` upgrades a vN program to vN+1. */
|
|
69
|
+
const MIGRATIONS = {
|
|
70
|
+
0: migrate_0_to_1,
|
|
71
|
+
};
|
|
72
|
+
/**
|
|
73
|
+
* Brings any stored program up to {@link CURRENT_PROGRAM_SCHEMA_VERSION} (form)
|
|
74
|
+
* before strict validation. Pure: clones, never mutates `raw`. Idempotent: a
|
|
75
|
+
* program already at the current form is returned with only its version stamped.
|
|
76
|
+
* Non-object input is returned untouched so the schema raises a clean error.
|
|
77
|
+
*/
|
|
78
|
+
export function upgradeProgram(raw) {
|
|
79
|
+
if (!isRecord(raw))
|
|
80
|
+
return raw;
|
|
81
|
+
let prog = { ...raw };
|
|
82
|
+
let v = readOriginSchemaVersion(raw);
|
|
83
|
+
while (v < CURRENT_PROGRAM_SCHEMA_VERSION) {
|
|
84
|
+
const migrate = MIGRATIONS[v];
|
|
85
|
+
if (!migrate)
|
|
86
|
+
break; // no path forward; let the schema reject a wrong shape
|
|
87
|
+
prog = migrate(prog);
|
|
88
|
+
v += 1;
|
|
89
|
+
}
|
|
90
|
+
prog.programSchemaVersion = CURRENT_PROGRAM_SCHEMA_VERSION;
|
|
91
|
+
return prog;
|
|
92
|
+
}
|
|
93
|
+
//# sourceMappingURL=program-migrations.js.map
|
|
@@ -13,6 +13,13 @@ export interface SignedExecutionProgramEnvelope {
|
|
|
13
13
|
signature: string;
|
|
14
14
|
meta?: {
|
|
15
15
|
stale?: boolean;
|
|
16
|
+
/**
|
|
17
|
+
* FORM version the stored program was at BEFORE the server migrated it
|
|
18
|
+
* (0 = legacy / no version field). Debug-only run provenance; lives in the
|
|
19
|
+
* UNSIGNED meta because it carries no security weight — the server reads it
|
|
20
|
+
* from the raw preset config before `extractExecutionProgram` normalizes.
|
|
21
|
+
*/
|
|
22
|
+
programSchemaVersionOrigin?: number;
|
|
16
23
|
};
|
|
17
24
|
}
|
|
18
25
|
export declare const ProgramSecurityMetadataSchema: z.ZodObject<{
|
|
@@ -31,6 +38,8 @@ export declare const SignedExecutionProgramEnvelopeSchema: z.ZodObject<{
|
|
|
31
38
|
program: z.ZodObject<{
|
|
32
39
|
presetId: z.ZodString;
|
|
33
40
|
programVersion: z.ZodNumber;
|
|
41
|
+
programSchemaVersion: z.ZodOptional<z.ZodNumber>;
|
|
42
|
+
engineVersion: z.ZodOptional<z.ZodNumber>;
|
|
34
43
|
mediaMode: z.ZodEnum<{
|
|
35
44
|
video: "video";
|
|
36
45
|
clip: "clip";
|
|
@@ -141,6 +150,7 @@ export declare const SignedExecutionProgramEnvelopeSchema: z.ZodObject<{
|
|
|
141
150
|
domain: z.ZodString;
|
|
142
151
|
path: z.ZodOptional<z.ZodString>;
|
|
143
152
|
}, z.core.$strict>>>;
|
|
153
|
+
scenario: z.ZodOptional<z.ZodString>;
|
|
144
154
|
}, z.core.$strict>;
|
|
145
155
|
steps: z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
146
156
|
url: z.ZodString;
|
|
@@ -1133,6 +1143,7 @@ export declare const SignedExecutionProgramEnvelopeSchema: z.ZodObject<{
|
|
|
1133
1143
|
signature: z.ZodString;
|
|
1134
1144
|
meta: z.ZodOptional<z.ZodObject<{
|
|
1135
1145
|
stale: z.ZodOptional<z.ZodBoolean>;
|
|
1146
|
+
programSchemaVersionOrigin: z.ZodOptional<z.ZodNumber>;
|
|
1136
1147
|
}, z.core.$strict>>;
|
|
1137
1148
|
}, z.core.$strict>;
|
|
1138
1149
|
export declare function normalizeAllowedOrigins(origins: Iterable<string>): string[];
|
package/dist/program-signing.js
CHANGED
|
@@ -30,6 +30,7 @@ export const SignedExecutionProgramEnvelopeSchema = z.object({
|
|
|
30
30
|
signature: z.string().regex(/^[a-f0-9]{64}$/i),
|
|
31
31
|
meta: z.object({
|
|
32
32
|
stale: z.boolean().optional(),
|
|
33
|
+
programSchemaVersionOrigin: z.number().int().nonnegative().optional(),
|
|
33
34
|
}).strict().optional(),
|
|
34
35
|
}).strict();
|
|
35
36
|
function stableNormalize(value) {
|
package/dist/recovery-chain.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* 5. LLM Healer (last resort)
|
|
10
10
|
*/
|
|
11
11
|
import { resolveSelector } from './selector-resolver.js';
|
|
12
|
-
import { evaluatePostcondition } from './postcondition.js';
|
|
12
|
+
import { evaluatePostcondition, evaluatePostconditionWithProgress } from './postcondition.js';
|
|
13
13
|
import { LLMHealer } from './llm-healer.js';
|
|
14
14
|
import { serializeAKTree } from './ak-tree.js';
|
|
15
15
|
import { executeOpcodeCoreAction } from './opcode-actions.js';
|
|
@@ -35,7 +35,7 @@ export class RecoveryChainImpl {
|
|
|
35
35
|
const retryBudget = Math.max(0, Math.min(recovery.retries, options.maxDeterministicRetries ?? recovery.retries));
|
|
36
36
|
if (retryBudget > 0) {
|
|
37
37
|
logger.debug(`[recovery ${opcodeIndex}] strategy 1 (retry x${retryBudget})`);
|
|
38
|
-
const result = await retryOpcode(failedOpcode, adapter, retryBudget, options.remainingTimeMs, options.currentVariant, this.credentials, options.suppressPageReloads);
|
|
38
|
+
const result = await retryOpcode(failedOpcode, adapter, retryBudget, options.remainingTimeMs, options.currentVariant, this.credentials, options.suppressPageReloads, options.globalDeadlineMs, options.getProgress);
|
|
39
39
|
logger.debug(`[recovery ${opcodeIndex}] strategy 1 → recovered=${result.recovered}, reason=${result.reason}`);
|
|
40
40
|
if (result.recovered)
|
|
41
41
|
return result;
|
|
@@ -86,7 +86,7 @@ export class RecoveryChainImpl {
|
|
|
86
86
|
}
|
|
87
87
|
}
|
|
88
88
|
// ── Strategy 1: Deterministic retry ─────────────────────────────────
|
|
89
|
-
async function retryOpcode(opcode, adapter, maxRetries, remainingTimeMs, currentVariant, credentials, suppressPageReloads = false) {
|
|
89
|
+
async function retryOpcode(opcode, adapter, maxRetries, remainingTimeMs, currentVariant, credentials, suppressPageReloads = false, globalDeadlineMs, getProgress) {
|
|
90
90
|
const deadlineMs = typeof remainingTimeMs === 'number'
|
|
91
91
|
? Date.now() + remainingTimeMs
|
|
92
92
|
: Number.POSITIVE_INFINITY;
|
|
@@ -97,7 +97,11 @@ async function retryOpcode(opcode, adapter, maxRetries, remainingTimeMs, current
|
|
|
97
97
|
await sleep(500 * (i + 1)); // backoff
|
|
98
98
|
try {
|
|
99
99
|
await executeRawAction(opcode, adapter, currentVariant, credentials, suppressPageReloads);
|
|
100
|
-
|
|
100
|
+
// AUT-240 (Phase 5): the re-check extends-on-progress up to the global
|
|
101
|
+
// deadline instead of replaying a fixed clamped budget. Without the global
|
|
102
|
+
// deadline (legacy callers), `getProgress` is dropped so it degrades to the
|
|
103
|
+
// compiled postcondition budget.
|
|
104
|
+
const postcondition = await evaluatePostconditionWithProgress(adapter, opcode.postcondition, Date.now(), globalDeadlineMs ?? deadlineMs, globalDeadlineMs !== undefined ? getProgress : undefined);
|
|
101
105
|
if (postcondition.passed) {
|
|
102
106
|
return { recovered: true, strategy: 'retry', reason: `retry ${i + 1}/${maxRetries} succeeded` };
|
|
103
107
|
}
|
|
@@ -363,11 +367,4 @@ async function resolveSelectorCoordinates(adapter, selector) {
|
|
|
363
367
|
function sleep(ms) {
|
|
364
368
|
return new Promise(resolve => setTimeout(resolve, ms));
|
|
365
369
|
}
|
|
366
|
-
function clampPostconditionWait(spec, deadlineMs) {
|
|
367
|
-
const remainingTimeMs = Math.max(1, deadlineMs - Date.now());
|
|
368
|
-
return {
|
|
369
|
-
...spec,
|
|
370
|
-
waitMs: Math.max(1, Math.min(spec.waitMs ?? remainingTimeMs, remainingTimeMs)),
|
|
371
|
-
};
|
|
372
|
-
}
|
|
373
370
|
//# sourceMappingURL=recovery-chain.js.map
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { PreconditionSpec } from './execution-types.js';
|
|
2
|
+
/**
|
|
3
|
+
* AUT-239 — signing side of the AutoKap Scenario cookie.
|
|
4
|
+
*
|
|
5
|
+
* The runner (this CLI) signs; the client app's `@autokap/scenario` package
|
|
6
|
+
* verifies. At runtime these live in two separate processes/deployments, so the
|
|
7
|
+
* only thing they share is the WIRE FORMAT, not a module. We therefore keep a
|
|
8
|
+
* tiny local signer here rather than taking a runtime dependency on the
|
|
9
|
+
* published `@autokap/scenario` package. `scenario-cookie.test.ts` locks the
|
|
10
|
+
* format against the real resolver so the two can never drift.
|
|
11
|
+
*
|
|
12
|
+
* Format: `<id>.<base64url(HMAC-SHA256(secret, id))>`.
|
|
13
|
+
*/
|
|
14
|
+
export declare const SCENARIO_COOKIE_NAME = "__ak_scenario";
|
|
15
|
+
export declare function signScenarioCookie(id: string, secret: string): string;
|
|
16
|
+
/** A cookie ready for Playwright's `context.addCookies`. */
|
|
17
|
+
export interface PreconditionCookie {
|
|
18
|
+
name: string;
|
|
19
|
+
value: string;
|
|
20
|
+
domain: string;
|
|
21
|
+
path?: string;
|
|
22
|
+
secure: boolean;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Build the cookie set injected before navigation: the preset's seed cookies
|
|
26
|
+
* plus, when configured, the signed scenario switch.
|
|
27
|
+
*
|
|
28
|
+
* `secure` is derived from the target protocol — a Secure cookie is never sent
|
|
29
|
+
* over http://localhost, so the runner must NOT default it to true (that would
|
|
30
|
+
* silently drop seed/scenario cookies in local captures and fall back to real
|
|
31
|
+
* data). Returns a `warning` when a scenario is requested but unsignable.
|
|
32
|
+
*/
|
|
33
|
+
export declare function buildPreconditionCookies(preconditions: Pick<PreconditionSpec, 'cookies' | 'scenario'>, baseUrl: string, secret: string | undefined): {
|
|
34
|
+
cookies: PreconditionCookie[];
|
|
35
|
+
warning?: string;
|
|
36
|
+
};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { createHmac } from 'node:crypto';
|
|
2
|
+
/**
|
|
3
|
+
* AUT-239 — signing side of the AutoKap Scenario cookie.
|
|
4
|
+
*
|
|
5
|
+
* The runner (this CLI) signs; the client app's `@autokap/scenario` package
|
|
6
|
+
* verifies. At runtime these live in two separate processes/deployments, so the
|
|
7
|
+
* only thing they share is the WIRE FORMAT, not a module. We therefore keep a
|
|
8
|
+
* tiny local signer here rather than taking a runtime dependency on the
|
|
9
|
+
* published `@autokap/scenario` package. `scenario-cookie.test.ts` locks the
|
|
10
|
+
* format against the real resolver so the two can never drift.
|
|
11
|
+
*
|
|
12
|
+
* Format: `<id>.<base64url(HMAC-SHA256(secret, id))>`.
|
|
13
|
+
*/
|
|
14
|
+
export const SCENARIO_COOKIE_NAME = '__ak_scenario';
|
|
15
|
+
export function signScenarioCookie(id, secret) {
|
|
16
|
+
const sig = createHmac('sha256', secret).update(id).digest('base64url');
|
|
17
|
+
return `${id}.${sig}`;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Build the cookie set injected before navigation: the preset's seed cookies
|
|
21
|
+
* plus, when configured, the signed scenario switch.
|
|
22
|
+
*
|
|
23
|
+
* `secure` is derived from the target protocol — a Secure cookie is never sent
|
|
24
|
+
* over http://localhost, so the runner must NOT default it to true (that would
|
|
25
|
+
* silently drop seed/scenario cookies in local captures and fall back to real
|
|
26
|
+
* data). Returns a `warning` when a scenario is requested but unsignable.
|
|
27
|
+
*/
|
|
28
|
+
export function buildPreconditionCookies(preconditions, baseUrl, secret) {
|
|
29
|
+
let secure = true;
|
|
30
|
+
let hostname = '';
|
|
31
|
+
try {
|
|
32
|
+
const url = new URL(baseUrl);
|
|
33
|
+
secure = url.protocol === 'https:';
|
|
34
|
+
hostname = url.hostname;
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
/* baseUrl is validated upstream; fall back to Secure on parse failure */
|
|
38
|
+
}
|
|
39
|
+
const cookies = (preconditions.cookies ?? []).map((c) => ({
|
|
40
|
+
...c,
|
|
41
|
+
secure,
|
|
42
|
+
}));
|
|
43
|
+
let warning;
|
|
44
|
+
if (preconditions.scenario) {
|
|
45
|
+
if (!secret) {
|
|
46
|
+
warning =
|
|
47
|
+
`preconditions.scenario="${preconditions.scenario}" set but AUTOKAP_SCENARIO_SECRET ` +
|
|
48
|
+
`is missing — scenario cookie NOT injected; capture will use real data`;
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
cookies.push({
|
|
52
|
+
name: SCENARIO_COOKIE_NAME,
|
|
53
|
+
value: signScenarioCookie(preconditions.scenario, secret),
|
|
54
|
+
domain: hostname,
|
|
55
|
+
path: '/',
|
|
56
|
+
secure,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return { cookies, warning };
|
|
61
|
+
}
|
|
62
|
+
//# sourceMappingURL=scenario-cookie.js.map
|
package/dist/security.d.ts
CHANGED
|
@@ -16,6 +16,27 @@ export interface SecurityDecision {
|
|
|
16
16
|
reason?: string;
|
|
17
17
|
target?: InteractiveElement | null;
|
|
18
18
|
}
|
|
19
|
+
/**
|
|
20
|
+
* Is `candidateUrl` part of the same first-party site as `scopeUrl`? Shares the
|
|
21
|
+
* navigation site-scope model (`isWithinProjectScope`): exact-host for IPs /
|
|
22
|
+
* localhost / shared-hosting suffixes (so sibling `*.vercel.app` previews are
|
|
23
|
+
* NOT same-site), sub-domain family match for real registrable domains.
|
|
24
|
+
*
|
|
25
|
+
* Used to scope the adaptive-wait progress signal (AUT-240) to the app's own
|
|
26
|
+
* traffic, so third-party telemetry (PostHog beacons, analytics/ad pixels,
|
|
27
|
+
* Sentry, …) no longer reads as "the page is making progress" and the stuck
|
|
28
|
+
* watchdog can still cut a wait whose condition will never be met.
|
|
29
|
+
*
|
|
30
|
+
* Fail-OPEN by design: a false "first-party" only makes the watchdog slightly
|
|
31
|
+
* more patient (still bounded by the per-media cap), whereas a false "foreign"
|
|
32
|
+
* could suppress a real progress signal and cut a legitimately-slow page early.
|
|
33
|
+
* So when in doubt we count it as first-party:
|
|
34
|
+
* - unparseable / non-http(s) `scopeUrl` (e.g. `about:blank` before the first
|
|
35
|
+
* navigation commits) ⇒ true (don't filter);
|
|
36
|
+
* - non-http(s) `candidateUrl` (`data:` / `blob:` / `about:`) ⇒ true (in-page
|
|
37
|
+
* resource).
|
|
38
|
+
*/
|
|
39
|
+
export declare function isFirstPartyUrl(scopeUrl: string | null | undefined, candidateUrl: string): boolean;
|
|
19
40
|
export declare function evaluateResolvedActionSecurity(action: ActionType, args: Record<string, unknown>, context: SecurityContext, target: InteractiveElement | null): SecurityDecision;
|
|
20
41
|
export declare function evaluateActionSecurity(action: ActionType, args: Record<string, unknown>, context: SecurityContext): SecurityDecision;
|
|
21
42
|
export declare function describeSecurityTarget(target: InteractiveElement | null | undefined): string;
|
package/dist/security.js
CHANGED
|
@@ -109,6 +109,16 @@ function isSharedHostingHostname(hostname) {
|
|
|
109
109
|
function normalizeScopeHostname(hostname) {
|
|
110
110
|
return hostname.startsWith('www.') ? hostname.slice(4) : hostname;
|
|
111
111
|
}
|
|
112
|
+
function scopeFromParsedUrl(parsed) {
|
|
113
|
+
const hostname = parsed.hostname.toLowerCase();
|
|
114
|
+
const exactHost = isLocalHostname(hostname) || isIpv4Hostname(hostname) || isIpv6Hostname(hostname) || isSharedHostingHostname(hostname);
|
|
115
|
+
const scopeHostname = exactHost ? hostname : normalizeScopeHostname(hostname);
|
|
116
|
+
return {
|
|
117
|
+
hostname: scopeHostname,
|
|
118
|
+
port: effectivePort(parsed),
|
|
119
|
+
exactHost,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
112
122
|
function buildNavigationScopes(context) {
|
|
113
123
|
const sources = [
|
|
114
124
|
context.rootUrl,
|
|
@@ -119,18 +129,46 @@ function buildNavigationScopes(context) {
|
|
|
119
129
|
const parsed = parseHttpUrl(value);
|
|
120
130
|
if (!parsed)
|
|
121
131
|
continue;
|
|
122
|
-
const
|
|
123
|
-
const exactHost = isLocalHostname(hostname) || isIpv4Hostname(hostname) || isIpv6Hostname(hostname) || isSharedHostingHostname(hostname);
|
|
124
|
-
const scopeHostname = exactHost ? hostname : normalizeScopeHostname(hostname);
|
|
125
|
-
const scope = {
|
|
126
|
-
hostname: scopeHostname,
|
|
127
|
-
port: effectivePort(parsed),
|
|
128
|
-
exactHost,
|
|
129
|
-
};
|
|
132
|
+
const scope = scopeFromParsedUrl(parsed);
|
|
130
133
|
dedup.set(`${scope.hostname}:${scope.port}:${scope.exactHost ? 'exact' : 'family'}`, scope);
|
|
131
134
|
}
|
|
132
135
|
return Array.from(dedup.values());
|
|
133
136
|
}
|
|
137
|
+
/**
|
|
138
|
+
* Is `candidateUrl` part of the same first-party site as `scopeUrl`? Shares the
|
|
139
|
+
* navigation site-scope model (`isWithinProjectScope`): exact-host for IPs /
|
|
140
|
+
* localhost / shared-hosting suffixes (so sibling `*.vercel.app` previews are
|
|
141
|
+
* NOT same-site), sub-domain family match for real registrable domains.
|
|
142
|
+
*
|
|
143
|
+
* Used to scope the adaptive-wait progress signal (AUT-240) to the app's own
|
|
144
|
+
* traffic, so third-party telemetry (PostHog beacons, analytics/ad pixels,
|
|
145
|
+
* Sentry, …) no longer reads as "the page is making progress" and the stuck
|
|
146
|
+
* watchdog can still cut a wait whose condition will never be met.
|
|
147
|
+
*
|
|
148
|
+
* Fail-OPEN by design: a false "first-party" only makes the watchdog slightly
|
|
149
|
+
* more patient (still bounded by the per-media cap), whereas a false "foreign"
|
|
150
|
+
* could suppress a real progress signal and cut a legitimately-slow page early.
|
|
151
|
+
* So when in doubt we count it as first-party:
|
|
152
|
+
* - unparseable / non-http(s) `scopeUrl` (e.g. `about:blank` before the first
|
|
153
|
+
* navigation commits) ⇒ true (don't filter);
|
|
154
|
+
* - non-http(s) `candidateUrl` (`data:` / `blob:` / `about:`) ⇒ true (in-page
|
|
155
|
+
* resource).
|
|
156
|
+
*/
|
|
157
|
+
export function isFirstPartyUrl(scopeUrl, candidateUrl) {
|
|
158
|
+
const scopeParsed = parseHttpUrl(scopeUrl ?? undefined);
|
|
159
|
+
if (!scopeParsed)
|
|
160
|
+
return true;
|
|
161
|
+
let candidate;
|
|
162
|
+
try {
|
|
163
|
+
candidate = new URL(candidateUrl);
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
return true;
|
|
167
|
+
}
|
|
168
|
+
if (candidate.protocol !== 'http:' && candidate.protocol !== 'https:')
|
|
169
|
+
return true;
|
|
170
|
+
return isWithinProjectScope(candidate, [scopeFromParsedUrl(scopeParsed)]);
|
|
171
|
+
}
|
|
134
172
|
function isWithinProjectScope(candidate, scopes) {
|
|
135
173
|
const hostname = candidate.hostname.toLowerCase();
|
|
136
174
|
const port = effectivePort(candidate);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { SupabaseClient } from '@supabase/supabase-js';
|
|
2
|
-
export type CreditUsageType = 'screenshot' | 'clip' | 'video' | 'cloud_recapture' | 'ai_chat' | 'studio_creation' | 'studio_iteration' | 'error_analysis' | 'obsolescence_analysis';
|
|
2
|
+
export type CreditUsageType = 'screenshot' | 'clip' | 'video' | 'cloud_recapture' | 'ai_chat' | 'studio_creation' | 'studio_iteration' | 'error_analysis' | 'obsolescence_analysis' | 'pr_generation';
|
|
3
3
|
export declare function recordCreditUsage(supabase: SupabaseClient, params: {
|
|
4
4
|
userId: string;
|
|
5
5
|
projectId: string | null;
|
package/dist/version.d.ts
CHANGED
package/dist/version.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createRequire } from 'node:module';
|
|
2
2
|
const require = createRequire(import.meta.url);
|
|
3
3
|
const pkg = require('../package.json');
|
|
4
|
+
/** npm package version of the CLI, transmitted as the `x-autokap-cli-version` header. */
|
|
4
5
|
export const APP_VERSION = pkg.version;
|
|
5
6
|
//# sourceMappingURL=version.js.map
|
|
@@ -57,6 +57,8 @@ export declare const VideoIngestPayloadSchema: z.ZodObject<{
|
|
|
57
57
|
program: z.ZodObject<{
|
|
58
58
|
presetId: z.ZodString;
|
|
59
59
|
programVersion: z.ZodNumber;
|
|
60
|
+
programSchemaVersion: z.ZodOptional<z.ZodNumber>;
|
|
61
|
+
engineVersion: z.ZodOptional<z.ZodNumber>;
|
|
60
62
|
mediaMode: z.ZodEnum<{
|
|
61
63
|
video: "video";
|
|
62
64
|
clip: "clip";
|
|
@@ -167,6 +169,7 @@ export declare const VideoIngestPayloadSchema: z.ZodObject<{
|
|
|
167
169
|
domain: z.ZodString;
|
|
168
170
|
path: z.ZodOptional<z.ZodString>;
|
|
169
171
|
}, z.core.$strict>>>;
|
|
172
|
+
scenario: z.ZodOptional<z.ZodString>;
|
|
170
173
|
}, z.core.$strict>;
|
|
171
174
|
steps: z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
172
175
|
url: z.ZodString;
|
|
@@ -83,6 +83,9 @@ export const VideoNarrationOverlaySchema = z.object({
|
|
|
83
83
|
* match a `program.steps[*].stepId`.
|
|
84
84
|
*/
|
|
85
85
|
export const VideoIngestPayloadSchema = z.object({
|
|
86
|
+
// Strict by design: this is a legacy compatibility-test ingest schema with no
|
|
87
|
+
// live consumer (test-only + barrel re-export), so it validates the raw form
|
|
88
|
+
// without migrate-on-read. Live program validation goes through parseProgram.
|
|
86
89
|
program: ExecutionProgramSchema,
|
|
87
90
|
narration: VideoNarrationOverlaySchema.optional(),
|
|
88
91
|
}).strict().superRefine((value, ctx) => {
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Capture Agent — Wait Contract (AUT-240)
|
|
3
|
+
*
|
|
4
|
+
* Single source of truth for the engine's adaptive waiting behaviour. Zero user
|
|
5
|
+
* configuration: every budget/deadline below is an internal constant. Versioned
|
|
6
|
+
* (`WAIT_CONTRACT_VERSION`) so runs can be debugged and future migrations
|
|
7
|
+
* reasoned about; the global engine-versioning infrastructure is a separate
|
|
8
|
+
* effort.
|
|
9
|
+
*
|
|
10
|
+
* The model has three layers (see the AUT-240 issue):
|
|
11
|
+
* - Layer A — actionability & semantic postconditions.
|
|
12
|
+
* - Layer B — `waitForVisuallyStable` (screenshot stabilization).
|
|
13
|
+
* - Layer C — dynamic budgets: a wait extends while the page is making progress
|
|
14
|
+
* (network / DOM) and is cut once it is genuinely stuck, up to a hard
|
|
15
|
+
* per-media global deadline. This is the real cure for the engine's #1 pain —
|
|
16
|
+
* a slow-but-progressing page no longer trips a fixed timeout.
|
|
17
|
+
*/
|
|
18
|
+
import type { MediaMode, ProgressSnapshot } from './execution-types.js';
|
|
19
|
+
/** Bump when the wait semantics change in a way worth tracing on a run. */
|
|
20
|
+
export declare const WAIT_CONTRACT_VERSION = 1;
|
|
21
|
+
/**
|
|
22
|
+
* Hard ceiling on adaptive waiting, per media mode. Applied as a FLOOR via
|
|
23
|
+
* `max()` with the compiled opcode timeout (see `resolveGlobalWaitDeadlineMs`)
|
|
24
|
+
* so it never shortens an intentionally-long opcode (`SLEEP`, `END_CLIP`).
|
|
25
|
+
*/
|
|
26
|
+
export declare const GLOBAL_WAIT_CAP_MS: Record<MediaMode, number>;
|
|
27
|
+
/** No observed progress for this long ⇒ the wait is "stuck" and gets cut. */
|
|
28
|
+
export declare const STUCK_WINDOW_MS = 4000;
|
|
29
|
+
/** Watchdog poll cadence while a wait is in flight. */
|
|
30
|
+
export declare const PROGRESS_POLL_INTERVAL_MS = 250;
|
|
31
|
+
/**
|
|
32
|
+
* A timed-out wait cannot be cancelled through the adapter API. Before recovery
|
|
33
|
+
* or the next opcode starts, give the orphaned operation this long to settle so
|
|
34
|
+
* it does not mutate the page concurrently. Mirrors the runner's drain.
|
|
35
|
+
*/
|
|
36
|
+
export declare const ORPHAN_DRAIN_MS = 2000;
|
|
37
|
+
/** DOM-quiet settle window for visual stabilization (Layer B). */
|
|
38
|
+
export declare const DOM_QUIET_WINDOW_MS = 250;
|
|
39
|
+
/** Bounded pixel-convergence fallback (Layer B). */
|
|
40
|
+
export declare const PIXEL_FALLBACK_MAX_PASSES = 3;
|
|
41
|
+
export declare const PIXEL_FALLBACK_DIFF_THRESHOLD = 0.01;
|
|
42
|
+
/**
|
|
43
|
+
* Resolve the per-opcode global wait deadline (absolute ms timestamp).
|
|
44
|
+
* `compiledTimeoutMs` is the opcode's compiled `timeoutMs` — treated as a FLOOR,
|
|
45
|
+
* never a ceiling, so the deadline can only ever be extended past the compiled
|
|
46
|
+
* value, never shortened below it.
|
|
47
|
+
*/
|
|
48
|
+
export declare function resolveGlobalWaitDeadlineMs(startedAtMs: number, compiledTimeoutMs: number, mediaMode: MediaMode): number;
|
|
49
|
+
/**
|
|
50
|
+
* Did the page make observable progress between two snapshots? Any of: a
|
|
51
|
+
* navigation in flight, new network events, in-flight count change, network
|
|
52
|
+
* activity advancing, readyState change, or DOM node-count change. A null
|
|
53
|
+
* current reading is treated conservatively as "no progress"; the first real
|
|
54
|
+
* reading (null `prev`) counts as progress to seed the watchdog window.
|
|
55
|
+
*
|
|
56
|
+
* The network fields are first-party-scoped at the source (see
|
|
57
|
+
* `getProgressSnapshot` / `isFirstPartyUrl`): background third-party telemetry
|
|
58
|
+
* (analytics beacons, ad pixels, polling to other origins) is excluded, so it
|
|
59
|
+
* cannot keep this `true` forever and starve the stuck-cut.
|
|
60
|
+
*/
|
|
61
|
+
export declare function hasProgress(prev: ProgressSnapshot | null | undefined, cur: ProgressSnapshot | null | undefined): boolean;
|
|
62
|
+
export type ProgressBudgetCut = 'stuck' | 'deadline';
|
|
63
|
+
export interface ProgressBudgetOptions {
|
|
64
|
+
/** `Date.now()` when the surrounding opcode started (for elapsed/min-budget). */
|
|
65
|
+
startedAtMs: number;
|
|
66
|
+
/** Absolute hard deadline — the wait never runs past this. */
|
|
67
|
+
globalDeadlineMs: number;
|
|
68
|
+
/**
|
|
69
|
+
* Minimum patience (the compiled `timeoutMs` floor) before the stuck-cut may
|
|
70
|
+
* fire. A slow page that produces no early signal still gets at least this
|
|
71
|
+
* long before being judged stuck.
|
|
72
|
+
*/
|
|
73
|
+
minBudgetMs: number;
|
|
74
|
+
/** No-progress window before cutting "stuck". Defaults to `STUCK_WINDOW_MS`. */
|
|
75
|
+
stuckWindowMs?: number;
|
|
76
|
+
/** Watchdog cadence. Defaults to `PROGRESS_POLL_INTERVAL_MS`. */
|
|
77
|
+
pollIntervalMs?: number;
|
|
78
|
+
/**
|
|
79
|
+
* Progress probe. When omitted, the stuck-cut is disabled and the operation
|
|
80
|
+
* simply runs with its compiled floor budget (legacy fixed-timeout behaviour)
|
|
81
|
+
* — graceful degradation for adapters with no progress signal.
|
|
82
|
+
*/
|
|
83
|
+
getProgress?: () => Promise<ProgressSnapshot | null>;
|
|
84
|
+
}
|
|
85
|
+
export interface ProgressBudgetResult<T> {
|
|
86
|
+
/** Present when the wrapped operation resolved before any cut. */
|
|
87
|
+
result?: T;
|
|
88
|
+
/** Present when the watchdog cut the wait early. */
|
|
89
|
+
cut?: ProgressBudgetCut;
|
|
90
|
+
/** Total time spent (ms). */
|
|
91
|
+
waitedMs: number;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Run an adaptive wait that extends while the page is making progress and is cut
|
|
95
|
+
* once it is genuinely stuck or the global deadline is reached.
|
|
96
|
+
*
|
|
97
|
+
* `run(budgetMs)` is given a generous budget (up to the global deadline) and is
|
|
98
|
+
* expected to resolve as soon as its condition is met (or to time out at the
|
|
99
|
+
* budget). A concurrent watchdog races it: while `run` is still pending, the
|
|
100
|
+
* watchdog cuts the wait if no progress is observed for `stuckWindowMs` (after
|
|
101
|
+
* `minBudgetMs` has elapsed) or once the global deadline passes. If `run`
|
|
102
|
+
* resolves first, its result is returned and the watchdog is moot.
|
|
103
|
+
*/
|
|
104
|
+
export declare function runWithProgressBudget<T>(run: (budgetMs: number) => Promise<T>, options: ProgressBudgetOptions): Promise<ProgressBudgetResult<T>>;
|