nerdsystem 0.0.17 → 0.0.19

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.
@@ -8,6 +8,7 @@ import { CliError, log, printJson, runOrThrow, table, dashboardUrl, ensureDeps }
8
8
  import { runChecks } from "./check.js";
9
9
  import { appendLineage, cliVersion, LOCAL_ONLY_NOTE, packProject, readLineage, uploadSnapshot, uploadStopMessage } from "./source.js";
10
10
  import { lockedGetPaths } from "../gates/tests.js";
11
+ import { platformScan } from "./platform-scan.js";
11
12
  import { BUILD_GATES, gateSummary, knownSecretsFrom, probeDeployment, runGates, skippedProbe, SOURCE_GATES, waitForDeployment } from "../gates/index.js";
12
13
  const MODULE_EXT = new Set([".js", ".mjs", ".wasm"]);
13
14
  function walk(dir, base = dir) {
@@ -155,6 +156,8 @@ export async function shipBuild(args) {
155
156
  appendLineage(root, source.id);
156
157
  // "Stored", not "saved": the one line that says "saved" is the final one, which also says the work after it is only on this computer (invariant 7).
157
158
  log.ok(`Stored ${pack.fileCount} files (${formatMb(pack.bytes)}) as ${source.id}`);
159
+ // The scanners this computer lacks run on the platform against the copy just stored, before migrations or upload.
160
+ args.results = await platformScan(api, `${projectPath(nerd)}/sources/${source.id}`, args.results);
158
161
  }
159
162
  log.step("Applying migrations to the production database");
160
163
  const mig = await applyRemoteMigrations(root, api);
@@ -192,4 +192,4 @@ export async function init(dir, args) {
192
192
  log.ok(`Ready. Next:\n cd ${dir}\n pnpm install\n nerd dev\n\nThen open the folder in your coding agent and ask it to build the first feature; the nerdsystem skill walks it through.\nDeploys go to ${project.url}`);
193
193
  }
194
194
  // Kept in sync with packages/sdk/package.json by the release script.
195
- const SDK_VERSION = "0.0.17";
195
+ const SDK_VERSION = "0.0.19";
@@ -0,0 +1,18 @@
1
+ import type { ApiClient } from "../api.js";
2
+ import type { GateResult } from "../gates/index.js";
3
+ export interface PlatformScanOptions {
4
+ /** How long the deploy waits for the answer. */
5
+ timeoutMs?: number;
6
+ pollMs?: number;
7
+ sleep?: (ms: number) => Promise<void>;
8
+ }
9
+ /**
10
+ * When gitleaks or semgrep did not run here because they are not installed, has the platform run them
11
+ * on the copy of the code that was just stored, and waits for the answer before anything goes live.
12
+ * Returns the gate results with the platform's answers in place of the skipped ones.
13
+ *
14
+ * A blocking finding stops the deploy, exactly as it would have locally. Anything else that goes wrong
15
+ * (the scanners are busy or down, the hourly allowance is used, the answer is late) does not: the two
16
+ * results stay "skip" with the reason, because a platform hiccup must not be what keeps an app offline.
17
+ */
18
+ export declare function platformScan(api: ApiClient, sourcePath: string, results: GateResult[], opts?: PlatformScanOptions): Promise<GateResult[]>;
@@ -0,0 +1,58 @@
1
+ import { CliError, log } from "../util.js";
2
+ /** The two scanners most computers do not have. The platform's builder has both. */
3
+ const SCANNERS = ["gitleaks", "semgrep"];
4
+ const defaultSleep = (ms) => new Promise((r) => setTimeout(r, ms));
5
+ /**
6
+ * When gitleaks or semgrep did not run here because they are not installed, has the platform run them
7
+ * on the copy of the code that was just stored, and waits for the answer before anything goes live.
8
+ * Returns the gate results with the platform's answers in place of the skipped ones.
9
+ *
10
+ * A blocking finding stops the deploy, exactly as it would have locally. Anything else that goes wrong
11
+ * (the scanners are busy or down, the hourly allowance is used, the answer is late) does not: the two
12
+ * results stay "skip" with the reason, because a platform hiccup must not be what keeps an app offline.
13
+ */
14
+ export async function platformScan(api, sourcePath, results, opts = {}) {
15
+ const missing = results.filter((r) => SCANNERS.includes(r.name) && r.status === "skip");
16
+ if (!missing.length)
17
+ return results;
18
+ const sleep = opts.sleep ?? defaultSleep;
19
+ const pollMs = opts.pollMs ?? 2500;
20
+ const deadline = Date.now() + (opts.timeoutMs ?? 150_000);
21
+ const skipped = (reason) => results.map((r) => (missing.includes(r) ? { ...r, summary: `${r.name} is not installed here, and the platform's scan ${reason}` } : r));
22
+ log.step(`Scanning the stored code on the platform (${missing.map((m) => m.name).join(" and ")} are not installed here)`);
23
+ let scan;
24
+ try {
25
+ scan = (await api.post(`${sourcePath}/scan`)).scan;
26
+ while ((scan.status === "queued" || scan.status === "running") && Date.now() < deadline) {
27
+ await sleep(pollMs);
28
+ const next = (await api.get(`${sourcePath}/scan`)).scan;
29
+ if (next)
30
+ scan = next;
31
+ }
32
+ }
33
+ catch (err) {
34
+ const why = err instanceof CliError && err.status === 429 ? "was not run: this organization has used its scans for the hour" : "could not be started";
35
+ log.warn(`The platform's scan ${why}. Continuing with the checks that ran here.`);
36
+ return skipped(why);
37
+ }
38
+ if (scan.status !== "done" || !scan.results) {
39
+ const why = scan.status === "failed" ? "failed" : "did not answer in time";
40
+ log.warn(`The platform's scan ${why}. Continuing with the checks that ran here.`);
41
+ return skipped(why);
42
+ }
43
+ const answered = new Map(scan.results.filter((r) => SCANNERS.includes(r.name)).map((r) => [r.name, r]));
44
+ const merged = results.map((r) => (missing.includes(r) && answered.has(r.name) ? { ...answered.get(r.name), summary: `${answered.get(r.name).summary} (scanned on the platform)` } : r));
45
+ const ran = merged.filter((r) => missing.some((m) => m.name === r.name));
46
+ for (const r of ran) {
47
+ log.info(` ${r.status === "pass" ? "✓" : r.status === "fail" ? "✗" : r.status === "warn" ? "!" : "–"} ${r.title}: ${r.summary}`);
48
+ for (const f of r.findings.slice(0, 20)) {
49
+ log.info(` ${f.severity === "block" ? "✗" : "!"} ${f.rule_id}${f.file ? ` ${f.file}${f.line ? `:${f.line}` : ""}` : ""}${f.masked ? ` (${f.masked})` : ""}`);
50
+ log.info(` ${f.risk}`);
51
+ log.info(` Fix: ${f.fix}`);
52
+ }
53
+ }
54
+ const blocked = ran.filter((r) => r.status === "fail");
55
+ if (blocked.length)
56
+ throw new CliError(`Blocked by: ${blocked.map((g) => g.title).join(", ")}. Nothing went live. Fix the findings above, then deploy again.`, 2, { code: "gate_failed" });
57
+ return merged;
58
+ }
@@ -53,6 +53,10 @@ export const semgrepGate = {
53
53
  const findings = [];
54
54
  try {
55
55
  const json = JSON.parse(r.stdout);
56
+ // Semgrep exits 0 and reports nothing when its rules file does not load, which reads exactly like a clean
57
+ // scan. That hid a broken rules file from 2026-11-02 to 2026-11-10: a run that could not use the rules is not a pass.
58
+ if ((json.errors ?? []).some((e) => e.level === "error" && /SemgrepError|InvalidRule|config/i.test(e.type ?? "")))
59
+ return result(this, "warn", "semgrep could not load its rules", [], t, "semgrep");
56
60
  for (const x of json.results.slice(0, 100)) {
57
61
  const short = x.check_id.split(".").pop() ?? x.check_id;
58
62
  const rule_id = `semgrep/${short}`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nerdsystem",
3
- "version": "0.0.17",
3
+ "version": "0.0.19",
4
4
  "description": "nerd: start, check, and deploy a web app on nerdsystem from the terminal or a coding agent.",
5
5
  "type": "module",
6
6
  "bin": {
package/rules/semgrep.yml CHANGED
@@ -22,8 +22,9 @@ rules:
22
22
  severity: WARNING
23
23
  message: dangerouslySetInnerHTML with a non-literal value can inject scripts. Sanitize or avoid.
24
24
  patterns:
25
- - pattern: dangerouslySetInnerHTML={{ __html: $X }}
26
- - pattern-not: dangerouslySetInnerHTML={{ __html: "..." }}
25
+ # Quoted: an unquoted "__html: $X" reads as a YAML mapping and makes the whole file invalid.
26
+ - pattern: '<$EL dangerouslySetInnerHTML={{ __html: $X }} />'
27
+ - pattern-not: '<$EL dangerouslySetInnerHTML={{ __html: "..." }} />'
27
28
  - id: nerd-innerhtml-assignment
28
29
  languages: [typescript, javascript]
29
30
  severity: WARNING
@@ -168,7 +168,7 @@ Without MCP, `nerd check --json` returns the same shape, and `nerd check --rule
168
168
 
169
169
  - **Scheduled jobs**: put the work in the `POST /api/cron` handler (already in `src/server/index.ts`, guarded by `verifyCron()`), then turn the schedule on with `nerd cron hourly` or `nerd cron daily`. The platform calls it; the app never needs its own timer.
170
170
  - **Secrets** (Stripe keys, API tokens): never in code or in the client. Locally they live in `.dev.vars` (gitignored). For production, ask the person to add it under the app's Settings tab in the dashboard, or to run `nerd secret set NAME` and paste the value; it reaches the app as `c.env.NAME` on the next deploy. Pull request previews do not get it unless it was set with `--previews` (use that only for keys that are safe in a preview, such as a test-mode key). `nerd secret list` shows names only. Do not put a real secret value in any command you run yourself.
171
- - **Email**: `sendEmail(c.env, { to, subject, text, html? })` from `@nerdsystem/sdk` sends through the platform. The relay only sends to addresses that have an account on this app (someone who signed up); any other recipient is refused with a 400, and the sender name is always the app's name. On the Free plan the recipient must also have confirmed their address by finishing a password reset (403 otherwise); paid plans can write to every signed-up user. It is limited per day by plan (50 on Free, 1,000 on Pro at $39 a month, 2,000 on Studio at $99 a month), so use it for transactional mail such as receipts and notifications, not bulk. Plans can be paid monthly or yearly at ten times the monthly price.
171
+ - **Email**: `sendEmail(c.env, { to, subject, text, html?, replyTo? })` from `@nerdsystem/sdk` sends through the platform. The relay only sends to addresses that have an account on this app (someone who signed up); any other recipient is refused with a 400, and the sender name is always the app's name. On the Free plan the recipient must also have confirmed their address by finishing a password reset (403 otherwise); paid plans can write to every signed-up user. It is limited per day by plan (50 on Free, 1,000 on Pro at $39 a month, 2,000 on Studio at $99 a month), so use it for transactional mail such as receipts and notifications, not bulk. Plans can be paid monthly or yearly at ten times the monthly price. Pass `replyTo` (the owner's address, or one they give you) whenever a person might answer the message; without it replies go to a mailbox nobody reads. Mail goes out under the app's name from the platform's address; on Pro and Studio the owner can switch it to their own domain in the dashboard under Settings, which needs no code change.
172
172
 
173
173
  ## Debugging production
174
174
 
@@ -2,7 +2,7 @@
2
2
  description: How to build, run, and deploy this app on nerdsystem. Applies to server, database, auth, and deploy work.
3
3
  alwaysApply: true
4
4
  ---
5
- <!-- generated from .claude/skills/nerdsystem/SKILL.md (e719563eac29); edit the skill, then run: nerd agents sync -->
5
+ <!-- generated from .claude/skills/nerdsystem/SKILL.md (f814c3a452ab); edit the skill, then run: nerd agents sync -->
6
6
 
7
7
  # Building on nerdsystem
8
8
 
@@ -169,7 +169,7 @@ Without MCP, `nerd check --json` returns the same shape, and `nerd check --rule
169
169
 
170
170
  - **Scheduled jobs**: put the work in the `POST /api/cron` handler (already in `src/server/index.ts`, guarded by `verifyCron()`), then turn the schedule on with `nerd cron hourly` or `nerd cron daily`. The platform calls it; the app never needs its own timer.
171
171
  - **Secrets** (Stripe keys, API tokens): never in code or in the client. Locally they live in `.dev.vars` (gitignored). For production, ask the person to add it under the app's Settings tab in the dashboard, or to run `nerd secret set NAME` and paste the value; it reaches the app as `c.env.NAME` on the next deploy. Pull request previews do not get it unless it was set with `--previews` (use that only for keys that are safe in a preview, such as a test-mode key). `nerd secret list` shows names only. Do not put a real secret value in any command you run yourself.
172
- - **Email**: `sendEmail(c.env, { to, subject, text, html? })` from `@nerdsystem/sdk` sends through the platform. The relay only sends to addresses that have an account on this app (someone who signed up); any other recipient is refused with a 400, and the sender name is always the app's name. On the Free plan the recipient must also have confirmed their address by finishing a password reset (403 otherwise); paid plans can write to every signed-up user. It is limited per day by plan (50 on Free, 1,000 on Pro at $39 a month, 2,000 on Studio at $99 a month), so use it for transactional mail such as receipts and notifications, not bulk. Plans can be paid monthly or yearly at ten times the monthly price.
172
+ - **Email**: `sendEmail(c.env, { to, subject, text, html?, replyTo? })` from `@nerdsystem/sdk` sends through the platform. The relay only sends to addresses that have an account on this app (someone who signed up); any other recipient is refused with a 400, and the sender name is always the app's name. On the Free plan the recipient must also have confirmed their address by finishing a password reset (403 otherwise); paid plans can write to every signed-up user. It is limited per day by plan (50 on Free, 1,000 on Pro at $39 a month, 2,000 on Studio at $99 a month), so use it for transactional mail such as receipts and notifications, not bulk. Plans can be paid monthly or yearly at ten times the monthly price. Pass `replyTo` (the owner's address, or one they give you) whenever a person might answer the message; without it replies go to a mailbox nobody reads. Mail goes out under the app's name from the platform's address; on Pro and Studio the owner can switch it to their own domain in the dashboard under Settings, which needs no code change.
173
173
 
174
174
  ## Debugging production
175
175
 
@@ -1,4 +1,4 @@
1
- <!-- generated from .claude/skills/nerdsystem/SKILL.md (e719563eac29); edit the skill, then run: nerd agents sync -->
1
+ <!-- generated from .claude/skills/nerdsystem/SKILL.md (f814c3a452ab); edit the skill, then run: nerd agents sync -->
2
2
 
3
3
  # Building on nerdsystem
4
4
 
@@ -165,7 +165,7 @@ Without MCP, `nerd check --json` returns the same shape, and `nerd check --rule
165
165
 
166
166
  - **Scheduled jobs**: put the work in the `POST /api/cron` handler (already in `src/server/index.ts`, guarded by `verifyCron()`), then turn the schedule on with `nerd cron hourly` or `nerd cron daily`. The platform calls it; the app never needs its own timer.
167
167
  - **Secrets** (Stripe keys, API tokens): never in code or in the client. Locally they live in `.dev.vars` (gitignored). For production, ask the person to add it under the app's Settings tab in the dashboard, or to run `nerd secret set NAME` and paste the value; it reaches the app as `c.env.NAME` on the next deploy. Pull request previews do not get it unless it was set with `--previews` (use that only for keys that are safe in a preview, such as a test-mode key). `nerd secret list` shows names only. Do not put a real secret value in any command you run yourself.
168
- - **Email**: `sendEmail(c.env, { to, subject, text, html? })` from `@nerdsystem/sdk` sends through the platform. The relay only sends to addresses that have an account on this app (someone who signed up); any other recipient is refused with a 400, and the sender name is always the app's name. On the Free plan the recipient must also have confirmed their address by finishing a password reset (403 otherwise); paid plans can write to every signed-up user. It is limited per day by plan (50 on Free, 1,000 on Pro at $39 a month, 2,000 on Studio at $99 a month), so use it for transactional mail such as receipts and notifications, not bulk. Plans can be paid monthly or yearly at ten times the monthly price.
168
+ - **Email**: `sendEmail(c.env, { to, subject, text, html?, replyTo? })` from `@nerdsystem/sdk` sends through the platform. The relay only sends to addresses that have an account on this app (someone who signed up); any other recipient is refused with a 400, and the sender name is always the app's name. On the Free plan the recipient must also have confirmed their address by finishing a password reset (403 otherwise); paid plans can write to every signed-up user. It is limited per day by plan (50 on Free, 1,000 on Pro at $39 a month, 2,000 on Studio at $99 a month), so use it for transactional mail such as receipts and notifications, not bulk. Plans can be paid monthly or yearly at ten times the monthly price. Pass `replyTo` (the owner's address, or one they give you) whenever a person might answer the message; without it replies go to a mailbox nobody reads. Mail goes out under the app's name from the platform's address; on Pro and Studio the owner can switch it to their own domain in the dashboard under Settings, which needs no code change.
169
169
 
170
170
  ## Debugging production
171
171
 
@@ -1,4 +1,4 @@
1
- <!-- generated from .claude/skills/nerdsystem/SKILL.md (e719563eac29); edit the skill, then run: nerd agents sync -->
1
+ <!-- generated from .claude/skills/nerdsystem/SKILL.md (f814c3a452ab); edit the skill, then run: nerd agents sync -->
2
2
 
3
3
  # Building on nerdsystem
4
4
 
@@ -165,7 +165,7 @@ Without MCP, `nerd check --json` returns the same shape, and `nerd check --rule
165
165
 
166
166
  - **Scheduled jobs**: put the work in the `POST /api/cron` handler (already in `src/server/index.ts`, guarded by `verifyCron()`), then turn the schedule on with `nerd cron hourly` or `nerd cron daily`. The platform calls it; the app never needs its own timer.
167
167
  - **Secrets** (Stripe keys, API tokens): never in code or in the client. Locally they live in `.dev.vars` (gitignored). For production, ask the person to add it under the app's Settings tab in the dashboard, or to run `nerd secret set NAME` and paste the value; it reaches the app as `c.env.NAME` on the next deploy. Pull request previews do not get it unless it was set with `--previews` (use that only for keys that are safe in a preview, such as a test-mode key). `nerd secret list` shows names only. Do not put a real secret value in any command you run yourself.
168
- - **Email**: `sendEmail(c.env, { to, subject, text, html? })` from `@nerdsystem/sdk` sends through the platform. The relay only sends to addresses that have an account on this app (someone who signed up); any other recipient is refused with a 400, and the sender name is always the app's name. On the Free plan the recipient must also have confirmed their address by finishing a password reset (403 otherwise); paid plans can write to every signed-up user. It is limited per day by plan (50 on Free, 1,000 on Pro at $39 a month, 2,000 on Studio at $99 a month), so use it for transactional mail such as receipts and notifications, not bulk. Plans can be paid monthly or yearly at ten times the monthly price.
168
+ - **Email**: `sendEmail(c.env, { to, subject, text, html?, replyTo? })` from `@nerdsystem/sdk` sends through the platform. The relay only sends to addresses that have an account on this app (someone who signed up); any other recipient is refused with a 400, and the sender name is always the app's name. On the Free plan the recipient must also have confirmed their address by finishing a password reset (403 otherwise); paid plans can write to every signed-up user. It is limited per day by plan (50 on Free, 1,000 on Pro at $39 a month, 2,000 on Studio at $99 a month), so use it for transactional mail such as receipts and notifications, not bulk. Plans can be paid monthly or yearly at ten times the monthly price. Pass `replyTo` (the owner's address, or one they give you) whenever a person might answer the message; without it replies go to a mailbox nobody reads. Mail goes out under the app's name from the platform's address; on Pro and Studio the owner can switch it to their own domain in the dashboard under Settings, which needs no code change.
169
169
 
170
170
  ## Debugging production
171
171
 
@@ -1,4 +1,4 @@
1
- <!-- generated from .claude/skills/nerdsystem/SKILL.md (e719563eac29); edit the skill, then run: nerd agents sync -->
1
+ <!-- generated from .claude/skills/nerdsystem/SKILL.md (f814c3a452ab); edit the skill, then run: nerd agents sync -->
2
2
 
3
3
  # Building on nerdsystem
4
4
 
@@ -165,7 +165,7 @@ Without MCP, `nerd check --json` returns the same shape, and `nerd check --rule
165
165
 
166
166
  - **Scheduled jobs**: put the work in the `POST /api/cron` handler (already in `src/server/index.ts`, guarded by `verifyCron()`), then turn the schedule on with `nerd cron hourly` or `nerd cron daily`. The platform calls it; the app never needs its own timer.
167
167
  - **Secrets** (Stripe keys, API tokens): never in code or in the client. Locally they live in `.dev.vars` (gitignored). For production, ask the person to add it under the app's Settings tab in the dashboard, or to run `nerd secret set NAME` and paste the value; it reaches the app as `c.env.NAME` on the next deploy. Pull request previews do not get it unless it was set with `--previews` (use that only for keys that are safe in a preview, such as a test-mode key). `nerd secret list` shows names only. Do not put a real secret value in any command you run yourself.
168
- - **Email**: `sendEmail(c.env, { to, subject, text, html? })` from `@nerdsystem/sdk` sends through the platform. The relay only sends to addresses that have an account on this app (someone who signed up); any other recipient is refused with a 400, and the sender name is always the app's name. On the Free plan the recipient must also have confirmed their address by finishing a password reset (403 otherwise); paid plans can write to every signed-up user. It is limited per day by plan (50 on Free, 1,000 on Pro at $39 a month, 2,000 on Studio at $99 a month), so use it for transactional mail such as receipts and notifications, not bulk. Plans can be paid monthly or yearly at ten times the monthly price.
168
+ - **Email**: `sendEmail(c.env, { to, subject, text, html?, replyTo? })` from `@nerdsystem/sdk` sends through the platform. The relay only sends to addresses that have an account on this app (someone who signed up); any other recipient is refused with a 400, and the sender name is always the app's name. On the Free plan the recipient must also have confirmed their address by finishing a password reset (403 otherwise); paid plans can write to every signed-up user. It is limited per day by plan (50 on Free, 1,000 on Pro at $39 a month, 2,000 on Studio at $99 a month), so use it for transactional mail such as receipts and notifications, not bulk. Plans can be paid monthly or yearly at ten times the monthly price. Pass `replyTo` (the owner's address, or one they give you) whenever a person might answer the message; without it replies go to a mailbox nobody reads. Mail goes out under the app's name from the platform's address; on Pro and Studio the owner can switch it to their own domain in the dashboard under Settings, which needs no code change.
169
169
 
170
170
  ## Debugging production
171
171