@visualautomate/plugin-sdk 0.6.1 → 0.8.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/README.md CHANGED
@@ -1,91 +1,79 @@
1
- # @visualautomate/plugin-sdk
2
-
3
- The types a VisualAutomate plugin is written against, and a way to run one on
4
- your own machine.
5
-
6
- Most people do not need to install this. `@visualautomate/cli` carries it and
7
- writes the types into your project, so a scaffolded plugin has no dependencies
8
- at all. Install it directly if you want the simulator in your own test suite.
9
-
10
- ```bash
11
- npm install --save-dev @visualautomate/plugin-sdk
12
- ```
13
-
14
- ## A plugin
15
-
16
- ```ts
17
- import type { PluginModule } from "@visualautomate/plugin-sdk"
18
-
19
- const plugin: PluginModule = {
20
- async execute(input, config, context) {
21
- const bytes = await context.storage.get(config.path as string)
22
- if (!bytes) return { output: "error", data: { error: "No such file." } }
23
-
24
- return {
25
- output: "success",
26
- data: { size: bytes.length },
27
- }
28
- },
29
- }
30
-
31
- export default plugin
32
- ```
33
-
34
- `output` names which port the run leaves by, and has to be one of the `outputs`
35
- in your manifest. `data` is what the next steps read.
36
-
37
- ## What `context` has
38
-
39
- | | |
40
- |---|---|
41
- | `storage` | the account's files — `put`, `get`, `delete`, `list`, `head`, `signedUrl`, `download` |
42
- | `state` | values that outlive one run, scoped to the workflow — `get`, `set`, `delete`, `list` |
43
- | `media` | ffmpeg, by operation name. Absent when the server has no sandbox configured |
44
- | `connections` | the accounts this step was configured with. Absent when it declares none |
45
- | `workflowId`, `nodeId`, `userId`, `executionId`, `startedAt`, `secrets` | about the run |
46
-
47
- You never see a credential and you never name a host. `connections.get("account")`
48
- takes the name of one of *your* properties, and the platform attaches the token
49
- on the way past — which is what stops a plugin pointing somebody's account
50
- somewhere it chose.
51
-
52
- ## `simulate`
53
-
54
- ```ts
55
- import { simulate } from "@visualautomate/plugin-sdk"
56
- import plugin from "./plugin"
57
-
58
- const result = await simulate(plugin, {
59
- config: { path: "notes/a.txt" },
60
- storage: { "notes/a.txt": "hello" },
61
- state: { runs: 0 },
62
- })
63
-
64
- result.status // "success" | "error" | "timeout" | ...
65
- result.output?.data
66
- result.files // what storage holds now
67
- result.state // what state holds now
68
- ```
69
-
70
- Storage and state are real, in memory, with the same edge cases the platform's
71
- have — `get` on a missing key is `null`, `put` takes a string or bytes, `list`
72
- is prefix-filtered.
73
-
74
- `connections` and `media` are not simulated. Pass a stub if you want to test the
75
- code around them:
76
-
77
- ```ts
78
- await simulate(plugin, {
79
- connections: {
80
- async get() {
81
- return { id: "test", async request() { return { ok: true, status: 200, body: {} } } }
82
- },
83
- },
84
- })
85
- ```
86
-
87
- Asserting on what your plugin *asked* the provider for is a better test than any
88
- imitation of the provider would be.
1
+ # @visualautomate/plugin-sdk
2
+
3
+ The types a VisualAutomate plugin is written against, and a way to run one on
4
+ your own machine.
5
+
6
+ Most people do not need to install this. `@visualautomate/cli` carries it and
7
+ writes the types into your project, so a scaffolded plugin has no dependencies
8
+ at all. Install it directly if you want the simulator in your own test suite.
9
+
10
+ ```bash
11
+ npm install --save-dev @visualautomate/plugin-sdk
12
+ ```
13
+
14
+ ## A plugin
15
+
16
+ ```ts
17
+ import type { PluginModule } from "@visualautomate/plugin-sdk"
18
+
19
+ const plugin: PluginModule = {
20
+ async execute(input, config, context) {
21
+ const bytes = await context.storage.get(config.path as string)
22
+ if (!bytes) return { output: "error", data: { error: "No such file." } }
23
+
24
+ return {
25
+ output: "success",
26
+ data: { size: bytes.length },
27
+ }
28
+ },
29
+ }
30
+
31
+ export default plugin
32
+ ```
33
+
34
+ `output` names which port the run leaves by, and has to be one of the `outputs`
35
+ in your manifest. `data` is what the next steps read.
36
+
37
+ ## What `context` has
38
+
39
+ | | |
40
+ |---|---|
41
+ | `storage` | the account's files — `put`, `get`, `delete`, `list`, `head`, `signedUrl`, `shareUrl`, `download` |
42
+ | `state` | values that outlive one run, scoped to the workflow — `get`, `set` (at most 128 KB, `ttlSeconds` at least 60), `delete`, `list` (`{ keys: [{ key, expiresAt }], complete }`) |
43
+ | `media` | ffmpeg, by operation name. Absent when the server has no sandbox configured |
44
+ | `workflowId`, `nodeId`, `userId`, `executionId`, `startedAt`, `secrets` | about the run |
45
+
46
+ There is no connected account for your steps: only VisualAutomate's own apps act
47
+ as one (`checkManifest` refuses a `connection` property, `optionsFrom`,
48
+ `action.connection`, a `trigger` and `auth.provider`). A step reaches an API with
49
+ the key a user enters in a `secret` property.
50
+
51
+ ## `simulate`
52
+
53
+ ```ts
54
+ import { simulate } from "@visualautomate/plugin-sdk"
55
+ import plugin from "./plugin"
56
+
57
+ const result = await simulate(plugin, {
58
+ config: { path: "notes/a.txt" },
59
+ storage: { "notes/a.txt": "hello" },
60
+ state: { runs: 0 },
61
+ })
62
+
63
+ result.status // "success" | "error" | "timeout" | ...
64
+ result.output?.data
65
+ result.files // what storage holds now
66
+ result.state // what state holds now
67
+ ```
68
+
69
+ Storage and state are real, in memory, with the same edge cases and limits the
70
+ platform's have — `get` on a missing key is `null`, `put` takes a string or bytes,
71
+ `list` is prefix-filtered, a state value over 128 KB or a `ttlSeconds` under 60 is
72
+ refused, and an expired value reads as `null`.
73
+
74
+ `media` is not simulated: `context.media` fails with a sentence saying so. Stub
75
+ `fetch` to test the code around an API call; asserting on what your plugin
76
+ *asked* for is a better test than any imitation of the API would be.
89
77
 
90
78
  ## The platform's code checks
91
79
 
@@ -104,7 +92,50 @@ check.packages // the allowed packages this code requires
104
92
  version the sandbox has installed. Nothing else is available: no `fs`, no
105
93
  `http`, and no subpaths like `lodash/get`.
106
94
 
107
- `sandboxRequire(load)` and `loadPluginCode(code, require)` load code the way
108
- the sandbox does, as the body of a function given `module`, `exports` and a
109
- `require` that only hands out allowed packages. `visualautomate test` uses
110
- them; they are here for your own test runner.
95
+ `validatePluginCode` also refuses code over 100,000 characters, as the upload does.
96
+
97
+ ## Loading code the way the runner does
98
+
99
+ ```ts
100
+ import { executeOf, loadPluginCode, sandboxRequire, simulate } from "@visualautomate/plugin-sdk"
101
+
102
+ const exported = loadPluginCode(code, sandboxRequire((name) => require(name)), {
103
+ fetch: myGuardedFetch, // optional: the step's fetch
104
+ console: { log: (...a) => logs.push(a) },
105
+ })
106
+ const result = await simulate(executeOf(exported), { config })
107
+ ```
108
+
109
+ `loadPluginCode` compiles the code into a `node:vm` context of its own, wrapped
110
+ as the platform's runner wraps it, with the platform's globals (`fetch`, `btoa`,
111
+ `TextEncoder`, `Blob`, `FormData`, the typed arrays …) and no others: `Buffer`,
112
+ `process`, `globalThis`, `structuredClone` and the Node modules are undefined,
113
+ and `console` has `log`, `error`, `warn` and `info`. `executeOf` finds the
114
+ function the runner would call: the export itself, its `execute`, or a `default`
115
+ that is a function.
116
+
117
+ Objects a step returns belong to its own context, so compare them as JSON rather
118
+ than with a deep-equality that checks prototypes.
119
+
120
+ ## Checking a manifest
121
+
122
+ ```ts
123
+ import { checkManifest } from "@visualautomate/plugin-sdk"
124
+
125
+ const check = checkManifest(JSON.parse(manifestText), /* has code */ true)
126
+ check.ok // false when the portal would refuse it
127
+ check.kind // "code" | "declared" | "outputs-only" | "trigger"
128
+ check.errors // why it would be refused
129
+ check.warnings // what it would drop or regret
130
+ ```
131
+
132
+ The SDK's own reading of the rules the portal enforces, for code steps and
133
+ declared steps (`action`), and the refusal of anything that would act as a
134
+ connected account (`accountUseIn` lists each way in); the portal's word is final.
135
+
136
+ ## The platform's facts
137
+
138
+ `PLATFORM_NODE_MAJOR`, `PROVIDED_GLOBALS`, `BLOCKED_GLOBAL_NAMES`,
139
+ `SANDBOX_TIERS`, `RETIRED_TIERS`, `STATE_LIMITS`, `SIGNED_URL_SECONDS`,
140
+ `MAX_CODE_LENGTH`, `SANDBOX_OVERRIDES` and `PUSH_CHECK` (the portal's default
141
+ `expectOutput` and time limits) are exported, generated from the platform.