flowstride 1.0.20-rc.2 → 1.0.20
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/NATIVE_CI.md +88 -145
- package/README.md +46 -221
- package/dist/adapters/web/playwright.d.ts +4 -1
- package/dist/adapters/web/playwright.js +75 -6
- package/dist/ci/config.js +6 -5
- package/dist/ci/runtime.js +27 -14
- package/dist/cli/commands/run.js +2 -1
- package/dist/cli/index.js +35 -50
- package/dist/cli/manual_server/workspace_server.js +44 -32
- package/dist/cli/utils/cloudSession.d.ts +7 -0
- package/dist/cli/utils/cloudSession.js +37 -0
- package/dist/cli/utils/proInstaller.d.ts +5 -0
- package/dist/cli/utils/proInstaller.js +141 -0
- package/dist/game/sdk.js +3 -2
- package/dist/index.d.ts +5 -0
- package/dist/index.js +9 -0
- package/dist/manual/security.d.ts +183 -0
- package/dist/manual/security.js +106 -0
- package/dist/runner/data_generators.d.ts +1 -0
- package/dist/runner/data_generators.js +3 -0
- package/dist/runner/dom_stripper.js +3 -0
- package/dist/runner/flow_orchestrator.js +13 -22
- package/dist/runner/flow_worker.d.ts +1 -0
- package/dist/runner/flow_worker.js +68 -47
- package/dist/runner/persistent_vault.js +2 -1
- package/dist/runner/plugin_manager.js +3 -1
- package/dist/security/localDashboard.d.ts +2 -0
- package/dist/security/localDashboard.js +22 -0
- package/dist/security/redaction.d.ts +4 -0
- package/dist/security/redaction.js +51 -0
- package/dist/sessions/manager.js +3 -1
- package/dist/types/index.d.ts +9 -0
- package/dist/utils/cloud.d.ts +6 -0
- package/dist/utils/cloud.js +33 -0
- package/dist/utils/projectPaths.d.ts +11 -0
- package/dist/utils/projectPaths.js +90 -0
- package/dist/utils/replayRequest.d.ts +8 -0
- package/dist/utils/replayRequest.js +77 -0
- package/package.json +23 -14
- package/src/ui/dist/assets/index-B7MuBTWm.js +460 -0
- package/src/ui/dist/index.html +1 -1
- package/RELEASE_NOTES.md +0 -40
- package/src/ui/dist/assets/index-DIC22ZK_.js +0 -638
package/dist/types/index.d.ts
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
export interface FlowstrideConfig {
|
|
2
2
|
ci?: boolean;
|
|
3
|
+
flowRoot?: string;
|
|
4
|
+
projectRoot?: string;
|
|
5
|
+
cloudSyncResult?: {
|
|
6
|
+
reporting: string;
|
|
7
|
+
video: string;
|
|
8
|
+
error?: unknown;
|
|
9
|
+
};
|
|
3
10
|
device?: 'desktop' | 'phone' | 'landscape';
|
|
4
11
|
viewport?: {
|
|
5
12
|
width: number;
|
|
@@ -46,6 +53,8 @@ export interface SessionData {
|
|
|
46
53
|
tokens: Record<string, any>;
|
|
47
54
|
cookies?: any[];
|
|
48
55
|
localStorage?: Record<string, string>;
|
|
56
|
+
sessionStorage?: Record<string, string>;
|
|
57
|
+
storageOrigin?: string;
|
|
49
58
|
createdAt: number;
|
|
50
59
|
lastUsedAt: number;
|
|
51
60
|
persist: boolean;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.cloudApiBase = cloudApiBase;
|
|
4
|
+
exports.cloudFailure = cloudFailure;
|
|
5
|
+
const redaction_1 = require("../security/redaction");
|
|
6
|
+
function cloudApiBase(env = process.env) {
|
|
7
|
+
const value = env.FLOWSTRIDE_API_URL || env.NEXTJS_WEBHOOK_URL || 'https://cloud.flowstride.io';
|
|
8
|
+
const url = new URL(value);
|
|
9
|
+
if (!['http:', 'https:'].includes(url.protocol) || url.username || url.password || url.search || url.hash)
|
|
10
|
+
throw new Error('Cloud URL must be an HTTP(S) URL without credentials or query parameters.');
|
|
11
|
+
const pathname = url.pathname.replace(/\/api\/cloud\/execution-complete\/?$/, '').replace(/\/+$/, '');
|
|
12
|
+
return url.origin + pathname;
|
|
13
|
+
}
|
|
14
|
+
function cloudFailure(error, secrets = []) {
|
|
15
|
+
const status = Number(error?.response?.status) || undefined;
|
|
16
|
+
const code = typeof error?.response?.data?.code === 'string' ? error.response.data.code.slice(0, 80) : typeof error?.code === 'string' ? error.code.slice(0, 80) : undefined;
|
|
17
|
+
const authenticationMessages = {
|
|
18
|
+
TOKEN_EXPIRED: 'This developer token has expired. Generate a replacement; logging in with the same key does not renew it.',
|
|
19
|
+
TOKEN_REVOKED: 'This developer token has been revoked. Use a replacement token.',
|
|
20
|
+
TOKEN_INVALID: 'Cloud did not recognize a valid developer token. Check the credential source and Cloud URL.',
|
|
21
|
+
TOKEN_REQUIRED: 'Cloud requires a Bearer developer token.',
|
|
22
|
+
TOKEN_SCOPE_DENIED: 'This token requires execution or admin scope for CLI runs.',
|
|
23
|
+
};
|
|
24
|
+
const message = (status === 401 || status === 403) && code && authenticationMessages[code] ? authenticationMessages[code]
|
|
25
|
+
: status === 401 ? 'Cloud rejected authentication. Check the selected token and Cloud URL; this response does not confirm expiration.'
|
|
26
|
+
: status === 403 ? 'Workspace permission or feature policy rejected this request.'
|
|
27
|
+
: status === 413 ? 'The Cloud/proxy upload limit was exceeded; evidence remains local.'
|
|
28
|
+
: status === 429 ? 'The Cloud request rate limit was reached.'
|
|
29
|
+
: status && status >= 500 ? 'Cloud service failed; evidence remains local.'
|
|
30
|
+
: status === 400 ? 'Cloud rejected the report schema; verify matching CLI and Cloud releases.'
|
|
31
|
+
: 'Cloud could not be reached; verify the configured URL and network connection.';
|
|
32
|
+
return { status, code: code ? (0, redaction_1.redactText)(code, secrets) : undefined, message };
|
|
33
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export declare function projectPath(root: string, target: string): string;
|
|
2
|
+
export declare function discoverFlows(target: string, projectRoot?: string): string[];
|
|
3
|
+
export declare function flowIdentity(file: string, root?: string): string;
|
|
4
|
+
export declare function videoFileName(identity: string): string;
|
|
5
|
+
export declare function selectedFeatures<T extends {
|
|
6
|
+
scenarios: {
|
|
7
|
+
isOnly?: boolean;
|
|
8
|
+
steps: unknown[];
|
|
9
|
+
}[];
|
|
10
|
+
}>(features: T[]): T[];
|
|
11
|
+
export declare function artifactPrefix(identity?: string): string;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.projectPath = projectPath;
|
|
7
|
+
exports.discoverFlows = discoverFlows;
|
|
8
|
+
exports.flowIdentity = flowIdentity;
|
|
9
|
+
exports.videoFileName = videoFileName;
|
|
10
|
+
exports.selectedFeatures = selectedFeatures;
|
|
11
|
+
exports.artifactPrefix = artifactPrefix;
|
|
12
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
13
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
14
|
+
const node_crypto_1 = require("node:crypto");
|
|
15
|
+
function projectPath(root, target) {
|
|
16
|
+
root = node_path_1.default.resolve(root);
|
|
17
|
+
target = node_path_1.default.resolve(root, target);
|
|
18
|
+
const inside = (base, value) => {
|
|
19
|
+
const relative = node_path_1.default.relative(base, value);
|
|
20
|
+
return relative !== '..' && !relative.startsWith(`..${node_path_1.default.sep}`) && !node_path_1.default.isAbsolute(relative);
|
|
21
|
+
};
|
|
22
|
+
if (!inside(root, target))
|
|
23
|
+
throw new Error(`Path must remain inside the project: ${target}`);
|
|
24
|
+
const realRoot = node_fs_1.default.realpathSync(root);
|
|
25
|
+
let parent = target;
|
|
26
|
+
const suffix = [];
|
|
27
|
+
while (true) {
|
|
28
|
+
try {
|
|
29
|
+
node_fs_1.default.lstatSync(parent);
|
|
30
|
+
break;
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
if (error.code !== 'ENOENT')
|
|
34
|
+
throw error;
|
|
35
|
+
suffix.unshift(node_path_1.default.basename(parent));
|
|
36
|
+
const next = node_path_1.default.dirname(parent);
|
|
37
|
+
if (next === parent)
|
|
38
|
+
throw error;
|
|
39
|
+
parent = next;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
const real = node_path_1.default.join(node_fs_1.default.realpathSync(parent), ...suffix);
|
|
43
|
+
if (!inside(realRoot, real))
|
|
44
|
+
throw new Error(`Symlink escapes the project: ${target}`);
|
|
45
|
+
return target;
|
|
46
|
+
}
|
|
47
|
+
function discoverFlows(target, projectRoot = process.cwd()) {
|
|
48
|
+
const files = [];
|
|
49
|
+
const visited = new Set();
|
|
50
|
+
const visit = (item) => {
|
|
51
|
+
item = projectPath(projectRoot, item);
|
|
52
|
+
const real = node_fs_1.default.realpathSync(item);
|
|
53
|
+
if (visited.has(real))
|
|
54
|
+
return;
|
|
55
|
+
visited.add(real);
|
|
56
|
+
const stat = node_fs_1.default.statSync(item);
|
|
57
|
+
if (stat.isFile()) {
|
|
58
|
+
if (item.endsWith('.flow'))
|
|
59
|
+
files.push(item);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (!stat.isDirectory())
|
|
63
|
+
return;
|
|
64
|
+
for (const entry of node_fs_1.default.readdirSync(item, { withFileTypes: true }).sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0)) {
|
|
65
|
+
if (['node_modules', '.git', '.flowstride', 'flowstride-artifacts'].includes(entry.name))
|
|
66
|
+
continue;
|
|
67
|
+
visit(node_path_1.default.join(item, entry.name));
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
visit(target);
|
|
71
|
+
return files.sort((a, b) => Number(!a.endsWith('.first.flow')) - Number(!b.endsWith('.first.flow')) || (a < b ? -1 : a > b ? 1 : 0));
|
|
72
|
+
}
|
|
73
|
+
function flowIdentity(file, root) { return (root ? node_path_1.default.relative(root, file) : node_path_1.default.basename(file)).split(node_path_1.default.sep).join('/'); }
|
|
74
|
+
function videoFileName(identity) {
|
|
75
|
+
if (!identity.includes('/') && identity.length < 180 && !identity.includes('%'))
|
|
76
|
+
return identity.replace(/\.flow$/, '') + '.webm';
|
|
77
|
+
const base = node_path_1.default.posix.basename(identity, '.flow').replace(/[^a-zA-Z0-9_.-]/g, '_').slice(0, 60);
|
|
78
|
+
return `${base}-${(0, node_crypto_1.createHash)('sha256').update(identity).digest('hex').slice(0, 16)}.webm`;
|
|
79
|
+
}
|
|
80
|
+
function selectedFeatures(features) {
|
|
81
|
+
if (!features.some(f => f.scenarios.some(s => s.isOnly)))
|
|
82
|
+
return features;
|
|
83
|
+
return features.map(f => ({ ...f, scenarios: f.scenarios.filter(s => s.isOnly) }));
|
|
84
|
+
}
|
|
85
|
+
function artifactPrefix(identity) {
|
|
86
|
+
if (!identity)
|
|
87
|
+
return '';
|
|
88
|
+
const safe = identity.replace(/[^a-zA-Z0-9_.-]/g, '_').slice(0, 100);
|
|
89
|
+
return safe + (identity.includes('/') || identity.length > 100 ? '-' + (0, node_crypto_1.createHash)('sha256').update(identity).digest('hex').slice(0, 12) : '') + '-';
|
|
90
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export declare class ReplayRequestError extends Error {
|
|
2
|
+
constructor(message: string);
|
|
3
|
+
}
|
|
4
|
+
/** Browser protocol metadata is not a portable set of Fetch request headers. */
|
|
5
|
+
export declare function buildReplayRequest(payload: any): {
|
|
6
|
+
url: string;
|
|
7
|
+
options: RequestInit;
|
|
8
|
+
};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ReplayRequestError = void 0;
|
|
4
|
+
exports.buildReplayRequest = buildReplayRequest;
|
|
5
|
+
class ReplayRequestError extends Error {
|
|
6
|
+
constructor(message) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.name = 'ReplayRequestError';
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
exports.ReplayRequestError = ReplayRequestError;
|
|
12
|
+
/** Browser protocol metadata is not a portable set of Fetch request headers. */
|
|
13
|
+
function buildReplayRequest(payload) {
|
|
14
|
+
let url;
|
|
15
|
+
try {
|
|
16
|
+
url = new URL(payload?.url);
|
|
17
|
+
if (!['http:', 'https:'].includes(url.protocol) || url.username || url.password)
|
|
18
|
+
throw new Error();
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
throw new ReplayRequestError('Replay URL must use HTTP or HTTPS without embedded credentials.');
|
|
22
|
+
}
|
|
23
|
+
const method = typeof payload?.method === 'string' ? payload.method.toUpperCase() : 'GET';
|
|
24
|
+
if (!/^[!#$%&'*+.^_`|~0-9A-Z-]+$/.test(method) || ['CONNECT', 'TRACE', 'TRACK'].includes(method)) {
|
|
25
|
+
throw new ReplayRequestError('Replay method is not supported.');
|
|
26
|
+
}
|
|
27
|
+
let supplied = payload?.headers;
|
|
28
|
+
if (typeof supplied === 'string') {
|
|
29
|
+
try {
|
|
30
|
+
supplied = supplied.trim() ? JSON.parse(supplied) : {};
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
throw new ReplayRequestError('Replay headers must be a valid JSON object.');
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
if (supplied === undefined)
|
|
37
|
+
supplied = {};
|
|
38
|
+
if (!supplied || typeof supplied !== 'object' || Array.isArray(supplied)) {
|
|
39
|
+
throw new ReplayRequestError('Replay headers must be a JSON object of string values.');
|
|
40
|
+
}
|
|
41
|
+
// Fetch owns the destination, framing and supported compression. Recompute
|
|
42
|
+
// Content-Length after edits and remove all Connection-nominated fields too.
|
|
43
|
+
const excluded = new Set([
|
|
44
|
+
'host', 'content-length', 'accept-encoding', 'connection', 'keep-alive',
|
|
45
|
+
'proxy-connection', 'proxy-authenticate', 'proxy-authorization', 'te',
|
|
46
|
+
'trailer', 'transfer-encoding', 'upgrade', 'expect',
|
|
47
|
+
]);
|
|
48
|
+
for (const [name, value] of Object.entries(supplied)) {
|
|
49
|
+
if (name.toLowerCase() === 'connection' && typeof value === 'string') {
|
|
50
|
+
for (const field of value.split(','))
|
|
51
|
+
excluded.add(field.trim().toLowerCase());
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
const headers = new Headers();
|
|
55
|
+
for (const [name, value] of Object.entries(supplied)) {
|
|
56
|
+
if (name.startsWith(':') || excluded.has(name.toLowerCase()))
|
|
57
|
+
continue;
|
|
58
|
+
if (typeof value !== 'string')
|
|
59
|
+
throw new ReplayRequestError('Replay header values must be strings.');
|
|
60
|
+
try {
|
|
61
|
+
headers.append(name, value);
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
throw new ReplayRequestError('Replay headers contain an invalid name or value.');
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
const options = { method, headers };
|
|
68
|
+
if (!['GET', 'HEAD'].includes(method) && payload?.body !== undefined && payload.body !== null) {
|
|
69
|
+
try {
|
|
70
|
+
options.body = typeof payload.body === 'string' ? payload.body : JSON.stringify(payload.body);
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
throw new ReplayRequestError('Replay body cannot be serialized.');
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return { url: url.href, options };
|
|
77
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "flowstride",
|
|
3
|
-
"version": "1.0.20
|
|
3
|
+
"version": "1.0.20",
|
|
4
4
|
"description": "An opensource flowfirst test automation framework",
|
|
5
5
|
"workspaces": [
|
|
6
6
|
"packages/flowstride_report"
|
|
@@ -11,8 +11,7 @@
|
|
|
11
11
|
"dist",
|
|
12
12
|
"src/ui/dist",
|
|
13
13
|
"NATIVE_CI.md",
|
|
14
|
-
"examples/native-game"
|
|
15
|
-
"RELEASE_NOTES.md"
|
|
14
|
+
"examples/native-game"
|
|
16
15
|
],
|
|
17
16
|
"bin": {
|
|
18
17
|
"flowstride": "dist/cli/index.js"
|
|
@@ -20,9 +19,18 @@
|
|
|
20
19
|
"scripts": {
|
|
21
20
|
"build": "tsc",
|
|
22
21
|
"flowstride": "node ./dist/cli/index.js",
|
|
23
|
-
"test": "
|
|
24
|
-
"prepublishOnly": "npm run build",
|
|
25
|
-
"test:native": "node --test --test-concurrency=1 tests/native/*.test.cjs"
|
|
22
|
+
"test": "npm run test:native && npm run test:manual-security && npm run test:dashboard",
|
|
23
|
+
"prepublishOnly": "npm run build:candidate && npm run verify:candidate",
|
|
24
|
+
"test:native": "node --test --test-concurrency=1 tests/native/*.test.cjs",
|
|
25
|
+
"test:manual-security": "node --test tests/manual-security.test.cjs",
|
|
26
|
+
"build:candidate": "node scripts/build-candidate.cjs",
|
|
27
|
+
"verify:candidate": "node scripts/verify-candidate.cjs",
|
|
28
|
+
"test:dashboard:unit": "node --test tests/dashboard/store.test.cjs",
|
|
29
|
+
"test:dashboard": "node --test --test-concurrency=1 tests/dashboard/*.test.cjs",
|
|
30
|
+
"test:unit": "node --test tests/native/core.test.cjs tests/native/combined.test.cjs tests/native/curl.test.cjs tests/native/upload.test.cjs tests/dashboard/store.test.cjs tests/native/replay.test.cjs tests/dashboard/replay.test.cjs tests/native/assertions-sessions.test.cjs",
|
|
31
|
+
"test:security": "node --test tests/manual-security.test.cjs tests/native/curl.test.cjs tests/native/combined.test.cjs tests/native/replay.test.cjs tests/native/assertions-sessions.test.cjs",
|
|
32
|
+
"test:replay": "node --test tests/native/replay.test.cjs tests/dashboard/replay.test.cjs tests/dashboard/curl.browser.test.cjs",
|
|
33
|
+
"test:assertions-sessions": "node --test --test-concurrency=1 tests/native/assertions-sessions*.test.cjs"
|
|
26
34
|
},
|
|
27
35
|
"keywords": [
|
|
28
36
|
"automation",
|
|
@@ -37,7 +45,7 @@
|
|
|
37
45
|
},
|
|
38
46
|
"dependencies": {
|
|
39
47
|
"@inquirer/prompts": "^7.10.1",
|
|
40
|
-
"axios": "^1.
|
|
48
|
+
"axios": "^1.20.0",
|
|
41
49
|
"chalk": "^4.1.2",
|
|
42
50
|
"commander": "^11.0.0",
|
|
43
51
|
"dotenv": "^17.4.2",
|
|
@@ -46,22 +54,23 @@
|
|
|
46
54
|
"playwright": "1.59.1",
|
|
47
55
|
"playwright-extra": "^4.3.6",
|
|
48
56
|
"puppeteer-extra-plugin-stealth": "^2.11.2",
|
|
49
|
-
"tar": "^7.5.
|
|
57
|
+
"tar": "^7.5.22",
|
|
50
58
|
"ts-node": "^10.9.2",
|
|
51
|
-
"undici": "^7.
|
|
52
|
-
"ws": "^8.
|
|
59
|
+
"undici": "^7.30.1",
|
|
60
|
+
"ws": "^8.22.0",
|
|
61
|
+
"sanitize-html": "2.18.1",
|
|
62
|
+
"zod": "4.4.3"
|
|
53
63
|
},
|
|
54
64
|
"devDependencies": {
|
|
55
65
|
"@types/node": "^25.9.1",
|
|
56
66
|
"@types/ws": "^8.18.1",
|
|
57
|
-
"typescript": "^5.9.3"
|
|
67
|
+
"typescript": "^5.9.3",
|
|
68
|
+
"@types/sanitize-html": "2.16.2"
|
|
58
69
|
},
|
|
59
70
|
"optionalDependencies": {
|
|
60
71
|
"@rolldown/binding-darwin-arm64": "^0.15.1"
|
|
61
72
|
},
|
|
62
73
|
"publishConfig": {
|
|
63
|
-
"tag": "
|
|
64
|
-
"access": "public",
|
|
65
|
-
"registry": "https://registry.npmjs.org"
|
|
74
|
+
"tag": "latest"
|
|
66
75
|
}
|
|
67
76
|
}
|