Catch integration issues, API contract mismatches, configuration problems, and breaking changes before deployment.
Today: Sentinel's phase-one CLI is functional and installable from npm. It scans frontend codebases, extracts API calls, and reports static analysis findings. When you point contractSource at a local OpenAPI v3 JSON file, it also checks request-body shapes against your backend schema for matched routes.
Not yet in this release: response-shape diffing, unmatched-endpoint flagging, third-party rules, and the cloud SDK, VS Code extension, and GitHub Action (future phases).
Sentinel is an open-source static analysis platform focused on API contract matching between backend and frontend. It helps teams detect integration problems at development time rather than in production.
The CLI (sentinel scan) walks your source, extracts fetch/axios/similar call sites, runs built-in rules, and optionally compares request bodies against an OpenAPI spec.
When contractSource is set, Sentinel:
- Parses a local OpenAPI v3 JSON spec
- Matches frontend calls to backend routes (method + path pattern)
- Diffs statically resolvable request bodies against the route schema
What v1 flags: missing required fields, unexpected fields, and literal type mismatches (api-contract-mismatch rule).
What v1 does not flag: response bodies, calls that cannot be matched to a route, calls with dynamic/unresolvable URLs or bodies, or query-string/trailing-slash path variants. Those cases are skipped (debug-logged during development, not surfaced as Findings).
OpenAPI support is deliberately scoped: local JSON file paths only — no remote URLs, no YAML.
See Testing API Contract Mismatches for a step-by-step walkthrough.
sentinel scan— end-to-end analysis (config load → scan → format → stdout or file)sentinel init— scaffoldssentinel.config.ts- Built-in rules:
no-hardcoded-url(default: error)missing-error-handler(default: warning)api-contract-mismatch(default: error, active whencontractSourceis configured)
- CLI output:
--format text|json|sarif,--output <file>,--max-warnings, exit codes 0/1/2 - Source extensions:
.ts,.tsx,.js,.jsx,.mts,.cts - Published on npm:
@sentinel-scan/core,@sentinel-scan/cli
- Response-shape diffing
- Unmatched-endpoint flagging
- Third-party / custom rule loading
@sentinel-scan/ai— AI explanations (future phase)@sentinel-scan/cloud-sdk— cloud dashboard upload (future phase)sentinel-vscode— VS Code extension (future phase)sentinel-action— GitHub Action (future phase)
npm install -g @sentinel-scan/cli
sentinel init
sentinel scan ./srcnpm install --save-dev @sentinel-scan/cli
npx sentinel init
npx sentinel scan ./srcRequires Node.js >=20.0.0.
sentinel init scaffolds a starter config with the two original rules. To enable contract checking, add contractSource and api-contract-mismatch manually:
import type { SentinelConfig } from '@sentinel-scan/core'
export default {
include: ['src/**/*.{ts,tsx,js,jsx,mts,cts}'],
// Omit exclude to use engine defaults, or add patterns (merged onto defaults):
// exclude: ['generated/**'],
rules: {
'no-hardcoded-url': 'error',
'missing-error-handler': 'warning',
'api-contract-mismatch': 'error',
},
// Request-body contract checking against a local OpenAPI v3 JSON file only
// (no remote URLs, no YAML)
contractSource: './openapi/api.json',
} satisfies SentinelConfigcontractSource paths are resolved relative to rootDir (the directory you scan, or the config file's directory when discovered automatically).
This section walks through enabling api-contract-mismatch and verifying it works end to end — including v1 scope limits discovered from real-project testing.
You need a local OpenAPI v3 JSON spec file on disk.
Supported: OpenAPI 3.x documents saved as .json with an openapi version field and a paths object.
Not supported:
- YAML specs (
.yaml/.yml) — rejected with an explicit parse error - Remote URLs —
contractSourcemust be a filesystem path - OpenAPI 2 / Swagger (
swagger: "2.0") — validation requiresopenapi: "3.x.x" - Postman collection exports — these are not OpenAPI specs. Pointing
contractSourceat a Postman JSON file produces aconfig-warning(for example,OpenAPI spec must declare openapi version 3.x.x). Convert via Postman's export-as-OpenAPI feature (if available in your Postman version) or generate a real spec from your backend framework.
Minimal working config:
import type { SentinelConfig } from '@sentinel-scan/core'
export default {
contractSource: './openapi.json',
rules: {
'api-contract-mismatch': 'error',
},
} satisfies SentinelConfigContract checking runs when contractSource is set and the rule is not 'off'. If you omit the rule from config, the engine default is error.
Be precise about scope — silence in a real codebase is often expected, not a bug:
- Request body mismatches only — missing required fields, unexpected fields, and literal type mismatches. Response shapes are not checked.
- Statically resolvable URLs only — the URL in the call must be a string literal or a matchable template literal (static path segments with
${…}placeholders). A call likefetch(apiUrl('/api/users'))where the URL is returned by a helper (call-expressionurlKind) will not match any route and produces no finding. - Statically resolvable request bodies only — the body must be an object literal directly in the call (e.g.
axios.post('/users', { name: 'Alice' })). GET requests, calls with no body, or bodies built from variables/spreads/functions produce no finding (not-diffable— expected). - Exact route matching — method and path segments must align with the spec (including
{param}templates). v1 does not normalize query strings (/users/1?page=1won't match{id}) or trailing slashes (/users/vs/users). - Unmatched calls produce no output — if no route in the spec corresponds to the call, Sentinel skips it silently (known v1 limitation, not an error).
Findings appear only when a matched call has request-body discrepancies (discrepancies-found). Unmatched, unresolvable, compatible, and not-diffable cases emit no findings (use --verbose for debug details).
Use this isolated recipe to confirm the feature works before debugging your real codebase's calling conventions:
1. Create openapi.json:
{
"openapi": "3.0.3",
"info": { "title": "Test", "version": "1.0.0" },
"paths": {
"/users": {
"post": {
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"required": ["name"],
"properties": {
"name": { "type": "string" }
}
}
}
}
},
"responses": { "201": { "description": "Created" } }
}
}
}
}2. Create api.ts:
axios.post('/users', { wrongField: 'x' })Use a direct string-literal URL and inline object literal — not a URL helper or variable body.
3. Create sentinel.config.ts (as shown in Config setup above).
4. Run:
sentinel scan . --verbose5. Expect: one api-contract-mismatch finding mentioning a missing required field name for POST /users.
If this recipe passes but your project shows silence, the feature is working — your project's URL/body patterns likely fall outside v1 static-analysis limits.
CLI inspection:
sentinel scan ./src --verbose --format json--verbose(stderr):Parsed N backend route(s),Contract match skipped(unmatched/unresolvable + reason),Contract diff skipped(not-diffable + reason)--format json: inspectapiCalls[],findings[], anddiagnostics[](look forconfig-warningif the spec failed to parse)
Common reasons for no api-contract-mismatch finding:
| Cause | Example | Verbose hint |
|---|---|---|
| Unresolvable URL | fetch(apiUrl('/users')) |
Contract match skipped — call-expression |
| No static request body | GET request, or axios.post(url, payload) where payload is a variable |
Contract diff skipped — body not statically resolvable |
| Unmatched route | Path or method doesn't exist in spec | Contract match skipped — unmatched |
| Compatible diff | Body matches spec (no discrepancy) | No diff skip — simply no findings |
| Spec parse failure | Wrong file format, Postman export, missing openapi 3.x |
config-warning in diagnostics |
Programmatic debugging (@sentinel-scan/core exports):
import {
scan,
resolveConfig,
parseOpenApiSpec,
matchApiCalls,
diffRequestBodies,
} from '@sentinel-scan/core'
const result = await scan(resolveConfig({ rootDir: '.', contractSource: './openapi.json' }))
const spec = await parseOpenApiSpec('/absolute/path/to/openapi.json')
if (spec.ok) {
const matches = matchApiCalls(result.apiCalls, spec.value)
const diffs = diffRequestBodies(result.apiCalls, matches, spec.value)
// Inspect match.status / match.reason and diff.status / diff.reason per call
}# Scan with JSON output
sentinel scan ./src --format json
# Write SARIF for GitHub Code Scanning
sentinel scan ./src --format sarif --output results.sarif
# Fail CI if more than 10 warnings
sentinel scan ./src --max-warnings 10Sentinel looks for config files by walking up from the scan directory:
sentinel.config.ts → sentinel.config.js → sentinel.config.mjs → .sentinelrc.json
If none is found, engine defaults apply. Override with --config <path>.
| Field | Default / notes |
|---|---|
include |
**/*.{ts,tsx,js,jsx,mts,cts} — replaces default when set in config |
exclude |
Merged onto defaults: node_modules, dist, build, *.test.* / *.spec.* (TS), *.d.ts, *.min.js / *.min.mjs, vendor dirs |
rules |
Per-rule severity: error, warning, info, hint, or off |
contractSource |
Optional path to OpenAPI v3 JSON spec (relative to rootDir). No URLs or YAML. |
baseUrl |
Optional prefix for relative URL display |
tsConfigPath |
Optional path to tsconfig.json for path alias resolution |
Default rule severities (when not overridden in config):
| Rule | Default |
|---|---|
no-hardcoded-url |
error |
missing-error-handler |
warning |
api-contract-mismatch |
error |
sentinel scan [path] [options]
| Flag | Description |
|---|---|
-f, --format <text|json|sarif> |
Output format (default: text) |
-o, --output <file> |
Write output to file instead of stdout |
-c, --config <file> |
Explicit config file path |
--root-dir <dir> |
Override scan root directory |
--max-warnings <n> |
Exit 1 if warning count exceeds n (default: disabled) |
-v, --verbose |
Enable debug logging (stderr) |
--no-color |
Disable ANSI colors |
-h, --help |
Show help |
Exit codes
| Code | Meaning |
|---|---|
0 |
Scan completed; no error-severity findings; warnings within --max-warnings threshold (if set) |
1 |
Scan completed; one or more error findings, or warnings exceeded --max-warnings |
2 |
Setup failure (invalid path, config load error, usage error) |
Log output goes to stderr; scan results go to stdout (or --output).
@sentinel-scan/core exposes the analysis engine for library consumers:
npm install @sentinel-scan/core typescriptTypeScript (>=5.0.0) is a required peer dependency — it is used at runtime for AST parsing. Install it alongside core if your project does not already have it.
import { resolveConfig, scan } from '@sentinel-scan/core'
const result = await scan(
resolveConfig({
rootDir: './src',
contractSource: './openapi/api.json',
}),
)
console.log(result.findings)
console.log(result.diagnostics)| Package | Status |
|---|---|
@sentinel-scan/core |
Published — analysis engine (npm) |
@sentinel-scan/cli |
Published — CLI (npm) |
@sentinel-scan/ai |
Planned — not on npm (future phase) |
@sentinel-scan/cloud-sdk |
Planned — not on npm (future phase) |
sentinel-vscode |
Planned — not on npm (future phase) |
sentinel-action |
Planned — not on npm (future phase) |
For contributors working from a clone (not the primary install path):
git clone https://github.com/blaycoder/Sentinel.git
cd Sentinel
npm install
npm run build
npm exec -w @sentinel-scan/cli -- sentinel scan ./srcSee CONTRIBUTING.md for commit conventions, dependency boundaries, and the PR checklist.
Architecture docs are planned.
See RELEASING.md for npm publish instructions.