flowstride 1.0.20 → 1.0.22

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.
Files changed (99) hide show
  1. package/NATIVE_BROWSER.md +381 -0
  2. package/NATIVE_CI.md +17 -5
  3. package/NATIVE_WORKFLOWS.md +44 -0
  4. package/README.md +11 -4
  5. package/dist/adapters/api/undici.d.ts +11 -1
  6. package/dist/adapters/api/undici.js +28 -3
  7. package/dist/adapters/web/playwright.d.ts +22 -2
  8. package/dist/adapters/web/playwright.js +254 -107
  9. package/dist/ci/config.js +14 -1
  10. package/dist/ci/distribution.d.ts +38 -0
  11. package/dist/ci/distribution.js +134 -0
  12. package/dist/ci/init.d.ts +4 -1
  13. package/dist/ci/init.js +13 -2
  14. package/dist/ci/runtime.js +33 -6
  15. package/dist/cli/commands/run.js +17 -4
  16. package/dist/cli/index.js +24 -5
  17. package/dist/cli/manual_server/workspace_server.js +90 -74
  18. package/dist/manual/security.d.ts +6 -6
  19. package/dist/native/accessibility.d.ts +4 -0
  20. package/dist/native/accessibility.js +84 -0
  21. package/dist/native/assertions.d.ts +3 -0
  22. package/dist/native/assertions.js +223 -0
  23. package/dist/native/compatibility.d.ts +3 -0
  24. package/dist/native/compatibility.js +16 -0
  25. package/dist/native/components.d.ts +12 -0
  26. package/dist/native/components.js +122 -0
  27. package/dist/native/dashboard.d.ts +7 -0
  28. package/dist/native/dashboard.js +32 -0
  29. package/dist/native/deadline.d.ts +2 -0
  30. package/dist/native/deadline.js +13 -0
  31. package/dist/native/events.d.ts +35 -0
  32. package/dist/native/events.js +308 -0
  33. package/dist/native/evidence.d.ts +3 -0
  34. package/dist/native/evidence.js +31 -0
  35. package/dist/native/functions.d.ts +10 -0
  36. package/dist/native/functions.js +131 -0
  37. package/dist/native/runtime.d.ts +17 -0
  38. package/dist/native/runtime.js +160 -0
  39. package/dist/native/validation.d.ts +107 -0
  40. package/dist/native/validation.js +84 -0
  41. package/dist/parser/assertion_contract.d.ts +12 -0
  42. package/dist/parser/assertion_contract.js +56 -0
  43. package/dist/parser/lexer.js +15 -4
  44. package/dist/parser/parser.js +15 -7
  45. package/dist/reporting/assertion_result.d.ts +36 -0
  46. package/dist/reporting/assertion_result.js +104 -0
  47. package/dist/reporting/pdf_generator.js +25 -20
  48. package/dist/reporting/runtime_failure.d.ts +10 -0
  49. package/dist/reporting/runtime_failure.js +64 -0
  50. package/dist/reporting/step_status.d.ts +10 -0
  51. package/dist/reporting/step_status.js +44 -0
  52. package/dist/runner/flow_orchestrator.d.ts +5 -0
  53. package/dist/runner/flow_orchestrator.js +30 -8
  54. package/dist/runner/flow_worker.d.ts +15 -1
  55. package/dist/runner/flow_worker.js +219 -42
  56. package/dist/runner/plugin_manager.js +3 -0
  57. package/dist/runner/temporary_store.d.ts +1 -0
  58. package/dist/runner/temporary_store.js +3 -0
  59. package/dist/sdk/api_proxy.d.ts +3 -0
  60. package/dist/sdk/api_proxy.js +16 -0
  61. package/dist/security/localWorkspace.d.ts +2 -0
  62. package/dist/security/localWorkspace.js +17 -0
  63. package/dist/security/redaction.js +1 -1
  64. package/dist/types/index.d.ts +13 -0
  65. package/examples/native-workflows/README.md +144 -0
  66. package/examples/native-workflows/api-and-ui/README.md +80 -0
  67. package/examples/native-workflows/api-and-ui/flows/api-and-ui.flow +27 -0
  68. package/examples/native-workflows/api-and-ui/negative/wrong-backend-state.flow +27 -0
  69. package/examples/native-workflows/api-ui-otp/README.md +140 -0
  70. package/examples/native-workflows/api-ui-otp/templates/api-ui-otp.flow +31 -0
  71. package/examples/native-workflows/api-ui-otp/templates/existing-account-otp.flow +20 -0
  72. package/examples/native-workflows/audio-to-text/README.md +131 -0
  73. package/examples/native-workflows/audio-to-text/fixtures/microphone-tone.wav +0 -0
  74. package/examples/native-workflows/audio-to-text/flows/microphone-demo.flow +23 -0
  75. package/examples/native-workflows/audio-to-text/negative/wrong-transcript.flow +23 -0
  76. package/examples/native-workflows/audio-to-text/templates/real-transcription.flow +25 -0
  77. package/examples/native-workflows/iframes/README.md +116 -0
  78. package/examples/native-workflows/iframes/flows/explicit-frame-plugin.flow +20 -0
  79. package/examples/native-workflows/iframes/flows/readable-frame.flow +13 -0
  80. package/examples/native-workflows/iframes/negative/wrong-frame-result.flow +13 -0
  81. package/examples/native-workflows/iframes/plugins/frame-actions.ts +7 -0
  82. package/examples/native-workflows/manual-testing/README.md +85 -0
  83. package/examples/native-workflows/manual-testing/fixtures/profile-review-mismatch.json +93 -0
  84. package/examples/native-workflows/manual-testing/fixtures/profile-review.json +93 -0
  85. package/examples/native-workflows/shared/server.mjs +80 -0
  86. package/examples/native-workflows/tables-and-cells/README.md +118 -0
  87. package/examples/native-workflows/tables-and-cells/flows/coordinates.flow +17 -0
  88. package/examples/native-workflows/tables-and-cells/flows/search-extract-reuse.flow +27 -0
  89. package/examples/native-workflows/tables-and-cells/negative/wrong-cell-value.flow +27 -0
  90. package/examples/native-workflows/variables-and-reuse/README.md +140 -0
  91. package/examples/native-workflows/variables-and-reuse/flows/reuse.flow +9 -0
  92. package/examples/native-workflows/variables-and-reuse/flows/setup.first.flow +27 -0
  93. package/examples/native-workflows/variables-and-reuse/negative/wrong-reused-value.flow +27 -0
  94. package/examples/native-workflows/variables-and-reuse/sessions/restore.flow +10 -0
  95. package/examples/native-workflows/variables-and-reuse/sessions/save.flow +29 -0
  96. package/package.json +23 -9
  97. package/src/ui/dist/assets/index-s0WYkGPt.js +636 -0
  98. package/src/ui/dist/index.html +1 -1
  99. package/src/ui/dist/assets/index-B7MuBTWm.js +0 -460
@@ -0,0 +1,160 @@
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.NativeRuntime = void 0;
7
+ const zod_1 = require("zod");
8
+ const node_fs_1 = __importDefault(require("node:fs"));
9
+ const node_crypto_1 = require("node:crypto");
10
+ const validation_1 = require("./validation");
11
+ const projectPaths_1 = require("../utils/projectPaths");
12
+ const events_1 = require("./events");
13
+ const functions_1 = require("./functions");
14
+ const components_1 = require("./components");
15
+ const assertions_1 = require("./assertions");
16
+ const accessibility_1 = require("./accessibility");
17
+ class NativeRuntime {
18
+ web;
19
+ config;
20
+ events;
21
+ lastAssertion;
22
+ assertionGeneration = 0;
23
+ functions = new functions_1.NativeFunctions();
24
+ components;
25
+ constructor(web, config) {
26
+ this.web = web;
27
+ this.config = config;
28
+ this.events = new events_1.NativeEvents(web, config);
29
+ this.components = new components_1.NativeComponents(config);
30
+ }
31
+ async execute(command, input = {}) {
32
+ this.lastAssertion = undefined;
33
+ const generation = ++this.assertionGeneration;
34
+ const report = result => { if (generation === this.assertionGeneration)
35
+ this.lastAssertion = result; };
36
+ const [area, action, extra] = command.split('.');
37
+ if (!action || extra)
38
+ throw new Error(`Unknown native command: flow.${command}`);
39
+ if (area === 'browser') {
40
+ if (action === 'create') {
41
+ const data = zod_1.z.object({ name: validation_1.name, options: zod_1.z.unknown().optional() }).strict().parse(input);
42
+ (0, validation_1.validateContextOptions)(data.options, this.config.browser, this.config.projectRoot);
43
+ await this.web.createProfile(data.name, data.options);
44
+ return;
45
+ }
46
+ if (action === 'use' || action === 'close') {
47
+ const data = zod_1.z.object({ name: validation_1.name }).strict().parse(input);
48
+ if (action === 'use')
49
+ await this.web.useProfile(data.name);
50
+ else {
51
+ if (data.name === 'default')
52
+ throw new Error('The default profile stays available until file completion.');
53
+ await this.events.disposeProfile(data.name);
54
+ await this.web.closeProfile(data.name);
55
+ }
56
+ return;
57
+ }
58
+ if (action === 'list') {
59
+ zod_1.z.object({}).strict().parse(input);
60
+ return this.web.profileNames();
61
+ }
62
+ if (action === 'offline') {
63
+ const data = zod_1.z.object({ value: zod_1.z.boolean() }).strict().parse(input);
64
+ await this.web.getContext().setOffline(data.value);
65
+ return;
66
+ }
67
+ if (action === 'permissions') {
68
+ const data = zod_1.z.object({ permissions: zod_1.z.array(zod_1.z.string()), origin: zod_1.z.string().url().optional() }).strict().parse(input);
69
+ await this.web.getContext().grantPermissions(data.permissions, { origin: data.origin });
70
+ return;
71
+ }
72
+ if (action === 'clearPermissions') {
73
+ zod_1.z.object({}).strict().parse(input);
74
+ await this.web.getContext().clearPermissions();
75
+ return;
76
+ }
77
+ if (action === 'geolocation') {
78
+ const data = zod_1.z.object({ latitude: zod_1.z.number().min(-90).max(90), longitude: zod_1.z.number().min(-180).max(180), accuracy: zod_1.z.number().nonnegative().optional() }).strict().parse(input);
79
+ await this.web.getContext().setGeolocation(data);
80
+ return;
81
+ }
82
+ throw new Error(`Unknown browser action: ${action}`);
83
+ }
84
+ if (area === 'assert') {
85
+ if (action !== 'that')
86
+ throw new Error(`Unknown assertion action: ${action}`);
87
+ return (0, assertions_1.assertBrowser)(this.web.getPage(), input, this.config.timeout, report);
88
+ }
89
+ if (area === 'network')
90
+ return this.events.network(action, input, report);
91
+ if (area === 'download')
92
+ return this.events.download(action, input, report);
93
+ if (area === 'stub')
94
+ return this.functions.execute(this.web.getPage(), action, input, this.config.timeout, report);
95
+ if (area === 'component')
96
+ return this.components.execute(this.web.getPage(), action, input);
97
+ if (area === 'a11y')
98
+ return (0, accessibility_1.accessibility)(this.web.getPage(), this.config, action, input, report);
99
+ if (area === 'clock') {
100
+ const clock = this.web.getPage().clock;
101
+ if (action === 'install') {
102
+ const data = zod_1.z.object({ time: zod_1.z.union([zod_1.z.string(), zod_1.z.number()]).optional() }).strict().parse(input);
103
+ await clock.install(data);
104
+ return;
105
+ }
106
+ if (action === 'resume') {
107
+ zod_1.z.object({}).strict().parse(input);
108
+ await clock.resume();
109
+ return;
110
+ }
111
+ if (['pauseAt', 'setFixedTime', 'setSystemTime'].includes(action)) {
112
+ const data = zod_1.z.object({ time: zod_1.z.union([zod_1.z.string().min(1), zod_1.z.number().finite()]) }).strict().parse(input);
113
+ if (typeof data.time === 'string' && !Number.isFinite(Date.parse(data.time)))
114
+ throw new Error('Clock time must be a valid date.');
115
+ await clock[action](data.time);
116
+ return;
117
+ }
118
+ if (['runFor', 'fastForward'].includes(action)) {
119
+ const data = zod_1.z.object({ milliseconds: zod_1.z.number().int().min(0).max(86400000) }).strict().parse(input);
120
+ await clock[action](data.milliseconds);
121
+ return;
122
+ }
123
+ throw new Error(`Unknown clock action: ${action}`);
124
+ }
125
+ if (area === 'trace') {
126
+ if (action === 'start') {
127
+ const data = zod_1.z.object({ name: validation_1.name.optional() }).strict().parse(input);
128
+ await this.web.beginTrace(undefined, data.name ? `${data.name}-trace.zip` : undefined);
129
+ return;
130
+ }
131
+ if (action === 'stop') {
132
+ zod_1.z.object({}).strict().parse(input);
133
+ return this.web.endTrace();
134
+ }
135
+ throw new Error(`Unknown trace action: ${action}`);
136
+ }
137
+ if (area === 'har') {
138
+ if (action === 'replay') {
139
+ const data = zod_1.z.object({ file: zod_1.z.string().min(1), url: zod_1.z.string().optional(), notFound: zod_1.z.enum(['abort', 'fallback']).default('abort') }).strict().parse(input);
140
+ (0, validation_1.readProjectFile)(this.config, data.file, 100 * 1024 * 1024);
141
+ await this.web.getContext().routeFromHAR((0, projectPaths_1.projectPath)(this.config.projectRoot || process.cwd(), data.file), { url: data.url, notFound: data.notFound, update: false });
142
+ return;
143
+ }
144
+ throw new Error(`Unknown HAR action: ${action}. Enable har in configuration before browser creation to record.`);
145
+ }
146
+ throw new Error(`Unknown native command: flow.${command}`);
147
+ }
148
+ async captureDOM(step) {
149
+ if (!this.config.domSnapshots)
150
+ return;
151
+ const page = this.web.getPage();
152
+ // Saved HTML is diagnostic data. It is never injected into an executable frame.
153
+ const html = await page.content();
154
+ const file = (0, validation_1.evidencePath)(this.config, `${validation_1.name.parse(step)}-${(0, node_crypto_1.randomUUID)()}.dom.html`);
155
+ node_fs_1.default.writeFileSync(file, html, { mode: 0o600 });
156
+ return file;
157
+ }
158
+ async dispose() { this.assertionGeneration++; this.lastAssertion = undefined; await this.events.dispose(); await this.functions.dispose(); await this.components.dispose(); }
159
+ }
160
+ exports.NativeRuntime = NativeRuntime;
@@ -0,0 +1,107 @@
1
+ import { z } from 'zod';
2
+ import type { BrowserContextOptions } from 'playwright';
3
+ import type { FlowstrideConfig } from '../types';
4
+ export declare const name: z.ZodString;
5
+ export declare const positive: z.ZodNumber;
6
+ export declare const headers: z.ZodRecord<z.ZodString, z.ZodString>;
7
+ export declare const contextOptions: z.ZodObject<{
8
+ acceptDownloads: z.ZodOptional<z.ZodBoolean>;
9
+ strictSelectors: z.ZodOptional<z.ZodBoolean>;
10
+ clientCertificates: z.ZodOptional<z.ZodArray<z.ZodObject<{
11
+ origin: z.ZodString;
12
+ certPath: z.ZodOptional<z.ZodString>;
13
+ keyPath: z.ZodOptional<z.ZodString>;
14
+ pfxPath: z.ZodOptional<z.ZodString>;
15
+ passphrase: z.ZodOptional<z.ZodString>;
16
+ }, z.core.$strict>>>;
17
+ storageState: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
18
+ cookies: z.ZodArray<z.ZodObject<{
19
+ name: z.ZodString;
20
+ value: z.ZodString;
21
+ domain: z.ZodString;
22
+ path: z.ZodString;
23
+ expires: z.ZodNumber;
24
+ httpOnly: z.ZodBoolean;
25
+ secure: z.ZodBoolean;
26
+ sameSite: z.ZodEnum<{
27
+ Strict: "Strict";
28
+ Lax: "Lax";
29
+ None: "None";
30
+ }>;
31
+ }, z.core.$strict>>;
32
+ origins: z.ZodArray<z.ZodObject<{
33
+ origin: z.ZodString;
34
+ localStorage: z.ZodArray<z.ZodObject<{
35
+ name: z.ZodString;
36
+ value: z.ZodString;
37
+ }, z.core.$strict>>;
38
+ indexedDB: z.ZodOptional<z.ZodArray<z.ZodUnknown>>;
39
+ }, z.core.$strict>>;
40
+ }, z.core.$strict>]>>;
41
+ baseURL: z.ZodOptional<z.ZodString>;
42
+ bypassCSP: z.ZodOptional<z.ZodBoolean>;
43
+ colorScheme: z.ZodOptional<z.ZodNullable<z.ZodEnum<{
44
+ light: "light";
45
+ dark: "dark";
46
+ "no-preference": "no-preference";
47
+ }>>>;
48
+ contrast: z.ZodOptional<z.ZodNullable<z.ZodEnum<{
49
+ "no-preference": "no-preference";
50
+ more: "more";
51
+ }>>>;
52
+ deviceScaleFactor: z.ZodOptional<z.ZodNumber>;
53
+ extraHTTPHeaders: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
54
+ forcedColors: z.ZodOptional<z.ZodNullable<z.ZodEnum<{
55
+ none: "none";
56
+ active: "active";
57
+ }>>>;
58
+ hasTouch: z.ZodOptional<z.ZodBoolean>;
59
+ httpCredentials: z.ZodOptional<z.ZodObject<{
60
+ username: z.ZodString;
61
+ password: z.ZodString;
62
+ origin: z.ZodOptional<z.ZodString>;
63
+ send: z.ZodOptional<z.ZodEnum<{
64
+ always: "always";
65
+ unauthorized: "unauthorized";
66
+ }>>;
67
+ }, z.core.$strict>>;
68
+ geolocation: z.ZodOptional<z.ZodObject<{
69
+ latitude: z.ZodNumber;
70
+ longitude: z.ZodNumber;
71
+ accuracy: z.ZodOptional<z.ZodNumber>;
72
+ }, z.core.$strict>>;
73
+ ignoreHTTPSErrors: z.ZodOptional<z.ZodBoolean>;
74
+ isMobile: z.ZodOptional<z.ZodBoolean>;
75
+ javaScriptEnabled: z.ZodOptional<z.ZodBoolean>;
76
+ locale: z.ZodOptional<z.ZodString>;
77
+ offline: z.ZodOptional<z.ZodBoolean>;
78
+ permissions: z.ZodOptional<z.ZodArray<z.ZodString>>;
79
+ proxy: z.ZodOptional<z.ZodObject<{
80
+ server: z.ZodString;
81
+ bypass: z.ZodOptional<z.ZodString>;
82
+ username: z.ZodOptional<z.ZodString>;
83
+ password: z.ZodOptional<z.ZodString>;
84
+ }, z.core.$strict>>;
85
+ reducedMotion: z.ZodOptional<z.ZodNullable<z.ZodEnum<{
86
+ reduce: "reduce";
87
+ "no-preference": "no-preference";
88
+ }>>>;
89
+ screen: z.ZodOptional<z.ZodObject<{
90
+ width: z.ZodNumber;
91
+ height: z.ZodNumber;
92
+ }, z.core.$strict>>;
93
+ serviceWorkers: z.ZodOptional<z.ZodEnum<{
94
+ allow: "allow";
95
+ block: "block";
96
+ }>>;
97
+ timezoneId: z.ZodOptional<z.ZodString>;
98
+ userAgent: z.ZodOptional<z.ZodString>;
99
+ viewport: z.ZodOptional<z.ZodNullable<z.ZodObject<{
100
+ width: z.ZodNumber;
101
+ height: z.ZodNumber;
102
+ }, z.core.$strict>>>;
103
+ }, z.core.$strict>;
104
+ export declare function validateContextOptions(value: unknown, browser?: string, root?: string): BrowserContextOptions;
105
+ export declare function evidencePath(config: FlowstrideConfig, file: string): string;
106
+ export declare function readProjectFile(config: FlowstrideConfig, file: string, maxSize?: number): Buffer;
107
+ export declare function poll(read: () => Promise<boolean>, timeout: number, label: string): Promise<void>;
@@ -0,0 +1,84 @@
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.contextOptions = exports.headers = exports.positive = exports.name = void 0;
7
+ exports.validateContextOptions = validateContextOptions;
8
+ exports.evidencePath = evidencePath;
9
+ exports.readProjectFile = readProjectFile;
10
+ exports.poll = poll;
11
+ const node_fs_1 = __importDefault(require("node:fs"));
12
+ const node_path_1 = __importDefault(require("node:path"));
13
+ const zod_1 = require("zod");
14
+ const projectPaths_1 = require("../utils/projectPaths");
15
+ exports.name = zod_1.z.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9_.-]{0,79}$/, 'Use a simple name without path separators.');
16
+ exports.positive = zod_1.z.number().int().min(1).max(2147483647);
17
+ exports.headers = zod_1.z.record(zod_1.z.string(), zod_1.z.string());
18
+ const size = zod_1.z.object({ width: exports.positive, height: exports.positive }).strict();
19
+ // File-bearing options are handled explicitly; evidence paths cannot bypass project containment.
20
+ exports.contextOptions = zod_1.z.object({
21
+ acceptDownloads: zod_1.z.boolean().optional(), strictSelectors: zod_1.z.boolean().optional(),
22
+ clientCertificates: zod_1.z.array(zod_1.z.object({ origin: zod_1.z.string().url(), certPath: zod_1.z.string().optional(), keyPath: zod_1.z.string().optional(), pfxPath: zod_1.z.string().optional(), passphrase: zod_1.z.string().optional() }).strict()).optional(),
23
+ storageState: zod_1.z.union([zod_1.z.string(), zod_1.z.object({ cookies: zod_1.z.array(zod_1.z.object({ name: zod_1.z.string(), value: zod_1.z.string(), domain: zod_1.z.string(), path: zod_1.z.string(), expires: zod_1.z.number(), httpOnly: zod_1.z.boolean(), secure: zod_1.z.boolean(), sameSite: zod_1.z.enum(['Strict', 'Lax', 'None']) }).strict()), origins: zod_1.z.array(zod_1.z.object({ origin: zod_1.z.string().url(), localStorage: zod_1.z.array(zod_1.z.object({ name: zod_1.z.string(), value: zod_1.z.string() }).strict()), indexedDB: zod_1.z.array(zod_1.z.unknown()).optional() }).strict()) }).strict()]).optional(),
24
+ baseURL: zod_1.z.string().url().optional(),
25
+ bypassCSP: zod_1.z.boolean().optional(), colorScheme: zod_1.z.enum(['light', 'dark', 'no-preference']).nullable().optional(),
26
+ contrast: zod_1.z.enum(['no-preference', 'more']).nullable().optional(),
27
+ deviceScaleFactor: zod_1.z.number().positive().optional(), extraHTTPHeaders: exports.headers.optional(),
28
+ forcedColors: zod_1.z.enum(['active', 'none']).nullable().optional(), hasTouch: zod_1.z.boolean().optional(),
29
+ httpCredentials: zod_1.z.object({ username: zod_1.z.string(), password: zod_1.z.string(), origin: zod_1.z.string().url().optional(), send: zod_1.z.enum(['always', 'unauthorized']).optional() }).strict().optional(),
30
+ geolocation: zod_1.z.object({ latitude: zod_1.z.number().min(-90).max(90), longitude: zod_1.z.number().min(-180).max(180), accuracy: zod_1.z.number().nonnegative().optional() }).strict().optional(),
31
+ ignoreHTTPSErrors: zod_1.z.boolean().optional(), isMobile: zod_1.z.boolean().optional(), javaScriptEnabled: zod_1.z.boolean().optional(),
32
+ locale: zod_1.z.string().min(1).optional(), offline: zod_1.z.boolean().optional(), permissions: zod_1.z.array(zod_1.z.string()).optional(),
33
+ proxy: zod_1.z.object({ server: zod_1.z.string().min(1), bypass: zod_1.z.string().optional(), username: zod_1.z.string().optional(), password: zod_1.z.string().optional() }).strict().optional(),
34
+ reducedMotion: zod_1.z.enum(['reduce', 'no-preference']).nullable().optional(), screen: size.optional(),
35
+ serviceWorkers: zod_1.z.enum(['allow', 'block']).optional(), timezoneId: zod_1.z.string().optional(),
36
+ userAgent: zod_1.z.string().optional(), viewport: size.nullable().optional(),
37
+ }).strict();
38
+ function validateContextOptions(value, browser = 'chromium', root = process.cwd()) {
39
+ const parsed = exports.contextOptions.parse(value ?? {});
40
+ if (typeof parsed.storageState === 'string')
41
+ parsed.storageState = (0, projectPaths_1.projectPath)(root, parsed.storageState);
42
+ for (const certificate of parsed.clientCertificates || [])
43
+ for (const key of ['certPath', 'keyPath', 'pfxPath'])
44
+ if (certificate[key])
45
+ certificate[key] = (0, projectPaths_1.projectPath)(root, certificate[key]);
46
+ if (browser === 'firefox' && parsed.isMobile)
47
+ throw new Error('Firefox does not support mobile emulation; use viewport and touch options instead.');
48
+ return parsed;
49
+ }
50
+ function evidencePath(config, file) {
51
+ const safe = exports.name.parse(file);
52
+ const root = config.projectRoot || process.cwd();
53
+ const dir = (0, projectPaths_1.projectPath)(root, config.artifactsDir || 'flowstride-artifacts');
54
+ node_fs_1.default.mkdirSync(dir, { recursive: true, mode: 0o700 });
55
+ return (0, projectPaths_1.projectPath)(root, node_path_1.default.join(dir, (0, projectPaths_1.artifactPrefix)(config.testFile) + safe));
56
+ }
57
+ function readProjectFile(config, file, maxSize = 20 * 1024 * 1024) {
58
+ const absolute = (0, projectPaths_1.projectPath)(config.projectRoot || process.cwd(), file);
59
+ if (!node_fs_1.default.statSync(absolute).isFile() || node_fs_1.default.statSync(absolute).size > maxSize)
60
+ throw new Error('Fixture must be a regular project file within the size limit.');
61
+ return node_fs_1.default.readFileSync(absolute);
62
+ }
63
+ async function poll(read, timeout, label) {
64
+ const deadline = Date.now() + exports.positive.parse(timeout);
65
+ let last;
66
+ for (;;) {
67
+ const remaining = deadline - Date.now();
68
+ if (remaining <= 0)
69
+ throw new Error(`Assertion timed out: ${label}`, { cause: last });
70
+ let timer;
71
+ try {
72
+ const ok = await Promise.race([read(), new Promise((_, reject) => { timer = setTimeout(() => reject(new Error('Read timed out')), remaining); })]);
73
+ if (ok)
74
+ return;
75
+ }
76
+ catch (error) {
77
+ last = error;
78
+ }
79
+ finally {
80
+ clearTimeout(timer);
81
+ }
82
+ await new Promise(resolve => setTimeout(resolve, Math.min(50, Math.max(0, deadline - Date.now()))));
83
+ }
84
+ }
@@ -0,0 +1,12 @@
1
+ export declare const expectationTypes: Set<string>;
2
+ export declare const apiAssertionConditions: Set<string>;
3
+ export declare function validateExpectationType(type: unknown): void;
4
+ /** Templates are checked after interpolation; literal thresholds fail at parse time. */
5
+ export declare function validateResponseTimeThreshold(value: unknown, allowTemplate?: boolean): number | undefined;
6
+ export declare function validateApiAssertions(assertions: unknown): asserts assertions is {
7
+ condition: string;
8
+ value: string;
9
+ }[];
10
+ /** And remains limited to assertions and existing custom assertion plugins. */
11
+ export declare const nativeAssertionCommands: Set<string>;
12
+ export declare function isAssertionStep(action: string, payload: any): boolean;
@@ -0,0 +1,56 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.nativeAssertionCommands = exports.apiAssertionConditions = exports.expectationTypes = void 0;
4
+ exports.validateExpectationType = validateExpectationType;
5
+ exports.validateResponseTimeThreshold = validateResponseTimeThreshold;
6
+ exports.validateApiAssertions = validateApiAssertions;
7
+ exports.isAssertionStep = isAssertionStep;
8
+ exports.expectationTypes = new Set([
9
+ 'status', 'responseTime', 'resBody', 'resHeader', 'cookie',
10
+ 'visible', 'text', 'value', 'attribute', 'transcript',
11
+ ]);
12
+ exports.apiAssertionConditions = new Set([
13
+ 'equals', 'contains', 'to.be', 'to.not.be', 'is', 'is.not',
14
+ 'type.of', 'format', 'matches', 'lessthan', 'greaterthan',
15
+ 'length', 'length.greaterthan', 'length.lessthan',
16
+ 'includes', 'does.not.include', 'has.key', 'does.not.have.key',
17
+ 'is.empty', 'not.empty',
18
+ ]);
19
+ function validateExpectationType(type) {
20
+ if (typeof type !== 'string' || !exports.expectationTypes.has(type)) {
21
+ throw new Error(`Unsupported flow.expect modifier: ${String(type)}.`);
22
+ }
23
+ }
24
+ /** Templates are checked after interpolation; literal thresholds fail at parse time. */
25
+ function validateResponseTimeThreshold(value, allowTemplate = false) {
26
+ if (allowTemplate && typeof value === 'string' &&
27
+ /\{\{env\.[^}]+\}\}|(?:@|\$)(?:global\[[^\]]+\]|[a-zA-Z_]\w*)/.test(value))
28
+ return;
29
+ if ((typeof value !== 'string' && typeof value !== 'number') ||
30
+ (typeof value === 'string' && value.trim() === '')) {
31
+ throw new Error('API responseTime threshold must be a finite nonnegative number.');
32
+ }
33
+ const threshold = Number(value);
34
+ if (!Number.isFinite(threshold) || threshold < 0) {
35
+ throw new Error('API responseTime threshold must be a finite nonnegative number.');
36
+ }
37
+ return threshold;
38
+ }
39
+ function validateApiAssertions(assertions) {
40
+ if (!Array.isArray(assertions) || assertions.length === 0) {
41
+ throw new Error('API assertion requires at least one supported condition.');
42
+ }
43
+ for (const assertion of assertions) {
44
+ if (!assertion || !exports.apiAssertionConditions.has(assertion.condition)) {
45
+ throw new Error(`Unsupported API assertion condition: ${String(assertion?.condition)}.`);
46
+ }
47
+ if (!['is.empty', 'not.empty'].includes(assertion.condition) && typeof assertion.value !== 'string') {
48
+ throw new Error(`API assertion ${assertion.condition} requires an expected string.`);
49
+ }
50
+ }
51
+ }
52
+ /** And remains limited to assertions and existing custom assertion plugins. */
53
+ exports.nativeAssertionCommands = new Set(['assert.that', 'network.expect', 'download.expect', 'stub.expect', 'a11y.scan', 'a11y.match']);
54
+ function isAssertionStep(action, payload) {
55
+ return action === 'expect' || action === 'plugin' || action === 'native' && exports.nativeAssertionCommands.has(payload?.pluginMethod);
56
+ }
@@ -53,11 +53,22 @@ class Lexer {
53
53
  let jsonStr = "";
54
54
  let depth = 0;
55
55
  let startLine = this.line;
56
+ let inString = false, escaped = false;
56
57
  while (this.position < this.input.length) {
57
58
  const c = this.input[this.position];
58
- if (c === "{")
59
+ if (inString) {
60
+ if (escaped)
61
+ escaped = false;
62
+ else if (c === "\\")
63
+ escaped = true;
64
+ else if (c === '"')
65
+ inString = false;
66
+ }
67
+ else if (c === '"')
68
+ inString = true;
69
+ else if (c === "{")
59
70
  depth++;
60
- if (c === "}")
71
+ else if (c === "}")
61
72
  depth--;
62
73
  if (c === "\n")
63
74
  this.line++;
@@ -217,7 +228,7 @@ class Lexer {
217
228
  });
218
229
  continue;
219
230
  }
220
- else if (ident.includes(".") && !lowerIdent.startsWith("flow.")) {
231
+ else if (/^flow\.(browser|assert|network|download|clock|stub|trace|har|a11y|component)\.[a-z]+$/i.test(ident) || (ident.includes(".") && !lowerIdent.startsWith("flow."))) {
221
232
  tokens.push({
222
233
  type: TokenType.ALIAS_COMMAND,
223
234
  value: ident,
@@ -392,7 +403,7 @@ class Lexer {
392
403
  line: this.line,
393
404
  });
394
405
  }
395
- else if (ident.includes(".") && !lowerIdent.startsWith("flow.")) {
406
+ else if (/^flow\.(browser|assert|network|download|clock|stub|trace|har|a11y|component)\.[a-z]+$/i.test(ident) || (ident.includes(".") && !lowerIdent.startsWith("flow."))) {
396
407
  tokens.push({
397
408
  type: TokenType.ALIAS_COMMAND,
398
409
  value: ident,
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.Parser = void 0;
4
+ const assertion_contract_1 = require("./assertion_contract");
4
5
  const lexer_1 = require("./lexer");
5
6
  class Parser {
6
7
  tokens;
@@ -99,8 +100,8 @@ class Parser {
99
100
  declarations.push(this.parseDeclaration());
100
101
  }
101
102
  else if (token.type === lexer_1.TokenType.BLOCK) {
102
- if (isLocked)
103
- throw new Error(`Line ${token.line}: Terminal Lock Violation. No steps allowed after Then.`);
103
+ if (isLocked && token.value.toLowerCase().replace(/^stop\./, '') !== 'and')
104
+ throw new Error(`Line ${token.line}: Terminal Lock Violation. Only assertion And blocks are allowed after Then.`);
104
105
  const blockToken = this.advance();
105
106
  let rawBlockType = blockToken.value.toLowerCase();
106
107
  if (rawBlockType.startsWith("stop.")) {
@@ -149,7 +150,7 @@ class Parser {
149
150
  }
150
151
  else if (token.type === lexer_1.TokenType.COMMAND ||
151
152
  token.type === lexer_1.TokenType.ALIAS_COMMAND) {
152
- if (isLocked && currentBlockType !== "then") {
153
+ if (isLocked && currentBlockType !== "then" && currentBlockType !== "and") {
153
154
  throw new Error(`Line ${token.line}: Terminal Lock Violation.`);
154
155
  }
155
156
  const step = this.parseCommand();
@@ -157,8 +158,7 @@ class Parser {
157
158
  step.isStop = true;
158
159
  }
159
160
  if (currentBlockType === "and" &&
160
- step.action !== "expect" &&
161
- step.action !== "plugin") {
161
+ !(0, assertion_contract_1.isAssertionStep)(step.action, step.payload)) {
162
162
  throw new Error(`Line ${token.line}: And blocks are strictly for assertions or custom assertion plugins.`);
163
163
  }
164
164
  steps.push(step);
@@ -242,7 +242,7 @@ class Parser {
242
242
  return {
243
243
  id: `scenario_${this.scenarioCounter}_S${this.stepCounter++}`,
244
244
  blockDescription: this.currentBlockDescription,
245
- action: "plugin",
245
+ action: alias === "flow" ? "native" : "plugin",
246
246
  payload: {
247
247
  pluginAlias: alias,
248
248
  pluginMethod: method,
@@ -371,7 +371,13 @@ class Parser {
371
371
  const viaToken = this.consume(lexer_1.TokenType.PREPOSITION, 'Expected "via"');
372
372
  if (viaToken.value.toLowerCase() !== "via")
373
373
  throw new Error(`Line ${viaToken.line}: Expected 'via'`);
374
- payload.verb = this.consume(lexer_1.TokenType.COMMAND, "Expected API verb (e.g. POST)").value.toLowerCase();
374
+ payload.verb = this.consume(lexer_1.TokenType.COMMAND, "Expected API verb (e.g. POST)").value.toLowerCase().replace(/^flow\./, '');
375
+ if (!['get', 'post', 'put', 'patch', 'delete'].includes(payload.verb)) {
376
+ throw new Error('Authentication refresh requires an HTTP verb: get, post, put, patch or delete.');
377
+ }
378
+ if (!['401', '403'].includes(payload.targetStatus)) {
379
+ throw new Error('Authentication refresh status must be 401 or 403.');
380
+ }
375
381
  payload.url = this.consume(lexer_1.TokenType.STRING, "Endpoint URL expected").value;
376
382
  payload.headers = [];
377
383
  this.parseApiModifiers(payload);
@@ -507,6 +513,7 @@ class Parser {
507
513
  if (mod.toLowerCase() === "responsetime" && mod !== "responseTime") {
508
514
  throw new Error(`Syntax Error on line ${modToken.line}: Invalid keyword '${mod}'. Flowstride uses strict camelCase. Did you mean 'responseTime'?`);
509
515
  }
516
+ (0, assertion_contract_1.validateExpectationType)(mod);
510
517
  payload.type = mod;
511
518
  if (mod === "status") {
512
519
  payload.statusCode = this.consume(lexer_1.TokenType.STRING, 'Status code expected (e.g., "200")').value;
@@ -517,6 +524,7 @@ class Parser {
517
524
  throw new Error(`Line ${modToken.line}: Expected 'lessThan' after responseTime`);
518
525
  }
519
526
  payload.threshold = this.consume(lexer_1.TokenType.STRING, "Time threshold expected").value;
527
+ (0, assertion_contract_1.validateResponseTimeThreshold)(payload.threshold, true);
520
528
  }
521
529
  else if (mod === "resBody" ||
522
530
  mod === "resHeader" ||
@@ -0,0 +1,36 @@
1
+ export type AssertionCheckStatus = 'passed' | 'failed' | 'not_evaluated' | 'running';
2
+ export interface AssertionCheck {
3
+ key: string;
4
+ label: string;
5
+ status: AssertionCheckStatus;
6
+ expected?: string;
7
+ actual?: string;
8
+ reason?: string;
9
+ children?: AssertionCheck[];
10
+ }
11
+ export interface AssertionResult {
12
+ command: string;
13
+ target: string;
14
+ status: 'passed' | 'failed' | 'running' | 'not_evaluated';
15
+ checks: AssertionCheck[];
16
+ timeout?: number;
17
+ elapsed?: number;
18
+ reason?: string;
19
+ sourceFile?: string;
20
+ sourceLine?: number;
21
+ profile?: string;
22
+ }
23
+ export type AssertionReporter = (result: AssertionResult) => void;
24
+ /** Diagnostic text is bounded data, including in terminals and exported reports. */
25
+ export declare function diagnosticValue(value: unknown): string;
26
+ export declare function assertionCheck(key: string, label: string, matched: boolean, expected?: unknown, actual?: unknown): AssertionCheck;
27
+ export declare function assertionText(result: AssertionResult): string;
28
+ export declare class AssertionFailure extends Error {
29
+ readonly assertion: AssertionResult;
30
+ constructor(assertion: AssertionResult);
31
+ }
32
+ export declare function reportAssertion(command: string, target: string, checks: AssertionCheck[], reporter?: AssertionReporter, metadata?: Partial<AssertionResult>): AssertionResult;
33
+ export declare function requireAssertion(command: string, target: string, checks: AssertionCheck[], reporter?: AssertionReporter, metadata?: Partial<AssertionResult>): void;
34
+ export declare function failedAssertion(result: AssertionResult | undefined, reason: string): AssertionResult | undefined;
35
+ export declare function commandLabel(action: string, payload: any): string;
36
+ export declare function unexecutedAssertion(action: string, payload: any, reason?: string): AssertionResult | undefined;