suitescript-verify 0.2.0 → 1.0.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 +222 -10
- package/dist/cli.js +21 -5
- package/dist/config.js +9 -2
- package/dist/doctor.js +4 -1
- package/dist/init.js +66 -17
- package/dist/run.js +9 -5
- package/dist/shared.js +15 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# suitescript-verify
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/suitescript-verify)
|
|
4
|
+
[](https://github.com/nemchik/suitescript-verify/actions/workflows/ci.yml)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
|
|
3
7
|
Run probes in a NetSuite **sandbox** to verify what SuiteScript modules do, from a CLI that is safe to hand to an agent: sandbox only, a person approves every write at a terminal, and output is scrubbed of identifiers.
|
|
4
8
|
|
|
5
9
|
> This package never accepts Oracle's SuiteCloud license. Install `@oracle/suitecloud-cli` in your project and accept the license yourself.
|
|
@@ -9,19 +13,133 @@ npm install --save-dev suitescript-verify @oracle/suitecloud-cli
|
|
|
9
13
|
npx suitescript-verify help
|
|
10
14
|
```
|
|
11
15
|
|
|
12
|
-
|
|
16
|
+
This README is the whole guide and ships with each published version. Read the copy installed with the package, which matches the version you use: `suitescript-verify init` prints its path (it is in `node_modules/suitescript-verify/` under npm and pnpm).
|
|
17
|
+
|
|
18
|
+
## Contents
|
|
19
|
+
|
|
20
|
+
- [Verification method](#verification-method): when to verify, how, and how to record it
|
|
21
|
+
- [Setup](#setup): `init`, credentials, the role and `doctor`
|
|
22
|
+
- [Deploy](#deploy): put the runner in the sandbox
|
|
23
|
+
- [Probes](#probes): write and run them
|
|
24
|
+
- [Settings](#settings): every `SUITESCRIPT_VERIFY_` key
|
|
25
|
+
- [Troubleshooting](#troubleshooting): by symptom
|
|
26
|
+
- [Adding a lesson](#adding-a-lesson)
|
|
27
|
+
- [Development](#development)
|
|
28
|
+
|
|
29
|
+
## Verification method
|
|
30
|
+
|
|
31
|
+
Oracle's skills, type definitions and documentation disagree with each other and sometimes with the platform. A verification is a test run in a NetSuite account that shows what the platform actually does. Where a source's claim about platform behavior and the verified behavior differ, the verified behavior wins.
|
|
32
|
+
|
|
33
|
+
### When to verify
|
|
34
|
+
|
|
35
|
+
Verify, or mark the claim **unverified**, whenever you depend on what the platform accepts, rejects or returns, research how a module, method or script type behaves, or find a source that contradicts itself or another. An unverified claim stays marked unverified until a results page settles it. Do not mark a claim verified from a paraphrase of documentation: verified means a test ran.
|
|
36
|
+
|
|
37
|
+
### For agents
|
|
38
|
+
|
|
39
|
+
- If you can run the test yourself, do: a **probe** runs in a sandbox through the runner. Run `suitescript-verify doctor` first.
|
|
40
|
+
- Setup is mostly the person's (browser login, role, certificate). When a check fails, relay its next step and wait.
|
|
41
|
+
- Never run anything in production, never create credentials, and never run a real deploy or cleanup yourself: both need a person at a terminal.
|
|
42
|
+
- Prompt the person to run a snippet by hand (the methods below) only when a probe cannot run: setup cannot pass, the claim depends on a script type the runner does not cover (anything but a RESTlet), or the probe would write to the account. Give the exact snippet and say where to run it.
|
|
43
|
+
- Before you write a batch of probes, read [Probes](#probes) and [Troubleshooting](#troubleshooting): each probe change costs a deploy, and one bad probe can take the whole runner down.
|
|
44
|
+
|
|
45
|
+
### Methods
|
|
46
|
+
|
|
47
|
+
Record the method, the NetSuite version (for example `2026.2`; Help > About), and whether the account is a sandbox or production.
|
|
48
|
+
|
|
49
|
+
| Method | Use it for | Limits |
|
|
50
|
+
| ------------------------------- | ----------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
51
|
+
| The runner (a deployed RESTlet) | RESTlet behavior | Sandbox only; RESTlet only |
|
|
52
|
+
| Browser console | Client scripts | Runs in the browser's engine, so it says nothing about server-only behavior |
|
|
53
|
+
| Script debugger | Quick server checks (Customization > Scripting > Script Debugger) | Behaves differently from a deployed script in places; confirm a surprising result with a deployed script |
|
|
54
|
+
| A deployed script | The authoritative server result | Upload a script, create the Script record and a Deployment (status Testing, audience yourself) and call it; other script types can behave differently from a RESTlet, so test the type you care about |
|
|
55
|
+
|
|
56
|
+
In the browser console, open any record page, open the developer tools, load modules with `require` and print the results:
|
|
57
|
+
|
|
58
|
+
```js
|
|
59
|
+
require(["N/util"], function (u) {
|
|
60
|
+
console.log(JSON.stringify({ isArray: u.isArray([]) }));
|
|
61
|
+
});
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
**Note:** do not put the account number in a verification, nor an account URL, user, token or the internal id of a real record. They identify a company's account, they are not needed to reproduce a result, and they are a security risk in a public repository. Replace ids with placeholders.
|
|
65
|
+
|
|
66
|
+
### Documenting a verification
|
|
67
|
+
|
|
68
|
+
Write one page per verification (lowercase, hyphens, for example `util-each.md`) in your project's results folder. Every section is required: a verification without its test, results and decision is not a verification. `run <probe> --draft` writes the page with the test, NetSuite version and results filled in.
|
|
69
|
+
|
|
70
|
+
```markdown
|
|
71
|
+
# <What was verified>
|
|
72
|
+
|
|
73
|
+
- **NetSuite version:** 2026.2
|
|
74
|
+
- **Account type:** sandbox
|
|
75
|
+
- **Date:** YYYY-MM-DD
|
|
76
|
+
- **Method:** browser console | script debugger | deployed <script type> | deployed RESTlet (the runner)
|
|
77
|
+
- **Probe:** the path to the probe, when there is one
|
|
78
|
+
|
|
79
|
+
## Test
|
|
80
|
+
|
|
81
|
+
The script, verbatim, so anyone can run it again.
|
|
82
|
+
|
|
83
|
+
## Results
|
|
84
|
+
|
|
85
|
+
The raw output, including error messages.
|
|
86
|
+
|
|
87
|
+
## Explanation
|
|
88
|
+
|
|
89
|
+
What the results mean, and how they relate to what each source claims.
|
|
90
|
+
|
|
91
|
+
## Decision
|
|
92
|
+
|
|
93
|
+
What was decided because of the results.
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Setup
|
|
97
|
+
|
|
98
|
+
Every step that creates a credential or accepts a license is the person's. `init` creates none.
|
|
99
|
+
|
|
100
|
+
1. **Scaffold.** `suitescript-verify init` creates `verification/probes`, `verification/sdf`, your results folder and a committed `verification/.env` of layout keys, then prints a to-do list. `--dir <path>` moves `verification/`.
|
|
101
|
+
- The package imposes no layout for results. Where results pages live is your choice, set as `SUITESCRIPT_VERIFY_RESULTS_DIR` in the committed `.env` (a path inside the project).
|
|
102
|
+
- `init` will not pick it for you: at a terminal it asks (suggesting `docs/verification/results`), and without one it needs `--results-dir <path>`, for example `init --results-dir docs/verification/results`.
|
|
103
|
+
- A project that already sets the key is not asked again, and a command that needs the folder without the key stops and names it.
|
|
104
|
+
2. **Install the SuiteCloud CLI** (`@oracle/suitecloud-cli`) in your project and accept Oracle's license yourself. On a desktop machine its postinstall opens the license. A headless machine needs the maintainer to accept it first, or an install with `--ignore-scripts` (see [Troubleshooting](#the-suitecloud-cli-postinstall-exits-3)).
|
|
105
|
+
3. **Gitignore `verification/.env.local`** before saving any value: it holds credentials.
|
|
106
|
+
4. **Create the certificate pairs.** `suitescript-verify certificate` generates both key pairs outside the project with `openssl` (`--keys-dir`, default `~/.suitescript-verify`; `--only runner|sdf`; `--overwrite` or `--keep`).
|
|
107
|
+
|
|
108
|
+
| Pair | Key type | Used for |
|
|
109
|
+
| ------ | ------------------------------------ | -------------------------------------------------------- |
|
|
110
|
+
| Runner | EC P-256, signed ES256 | The token for calls to the runner |
|
|
111
|
+
| SDF | RSA-4096 with an RSA-PSS certificate | Oracle's SDK login (PS256), which only parses an RSA key |
|
|
112
|
+
|
|
113
|
+
Oracle allows one certificate per integration, role and entity, so the two pairs cannot be shared, and an EC key fails the SDF login (see [Troubleshooting](#setup-ci-fails-with-an-error-with-the-private-key)).
|
|
114
|
+
5. **In the sandbox, create the integration record, the role and the certificate mappings.**
|
|
115
|
+
- The integration needs more than the machine-to-machine grant: also enable the token-based authentication flags and the RESTlets and REST Web Services scopes (see [Troubleshooting](#every-call-fails-with-401-invalid_login_attempt)).
|
|
116
|
+
- The runner is limited to a role with the id `customrole_suitescript_verify`, which you create. Use your own id by setting `SUITESCRIPT_VERIFY_ROLE_ID`; do not rely on a fixed id, since a custom role's id may be read-only after saving.
|
|
117
|
+
- Keep the role minimal (see [Probes](#probes)).
|
|
118
|
+
6. **Save the settings.** `suitescript-verify passkey` generates the SuiteCloud passkey into `verification/.env.local`. `suitescript-verify env set NAME=value` saves any other key there (values are never printed; a different existing value is kept unless `--overwrite`). See [Settings](#settings) for the names.
|
|
119
|
+
7. **Run `suitescript-verify doctor`** until every step passes. It checks the SuiteCloud CLI, account, sandbox, certificates, passkey and project files, says which failing steps are the maintainer's own, and exits 1 while any fails.
|
|
120
|
+
8. **`suitescript-verify setup-ci`** (for a pipeline) registers the SDF credentials (auth id `suitescript-verify`) with your project's SuiteCloud CLI, passing the passkey through the environment, never an argument. On success it prints the `SUITESCRIPT_VERIFY_` names the pipeline must supply as its own secrets, never a value. It writes no file, no credential variable and no license variable. Accepting Oracle's license in the pipeline is yours.
|
|
121
|
+
|
|
122
|
+
**Note:** never put `SUITECLOUD_CI_PASSKEY` in your user-wide environment. It stops browser login in your other SuiteCloud projects. Keep the passkey in `.env.local`, where the package hands it to the CLI for its own child process only.
|
|
13
123
|
|
|
14
|
-
|
|
124
|
+
Every command that reaches the sandbox refuses an account that is not a sandbox (`digits_SBn`).
|
|
15
125
|
|
|
16
|
-
|
|
126
|
+
## Deploy
|
|
17
127
|
|
|
18
|
-
|
|
128
|
+
```sh
|
|
129
|
+
suitescript-verify deploy # generate, then dry-run
|
|
130
|
+
suitescript-verify deploy --approved # the real deploy: a person at a terminal types DEPLOY
|
|
131
|
+
suitescript-verify cleanup --approved # turns the runner's deployment off: the person types CLEANUP
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
`deploy` generates the runner and an SDF project into `verification/sdf` (the generated `src/` there is yours to gitignore) from the RESTlet probes in `verification/probes`, then dry-runs it through your project's SuiteCloud CLI. `cleanup` does the same with the deployment switched off, and without `--approved` it is a dry run. The runner's script id is `customscript_suitescript_verify_runner`.
|
|
19
135
|
|
|
20
|
-
|
|
136
|
+
- **Every probe change needs a deploy.** The runner's probe list is generated into its dependency list, so a new, renamed or edited probe only exists in the sandbox after a deploy. Write and review **all** the probes of a batch first, dry-run once, and ask for one deploy. A deploy is the person's, in their own terminal, so a wasted one costs a person's time.
|
|
137
|
+
- **After every deploy, run the chain-proving probe first** (a trivial probe, for example `probe-zero`, that only proves the runner loads). If it fails, the runner is broken and no other result means anything (see [Troubleshooting](#every-probe-returns-http-400-and-names-no-probe)).
|
|
138
|
+
- **A sandbox refresh clears the token mappings and, presumably, the deployed runner.** After one, redo the role, mapping and deploy steps.
|
|
21
139
|
|
|
22
|
-
|
|
140
|
+
## Probes
|
|
23
141
|
|
|
24
|
-
|
|
142
|
+
A probe is a version-controlled module that exercises one platform behavior and returns the result as data. It must declare `@scriptType Restlet` and no `@writes`.
|
|
25
143
|
|
|
26
144
|
```js
|
|
27
145
|
/**
|
|
@@ -33,13 +151,107 @@ define(["N/record"], (record) => ({
|
|
|
33
151
|
}));
|
|
34
152
|
```
|
|
35
153
|
|
|
36
|
-
|
|
154
|
+
### Running one
|
|
155
|
+
|
|
156
|
+
```sh
|
|
157
|
+
suitescript-verify run <probe> [--param name=value ...] [--draft]
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
- The name must be one of the probes in the folder: the runner only runs the fixed list generated into it and never code from a request.
|
|
161
|
+
- Output is scrubbed of the account, client and certificate ids, and refused if it still looks like an identifier.
|
|
162
|
+
- A probe with an `<probe>.expect.json` is compared with it, and a difference exits 2 as drift. The expectation is the `result` object, sorted and indented, not the whole output. Commit it only once the output is stable: a flaky probe reports drift.
|
|
163
|
+
- `--draft` writes `<probe>.md` into your results folder, never over an existing page. A draft goes stale: after a probe changes, refresh the page's Test and Results sections from the probe and its expected output.
|
|
164
|
+
|
|
165
|
+
### Parameters
|
|
166
|
+
|
|
167
|
+
Declare each parameter in the probe's leading JSDoc block, one tag per parameter: `@param <name> <pattern>`.
|
|
168
|
+
|
|
169
|
+
- The name is lowercase-initial letters and digits (`recordType`).
|
|
170
|
+
- The pattern is a JavaScript regular expression (Unicode mode) that the whole value must match, so `[a-z_]+` cannot match `a;b`.
|
|
171
|
+
- Pass each value with `run <probe> --param name=value`. Every declared parameter is required and its value is always a string.
|
|
172
|
+
- The runner validates the parameters against the declarations before any request, rejecting an invalid, undeclared, missing or repeated one. A probe that declares none takes none.
|
|
173
|
+
- The generated runner checks them again and hands the probe only declared strings in `run(params)`. A value goes in the JSON request body and is never evaluated or turned into code.
|
|
174
|
+
- A bad `@param` tag (no pattern, an invalid pattern, a duplicate name) stops `run` and `deploy`.
|
|
175
|
+
|
|
176
|
+
### Tags
|
|
177
|
+
|
|
178
|
+
The package validates the tags in a probe's leading JSDoc block: `@scriptType`, `@writes`, `@param`, the NetSuite `@NApiVersion`, `@NScriptType` and `@NModuleScope`, and the ordinary `@see`, `@link`, `@example`, `@returns`, `@since` and `@deprecated`. Any other tag is reported and stops `run` and `deploy`, so a typo cannot pass silently. A project that keeps its own tags registers them with `SUITESCRIPT_VERIFY_EXTRA_TAGS`, a comma-separated list of tag names without the `@` (for example `flavor,other`). The package checks only that a registered tag is known; what a tag means is up to the project.
|
|
179
|
+
|
|
180
|
+
### Writing probes that do not cost a deploy
|
|
181
|
+
|
|
182
|
+
- **Keep module-scope code rare and tiny.** The runner loads every probe as a dependency of its own `define()`. A probe whose top-level code fails in a way its own `try/catch` cannot capture takes the whole runner down. If you need one, make it one governed call, run it last and one per deploy, so a failure points at one call. Do not keep a probe that is expected to take the runner down; its results page carries the code.
|
|
183
|
+
- **The role is minimal.** A probe that searches transactions or creates a record gets `INSUFFICIENT_PERMISSION`. That error comes after type validation, which is often still evidence, but a probe that needs a record permission should say so in its header. Prefer objects that need no permission (`runtime`, `search.createColumn`).
|
|
184
|
+
- **Check how an API is chained before relying on it.** `crypto.createHash(...).update(...)` returns nothing, so chaining `.digest()` threw `Cannot read property 'digest' of undefined` and cost a deploy. Read the method page for the return type.
|
|
185
|
+
- **Outbound calls rate-limit.** A public echo service answered four requests in a row and failed the fifth with a body that had no `headers`. Keep a probe to four requests or fewer and put the status code in the failure, not just a `TypeError`.
|
|
186
|
+
- **Write probes with your editor or file tools, not a shell heredoc.** Backticks, `${}` and quotes in a probe broke a shell heredoc on Windows.
|
|
187
|
+
- **An agent's tool can refuse the deploy command.** An auto-mode classifier blocked a deploy dry run chained in one shell line with other commands. Run the dry run alone and say so if it is still refused; the real deploy is the person's anyway.
|
|
188
|
+
|
|
189
|
+
## Settings
|
|
190
|
+
|
|
191
|
+
Every setting is a `SUITESCRIPT_VERIFY_` key. They layer from the committed `verification/.env`, the gitignored `verification/.env.local` and the process environment, in that order. The committed file may hold only the keys marked "committed", and its values must not look like a secret. A bad value stops every command that reaches the sandbox, and the error names the key, never the value.
|
|
192
|
+
|
|
193
|
+
| Key | Where | What it is |
|
|
194
|
+
| ----------------------------------------- | --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
195
|
+
| `SUITESCRIPT_VERIFY_PROBES_DIR` | committed | Folder of probes (a path in the project) |
|
|
196
|
+
| `SUITESCRIPT_VERIFY_RESULTS_DIR` | committed | Folder of results pages (a path in the project). No default: you choose |
|
|
197
|
+
| `SUITESCRIPT_VERIFY_SDF_DIR` | committed | Folder for the generated SDF project (a path in the project) |
|
|
198
|
+
| `SUITESCRIPT_VERIFY_ROLE_ID` | committed | The runner's role id, matching `^customrole_[a-z0-9_]+$`; default `customrole_suitescript_verify`. Used in the generated manifest and the runner, and reported by `doctor` |
|
|
199
|
+
| `SUITESCRIPT_VERIFY_EXTRA_TAGS` | committed | Extra probe tag names, comma-separated, without the `@` |
|
|
200
|
+
| `SUITESCRIPT_VERIFY_ACCOUNT` | local | The sandbox account (`digits_SBn`) |
|
|
201
|
+
| `SUITESCRIPT_VERIFY_CLIENT_ID` | local | The integration's client id |
|
|
202
|
+
| `SUITESCRIPT_VERIFY_CERTIFICATE_ID` | local | Certificate id of the runner pair |
|
|
203
|
+
| `SUITESCRIPT_VERIFY_PRIVATE_KEY_PATH` | local | Path to the runner pair's private key (outside the project) |
|
|
204
|
+
| `SUITESCRIPT_VERIFY_SDF_CERTIFICATE_ID` | local | Certificate id of the SDF pair |
|
|
205
|
+
| `SUITESCRIPT_VERIFY_SDF_PRIVATE_KEY_PATH` | local | Path to the SDF pair's private key (outside the project) |
|
|
206
|
+
| `SUITESCRIPT_VERIFY_PASSKEY` | local | The SuiteCloud passkey, saved by `passkey` |
|
|
207
|
+
| `SUITESCRIPT_VERIFY_DOMAIN` | local | Optional NetSuite domain passed to the SuiteCloud CLI |
|
|
208
|
+
|
|
209
|
+
"Local" keys go in `.env.local` or the process environment (a pipeline's secrets), never in the committed file.
|
|
210
|
+
|
|
211
|
+
## Troubleshooting
|
|
212
|
+
|
|
213
|
+
### Every probe returns HTTP 400 and names no probe
|
|
214
|
+
|
|
215
|
+
The message looks like `com.netsuite.suitescript.scriptobject.ScriptNullObjectAdapter@...`. One probe's top-level code failed in a way its own `try/catch` cannot capture, so the runner never loaded.
|
|
216
|
+
|
|
217
|
+
1. Move the suspect probe out of the probes folder.
|
|
218
|
+
2. Dry-run, then ask the person for a redeploy.
|
|
219
|
+
|
|
220
|
+
### A probe returns `INSUFFICIENT_PERMISSION`
|
|
221
|
+
|
|
222
|
+
The runner's role is minimal, so a probe that searches transactions or creates a record gets this error. It comes after type validation, which is often still evidence. See [Probes](#writing-probes-that-do-not-cost-a-deploy) for how to write around it.
|
|
223
|
+
|
|
224
|
+
### Every call fails with `401 INVALID_LOGIN_ATTEMPT`
|
|
225
|
+
|
|
226
|
+
With the machine-to-machine grant alone, the token endpoint issues a token but every call fails. On the integration record, also enable the token-based authentication flags and the RESTlets and REST Web Services scopes.
|
|
227
|
+
|
|
228
|
+
### `setup-ci` fails with "an error with the private key"
|
|
229
|
+
|
|
230
|
+
The full message is "There was an error with the private key used to authenticate". The SDF pair is not RSA. Oracle's SDK signs the SDF login with PS256 and only parses an RSA key. Use an RSA-4096 key with an RSA-PSS certificate for the SDF pair, and an EC P-256 pair for the runner. They cannot share a pair (see [Setup](#setup)).
|
|
231
|
+
|
|
232
|
+
### An authentication or "not found" error after a sandbox refresh
|
|
233
|
+
|
|
234
|
+
A refresh clears the token mappings and, presumably, the deployed runner. Redo the role, mapping and deploy steps.
|
|
235
|
+
|
|
236
|
+
### The SuiteCloud CLI postinstall exits 3
|
|
237
|
+
|
|
238
|
+
On a headless machine, with `SUITECLOUD_CLI_ACCEPT_LICENSE` unset, the postinstall opens the license with `start`, `open` or `xdg-open`, which fails without a desktop. A desktop install is fine. A headless job needs the maintainer to accept the license, or an install with `--ignore-scripts`.
|
|
239
|
+
|
|
240
|
+
### SuiteCloud browser login stopped working in another project
|
|
241
|
+
|
|
242
|
+
A user-wide `SUITECLOUD_CI_PASSKEY` stops browser login in your other SuiteCloud projects. Remove it from the environment and keep the passkey in `verification/.env.local`.
|
|
243
|
+
|
|
244
|
+
## Adding a lesson
|
|
245
|
+
|
|
246
|
+
A **lesson** is a short, generic account of a runner, deploy, probe or sandbox problem: what happened, what it cost and how to avoid it. Keep it concrete (the message, the cause, the fix), and never put an account number, id, token or real record id in it. Fix an entry that proves wrong instead of adding a second one that contradicts it.
|
|
37
247
|
|
|
38
|
-
|
|
248
|
+
- **A generic problem with this package or its flow:** open an issue or pull request at [nemchik/suitescript-verify](https://github.com/nemchik/suitescript-verify). It is added in the same pull request to the matching section above (setup, deploy, probes or troubleshooting), so it ships with the next version.
|
|
249
|
+
- **A release or toolchain problem in that repository:** it is added to the repository's `docs/maintaining.md` instead, which is not shipped because it does not apply to anyone using the package.
|
|
250
|
+
- **A finding about NetSuite itself:** write a [results page](#documenting-a-verification) in your own project. It is not a lesson.
|
|
39
251
|
|
|
40
252
|
## Development
|
|
41
253
|
|
|
42
|
-
You need the Node.js and npm versions in `package.json` `devEngines`.
|
|
254
|
+
You need the Node.js and npm versions in `package.json` `devEngines` (mirrored in `.nvmrc`). `npm pack` and `npm test` refuse any other Node version.
|
|
43
255
|
|
|
44
256
|
```sh
|
|
45
257
|
npm install
|
package/dist/cli.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { createInterface } from "node:readline/promises";
|
|
2
3
|
import { certificate } from "./certificate.js";
|
|
3
|
-
import { loadConfig } from "./config.js";
|
|
4
|
+
import { loadConfig, resultsDirGuidance, resultsDirKey, suggestedResultsDir, } from "./config.js";
|
|
4
5
|
import { formatChecks, names, runChecks } from "./doctor.js";
|
|
5
6
|
import { envNames } from "./envfile.js";
|
|
6
7
|
import { init } from "./init.js";
|
|
@@ -10,7 +11,7 @@ import { confirmWrite, flagValue, positionals, requireSandbox, saveEnv, suiteclo
|
|
|
10
11
|
export const USAGE = `Usage: suitescript-verify <command> [options]
|
|
11
12
|
|
|
12
13
|
Commands:
|
|
13
|
-
init
|
|
14
|
+
init [--results-dir <path>] Scaffold the folders and .env, and print a to-do list; the results folder is yours to choose (suggested: docs/verification/results)
|
|
14
15
|
doctor Check the setup; each failing step says who does it
|
|
15
16
|
certificate [--only runner|sdf] [--keys-dir <path>] [--overwrite | --keep]
|
|
16
17
|
Generate the key pairs outside the project
|
|
@@ -53,8 +54,23 @@ function doctor(args) {
|
|
|
53
54
|
console.log(formatChecks(checks));
|
|
54
55
|
return config.problems.length === 0 && checks.every((c) => c.ok) ? 0 : 1;
|
|
55
56
|
}
|
|
56
|
-
|
|
57
|
-
|
|
57
|
+
/** The results folder `init` was told to use: the flag, or a prompt at a terminal when the key is not set. Never a silent default. */
|
|
58
|
+
async function chooseResultsDir(config, args) {
|
|
59
|
+
const flagged = flagValue(args, "--results-dir");
|
|
60
|
+
// Already configured: the flag is passed on only so `init` can say it was ignored.
|
|
61
|
+
if (config.resultsDir !== undefined || flagged !== undefined)
|
|
62
|
+
return flagged;
|
|
63
|
+
if (!process.stdin.isTTY) {
|
|
64
|
+
throw new UsageError(`${resultsDirKey} is not set. ${resultsDirGuidance} Nothing was created.`);
|
|
65
|
+
}
|
|
66
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
67
|
+
const answer = await rl.question(`Where should results pages live in this project? [${suggestedResultsDir}] `);
|
|
68
|
+
rl.close();
|
|
69
|
+
return answer.trim() || suggestedResultsDir;
|
|
70
|
+
}
|
|
71
|
+
async function initCommand(args) {
|
|
72
|
+
const config = reporting(args);
|
|
73
|
+
console.log(init(process.cwd(), config, await chooseResultsDir(config, args)));
|
|
58
74
|
return 0;
|
|
59
75
|
}
|
|
60
76
|
async function deploy(args) {
|
|
@@ -185,7 +201,7 @@ export async function main(args) {
|
|
|
185
201
|
if (command === "doctor")
|
|
186
202
|
return doctor(rest);
|
|
187
203
|
if (command === "init")
|
|
188
|
-
return initCommand(rest);
|
|
204
|
+
return await initCommand(rest);
|
|
189
205
|
throw new UsageError(`Unknown command "${command}".\n\n${USAGE}`);
|
|
190
206
|
}
|
|
191
207
|
catch (error) {
|
package/dist/config.js
CHANGED
|
@@ -8,6 +8,11 @@ export const layoutKeys = [
|
|
|
8
8
|
"SUITESCRIPT_VERIFY_RESULTS_DIR",
|
|
9
9
|
"SUITESCRIPT_VERIFY_SDF_DIR",
|
|
10
10
|
];
|
|
11
|
+
export const resultsDirKey = "SUITESCRIPT_VERIFY_RESULTS_DIR";
|
|
12
|
+
/** What `init` suggests for the results folder; it is never applied without the project's say-so. */
|
|
13
|
+
export const suggestedResultsDir = "docs/verification/results";
|
|
14
|
+
/** What every command says when the results folder is not chosen: one sentence, so the commands cannot drift apart. */
|
|
15
|
+
export const resultsDirGuidance = `Choose where results pages live in your project, then run suitescript-verify init --results-dir <path> (${suggestedResultsDir} is the suggested value) or set ${resultsDirKey} in the committed .env.`;
|
|
11
16
|
/** The committed `.env` may also register extra probe tags: a comma-separated list of tag names, not a path. */
|
|
12
17
|
export const extraTagsKey = "SUITESCRIPT_VERIFY_EXTRA_TAGS";
|
|
13
18
|
/** The committed `.env` may also name the runner role: a NetSuite custom role id. */
|
|
@@ -45,7 +50,7 @@ function readEnvFile(path) {
|
|
|
45
50
|
? parseEnv(readFileSync(path, "utf8"))
|
|
46
51
|
: new Map();
|
|
47
52
|
}
|
|
48
|
-
const insideProject = (project, path) => {
|
|
53
|
+
export const insideProject = (project, path) => {
|
|
49
54
|
const rel = relative(project, resolve(project, path));
|
|
50
55
|
return !isAbsolute(rel) && rel.split(/[\\/]/)[0] !== "..";
|
|
51
56
|
};
|
|
@@ -153,7 +158,9 @@ export function loadConfig(project, dir, processEnv = process.env) {
|
|
|
153
158
|
...checkLocalIgnored(project, localFile),
|
|
154
159
|
...roleProblems,
|
|
155
160
|
],
|
|
156
|
-
resultsDir:
|
|
161
|
+
resultsDir: values[resultsDirKey]
|
|
162
|
+
? resolve(project, values[resultsDirKey])
|
|
163
|
+
: undefined,
|
|
157
164
|
roleId,
|
|
158
165
|
root,
|
|
159
166
|
sdfDir: path("SUITESCRIPT_VERIFY_SDF_DIR", join(root, "sdf")),
|
package/dist/doctor.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createPrivateKey } from "node:crypto";
|
|
2
2
|
import { existsSync, readFileSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
+
import { insideProject, resultsDirGuidance, resultsDirKey, } from "./config.js";
|
|
4
5
|
export const names = {
|
|
5
6
|
account: "SUITESCRIPT_VERIFY_ACCOUNT",
|
|
6
7
|
certificateId: "SUITESCRIPT_VERIFY_CERTIFICATE_ID",
|
|
@@ -76,7 +77,9 @@ export function runChecks(project, config, env = process.env) {
|
|
|
76
77
|
// A malformed project.json fails the step.
|
|
77
78
|
}
|
|
78
79
|
}
|
|
79
|
-
|
|
80
|
+
add(`The results folder is set, inside the project (${resultsDirKey})`, config.resultsDir !== undefined &&
|
|
81
|
+
insideProject(project, config.resultsDir), "maintainer", resultsDirGuidance);
|
|
82
|
+
const absent = [config.probesDir, config.resultsDir, config.sdfDir].filter((dir) => dir !== undefined && !existsSync(dir));
|
|
80
83
|
add("Project files: the probe, results and SDF folders exist and the SDF project has a default auth id", absent.length === 0 && authId !== "", absent.length > 0 ? "agent" : "maintainer", absent.length > 0
|
|
81
84
|
? "Run suitescript-verify init to create the missing folders."
|
|
82
85
|
: "Run account:setup:ci from the SDF folder once the steps above pass, so project.json gets a default auth id.");
|
package/dist/init.js
CHANGED
|
@@ -1,35 +1,84 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
2
|
-
import { dirname, join, relative } from "node:path";
|
|
1
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync, } from "node:fs";
|
|
2
|
+
import { dirname, join, relative, resolve } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { insideProject, resultsDirKey } from "./config.js";
|
|
5
|
+
import { UsageError } from "./shared.js";
|
|
6
|
+
/** The README shipped with the running package, wherever the package manager put it. */
|
|
7
|
+
const readme = fileURLToPath(new URL("../README.md", import.meta.url));
|
|
8
|
+
/** A path relative to the project, with forward slashes, for messages and the committed `.env`. */
|
|
9
|
+
const shown = (project, path) => relative(project, path).replaceAll("\\", "/");
|
|
3
10
|
/**
|
|
4
|
-
*
|
|
5
|
-
* the
|
|
11
|
+
* Which folder holds results: the one the configuration already sets, else the
|
|
12
|
+
* one the project just chose. `ignoredNote` is non-empty when a choice was
|
|
13
|
+
* given but the key was already set. Refuses a choice outside the project
|
|
14
|
+
* before anything is created.
|
|
6
15
|
*/
|
|
7
|
-
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
16
|
+
function pickResultsDir(project, config, chosenResultsDir) {
|
|
17
|
+
const chosen = chosenResultsDir === undefined
|
|
18
|
+
? undefined
|
|
19
|
+
: resolve(project, chosenResultsDir);
|
|
20
|
+
if (chosen !== undefined && !insideProject(project, chosen)) {
|
|
21
|
+
throw new UsageError(`${resultsDirKey} must be a path inside the project, because the committed .env holds layout only. Nothing was created.`);
|
|
22
|
+
}
|
|
23
|
+
if (config.resultsDir !== undefined) {
|
|
24
|
+
const ignoredNote = chosen !== undefined && chosen !== config.resultsDir
|
|
25
|
+
? `${resultsDirKey} is already set to ${shown(project, config.resultsDir)}, so --results-dir ${shown(project, chosen)} was ignored. Change the key itself to move the results folder.\n\n`
|
|
26
|
+
: "";
|
|
27
|
+
return { alreadySet: true, dir: config.resultsDir, ignoredNote };
|
|
28
|
+
}
|
|
29
|
+
if (chosen === undefined) {
|
|
30
|
+
throw new Error("init needs a results folder; the caller must choose one.");
|
|
31
|
+
}
|
|
32
|
+
return { alreadySet: false, dir: chosen, ignoredNote: "" };
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Writes the layout keys to the committed `.env`: a new file with all of them,
|
|
36
|
+
* or, for an existing one, only the results key when it is missing. Returns
|
|
37
|
+
* whether the file was created.
|
|
38
|
+
*/
|
|
39
|
+
function writeLayoutEnv(project, config, resultsDir) {
|
|
12
40
|
const envFile = join(config.root, ".env");
|
|
13
|
-
const
|
|
14
|
-
if (
|
|
41
|
+
const results = `${resultsDirKey}=${shown(project, resultsDir.dir)}`;
|
|
42
|
+
if (!existsSync(envFile)) {
|
|
15
43
|
mkdirSync(dirname(envFile), { recursive: true });
|
|
16
44
|
writeFileSync(envFile, [
|
|
17
45
|
"# Layout only: this file is committed. Credentials go in .env.local, which is gitignored.",
|
|
18
|
-
`SUITESCRIPT_VERIFY_PROBES_DIR=${
|
|
19
|
-
|
|
20
|
-
`SUITESCRIPT_VERIFY_SDF_DIR=${
|
|
46
|
+
`SUITESCRIPT_VERIFY_PROBES_DIR=${shown(project, config.probesDir)}`,
|
|
47
|
+
results,
|
|
48
|
+
`SUITESCRIPT_VERIFY_SDF_DIR=${shown(project, config.sdfDir)}`,
|
|
21
49
|
"",
|
|
22
50
|
].join("\n"));
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
if (!resultsDir.alreadySet) {
|
|
54
|
+
const text = readFileSync(envFile, "utf8");
|
|
55
|
+
const lead = text === "" || text.endsWith("\n") ? "" : "\n";
|
|
56
|
+
appendFileSync(envFile, `${lead}${results}\n`);
|
|
23
57
|
}
|
|
24
|
-
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Scaffolds the folders and a committed `.env` of layout keys, then returns
|
|
62
|
+
* the to-do list for the person. Creates no credential and installs nothing.
|
|
63
|
+
* `chosenResultsDir` is the project's own choice for the results folder; it is
|
|
64
|
+
* used only when the configuration does not already set one.
|
|
65
|
+
*/
|
|
66
|
+
export function init(project, config, chosenResultsDir) {
|
|
67
|
+
const results = pickResultsDir(project, config, chosenResultsDir);
|
|
68
|
+
const dirs = [config.probesDir, results.dir, config.sdfDir];
|
|
69
|
+
for (const dir of dirs)
|
|
70
|
+
mkdirSync(dir, { recursive: true });
|
|
71
|
+
const created = writeLayoutEnv(project, config, results);
|
|
72
|
+
const envFile = shown(project, join(config.root, ".env"));
|
|
73
|
+
const local = shown(project, join(config.root, ".env.local"));
|
|
25
74
|
return [
|
|
26
|
-
`${created ? "Created" : "Kept"} ${
|
|
75
|
+
`${results.ignoredNote}${created ? "Created" : "Kept"} ${envFile} and the folders ${dirs.map((dir) => shown(project, dir)).join(", ")}. No credentials were created.`,
|
|
27
76
|
"",
|
|
28
77
|
"To do (each step is yours, the maintainer's):",
|
|
29
78
|
"1. Install the SuiteCloud CLI: npm install --save-dev @oracle/suitecloud-cli. Read Oracle's license and accept it yourself; this package never does.",
|
|
30
79
|
`2. Add ${local} to .gitignore before saving any value: it holds credentials.`,
|
|
31
80
|
"3. In a sandbox, create the integration record, the role and the certificate mappings, and generate the private keys outside the repository.",
|
|
32
|
-
`4. Save the account, client id, certificate ids, key paths and a passkey in ${local} (
|
|
81
|
+
`4. Save the account, client id, certificate ids, key paths and a passkey in ${local}. Setup is described in the installed package README: ${shown(project, readme)}`,
|
|
33
82
|
"5. Run suitescript-verify doctor until every step passes.",
|
|
34
83
|
].join("\n");
|
|
35
84
|
}
|
package/dist/run.js
CHANGED
|
@@ -3,7 +3,7 @@ import { join, relative } from "node:path";
|
|
|
3
3
|
import { clientAssertion, restletUrl, scrub, tokenUrl } from "./netsuite.js";
|
|
4
4
|
import { anchored, isRestlet, isSlug, listProbes, matchesExpect, read, readExpect, tagProblems, } from "./probes.js";
|
|
5
5
|
import { resultsDraft } from "./results.js";
|
|
6
|
-
import { positionals, requireSandbox, UsageError } from "./shared.js";
|
|
6
|
+
import { positionals, requireResultsDir, requireSandbox, UsageError, } from "./shared.js";
|
|
7
7
|
/** Every `--param name=value` in the arguments, in order. The value may start with anything. */
|
|
8
8
|
function suppliedParams(args) {
|
|
9
9
|
const out = [];
|
|
@@ -63,6 +63,10 @@ function validateParams(probe, args) {
|
|
|
63
63
|
* code: the runner only knows the probes generated into it by `deploy`.
|
|
64
64
|
*/
|
|
65
65
|
export async function runProbe(project, config, args) {
|
|
66
|
+
// A missing results key is the project's own layout choice: report it before the credentials.
|
|
67
|
+
const resultsDir = args.includes("--draft")
|
|
68
|
+
? requireResultsDir(config)
|
|
69
|
+
: undefined;
|
|
66
70
|
const creds = requireSandbox(config);
|
|
67
71
|
const name = positionals(args)[0] ?? "";
|
|
68
72
|
const probes = listProbes(config.probesDir, config.extraTags);
|
|
@@ -73,7 +77,7 @@ export async function runProbe(project, config, args) {
|
|
|
73
77
|
throw new UsageError(`Unknown probe "${name}". Known: ${probes.map((p) => p.slug).join(", ") || "(none)"}`);
|
|
74
78
|
}
|
|
75
79
|
if (!isRestlet(probe)) {
|
|
76
|
-
throw new UsageError(`${probe.slug} needs a ${probe.scriptType || "(undeclared)"} script. The runner only runs RESTlet probes (declare @scriptType Restlet); ask the user to run it by hand
|
|
80
|
+
throw new UsageError(`${probe.slug} needs a ${probe.scriptType || "(undeclared)"} script. The runner only runs RESTlet probes (declare @scriptType Restlet); ask the user to run it by hand in a sandbox console.`);
|
|
77
81
|
}
|
|
78
82
|
if (probe.writes) {
|
|
79
83
|
throw new UsageError(`${probe.slug} declares @writes, which needs the user's separate approval. That path is not automated.`);
|
|
@@ -156,9 +160,9 @@ export async function runProbe(project, config, args) {
|
|
|
156
160
|
console.log(`\nDRIFT: the output differs from ${probe.slug}.expect.json. The platform changed, or the expectation is wrong.`);
|
|
157
161
|
}
|
|
158
162
|
}
|
|
159
|
-
if (
|
|
163
|
+
if (resultsDir !== undefined) {
|
|
160
164
|
const probeFile = join(config.probesDir, `${probe.slug}.js`);
|
|
161
|
-
const page = join(
|
|
165
|
+
const page = join(resultsDir, `${probe.slug}.md`);
|
|
162
166
|
const shown = relative(project, page).replaceAll("\\", "/");
|
|
163
167
|
if (existsSync(page)) {
|
|
164
168
|
console.log(`\n${shown} already exists; not overwriting.`);
|
|
@@ -169,7 +173,7 @@ export async function runProbe(project, config, args) {
|
|
|
169
173
|
output: body.result,
|
|
170
174
|
probe,
|
|
171
175
|
probeFile,
|
|
172
|
-
resultsDir
|
|
176
|
+
resultsDir,
|
|
173
177
|
scriptSource: read(probeFile),
|
|
174
178
|
version: body.version ?? "unknown",
|
|
175
179
|
}), known);
|
package/dist/shared.js
CHANGED
|
@@ -2,12 +2,19 @@ import { spawnSync } from "node:child_process";
|
|
|
2
2
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
4
|
import { createInterface } from "node:readline/promises";
|
|
5
|
+
import { resultsDirGuidance, resultsDirKey } from "./config.js";
|
|
5
6
|
import { isSandboxAccount, names } from "./doctor.js";
|
|
6
7
|
import { applyEnvUpdate, planEnvUpdate } from "./envfile.js";
|
|
7
8
|
/** A refusal or mistake to report to the person; the message holds no secret. */
|
|
8
9
|
export class UsageError extends Error {
|
|
9
10
|
}
|
|
10
|
-
const valueFlags = [
|
|
11
|
+
const valueFlags = [
|
|
12
|
+
"--dir",
|
|
13
|
+
"--keys-dir",
|
|
14
|
+
"--only",
|
|
15
|
+
"--param",
|
|
16
|
+
"--results-dir",
|
|
17
|
+
];
|
|
11
18
|
/** The value after a flag, when given. */
|
|
12
19
|
export function flagValue(args, flag) {
|
|
13
20
|
const at = args.indexOf(flag);
|
|
@@ -110,6 +117,13 @@ export async function saveEnv(config, args, entries) {
|
|
|
110
117
|
console.log(`Kept the existing ${name}. To replace it, ask the user, then rerun with --overwrite.`);
|
|
111
118
|
}
|
|
112
119
|
}
|
|
120
|
+
/** The results folder, or a refusal naming the key: the package imposes no layout for results. */
|
|
121
|
+
export function requireResultsDir(config) {
|
|
122
|
+
if (config.resultsDir === undefined) {
|
|
123
|
+
throw new UsageError(`${resultsDirKey} is not set. ${resultsDirGuidance}`);
|
|
124
|
+
}
|
|
125
|
+
return config.resultsDir;
|
|
126
|
+
}
|
|
113
127
|
/** The runner credentials, or a refusal: every command that reaches the sandbox needs a sandbox account. */
|
|
114
128
|
export function requireSandbox(config) {
|
|
115
129
|
const v = config.values;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "suitescript-verify",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"description": "Run probes in a NetSuite sandbox to verify what SuiteScript modules do, with guardrails for agents.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"suitescript",
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
"@types/node": "24.19.2",
|
|
51
51
|
"git-pull-run": "2.1.0",
|
|
52
52
|
"lefthook": "2.2.2",
|
|
53
|
-
"skills": "1.
|
|
53
|
+
"skills": "1.8.1",
|
|
54
54
|
"typescript": "7.0.2"
|
|
55
55
|
},
|
|
56
56
|
"peerDependencies": {
|