autokap 1.6.6 → 1.6.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/browser-bar.js +8 -6
- package/dist/cli-config.d.ts +4 -1
- package/dist/cli-config.js +15 -1
- package/dist/cli-runner-local.d.ts +1 -0
- package/dist/cli-runner-local.js +1 -0
- package/dist/cli-runner.d.ts +5 -0
- package/dist/cli-runner.js +78 -73
- package/dist/cli.js +12 -0
- package/dist/log-collector.d.ts +46 -0
- package/dist/log-collector.js +120 -0
- package/dist/logger.d.ts +1 -0
- package/dist/logger.js +3 -0
- package/dist/mockup.js +18 -1
- package/dist/safari-browser-bar.js +7 -5
- package/dist/server-credit-usage.d.ts +1 -1
- package/dist/sf-pro-resvg-fonts.d.ts +44 -0
- package/dist/sf-pro-resvg-fonts.js +126 -0
- package/dist/telemetry-redaction.d.ts +2 -0
- package/dist/telemetry-redaction.js +25 -0
- package/package.json +1 -1
package/dist/browser-bar.js
CHANGED
|
@@ -155,11 +155,14 @@ function attr(s) {
|
|
|
155
155
|
}
|
|
156
156
|
// ── Chrome generator (SVG, for sharp compositing — no Playwright) ──────
|
|
157
157
|
// Produces a self-contained SVG string rasterizable by @resvg/resvg-js.
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
158
|
+
// SF Pro fonts for the SVG output are NOT embedded inline — Resvg-js 2.6.2
|
|
159
|
+
// does not honor `@font-face url(data:font/woff2;base64,…)` declarations
|
|
160
|
+
// inside an SVG <defs><style> block. They must instead be supplied via the
|
|
161
|
+
// `font.fontFiles` Resvg option, which is the job of
|
|
162
|
+
// `mockup.ts::rasterizeSvg` + `sf-pro-resvg-fonts.ts`. The `font-family`
|
|
163
|
+
// declarations on the <text> elements below remain as a hint for any
|
|
164
|
+
// downstream consumer that DOES honor CSS @font-face (e.g. a future Resvg
|
|
165
|
+
// release, or a different SVG renderer entirely).
|
|
163
166
|
const SVG_BB_FF = "'SF Pro Text',system-ui,sans-serif";
|
|
164
167
|
/**
|
|
165
168
|
* Generate a self-contained SVG string for the Chrome browser bar.
|
|
@@ -215,7 +218,6 @@ export function generateChromeBrowserBarSvg(options) {
|
|
|
215
218
|
// and hide the colored circles underneath.
|
|
216
219
|
return `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="${width}" height="${height}" viewBox="0 0 ${iw} ${REF_H}" fill="none">
|
|
217
220
|
<defs>
|
|
218
|
-
<style>${SVG_BB_FONT_CSS}</style>
|
|
219
221
|
<linearGradient id="bb_profile_grad" x1="${profileX + 15}" y1="109.5" x2="${profileX + 15}" y2="79.5" gradientUnits="userSpaceOnUse">
|
|
220
222
|
<stop stop-color="#D5D8E4"/><stop offset="0.45" stop-color="#D1CAD6"/><stop offset="1" stop-color="#B7BAD1"/>
|
|
221
223
|
</linearGradient>
|
package/dist/cli-config.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ export interface AutokapConfig {
|
|
|
2
2
|
apiKey: string;
|
|
3
3
|
apiBaseUrl: string;
|
|
4
4
|
wsUrl: string;
|
|
5
|
+
exportDebugLogs?: boolean;
|
|
5
6
|
}
|
|
6
7
|
declare const DEFAULT_API_BASE_URL = "https://autokap.app";
|
|
7
8
|
declare const DEFAULT_WS_URL = "wss://autokap.app/ws";
|
|
@@ -12,6 +13,8 @@ declare const RUN_TOKEN_ENV_VAR = "AUTOKAP_RUN_TOKEN";
|
|
|
12
13
|
declare const API_BASE_URL_ENV_VAR = "AUTOKAP_API_BASE_URL";
|
|
13
14
|
declare const WS_URL_ENV_VAR = "AUTOKAP_WS_URL";
|
|
14
15
|
declare const ALLOW_UNSAFE_SERVER_ORIGIN_ENV_VAR = "AUTOKAP_ALLOW_UNSAFE_SERVER_ORIGIN";
|
|
16
|
+
declare const EXPORT_DEBUG_LOGS_ENV_VAR = "AUTOKAP_EXPORT_DEBUG_LOGS";
|
|
17
|
+
export declare function resolveExportDebugLogs(stored: boolean | undefined): boolean;
|
|
15
18
|
export declare function getConfigDir(): string;
|
|
16
19
|
export declare function getConfigPath(): string;
|
|
17
20
|
export declare function getDefaultApiBaseUrl(): string;
|
|
@@ -20,4 +23,4 @@ export declare function readConfig(): Promise<AutokapConfig | null>;
|
|
|
20
23
|
export declare function writeConfig(config: AutokapConfig): Promise<void>;
|
|
21
24
|
export declare function deleteConfig(): Promise<void>;
|
|
22
25
|
export declare function requireConfig(): Promise<AutokapConfig>;
|
|
23
|
-
export { DEFAULT_API_BASE_URL, DEFAULT_WS_URL, LOCAL_API_BASE_URL, LOCAL_WS_URL, API_KEY_ENV_VAR, RUN_TOKEN_ENV_VAR, API_BASE_URL_ENV_VAR, WS_URL_ENV_VAR, ALLOW_UNSAFE_SERVER_ORIGIN_ENV_VAR, };
|
|
26
|
+
export { DEFAULT_API_BASE_URL, DEFAULT_WS_URL, LOCAL_API_BASE_URL, LOCAL_WS_URL, API_KEY_ENV_VAR, RUN_TOKEN_ENV_VAR, API_BASE_URL_ENV_VAR, WS_URL_ENV_VAR, ALLOW_UNSAFE_SERVER_ORIGIN_ENV_VAR, EXPORT_DEBUG_LOGS_ENV_VAR, };
|
package/dist/cli-config.js
CHANGED
|
@@ -24,6 +24,15 @@ const RUN_TOKEN_ENV_VAR = 'AUTOKAP_RUN_TOKEN';
|
|
|
24
24
|
const API_BASE_URL_ENV_VAR = 'AUTOKAP_API_BASE_URL';
|
|
25
25
|
const WS_URL_ENV_VAR = 'AUTOKAP_WS_URL';
|
|
26
26
|
const ALLOW_UNSAFE_SERVER_ORIGIN_ENV_VAR = 'AUTOKAP_ALLOW_UNSAFE_SERVER_ORIGIN';
|
|
27
|
+
const EXPORT_DEBUG_LOGS_ENV_VAR = 'AUTOKAP_EXPORT_DEBUG_LOGS';
|
|
28
|
+
export function resolveExportDebugLogs(stored) {
|
|
29
|
+
const envRaw = process.env[EXPORT_DEBUG_LOGS_ENV_VAR]?.trim().toLowerCase();
|
|
30
|
+
if (envRaw === '0' || envRaw === 'false')
|
|
31
|
+
return false;
|
|
32
|
+
if (envRaw === '1' || envRaw === 'true')
|
|
33
|
+
return true;
|
|
34
|
+
return stored !== false;
|
|
35
|
+
}
|
|
27
36
|
export function getConfigDir() {
|
|
28
37
|
return path.join(os.homedir(), '.autokap');
|
|
29
38
|
}
|
|
@@ -82,6 +91,7 @@ export async function readConfig() {
|
|
|
82
91
|
return null;
|
|
83
92
|
const storedApiBaseUrlRaw = typeof record.apiBaseUrl === 'string' ? record.apiBaseUrl : null;
|
|
84
93
|
const storedWsUrlRaw = typeof record.wsUrl === 'string' ? record.wsUrl : null;
|
|
94
|
+
const storedExportDebugLogs = typeof record.exportDebugLogs === 'boolean' ? record.exportDebugLogs : undefined;
|
|
85
95
|
const envApiBaseUrl = normalizeUrl(process.env[API_BASE_URL_ENV_VAR]);
|
|
86
96
|
const envWsUrl = normalizeUrl(process.env[WS_URL_ENV_VAR]);
|
|
87
97
|
const storedApiBaseUrl = normalizeUrl(storedApiBaseUrlRaw) ?? DEFAULT_API_BASE_URL;
|
|
@@ -98,6 +108,7 @@ export async function readConfig() {
|
|
|
98
108
|
apiKey: record.apiKey,
|
|
99
109
|
apiBaseUrl,
|
|
100
110
|
wsUrl,
|
|
111
|
+
exportDebugLogs: storedExportDebugLogs,
|
|
101
112
|
};
|
|
102
113
|
}
|
|
103
114
|
export async function writeConfig(config) {
|
|
@@ -116,6 +127,9 @@ export async function writeConfig(config) {
|
|
|
116
127
|
...config,
|
|
117
128
|
apiBaseUrl: normalizeUrl(config.apiBaseUrl) ?? DEFAULT_API_BASE_URL,
|
|
118
129
|
wsUrl: normalizeUrl(config.wsUrl) ?? deriveWsUrl(config.apiBaseUrl),
|
|
130
|
+
...(typeof config.exportDebugLogs === 'boolean'
|
|
131
|
+
? { exportDebugLogs: config.exportDebugLogs }
|
|
132
|
+
: {}),
|
|
119
133
|
};
|
|
120
134
|
// Atomic write — see packages/core/src/config.ts for the canonical impl.
|
|
121
135
|
const tmpPath = `${configPath}.${process.pid}.${crypto.randomBytes(4).toString('hex')}.tmp`;
|
|
@@ -215,5 +229,5 @@ function assertAllowedApiOrigin(candidateUrl, baselineUrl, envVar) {
|
|
|
215
229
|
}
|
|
216
230
|
throw new Error(`Refusing unsafe server override to ${candidateOrigin}. Set ${ALLOW_UNSAFE_SERVER_ORIGIN_ENV_VAR}=1 to allow ${envVar ?? 'this override'} explicitly.`);
|
|
217
231
|
}
|
|
218
|
-
export { DEFAULT_API_BASE_URL, DEFAULT_WS_URL, LOCAL_API_BASE_URL, LOCAL_WS_URL, API_KEY_ENV_VAR, RUN_TOKEN_ENV_VAR, API_BASE_URL_ENV_VAR, WS_URL_ENV_VAR, ALLOW_UNSAFE_SERVER_ORIGIN_ENV_VAR, };
|
|
232
|
+
export { DEFAULT_API_BASE_URL, DEFAULT_WS_URL, LOCAL_API_BASE_URL, LOCAL_WS_URL, API_KEY_ENV_VAR, RUN_TOKEN_ENV_VAR, API_BASE_URL_ENV_VAR, WS_URL_ENV_VAR, ALLOW_UNSAFE_SERVER_ORIGIN_ENV_VAR, EXPORT_DEBUG_LOGS_ENV_VAR, };
|
|
219
233
|
//# sourceMappingURL=cli-config.js.map
|
package/dist/cli-runner-local.js
CHANGED
|
@@ -33,6 +33,7 @@ export async function runLocal(presetId, opts) {
|
|
|
33
33
|
dryRun: opts.dry,
|
|
34
34
|
regenerateTts: opts.regenerateTts,
|
|
35
35
|
headed: opts.headed,
|
|
36
|
+
exportDebugLogs: opts.exportLogs,
|
|
36
37
|
onProgress: (event) => {
|
|
37
38
|
const prefix = `[capture][${event.variantId}]`;
|
|
38
39
|
switch (event.type) {
|
package/dist/cli-runner.d.ts
CHANGED
|
@@ -45,6 +45,11 @@ export interface CLIRunnerOptions {
|
|
|
45
45
|
abortSignal?: AbortSignal;
|
|
46
46
|
/** Progress callback */
|
|
47
47
|
onProgress?: (event: ProgressEvent) => void;
|
|
48
|
+
/**
|
|
49
|
+
* Override the user/CLI `exportDebugLogs` preference for this run.
|
|
50
|
+
* When `false`, skips the failed-run debug logs export (AUT-149).
|
|
51
|
+
*/
|
|
52
|
+
exportDebugLogs?: boolean;
|
|
48
53
|
}
|
|
49
54
|
export interface CLIRunResult {
|
|
50
55
|
success: boolean;
|
package/dist/cli-runner.js
CHANGED
|
@@ -16,7 +16,7 @@ import path from 'node:path';
|
|
|
16
16
|
import { createHash, randomUUID } from 'node:crypto';
|
|
17
17
|
import sharp from 'sharp';
|
|
18
18
|
import { Browser } from './browser.js';
|
|
19
|
-
import { API_BASE_URL_ENV_VAR, requireConfig } from './cli-config.js';
|
|
19
|
+
import { API_BASE_URL_ENV_VAR, requireConfig, resolveExportDebugLogs } from './cli-config.js';
|
|
20
20
|
import { WebPlaywrightLocal } from './web-playwright-local.js';
|
|
21
21
|
import { executeProgram } from './opcode-runner.js';
|
|
22
22
|
import { ensureChromiumInstalled } from './playwright-installer.js';
|
|
@@ -32,6 +32,8 @@ import { logger } from './logger.js';
|
|
|
32
32
|
import { callLLM } from './llm-provider.js';
|
|
33
33
|
import { APP_VERSION } from './version.js';
|
|
34
34
|
import { normalizeAllowedOrigins, normalizeHttpOrigin, verifySignedExecutionProgramEnvelope, } from './program-signing.js';
|
|
35
|
+
import { redactTelemetryText, redactUrl } from './telemetry-redaction.js';
|
|
36
|
+
import { LogCollector } from './log-collector.js';
|
|
35
37
|
const MAX_CLIP_CAPTURE_DEVICE_SCALE_FACTOR = 1;
|
|
36
38
|
const DEFAULT_VIDEO_DELIVERY_RESOLUTION = { width: 1920, height: 1080 };
|
|
37
39
|
const DEFAULT_VIDEO_CAPTURE_RESOLUTION = DEFAULT_VIDEO_DELIVERY_RESOLUTION;
|
|
@@ -218,6 +220,9 @@ export async function runCapture(options) {
|
|
|
218
220
|
// honors the requested concurrency.
|
|
219
221
|
const isRecordable = program.mediaMode === 'clip' || program.mediaMode === 'video';
|
|
220
222
|
const maxParallelVariants = isRecordable ? 1 : program.maxParallelCaptures;
|
|
223
|
+
// AUT-149: collect structured logs + progress events for export on failure.
|
|
224
|
+
const logCollector = new LogCollector();
|
|
225
|
+
logCollector.start();
|
|
221
226
|
const runOptions = {
|
|
222
227
|
recoveryChain,
|
|
223
228
|
abortSignal: options.abortSignal,
|
|
@@ -226,6 +231,7 @@ export async function runCapture(options) {
|
|
|
226
231
|
presetName: program.presetId,
|
|
227
232
|
dryRun: options.dryRun,
|
|
228
233
|
onProgress: (event) => {
|
|
234
|
+
logCollector.onProgress(event);
|
|
229
235
|
if (!options.onProgress) {
|
|
230
236
|
logProgress(event);
|
|
231
237
|
}
|
|
@@ -286,59 +292,81 @@ export async function runCapture(options) {
|
|
|
286
292
|
await applyPreconditions(browser, program);
|
|
287
293
|
return new WebPlaywrightLocal(browser, recordingDir);
|
|
288
294
|
};
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
logger.info(`[capture] Run completed successfully — ${runResult.telemetry.totalOpcodes} opcodes, ${runResult.telemetry.recoveredOpcodes} recovered, ${runResult.totalDurationMs}ms`);
|
|
292
|
-
}
|
|
293
|
-
else {
|
|
294
|
-
logger.error(`[capture] Run failed: ${runResult.error}`);
|
|
295
|
-
}
|
|
296
|
-
if (options.dryRun) {
|
|
297
|
-
logger.info(`[capture] DRY RUN complete — ${runResult.telemetry.totalOpcodes} opcodes executed, 0 captures, 0 credits charged`);
|
|
298
|
-
return { success: runResult.success, runId, runResult };
|
|
299
|
-
}
|
|
295
|
+
let runResult;
|
|
296
|
+
let cliResult;
|
|
300
297
|
try {
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
if (
|
|
309
|
-
|
|
298
|
+
runResult = await executeProgram(program, createAdapter, runOptions);
|
|
299
|
+
if (runResult.success) {
|
|
300
|
+
logger.info(`[capture] Run completed successfully — ${runResult.telemetry.totalOpcodes} opcodes, ${runResult.telemetry.recoveredOpcodes} recovered, ${runResult.totalDurationMs}ms`);
|
|
301
|
+
}
|
|
302
|
+
else {
|
|
303
|
+
logger.error(`[capture] Run failed: ${runResult.error}`);
|
|
304
|
+
}
|
|
305
|
+
if (options.dryRun) {
|
|
306
|
+
logger.info(`[capture] DRY RUN complete — ${runResult.telemetry.totalOpcodes} opcodes executed, 0 captures, 0 credits charged`);
|
|
307
|
+
cliResult = { success: runResult.success, runId, runResult };
|
|
308
|
+
}
|
|
309
|
+
else {
|
|
310
|
+
try {
|
|
311
|
+
logger.info('[capture] Saving captures, might take a few seconds...');
|
|
312
|
+
options.onProgress?.({
|
|
313
|
+
type: 'upload_start',
|
|
314
|
+
variantId: 'run',
|
|
315
|
+
message: 'saving captures',
|
|
316
|
+
});
|
|
317
|
+
const uploadOutcome = await uploadResults(config, program, runResult, runId);
|
|
318
|
+
if (program.mediaMode === 'video' && runResult.success) {
|
|
319
|
+
await signalVideoComplete(config, program, runResult, uploadOutcome.runId, videoAudioAssets, videoAudioAssetsByLocale);
|
|
320
|
+
}
|
|
321
|
+
const totalDurationSec = ((Date.now() - captureStart) / 1000).toFixed(1);
|
|
322
|
+
logger.info(`[capture] Captures saved successfully — total ${totalDurationSec}s`);
|
|
323
|
+
options.onProgress?.({
|
|
324
|
+
type: 'upload_end',
|
|
325
|
+
variantId: 'run',
|
|
326
|
+
status: 'ok',
|
|
327
|
+
message: 'captures saved',
|
|
328
|
+
});
|
|
329
|
+
cliResult = { success: runResult.success, runId, runResult };
|
|
330
|
+
}
|
|
331
|
+
catch (err) {
|
|
332
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
333
|
+
logger.error(`[capture] Failed to upload results: ${message}`);
|
|
334
|
+
options.onProgress?.({
|
|
335
|
+
type: 'upload_end',
|
|
336
|
+
variantId: 'run',
|
|
337
|
+
status: 'failed',
|
|
338
|
+
message,
|
|
339
|
+
});
|
|
340
|
+
if (!options.allowUploadFailure) {
|
|
341
|
+
cliResult = {
|
|
342
|
+
success: false,
|
|
343
|
+
runId,
|
|
344
|
+
runResult,
|
|
345
|
+
error: runResult.success
|
|
346
|
+
? `upload failed: ${message}`
|
|
347
|
+
: `${runResult.error ?? 'capture failed'}; upload failed: ${message}`,
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
else {
|
|
351
|
+
logger.warn('[capture] Continuing after upload failure because --allow-upload-failure was set');
|
|
352
|
+
cliResult = { success: runResult.success, runId, runResult };
|
|
353
|
+
}
|
|
354
|
+
}
|
|
310
355
|
}
|
|
311
|
-
const totalDurationSec = ((Date.now() - captureStart) / 1000).toFixed(1);
|
|
312
|
-
logger.info(`[capture] Captures saved successfully — total ${totalDurationSec}s`);
|
|
313
|
-
options.onProgress?.({
|
|
314
|
-
type: 'upload_end',
|
|
315
|
-
variantId: 'run',
|
|
316
|
-
status: 'ok',
|
|
317
|
-
message: 'captures saved',
|
|
318
|
-
});
|
|
319
356
|
}
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
options.
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
if (!options.allowUploadFailure) {
|
|
330
|
-
return {
|
|
331
|
-
success: false,
|
|
332
|
-
runId,
|
|
333
|
-
runResult,
|
|
334
|
-
error: runResult.success
|
|
335
|
-
? `upload failed: ${message}`
|
|
336
|
-
: `${runResult.error ?? 'capture failed'}; upload failed: ${message}`,
|
|
337
|
-
};
|
|
357
|
+
finally {
|
|
358
|
+
// AUT-149: export structured debug logs to AutoKap on capture failure.
|
|
359
|
+
// Best-effort — the LogCollector swallows network errors.
|
|
360
|
+
const shouldExport = options.exportDebugLogs !== false
|
|
361
|
+
&& resolveExportDebugLogs(config.exportDebugLogs)
|
|
362
|
+
&& (runResult ? !runResult.success : true);
|
|
363
|
+
if (shouldExport) {
|
|
364
|
+
logger.info('[debug-logs] Exporting debug logs to AutoKap…');
|
|
365
|
+
await logCollector.flushTo(runId, program.presetId, config.apiBaseUrl, config.apiKey, options.env);
|
|
338
366
|
}
|
|
339
|
-
|
|
367
|
+
logCollector.stop();
|
|
340
368
|
}
|
|
341
|
-
return
|
|
369
|
+
return cliResult;
|
|
342
370
|
}
|
|
343
371
|
// ── Server communication ────────────────────────────────────────────
|
|
344
372
|
async function fetchProgram(config, presetId, environmentName) {
|
|
@@ -1388,30 +1416,7 @@ function redactOpcodeForTelemetry(opcode) {
|
|
|
1388
1416
|
}
|
|
1389
1417
|
return base;
|
|
1390
1418
|
}
|
|
1391
|
-
|
|
1392
|
-
if (!value)
|
|
1393
|
-
return value;
|
|
1394
|
-
return value
|
|
1395
|
-
.replace(/\{\{(email|password|loginUrl)\}\}/g, '<credential-placeholder>')
|
|
1396
|
-
.replace(/https?:\/\/[^\s"'<>]+/gi, (match) => redactUrl(match) ?? '<redacted-url>')
|
|
1397
|
-
.replace(/[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/g, '<redacted-email>')
|
|
1398
|
-
.slice(0, 512);
|
|
1399
|
-
}
|
|
1400
|
-
function redactUrl(value) {
|
|
1401
|
-
if (!value)
|
|
1402
|
-
return value;
|
|
1403
|
-
try {
|
|
1404
|
-
const parsed = new URL(value);
|
|
1405
|
-
parsed.username = '';
|
|
1406
|
-
parsed.password = '';
|
|
1407
|
-
parsed.search = parsed.search ? '?<redacted>' : '';
|
|
1408
|
-
parsed.hash = parsed.hash ? '#<redacted>' : '';
|
|
1409
|
-
return parsed.toString();
|
|
1410
|
-
}
|
|
1411
|
-
catch {
|
|
1412
|
-
return value.slice(0, 256);
|
|
1413
|
-
}
|
|
1414
|
-
}
|
|
1419
|
+
// redactTelemetryText / redactUrl moved to ./telemetry-redaction
|
|
1415
1420
|
// ── Progress logging ────────────────────────────────────────────────
|
|
1416
1421
|
function logProgress(event) {
|
|
1417
1422
|
const prefix = `[capture][${event.variantId}]`;
|
package/dist/cli.js
CHANGED
|
@@ -87,6 +87,7 @@ program
|
|
|
87
87
|
.option('--ws-url <url>', `WebSocket server URL (env: ${WS_URL_ENV_VAR})`)
|
|
88
88
|
.action(async (key, opts) => {
|
|
89
89
|
const wsUrl = opts.wsUrl?.trim() || getDefaultWsUrl(opts.apiBaseUrl);
|
|
90
|
+
let exportDebugLogs;
|
|
90
91
|
try {
|
|
91
92
|
const res = await fetch(`${opts.apiBaseUrl}/api/cli/validate`, {
|
|
92
93
|
headers: { Authorization: `Bearer ${key}` },
|
|
@@ -95,6 +96,15 @@ program
|
|
|
95
96
|
logger.error('Invalid API key. Generate one in the AutoKap dashboard.');
|
|
96
97
|
process.exit(1);
|
|
97
98
|
}
|
|
99
|
+
try {
|
|
100
|
+
const body = (await res.json());
|
|
101
|
+
if (typeof body.exportDebugLogs === 'boolean') {
|
|
102
|
+
exportDebugLogs = body.exportDebugLogs;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
// ignore parse failures — preference will fall back to its default
|
|
107
|
+
}
|
|
98
108
|
}
|
|
99
109
|
catch (err) {
|
|
100
110
|
logger.error(`Cannot reach API: ${err.message}`);
|
|
@@ -104,6 +114,7 @@ program
|
|
|
104
114
|
apiKey: key,
|
|
105
115
|
apiBaseUrl: opts.apiBaseUrl,
|
|
106
116
|
wsUrl,
|
|
117
|
+
...(typeof exportDebugLogs === 'boolean' ? { exportDebugLogs } : {}),
|
|
107
118
|
});
|
|
108
119
|
logger.success(`Authenticated. Key stored in ${getConfigPath()}`);
|
|
109
120
|
process.exit(0);
|
|
@@ -166,6 +177,7 @@ program
|
|
|
166
177
|
.option('--dry', 'Dry run: execute all opcodes without capturing or uploading artifacts (0 credits charged)', false)
|
|
167
178
|
.option('--regenerate-tts', 'Force fresh TTS synthesis for video presets — ignore cached audio segments. Reused segments become billable at full rate.', false)
|
|
168
179
|
.option('--debug', 'Verbose logging: per-substep timing, opcode dumps, recovery strategy traces', false)
|
|
180
|
+
.option('--no-export-logs', 'Disable exporting debug logs to AutoKap on capture failure (overrides user/CLI setting)')
|
|
169
181
|
.action(async (presetId, opts) => {
|
|
170
182
|
if (opts.debug) {
|
|
171
183
|
const { setDebugEnabled } = await import('./logger.js');
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { LogEntry } from './logger.js';
|
|
2
|
+
import type { ProgressEvent } from './opcode-runner.js';
|
|
3
|
+
export interface OpcodeContext {
|
|
4
|
+
index: number;
|
|
5
|
+
kind: string;
|
|
6
|
+
targetId?: string;
|
|
7
|
+
}
|
|
8
|
+
export interface ExportedLogEntry extends LogEntry {
|
|
9
|
+
opcode?: OpcodeContext;
|
|
10
|
+
recovery?: {
|
|
11
|
+
strategy: string;
|
|
12
|
+
reason: string;
|
|
13
|
+
succeeded: boolean;
|
|
14
|
+
};
|
|
15
|
+
postcondition?: {
|
|
16
|
+
type: string;
|
|
17
|
+
reason: string;
|
|
18
|
+
};
|
|
19
|
+
meta?: Record<string, unknown>;
|
|
20
|
+
}
|
|
21
|
+
export interface ErrorLogsPayload {
|
|
22
|
+
runId: string;
|
|
23
|
+
presetId: string;
|
|
24
|
+
entries: ExportedLogEntry[];
|
|
25
|
+
envName?: string;
|
|
26
|
+
endedAt: string;
|
|
27
|
+
}
|
|
28
|
+
export declare class LogCollector {
|
|
29
|
+
private entries;
|
|
30
|
+
private previousLogCallback;
|
|
31
|
+
private started;
|
|
32
|
+
private runStartedAt;
|
|
33
|
+
start(): void;
|
|
34
|
+
stop(): void;
|
|
35
|
+
onProgress(event: ProgressEvent): void;
|
|
36
|
+
snapshot(): ExportedLogEntry[];
|
|
37
|
+
size(): number;
|
|
38
|
+
flushTo(runId: string, presetId: string, apiBaseUrl: string, apiKey: string, envName?: string): Promise<{
|
|
39
|
+
ok: boolean;
|
|
40
|
+
status?: number;
|
|
41
|
+
error?: string;
|
|
42
|
+
}>;
|
|
43
|
+
private push;
|
|
44
|
+
private mapLogEntry;
|
|
45
|
+
private captureExistingCallback;
|
|
46
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { setOnLog, getOnLog, logger } from './logger.js';
|
|
2
|
+
import { redactTelemetryText } from './telemetry-redaction.js';
|
|
3
|
+
function redactParams(params) {
|
|
4
|
+
const out = {};
|
|
5
|
+
for (const [key, value] of Object.entries(params)) {
|
|
6
|
+
if (typeof value === 'string') {
|
|
7
|
+
out[key] = redactTelemetryText(value) ?? value;
|
|
8
|
+
}
|
|
9
|
+
else {
|
|
10
|
+
out[key] = value;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
return out;
|
|
14
|
+
}
|
|
15
|
+
const MAX_ENTRIES = 1000;
|
|
16
|
+
const FLUSH_TIMEOUT_MS = 5000;
|
|
17
|
+
export class LogCollector {
|
|
18
|
+
entries = [];
|
|
19
|
+
previousLogCallback = null;
|
|
20
|
+
started = false;
|
|
21
|
+
runStartedAt = null;
|
|
22
|
+
start() {
|
|
23
|
+
if (this.started)
|
|
24
|
+
return;
|
|
25
|
+
this.started = true;
|
|
26
|
+
this.runStartedAt = Date.now();
|
|
27
|
+
this.previousLogCallback = this.captureExistingCallback();
|
|
28
|
+
setOnLog((entry) => {
|
|
29
|
+
this.push(this.mapLogEntry(entry));
|
|
30
|
+
if (this.previousLogCallback)
|
|
31
|
+
this.previousLogCallback(entry);
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
stop() {
|
|
35
|
+
if (!this.started)
|
|
36
|
+
return;
|
|
37
|
+
this.started = false;
|
|
38
|
+
setOnLog(this.previousLogCallback);
|
|
39
|
+
this.previousLogCallback = null;
|
|
40
|
+
}
|
|
41
|
+
onProgress(event) {
|
|
42
|
+
if (!this.started)
|
|
43
|
+
return;
|
|
44
|
+
const level = event.status === 'failed' ? 'error' : event.status === 'recovered' ? 'warn' : 'info';
|
|
45
|
+
const message = redactTelemetryText(event.message) ?? '';
|
|
46
|
+
this.push({
|
|
47
|
+
level: level,
|
|
48
|
+
message,
|
|
49
|
+
timestamp: Date.now(),
|
|
50
|
+
runStartedAt: this.runStartedAt ?? undefined,
|
|
51
|
+
runElapsedMs: this.runStartedAt ? Date.now() - this.runStartedAt : undefined,
|
|
52
|
+
opcode: event.opcodeIndex !== undefined && event.opcodeKind
|
|
53
|
+
? { index: event.opcodeIndex, kind: event.opcodeKind }
|
|
54
|
+
: undefined,
|
|
55
|
+
meta: { progressType: event.type, variantId: event.variantId, status: event.status },
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
snapshot() {
|
|
59
|
+
return [...this.entries];
|
|
60
|
+
}
|
|
61
|
+
size() {
|
|
62
|
+
return this.entries.length;
|
|
63
|
+
}
|
|
64
|
+
async flushTo(runId, presetId, apiBaseUrl, apiKey, envName) {
|
|
65
|
+
if (this.entries.length === 0) {
|
|
66
|
+
return { ok: true, status: 204 };
|
|
67
|
+
}
|
|
68
|
+
const payload = {
|
|
69
|
+
runId,
|
|
70
|
+
presetId,
|
|
71
|
+
entries: this.entries,
|
|
72
|
+
envName,
|
|
73
|
+
endedAt: new Date().toISOString(),
|
|
74
|
+
};
|
|
75
|
+
const controller = new AbortController();
|
|
76
|
+
const timeout = setTimeout(() => controller.abort(), FLUSH_TIMEOUT_MS);
|
|
77
|
+
try {
|
|
78
|
+
const response = await fetch(`${apiBaseUrl}/api/cli/error-logs`, {
|
|
79
|
+
method: 'POST',
|
|
80
|
+
headers: {
|
|
81
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
82
|
+
'Content-Type': 'application/json',
|
|
83
|
+
},
|
|
84
|
+
body: JSON.stringify(payload),
|
|
85
|
+
signal: controller.signal,
|
|
86
|
+
});
|
|
87
|
+
if (!response.ok && response.status !== 204) {
|
|
88
|
+
const text = await response.text().catch(() => '');
|
|
89
|
+
logger.warn(`[debug-logs] Export failed (HTTP ${response.status}): ${text.slice(0, 200)}`);
|
|
90
|
+
return { ok: false, status: response.status, error: text };
|
|
91
|
+
}
|
|
92
|
+
return { ok: true, status: response.status };
|
|
93
|
+
}
|
|
94
|
+
catch (err) {
|
|
95
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
96
|
+
logger.warn(`[debug-logs] Export error: ${message}`);
|
|
97
|
+
return { ok: false, error: message };
|
|
98
|
+
}
|
|
99
|
+
finally {
|
|
100
|
+
clearTimeout(timeout);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
push(entry) {
|
|
104
|
+
this.entries.push(entry);
|
|
105
|
+
if (this.entries.length > MAX_ENTRIES) {
|
|
106
|
+
this.entries.splice(0, this.entries.length - MAX_ENTRIES);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
mapLogEntry(entry) {
|
|
110
|
+
return {
|
|
111
|
+
...entry,
|
|
112
|
+
message: redactTelemetryText(entry.message) ?? entry.message,
|
|
113
|
+
params: entry.params ? redactParams(entry.params) : entry.params,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
captureExistingCallback() {
|
|
117
|
+
return getOnLog();
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
//# sourceMappingURL=log-collector.js.map
|
package/dist/logger.d.ts
CHANGED
|
@@ -29,6 +29,7 @@ export declare function setDebugEnabled(enabled: boolean): void;
|
|
|
29
29
|
export declare function isDebugEnabled(): boolean;
|
|
30
30
|
export declare function runWithLoggerCallbacks<T>(callbacks: LoggerContext, fn: () => Promise<T>): Promise<T>;
|
|
31
31
|
export declare function setOnLog(cb: OnLogCallback | null): void;
|
|
32
|
+
export declare function getOnLog(): OnLogCallback | null;
|
|
32
33
|
export declare function setOnScreenshot(cb: OnScreenshotCallback | null): void;
|
|
33
34
|
export declare function emitScreenshot(base64: string): void;
|
|
34
35
|
export declare function emitReasoningDelta(delta: string, messageId: string): void;
|
package/dist/logger.js
CHANGED
package/dist/mockup.js
CHANGED
|
@@ -5,6 +5,7 @@ import { fileURLToPath } from 'url';
|
|
|
5
5
|
import { renderStatusBarBuffer } from './status-bar-render.js';
|
|
6
6
|
import { generateBrowserBarSvg } from './browser-bar.js';
|
|
7
7
|
import { computeMockupLayout } from './mockup-html.js';
|
|
8
|
+
import { getSfProResvgFontFiles } from './sf-pro-resvg-fonts.js';
|
|
8
9
|
import { logger } from './logger.js';
|
|
9
10
|
const __filename = fileURLToPath(import.meta.url);
|
|
10
11
|
const __dirname = path.dirname(__filename);
|
|
@@ -415,11 +416,27 @@ export async function getDeviceFrame(id) {
|
|
|
415
416
|
}
|
|
416
417
|
// ── Sharp Compositing Helpers ──────────────────────────────────────────
|
|
417
418
|
/** Rasterize an SVG string to a PNG buffer using resvg-js.
|
|
418
|
-
* Resolves external image references (e.g. favicon URLs) before rendering.
|
|
419
|
+
* Resolves external image references (e.g. favicon URLs) before rendering.
|
|
420
|
+
*
|
|
421
|
+
* Font handling: Resvg-js 2.6.2 does NOT read fonts from `@font-face url(data:...)`
|
|
422
|
+
* declarations embedded inside the SVG. They MUST be supplied via `font.fontFiles`
|
|
423
|
+
* with paths to on-disk TTF/OTF files. Without this, browser-bar text falls back
|
|
424
|
+
* to Linux system fonts (DejaVu/Liberation) on Vercel, producing the wrong glyph
|
|
425
|
+
* shapes that the user calls "SF Pro fonts don't load". `getSfProResvgFontFiles`
|
|
426
|
+
* decompresses the SF Pro woff2 base64 data URIs to TTF in `os.tmpdir()` and
|
|
427
|
+
* caches the paths for the lifetime of the function instance.
|
|
428
|
+
* `loadSystemFonts: false` keeps the render deterministic across OSes — a font
|
|
429
|
+
* load failure produces blank text (loud) instead of fallback (silent). */
|
|
419
430
|
async function rasterizeSvg(svg, width) {
|
|
420
431
|
const { Resvg } = await import('@resvg/resvg-js');
|
|
432
|
+
const fontFiles = await getSfProResvgFontFiles();
|
|
421
433
|
const opts = {
|
|
422
434
|
fitTo: { mode: 'width', value: width },
|
|
435
|
+
font: {
|
|
436
|
+
fontFiles,
|
|
437
|
+
loadSystemFonts: false,
|
|
438
|
+
defaultFontFamily: 'SF Pro Text',
|
|
439
|
+
},
|
|
423
440
|
};
|
|
424
441
|
const resvg = new Resvg(svg, opts);
|
|
425
442
|
// Resolve external image references (e.g. <image href="https://..."/>)
|
|
@@ -17,10 +17,13 @@ const FONT_CSS_HTML = `<style>
|
|
|
17
17
|
@font-face{font-family:'SF Pro Text';src:local('SF Pro Text'),local('.SFNSText'),url('${SF_PRO_TEXT_REGULAR}') format('woff2');font-weight:400;font-style:normal}
|
|
18
18
|
@font-face{font-family:'SF Pro Text';src:local('SF Pro Text Semibold'),local('.SFNSText-Semibold'),url('${SF_PRO_TEXT_SEMIBOLD}') format('woff2');font-weight:600;font-style:normal}
|
|
19
19
|
</style>`;
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
20
|
+
// SVG output: fonts are NOT embedded inline. Resvg-js 2.6.2 does not honor
|
|
21
|
+
// `@font-face url(data:font/woff2;base64,…)` declarations inside SVG style
|
|
22
|
+
// blocks; fonts must instead be supplied via `font.fontFiles` to the Resvg
|
|
23
|
+
// constructor. The browser-bar pipeline (mockup.ts::rasterizeSvg) loads SF
|
|
24
|
+
// Pro Text from disk via `sf-pro-resvg-fonts.ts`. The `font-family` hint on
|
|
25
|
+
// the URL <text> element below remains for any consumer that DOES honor
|
|
26
|
+
// CSS @font-face (a future Resvg release, a different SVG renderer, etc.).
|
|
24
27
|
const FF = "'SF Pro Text',-apple-system,BlinkMacSystemFont,system-ui,sans-serif";
|
|
25
28
|
// ── Helpers ──────────────────────────────────────────────────────────────
|
|
26
29
|
function escText(s) {
|
|
@@ -88,7 +91,6 @@ export function generateSafariBrowserBarSvg(options) {
|
|
|
88
91
|
const inner = buildSafariSvgInner(url, isDark);
|
|
89
92
|
const vb = SAFARI_TOOLBAR_VIEWBOX;
|
|
90
93
|
return `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="${width}" height="${height}" viewBox="${vb.x} ${vb.y} ${vb.width} ${vb.height}" preserveAspectRatio="none" fill="none">
|
|
91
|
-
<defs><style>${FONT_CSS_SVG}</style></defs>
|
|
92
94
|
${inner}
|
|
93
95
|
</svg>`;
|
|
94
96
|
}
|
|
@@ -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';
|
|
2
|
+
export type CreditUsageType = 'screenshot' | 'clip' | 'video' | 'cloud_recapture' | 'ai_chat' | 'studio_creation' | 'studio_iteration' | 'error_analysis';
|
|
3
3
|
export declare function recordCreditUsage(supabase: SupabaseClient, params: {
|
|
4
4
|
userId: string;
|
|
5
5
|
projectId: string | null;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SF Pro font files on disk for @resvg/resvg-js consumption.
|
|
3
|
+
*
|
|
4
|
+
* Why this exists: Resvg-js 2.6.2 cannot load fonts from in-memory buffers,
|
|
5
|
+
* and it does NOT honor `@font-face url('data:font/woff2;base64,…')`
|
|
6
|
+
* declarations inside an SVG `<defs><style>` block. It only reads fonts
|
|
7
|
+
* from on-disk files (`fontFiles`) or directories (`fontDirs`).
|
|
8
|
+
*
|
|
9
|
+
* The browser-bar rendering pipeline (`generateBrowserBarSvg` →
|
|
10
|
+
* `rasterizeSvg` in mockup.ts) used to rely on the SVG <defs><style>
|
|
11
|
+
* @font-face approach. That silently fell back to system fonts
|
|
12
|
+
* (DejaVu/Liberation on Vercel Linux) — empirically verified by rendering
|
|
13
|
+
* the same SVG with and without `font.fontFiles` and observing the output
|
|
14
|
+
* change only when `fontFiles` is provided.
|
|
15
|
+
*
|
|
16
|
+
* This module decompresses the SF Pro Text Regular/Semibold woff2 data URIs
|
|
17
|
+
* from `sf-pro-fonts.ts` to TTF and writes them to `os.tmpdir()`, returning
|
|
18
|
+
* the file paths so callers can pass them to `new Resvg(svg, { font:
|
|
19
|
+
* { fontFiles: paths, loadSystemFonts: false } })`.
|
|
20
|
+
*
|
|
21
|
+
* Caching: paths are memoized per-process. On Vercel Fluid Compute the
|
|
22
|
+
* instance is reused across requests so the woff2→ttf decompression and
|
|
23
|
+
* disk writes happen at most once per cold start (~200ms total).
|
|
24
|
+
*
|
|
25
|
+
* Concurrency: two requests arriving before the first decompression
|
|
26
|
+
* finishes share a single in-flight Promise (no double work, no
|
|
27
|
+
* partial-file races).
|
|
28
|
+
*
|
|
29
|
+
* Robustness: if /tmp gets wiped between requests on the same instance
|
|
30
|
+
* (rare but possible), `fs.access` detects the missing files and a fresh
|
|
31
|
+
* init re-writes them.
|
|
32
|
+
*
|
|
33
|
+
* Windows-safe: uses `os.tmpdir()` + `path.join` (no hard-coded /tmp/),
|
|
34
|
+
* `fs.chmod` is intentionally skipped (no-op on NTFS anyway), no bash-isms.
|
|
35
|
+
*/
|
|
36
|
+
/**
|
|
37
|
+
* Returns the absolute paths to SF Pro Text Regular + Semibold TTF files on
|
|
38
|
+
* disk, suitable for `font.fontFiles` on a `new Resvg(svg, { … })` call.
|
|
39
|
+
*
|
|
40
|
+
* First call: decompresses woff2 → TTF and writes to `os.tmpdir()`. Subsequent
|
|
41
|
+
* calls: returns cached paths in O(2) `fs.access` checks (skipped after a
|
|
42
|
+
* single confirmed lookup per process — see implementation).
|
|
43
|
+
*/
|
|
44
|
+
export declare function getSfProResvgFontFiles(): Promise<string[]>;
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SF Pro font files on disk for @resvg/resvg-js consumption.
|
|
3
|
+
*
|
|
4
|
+
* Why this exists: Resvg-js 2.6.2 cannot load fonts from in-memory buffers,
|
|
5
|
+
* and it does NOT honor `@font-face url('data:font/woff2;base64,…')`
|
|
6
|
+
* declarations inside an SVG `<defs><style>` block. It only reads fonts
|
|
7
|
+
* from on-disk files (`fontFiles`) or directories (`fontDirs`).
|
|
8
|
+
*
|
|
9
|
+
* The browser-bar rendering pipeline (`generateBrowserBarSvg` →
|
|
10
|
+
* `rasterizeSvg` in mockup.ts) used to rely on the SVG <defs><style>
|
|
11
|
+
* @font-face approach. That silently fell back to system fonts
|
|
12
|
+
* (DejaVu/Liberation on Vercel Linux) — empirically verified by rendering
|
|
13
|
+
* the same SVG with and without `font.fontFiles` and observing the output
|
|
14
|
+
* change only when `fontFiles` is provided.
|
|
15
|
+
*
|
|
16
|
+
* This module decompresses the SF Pro Text Regular/Semibold woff2 data URIs
|
|
17
|
+
* from `sf-pro-fonts.ts` to TTF and writes them to `os.tmpdir()`, returning
|
|
18
|
+
* the file paths so callers can pass them to `new Resvg(svg, { font:
|
|
19
|
+
* { fontFiles: paths, loadSystemFonts: false } })`.
|
|
20
|
+
*
|
|
21
|
+
* Caching: paths are memoized per-process. On Vercel Fluid Compute the
|
|
22
|
+
* instance is reused across requests so the woff2→ttf decompression and
|
|
23
|
+
* disk writes happen at most once per cold start (~200ms total).
|
|
24
|
+
*
|
|
25
|
+
* Concurrency: two requests arriving before the first decompression
|
|
26
|
+
* finishes share a single in-flight Promise (no double work, no
|
|
27
|
+
* partial-file races).
|
|
28
|
+
*
|
|
29
|
+
* Robustness: if /tmp gets wiped between requests on the same instance
|
|
30
|
+
* (rare but possible), `fs.access` detects the missing files and a fresh
|
|
31
|
+
* init re-writes them.
|
|
32
|
+
*
|
|
33
|
+
* Windows-safe: uses `os.tmpdir()` + `path.join` (no hard-coded /tmp/),
|
|
34
|
+
* `fs.chmod` is intentionally skipped (no-op on NTFS anyway), no bash-isms.
|
|
35
|
+
*/
|
|
36
|
+
import fs from 'node:fs/promises';
|
|
37
|
+
import os from 'node:os';
|
|
38
|
+
import path from 'node:path';
|
|
39
|
+
import { SF_PRO_TEXT_REGULAR, SF_PRO_TEXT_SEMIBOLD } from './sf-pro-fonts.js';
|
|
40
|
+
const FONT_DIR_NAME = 'autokap-sf-pro';
|
|
41
|
+
// Browser bar (Chrome + Safari chrome) uses only SF Pro Text Regular/Semibold.
|
|
42
|
+
// Other variants (Display, Symbols) are consumed by the Satori-based status
|
|
43
|
+
// bar renderer which already handles font embedding through its own
|
|
44
|
+
// in-memory buffer pipeline (see `status-bar-render.ts::getSatoriFonts`).
|
|
45
|
+
const FONTS = [
|
|
46
|
+
{ filename: 'SF-Pro-Text-Regular.ttf', dataUri: SF_PRO_TEXT_REGULAR },
|
|
47
|
+
{ filename: 'SF-Pro-Text-Semibold.ttf', dataUri: SF_PRO_TEXT_SEMIBOLD },
|
|
48
|
+
];
|
|
49
|
+
let cachedPaths = null;
|
|
50
|
+
let pendingInit = null;
|
|
51
|
+
async function decompressWoff2DataUri(dataUri) {
|
|
52
|
+
const raw = dataUri.replace(/^data:font\/woff2;base64,/, '');
|
|
53
|
+
const woff2 = Buffer.from(raw, 'base64');
|
|
54
|
+
const { decompress } = await import('wawoff2');
|
|
55
|
+
return Buffer.from(await decompress(woff2));
|
|
56
|
+
}
|
|
57
|
+
async function fileExists(p) {
|
|
58
|
+
try {
|
|
59
|
+
await fs.access(p);
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Write a single TTF file atomically. If a concurrent call (same process or
|
|
68
|
+
* a sibling instance sharing the tmpdir) already produced the final file,
|
|
69
|
+
* the second `rename` overwrites identical bytes — still safe.
|
|
70
|
+
*
|
|
71
|
+
* The `.${pid}.partial` suffix prevents Resvg from reading a half-written
|
|
72
|
+
* file if it loads paths while a write is in flight.
|
|
73
|
+
*/
|
|
74
|
+
async function ensureFontFile(targetPath, dataUri) {
|
|
75
|
+
if (await fileExists(targetPath))
|
|
76
|
+
return;
|
|
77
|
+
const ttf = await decompressWoff2DataUri(dataUri);
|
|
78
|
+
const tmpPath = `${targetPath}.${process.pid}.partial`;
|
|
79
|
+
await fs.writeFile(tmpPath, ttf);
|
|
80
|
+
try {
|
|
81
|
+
await fs.rename(tmpPath, targetPath);
|
|
82
|
+
}
|
|
83
|
+
catch (err) {
|
|
84
|
+
// Best-effort cleanup of the .partial if rename failed for some reason
|
|
85
|
+
// (e.g. cross-device rename on exotic mounts). Swallow — the rename
|
|
86
|
+
// error is what matters and will surface to the caller.
|
|
87
|
+
await fs.rm(tmpPath, { force: true }).catch(() => { });
|
|
88
|
+
throw err;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
async function initFontFiles() {
|
|
92
|
+
const dir = path.join(os.tmpdir(), FONT_DIR_NAME);
|
|
93
|
+
await fs.mkdir(dir, { recursive: true });
|
|
94
|
+
const paths = FONTS.map((f) => path.join(dir, f.filename));
|
|
95
|
+
await Promise.all(FONTS.map((f, i) => ensureFontFile(paths[i], f.dataUri)));
|
|
96
|
+
return paths;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Returns the absolute paths to SF Pro Text Regular + Semibold TTF files on
|
|
100
|
+
* disk, suitable for `font.fontFiles` on a `new Resvg(svg, { … })` call.
|
|
101
|
+
*
|
|
102
|
+
* First call: decompresses woff2 → TTF and writes to `os.tmpdir()`. Subsequent
|
|
103
|
+
* calls: returns cached paths in O(2) `fs.access` checks (skipped after a
|
|
104
|
+
* single confirmed lookup per process — see implementation).
|
|
105
|
+
*/
|
|
106
|
+
export async function getSfProResvgFontFiles() {
|
|
107
|
+
if (cachedPaths) {
|
|
108
|
+
// Defensive: ensure /tmp wasn't wiped under us.
|
|
109
|
+
const allExist = await Promise.all(cachedPaths.map(fileExists));
|
|
110
|
+
if (allExist.every(Boolean))
|
|
111
|
+
return cachedPaths;
|
|
112
|
+
cachedPaths = null;
|
|
113
|
+
pendingInit = null;
|
|
114
|
+
}
|
|
115
|
+
if (!pendingInit) {
|
|
116
|
+
pendingInit = initFontFiles().then((paths) => {
|
|
117
|
+
cachedPaths = paths;
|
|
118
|
+
return paths;
|
|
119
|
+
}, (err) => {
|
|
120
|
+
pendingInit = null;
|
|
121
|
+
throw err;
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
return pendingInit;
|
|
125
|
+
}
|
|
126
|
+
//# sourceMappingURL=sf-pro-resvg-fonts.js.map
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export function redactUrl(value) {
|
|
2
|
+
if (!value)
|
|
3
|
+
return value;
|
|
4
|
+
try {
|
|
5
|
+
const parsed = new URL(value);
|
|
6
|
+
parsed.username = '';
|
|
7
|
+
parsed.password = '';
|
|
8
|
+
parsed.search = parsed.search ? '?<redacted>' : '';
|
|
9
|
+
parsed.hash = parsed.hash ? '#<redacted>' : '';
|
|
10
|
+
return parsed.toString();
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return value.slice(0, 256);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export function redactTelemetryText(value) {
|
|
17
|
+
if (!value)
|
|
18
|
+
return value;
|
|
19
|
+
return value
|
|
20
|
+
.replace(/\{\{(email|password|loginUrl)\}\}/g, '<credential-placeholder>')
|
|
21
|
+
.replace(/https?:\/\/[^\s"'<>]+/gi, (match) => redactUrl(match) ?? '<redacted-url>')
|
|
22
|
+
.replace(/[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/g, '<redacted-email>')
|
|
23
|
+
.slice(0, 512);
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=telemetry-redaction.js.map
|