vanara 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 +14 -0
- package/bin/vanara.js +66 -2
- package/catalog/agents/api-designer/AGENT.md +140 -0
- package/catalog/agents/api-designer/examples/openapi-snippet.yaml +130 -0
- package/catalog/agents/api-designer/examples/review-notes.md +61 -0
- package/catalog/agents/api-designer/references/contract-and-openapi.md +82 -0
- package/catalog/agents/api-designer/references/design-checklist.md +68 -0
- package/catalog/agents/api-designer/references/versioning-and-evolution.md +63 -0
- package/catalog/agents/api-designer/scripts/lint-openapi.mjs +180 -0
- package/catalog/agents/code-reviewer/AGENT.md +168 -0
- package/catalog/agents/code-reviewer/examples/pr-comment-template.md +48 -0
- package/catalog/agents/code-reviewer/examples/sample-review-output.md +38 -0
- package/catalog/agents/code-reviewer/references/review-checklist.md +46 -0
- package/catalog/agents/code-reviewer/references/security-review.md +62 -0
- package/catalog/agents/code-reviewer/references/severity-rubric.md +63 -0
- package/catalog/agents/code-reviewer/scripts/review-guard.mjs +102 -0
- package/catalog/agents/debugger/AGENT.md +112 -0
- package/catalog/agents/debugger/examples/postmortem-template.md +59 -0
- package/catalog/agents/debugger/examples/repro-template.md +50 -0
- package/catalog/agents/debugger/references/bisection.md +63 -0
- package/catalog/agents/debugger/references/debugging-method.md +57 -0
- package/catalog/agents/debugger/references/observability.md +61 -0
- package/catalog/agents/debugger/scripts/parse-stacktrace.mjs +109 -0
- package/catalog/agents/pr-summarizer/AGENT.md +156 -0
- package/catalog/agents/pr-summarizer/examples/sample-pr-summary.md +46 -0
- package/catalog/agents/pr-summarizer/examples/weak-vs-strong-summary.md +68 -0
- package/catalog/agents/pr-summarizer/references/identifying-risk.md +68 -0
- package/catalog/agents/pr-summarizer/references/reading-the-diff.md +68 -0
- package/catalog/agents/pr-summarizer/references/test-plan-and-missing-tests.md +64 -0
- package/catalog/agents/pr-summarizer/references/writing-the-summary.md +63 -0
- package/catalog/agents/pr-summarizer/scripts/diff-risk.mjs +111 -0
- package/catalog/agents/refactoring-specialist/AGENT.md +185 -0
- package/catalog/agents/refactoring-specialist/examples/before-after.md +91 -0
- package/catalog/agents/refactoring-specialist/examples/refactor-plan.md +97 -0
- package/catalog/agents/refactoring-specialist/references/large-scale-refactor.md +50 -0
- package/catalog/agents/refactoring-specialist/references/safe-workflow.md +75 -0
- package/catalog/agents/refactoring-specialist/references/smell-catalog.md +76 -0
- package/catalog/agents/refactoring-specialist/scripts/detect-smells.mjs +150 -0
- package/catalog/agents/security-auditor/AGENT.md +168 -0
- package/catalog/agents/security-auditor/examples/audit-report.md +68 -0
- package/catalog/agents/security-auditor/examples/finding-template.md +31 -0
- package/catalog/agents/security-auditor/references/audit-workflow.md +116 -0
- package/catalog/agents/security-auditor/references/severity-and-reporting.md +56 -0
- package/catalog/agents/security-auditor/references/vuln-classes.md +129 -0
- package/catalog/agents/security-auditor/scripts/scan-secrets.mjs +86 -0
- package/catalog/agents/technical-writer/AGENT.md +163 -0
- package/catalog/agents/technical-writer/examples/how-to-example.md +76 -0
- package/catalog/agents/technical-writer/examples/reference-example.md +75 -0
- package/catalog/agents/technical-writer/references/clarity-and-style.md +52 -0
- package/catalog/agents/technical-writer/references/diataxis-and-doc-types.md +61 -0
- package/catalog/agents/technical-writer/references/structure-and-maintenance.md +59 -0
- package/catalog/agents/technical-writer/scripts/readability.mjs +103 -0
- package/catalog/agents/test-author/AGENT.md +177 -0
- package/catalog/agents/test-author/examples/test-plan.md +54 -0
- package/catalog/agents/test-author/examples/unit-test-example.ts +55 -0
- package/catalog/agents/test-author/references/aaa-and-naming.md +75 -0
- package/catalog/agents/test-author/references/mocking-and-fixtures.md +94 -0
- package/catalog/agents/test-author/references/test-types-and-pyramid.md +60 -0
- package/catalog/agents/test-author/scripts/check-coverage.mjs +145 -0
- package/catalog/agents/threat-modeler/AGENT.md +163 -0
- package/catalog/agents/threat-modeler/examples/dfd-example.md +55 -0
- package/catalog/agents/threat-modeler/examples/threat-model.md +81 -0
- package/catalog/agents/threat-modeler/references/dfd-and-trust-boundaries.md +76 -0
- package/catalog/agents/threat-modeler/references/mitigation-catalog.md +77 -0
- package/catalog/agents/threat-modeler/references/stride-method.md +68 -0
- package/catalog/agents/threat-modeler/scripts/stride-checklist.mjs +183 -0
- package/catalog/agents/vuln-scanner/AGENT.md +146 -0
- package/catalog/agents/vuln-scanner/examples/finding-template.md +57 -0
- package/catalog/agents/vuln-scanner/examples/scan-report.md +49 -0
- package/catalog/agents/vuln-scanner/references/remediation-and-severity.md +68 -0
- package/catalog/agents/vuln-scanner/references/scan-types-and-tools.md +83 -0
- package/catalog/agents/vuln-scanner/references/triage-and-false-positives.md +73 -0
- package/catalog/agents/vuln-scanner/scripts/parse-scan-results.mjs +133 -0
- package/catalog/packs/code-review-pack/PACK.md +100 -0
- package/catalog/packs/code-review-pack/examples/pr-flow-example.md +50 -0
- package/catalog/packs/code-review-pack/references/rollout-guide.md +40 -0
- package/catalog/packs/security-pack/PACK.md +128 -0
- package/catalog/packs/security-pack/examples/owasp-vuln-triage-and-fix.md +66 -0
- package/catalog/packs/security-pack/examples/threat-model-then-audit.md +77 -0
- package/catalog/packs/security-pack/references/choosing-the-right-tool.md +37 -0
- package/catalog/packs/security-pack/references/threat-model-to-remediation.md +69 -0
- package/catalog/packs/security-pack/references/workflow.md +56 -0
- package/catalog/skills/api-pagination/SKILL.md +83 -0
- package/catalog/skills/api-pagination/examples/keyset-queries.sql +38 -0
- package/catalog/skills/api-pagination/examples/response-shapes.md +51 -0
- package/catalog/skills/api-pagination/references/consuming-pages.md +39 -0
- package/catalog/skills/api-pagination/references/counting-and-totals.md +41 -0
- package/catalog/skills/api-pagination/references/cursor-encoding.md +34 -0
- package/catalog/skills/api-pagination/scripts/check-cursor.mjs +51 -0
- package/catalog/skills/caching-strategies/SKILL.md +180 -0
- package/catalog/skills/caching-strategies/examples/http-cache-headers.md +82 -0
- package/catalog/skills/caching-strategies/examples/redis-cache-aside.js +110 -0
- package/catalog/skills/caching-strategies/references/cache-patterns.md +93 -0
- package/catalog/skills/caching-strategies/references/eviction-and-ttl.md +67 -0
- package/catalog/skills/caching-strategies/references/invalidation-and-stampede.md +99 -0
- package/catalog/skills/caching-strategies/scripts/ttl-jitter.mjs +99 -0
- package/catalog/skills/conventional-commits/SKILL.md +135 -0
- package/catalog/skills/conventional-commits/examples/commit-examples.md +60 -0
- package/catalog/skills/conventional-commits/examples/commitlint.config.js +31 -0
- package/catalog/skills/conventional-commits/references/breaking-changes-semver.md +50 -0
- package/catalog/skills/conventional-commits/references/scoping-commits.md +42 -0
- package/catalog/skills/conventional-commits/references/spec.md +78 -0
- package/catalog/skills/conventional-commits/scripts/lint-commit.mjs +97 -0
- package/catalog/skills/database-migrations/SKILL.md +137 -0
- package/catalog/skills/database-migrations/examples/add-column-safe.sql +34 -0
- package/catalog/skills/database-migrations/examples/migration-config.example.json +27 -0
- package/catalog/skills/database-migrations/references/expand-contract.md +74 -0
- package/catalog/skills/database-migrations/references/rollback-and-safety.md +71 -0
- package/catalog/skills/database-migrations/references/zero-downtime-changes.md +84 -0
- package/catalog/skills/database-migrations/scripts/check-migration-reversible.mjs +139 -0
- package/catalog/skills/error-handling-patterns/SKILL.md +136 -0
- package/catalog/skills/error-handling-patterns/examples/express-error-middleware.js +93 -0
- package/catalog/skills/error-handling-patterns/examples/result-pattern.ts +81 -0
- package/catalog/skills/error-handling-patterns/references/error-taxonomy.md +91 -0
- package/catalog/skills/error-handling-patterns/references/language-patterns.md +102 -0
- package/catalog/skills/error-handling-patterns/references/retry-and-backoff.md +76 -0
- package/catalog/skills/error-handling-patterns/scripts/lint-empty-catch.mjs +113 -0
- package/catalog/skills/git-collaboration-workflows/SKILL.md +104 -0
- package/catalog/skills/git-collaboration-workflows/examples/branch-protection.md +48 -0
- package/catalog/skills/git-collaboration-workflows/examples/pr-review-playbook.md +48 -0
- package/catalog/skills/git-collaboration-workflows/references/branching-models.md +52 -0
- package/catalog/skills/git-collaboration-workflows/references/history-hygiene.md +44 -0
- package/catalog/skills/git-collaboration-workflows/scripts/check-branch-hygiene.mjs +62 -0
- package/catalog/skills/owasp-top10/SKILL.md +112 -0
- package/catalog/skills/owasp-top10/examples/sql-injection-fix.md +33 -0
- package/catalog/skills/owasp-top10/examples/xss-fix.md +27 -0
- package/catalog/skills/owasp-top10/references/access-control.md +36 -0
- package/catalog/skills/owasp-top10/references/crypto-failures.md +29 -0
- package/catalog/skills/owasp-top10/references/injection.md +39 -0
- package/catalog/skills/owasp-top10/references/ssrf-and-design.md +35 -0
- package/catalog/skills/owasp-top10/scripts/scan-injection.mjs +73 -0
- package/catalog/skills/prompt-engineering/SKILL.md +100 -0
- package/catalog/skills/prompt-engineering/examples/classification-prompt.md +29 -0
- package/catalog/skills/prompt-engineering/examples/extraction-prompt.md +25 -0
- package/catalog/skills/prompt-engineering/references/prompt-injection.md +34 -0
- package/catalog/skills/prompt-engineering/references/structured-output.md +34 -0
- package/catalog/skills/prompt-engineering/references/techniques.md +33 -0
- package/catalog/skills/prompt-engineering/scripts/validate-output.mjs +71 -0
- package/catalog/skills/readme-writing/SKILL.md +176 -0
- package/catalog/skills/readme-writing/examples/README.template.md +73 -0
- package/catalog/skills/readme-writing/examples/good-readme-example.md +95 -0
- package/catalog/skills/readme-writing/references/anatomy.md +107 -0
- package/catalog/skills/readme-writing/references/maintenance.md +68 -0
- package/catalog/skills/readme-writing/references/writing-style.md +79 -0
- package/catalog/skills/readme-writing/scripts/lint-readme.mjs +117 -0
- package/catalog/skills/refactoring-patterns/SKILL.md +178 -0
- package/catalog/skills/refactoring-patterns/examples/extract-function.before-after.js +57 -0
- package/catalog/skills/refactoring-patterns/examples/replace-conditional-with-polymorphism.before-after.ts +63 -0
- package/catalog/skills/refactoring-patterns/references/code-smells.md +92 -0
- package/catalog/skills/refactoring-patterns/references/refactoring-catalog.md +78 -0
- package/catalog/skills/refactoring-patterns/references/safe-workflow.md +74 -0
- package/catalog/skills/refactoring-patterns/scripts/detect-smells.mjs +137 -0
- package/catalog/skills/rest-api-design/SKILL.md +147 -0
- package/catalog/skills/rest-api-design/examples/error-envelope.json +26 -0
- package/catalog/skills/rest-api-design/examples/orders-api.openapi.yaml +106 -0
- package/catalog/skills/rest-api-design/references/error-handling.md +43 -0
- package/catalog/skills/rest-api-design/references/pagination.md +41 -0
- package/catalog/skills/rest-api-design/references/status-codes.md +32 -0
- package/catalog/skills/rest-api-design/references/versioning.md +31 -0
- package/catalog/skills/rest-api-design/scripts/check-envelope.mjs +62 -0
- package/catalog/skills/secrets-management/SKILL.md +134 -0
- package/catalog/skills/secrets-management/examples/env-example.md +41 -0
- package/catalog/skills/secrets-management/examples/rotation-runbook.md +50 -0
- package/catalog/skills/secrets-management/references/leak-response.md +47 -0
- package/catalog/skills/secrets-management/references/rotation.md +46 -0
- package/catalog/skills/secrets-management/references/secret-managers.md +52 -0
- package/catalog/skills/secrets-management/scripts/detect-hardcoded.mjs +96 -0
- package/catalog/skills/secure-auth/SKILL.md +135 -0
- package/catalog/skills/secure-auth/examples/auth-flow.md +60 -0
- package/catalog/skills/secure-auth/examples/jwt-verify.ts +89 -0
- package/catalog/skills/secure-auth/references/auth-attacks.md +76 -0
- package/catalog/skills/secure-auth/references/oauth2-oidc.md +65 -0
- package/catalog/skills/secure-auth/references/password-hashing.md +70 -0
- package/catalog/skills/secure-auth/references/sessions-vs-jwt.md +69 -0
- package/catalog/skills/secure-auth/scripts/check-password-policy.mjs +114 -0
- package/catalog/skills/sql-index-tuning/SKILL.md +171 -0
- package/catalog/skills/sql-index-tuning/examples/explain-walkthrough.sql +91 -0
- package/catalog/skills/sql-index-tuning/examples/index-ddl.sql +67 -0
- package/catalog/skills/sql-index-tuning/references/btree-internals.md +58 -0
- package/catalog/skills/sql-index-tuning/references/composite-and-covering.md +60 -0
- package/catalog/skills/sql-index-tuning/references/reading-explain.md +54 -0
- package/catalog/skills/sql-index-tuning/references/selectivity-and-cardinality.md +62 -0
- package/catalog/skills/sql-index-tuning/scripts/suggest-index.mjs +213 -0
- package/catalog/skills/test-plan-design/SKILL.md +154 -0
- package/catalog/skills/test-plan-design/examples/test-case-template.md +45 -0
- package/catalog/skills/test-plan-design/examples/test-plan.md +57 -0
- package/catalog/skills/test-plan-design/references/case-design.md +79 -0
- package/catalog/skills/test-plan-design/references/risk-prioritization.md +58 -0
- package/catalog/skills/test-plan-design/references/test-pyramid.md +58 -0
- package/catalog/skills/test-plan-design/scripts/coverage-gaps.mjs +109 -0
- package/free-tier.json +15 -0
- package/package.json +15 -4
- package/premium-index.json +1 -0
- package/src/config.js +31 -1
- package/src/license.js +143 -0
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// detect-smells.mjs — a tiny, dependency-free heuristic scanner that flags two
|
|
3
|
+
// structural smells in a code string: long functions and deep nesting. It is a
|
|
4
|
+
// teaching aid, not a real linter — it works on brace/indent heuristics, so it
|
|
5
|
+
// is intentionally conservative.
|
|
6
|
+
//
|
|
7
|
+
// Usage:
|
|
8
|
+
// node detect-smells.mjs '<code string>' # scan a literal string
|
|
9
|
+
// echo '<code>' | node detect-smells.mjs # scan from stdin
|
|
10
|
+
// node detect-smells.mjs --selftest # run built-in cases (exit 0/1)
|
|
11
|
+
//
|
|
12
|
+
// Thresholds (tunable):
|
|
13
|
+
const MAX_FUNCTION_LINES = 25; // longer than this -> "long function"
|
|
14
|
+
const MAX_NESTING_DEPTH = 4; // deeper brace nesting than this -> "deep nesting"
|
|
15
|
+
|
|
16
|
+
// Scan a code string and return an array of { type, message, line } findings.
|
|
17
|
+
export function detectSmells(code, {
|
|
18
|
+
maxFunctionLines = MAX_FUNCTION_LINES,
|
|
19
|
+
maxNestingDepth = MAX_NESTING_DEPTH,
|
|
20
|
+
} = {}) {
|
|
21
|
+
const lines = code.split(/\r?\n/);
|
|
22
|
+
const findings = [];
|
|
23
|
+
|
|
24
|
+
// --- deep nesting: track running brace depth ---
|
|
25
|
+
let depth = 0;
|
|
26
|
+
let maxSeenDepth = 0;
|
|
27
|
+
let deepLine = 0;
|
|
28
|
+
for (let i = 0; i < lines.length; i++) {
|
|
29
|
+
const line = lines[i];
|
|
30
|
+
for (const ch of line) {
|
|
31
|
+
if (ch === "{") {
|
|
32
|
+
depth++;
|
|
33
|
+
if (depth > maxSeenDepth) { maxSeenDepth = depth; deepLine = i + 1; }
|
|
34
|
+
} else if (ch === "}") {
|
|
35
|
+
depth = Math.max(0, depth - 1);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (maxSeenDepth > maxNestingDepth) {
|
|
40
|
+
findings.push({
|
|
41
|
+
type: "deep-nesting",
|
|
42
|
+
message: `nesting depth ${maxSeenDepth} exceeds ${maxNestingDepth} (consider guard clauses)`,
|
|
43
|
+
line: deepLine,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// --- long function: brace-counted spans from a function header ---
|
|
48
|
+
const fnHeader = /\b(function\b|=>\s*\{|\)\s*\{)/;
|
|
49
|
+
for (let i = 0; i < lines.length; i++) {
|
|
50
|
+
if (!fnHeader.test(lines[i]) || !lines[i].includes("{")) continue;
|
|
51
|
+
let d = 0, started = false, span = 0, startLine = i + 1;
|
|
52
|
+
for (let j = i; j < lines.length; j++) {
|
|
53
|
+
for (const ch of lines[j]) {
|
|
54
|
+
if (ch === "{") { d++; started = true; }
|
|
55
|
+
else if (ch === "}") d--;
|
|
56
|
+
}
|
|
57
|
+
if (started) { span++; if (d <= 0) break; }
|
|
58
|
+
}
|
|
59
|
+
if (span > maxFunctionLines) {
|
|
60
|
+
findings.push({
|
|
61
|
+
type: "long-function",
|
|
62
|
+
message: `function spanning ${span} lines exceeds ${maxFunctionLines} (consider Extract Function)`,
|
|
63
|
+
line: startLine,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return findings;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function report(label, code) {
|
|
72
|
+
const findings = detectSmells(code);
|
|
73
|
+
if (findings.length === 0) {
|
|
74
|
+
console.log(`✓ ${label}: no smells detected`);
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
console.error(`✗ ${label}: ${findings.length} smell(s)`);
|
|
78
|
+
for (const f of findings) console.error(` - [${f.type}] line ${f.line}: ${f.message}`);
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function selftest() {
|
|
83
|
+
const clean = `
|
|
84
|
+
function add(a, b) {
|
|
85
|
+
return a + b;
|
|
86
|
+
}`;
|
|
87
|
+
|
|
88
|
+
const smelly = `
|
|
89
|
+
function process(x) {
|
|
90
|
+
if (x) {
|
|
91
|
+
if (x.a) {
|
|
92
|
+
if (x.b) {
|
|
93
|
+
if (x.c) {
|
|
94
|
+
if (x.d) {
|
|
95
|
+
return x.a + x.b + x.c + x.d;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return 0;
|
|
102
|
+
}`;
|
|
103
|
+
|
|
104
|
+
const longFn =
|
|
105
|
+
"function big() {\n" +
|
|
106
|
+
Array.from({ length: 30 }, (_, i) => ` const v${i} = ${i};`).join("\n") +
|
|
107
|
+
"\n return 0;\n}";
|
|
108
|
+
|
|
109
|
+
const cases = [
|
|
110
|
+
{ name: "clean (no smells)", code: clean, expectSmells: false },
|
|
111
|
+
{ name: "deeply nested", code: smelly, expectSmells: true },
|
|
112
|
+
{ name: "long function", code: longFn, expectSmells: true },
|
|
113
|
+
];
|
|
114
|
+
|
|
115
|
+
let allOk = true;
|
|
116
|
+
for (const c of cases) {
|
|
117
|
+
const found = detectSmells(c.code);
|
|
118
|
+
const hasSmells = found.length > 0;
|
|
119
|
+
const ok = hasSmells === c.expectSmells;
|
|
120
|
+
allOk = allOk && ok;
|
|
121
|
+
console.log(
|
|
122
|
+
`${ok ? "✓" : "✗"} selftest ${c.name}: ${hasSmells ? `${found.length} smell(s)` : "clean"} ` +
|
|
123
|
+
`(expected ${c.expectSmells ? "smells" : "clean"})`
|
|
124
|
+
);
|
|
125
|
+
if (!ok) for (const f of found) console.log(` [${f.type}] line ${f.line}: ${f.message}`);
|
|
126
|
+
}
|
|
127
|
+
process.exit(allOk ? 0 : 1);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const arg = process.argv[2];
|
|
131
|
+
if (arg === "--selftest") selftest();
|
|
132
|
+
else if (arg) process.exit(report("code", arg) ? 0 : 1);
|
|
133
|
+
else {
|
|
134
|
+
let buf = "";
|
|
135
|
+
process.stdin.on("data", (c) => (buf += c));
|
|
136
|
+
process.stdin.on("end", () => process.exit(report("stdin", buf) ? 0 : 1));
|
|
137
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: rest-api-design
|
|
3
|
+
description: Conventions for designing clean, consistent, evolvable REST APIs — resource modeling, HTTP semantics, status codes, pagination, filtering, error envelopes, versioning, idempotency, and security. A deep reference with worked examples and runnable checks.
|
|
4
|
+
type: skill
|
|
5
|
+
version: 2.0.0
|
|
6
|
+
updated: 2026-06-28
|
|
7
|
+
---
|
|
8
|
+
# REST API Design
|
|
9
|
+
|
|
10
|
+
A good REST API is **guessable**: once a consumer learns one endpoint, they can predict the rest. This
|
|
11
|
+
skill is the deep reference for designing one — the principles, the decisions, the trade-offs, and the
|
|
12
|
+
mistakes to avoid. Heavy detail lives in `references/`; copy-paste material in `examples/`; a runnable
|
|
13
|
+
contract check in `scripts/`.
|
|
14
|
+
|
|
15
|
+
## Mental model
|
|
16
|
+
|
|
17
|
+
Design the API around **resources** (nouns) and use HTTP **verbs** to act on them. The protocol already
|
|
18
|
+
gives you a rich vocabulary — methods, status codes, headers, caching — so lean on it instead of
|
|
19
|
+
inventing your own conventions on top.
|
|
20
|
+
|
|
21
|
+
| Concern | REST answer |
|
|
22
|
+
|---|---|
|
|
23
|
+
| What | a resource, named as a plural noun (`/orders`) |
|
|
24
|
+
| Action | the HTTP method (GET/POST/PUT/PATCH/DELETE) |
|
|
25
|
+
| Outcome | the status code (200/201/404/409…) |
|
|
26
|
+
| Shape | a consistent response envelope |
|
|
27
|
+
| Change | an explicit versioning strategy |
|
|
28
|
+
|
|
29
|
+
## 1. Resource modeling
|
|
30
|
+
|
|
31
|
+
1. Name collections as **plural nouns**: `/users`, `/orders`. Never verbs in the path (`/getUsers` is wrong — the verb is `GET`).
|
|
32
|
+
2. Nest to show ownership, but only **one level deep**: `/users/{id}/orders` is fine; `/users/{id}/orders/{id}/items/{id}/...` is a smell — link instead.
|
|
33
|
+
3. Model real-world actions that aren't CRUD as sub-resources or controller endpoints: `POST /orders/{id}/refunds` rather than `POST /refundOrder`.
|
|
34
|
+
4. Keep identifiers stable and opaque to clients; don't leak DB internals (prefer UUIDs/ULIDs over auto-increment IDs where enumeration is a risk).
|
|
35
|
+
|
|
36
|
+
## 2. HTTP method semantics
|
|
37
|
+
|
|
38
|
+
| Method | Use | Safe | Idempotent |
|
|
39
|
+
|---|---|---|---|
|
|
40
|
+
| GET | read | yes | yes |
|
|
41
|
+
| POST | create / non-idempotent action | no | no |
|
|
42
|
+
| PUT | full replace | no | yes |
|
|
43
|
+
| PATCH | partial update | no | no* |
|
|
44
|
+
| DELETE | remove | no | yes |
|
|
45
|
+
|
|
46
|
+
Respect these contracts — clients, proxies, and caches rely on them. A `GET` must never mutate state.
|
|
47
|
+
See `references/status-codes.md` for the full status-code decision guide.
|
|
48
|
+
|
|
49
|
+
## 3. Status codes
|
|
50
|
+
|
|
51
|
+
Return the **accurate** code; never `200 OK` with an error body (it breaks every client's error handling).
|
|
52
|
+
|
|
53
|
+
```http
|
|
54
|
+
201 Created Location: /orders/101 # after a successful POST
|
|
55
|
+
204 No Content # successful DELETE, no body
|
|
56
|
+
400 Bad Request # malformed/invalid input
|
|
57
|
+
401 Unauthorized # not authenticated 403 Forbidden # authenticated, not allowed
|
|
58
|
+
404 Not Found 409 Conflict 422 Unprocessable Entity 429 Too Many Requests
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Full guidance and edge cases (404 vs 403 to avoid leaking existence, 409 vs 422) live in
|
|
62
|
+
`references/status-codes.md`.
|
|
63
|
+
|
|
64
|
+
## 4. The response envelope
|
|
65
|
+
|
|
66
|
+
Standardize one shape across **every** endpoint so clients parse uniformly. See
|
|
67
|
+
`examples/error-envelope.json` and validate any payload with `scripts/check-envelope.mjs`.
|
|
68
|
+
|
|
69
|
+
```json
|
|
70
|
+
{
|
|
71
|
+
"data": [ { "id": 101, "status": "open" } ],
|
|
72
|
+
"meta": { "nextCursor": "eyJpZCI6MTIwfQ", "limit": 20 },
|
|
73
|
+
"error": null
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
On error, the same shape with `data: null` and a populated `error` (see §6).
|
|
78
|
+
|
|
79
|
+
## 5. Pagination, filtering, sorting
|
|
80
|
+
|
|
81
|
+
Every collection endpoint **must** paginate — an unbounded list is a latent outage. Prefer **cursor**
|
|
82
|
+
pagination for large or frequently-changing data (offset pagination scans and skips rows, getting slower
|
|
83
|
+
the deeper you go, and double-counts when rows are inserted mid-scan).
|
|
84
|
+
|
|
85
|
+
```http
|
|
86
|
+
GET /v1/orders?status=open&sort=-created_at&limit=20&cursor=eyJpZCI6MTAwfQ
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
The full comparison, cursor encoding, and pitfalls are in `references/pagination.md`.
|
|
90
|
+
|
|
91
|
+
## 6. Error model
|
|
92
|
+
|
|
93
|
+
One error shape, everywhere. A machine-readable `code`, a human `message`, and optional field-level
|
|
94
|
+
`details`:
|
|
95
|
+
|
|
96
|
+
```json
|
|
97
|
+
{ "data": null, "error": {
|
|
98
|
+
"code": "validation_failed",
|
|
99
|
+
"message": "The request was invalid.",
|
|
100
|
+
"details": [ { "field": "email", "issue": "must be a valid email" } ]
|
|
101
|
+
} }
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Rules and the full catalogue of codes are in `references/error-handling.md`.
|
|
105
|
+
|
|
106
|
+
## 7. Versioning & evolution
|
|
107
|
+
|
|
108
|
+
APIs are forever once published. Version explicitly (`/v1/...` in the path is the most operationally
|
|
109
|
+
clear) and treat additive changes as backward-compatible; breaking changes require a new version.
|
|
110
|
+
Strategy and a deprecation playbook: `references/versioning.md`.
|
|
111
|
+
|
|
112
|
+
## 8. Idempotency, concurrency, security
|
|
113
|
+
|
|
114
|
+
- **Idempotency:** accept an `Idempotency-Key` header on `POST` so client retries don't double-create
|
|
115
|
+
(the server stores key → result and replays it). Essential for payments.
|
|
116
|
+
- **Concurrency:** use `ETag` + `If-Match` for optimistic locking on updates to prevent lost writes.
|
|
117
|
+
- **Security:** authenticate every request, authorize every action server-side (guard against IDOR by
|
|
118
|
+
checking ownership, not just authentication), rate-limit (return `429` + `Retry-After`), and validate
|
|
119
|
+
all input. Pairs with the `security-auditor` agent and the `owasp-top10` skill.
|
|
120
|
+
|
|
121
|
+
## Common pitfalls (anti-patterns)
|
|
122
|
+
|
|
123
|
+
- **`200 OK` with `{"success": false}`** — breaks HTTP error handling; use real status codes.
|
|
124
|
+
- **Verbs in URLs** (`/createUser`) — the method is the verb.
|
|
125
|
+
- **Unbounded list endpoints** — always paginate; a `/users` that returns 2M rows is a DoS on yourself.
|
|
126
|
+
- **Inconsistent shapes** — one endpoint returns an array, another an object; clients can't generalize.
|
|
127
|
+
- **Leaking existence** via 404-vs-403 to unauthorized users — be deliberate (see status-codes ref).
|
|
128
|
+
- **Breaking changes without a version bump** — renaming/removing a field silently breaks consumers.
|
|
129
|
+
|
|
130
|
+
## When NOT to use REST
|
|
131
|
+
|
|
132
|
+
REST isn't always the right tool. Prefer **GraphQL** when clients need flexible, nested selections and
|
|
133
|
+
you want to avoid over/under-fetching; prefer **gRPC** for low-latency internal service-to-service
|
|
134
|
+
calls; prefer **webhooks/event streams** for server-push. REST shines for resource-oriented public
|
|
135
|
+
HTTP APIs where cacheability and ubiquity matter.
|
|
136
|
+
|
|
137
|
+
## Files in this package
|
|
138
|
+
|
|
139
|
+
- `references/status-codes.md` — full status-code decision guide + edge cases
|
|
140
|
+
- `references/pagination.md` — cursor vs offset, encoding, pitfalls
|
|
141
|
+
- `references/versioning.md` — versioning strategies + deprecation playbook
|
|
142
|
+
- `references/error-handling.md` — error shape, codes, validation details
|
|
143
|
+
- `examples/orders-api.openapi.yaml` — a complete OpenAPI snippet
|
|
144
|
+
- `examples/error-envelope.json` — canonical success/error payloads
|
|
145
|
+
- `scripts/check-envelope.mjs` — runnable Node check that a payload matches the envelope
|
|
146
|
+
|
|
147
|
+
Pairs with the `api-designer` agent, the `api-documenter` agent, and the `webhook-design` skill.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"success_list": {
|
|
3
|
+
"data": [
|
|
4
|
+
{ "id": "101", "status": "open", "total": 49.0, "created_at": "2026-06-28T10:00:00Z" }
|
|
5
|
+
],
|
|
6
|
+
"meta": { "nextCursor": "eyJpZCI6MTAxfQ", "limit": 20, "hasMore": true },
|
|
7
|
+
"error": null
|
|
8
|
+
},
|
|
9
|
+
"success_single": {
|
|
10
|
+
"data": { "id": "101", "status": "open", "total": 49.0 },
|
|
11
|
+
"error": null
|
|
12
|
+
},
|
|
13
|
+
"error_validation": {
|
|
14
|
+
"data": null,
|
|
15
|
+
"error": {
|
|
16
|
+
"code": "validation_failed",
|
|
17
|
+
"message": "The request was invalid.",
|
|
18
|
+
"details": [{ "field": "items", "issue": "must contain at least one item" }],
|
|
19
|
+
"requestId": "req_01HX2K"
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"error_not_found": {
|
|
23
|
+
"data": null,
|
|
24
|
+
"error": { "code": "not_found", "message": "Order 999 not found.", "requestId": "req_01HX2M" }
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
openapi: 3.1.0
|
|
2
|
+
info:
|
|
3
|
+
title: Orders API
|
|
4
|
+
version: "1.0.0"
|
|
5
|
+
description: Example of the conventions in this skill — plural nouns, accurate status codes, cursor pagination, consistent envelope.
|
|
6
|
+
servers:
|
|
7
|
+
- url: https://api.example.com/v1
|
|
8
|
+
paths:
|
|
9
|
+
/orders:
|
|
10
|
+
get:
|
|
11
|
+
summary: List orders
|
|
12
|
+
parameters:
|
|
13
|
+
- { name: status, in: query, schema: { type: string, enum: [open, paid, refunded] } }
|
|
14
|
+
- { name: sort, in: query, schema: { type: string, example: "-created_at" } }
|
|
15
|
+
- { name: limit, in: query, schema: { type: integer, default: 20, maximum: 100 } }
|
|
16
|
+
- { name: cursor, in: query, schema: { type: string } }
|
|
17
|
+
responses:
|
|
18
|
+
"200":
|
|
19
|
+
description: A page of orders
|
|
20
|
+
content:
|
|
21
|
+
application/json:
|
|
22
|
+
schema: { $ref: "#/components/schemas/OrderList" }
|
|
23
|
+
"401": { $ref: "#/components/responses/Error" }
|
|
24
|
+
post:
|
|
25
|
+
summary: Create an order
|
|
26
|
+
parameters:
|
|
27
|
+
- name: Idempotency-Key
|
|
28
|
+
in: header
|
|
29
|
+
required: true
|
|
30
|
+
schema: { type: string }
|
|
31
|
+
requestBody:
|
|
32
|
+
required: true
|
|
33
|
+
content:
|
|
34
|
+
application/json:
|
|
35
|
+
schema: { $ref: "#/components/schemas/OrderInput" }
|
|
36
|
+
responses:
|
|
37
|
+
"201":
|
|
38
|
+
description: Created
|
|
39
|
+
headers:
|
|
40
|
+
Location: { schema: { type: string }, description: URL of the new order }
|
|
41
|
+
content:
|
|
42
|
+
application/json:
|
|
43
|
+
schema: { $ref: "#/components/schemas/OrderEnvelope" }
|
|
44
|
+
"422": { $ref: "#/components/responses/Error" }
|
|
45
|
+
/orders/{id}:
|
|
46
|
+
get:
|
|
47
|
+
summary: Get one order
|
|
48
|
+
parameters:
|
|
49
|
+
- { name: id, in: path, required: true, schema: { type: string } }
|
|
50
|
+
responses:
|
|
51
|
+
"200":
|
|
52
|
+
description: The order
|
|
53
|
+
content:
|
|
54
|
+
application/json:
|
|
55
|
+
schema: { $ref: "#/components/schemas/OrderEnvelope" }
|
|
56
|
+
"404": { $ref: "#/components/responses/Error" }
|
|
57
|
+
components:
|
|
58
|
+
responses:
|
|
59
|
+
Error:
|
|
60
|
+
description: Standard error envelope
|
|
61
|
+
content:
|
|
62
|
+
application/json:
|
|
63
|
+
schema: { $ref: "#/components/schemas/ErrorEnvelope" }
|
|
64
|
+
schemas:
|
|
65
|
+
Order:
|
|
66
|
+
type: object
|
|
67
|
+
properties:
|
|
68
|
+
id: { type: string }
|
|
69
|
+
status: { type: string, enum: [open, paid, refunded] }
|
|
70
|
+
total: { type: number }
|
|
71
|
+
created_at: { type: string, format: date-time }
|
|
72
|
+
OrderInput:
|
|
73
|
+
type: object
|
|
74
|
+
required: [items]
|
|
75
|
+
properties:
|
|
76
|
+
items:
|
|
77
|
+
type: array
|
|
78
|
+
items: { type: object, properties: { product_id: { type: string }, quantity: { type: integer } } }
|
|
79
|
+
Meta:
|
|
80
|
+
type: object
|
|
81
|
+
properties:
|
|
82
|
+
nextCursor: { type: string, nullable: true }
|
|
83
|
+
limit: { type: integer }
|
|
84
|
+
hasMore: { type: boolean }
|
|
85
|
+
OrderList:
|
|
86
|
+
type: object
|
|
87
|
+
properties:
|
|
88
|
+
data: { type: array, items: { $ref: "#/components/schemas/Order" } }
|
|
89
|
+
meta: { $ref: "#/components/schemas/Meta" }
|
|
90
|
+
error: { type: object, nullable: true }
|
|
91
|
+
OrderEnvelope:
|
|
92
|
+
type: object
|
|
93
|
+
properties:
|
|
94
|
+
data: { $ref: "#/components/schemas/Order" }
|
|
95
|
+
error: { type: object, nullable: true }
|
|
96
|
+
ErrorEnvelope:
|
|
97
|
+
type: object
|
|
98
|
+
properties:
|
|
99
|
+
data: { nullable: true }
|
|
100
|
+
error:
|
|
101
|
+
type: object
|
|
102
|
+
properties:
|
|
103
|
+
code: { type: string }
|
|
104
|
+
message: { type: string }
|
|
105
|
+
details: { type: array, items: { type: object } }
|
|
106
|
+
requestId: { type: string }
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Error Handling
|
|
2
|
+
|
|
3
|
+
One error shape across every endpoint, with machine-readable codes and human-readable messages.
|
|
4
|
+
|
|
5
|
+
## The shape
|
|
6
|
+
```json
|
|
7
|
+
{
|
|
8
|
+
"data": null,
|
|
9
|
+
"error": {
|
|
10
|
+
"code": "validation_failed",
|
|
11
|
+
"message": "The request was invalid.",
|
|
12
|
+
"details": [
|
|
13
|
+
{ "field": "email", "issue": "must be a valid email" },
|
|
14
|
+
{ "field": "age", "issue": "must be >= 18" }
|
|
15
|
+
],
|
|
16
|
+
"requestId": "req_01HX..."
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Rules
|
|
22
|
+
- `code` is a **stable, machine-readable** string (clients branch on it) — e.g. `not_found`,
|
|
23
|
+
`validation_failed`, `rate_limited`, `conflict`. Don't change codes once published.
|
|
24
|
+
- `message` is for humans/logs; never put sensitive data or stack traces in it.
|
|
25
|
+
- `details` is optional, used for field-level validation errors.
|
|
26
|
+
- Include a `requestId` so support can correlate a client report with server logs.
|
|
27
|
+
- The HTTP status code and the `error.code` must agree (a `404` carries `not_found`).
|
|
28
|
+
|
|
29
|
+
## Common codes ↔ status
|
|
30
|
+
| code | status |
|
|
31
|
+
|---|---|
|
|
32
|
+
| `validation_failed` | 422 |
|
|
33
|
+
| `not_found` | 404 |
|
|
34
|
+
| `unauthorized` | 401 |
|
|
35
|
+
| `forbidden` | 403 |
|
|
36
|
+
| `conflict` | 409 |
|
|
37
|
+
| `rate_limited` | 429 |
|
|
38
|
+
| `internal_error` | 500 |
|
|
39
|
+
|
|
40
|
+
## Don'ts
|
|
41
|
+
- Don't return `200` with an error body.
|
|
42
|
+
- Don't expose internal exception messages, SQL, or stack traces.
|
|
43
|
+
- Don't use free-form, ever-changing error strings as the thing clients match on — that's what `code` is for.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Pagination — Cursor vs Offset
|
|
2
|
+
|
|
3
|
+
Every collection endpoint must paginate. Choosing the wrong style causes slow queries and inconsistent
|
|
4
|
+
results at scale.
|
|
5
|
+
|
|
6
|
+
## Offset/limit
|
|
7
|
+
```http
|
|
8
|
+
GET /orders?limit=20&offset=40
|
|
9
|
+
```
|
|
10
|
+
- **Pros:** simple, allows jumping to an arbitrary page.
|
|
11
|
+
- **Cons:** the database must scan and discard `offset` rows — page 10,000 is slow. Worse, if rows are
|
|
12
|
+
inserted/deleted while a client paginates, items get **skipped or duplicated** across pages.
|
|
13
|
+
- **Use when:** datasets are small, or users genuinely need page numbers.
|
|
14
|
+
|
|
15
|
+
## Cursor (keyset) pagination
|
|
16
|
+
```http
|
|
17
|
+
GET /orders?limit=20&cursor=eyJpZCI6MTAwfQ
|
|
18
|
+
```
|
|
19
|
+
- The cursor encodes the position (e.g. the last seen sort key), so the DB query is
|
|
20
|
+
`WHERE (created_at, id) < (:lastCreated, :lastId) ORDER BY created_at DESC, id DESC LIMIT :limit`.
|
|
21
|
+
- **Pros:** O(limit) regardless of depth; stable under concurrent inserts/deletes.
|
|
22
|
+
- **Cons:** no arbitrary page jumps; requires a stable, unique sort key (add a tiebreaker like `id`).
|
|
23
|
+
- **Use when:** large, growing, or real-time data — the default for most production APIs.
|
|
24
|
+
|
|
25
|
+
## Cursor encoding
|
|
26
|
+
Encode the cursor opaquely (base64 of the sort key) so clients treat it as a token, not a number to
|
|
27
|
+
manipulate:
|
|
28
|
+
```
|
|
29
|
+
cursor = base64({"created_at":"2026-06-28T10:00:00Z","id":100})
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Response shape
|
|
33
|
+
```json
|
|
34
|
+
{ "data": [ /* ... */ ], "meta": { "nextCursor": "eyJ...", "limit": 20, "hasMore": true } }
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Pitfalls
|
|
38
|
+
- Always include a **unique tiebreaker** in the sort (e.g. `id`); sorting by a non-unique column alone
|
|
39
|
+
drops or repeats rows at page boundaries.
|
|
40
|
+
- Cap `limit` server-side (e.g. max 100) so a client can't request a million rows.
|
|
41
|
+
- Don't mix offset and cursor on the same endpoint — pick one.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Status Codes — Decision Guide
|
|
2
|
+
|
|
3
|
+
Return the most specific accurate code. Clients, proxies, and caches all key off the status line.
|
|
4
|
+
|
|
5
|
+
## Success
|
|
6
|
+
- `200 OK` — successful GET/PUT/PATCH with a body.
|
|
7
|
+
- `201 Created` — successful POST that created a resource. Include a `Location` header pointing to it.
|
|
8
|
+
- `202 Accepted` — accepted for async processing; not done yet (return a status URL).
|
|
9
|
+
- `204 No Content` — success with no body (typical for DELETE).
|
|
10
|
+
|
|
11
|
+
## Client errors
|
|
12
|
+
- `400 Bad Request` — malformed syntax / unparseable.
|
|
13
|
+
- `401 Unauthorized` — not authenticated (no/invalid credentials). Add `WWW-Authenticate`.
|
|
14
|
+
- `403 Forbidden` — authenticated but not allowed.
|
|
15
|
+
- `404 Not Found` — resource doesn't exist (or you're hiding its existence — see below).
|
|
16
|
+
- `405 Method Not Allowed` — wrong verb for the resource. Include an `Allow` header.
|
|
17
|
+
- `409 Conflict` — state conflict (e.g. duplicate, version mismatch on PUT).
|
|
18
|
+
- `422 Unprocessable Entity` — syntactically valid but semantically invalid (failed validation).
|
|
19
|
+
- `429 Too Many Requests` — rate limited. Include `Retry-After`.
|
|
20
|
+
|
|
21
|
+
## Server errors
|
|
22
|
+
- `500 Internal Server Error` — unexpected fault. Never leak stack traces to clients.
|
|
23
|
+
- `503 Service Unavailable` — overloaded/down for maintenance. Include `Retry-After`.
|
|
24
|
+
|
|
25
|
+
## Edge cases & judgement calls
|
|
26
|
+
- **404 vs 403 (existence leakage):** if telling an unauthorized user a resource exists is itself a leak
|
|
27
|
+
(e.g. `/users/{id}` in a multi-tenant app), return `404` instead of `403` so attackers can't enumerate.
|
|
28
|
+
- **400 vs 422:** use `400` for unparseable requests; `422` for well-formed requests that fail business
|
|
29
|
+
validation. Pick one convention and apply it consistently.
|
|
30
|
+
- **409 vs 422 on conflict:** `409` for a conflict with current resource state (stale version, duplicate
|
|
31
|
+
unique key); `422` for input that can't be processed regardless of state.
|
|
32
|
+
- **PUT semantics:** `200`/`204` if it replaced an existing resource; `201` if PUT created one.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Versioning & Evolution
|
|
2
|
+
|
|
3
|
+
Once an API is public, other people's software depends on it. Plan to evolve without breaking them.
|
|
4
|
+
|
|
5
|
+
## What is a breaking change?
|
|
6
|
+
**Breaking** (needs a new version): removing/renaming a field or endpoint, changing a type, adding a
|
|
7
|
+
required request field, changing status-code or error semantics, tightening validation.
|
|
8
|
+
|
|
9
|
+
**Non-breaking** (safe in place): adding a new endpoint, adding an **optional** request field, adding a
|
|
10
|
+
new field to a response (clients must ignore unknown fields — document this expectation).
|
|
11
|
+
|
|
12
|
+
## Strategies
|
|
13
|
+
| Strategy | Example | Notes |
|
|
14
|
+
|---|---|---|
|
|
15
|
+
| URI path | `/v1/orders` | Most operationally clear; easy routing, caching, logs. Most common. |
|
|
16
|
+
| Header | `Accept: application/vnd.api.v1+json` | Cleaner URLs, harder to test/debug. |
|
|
17
|
+
| Query param | `/orders?version=1` | Simple but easy to omit; weakest. |
|
|
18
|
+
|
|
19
|
+
Recommendation: **URI path versioning** for public APIs — explicit and unambiguous.
|
|
20
|
+
|
|
21
|
+
## Deprecation playbook
|
|
22
|
+
1. Announce the new version and a deprecation date for the old one.
|
|
23
|
+
2. Add a `Deprecation` (and optional `Sunset`) header to old-version responses.
|
|
24
|
+
3. Maintain both versions during a published overlap window.
|
|
25
|
+
4. Monitor old-version usage; reach out to remaining consumers.
|
|
26
|
+
5. Remove only after usage approaches zero and the sunset date passes.
|
|
27
|
+
|
|
28
|
+
## Tips
|
|
29
|
+
- Avoid versioning as long as possible by making additive changes.
|
|
30
|
+
- Never silently change behavior within a version — that's a breaking change in disguise.
|
|
31
|
+
- Document unknown-field tolerance so adding fields stays non-breaking.
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Runnable check: validates that a JSON payload matches the standard response
|
|
3
|
+
// envelope from this skill. Demonstrates an *executed*, verifiable example asset.
|
|
4
|
+
//
|
|
5
|
+
// Usage:
|
|
6
|
+
// node check-envelope.mjs '<json>' # check a literal JSON string
|
|
7
|
+
// echo '<json>' | node check-envelope.mjs # check from stdin
|
|
8
|
+
// node check-envelope.mjs --selftest # run built-in test cases
|
|
9
|
+
|
|
10
|
+
const RULES = [
|
|
11
|
+
['has "data" key (may be null)', (o) => 'data' in o],
|
|
12
|
+
['has "error" key', (o) => 'error' in o],
|
|
13
|
+
['error is null OR an object with a string code', (o) =>
|
|
14
|
+
o.error === null || (typeof o.error === 'object' && typeof o.error.code === 'string')],
|
|
15
|
+
['on error, data is null', (o) => o.error === null || o.data === null],
|
|
16
|
+
['list responses include meta.limit', (o) =>
|
|
17
|
+
!Array.isArray(o.data) || (o.meta && typeof o.meta.limit === 'number')],
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
export function validateEnvelope(obj) {
|
|
21
|
+
const failures = RULES.filter(([, fn]) => !safe(fn, obj)).map(([msg]) => msg);
|
|
22
|
+
return { ok: failures.length === 0, failures };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function safe(fn, o) { try { return fn(o); } catch { return false; } }
|
|
26
|
+
|
|
27
|
+
function check(label, json) {
|
|
28
|
+
let obj;
|
|
29
|
+
try { obj = typeof json === 'string' ? JSON.parse(json) : json; }
|
|
30
|
+
catch { console.error(`✗ ${label}: invalid JSON`); return false; }
|
|
31
|
+
const { ok, failures } = validateEnvelope(obj);
|
|
32
|
+
if (ok) console.log(`✓ ${label}: valid envelope`);
|
|
33
|
+
else { console.error(`✗ ${label}:`); failures.forEach((f) => console.error(` - ${f}`)); }
|
|
34
|
+
return ok;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function selftest() {
|
|
38
|
+
const cases = {
|
|
39
|
+
valid_list: { data: [{ id: '1' }], meta: { limit: 20 }, error: null },
|
|
40
|
+
valid_error: { data: null, error: { code: 'not_found', message: 'x' } },
|
|
41
|
+
bad_200_error: { data: { id: '1' }, error: { code: 'oops' } }, // data not null on error -> fail
|
|
42
|
+
bad_list_no_meta: { data: [{ id: '1' }], error: null }, // list without meta.limit -> fail
|
|
43
|
+
};
|
|
44
|
+
let allExpected = true;
|
|
45
|
+
for (const [name, payload] of Object.entries(cases)) {
|
|
46
|
+
const { ok } = validateEnvelope(payload);
|
|
47
|
+
const shouldPass = name.startsWith('valid');
|
|
48
|
+
const correct = ok === shouldPass;
|
|
49
|
+
allExpected &&= correct;
|
|
50
|
+
console.log(`${correct ? '✓' : '✗'} selftest ${name}: ${ok ? 'valid' : 'invalid'} (expected ${shouldPass ? 'valid' : 'invalid'})`);
|
|
51
|
+
}
|
|
52
|
+
process.exit(allExpected ? 0 : 1);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const arg = process.argv[2];
|
|
56
|
+
if (arg === '--selftest') selftest();
|
|
57
|
+
else if (arg) process.exit(check('payload', arg) ? 0 : 1);
|
|
58
|
+
else {
|
|
59
|
+
let buf = '';
|
|
60
|
+
process.stdin.on('data', (c) => (buf += c));
|
|
61
|
+
process.stdin.on('end', () => process.exit(check('stdin', buf) ? 0 : 1));
|
|
62
|
+
}
|