suitescript-verify 0.1.0 → 0.2.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
@@ -33,7 +33,7 @@ define(["N/record"], (record) => ({
33
33
  }));
34
34
  ```
35
35
 
36
- Every command that reaches the sandbox refuses an account that is not a sandbox (`digits_SBn`). The runner uses the script id `customscript_suitescript_verify_runner` and is limited to a role with the ID `customrole_suitescript_verify`, which you create in the sandbox.
36
+ Every command that reaches the sandbox refuses an account that is not a sandbox (`digits_SBn`). The runner uses the script id `customscript_suitescript_verify_runner` and is limited to a role with the ID `customrole_suitescript_verify`, which you create in the sandbox. A project that names its own role sets `SUITESCRIPT_VERIFY_ROLE_ID` (for example `SUITESCRIPT_VERIFY_ROLE_ID=customrole_my_runner`); the generated SDF project uses it in the manifest and the runner. Like every `SUITESCRIPT_VERIFY_` setting it layers from the committed `verification/.env`, the gitignored `.env.local` and the process environment. The value must match `^customrole_[a-z0-9_]+$`; a bad value stops every command that reaches the sandbox, and the error names the key, never the value. `doctor` reports the id in use.
37
37
 
38
38
  See [docs/verification/README.md](docs/verification/README.md) and [docs/verification/lessons.md](docs/verification/lessons.md).
39
39
 
package/dist/cli.js CHANGED
@@ -64,6 +64,7 @@ async function deploy(args) {
64
64
  const { files, slugs } = buildSdfProject(config.sdfDir, config.probesDir, {
65
65
  deployed: true,
66
66
  extraTags: config.extraTags,
67
+ roleId: config.roleId,
67
68
  });
68
69
  console.log(`Probes in the runner: ${slugs.join(", ") || "(none)"}`);
69
70
  console.log(`Files to upload: ${files.join(", ")}`);
@@ -92,6 +93,7 @@ async function cleanup(args) {
92
93
  buildSdfProject(config.sdfDir, config.probesDir, {
93
94
  deployed: false,
94
95
  extraTags: config.extraTags,
96
+ roleId: config.roleId,
95
97
  });
96
98
  return suitecloud(project, config, [
97
99
  "project:deploy",
package/dist/config.js CHANGED
@@ -10,6 +10,10 @@ export const layoutKeys = [
10
10
  ];
11
11
  /** The committed `.env` may also register extra probe tags: a comma-separated list of tag names, not a path. */
12
12
  export const extraTagsKey = "SUITESCRIPT_VERIFY_EXTRA_TAGS";
13
+ /** The committed `.env` may also name the runner role: a NetSuite custom role id. */
14
+ export const roleIdKey = "SUITESCRIPT_VERIFY_ROLE_ID";
15
+ export const defaultRoleId = "customrole_suitescript_verify";
16
+ const roleIdPattern = /^customrole_[a-z0-9_]+$/;
13
17
  const tagName = /^[A-Za-z][A-Za-z0-9_-]*$/;
14
18
  /** The tag names in a registration value, without the `@`; blank entries are dropped. */
15
19
  export const parseTagList = (value) => value
@@ -49,11 +53,19 @@ const insideProject = (project, path) => {
49
53
  const looksSecret = (value) => /-----BEGIN /.test(value) ||
50
54
  /^eyJ[\w-]+\.[\w-]+\.[\w-]+$/.test(value) ||
51
55
  /^[A-Za-z0-9+_=-]{32,}$/.test(value);
56
+ const roleIdProblem = (file) => ({
57
+ message: `${roleIdKey} in ${file} is not a NetSuite custom role id like customrole_suitescript_verify (lowercase letters, digits and underscores after customrole_).`,
58
+ });
52
59
  /** Checks the committed `.env` against the allowlist. Messages name the key, never the value. */
53
60
  function checkCommitted(project, committed, file) {
54
61
  const problems = [];
55
62
  for (const [key, value] of committed) {
56
- if (key === extraTagsKey) {
63
+ if (key === roleIdKey) {
64
+ // The pattern is the whole check: a valid id may be long enough to look like a token.
65
+ if (!roleIdPattern.test(value))
66
+ problems.push(roleIdProblem(file));
67
+ }
68
+ else if (key === extraTagsKey) {
57
69
  if (!parseTagList(value).every((name) => tagName.test(name))) {
58
70
  problems.push({
59
71
  message: `${key} in ${file} is not a comma-separated list of tag names like flavor,other.`,
@@ -67,7 +79,7 @@ function checkCommitted(project, committed, file) {
67
79
  }
68
80
  else if (!layoutKeys.includes(key)) {
69
81
  problems.push({
70
- message: `${key} in ${file} is not a layout key. The committed file holds layout only (${[...layoutKeys, extraTagsKey].join(", ")}); put anything else in .env.local or the process environment.`,
82
+ message: `${key} in ${file} is not a layout key. The committed file holds layout only (${[...layoutKeys, extraTagsKey, roleIdKey].join(", ")}); put anything else in .env.local or the process environment.`,
71
83
  });
72
84
  }
73
85
  else if (!insideProject(project, value)) {
@@ -125,6 +137,13 @@ export function loadConfig(project, dir, processEnv = process.env) {
125
137
  }
126
138
  }
127
139
  const path = (key, fallback) => resolve(project, values[key] || fallback);
140
+ const roleId = values[roleIdKey] || defaultRoleId;
141
+ // The committed file reports its own bad value; this covers .env.local and the environment.
142
+ const committedRole = committed.get(roleIdKey);
143
+ const roleProblems = roleIdPattern.test(roleId) ||
144
+ (committedRole !== undefined && !roleIdPattern.test(committedRole))
145
+ ? []
146
+ : [roleIdProblem(".env.local or the environment")];
128
147
  const shown = relative(project, envFile).replaceAll("\\", "/");
129
148
  return {
130
149
  extraTags: parseTagList(values[extraTagsKey] ?? ""),
@@ -132,8 +151,10 @@ export function loadConfig(project, dir, processEnv = process.env) {
132
151
  problems: [
133
152
  ...checkCommitted(project, committed, shown),
134
153
  ...checkLocalIgnored(project, localFile),
154
+ ...roleProblems,
135
155
  ],
136
156
  resultsDir: path("SUITESCRIPT_VERIFY_RESULTS_DIR", "docs/verification/results"),
157
+ roleId,
137
158
  root,
138
159
  sdfDir: path("SUITESCRIPT_VERIFY_SDF_DIR", join(root, "sdf")),
139
160
  values,
package/dist/doctor.js CHANGED
@@ -80,6 +80,7 @@ export function runChecks(project, config, env = process.env) {
80
80
  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
81
  ? "Run suitescript-verify init to create the missing folders."
82
82
  : "Run account:setup:ci from the SDF folder once the steps above pass, so project.json gets a default auth id.");
83
+ add(`Runner role id: ${config.roleId}`, true, "agent", "");
83
84
  return checks;
84
85
  }
85
86
  export function formatChecks(checks) {
package/dist/sdf.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
+ import { defaultRoleId } from "./config.js";
3
4
  import { fileCabinetFolder } from "./netsuite.js";
4
5
  import { isRestlet, listProbes, read, tagProblems, } from "./probes.js";
5
6
  import { UsageError } from "./shared.js";
@@ -24,7 +25,7 @@ export function generateRunner(probes, source) {
24
25
  * the SDF folder. Only RESTlet probes go in: the runner is a RESTlet. With
25
26
  * `deployed: false` the runner's deployment is switched off (cleanup).
26
27
  */
27
- export function buildSdfProject(sdfDir, probesDir, { deployed, extraTags = [], }) {
28
+ export function buildSdfProject(sdfDir, probesDir, { deployed, extraTags = [], roleId = defaultRoleId, }) {
28
29
  const probes = listProbes(probesDir, extraTags).filter(isRestlet);
29
30
  const bad = probes.filter((probe) => tagProblems(probe).length > 0);
30
31
  if (bad.length > 0) {
@@ -39,9 +40,11 @@ export function buildSdfProject(sdfDir, probesDir, { deployed, extraTags = [], }
39
40
  mkdirSync(join(src, "Objects"), { recursive: true });
40
41
  writeFileSync(join(sdfDir, "package.json"), template("sdf/package.json"));
41
42
  writeFileSync(join(sdfDir, "suitecloud.config.js"), template("sdf/suitecloud.config.js"));
42
- writeFileSync(join(src, "manifest.xml"), template("sdf/manifest.xml"));
43
+ writeFileSync(join(src, "manifest.xml"), template("sdf/manifest.xml").replaceAll("%%ROLE_ID%%", roleId));
43
44
  writeFileSync(join(src, "deploy.xml"), template("sdf/deploy.xml"));
44
- writeFileSync(join(src, "Objects", "customscript_suitescript_verify_runner.xml"), template("sdf/runner.xml").replace("%%DEPLOYED%%", deployed ? "T" : "F"));
45
+ writeFileSync(join(src, "Objects", "customscript_suitescript_verify_runner.xml"), template("sdf/runner.xml")
46
+ .replace("%%DEPLOYED%%", deployed ? "T" : "F")
47
+ .replaceAll("%%ROLE_ID%%", roleId));
45
48
  const slugs = probes.map((probe) => probe.slug);
46
49
  const files = ["runner.js"];
47
50
  writeFileSync(join(folder, "runner.js"), generateRunner(probes, template("runner.template.js")));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "suitescript-verify",
3
- "version": "0.1.0",
3
+ "version": "0.2.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",
@@ -6,7 +6,7 @@
6
6
  <feature required="true">SERVERSIDESCRIPTING</feature>
7
7
  </features>
8
8
  <objects>
9
- <object>customrole_suitescript_verify</object>
9
+ <object>%%ROLE_ID%%</object>
10
10
  </objects>
11
11
  </dependencies>
12
12
  </manifest>
@@ -7,7 +7,7 @@
7
7
  <scriptdeployments>
8
8
  <scriptdeployment scriptid="customdeploy_suitescript_verify_runner">
9
9
  <allroles>F</allroles>
10
- <audslctrole>[customrole_suitescript_verify]</audslctrole>
10
+ <audslctrole>[%%ROLE_ID%%]</audslctrole>
11
11
  <isdeployed>%%DEPLOYED%%</isdeployed>
12
12
  <loglevel>DEBUG</loglevel>
13
13
  <status>RELEASED</status>