@visualautomate/plugin-sdk 0.6.1 → 0.7.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,93 @@
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
+ | `connections` | the accounts this step was configured with. Absent when it declares none, or none was chosen |
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 and limits the
71
+ platform's have — `get` on a missing key is `null`, `put` takes a string or bytes,
72
+ `list` is prefix-filtered, a state value over 128 KB or a `ttlSeconds` under 60 is
73
+ refused, and an expired value reads as `null`.
74
+
75
+ `connections` and `media` are not simulated. Without a stub, `context.connections`
76
+ is absent, as on the platform when no account was chosen, and `context.media`
77
+ fails with a sentence saying so. Pass a stub to test the code around them:
78
+
79
+ ```ts
80
+ await simulate(plugin, {
81
+ connections: {
82
+ async get() {
83
+ return { id: "test", async request() { return { ok: true, status: 200, body: {} } } }
84
+ },
85
+ },
86
+ })
87
+ ```
88
+
89
+ Asserting on what your plugin *asked* the provider for is a better test than any
90
+ imitation of the provider would be.
89
91
 
90
92
  ## The platform's code checks
91
93
 
@@ -104,7 +106,49 @@ check.packages // the allowed packages this code requires
104
106
  version the sandbox has installed. Nothing else is available: no `fs`, no
105
107
  `http`, and no subpaths like `lodash/get`.
106
108
 
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.
109
+ `validatePluginCode` also refuses code over 100,000 characters, as the upload does.
110
+
111
+ ## Loading code the way the runner does
112
+
113
+ ```ts
114
+ import { executeOf, loadPluginCode, sandboxRequire, simulate } from "@visualautomate/plugin-sdk"
115
+
116
+ const exported = loadPluginCode(code, sandboxRequire((name) => require(name)), {
117
+ fetch: myGuardedFetch, // optional: the step's fetch
118
+ console: { log: (...a) => logs.push(a) },
119
+ })
120
+ const result = await simulate(executeOf(exported), { config })
121
+ ```
122
+
123
+ `loadPluginCode` compiles the code into a `node:vm` context of its own, wrapped
124
+ as the platform's runner wraps it, with the platform's globals (`fetch`, `btoa`,
125
+ `TextEncoder`, `Blob`, `FormData`, the typed arrays …) and no others: `Buffer`,
126
+ `process`, `globalThis`, `structuredClone` and the Node modules are undefined,
127
+ and `console` has `log`, `error`, `warn` and `info`. `executeOf` finds the
128
+ function the runner would call: the export itself, its `execute`, or a `default`
129
+ that is a function.
130
+
131
+ Objects a step returns belong to its own context, so compare them as JSON rather
132
+ than with a deep-equality that checks prototypes.
133
+
134
+ ## Checking a manifest
135
+
136
+ ```ts
137
+ import { checkManifest } from "@visualautomate/plugin-sdk"
138
+
139
+ const check = checkManifest(JSON.parse(manifestText), /* has code */ true)
140
+ check.ok // false when the portal would refuse it
141
+ check.kind // "code" | "declared" | "outputs-only" | "trigger"
142
+ check.errors // why it would be refused
143
+ check.warnings // what it would drop or regret
144
+ ```
145
+
146
+ The SDK's own reading of the rules the portal enforces, for code steps,
147
+ declared steps (`action`), triggers and `optionsFrom`; the portal's word is final.
148
+
149
+ ## The platform's facts
150
+
151
+ `PLATFORM_NODE_MAJOR`, `PROVIDED_GLOBALS`, `BLOCKED_GLOBAL_NAMES`,
152
+ `SANDBOX_TIERS`, `RETIRED_TIERS`, `STATE_LIMITS`, `SIGNED_URL_SECONDS`,
153
+ `MAX_CODE_LENGTH`, `SANDBOX_OVERRIDES` and `PUSH_CHECK` (the portal's default
154
+ `expectOutput` and time limits) are exported, generated from the platform.