@revyl/verify 0.2.0 → 0.3.0
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/CHANGELOG.md +30 -5
- package/README.md +99 -69
- package/dist/api/client.d.ts +6 -4
- package/dist/api/client.js +64 -14
- package/dist/api/types.d.ts +1 -0
- package/dist/cli/install.d.ts +2 -0
- package/dist/cli/install.js +64 -0
- package/dist/cli/login.d.ts +6 -0
- package/dist/cli/login.js +200 -0
- package/dist/cli/main.js +45 -11
- package/dist/computer-session.d.ts +14 -4
- package/dist/computer-session.js +106 -23
- package/dist/config.d.ts +7 -0
- package/dist/config.js +16 -2
- package/dist/errors.d.ts +6 -1
- package/dist/errors.js +4 -0
- package/dist/index.d.ts +5 -5
- package/dist/index.js +1 -1
- package/dist/open.d.ts +10 -2
- package/dist/open.js +35 -6
- package/dist/session.d.ts +21 -2
- package/dist/session.js +134 -13
- package/docs/README.md +0 -1
- package/docs/agents.md +2 -2
- package/docs/api.md +65 -3
- package/docs/ci.md +1 -1
- package/docs/computers.md +14 -13
- package/docs/configuration.md +8 -7
- package/docs/end-to-end.md +3 -3
- package/docs/errors.md +27 -2
- package/docs/getting-started.md +29 -15
- package/docs/llms.txt +4 -5
- package/docs/web.md +1 -1
- package/package.json +1 -1
- package/docs/linux-computers-contract.md +0 -53
package/CHANGELOG.md
CHANGED
|
@@ -1,14 +1,39 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
##
|
|
3
|
+
## 0.3.0 (2026-10-31)
|
|
4
4
|
|
|
5
|
-
-
|
|
5
|
+
- Sign in automatically on the first interactive `open()`, `attach()`, or `currentComputer()` call using the Revyl CLI browser-approval flow; CI remains key-only and browser-free.
|
|
6
|
+
- Make `npx @revyl/verify init` install Verify, scaffold a project, sign in through the Revyl CLI browser approval flow when needed, and print the example command. Non-interactive runs use `REVYL_API_KEY` without prompting or opening a browser.
|
|
7
|
+
- Add `currentComputer()`, Python `current_computer()` / `async_current_computer()`, and Go `CurrentComputer` to attach non-owning to `REVYL_COMPUTER_ID`, with explicit ownership opt-in and a typed error when no current computer is configured.
|
|
8
|
+
- Add explicit non-owning computer attachment with `stopOnClose: false`, Python `stop_on_close=False`, and Go `OpenOptions.StopOnClose`. Closing these attachments still cleans up hosted children but leaves the computer running for its owner to release; the default remains owning.
|
|
9
|
+
|
|
10
|
+
- Removed environment overrides.
|
|
11
|
+
|
|
12
|
+
- **Breaking wire change:** encode computer-operation arguments as `argv_b64` and file paths as `path_b64` using padded URL-safe base64 of UTF-8, without changing public APIs, argument boundaries, or existing stdin/data fields. Verify 0.3.0 requires the encoded-only backend, which is already live on revyl.ai; no plaintext fallback or automatic replay is provided.
|
|
13
|
+
|
|
14
|
+
- Classify HTTP 403 responses with a `text/html` content type and nonempty `cf-ray` as `EDGE_BLOCKED` across TypeScript, Go and Python, unless the body is syntactically valid JSON. Preserve origin authentication errors, omit response content from edge-block messages, and do not retry blocked requests.
|
|
15
|
+
|
|
16
|
+
- Map hosted Web navigation `request_timeout` and `protocol_error` responses to `ACTION_UNCERTAIN` with safe reasons and HTTP status. Timeout messages explain that navigation may still commit and the session remains open; Verify never replays it.
|
|
17
|
+
|
|
18
|
+
- Treat ordinary Web `409 web_runtime_unavailable` admission refusals as definite pre-dispatch failures: preserve `API_ERROR` plus safe reason/status details and do not send a stop request. Allocated or otherwise uncertain Web failures retain cleanup.
|
|
19
|
+
|
|
20
|
+
- Bound parent computer cleanup's child phase to five seconds so slow child stops cannot prevent requesting parent release. Recognize `web_runtime_not_configured` as a definite hosted refusal, reject reports without a known session ID locally, preserve safe TypeScript startup diagnostics, and keep the original TypeScript `Session` interface compatible through the extended `OpenedSession` interface.
|
|
21
|
+
|
|
22
|
+
- Add public computer attachment, computer-hosted child sessions and deterministic Web navigation in TypeScript, Go and Python. Child sessions reuse the parent's authenticated client and expire with it; parent close attempts bounded child cleanup before requesting computer release. Initial hosted Web builds remain HTTPS; localhost is reached by navigation after start, subject to runtime availability.
|
|
23
|
+
- Preserve typed server refusal reasons and HTTP status alongside existing error codes, including Web `web_runtime_unavailable`, computer `computer_limit` and `runtime_unavailable`, without replaying starts or navigation.
|
|
24
|
+
|
|
25
|
+
- Preserve Python startup session identity, viewer links and separate cleanup outcomes on typed errors; add optional lifecycle logging to sync/async opening. Clean up identifiable malformed or uncertain starts, interrupt cancelled async startup, validate device stop acknowledgments, and preserve original failures when cleanup also fails.
|
|
26
|
+
|
|
27
|
+
- Prepare Python SDK 0.1.0 (`revyl-verify`, import `revyl_verify`) for Python ≥3.10, with typed sync and thread-adapted async sessions, seconds-based timeouts, web/mobile steps and early-access computer operations. Include fake-server coverage, wheel/sdist checks and an isolated installed-wheel smoke; Python has not been published or verified live. Its independent `py/v*` release workflow validates without publishing on dispatch, skips exact published versions, and requires administrator-enabled, protected-environment PyPI trusted publishing for new tag releases.
|
|
28
|
+
|
|
29
|
+
- Add the source-free `revyl-verify` npm alias with matching API, TypeScript types and CLI, pinned to the exact `@revyl/verify` version. Validate and pack both packages; optional CI publication checks each version independently and publishes the scoped package before the alias.
|
|
30
|
+
- Add a standard-library-only Go SDK for web, iOS, Android, and early-access Mac and Linux computers. Computer commands, binary files, process handles, screenshots, recording metadata, and confirmed-release cleanup require computer access through support@revyl.ai. Capacity refusals have a distinct `COMPUTER_LIMIT` code; no start or operation is automatically replayed.
|
|
6
31
|
|
|
7
32
|
## 0.2.0
|
|
8
33
|
|
|
9
34
|
Prepared for release; see npm for publication status.
|
|
10
35
|
|
|
11
|
-
- Enable Linux named and inline computer targets with the shared Mac command, file, process, screenshot, and confirmed-release cleanup surface. Both operating systems remain early access
|
|
36
|
+
- Enable Linux named and inline computer targets with the shared Mac command, file, process, screenshot, and confirmed-release cleanup surface. Both operating systems remain early access; contact support@revyl.ai for access.
|
|
12
37
|
- Add `COMPUTER_LIMIT` for queued work (including iOS) or concurrency admission refusals, without retrying or stopping a definitely refused start. Preserve `SESSION_BUSY` for operation limits.
|
|
13
38
|
- Explain the 120-second unclaimed-start failure without shortening the default 10-minute start timeout.
|
|
14
39
|
- Expose optional `expiresAt` and bound operation/screenshot observation by the hard 60-minute lifetime from create, while keeping explicit close possible.
|
|
@@ -29,7 +54,7 @@ Published manually from a maintainer's Mac without provenance.
|
|
|
29
54
|
- Save screenshots, share session links, and close sessions explicitly or during best-effort process cleanup.
|
|
30
55
|
- Configure named targets and initialize a script with the `verify` or `revyl-verify` command.
|
|
31
56
|
- Use environment credentials or an existing Revyl CLI login, with documentation included in the package.
|
|
32
|
-
- Include **early-access Mac computers
|
|
57
|
+
- Include **early-access Mac computers** for commands, binary files, process handles, screenshots, and confirmed-release cleanup. Ask support@revyl.ai about access.
|
|
33
58
|
- Reserve Linux target types and document the future contract; opening Linux is not available in this release.
|
|
34
59
|
|
|
35
|
-
Requires Node.js 22.18 or later and a Revyl account. There is no bundled test runner or step replay. Mac operations remain covered by source-aligned fake-API tests;
|
|
60
|
+
Requires Node.js 22.18 or later and a Revyl account. There is no bundled test runner or step replay. Mac operations remain covered by source-aligned fake-API tests; the attempted live start never reached active, so it did not prove any operation or confirmed cleanup.
|
package/README.md
CHANGED
|
@@ -1,78 +1,100 @@
|
|
|
1
|
-
|
|
1
|
+
# Verify
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
<em>Cloud browsers, mobile devices and computers from TypeScript and Go.</em>
|
|
5
|
-
</p>
|
|
3
|
+
Verify runs end-to-end checks in a real browser, iOS simulator, or Android emulator on Revyl. You describe what the agent should do and what must be true afterward, instead of maintaining selectors and coordinates that break when a screen changes. Use it from a script, a coding agent, or the test runner you already have. Mac and Linux computers are also available for commands, files, and screenshots, not natural-language steps.
|
|
6
4
|
|
|
7
|
-
|
|
8
|
-
<a href="https://github.com/RevylAI/verify/blob/v0.2.0/CHANGELOG.md"><img src="https://img.shields.io/badge/version-0.2.0-9D61FF" alt="Source version" /></a>
|
|
9
|
-
<a href="https://github.com/RevylAI/verify/blob/v0.2.0/LICENSE"><img src="https://img.shields.io/badge/license-Apache%202.0-blue.svg" alt="License: Apache 2.0" /></a>
|
|
10
|
-
<a href="https://www.npmjs.com/package/@revyl/verify"><img src="https://img.shields.io/npm/v/@revyl/verify?color=9D61FF" alt="npm" /></a>
|
|
11
|
-
<a href="https://pkg.go.dev/github.com/RevylAI/verify/go"><img src="https://img.shields.io/badge/Go-reference-00ADD8?logo=go&logoColor=white" alt="Go reference" /></a>
|
|
12
|
-
<a href="https://github.com/RevylAI/verify/actions/workflows/ci.yml"><img src="https://img.shields.io/github/actions/workflow/status/RevylAI/verify/ci.yml?branch=main&label=CI" alt="CI" /></a>
|
|
13
|
-
</p>
|
|
5
|
+
Every run gives you a link to watch the target. The session keeps its report and available recording, and the SDK keeps instruction and validation results in order. A failed check leaves evidence you can inspect.
|
|
14
6
|
|
|
15
|
-
|
|
7
|
+
## Install and run
|
|
16
8
|
|
|
17
|
-
|
|
9
|
+
```bash
|
|
10
|
+
npm install @revyl/verify
|
|
11
|
+
```
|
|
18
12
|
|
|
19
|
-
|
|
13
|
+
Save this as `verify/check.ts`:
|
|
20
14
|
|
|
21
|
-
|
|
15
|
+
```ts
|
|
16
|
+
import { open } from '@revyl/verify';
|
|
22
17
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
18
|
+
const session = await open({ platform: 'ios', app: { appId: 'YOUR_REVYL_APP_ID' } });
|
|
19
|
+
try {
|
|
20
|
+
await session.instruction('Open General, then About');
|
|
21
|
+
await session.validation('The screen shows the operating system version');
|
|
22
|
+
await session.screenshot({ path: '.verify/about.png' });
|
|
23
|
+
} finally {
|
|
24
|
+
await session.close();
|
|
25
|
+
}
|
|
28
26
|
```
|
|
29
27
|
|
|
30
|
-
|
|
28
|
+
Run the script:
|
|
31
29
|
|
|
32
|
-
|
|
30
|
+
```bash
|
|
31
|
+
node verify/check.ts
|
|
32
|
+
```
|
|
33
33
|
|
|
34
|
-
|
|
34
|
+
On the first interactive run, Verify opens the Revyl browser approval flow once and saves the login in `~/.revyl/credentials.json` for the CLI and SDK to share.
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
Verify prints a live session URL and progress like this:
|
|
37
37
|
|
|
38
|
-
```
|
|
39
|
-
|
|
38
|
+
```text
|
|
39
|
+
[verify] ios: starting ios session https://app.revyl.ai/sessions/…
|
|
40
|
+
[verify] ios: ready
|
|
41
|
+
[verify] ios: ✓ instruction "Open General, then About" (12.0s)
|
|
42
|
+
[verify] ios: ✓ validation "The screen shows the operating system version" (9.8s)
|
|
43
|
+
[verify] ios: closed
|
|
40
44
|
```
|
|
41
45
|
|
|
42
|
-
|
|
46
|
+
The URL is live during the run and keeps the report and available recording after it closes. A failed instruction or validation throws a typed error and includes the same URL. In CI, Verify never opens a browser: provide `REVYL_API_KEY` as a secret.
|
|
43
47
|
|
|
44
|
-
|
|
48
|
+
For Web, use `open({ platform: 'web', app: { appId, url } })`; for a computer, use `open({ platform: 'mac' })`.
|
|
49
|
+
|
|
50
|
+
## Scaffold a config (optional)
|
|
51
|
+
|
|
52
|
+
Use `init` when you want named targets in `verify.config.ts` and a generated `verify/example.ts` script:
|
|
45
53
|
|
|
46
54
|
```bash
|
|
47
|
-
|
|
55
|
+
npx @revyl/verify init
|
|
48
56
|
```
|
|
49
57
|
|
|
50
|
-
|
|
58
|
+
`init` detects your package manager, asks before adding `@revyl/verify` as a dev dependency, creates `verify.config.ts` and `verify/example.ts`, and prints the command that runs the example. Use `--yes` to accept installation without a prompt, or `--no-install` when Verify is already installed. It is optional; installing the package is enough to use inline targets.
|
|
51
59
|
|
|
52
|
-
|
|
60
|
+
The generated project starts with a named target. Use `--platform web`, `--platform ios`, `--platform android`, or `--platform mac` when you want to skip the platform question. Web and mobile targets need a Revyl app id; a Mac target does not. The generated config is ordinary TypeScript, so edit it when the example needs a pinned build, a public Web URL, or a different target.
|
|
53
61
|
|
|
54
|
-
|
|
62
|
+
See the [TypeScript getting-started guide](https://github.com/RevylAI/verify/blob/v0.3.0/packages/verify/docs/getting-started.md), the [Python SDK guide](https://github.com/RevylAI/verify/blob/v0.3.0/python/README.md), and the [Go SDK guide](https://github.com/RevylAI/verify/tree/go/v0.2.0/go).
|
|
55
63
|
|
|
56
|
-
|
|
57
|
-
2. `REVYL_API_KEY` from the environment.
|
|
58
|
-
3. Another usable CLI login stored by `revyl auth login`.
|
|
64
|
+
## CI
|
|
59
65
|
|
|
60
|
-
|
|
66
|
+
For a normal GitHub Actions job, install from the lockfile with `npm ci` and pass `REVYL_API_KEY` from repository secrets to the script that runs Verify:
|
|
61
67
|
|
|
62
|
-
```
|
|
63
|
-
|
|
68
|
+
```yaml
|
|
69
|
+
- uses: actions/checkout@v5
|
|
70
|
+
- uses: actions/setup-node@v5
|
|
71
|
+
with:
|
|
72
|
+
node-version: 24
|
|
73
|
+
cache: npm
|
|
74
|
+
- run: npm ci
|
|
75
|
+
- run: node verify/example.ts
|
|
76
|
+
env:
|
|
77
|
+
REVYL_API_KEY: ${{ secrets.REVYL_API_KEY }}
|
|
64
78
|
```
|
|
65
79
|
|
|
66
|
-
See [
|
|
80
|
+
On a `revyl-macos` job, `currentComputer()` attaches to the runner's existing computer without taking ownership. The runner releases the computer; closing the Verify attachment still cleans up hosted children. See the [CI guide](https://github.com/RevylAI/verify/blob/v0.3.0/packages/verify/docs/ci.md) and [computer API](https://github.com/RevylAI/verify/blob/v0.3.0/packages/verify/docs/api.md#inside-a-revyl-macos-github-actions-job).
|
|
67
81
|
|
|
68
|
-
##
|
|
82
|
+
## Reference authentication
|
|
83
|
+
|
|
84
|
+
Use manual setup when you need to control the package manager or keep the generated files out of the project:
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
revyl auth login
|
|
88
|
+
npx @revyl/verify whoami
|
|
89
|
+
```
|
|
69
90
|
|
|
70
|
-
|
|
91
|
+
The SDK resolves credentials in this order: a usable stored CLI login marked `local_auth_override`, `REVYL_API_KEY`, then another usable stored CLI login. In CI, use a secret manager rather than putting a key in source, command arguments, or logs. `whoami` checks the credentials without starting a session.
|
|
71
92
|
|
|
72
|
-
|
|
93
|
+
## TypeScript
|
|
73
94
|
|
|
74
95
|
```ts
|
|
75
96
|
import { open } from '@revyl/verify';
|
|
97
|
+
|
|
76
98
|
const session = await open('ios');
|
|
77
99
|
try {
|
|
78
100
|
await session.instruction('Open General, then About');
|
|
@@ -82,22 +104,18 @@ try {
|
|
|
82
104
|
}
|
|
83
105
|
```
|
|
84
106
|
|
|
85
|
-
For
|
|
107
|
+
For Web, use a public HTTPS URL in the target config. For iOS and Android, point the target at an app or build in Revyl. The [API guide](https://github.com/RevylAI/verify/blob/v0.3.0/packages/verify/docs/api.md) covers target options, step results, screenshots, reports, and cleanup.
|
|
86
108
|
|
|
87
|
-
|
|
109
|
+
## Go
|
|
88
110
|
|
|
89
|
-
Inside a function returning `error`, with a bounded `ctx
|
|
111
|
+
Inside a function returning `error`, with a bounded `ctx`:
|
|
90
112
|
|
|
91
113
|
```go
|
|
92
114
|
session, err := verify.Open(ctx, verify.Target{Platform: verify.IOS, BundleID: "com.apple.Preferences"})
|
|
93
115
|
if err != nil {
|
|
94
116
|
return err
|
|
95
117
|
}
|
|
96
|
-
defer func() {
|
|
97
|
-
cleanup, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
98
|
-
defer cancel()
|
|
99
|
-
_ = session.Close(cleanup)
|
|
100
|
-
}()
|
|
118
|
+
defer func() { _ = session.Close(ctx) }()
|
|
101
119
|
if _, err := session.Instruction(ctx, "Open General, then About"); err != nil {
|
|
102
120
|
return err
|
|
103
121
|
}
|
|
@@ -105,34 +123,46 @@ _, err = session.Validation(ctx, "The screen shows the operating system version"
|
|
|
105
123
|
return err
|
|
106
124
|
```
|
|
107
125
|
|
|
108
|
-
|
|
126
|
+
Go calls return errors instead of throwing. The [Go reference](https://github.com/RevylAI/verify/tree/go/v0.2.0/go) covers context deadlines, screenshots, computer operations, and hosted sessions.
|
|
127
|
+
|
|
128
|
+
## Python
|
|
129
|
+
|
|
130
|
+
```python
|
|
131
|
+
from revyl_verify import Target, open
|
|
132
|
+
|
|
133
|
+
with open(Target(platform="ios", bundle_id="com.apple.Preferences")) as session:
|
|
134
|
+
session.instruction("Open General, then About")
|
|
135
|
+
session.validation("The screen shows the operating system version")
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Python supports synchronous and asyncio APIs, but it has no bundled CLI. The [Python reference](https://github.com/RevylAI/verify/blob/v0.3.0/python/README.md) covers both APIs and computer operations.
|
|
109
139
|
|
|
110
|
-
|
|
140
|
+
## Platforms
|
|
111
141
|
|
|
112
|
-
| Platform | Instructions / validations | Commands / files / processes | PNG screenshot |
|
|
113
|
-
| --- | :---: | :---: | :---: |
|
|
114
|
-
| Web | Yes | — | Yes |
|
|
115
|
-
| iOS simulator | Yes | — | Yes |
|
|
116
|
-
| Android emulator | Yes | — | Yes |
|
|
117
|
-
| Mac computer | — | Yes | Yes |
|
|
118
|
-
| Linux computer | — | Yes | Yes |
|
|
142
|
+
| Platform | Instructions / validations | Commands / files / processes | PNG screenshot |
|
|
143
|
+
| --- | :---: | :---: | :---: |
|
|
144
|
+
| Web | Yes | — | Yes |
|
|
145
|
+
| iOS simulator | Yes | — | Yes |
|
|
146
|
+
| Android emulator | Yes | — | Yes |
|
|
147
|
+
| Mac computer | — | Yes | Yes |
|
|
148
|
+
| Linux computer | — | Yes | Yes |
|
|
119
149
|
|
|
120
|
-
|
|
150
|
+
Computer targets are early access and use commands, files, processes, and screenshots rather than natural-language agent steps. They have a server-bounded lifetime. See [computer setup](https://github.com/RevylAI/verify/blob/v0.3.0/packages/verify/docs/computers.md).
|
|
121
151
|
|
|
122
152
|
## Documentation
|
|
123
153
|
|
|
124
|
-
- [Documentation index](https://github.com/RevylAI/verify/tree/v0.
|
|
125
|
-
- [End-to-end results and limitations](https://github.com/RevylAI/verify/blob/v0.
|
|
126
|
-
- [Examples](https://github.com/RevylAI/verify/tree/v0.
|
|
127
|
-
- [Contributing](https://github.com/RevylAI/verify/blob/v0.
|
|
154
|
+
- [Documentation index](https://github.com/RevylAI/verify/tree/v0.3.0/packages/verify/docs/) · [API](https://github.com/RevylAI/verify/blob/v0.3.0/packages/verify/docs/api.md) · [CI](https://github.com/RevylAI/verify/blob/v0.3.0/packages/verify/docs/ci.md) · [Coding agents](https://github.com/RevylAI/verify/blob/v0.3.0/packages/verify/docs/agents.md)
|
|
155
|
+
- [End-to-end results and limitations](https://github.com/RevylAI/verify/blob/v0.3.0/packages/verify/docs/end-to-end.md)
|
|
156
|
+
- [Examples](https://github.com/RevylAI/verify/tree/v0.3.0/examples/): [Web store](https://github.com/RevylAI/verify/tree/v0.3.0/examples/web-store/) · [Mobile Settings](https://github.com/RevylAI/verify/tree/v0.3.0/examples/mobile-settings/) · [Mac workspace](https://github.com/RevylAI/verify/tree/v0.3.0/examples/mac-workspace/)
|
|
157
|
+
- [Contributing](https://github.com/RevylAI/verify/blob/v0.3.0/CONTRIBUTING.md) · [Security](https://github.com/RevylAI/verify/blob/v0.3.0/SECURITY.md) · [Releasing](https://github.com/RevylAI/verify/blob/v0.3.0/RELEASING.md)
|
|
128
158
|
|
|
129
159
|
## Troubleshooting
|
|
130
160
|
|
|
131
|
-
- **`NOT_AVAILABLE_ON_PLATFORM`:**
|
|
132
|
-
- **`COMPUTER_LIMIT`:** admission was refused because of queued work or a concurrency limit.
|
|
133
|
-
- **Blank first screen:** mobile startup
|
|
134
|
-
- **A report
|
|
161
|
+
- **`NOT_AVAILABLE_ON_PLATFORM`:** the requested method is not available for that target. Computer targets do not run instruction or validation steps; Web and mobile targets do not expose computer commands or files.
|
|
162
|
+
- **`COMPUTER_LIMIT`:** computer admission was refused because of queued work or a concurrency limit. Verify does not retry creation or stop a definitely refused start.
|
|
163
|
+
- **Blank first screen:** mobile startup uses a bounded, best-effort blank-frame observation, not a rendered-readiness guarantee. Begin with an instruction that reaches the screen your workflow needs.
|
|
164
|
+
- **A validation report looks successful but the assertion failed:** inspect each step's `effective_status` and `validation_result`, and compare `validations_passed` with `total_validations` rather than transport counters.
|
|
135
165
|
|
|
136
166
|
## License
|
|
137
167
|
|
|
138
|
-
[Apache 2.0](https://github.com/RevylAI/verify/blob/v0.
|
|
168
|
+
[Apache 2.0](https://github.com/RevylAI/verify/blob/v0.3.0/LICENSE).
|
package/dist/api/client.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { VerifyError } from '../errors.js';
|
|
1
|
+
import { VerifyError, type RefusalReason } from '../errors.js';
|
|
2
2
|
import type { AppResponse, BuildResponse, DeviceSessionDetail, DeviceSessionStopResponse, StartDeviceRequest, StartDeviceResponse, WhoAmI, WorkerConnection } from './types.js';
|
|
3
3
|
export declare const DEFAULT_BACKEND_URL = "https://backend.revyl.ai";
|
|
4
4
|
export declare const DEFAULT_APP_URL = "https://app.revyl.ai";
|
|
@@ -34,7 +34,7 @@ export declare class RevylClient {
|
|
|
34
34
|
startDevice(request: StartDeviceRequest, signal?: AbortSignal): Promise<StartDeviceResponse>;
|
|
35
35
|
getDeviceSession(sessionId: string, signal?: AbortSignal): Promise<DeviceSessionDetail>;
|
|
36
36
|
getWorkerConnection(workflowRunId: string, signal?: AbortSignal): Promise<WorkerConnection>;
|
|
37
|
-
stopDeviceSession(sessionId: string): Promise<DeviceSessionStopResponse>;
|
|
37
|
+
stopDeviceSession(sessionId: string, retry?: boolean): Promise<DeviceSessionStopResponse>;
|
|
38
38
|
cancelDevice(workflowRunId: string): Promise<unknown>;
|
|
39
39
|
getApp(appId: string, signal?: AbortSignal): Promise<AppResponse>;
|
|
40
40
|
registerWebUrlBuild(appId: string, request: {
|
|
@@ -55,9 +55,11 @@ export declare class RevylClient {
|
|
|
55
55
|
}): Promise<RawResponse>;
|
|
56
56
|
private sendOnce;
|
|
57
57
|
}
|
|
58
|
-
export declare function appUrl(): string;
|
|
59
|
-
export declare function sessionViewerUrl(sessionId: string, kind?: 'computer'): string;
|
|
58
|
+
export declare function appUrl(origin?: string): string;
|
|
59
|
+
export declare function sessionViewerUrl(sessionId: string, kind?: 'computer', origin?: string): string;
|
|
60
60
|
export declare function parseJson<T>(response: RawResponse, what: string): T;
|
|
61
61
|
export declare function responseDetail(response: RawResponse): string | undefined;
|
|
62
62
|
export declare function responseErrorCode(response: RawResponse): string | undefined;
|
|
63
63
|
export declare function apiError(method: string, path: string, response: RawResponse): VerifyError;
|
|
64
|
+
export declare function edgeBlockedError(response: RawResponse): VerifyError | undefined;
|
|
65
|
+
export declare function responseRefusalReason(response: RawResponse): RefusalReason | undefined;
|
package/dist/api/client.js
CHANGED
|
@@ -15,7 +15,7 @@ export class RevylClient {
|
|
|
15
15
|
requestTimeoutMs;
|
|
16
16
|
constructor(options) {
|
|
17
17
|
this.token = options.token;
|
|
18
|
-
this.backendUrl = validateBackendUrl(options.backendUrl ??
|
|
18
|
+
this.backendUrl = validateBackendUrl(options.backendUrl ?? DEFAULT_BACKEND_URL);
|
|
19
19
|
this.fetchImpl = options.fetch ?? fetch;
|
|
20
20
|
this.requestTimeoutMs = options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
21
21
|
}
|
|
@@ -55,8 +55,8 @@ export class RevylClient {
|
|
|
55
55
|
getWorkerConnection(workflowRunId, signal) {
|
|
56
56
|
return this.json('GET', `/api/v1/execution/streaming/worker-connection/${encodeURIComponent(workflowRunId)}`, { signal });
|
|
57
57
|
}
|
|
58
|
-
stopDeviceSession(sessionId) {
|
|
59
|
-
return this.json('POST', `/api/v1/execution/device/sessions/${encodeURIComponent(sessionId)}/stop`, { retry
|
|
58
|
+
stopDeviceSession(sessionId, retry = true) {
|
|
59
|
+
return this.json('POST', `/api/v1/execution/device/sessions/${encodeURIComponent(sessionId)}/stop`, { retry });
|
|
60
60
|
}
|
|
61
61
|
cancelDevice(workflowRunId) {
|
|
62
62
|
return this.json('POST', `/api/v1/execution/device/status/cancel/${encodeURIComponent(workflowRunId)}`, { retry: true });
|
|
@@ -127,11 +127,12 @@ export class RevylClient {
|
|
|
127
127
|
}
|
|
128
128
|
}
|
|
129
129
|
}
|
|
130
|
-
export function appUrl() {
|
|
131
|
-
|
|
130
|
+
export function appUrl(origin = DEFAULT_APP_URL) {
|
|
131
|
+
validateBackendUrl(origin, 'viewer');
|
|
132
|
+
return origin.replace(/\/+$/, '');
|
|
132
133
|
}
|
|
133
|
-
export function sessionViewerUrl(sessionId, kind) {
|
|
134
|
-
return `${appUrl()}/sessions/${encodeURIComponent(sessionId)}${kind ? `?kind=${kind}` : ''}`;
|
|
134
|
+
export function sessionViewerUrl(sessionId, kind, origin) {
|
|
135
|
+
return `${appUrl(origin)}/sessions/${encodeURIComponent(sessionId)}${kind ? `?kind=${kind}` : ''}`;
|
|
135
136
|
}
|
|
136
137
|
export function parseJson(response, what) {
|
|
137
138
|
try {
|
|
@@ -175,27 +176,76 @@ export function responseErrorCode(response) {
|
|
|
175
176
|
}
|
|
176
177
|
}
|
|
177
178
|
export function apiError(method, path, response) {
|
|
178
|
-
const
|
|
179
|
+
const blocked = edgeBlockedError(response);
|
|
180
|
+
if (blocked)
|
|
181
|
+
return blocked;
|
|
182
|
+
const detail = path === '/api/v1/execution/start_device' ? responseKnownDetail(response, START_ERROR_DETAILS) : responseDetail(response);
|
|
179
183
|
const suffix = detail ? `: ${detail}` : '';
|
|
180
184
|
if (response.status === 401 || response.status === 403) {
|
|
181
|
-
return new VerifyError('AUTH_REJECTED', `Revyl rejected the request ${method} ${path} (HTTP ${response.status})${suffix}. Check REVYL_API_KEY or run 'revyl auth login'
|
|
185
|
+
return new VerifyError('AUTH_REJECTED', `Revyl rejected the request ${method} ${path} (HTTP ${response.status})${suffix}. Check REVYL_API_KEY or run 'revyl auth login'.`, { reason: responseRefusalReason(response), httpStatus: response.status });
|
|
182
186
|
}
|
|
183
|
-
return new VerifyError('API_ERROR', `Revyl request ${method} ${path} failed with HTTP ${response.status}${suffix}
|
|
187
|
+
return new VerifyError('API_ERROR', `Revyl request ${method} ${path} failed with HTTP ${response.status}${suffix}`, { reason: responseRefusalReason(response), httpStatus: response.status });
|
|
184
188
|
}
|
|
185
|
-
function
|
|
189
|
+
export function edgeBlockedError(response) {
|
|
190
|
+
if (response.status !== 403 || response.headers.get('content-type')?.split(';')[0]?.trim().toLowerCase() !== 'text/html' || !response.headers.get('cf-ray')?.trim())
|
|
191
|
+
return undefined;
|
|
192
|
+
try {
|
|
193
|
+
JSON.parse(new TextDecoder().decode(response.body));
|
|
194
|
+
return undefined;
|
|
195
|
+
}
|
|
196
|
+
catch { }
|
|
197
|
+
return new VerifyError('EDGE_BLOCKED', 'Revyl request was blocked at the network edge (HTTP 403). Check network access or contact support. Verify will not retry this request.', { httpStatus: response.status });
|
|
198
|
+
}
|
|
199
|
+
const REFUSAL_REASONS = new Set([
|
|
200
|
+
'web_runtime_unavailable', 'web_runtime_not_configured', 'computer_limit', 'runtime_unavailable', 'computer_runtime_unavailable',
|
|
201
|
+
'computer_at_capacity', 'computer_not_running', 'computer_cannot_host_platform', 'computer_not_found',
|
|
202
|
+
'web_build_required', 'web_build_not_supported', 'ios_build_required', 'ios_build_not_supported',
|
|
203
|
+
'android_build_required', 'android_build_not_supported', 'web_session_conflict', 'ios_session_conflict',
|
|
204
|
+
'android_session_conflict', 'not_available_on_platform', 'request_timeout', 'protocol_error',
|
|
205
|
+
]);
|
|
206
|
+
const START_ERROR_DETAILS = new Set([
|
|
207
|
+
...REFUSAL_REASONS,
|
|
208
|
+
'web_start_actor_invalid', 'web_app_required', 'web_app_not_found', 'web_idle_timeout_invalid',
|
|
209
|
+
'web_preview_overrides_unsupported', 'web_env_override_invalid', 'web_env_override_secret',
|
|
210
|
+
'web_sign_in_config_invalid', 'web_sign_in_override_not_secret', 'web_env_overrides_not_supported',
|
|
211
|
+
'web_build_selection_invalid', 'web_build_not_found', 'web_build_invalid', 'web_billing_not_configured',
|
|
212
|
+
'web_concurrency_unavailable', 'web_worker_unavailable', 'web_session_fence_conflict',
|
|
213
|
+
'web_callback_conflict', 'web_start_in_progress', 'web_start_unconfirmed', 'web_session_inputs_unavailable',
|
|
214
|
+
'sign_in_secret_not_found', 'sign_in_secret_unavailable', 'proof_sign_in_not_configured',
|
|
215
|
+
]);
|
|
216
|
+
export function responseRefusalReason(response) {
|
|
217
|
+
return responseKnownDetail(response, REFUSAL_REASONS);
|
|
218
|
+
}
|
|
219
|
+
function responseKnownDetail(response, codes) {
|
|
220
|
+
try {
|
|
221
|
+
const parsed = JSON.parse(new TextDecoder().decode(response.body));
|
|
222
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
|
|
223
|
+
return undefined;
|
|
224
|
+
const record = parsed;
|
|
225
|
+
const values = [record.detail];
|
|
226
|
+
if (record.detail && typeof record.detail === 'object')
|
|
227
|
+
values.push(record.detail.error);
|
|
228
|
+
values.push(record.error);
|
|
229
|
+
return values.find((value) => typeof value === 'string' && codes.has(value));
|
|
230
|
+
}
|
|
231
|
+
catch {
|
|
232
|
+
return undefined;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
function validateBackendUrl(raw, label = 'backend') {
|
|
186
236
|
let url;
|
|
187
237
|
try {
|
|
188
238
|
url = new URL(raw);
|
|
189
239
|
}
|
|
190
240
|
catch {
|
|
191
|
-
throw new VerifyError('CONFIG_INVALID',
|
|
241
|
+
throw new VerifyError('CONFIG_INVALID', `The ${label} URL is not a valid URL.`);
|
|
192
242
|
}
|
|
193
243
|
const loopback = ['localhost', '127.0.0.1', '[::1]'].includes(url.hostname);
|
|
194
244
|
if (url.protocol !== 'https:' && !(loopback && url.protocol === 'http:')) {
|
|
195
|
-
throw new VerifyError('CONFIG_INVALID',
|
|
245
|
+
throw new VerifyError('CONFIG_INVALID', `The ${label} URL must use HTTPS (plain HTTP is allowed only on loopback).`);
|
|
196
246
|
}
|
|
197
247
|
if (url.username || url.password || url.search || url.hash) {
|
|
198
|
-
throw new VerifyError('CONFIG_INVALID',
|
|
248
|
+
throw new VerifyError('CONFIG_INVALID', `The ${label} URL must be an origin without credentials, query, or fragment.`);
|
|
199
249
|
}
|
|
200
250
|
return url.origin;
|
|
201
251
|
}
|
package/dist/api/types.d.ts
CHANGED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { readFile, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { spawn } from 'node:child_process';
|
|
4
|
+
import { dirname, join } from 'node:path';
|
|
5
|
+
import { VerifyError } from '../errors.js';
|
|
6
|
+
import { PACKAGE_NAME } from '../version.js';
|
|
7
|
+
export async function packageHasVerify(cwd) {
|
|
8
|
+
let raw;
|
|
9
|
+
try {
|
|
10
|
+
raw = await readFile(join(cwd, 'package.json'), 'utf8');
|
|
11
|
+
}
|
|
12
|
+
catch (error) {
|
|
13
|
+
if (error.code === 'ENOENT')
|
|
14
|
+
return false;
|
|
15
|
+
throw new VerifyError('CONFIG_INVALID', 'Could not read package.json.', { cause: error });
|
|
16
|
+
}
|
|
17
|
+
let manifest;
|
|
18
|
+
try {
|
|
19
|
+
manifest = JSON.parse(raw);
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
throw new VerifyError('CONFIG_INVALID', 'package.json is not valid JSON.');
|
|
23
|
+
}
|
|
24
|
+
if (!manifest || typeof manifest !== 'object' || Array.isArray(manifest))
|
|
25
|
+
throw new VerifyError('CONFIG_INVALID', 'package.json must contain an object.');
|
|
26
|
+
return ['dependencies', 'devDependencies', 'optionalDependencies', 'peerDependencies'].some((field) => {
|
|
27
|
+
const dependencies = manifest[field];
|
|
28
|
+
return dependencies && typeof dependencies === 'object' && !Array.isArray(dependencies) && Object.hasOwn(dependencies, PACKAGE_NAME);
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
export async function installVerify(cwd) {
|
|
32
|
+
const manager = detectPackageManager(cwd);
|
|
33
|
+
if (!existsSync(join(cwd, 'package.json')))
|
|
34
|
+
await writeFile(join(cwd, 'package.json'), '{\n "private": true,\n "type": "module"\n}\n', { flag: 'wx' });
|
|
35
|
+
const args = manager === 'npm'
|
|
36
|
+
? ['install', '--save-dev', '--ignore-scripts']
|
|
37
|
+
: manager === 'yarn'
|
|
38
|
+
? ['add', '--dev', '--mode=skip-builds']
|
|
39
|
+
: ['add', '--dev', '--ignore-scripts'];
|
|
40
|
+
args.push(PACKAGE_NAME);
|
|
41
|
+
process.stdout.write(`Installing ${PACKAGE_NAME} with ${manager}...\n`);
|
|
42
|
+
await new Promise((resolve, reject) => {
|
|
43
|
+
const env = Object.fromEntries(['PATH', 'HOME', 'USERPROFILE', 'TMPDIR', 'TEMP', 'TMP'].flatMap((key) => {
|
|
44
|
+
const value = process.env[key];
|
|
45
|
+
return value === undefined ? [] : [[key, value]];
|
|
46
|
+
}));
|
|
47
|
+
const child = spawn(manager, args, { cwd, env, stdio: 'inherit', shell: process.platform === 'win32', timeout: 180_000, killSignal: 'SIGKILL' });
|
|
48
|
+
const fail = () => reject(new VerifyError('CONFIG_INVALID', `Could not install ${PACKAGE_NAME} with ${manager}. Install it manually, then retry init --no-install.`));
|
|
49
|
+
child.once('error', fail);
|
|
50
|
+
child.once('exit', (code) => code === 0 ? resolve() : fail());
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
function detectPackageManager(cwd) {
|
|
54
|
+
const lockfiles = [['pnpm-lock.yaml', 'pnpm'], ['yarn.lock', 'yarn'], ['bun.lock', 'bun'], ['bun.lockb', 'bun'], ['package-lock.json', 'npm'], ['npm-shrinkwrap.json', 'npm']];
|
|
55
|
+
for (let directory = cwd;; directory = dirname(directory)) {
|
|
56
|
+
const managers = [...new Set(lockfiles.filter(([file]) => existsSync(join(directory, file))).map(([, manager]) => manager))];
|
|
57
|
+
if (managers.length > 1)
|
|
58
|
+
throw new VerifyError('CONFIG_INVALID', 'Found lockfiles for multiple package managers. Keep the project lockfile and retry init.');
|
|
59
|
+
if (managers[0])
|
|
60
|
+
return managers[0];
|
|
61
|
+
if (dirname(directory) === directory)
|
|
62
|
+
return 'npm';
|
|
63
|
+
}
|
|
64
|
+
}
|