autokap 1.0.8 → 1.0.10
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/assets/skill/OPCODE-REFERENCE.md +29 -1
- package/assets/skill/SKILL.md +2 -1
- package/dist/auth-capture.js +35 -2
- package/dist/billing-operation-logging.d.ts +3 -1
- package/dist/billing-operation-logging.js +4 -0
- package/dist/browser.d.ts +10 -10
- package/dist/browser.js +32 -28
- package/dist/capture-encryption.d.ts +3 -1
- package/dist/capture-encryption.js +21 -6
- package/dist/capture-strategy.js +3 -2
- package/dist/cli-config.d.ts +2 -1
- package/dist/cli-config.js +51 -2
- package/dist/cli-contract.d.ts +5 -1
- package/dist/cli-contract.js +7 -1
- package/dist/cli-runner-local.js +16 -3
- package/dist/cli-runner.js +165 -18
- package/dist/cli.js +25 -19
- package/dist/clip-begin-frame-recorder.d.ts +44 -0
- package/dist/clip-begin-frame-recorder.js +250 -0
- package/dist/clip-capture-backend.d.ts +25 -0
- package/dist/clip-capture-backend.js +189 -0
- package/dist/clip-capture-loop.d.ts +61 -0
- package/dist/clip-capture-loop.js +111 -0
- package/dist/clip-frame-recorder.d.ts +63 -0
- package/dist/clip-frame-recorder.js +305 -0
- package/dist/clip-postprocess.d.ts +31 -2
- package/dist/clip-postprocess.js +174 -57
- package/dist/clip-runtime.d.ts +18 -0
- package/dist/clip-runtime.js +67 -0
- package/dist/clip-scale.d.ts +10 -0
- package/dist/clip-scale.js +21 -0
- package/dist/clip-screencast-recorder.d.ts +42 -0
- package/dist/clip-screencast-recorder.js +242 -0
- package/dist/clip-sidecar.d.ts +54 -0
- package/dist/clip-sidecar.js +208 -0
- package/dist/env-validation.js +38 -4
- package/dist/execution-schema.d.ts +690 -360
- package/dist/execution-schema.js +98 -42
- package/dist/execution-types.d.ts +53 -3
- package/dist/execution-types.js +2 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/llm-healer.d.ts +2 -10
- package/dist/llm-healer.js +109 -62
- package/dist/llm-provider.js +3 -0
- package/dist/opcode-actions.js +13 -0
- package/dist/opcode-runner.js +21 -12
- package/dist/program-signing.d.ts +1094 -0
- package/dist/program-signing.js +140 -0
- package/dist/provider-config.d.ts +5 -0
- package/dist/provider-config.js +28 -1
- package/dist/recovery-chain.js +40 -16
- package/dist/types.d.ts +8 -2
- package/dist/web-playwright-local.d.ts +31 -1
- package/dist/web-playwright-local.js +207 -37
- package/package.json +12 -2
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# AutoKap Opcode Reference
|
|
2
2
|
|
|
3
|
-
Detailed parameter documentation for all
|
|
3
|
+
Detailed parameter documentation for all 25 opcodes. For workflow, rules, and examples, see [SKILL.md](SKILL.md).
|
|
4
4
|
|
|
5
5
|
## Common Fields (all opcodes)
|
|
6
6
|
|
|
@@ -248,6 +248,34 @@ Double-click an element.
|
|
|
248
248
|
{ "kind": "DOUBLE_CLICK", "selector": "[data-ak=\"editable-title\"]", "postcondition": { "type": "element_visible", "selector": "[data-ak=\"title-editor\"]", "waitMs": 3000 } }
|
|
249
249
|
```
|
|
250
250
|
|
|
251
|
+
## DRAG
|
|
252
|
+
|
|
253
|
+
Drag the source element from point A to point B with an animated cursor. In clip recordings the cursor overlay glides along a Bezier curve, shows a pressed state during the drag, and emits a drop pulse at the destination.
|
|
254
|
+
|
|
255
|
+
| Param | Type | Required | Description |
|
|
256
|
+
|-------|------|----------|-------------|
|
|
257
|
+
| `selector` | string | yes | CSS selector of the source element (the one being dragged) |
|
|
258
|
+
| `target` | SemanticTarget | no | Semantic fallback for the source |
|
|
259
|
+
| `fingerprint` | string | no | AKTree fingerprint for the source |
|
|
260
|
+
| `selectorAlternates` | string[] | no | Alternative source selectors |
|
|
261
|
+
| `toSelector` | string | no* | CSS selector of the drop target element |
|
|
262
|
+
| `toTarget` | SemanticTarget | no* | Semantic fallback for the drop target |
|
|
263
|
+
| `toSelectorAlternates` | string[] | no | Alternative destination selectors |
|
|
264
|
+
| `offset` | `{ dx: number, dy: number }` | no* | Pixel offset from the source center (use for sliders, canvas drawing, or any drop that isn't a DOM node) |
|
|
265
|
+
|
|
266
|
+
*Provide EITHER `toSelector` / `toTarget` (element drag) OR `offset` (relative drag). Not both.
|
|
267
|
+
|
|
268
|
+
**Can:** Element-to-element drag (Kanban columns, sortable lists), slider adjustments, canvas drawing, any interaction driven by mousedown + mousemove + mouseup. Works with native HTML5 drag, dnd-kit, react-dnd, Radix DnD primitives — `page.mouse.*` fires both mouse and pointer events.
|
|
269
|
+
**Cannot:** Multi-touch gestures, drag between different browser windows, file drops (use a file input instead).
|
|
270
|
+
|
|
271
|
+
```json
|
|
272
|
+
{ "kind": "DRAG", "selector": "[data-ak=\"task-card-todo-1\"]", "toSelector": "[data-ak=\"column-in-progress\"]", "postcondition": { "type": "element_visible", "selector": "[data-ak=\"column-in-progress\"] [data-ak=\"task-card-todo-1\"]", "waitMs": 3000 } }
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
```json
|
|
276
|
+
{ "kind": "DRAG", "selector": "[data-ak=\"volume-slider-thumb\"]", "offset": { "dx": 120, "dy": 0 }, "postcondition": { "type": "any_change" } }
|
|
277
|
+
```
|
|
278
|
+
|
|
251
279
|
## SET_LOCALE
|
|
252
280
|
|
|
253
281
|
Set the application's locale/language for the current variant.
|
package/assets/skill/SKILL.md
CHANGED
|
@@ -134,7 +134,7 @@ interface VariantSpec {
|
|
|
134
134
|
|
|
135
135
|
## Opcode Quick Reference
|
|
136
136
|
|
|
137
|
-
|
|
137
|
+
25 opcodes available. For full parameter documentation, see [OPCODE-REFERENCE.md](OPCODE-REFERENCE.md).
|
|
138
138
|
|
|
139
139
|
| Kind | Selector? | Key Params | Typical Postcondition | Notes |
|
|
140
140
|
|------|-----------|-----------|----------------------|-------|
|
|
@@ -149,6 +149,7 @@ interface VariantSpec {
|
|
|
149
149
|
| `SELECT_OPTION` | yes | `optionLabel` / `optionValue` / `optionIndex` | `text_contains` | **Native `<select>` only.** Custom dropdowns: use CLICK sequence |
|
|
150
150
|
| `CHECK` | yes | `checked` | `always` | Idempotent. Safer than CLICK for checkboxes |
|
|
151
151
|
| `DOUBLE_CLICK` | yes | — | `element_visible` / `any_change` | Inline editing, text selection |
|
|
152
|
+
| `DRAG` | yes | `toSelector?` / `toTarget?` / `offset?` | `element_visible` / `any_change` | Animated cursor A→B. Use `toSelector` for Kanban-style drops, `offset` for sliders / canvas |
|
|
152
153
|
| `SET_LOCALE` | no | `locale`, `method`, `storageHints?` | `always` | Use `"$variant"`. Prefer `method: "storage"` |
|
|
153
154
|
| `SET_THEME` | no | `theme`, `method`, `storageHints?` | `always` | Use `"$variant"`. Prefer `method: "storage"` |
|
|
154
155
|
| `ASSERT_ROUTE` | no | `urlPattern` | `route_matches` | Validation checkpoint |
|
package/dist/auth-capture.js
CHANGED
|
@@ -95,7 +95,7 @@ export async function captureAuthSession(options) {
|
|
|
95
95
|
if (!context || !browser.isConnected())
|
|
96
96
|
return;
|
|
97
97
|
try {
|
|
98
|
-
const snapshot = (await context.storageState());
|
|
98
|
+
const snapshot = sanitizeStorageStateForStartUrl((await context.storageState()), startUrl);
|
|
99
99
|
const hasAny = (snapshot.cookies?.length ?? 0) > 0 || (snapshot.origins?.length ?? 0) > 0;
|
|
100
100
|
if (hasAny)
|
|
101
101
|
lastGoodState = snapshot;
|
|
@@ -119,7 +119,7 @@ export async function captureAuthSession(options) {
|
|
|
119
119
|
// snapshot (most up-to-date) before tearing down.
|
|
120
120
|
if (userConfirmed && browser.isConnected()) {
|
|
121
121
|
try {
|
|
122
|
-
lastGoodState = (await context.storageState());
|
|
122
|
+
lastGoodState = sanitizeStorageStateForStartUrl((await context.storageState()), startUrl);
|
|
123
123
|
}
|
|
124
124
|
catch {
|
|
125
125
|
// fall back to whatever the poll captured
|
|
@@ -161,4 +161,37 @@ export async function captureAuthSession(options) {
|
|
|
161
161
|
}
|
|
162
162
|
}
|
|
163
163
|
}
|
|
164
|
+
function sanitizeStorageStateForStartUrl(state, startUrl) {
|
|
165
|
+
try {
|
|
166
|
+
const parsed = new URL(startUrl);
|
|
167
|
+
const allowedSuffix = buildAllowedCookieSuffix(parsed.hostname);
|
|
168
|
+
const cookies = (state.cookies ?? []).filter((cookie) => {
|
|
169
|
+
const domain = String(cookie.domain ?? '').trim().replace(/^\./, '').toLowerCase();
|
|
170
|
+
return domain === parsed.hostname || domain.endsWith(`.${allowedSuffix}`) || domain === allowedSuffix;
|
|
171
|
+
});
|
|
172
|
+
const origins = (state.origins ?? []).filter((originEntry) => {
|
|
173
|
+
try {
|
|
174
|
+
const origin = new URL(String(originEntry.origin ?? ''));
|
|
175
|
+
return origin.hostname === parsed.hostname
|
|
176
|
+
|| origin.hostname === allowedSuffix
|
|
177
|
+
|| origin.hostname.endsWith(`.${allowedSuffix}`);
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
return { cookies, origins };
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
return state;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
function buildAllowedCookieSuffix(hostname) {
|
|
190
|
+
const normalized = hostname.trim().toLowerCase().replace(/^\.+/, '');
|
|
191
|
+
if (!normalized || normalized === 'localhost' || /^\d{1,3}(?:\.\d{1,3}){3}$/.test(normalized)) {
|
|
192
|
+
return normalized;
|
|
193
|
+
}
|
|
194
|
+
const parts = normalized.split('.');
|
|
195
|
+
return parts.length >= 2 ? parts.slice(-2).join('.') : normalized;
|
|
196
|
+
}
|
|
164
197
|
//# sourceMappingURL=auth-capture.js.map
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { SupabaseClient } from '@supabase/supabase-js';
|
|
2
2
|
import type { StepUsage } from './types.js';
|
|
3
|
-
export type BillingOperationType = 'screenshot' | 'clip' | 'video';
|
|
3
|
+
export type BillingOperationType = 'screenshot' | 'clip' | 'video' | 'interactive_demo';
|
|
4
4
|
type BillingOperationOutcome = 'succeeded' | 'failed' | 'cancelled';
|
|
5
5
|
interface BillingOperationContext {
|
|
6
6
|
runId: string;
|
|
@@ -10,6 +10,7 @@ interface BillingOperationContext {
|
|
|
10
10
|
captureId?: string | null;
|
|
11
11
|
videoId?: string | null;
|
|
12
12
|
clipRecordId?: string | null;
|
|
13
|
+
interactiveDemoStateId?: string | null;
|
|
13
14
|
operationType: BillingOperationType;
|
|
14
15
|
captureType?: 'fullpage' | 'element' | 'video' | null;
|
|
15
16
|
lang?: string | null;
|
|
@@ -33,6 +34,7 @@ export declare function insertBillingOperationLog(supabase: SupabaseClient, ctx:
|
|
|
33
34
|
export declare function insertScreenshotOperationLog(supabase: SupabaseClient, ctx: Omit<BillingOperationContext, 'operationType'>, params: BillingOperationParams): Promise<string | null>;
|
|
34
35
|
export declare function insertClipOperationLog(supabase: SupabaseClient, ctx: Omit<BillingOperationContext, 'operationType'>, params: BillingOperationParams): Promise<string | null>;
|
|
35
36
|
export declare function insertVideoOperationLog(supabase: SupabaseClient, ctx: Omit<BillingOperationContext, 'operationType'>, params: BillingOperationParams): Promise<string | null>;
|
|
37
|
+
export declare function insertInteractiveDemoOperationLog(supabase: SupabaseClient, ctx: Omit<BillingOperationContext, 'operationType'>, params: BillingOperationParams): Promise<string | null>;
|
|
36
38
|
export declare function cancelScreenshotOperationLogsForCapture(supabase: SupabaseClient, captureId: string, reason: string): Promise<void>;
|
|
37
39
|
export declare function reconcilePendingBillingOperationCosts(supabase: SupabaseClient, operationIds?: string[]): Promise<void>;
|
|
38
40
|
export {};
|
|
@@ -131,6 +131,7 @@ export async function insertBillingOperationLog(supabase, ctx, params) {
|
|
|
131
131
|
capture_id: ctx.captureId ?? null,
|
|
132
132
|
video_id: ctx.videoId ?? null,
|
|
133
133
|
clip_record_id: ctx.clipRecordId ?? null,
|
|
134
|
+
interactive_demo_state_id: ctx.interactiveDemoStateId ?? null,
|
|
134
135
|
operation_type: ctx.operationType,
|
|
135
136
|
operation_outcome: params.outcome,
|
|
136
137
|
outcome_reason: params.outcomeReason ?? null,
|
|
@@ -180,6 +181,9 @@ export async function insertClipOperationLog(supabase, ctx, params) {
|
|
|
180
181
|
export async function insertVideoOperationLog(supabase, ctx, params) {
|
|
181
182
|
return insertBillingOperationLog(supabase, { ...ctx, operationType: 'video' }, params);
|
|
182
183
|
}
|
|
184
|
+
export async function insertInteractiveDemoOperationLog(supabase, ctx, params) {
|
|
185
|
+
return insertBillingOperationLog(supabase, { ...ctx, operationType: 'interactive_demo' }, params);
|
|
186
|
+
}
|
|
183
187
|
export async function cancelScreenshotOperationLogsForCapture(supabase, captureId, reason) {
|
|
184
188
|
try {
|
|
185
189
|
const { error } = await supabase
|
package/dist/browser.d.ts
CHANGED
|
@@ -107,20 +107,20 @@ export declare class Browser {
|
|
|
107
107
|
*/
|
|
108
108
|
static fromPool(options: BrowserOptions): Promise<Browser>;
|
|
109
109
|
/**
|
|
110
|
-
* Create a Browser
|
|
111
|
-
*
|
|
110
|
+
* Create a Browser dedicated to clip capture. Frames are pulled via CDP
|
|
111
|
+
* `Page.captureScreenshot` in a tight loop by `ClipCaptureLoop` — NOT via
|
|
112
|
+
* Playwright's built-in `recordVideo` (which plateaus at 27 FPS with ~12%
|
|
113
|
+
* duplicates due to the CDP screencast throttler).
|
|
112
114
|
*
|
|
113
|
-
*
|
|
114
|
-
* `
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
* @param videoDir - Directory where Playwright will write the WebM file.
|
|
115
|
+
* Preserves the HiDPI rendering path (`--force-device-scale-factor` +
|
|
116
|
+
* `--window-size`) so the captured frames match viewport × DSF pixels, and
|
|
117
|
+
* the cursor overlay script so clicks/hover moments are visible.
|
|
118
118
|
*/
|
|
119
|
-
static
|
|
119
|
+
static forClipCapture(options: BrowserOptions, cursorScript: string): Promise<Browser>;
|
|
120
120
|
/**
|
|
121
121
|
* Close only the browser context (not the browser process).
|
|
122
|
-
*
|
|
123
|
-
* while keeping the browser process alive
|
|
122
|
+
* Used by clip capture to release the context promptly after the CDP loop
|
|
123
|
+
* has stopped, while keeping the browser process alive for any pending teardown.
|
|
124
124
|
* Call browser.close() afterwards to shut down the browser process.
|
|
125
125
|
*/
|
|
126
126
|
closeContext(): Promise<void>;
|
package/dist/browser.js
CHANGED
|
@@ -141,16 +141,6 @@ function normalizeDeviceScaleFactor(value) {
|
|
|
141
141
|
return 2;
|
|
142
142
|
return Math.max(0.5, Math.min(4, Number(value)));
|
|
143
143
|
}
|
|
144
|
-
function resolveRecordedVideoSize(viewport) {
|
|
145
|
-
// Playwright's video recorder expects a canvas size close to the CSS viewport.
|
|
146
|
-
// Using viewport × deviceScaleFactor can produce a larger recording surface
|
|
147
|
-
// with the page rendered only in the top-left corner, leaving the remainder
|
|
148
|
-
// as empty matte. Keep the recorded frame size aligned to the viewport and
|
|
149
|
-
// let the browser's deviceScaleFactor handle HiDPI rendering internally.
|
|
150
|
-
const width = Math.max(2, Math.round(viewport.width)) & ~1;
|
|
151
|
-
const height = Math.max(2, Math.round(viewport.height)) & ~1;
|
|
152
|
-
return { width, height };
|
|
153
|
-
}
|
|
154
144
|
function escapeCssAttributeValue(value) {
|
|
155
145
|
return value
|
|
156
146
|
.replace(/\\/g, '\\\\')
|
|
@@ -327,24 +317,42 @@ export class Browser {
|
|
|
327
317
|
return instance;
|
|
328
318
|
}
|
|
329
319
|
/**
|
|
330
|
-
* Create a Browser
|
|
331
|
-
*
|
|
332
|
-
*
|
|
333
|
-
*
|
|
334
|
-
* `browser.currentPage.video()?.path()` BEFORE closing (save ref beforehand).
|
|
335
|
-
* Or call `browser.saveVideo(outputPath)` before closing.
|
|
320
|
+
* Create a Browser dedicated to clip capture. Frames are pulled via CDP
|
|
321
|
+
* `Page.captureScreenshot` in a tight loop by `ClipCaptureLoop` — NOT via
|
|
322
|
+
* Playwright's built-in `recordVideo` (which plateaus at 27 FPS with ~12%
|
|
323
|
+
* duplicates due to the CDP screencast throttler).
|
|
336
324
|
*
|
|
337
|
-
*
|
|
325
|
+
* Preserves the HiDPI rendering path (`--force-device-scale-factor` +
|
|
326
|
+
* `--window-size`) so the captured frames match viewport × DSF pixels, and
|
|
327
|
+
* the cursor overlay script so clicks/hover moments are visible.
|
|
338
328
|
*/
|
|
339
|
-
static async
|
|
329
|
+
static async forClipCapture(options, cursorScript) {
|
|
340
330
|
const instance = new Browser(options);
|
|
341
331
|
const deviceScaleFactor = normalizeDeviceScaleFactor(options.deviceScaleFactor);
|
|
342
|
-
|
|
343
|
-
//
|
|
344
|
-
// `
|
|
332
|
+
// Enable GPU compositor on non-Linux platforms so Chrome can render
|
|
333
|
+
// 2880×1800 without saturating the CPU. Linux (Docker/CI) keeps
|
|
334
|
+
// `--disable-gpu` from CHROMIUM_ARGS because GPU is rarely available there.
|
|
335
|
+
const baseArgs = process.platform === 'linux'
|
|
336
|
+
? CHROMIUM_ARGS
|
|
337
|
+
: CHROMIUM_ARGS.filter(arg => arg !== '--disable-gpu' && arg !== '--disable-gpu-sandbox');
|
|
338
|
+
// Pin ANGLE to the platform's native graphics API. Chrome's default
|
|
339
|
+
// backend is OpenGL on macOS, which is far slower than Metal for the
|
|
340
|
+
// compositor (measured 4 FPS vs 32 FPS at 2880×1800 on a heavy React UI).
|
|
341
|
+
// Same story on Windows where D3D11 is the native fast path.
|
|
342
|
+
const angleArg = process.platform === 'darwin' ? '--use-angle=metal'
|
|
343
|
+
: process.platform === 'win32' ? '--use-angle=d3d11'
|
|
344
|
+
: null; // Linux: skip — GPU is rarely present in CI anyway
|
|
345
|
+
const clipArgs = [
|
|
346
|
+
...baseArgs,
|
|
347
|
+
`--force-device-scale-factor=${deviceScaleFactor}`,
|
|
348
|
+
`--window-size=${Math.round(options.viewport.width)},${Math.round(options.viewport.height)}`,
|
|
349
|
+
...(angleArg ? [angleArg] : []),
|
|
350
|
+
];
|
|
351
|
+
// Dedicated browser process for clip capture. Not pooled because clip
|
|
352
|
+
// capture installs context-level init scripts (cursor overlay).
|
|
345
353
|
instance.browser = await chromium.launch({
|
|
346
354
|
headless: !options.headed,
|
|
347
|
-
args:
|
|
355
|
+
args: clipArgs,
|
|
348
356
|
});
|
|
349
357
|
instance.context = await instance.browser.newContext({
|
|
350
358
|
viewport: options.viewport,
|
|
@@ -352,10 +360,6 @@ export class Browser {
|
|
|
352
360
|
locale: langToLocale(options.lang ?? 'en'),
|
|
353
361
|
colorScheme: options.colorScheme ?? 'light',
|
|
354
362
|
storageState: options.storageState,
|
|
355
|
-
recordVideo: {
|
|
356
|
-
dir: videoDir,
|
|
357
|
-
size: recordedVideoSize,
|
|
358
|
-
},
|
|
359
363
|
});
|
|
360
364
|
// Inject cursor overlay at context level — survives all navigations in this session
|
|
361
365
|
await instance.context.addInitScript(cursorScript);
|
|
@@ -364,8 +368,8 @@ export class Browser {
|
|
|
364
368
|
}
|
|
365
369
|
/**
|
|
366
370
|
* Close only the browser context (not the browser process).
|
|
367
|
-
*
|
|
368
|
-
* while keeping the browser process alive
|
|
371
|
+
* Used by clip capture to release the context promptly after the CDP loop
|
|
372
|
+
* has stopped, while keeping the browser process alive for any pending teardown.
|
|
369
373
|
* Call browser.close() afterwards to shut down the browser process.
|
|
370
374
|
*/
|
|
371
375
|
async closeContext() {
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
export interface EncryptedEnvelope {
|
|
2
2
|
__encrypted: true;
|
|
3
|
-
version: 1;
|
|
3
|
+
version: 1 | 2;
|
|
4
|
+
keyVersion?: 2;
|
|
4
5
|
iv: string;
|
|
5
6
|
tag: string;
|
|
6
7
|
ciphertext: string;
|
|
8
|
+
salt?: string;
|
|
7
9
|
}
|
|
8
10
|
export declare function encrypt(plaintext: string, secret: string): EncryptedEnvelope;
|
|
9
11
|
export declare function decrypt(envelope: EncryptedEnvelope, secret: string): string;
|
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
import crypto from 'node:crypto';
|
|
2
|
-
function
|
|
2
|
+
function deriveLegacyKey(secret) {
|
|
3
3
|
return crypto.createHash('sha256').update(secret).digest();
|
|
4
4
|
}
|
|
5
|
+
function deriveKey(secret, salt) {
|
|
6
|
+
return crypto.scryptSync(secret, salt, 32, {
|
|
7
|
+
N: 1 << 15,
|
|
8
|
+
r: 8,
|
|
9
|
+
p: 1,
|
|
10
|
+
maxmem: 128 * 1024 * 1024,
|
|
11
|
+
});
|
|
12
|
+
}
|
|
5
13
|
export function encrypt(plaintext, secret) {
|
|
6
|
-
const
|
|
14
|
+
const salt = crypto.randomBytes(16);
|
|
15
|
+
const key = deriveKey(secret, salt);
|
|
7
16
|
const iv = crypto.randomBytes(12);
|
|
8
17
|
const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
|
|
9
18
|
const ciphertext = Buffer.concat([
|
|
@@ -13,14 +22,18 @@ export function encrypt(plaintext, secret) {
|
|
|
13
22
|
const tag = cipher.getAuthTag();
|
|
14
23
|
return {
|
|
15
24
|
__encrypted: true,
|
|
16
|
-
version:
|
|
25
|
+
version: 2,
|
|
26
|
+
keyVersion: 2,
|
|
17
27
|
iv: iv.toString('base64'),
|
|
18
28
|
tag: tag.toString('base64'),
|
|
19
29
|
ciphertext: ciphertext.toString('base64'),
|
|
30
|
+
salt: salt.toString('base64'),
|
|
20
31
|
};
|
|
21
32
|
}
|
|
22
33
|
export function decrypt(envelope, secret) {
|
|
23
|
-
const key =
|
|
34
|
+
const key = envelope.version === 1
|
|
35
|
+
? deriveLegacyKey(secret)
|
|
36
|
+
: deriveKey(secret, Buffer.from(envelope.salt ?? '', 'base64'));
|
|
24
37
|
const decipher = crypto.createDecipheriv('aes-256-gcm', key, Buffer.from(envelope.iv, 'base64'));
|
|
25
38
|
decipher.setAuthTag(Buffer.from(envelope.tag, 'base64'));
|
|
26
39
|
return Buffer.concat([
|
|
@@ -33,9 +46,11 @@ export function isEncryptedEnvelope(value) {
|
|
|
33
46
|
return false;
|
|
34
47
|
const candidate = value;
|
|
35
48
|
return (candidate.__encrypted === true
|
|
36
|
-
&& candidate.version === 1
|
|
49
|
+
&& (candidate.version === 1 || candidate.version === 2)
|
|
37
50
|
&& typeof candidate.iv === 'string'
|
|
38
51
|
&& typeof candidate.tag === 'string'
|
|
39
|
-
&& typeof candidate.ciphertext === 'string'
|
|
52
|
+
&& typeof candidate.ciphertext === 'string'
|
|
53
|
+
&& (candidate.version === 1
|
|
54
|
+
|| (candidate.keyVersion === 2 && typeof candidate.salt === 'string')));
|
|
40
55
|
}
|
|
41
56
|
//# sourceMappingURL=capture-encryption.js.map
|
package/dist/capture-strategy.js
CHANGED
|
@@ -34,8 +34,9 @@ export class ClipStrategy {
|
|
|
34
34
|
mediaMode = 'clip';
|
|
35
35
|
async prepare(adapter, _spec) {
|
|
36
36
|
// Clip recording preparation:
|
|
37
|
-
// -
|
|
38
|
-
// - Cursor overlay is injected
|
|
37
|
+
// - Browser is launched via Browser.forClipCapture() with HiDPI flags
|
|
38
|
+
// - Cursor overlay script is injected at context level
|
|
39
|
+
// - Frame capture starts in adapter.beginRecording() via ClipCaptureLoop
|
|
39
40
|
}
|
|
40
41
|
async capture(adapter, _spec) {
|
|
41
42
|
// Clip capture is bounded by BEGIN_CLIP / END_CLIP opcodes.
|
package/dist/cli-config.d.ts
CHANGED
|
@@ -9,6 +9,7 @@ declare const LOCAL_API_BASE_URL = "http://localhost:3000";
|
|
|
9
9
|
declare const LOCAL_WS_URL = "ws://localhost:3000/ws";
|
|
10
10
|
declare const API_BASE_URL_ENV_VAR = "AUTOKAP_API_BASE_URL";
|
|
11
11
|
declare const WS_URL_ENV_VAR = "AUTOKAP_WS_URL";
|
|
12
|
+
declare const ALLOW_UNSAFE_SERVER_ORIGIN_ENV_VAR = "AUTOKAP_ALLOW_UNSAFE_SERVER_ORIGIN";
|
|
12
13
|
export declare function getConfigDir(): string;
|
|
13
14
|
export declare function getConfigPath(): string;
|
|
14
15
|
export declare function getDefaultApiBaseUrl(): string;
|
|
@@ -17,4 +18,4 @@ export declare function readConfig(): Promise<AutokapConfig | null>;
|
|
|
17
18
|
export declare function writeConfig(config: AutokapConfig): Promise<void>;
|
|
18
19
|
export declare function deleteConfig(): Promise<void>;
|
|
19
20
|
export declare function requireConfig(): Promise<AutokapConfig>;
|
|
20
|
-
export { DEFAULT_API_BASE_URL, DEFAULT_WS_URL, LOCAL_API_BASE_URL, LOCAL_WS_URL, API_BASE_URL_ENV_VAR, WS_URL_ENV_VAR, };
|
|
21
|
+
export { DEFAULT_API_BASE_URL, DEFAULT_WS_URL, LOCAL_API_BASE_URL, LOCAL_WS_URL, API_BASE_URL_ENV_VAR, WS_URL_ENV_VAR, ALLOW_UNSAFE_SERVER_ORIGIN_ENV_VAR, };
|
package/dist/cli-config.js
CHANGED
|
@@ -8,6 +8,7 @@ const LOCAL_API_BASE_URL = 'http://localhost:3000';
|
|
|
8
8
|
const LOCAL_WS_URL = 'ws://localhost:3000/ws';
|
|
9
9
|
const API_BASE_URL_ENV_VAR = 'AUTOKAP_API_BASE_URL';
|
|
10
10
|
const WS_URL_ENV_VAR = 'AUTOKAP_WS_URL';
|
|
11
|
+
const ALLOW_UNSAFE_SERVER_ORIGIN_ENV_VAR = 'AUTOKAP_ALLOW_UNSAFE_SERVER_ORIGIN';
|
|
11
12
|
export function getConfigDir() {
|
|
12
13
|
return path.join(os.homedir(), '.autokap');
|
|
13
14
|
}
|
|
@@ -32,6 +33,10 @@ export async function readConfig() {
|
|
|
32
33
|
const apiBaseUrl = envApiBaseUrl ?? storedApiBaseUrl;
|
|
33
34
|
const wsUrl = envWsUrl
|
|
34
35
|
?? (envApiBaseUrl ? deriveWsUrl(apiBaseUrl) : normalizeUrl(parsed.wsUrl) ?? deriveWsUrl(apiBaseUrl));
|
|
36
|
+
assertAllowedApiOrigin(apiBaseUrl, storedApiBaseUrl, envApiBaseUrl ? API_BASE_URL_ENV_VAR : undefined);
|
|
37
|
+
if (envWsUrl) {
|
|
38
|
+
assertAllowedApiOrigin(wsUrl.replace(/^ws/, 'http'), deriveWsUrl(storedApiBaseUrl).replace(/^ws/, 'http'), WS_URL_ENV_VAR);
|
|
39
|
+
}
|
|
35
40
|
return {
|
|
36
41
|
apiKey: parsed.apiKey,
|
|
37
42
|
apiBaseUrl,
|
|
@@ -67,7 +72,14 @@ export async function deleteConfig() {
|
|
|
67
72
|
}
|
|
68
73
|
}
|
|
69
74
|
export async function requireConfig() {
|
|
70
|
-
|
|
75
|
+
let config = null;
|
|
76
|
+
try {
|
|
77
|
+
config = await readConfig();
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
logger.error(error instanceof Error ? error.message : String(error));
|
|
81
|
+
process.exit(1);
|
|
82
|
+
}
|
|
71
83
|
if (!config) {
|
|
72
84
|
logger.error('Not authenticated. Run `npx autokap@latest init --cli-key <key>` first.');
|
|
73
85
|
process.exit(1);
|
|
@@ -94,5 +106,42 @@ function deriveWsUrl(apiBaseUrl) {
|
|
|
94
106
|
return DEFAULT_WS_URL;
|
|
95
107
|
}
|
|
96
108
|
}
|
|
97
|
-
|
|
109
|
+
function normalizeOrigin(value) {
|
|
110
|
+
const normalized = normalizeUrl(value);
|
|
111
|
+
if (!normalized)
|
|
112
|
+
return null;
|
|
113
|
+
try {
|
|
114
|
+
const parsed = new URL(normalized);
|
|
115
|
+
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:')
|
|
116
|
+
return null;
|
|
117
|
+
return parsed.origin;
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
function isUnsafeOverrideEnabled() {
|
|
124
|
+
const raw = process.env[ALLOW_UNSAFE_SERVER_ORIGIN_ENV_VAR]?.trim().toLowerCase();
|
|
125
|
+
return raw === '1' || raw === 'true';
|
|
126
|
+
}
|
|
127
|
+
function isTrustedOrigin(origin) {
|
|
128
|
+
return origin === normalizeOrigin(DEFAULT_API_BASE_URL)
|
|
129
|
+
|| origin === normalizeOrigin(LOCAL_API_BASE_URL);
|
|
130
|
+
}
|
|
131
|
+
function assertAllowedApiOrigin(candidateUrl, baselineUrl, envVar) {
|
|
132
|
+
const candidateOrigin = normalizeOrigin(candidateUrl);
|
|
133
|
+
const baselineOrigin = normalizeOrigin(baselineUrl);
|
|
134
|
+
if (!candidateOrigin || !baselineOrigin) {
|
|
135
|
+
throw new Error('AutoKap CLI config contains an invalid server origin');
|
|
136
|
+
}
|
|
137
|
+
if (candidateOrigin === baselineOrigin || isTrustedOrigin(candidateOrigin)) {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
if (isUnsafeOverrideEnabled()) {
|
|
141
|
+
logger.warn(`[config] Allowing unsafe server override from ${baselineOrigin} to ${candidateOrigin} because ${ALLOW_UNSAFE_SERVER_ORIGIN_ENV_VAR}=1`);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
throw new Error(`Refusing unsafe server override to ${candidateOrigin}. Set ${ALLOW_UNSAFE_SERVER_ORIGIN_ENV_VAR}=1 to allow ${envVar ?? 'this override'} explicitly.`);
|
|
145
|
+
}
|
|
146
|
+
export { DEFAULT_API_BASE_URL, DEFAULT_WS_URL, LOCAL_API_BASE_URL, LOCAL_WS_URL, API_BASE_URL_ENV_VAR, WS_URL_ENV_VAR, ALLOW_UNSAFE_SERVER_ORIGIN_ENV_VAR, };
|
|
98
147
|
//# sourceMappingURL=cli-config.js.map
|
package/dist/cli-contract.d.ts
CHANGED
|
@@ -5,11 +5,15 @@ export interface CliPublicCommandDescriptor {
|
|
|
5
5
|
docsDescriptionKey?: string;
|
|
6
6
|
}
|
|
7
7
|
export declare const CLI_KEY_TERM = "CLI key";
|
|
8
|
+
export declare const CLI_VERSION_HEADER = "x-autokap-cli-version";
|
|
9
|
+
export declare const MIN_CLI_VERSION_FOR_SIGNED_PROGRAMS = "1.0.9";
|
|
8
10
|
export declare const CLI_DEFAULT_INSTALL_COMMAND = "npm install -g autokap";
|
|
11
|
+
export declare const CLI_DEFAULT_INSTALLED_SETUP_COMMAND = "autokap init --cli-key <your-cli-key>";
|
|
9
12
|
export declare const CLI_DEFAULT_SETUP_COMMAND = "npx autokap@latest init --cli-key <your-cli-key>";
|
|
10
|
-
export declare const CLI_ADVANCED_SKILL_COMMAND = "
|
|
13
|
+
export declare const CLI_ADVANCED_SKILL_COMMAND = "autokap skill --agent <agent>";
|
|
11
14
|
export declare const CLI_FALLBACK_PROGRAM_COMMAND = "autokap run <preset-id> --program <file>";
|
|
12
15
|
export declare function buildCliRunCommand(presetId: string, options?: {
|
|
13
16
|
local?: boolean;
|
|
14
17
|
}): string;
|
|
18
|
+
export declare function buildCliInstalledSetupCommand(cliKey: string): string;
|
|
15
19
|
export declare const CLI_PUBLIC_COMMANDS: CliPublicCommandDescriptor[];
|
package/dist/cli-contract.js
CHANGED
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
export const CLI_KEY_TERM = "CLI key";
|
|
2
|
+
export const CLI_VERSION_HEADER = "x-autokap-cli-version";
|
|
3
|
+
export const MIN_CLI_VERSION_FOR_SIGNED_PROGRAMS = "1.0.9";
|
|
2
4
|
export const CLI_DEFAULT_INSTALL_COMMAND = "npm install -g autokap";
|
|
5
|
+
export const CLI_DEFAULT_INSTALLED_SETUP_COMMAND = "autokap init --cli-key <your-cli-key>";
|
|
3
6
|
export const CLI_DEFAULT_SETUP_COMMAND = "npx autokap@latest init --cli-key <your-cli-key>";
|
|
4
|
-
export const CLI_ADVANCED_SKILL_COMMAND = "
|
|
7
|
+
export const CLI_ADVANCED_SKILL_COMMAND = "autokap skill --agent <agent>";
|
|
5
8
|
export const CLI_FALLBACK_PROGRAM_COMMAND = "autokap run <preset-id> --program <file>";
|
|
6
9
|
export function buildCliRunCommand(presetId, options = {}) {
|
|
7
10
|
return `autokap run${options.local ? " --local" : ""} ${presetId}`;
|
|
8
11
|
}
|
|
12
|
+
export function buildCliInstalledSetupCommand(cliKey) {
|
|
13
|
+
return `autokap init --cli-key ${cliKey}`;
|
|
14
|
+
}
|
|
9
15
|
export const CLI_PUBLIC_COMMANDS = [
|
|
10
16
|
{
|
|
11
17
|
id: "init",
|
package/dist/cli-runner-local.js
CHANGED
|
@@ -87,16 +87,29 @@ async function persistArtifactsLocally(presetId, outputDirOption, variants) {
|
|
|
87
87
|
let suffix = '';
|
|
88
88
|
if (artifact.mediaMode === 'dom') {
|
|
89
89
|
if (artifact.fragmentName && artifact.parentStateName) {
|
|
90
|
-
suffix = `-${artifact.parentStateName}-fragment-${artifact.fragmentName}`;
|
|
90
|
+
suffix = `-${sanitizePathToken(artifact.parentStateName)}-fragment-${sanitizePathToken(artifact.fragmentName)}`;
|
|
91
91
|
}
|
|
92
92
|
else if (artifact.stateName) {
|
|
93
|
-
suffix = `-${artifact.stateName}`;
|
|
93
|
+
suffix = `-${sanitizePathToken(artifact.stateName)}`;
|
|
94
94
|
}
|
|
95
95
|
}
|
|
96
|
-
const
|
|
96
|
+
const fileName = `capture-${sanitizePathToken(presetId.slice(0, 8))}-${sanitizePathToken(variant.variantId)}${suffix}-${index}.${ext}`;
|
|
97
|
+
const filePath = resolveContainedPath(outputDir, fileName);
|
|
97
98
|
await fs.writeFile(filePath, artifact.buffer);
|
|
98
99
|
logger.info(`[capture] Artifact saved locally: ${filePath}`);
|
|
99
100
|
}
|
|
100
101
|
}
|
|
101
102
|
}
|
|
103
|
+
function sanitizePathToken(value) {
|
|
104
|
+
const cleaned = value.trim().replace(/[^a-zA-Z0-9._-]+/g, '-').replace(/-+/g, '-');
|
|
105
|
+
return cleaned.length > 0 ? cleaned.slice(0, 80) : 'artifact';
|
|
106
|
+
}
|
|
107
|
+
function resolveContainedPath(outputDir, fileName) {
|
|
108
|
+
const resolved = path.resolve(outputDir, fileName);
|
|
109
|
+
const relative = path.relative(outputDir, resolved);
|
|
110
|
+
if (relative.startsWith('..') || path.isAbsolute(relative)) {
|
|
111
|
+
throw new Error(`Refusing to write outside output directory: ${fileName}`);
|
|
112
|
+
}
|
|
113
|
+
return resolved;
|
|
114
|
+
}
|
|
102
115
|
//# sourceMappingURL=cli-runner-local.js.map
|