@zof-ai/api-contract-check 0.0.0-stage → 0.1.1
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/CHANGELOG.md +9 -0
- package/CODE_OF_CONDUCT.md +3 -0
- package/CONTRIBUTING.md +5 -0
- package/LICENSE +202 -0
- package/README.md +89 -2
- package/SECURITY.md +7 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +48 -0
- package/dist/common.d.ts +26 -0
- package/dist/common.js +114 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +419 -0
- package/docs/API.md +11 -0
- package/docs/LIMITATIONS.md +9 -0
- package/docs/RELEASE.md +13 -0
- package/docs/VALIDATION.md +5 -0
- package/examples/authentication.json +46 -0
- package/examples/baseline.json +33 -0
- package/examples/breaking.json +33 -0
- package/examples/compatible.json +36 -0
- package/examples/policy.json +6 -0
- package/package.json +60 -4
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.1
|
|
4
|
+
|
|
5
|
+
Document public installation, executable entry points and reproducible packaged examples. Align release dependency pins; runtime behavior is unchanged.
|
|
6
|
+
|
|
7
|
+
## 0.1.0 (unreleased)
|
|
8
|
+
|
|
9
|
+
Initial independently authored public implementation, distributed under Apache-2.0.
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
# Community conduct
|
|
2
|
+
|
|
3
|
+
Be respectful, constructive and specific. Harassment, discrimination, personal attacks and publication of private information are unacceptable. Maintainers may remove harmful content and restrict participation. Report conduct privately through a verified organization maintainer channel; avoid exposing personal information in public issues.
|
package/CONTRIBUTING.md
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
# Contributing
|
|
2
|
+
|
|
3
|
+
Use Node.js 22 or 24, run `npm ci --ignore-scripts`, `npm run build`, and `npm test`. Add a regression test for functional or security changes. Keep changes independently authored and free of proprietary code, credentials and customer data. Discuss incompatible API changes before implementation. Contributions use the repository license and require maintainer review; dependency updates are never automatically merged.
|
|
4
|
+
|
|
5
|
+
Maintainers review issue reproductions, security boundaries, tests, compatibility and package contents. Use semantic versioning; announce breaking interfaces in the changelog. Deprecations require a documented replacement and at least one minor release before removal.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
|
|
2
|
+
Apache License
|
|
3
|
+
Version 2.0, January 2004
|
|
4
|
+
http://www.apache.org/licenses/
|
|
5
|
+
|
|
6
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
7
|
+
|
|
8
|
+
1. Definitions.
|
|
9
|
+
|
|
10
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
11
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
12
|
+
|
|
13
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
14
|
+
the copyright owner that is granting the License.
|
|
15
|
+
|
|
16
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
17
|
+
other entities that control, are controlled by, or are under common
|
|
18
|
+
control with that entity. For the purposes of this definition,
|
|
19
|
+
"control" means (i) the power, direct or indirect, to cause the
|
|
20
|
+
direction or management of such entity, whether by contract or
|
|
21
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
22
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
23
|
+
|
|
24
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
|
25
|
+
exercising permissions granted by this License.
|
|
26
|
+
|
|
27
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
28
|
+
including but not limited to software source code, documentation
|
|
29
|
+
source, and configuration files.
|
|
30
|
+
|
|
31
|
+
"Object" form shall mean any form resulting from mechanical
|
|
32
|
+
transformation or translation of a Source form, including but
|
|
33
|
+
not limited to compiled object code, generated documentation,
|
|
34
|
+
and conversions to other media types.
|
|
35
|
+
|
|
36
|
+
"Work" shall mean the work of authorship, whether in Source or
|
|
37
|
+
Object form, made available under the License, as indicated by a
|
|
38
|
+
copyright notice that is included in or attached to the work
|
|
39
|
+
(an example is provided in the Appendix below).
|
|
40
|
+
|
|
41
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
|
42
|
+
form, that is based on (or derived from) the Work and for which the
|
|
43
|
+
editorial revisions, annotations, elaborations, or other modifications
|
|
44
|
+
represent, as a whole, an original work of authorship. For the purposes
|
|
45
|
+
of this License, Derivative Works shall not include works that remain
|
|
46
|
+
separable from, or merely link (or bind by name) to the interfaces of,
|
|
47
|
+
the Work and Derivative Works thereof.
|
|
48
|
+
|
|
49
|
+
"Contribution" shall mean any work of authorship, including
|
|
50
|
+
the original version of the Work and any modifications or additions
|
|
51
|
+
to that Work or Derivative Works thereof, that is intentionally
|
|
52
|
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
53
|
+
or by an individual or Legal Entity authorized to submit on behalf of
|
|
54
|
+
the copyright owner. For the purposes of this definition, "submitted"
|
|
55
|
+
means any form of electronic, verbal, or written communication sent
|
|
56
|
+
to the Licensor or its representatives, including but not limited to
|
|
57
|
+
communication on electronic mailing lists, source code control systems,
|
|
58
|
+
and issue tracking systems that are managed by, or on behalf of, the
|
|
59
|
+
Licensor for the purpose of discussing and improving the Work, but
|
|
60
|
+
excluding communication that is conspicuously marked or otherwise
|
|
61
|
+
designated in writing by the copyright owner as "Not a Contribution."
|
|
62
|
+
|
|
63
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
64
|
+
on behalf of whom a Contribution has been received by Licensor and
|
|
65
|
+
subsequently incorporated within the Work.
|
|
66
|
+
|
|
67
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
68
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
69
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
70
|
+
copyright license to reproduce, prepare Derivative Works of,
|
|
71
|
+
publicly display, publicly perform, sublicense, and distribute the
|
|
72
|
+
Work and such Derivative Works in Source or Object form.
|
|
73
|
+
|
|
74
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
75
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
76
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
77
|
+
(except as stated in this section) patent license to make, have made,
|
|
78
|
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
79
|
+
where such license applies only to those patent claims licensable
|
|
80
|
+
by such Contributor that are necessarily infringed by their
|
|
81
|
+
Contribution(s) alone or by combination of their Contribution(s)
|
|
82
|
+
with the Work to which such Contribution(s) was submitted. If You
|
|
83
|
+
institute patent litigation against any entity (including a
|
|
84
|
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
85
|
+
or a Contribution incorporated within the Work constitutes direct
|
|
86
|
+
or contributory patent infringement, then any patent licenses
|
|
87
|
+
granted to You under this License for that Work shall terminate
|
|
88
|
+
as of the date such litigation is filed.
|
|
89
|
+
|
|
90
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
91
|
+
Work or Derivative Works thereof in any medium, with or without
|
|
92
|
+
modifications, and in Source or Object form, provided that You
|
|
93
|
+
meet the following conditions:
|
|
94
|
+
|
|
95
|
+
(a) You must give any other recipients of the Work or
|
|
96
|
+
Derivative Works a copy of this License; and
|
|
97
|
+
|
|
98
|
+
(b) You must cause any modified files to carry prominent notices
|
|
99
|
+
stating that You changed the files; and
|
|
100
|
+
|
|
101
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
102
|
+
that You distribute, all copyright, patent, trademark, and
|
|
103
|
+
attribution notices from the Source form of the Work,
|
|
104
|
+
excluding those notices that do not pertain to any part of
|
|
105
|
+
the Derivative Works; and
|
|
106
|
+
|
|
107
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
|
108
|
+
distribution, then any Derivative Works that You distribute must
|
|
109
|
+
include a readable copy of the attribution notices contained
|
|
110
|
+
within such NOTICE file, excluding those notices that do not
|
|
111
|
+
pertain to any part of the Derivative Works, in at least one
|
|
112
|
+
of the following places: within a NOTICE text file distributed
|
|
113
|
+
as part of the Derivative Works; within the Source form or
|
|
114
|
+
documentation, if provided along with the Derivative Works; or,
|
|
115
|
+
within a display generated by the Derivative Works, if and
|
|
116
|
+
wherever such third-party notices normally appear. The contents
|
|
117
|
+
of the NOTICE file are for informational purposes only and
|
|
118
|
+
do not modify the License. You may add Your own attribution
|
|
119
|
+
notices within Derivative Works that You distribute, alongside
|
|
120
|
+
or as an addendum to the NOTICE text from the Work, provided
|
|
121
|
+
that such additional attribution notices cannot be construed
|
|
122
|
+
as modifying the License.
|
|
123
|
+
|
|
124
|
+
You may add Your own copyright statement to Your modifications and
|
|
125
|
+
may provide additional or different license terms and conditions
|
|
126
|
+
for use, reproduction, or distribution of Your modifications, or
|
|
127
|
+
for any such Derivative Works as a whole, provided Your use,
|
|
128
|
+
reproduction, and distribution of the Work otherwise complies with
|
|
129
|
+
the conditions stated in this License.
|
|
130
|
+
|
|
131
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
132
|
+
any Contribution intentionally submitted for inclusion in the Work
|
|
133
|
+
by You to the Licensor shall be under the terms and conditions of
|
|
134
|
+
this License, without any additional terms or conditions.
|
|
135
|
+
Notwithstanding the above, nothing herein shall supersede or modify
|
|
136
|
+
the terms of any separate license agreement you may have executed
|
|
137
|
+
with Licensor regarding such Contributions.
|
|
138
|
+
|
|
139
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
140
|
+
names, trademarks, service marks, or product names of the Licensor,
|
|
141
|
+
except as required for reasonable and customary use in describing the
|
|
142
|
+
origin of the Work and reproducing the content of the NOTICE file.
|
|
143
|
+
|
|
144
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
145
|
+
agreed to in writing, Licensor provides the Work (and each
|
|
146
|
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
147
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
148
|
+
implied, including, without limitation, any warranties or conditions
|
|
149
|
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
150
|
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
151
|
+
appropriateness of using or redistributing the Work and assume any
|
|
152
|
+
risks associated with Your exercise of permissions under this License.
|
|
153
|
+
|
|
154
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
|
155
|
+
whether in tort (including negligence), contract, or otherwise,
|
|
156
|
+
unless required by applicable law (such as deliberate and grossly
|
|
157
|
+
negligent acts) or agreed to in writing, shall any Contributor be
|
|
158
|
+
liable to You for damages, including any direct, indirect, special,
|
|
159
|
+
incidental, or consequential damages of any character arising as a
|
|
160
|
+
result of this License or out of the use or inability to use the
|
|
161
|
+
Work (including but not limited to damages for loss of goodwill,
|
|
162
|
+
work stoppage, computer failure or malfunction, or any and all
|
|
163
|
+
other commercial damages or losses), even if such Contributor
|
|
164
|
+
has been advised of the possibility of such damages.
|
|
165
|
+
|
|
166
|
+
9. Accepting Warranty or Additional Liability. While redistributing
|
|
167
|
+
the Work or Derivative Works thereof, You may choose to offer,
|
|
168
|
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
169
|
+
or other liability obligations and/or rights consistent with this
|
|
170
|
+
License. However, in accepting such obligations, You may act only
|
|
171
|
+
on Your own behalf and on Your sole responsibility, not on behalf
|
|
172
|
+
of any other Contributor, and only if You agree to indemnify,
|
|
173
|
+
defend, and hold each Contributor harmless for any liability
|
|
174
|
+
incurred by, or claims asserted against, such Contributor by reason
|
|
175
|
+
of your accepting any such warranty or additional liability.
|
|
176
|
+
|
|
177
|
+
END OF TERMS AND CONDITIONS
|
|
178
|
+
|
|
179
|
+
APPENDIX: How to apply the Apache License to your work.
|
|
180
|
+
|
|
181
|
+
To apply the Apache License to your work, attach the following
|
|
182
|
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
183
|
+
replaced with your own identifying information. (Don't include
|
|
184
|
+
the brackets!) The text should be enclosed in the appropriate
|
|
185
|
+
comment syntax for the file format. We also recommend that a
|
|
186
|
+
file or class name and description of purpose be included on the
|
|
187
|
+
same "printed page" as the copyright notice for easier
|
|
188
|
+
identification within third-party archives.
|
|
189
|
+
|
|
190
|
+
Copyright [yyyy] [name of copyright owner]
|
|
191
|
+
|
|
192
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
193
|
+
you may not use this file except in compliance with the License.
|
|
194
|
+
You may obtain a copy of the License at
|
|
195
|
+
|
|
196
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
197
|
+
|
|
198
|
+
Unless required by applicable law or agreed to in writing, software
|
|
199
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
200
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
201
|
+
See the License for the specific language governing permissions and
|
|
202
|
+
limitations under the License.
|
package/README.md
CHANGED
|
@@ -1,3 +1,90 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Zof API Contract Check
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Compare local OpenAPI 3.0/3.1 JSON contracts or a supported JSON Schema subset. Findings explain endpoint/method removal, required inputs, incompatible request/response types, constraints, media types and authentication changes. The implementation is independently authored and works offline without a Zof account.
|
|
4
|
+
|
|
5
|
+
<!-- ZOF_NPM_INSTALL_START -->
|
|
6
|
+
## Install from npm
|
|
7
|
+
|
|
8
|
+
Use Node.js 22 or 24 and npm. File-based operations require Linux or macOS with Python 3.10 or newer; Windows file operations fail closed. In a new project directory, install the version used by this guide:
|
|
9
|
+
|
|
10
|
+
```sh
|
|
11
|
+
npm install --ignore-scripts --save-exact @zof-ai/api-contract-check@0.1.1
|
|
12
|
+
npx --no-install zof-api-contract-check --help
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
The package includes compiled JavaScript, TypeScript declarations and its runtime dependencies. No source build, Zof account or repository upload is required. `--ignore-scripts` disables installation lifecycle scripts; `--no-install` keeps the example commands from downloading another package.
|
|
16
|
+
|
|
17
|
+
Confirm the public ESM entry point; this prints `function`:
|
|
18
|
+
|
|
19
|
+
```sh
|
|
20
|
+
node --input-type=module -e "import('@zof-ai/api-contract-check').then(api => console.log(typeof api.compareContracts))"
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Run a packaged example from the same directory:
|
|
24
|
+
|
|
25
|
+
```sh
|
|
26
|
+
npx --no-install zof-api-contract-check baseline.json compatible.json --root node_modules/@zof-ai/api-contract-check/examples --kind openapi --json
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
The compatible OpenAPI example reports `compatible: true`; the breaking example described below exits 1.
|
|
30
|
+
|
|
31
|
+
Package: [@zof-ai/api-contract-check](https://www.npmjs.com/package/@zof-ai/api-contract-check). The source-checkout instructions and three additional workflows follow.
|
|
32
|
+
<!-- ZOF_NPM_INSTALL_END -->
|
|
33
|
+
|
|
34
|
+
<!-- ZOF_RELEASE_INSTALL_START -->
|
|
35
|
+
## Install from GitHub source
|
|
36
|
+
|
|
37
|
+
Requires Node.js 22 or 24, npm, and Git. Filesystem operations require Linux or macOS with Python 3.10 or newer; Windows file operations fail closed. Clone this repository and explicitly prepare its reviewed release dependencies:
|
|
38
|
+
|
|
39
|
+
```sh
|
|
40
|
+
git clone --branch v0.1.1 --depth 1 https://github.com/Zof-AI/zof-api-contract-check.git
|
|
41
|
+
cd zof-api-contract-check
|
|
42
|
+
node scripts/bootstrap.mjs --allow-network
|
|
43
|
+
npm ci --ignore-scripts --prefer-offline
|
|
44
|
+
npm run build
|
|
45
|
+
npm test
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
The bootstrap downloads only the exact versioned dependency assets from [GitHub Releases](https://github.com/Zof-AI/zof-api-contract-check/releases), as listed in `scripts/release-dependencies.json`, verifies SHA256 and byte size, and adds them to the npm cache. It does not extract archives, run package scripts, collect application data, or send credentials. Downloads require the explicit flag and follow only HTTPS GitHub asset redirects. Installation then fetches ordinary third-party dependencies from npm. Use a writable npm cache; set `npm_config_cache` consistently for both commands if needed. An unavailable asset, changed hash, or oversized response stops installation.
|
|
49
|
+
|
|
50
|
+
<!-- ZOF_RELEASE_INSTALL_END -->
|
|
51
|
+
|
|
52
|
+
## Local installation
|
|
53
|
+
|
|
54
|
+
Use Node.js 22 or 24. After the source installation above:
|
|
55
|
+
|
|
56
|
+
```sh
|
|
57
|
+
npm ci --ignore-scripts
|
|
58
|
+
npm run build
|
|
59
|
+
npm test
|
|
60
|
+
npm pack --ignore-scripts
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Use the GitHub source installation above to obtain the exact release dependencies before building or importing the library. The npm package name is an identifier; registry publication remains separate.
|
|
64
|
+
|
|
65
|
+
## Three examples
|
|
66
|
+
|
|
67
|
+
Run from the repository root after building, or substitute the installed `zof-api-contract-check` executable.
|
|
68
|
+
|
|
69
|
+
```sh
|
|
70
|
+
# Compatible optional response field: exit 0
|
|
71
|
+
node dist/cli.js examples/baseline.json examples/compatible.json --json
|
|
72
|
+
# Response integer changed to string: exit 1
|
|
73
|
+
node dist/cli.js examples/baseline.json examples/breaking.json --markdown
|
|
74
|
+
# Authentication rollout review with explicit policy: exit 1
|
|
75
|
+
node dist/cli.js examples/baseline.json examples/authentication.json --policy examples/policy.json --json
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Pass `--root DIR` to set the only readable workspace root. For standalone JSON Schema comparisons use `--kind json-schema`; this compares accepted inputs, so narrower candidate constraints are incompatible. Default reports are Markdown. Exit codes: 0 completed below the policy threshold, 1 findings at or above it, 2 invalid input or unsupported/incomplete analysis. All findings remain in the report when policy lowers severity.
|
|
79
|
+
|
|
80
|
+
## Library
|
|
81
|
+
|
|
82
|
+
```js
|
|
83
|
+
import { compareContracts, readContract, renderMarkdown } from '@zof-ai/api-contract-check';
|
|
84
|
+
const baseline = await readContract(process.cwd(), 'baseline.json');
|
|
85
|
+
const candidate = await readContract(process.cwd(), 'candidate.json');
|
|
86
|
+
const report = compareContracts(baseline, candidate, { failOn: 'high' });
|
|
87
|
+
console.log(renderMarkdown(report));
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
See [API](docs/API.md), [limitations](docs/LIMITATIONS.md), [security](SECURITY.md), and [release gates](docs/RELEASE.md). No hosted integration or telemetry is implemented.
|
package/SECURITY.md
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# Security policy
|
|
2
|
+
|
|
3
|
+
The 0.1.x line is the initial supported release line. Local analyses are bounded and offline unless an operation is explicitly documented otherwise. Do not provide production secrets or execute untrusted generated code.
|
|
4
|
+
|
|
5
|
+
Use GitHub private vulnerability reporting when enabled for this repository. If it is unavailable, contact an organization owner through an already verified private channel; do not file credentials, exploit details, or customer data in a public issue. Reports will be triaged, reproduced using synthetic data, fixed and reviewed before coordinated disclosure. No response SLA is promised for this initial release.
|
|
6
|
+
|
|
7
|
+
Filesystem operations use bounded, directory-relative descriptors on Linux/macOS with Python 3.10 or newer. Symlinks, hard-linked inputs, special files and traversal are rejected. Open descriptors remain attached to the original directory when names change. File contents can still change during a read or snapshot; this is not an atomic filesystem snapshot. Windows filesystem operations fail closed; in-memory JSON validation and analysis do not require the file helper.
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { compareContracts, readContract, renderMarkdown, } from "./index.js";
|
|
3
|
+
import { cliError, fail, record } from "./common.js";
|
|
4
|
+
try {
|
|
5
|
+
const args = process.argv.slice(2);
|
|
6
|
+
if (args.includes("--help")) {
|
|
7
|
+
console.log("zof-api-contract-check BASELINE.json CANDIDATE.json [--root DIR] [--kind openapi|json-schema] [--policy POLICY.json] [--json|--markdown]\nOffline JSON contracts. Exit 0: completed below threshold; 1: findings; 2: invalid/unsupported input.");
|
|
8
|
+
}
|
|
9
|
+
else if (args.includes("--version"))
|
|
10
|
+
console.log("0.1.1");
|
|
11
|
+
else {
|
|
12
|
+
let root = process.cwd(), format = "markdown", policy, kind;
|
|
13
|
+
const files = [];
|
|
14
|
+
for (let i = 0; i < args.length; i++) {
|
|
15
|
+
const a = args[i];
|
|
16
|
+
if (a === "--root") {
|
|
17
|
+
root = args[++i] ?? fail("ARGUMENT_REQUIRED");
|
|
18
|
+
}
|
|
19
|
+
else if (a === "--policy")
|
|
20
|
+
policy = args[++i] ?? fail("ARGUMENT_REQUIRED");
|
|
21
|
+
else if (a === "--kind")
|
|
22
|
+
kind = args[++i];
|
|
23
|
+
else if (a === "--json")
|
|
24
|
+
format = "json";
|
|
25
|
+
else if (a === "--markdown")
|
|
26
|
+
format = "markdown";
|
|
27
|
+
else if (a.startsWith("-"))
|
|
28
|
+
fail("ARGUMENT_UNKNOWN");
|
|
29
|
+
else
|
|
30
|
+
files.push(a);
|
|
31
|
+
}
|
|
32
|
+
if (files.length !== 2)
|
|
33
|
+
fail("TWO_FILES_REQUIRED");
|
|
34
|
+
const options = policy
|
|
35
|
+
? record(await readContract(root, policy))
|
|
36
|
+
: {};
|
|
37
|
+
if (kind)
|
|
38
|
+
options.kind = kind;
|
|
39
|
+
const result = compareContracts(await readContract(root, files[0]), await readContract(root, files[1]), options);
|
|
40
|
+
console.log(format === "json"
|
|
41
|
+
? JSON.stringify(result, null, 2)
|
|
42
|
+
: renderMarkdown(result));
|
|
43
|
+
process.exitCode = result.compatible ? 0 : 1;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
cliError(error);
|
|
48
|
+
}
|
package/dist/common.d.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export declare const MAX_INPUT_BYTES: number;
|
|
2
|
+
export type Severity = "info" | "low" | "medium" | "high" | "critical";
|
|
3
|
+
export interface Finding {
|
|
4
|
+
id: string;
|
|
5
|
+
severity: Severity;
|
|
6
|
+
message: string;
|
|
7
|
+
path?: string;
|
|
8
|
+
}
|
|
9
|
+
export declare class SafeInputError extends Error {
|
|
10
|
+
readonly code: string;
|
|
11
|
+
constructor(code: string);
|
|
12
|
+
}
|
|
13
|
+
export declare function fail(code: string): never;
|
|
14
|
+
export declare function record(v: unknown): Record<string, unknown>;
|
|
15
|
+
export declare function guard(v: unknown, depth?: number, budget?: {
|
|
16
|
+
n: number;
|
|
17
|
+
bytes: number;
|
|
18
|
+
}): void;
|
|
19
|
+
export declare function parseJson(text: string): unknown;
|
|
20
|
+
export declare function relativePath(path: string): void;
|
|
21
|
+
export declare function readBounded(root: string, path: string): Promise<string>;
|
|
22
|
+
export declare function severity(value: unknown): Severity;
|
|
23
|
+
export declare function exceeds(findings: Finding[], level?: Severity): boolean;
|
|
24
|
+
export declare function display(path: string): string;
|
|
25
|
+
export declare function markdown(title: string, findings: Finding[], limitations: string[]): string;
|
|
26
|
+
export declare function cliError(error: unknown): void;
|
package/dist/common.js
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { redactText, safeReadFile } from "@zof-ai/evidence-spec";
|
|
2
|
+
import { isAbsolute } from "node:path";
|
|
3
|
+
export const MAX_INPUT_BYTES = 4 * 1024 * 1024;
|
|
4
|
+
export class SafeInputError extends Error {
|
|
5
|
+
code;
|
|
6
|
+
constructor(code) {
|
|
7
|
+
super(`Input rejected (${code}).`);
|
|
8
|
+
this.code = code;
|
|
9
|
+
this.name = "SafeInputError";
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
export function fail(code) {
|
|
13
|
+
throw new SafeInputError(code);
|
|
14
|
+
}
|
|
15
|
+
export function record(v) {
|
|
16
|
+
if (!v || typeof v !== "object" || Array.isArray(v))
|
|
17
|
+
fail("OBJECT_REQUIRED");
|
|
18
|
+
return v;
|
|
19
|
+
}
|
|
20
|
+
export function guard(v, depth = 0, budget = { n: 0, bytes: 0 }) {
|
|
21
|
+
if (depth > 48 || ++budget.n > 100000)
|
|
22
|
+
fail("INPUT_COMPLEXITY");
|
|
23
|
+
if (v === null || typeof v === "boolean")
|
|
24
|
+
return;
|
|
25
|
+
if (typeof v === "string") {
|
|
26
|
+
budget.bytes += Buffer.byteLength(v);
|
|
27
|
+
if (budget.bytes > MAX_INPUT_BYTES)
|
|
28
|
+
fail("INPUT_SIZE");
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
if (typeof v === "number") {
|
|
32
|
+
if (!Number.isFinite(v))
|
|
33
|
+
fail("NUMBER_INVALID");
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
if (typeof v !== "object")
|
|
37
|
+
fail("JSON_REQUIRED");
|
|
38
|
+
if (!Array.isArray(v) &&
|
|
39
|
+
Object.getPrototypeOf(v) !== Object.prototype &&
|
|
40
|
+
Object.getPrototypeOf(v) !== null)
|
|
41
|
+
fail("JSON_REQUIRED");
|
|
42
|
+
for (const d of Object.values(Object.getOwnPropertyDescriptors(v))) {
|
|
43
|
+
if (d.get || d.set)
|
|
44
|
+
fail("ACCESSOR_REJECTED");
|
|
45
|
+
guard(d.value, depth + 1, budget);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
export function parseJson(text) {
|
|
49
|
+
if (Buffer.byteLength(text) > MAX_INPUT_BYTES)
|
|
50
|
+
fail("INPUT_SIZE");
|
|
51
|
+
let v;
|
|
52
|
+
try {
|
|
53
|
+
v = JSON.parse(text);
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
fail("JSON_INVALID");
|
|
57
|
+
}
|
|
58
|
+
guard(v);
|
|
59
|
+
return v;
|
|
60
|
+
}
|
|
61
|
+
export function relativePath(path) {
|
|
62
|
+
if (typeof path !== "string" ||
|
|
63
|
+
path.length > 512 ||
|
|
64
|
+
isAbsolute(path) ||
|
|
65
|
+
path.includes("\\") ||
|
|
66
|
+
path.includes("\0") ||
|
|
67
|
+
path.includes(":") ||
|
|
68
|
+
path.split("/").some((x) => !x || x === "." || x === ".."))
|
|
69
|
+
fail("PATH_INVALID");
|
|
70
|
+
}
|
|
71
|
+
export async function readBounded(root, path) {
|
|
72
|
+
relativePath(path);
|
|
73
|
+
try {
|
|
74
|
+
const data = await safeReadFile(root, path, MAX_INPUT_BYTES);
|
|
75
|
+
return new TextDecoder("utf-8", { fatal: true }).decode(data);
|
|
76
|
+
}
|
|
77
|
+
catch (e) {
|
|
78
|
+
if (e instanceof SafeInputError)
|
|
79
|
+
throw e;
|
|
80
|
+
fail("FILE_UNAVAILABLE");
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
const severityOrder = ["info", "low", "medium", "high", "critical"];
|
|
84
|
+
export function severity(value) {
|
|
85
|
+
if (!severityOrder.includes(value))
|
|
86
|
+
fail("SEVERITY_INVALID");
|
|
87
|
+
return value;
|
|
88
|
+
}
|
|
89
|
+
export function exceeds(findings, level = "high") {
|
|
90
|
+
return findings.some((f) => severityOrder.indexOf(f.severity) >= severityOrder.indexOf(level));
|
|
91
|
+
}
|
|
92
|
+
export function display(path) {
|
|
93
|
+
path = redactText(path);
|
|
94
|
+
return /^[\w/.{}~:$ -]{1,400}$/.test(path) && !path.includes("://")
|
|
95
|
+
? path
|
|
96
|
+
: "[location omitted]";
|
|
97
|
+
}
|
|
98
|
+
export function markdown(title, findings, limitations) {
|
|
99
|
+
return (`# ${title}\n\n` +
|
|
100
|
+
(findings.length
|
|
101
|
+
? findings
|
|
102
|
+
.map((f) => `- **${f.severity}** ${f.id}: ${f.message}${f.path ? " (" + display(f.path) + ")" : ""}`)
|
|
103
|
+
.join("\n")
|
|
104
|
+
: "No findings in the supported static checks.") +
|
|
105
|
+
"\n\nLimitations:\n" +
|
|
106
|
+
limitations.map((s) => "- " + s).join("\n") +
|
|
107
|
+
"\n");
|
|
108
|
+
}
|
|
109
|
+
export function cliError(error) {
|
|
110
|
+
process.stderr.write(error instanceof SafeInputError
|
|
111
|
+
? error.message + "\n"
|
|
112
|
+
: "Operation failed safely.\n");
|
|
113
|
+
process.exitCode = 2;
|
|
114
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type Finding, type Severity } from "./common.js";
|
|
2
|
+
export { SafeInputError, type Finding, type Severity } from "./common.js";
|
|
3
|
+
export interface ContractOptions {
|
|
4
|
+
kind?: "openapi" | "json-schema";
|
|
5
|
+
failOn?: Severity;
|
|
6
|
+
severity?: Record<string, Severity>;
|
|
7
|
+
}
|
|
8
|
+
export interface ContractReport {
|
|
9
|
+
kind: "openapi" | "json-schema";
|
|
10
|
+
compatible: boolean;
|
|
11
|
+
findings: Finding[];
|
|
12
|
+
limitations: string[];
|
|
13
|
+
}
|
|
14
|
+
export declare function compareContracts(baseline: unknown, candidate: unknown, options?: ContractOptions): ContractReport;
|
|
15
|
+
export declare const readContract: (root: string, path: string) => Promise<unknown>;
|
|
16
|
+
export declare const renderMarkdown: (report: ContractReport) => string;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
import { fail, record, guard, parseJson, readBounded, severity, exceeds, display, markdown, } from "./common.js";
|
|
2
|
+
export { SafeInputError } from "./common.js";
|
|
3
|
+
const methods = [
|
|
4
|
+
"get",
|
|
5
|
+
"put",
|
|
6
|
+
"post",
|
|
7
|
+
"delete",
|
|
8
|
+
"options",
|
|
9
|
+
"head",
|
|
10
|
+
"patch",
|
|
11
|
+
"trace",
|
|
12
|
+
];
|
|
13
|
+
const keys = new Set([
|
|
14
|
+
"type",
|
|
15
|
+
"enum",
|
|
16
|
+
"const",
|
|
17
|
+
"required",
|
|
18
|
+
"properties",
|
|
19
|
+
"items",
|
|
20
|
+
"$ref",
|
|
21
|
+
"$defs",
|
|
22
|
+
"definitions",
|
|
23
|
+
"additionalProperties",
|
|
24
|
+
"minimum",
|
|
25
|
+
"maximum",
|
|
26
|
+
"exclusiveMinimum",
|
|
27
|
+
"exclusiveMaximum",
|
|
28
|
+
"minLength",
|
|
29
|
+
"maxLength",
|
|
30
|
+
"minItems",
|
|
31
|
+
"maxItems",
|
|
32
|
+
"pattern",
|
|
33
|
+
"format",
|
|
34
|
+
"nullable",
|
|
35
|
+
"description",
|
|
36
|
+
"title",
|
|
37
|
+
"default",
|
|
38
|
+
"examples",
|
|
39
|
+
"example",
|
|
40
|
+
"deprecated",
|
|
41
|
+
"readOnly",
|
|
42
|
+
"writeOnly",
|
|
43
|
+
"xml",
|
|
44
|
+
"externalDocs",
|
|
45
|
+
"$schema",
|
|
46
|
+
"$id",
|
|
47
|
+
]);
|
|
48
|
+
const limits = [
|
|
49
|
+
"minimum",
|
|
50
|
+
"maximum",
|
|
51
|
+
"exclusiveMinimum",
|
|
52
|
+
"exclusiveMaximum",
|
|
53
|
+
"minLength",
|
|
54
|
+
"maxLength",
|
|
55
|
+
"minItems",
|
|
56
|
+
"maxItems",
|
|
57
|
+
];
|
|
58
|
+
const limitations = [
|
|
59
|
+
"Static JSON OpenAPI 3.0/3.1 and a documented JSON Schema subset only; YAML, composition and remote/file references are rejected.",
|
|
60
|
+
"No runtime implementation, production traffic or behavioral compatibility is verified.",
|
|
61
|
+
"Response property removal is conservatively flagged even when optional; severity policies change CI disposition, not the facts.",
|
|
62
|
+
];
|
|
63
|
+
function stable(v) {
|
|
64
|
+
if (Array.isArray(v))
|
|
65
|
+
return "[" + v.map(stable).join(",") + "]";
|
|
66
|
+
if (v && typeof v === "object")
|
|
67
|
+
return ("{" +
|
|
68
|
+
Object.keys(v)
|
|
69
|
+
.sort()
|
|
70
|
+
.map((k) => JSON.stringify(k) + ":" + stable(v[k]))
|
|
71
|
+
.join(",") +
|
|
72
|
+
"}");
|
|
73
|
+
return JSON.stringify(v);
|
|
74
|
+
}
|
|
75
|
+
function strings(v) {
|
|
76
|
+
if (v === undefined)
|
|
77
|
+
return [];
|
|
78
|
+
if (!Array.isArray(v) || v.some((s) => typeof s !== "string"))
|
|
79
|
+
fail("STRING_ARRAY_REQUIRED");
|
|
80
|
+
return v;
|
|
81
|
+
}
|
|
82
|
+
function deref(root, value, stack = []) {
|
|
83
|
+
const obj = record(value);
|
|
84
|
+
if (obj.$ref === undefined)
|
|
85
|
+
return obj;
|
|
86
|
+
const ref = obj.$ref;
|
|
87
|
+
if (typeof ref !== "string" ||
|
|
88
|
+
(ref !== "#" && !ref.startsWith("#/")) ||
|
|
89
|
+
ref.length > 512)
|
|
90
|
+
fail("REFERENCE_UNSUPPORTED");
|
|
91
|
+
if (stack.includes(ref) || stack.length >= 24)
|
|
92
|
+
fail("REFERENCE_CYCLE");
|
|
93
|
+
if (Object.keys(obj).some((k) => !["$ref", "description", "summary"].includes(k)))
|
|
94
|
+
fail("REFERENCE_SIBLING_UNSUPPORTED");
|
|
95
|
+
let target = root;
|
|
96
|
+
for (const segment of ref === "#" ? [] : ref.slice(2).split("/")) {
|
|
97
|
+
const key = segment.replace(/~1/g, "/").replace(/~0/g, "~");
|
|
98
|
+
const r = record(target);
|
|
99
|
+
if (!Object.hasOwn(r, key))
|
|
100
|
+
fail("REFERENCE_MISSING");
|
|
101
|
+
target = r[key];
|
|
102
|
+
}
|
|
103
|
+
return deref(root, target, [...stack, ref]);
|
|
104
|
+
}
|
|
105
|
+
function checkSchema(root, value, refs = new Set(), depth = 0) {
|
|
106
|
+
if (depth > 32 || refs.has(value))
|
|
107
|
+
fail("SCHEMA_RECURSION");
|
|
108
|
+
const next = new Set(refs);
|
|
109
|
+
next.add(value);
|
|
110
|
+
const s = deref(root, value);
|
|
111
|
+
if (next.has(s) && s !== value)
|
|
112
|
+
fail("SCHEMA_RECURSION");
|
|
113
|
+
for (const k of Object.keys(s))
|
|
114
|
+
if (!keys.has(k))
|
|
115
|
+
fail("SCHEMA_KEYWORD_UNSUPPORTED");
|
|
116
|
+
const types = s.type === undefined
|
|
117
|
+
? []
|
|
118
|
+
: typeof s.type === "string"
|
|
119
|
+
? [s.type]
|
|
120
|
+
: strings(s.type);
|
|
121
|
+
if (types.some((t) => ![
|
|
122
|
+
"null",
|
|
123
|
+
"string",
|
|
124
|
+
"integer",
|
|
125
|
+
"number",
|
|
126
|
+
"boolean",
|
|
127
|
+
"array",
|
|
128
|
+
"object",
|
|
129
|
+
].includes(t)))
|
|
130
|
+
fail("SCHEMA_TYPE_INVALID");
|
|
131
|
+
strings(s.required);
|
|
132
|
+
if (s.enum !== undefined && (!Array.isArray(s.enum) || !s.enum.length))
|
|
133
|
+
fail("SCHEMA_ENUM_INVALID");
|
|
134
|
+
for (const k of limits)
|
|
135
|
+
if (s[k] !== undefined &&
|
|
136
|
+
(typeof s[k] !== "number" || !Number.isFinite(s[k])))
|
|
137
|
+
fail("SCHEMA_LIMIT_INVALID");
|
|
138
|
+
for (const k of ["pattern", "format"])
|
|
139
|
+
if (s[k] !== undefined && typeof s[k] !== "string")
|
|
140
|
+
fail("SCHEMA_STRING_INVALID");
|
|
141
|
+
for (const k of ["nullable", "readOnly", "writeOnly"])
|
|
142
|
+
if (s[k] !== undefined && typeof s[k] !== "boolean")
|
|
143
|
+
fail("SCHEMA_BOOLEAN_INVALID");
|
|
144
|
+
for (const k of ["properties", "$defs", "definitions"])
|
|
145
|
+
if (s[k] !== undefined)
|
|
146
|
+
for (const v of Object.values(record(s[k])))
|
|
147
|
+
checkSchema(root, v, next, depth + 1);
|
|
148
|
+
if (s.items !== undefined)
|
|
149
|
+
checkSchema(root, s.items, next, depth + 1);
|
|
150
|
+
if (s.additionalProperties !== undefined &&
|
|
151
|
+
typeof s.additionalProperties !== "boolean")
|
|
152
|
+
checkSchema(root, s.additionalProperties, next, depth + 1);
|
|
153
|
+
}
|
|
154
|
+
function schemaType(s) {
|
|
155
|
+
const t = s.type === undefined
|
|
156
|
+
? ["any"]
|
|
157
|
+
: typeof s.type === "string"
|
|
158
|
+
? [s.type]
|
|
159
|
+
: strings(s.type);
|
|
160
|
+
return s.nullable === true ? [...t, "null"] : t;
|
|
161
|
+
}
|
|
162
|
+
function isSubset(a, b) {
|
|
163
|
+
return a.every((t) => b.includes(t) ||
|
|
164
|
+
b.includes("any") ||
|
|
165
|
+
(t === "integer" && b.includes("number")));
|
|
166
|
+
}
|
|
167
|
+
export function compareContracts(baseline, candidate, options = {}) {
|
|
168
|
+
guard(baseline);
|
|
169
|
+
guard(candidate);
|
|
170
|
+
guard(options);
|
|
171
|
+
const b = record(baseline), c = record(candidate);
|
|
172
|
+
const kind = options.kind ?? (b.openapi !== undefined ? "openapi" : "json-schema");
|
|
173
|
+
if (!["openapi", "json-schema"].includes(kind))
|
|
174
|
+
fail("KIND_INVALID");
|
|
175
|
+
const threshold = severity(options.failOn ?? "high");
|
|
176
|
+
const policy = options.severity ?? {};
|
|
177
|
+
record(policy);
|
|
178
|
+
for (const v of Object.values(policy))
|
|
179
|
+
severity(v);
|
|
180
|
+
const findings = [];
|
|
181
|
+
const add = (id, message, path, level = "high") => {
|
|
182
|
+
if (findings.length >= 10000)
|
|
183
|
+
fail("FINDING_LIMIT");
|
|
184
|
+
findings.push({
|
|
185
|
+
id,
|
|
186
|
+
severity: policy[id] ?? level,
|
|
187
|
+
message,
|
|
188
|
+
path: display(path),
|
|
189
|
+
});
|
|
190
|
+
};
|
|
191
|
+
function compare(bs, cs, path, direction, depth = 0) {
|
|
192
|
+
if (depth > 32)
|
|
193
|
+
fail("SCHEMA_RECURSION");
|
|
194
|
+
const x = deref(b, bs), y = deref(c, cs);
|
|
195
|
+
const left = schemaType(x), right = schemaType(y);
|
|
196
|
+
if (!(direction === "request" ? isSubset(left, right) : isSubset(right, left)))
|
|
197
|
+
add("type-incompatible", "The accepted or returned value type is incompatible.", path);
|
|
198
|
+
const requiredB = strings(x.required), requiredC = strings(y.required);
|
|
199
|
+
for (const prop of direction === "request"
|
|
200
|
+
? requiredC.filter((k) => !requiredB.includes(k))
|
|
201
|
+
: requiredB.filter((k) => !requiredC.includes(k)))
|
|
202
|
+
add("required-field-changed", direction === "request"
|
|
203
|
+
? "A request property became required."
|
|
204
|
+
: "A response property is no longer guaranteed.", path + "/properties/" + prop);
|
|
205
|
+
const en = (s) => s.const !== undefined
|
|
206
|
+
? [s.const]
|
|
207
|
+
: Array.isArray(s.enum)
|
|
208
|
+
? s.enum
|
|
209
|
+
: undefined;
|
|
210
|
+
const eb = en(x), ec = en(y);
|
|
211
|
+
if (direction === "request"
|
|
212
|
+
? ec &&
|
|
213
|
+
(!eb || eb.some((v) => !ec.some((w) => stable(v) === stable(w))))
|
|
214
|
+
: eb &&
|
|
215
|
+
(!ec || ec.some((v) => !eb.some((w) => stable(v) === stable(w)))))
|
|
216
|
+
add("enum-incompatible", "Allowed values changed incompatibly.", path);
|
|
217
|
+
for (const k of limits) {
|
|
218
|
+
const old = x[k], now = y[k];
|
|
219
|
+
const min = k.includes("inimum") || k.startsWith("min");
|
|
220
|
+
const tight = now !== undefined &&
|
|
221
|
+
(old === undefined || (min ? now > old : now < old));
|
|
222
|
+
const loose = old !== undefined &&
|
|
223
|
+
(now === undefined || (min ? now < old : now > old));
|
|
224
|
+
if (direction === "request" ? tight : loose)
|
|
225
|
+
add("constraint-incompatible", "A value constraint changed incompatibly.", path + "/" + k);
|
|
226
|
+
}
|
|
227
|
+
for (const k of ["pattern", "format", "readOnly", "writeOnly"])
|
|
228
|
+
if (x[k] !== y[k])
|
|
229
|
+
add("constraint-changed", "A format, pattern or visibility constraint changed.", path + "/" + k);
|
|
230
|
+
const xp = x.properties === undefined ? {} : record(x.properties), yp = y.properties === undefined ? {} : record(y.properties);
|
|
231
|
+
for (const key of Object.keys(xp).sort()) {
|
|
232
|
+
if (!Object.hasOwn(yp, key)) {
|
|
233
|
+
if (direction === "response" || y.additionalProperties === false)
|
|
234
|
+
add("property-removed", "A previously described property was removed.", path + "/properties/" + key);
|
|
235
|
+
}
|
|
236
|
+
else
|
|
237
|
+
compare(xp[key], yp[key], path + "/properties/" + key, direction, depth + 1);
|
|
238
|
+
}
|
|
239
|
+
if (direction === "request" &&
|
|
240
|
+
y.additionalProperties === false &&
|
|
241
|
+
x.additionalProperties !== false)
|
|
242
|
+
add("additional-properties-restricted", "Requests no longer permit additional properties.", path);
|
|
243
|
+
if (direction === "response" &&
|
|
244
|
+
x.additionalProperties === false &&
|
|
245
|
+
(y.additionalProperties !== false || Object.keys(yp).some(k => !Object.hasOwn(xp, k))))
|
|
246
|
+
add("additional-properties-changed", "Responses may include properties previously forbidden by the contract.", path);
|
|
247
|
+
if (typeof x.additionalProperties === "object" ||
|
|
248
|
+
typeof y.additionalProperties === "object") {
|
|
249
|
+
if (typeof x.additionalProperties === "object" &&
|
|
250
|
+
typeof y.additionalProperties === "object")
|
|
251
|
+
compare(x.additionalProperties, y.additionalProperties, path + "/additionalProperties", direction, depth + 1);
|
|
252
|
+
else if (direction === "request"
|
|
253
|
+
? typeof y.additionalProperties === "object"
|
|
254
|
+
: typeof x.additionalProperties === "object")
|
|
255
|
+
add("additional-properties-changed", "Additional property constraints changed incompatibly.", path);
|
|
256
|
+
}
|
|
257
|
+
if (x.items !== undefined && y.items !== undefined)
|
|
258
|
+
compare(x.items, y.items, path + "/items", direction, depth + 1);
|
|
259
|
+
else if (direction === "request"
|
|
260
|
+
? x.items === undefined && y.items !== undefined
|
|
261
|
+
: x.items !== undefined && y.items === undefined)
|
|
262
|
+
add("items-changed", "Array item constraints changed incompatibly.", path);
|
|
263
|
+
}
|
|
264
|
+
if (kind === "json-schema") {
|
|
265
|
+
checkSchema(b, b);
|
|
266
|
+
checkSchema(c, c);
|
|
267
|
+
compare(b, c, "#", "request");
|
|
268
|
+
}
|
|
269
|
+
else {
|
|
270
|
+
for (const root of [b, c]) {
|
|
271
|
+
if (typeof root.openapi !== "string" ||
|
|
272
|
+
!/^3\.(0|1)\.\d+$/.test(root.openapi))
|
|
273
|
+
fail("OPENAPI_VERSION_UNSUPPORTED");
|
|
274
|
+
if (root.paths === undefined)
|
|
275
|
+
fail("OPENAPI_PATHS_REQUIRED");
|
|
276
|
+
record(root.paths);
|
|
277
|
+
const scan = (v, depth = 0) => {
|
|
278
|
+
if (depth > 40)
|
|
279
|
+
fail("INPUT_COMPLEXITY");
|
|
280
|
+
if (!v || typeof v !== "object")
|
|
281
|
+
return;
|
|
282
|
+
if (Array.isArray(v)) {
|
|
283
|
+
for (const x of v)
|
|
284
|
+
scan(x, depth + 1);
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
const r = record(v);
|
|
288
|
+
if (r.$ref !== undefined)
|
|
289
|
+
deref(root, r);
|
|
290
|
+
for (const [k, x] of Object.entries(r)) {
|
|
291
|
+
if (k === "schema")
|
|
292
|
+
checkSchema(root, x);
|
|
293
|
+
else if (k === "schemas")
|
|
294
|
+
for (const s of Object.values(record(x)))
|
|
295
|
+
checkSchema(root, s);
|
|
296
|
+
else
|
|
297
|
+
scan(x, depth + 1);
|
|
298
|
+
}
|
|
299
|
+
};
|
|
300
|
+
scan(root);
|
|
301
|
+
}
|
|
302
|
+
const bp = record(b.paths), cp = record(c.paths);
|
|
303
|
+
function security(root, op) {
|
|
304
|
+
const v = op.security ?? root.security ?? [];
|
|
305
|
+
if (!Array.isArray(v))
|
|
306
|
+
fail("SECURITY_INVALID");
|
|
307
|
+
return v.map((x) => {
|
|
308
|
+
const r = record(x);
|
|
309
|
+
for (const a of Object.values(r))
|
|
310
|
+
strings(a);
|
|
311
|
+
return r;
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
function parameters(root, path, op) {
|
|
315
|
+
const out = new Map();
|
|
316
|
+
for (const group of [path.parameters ?? [], op.parameters ?? []]) {
|
|
317
|
+
if (!Array.isArray(group))
|
|
318
|
+
fail("PARAMETERS_INVALID");
|
|
319
|
+
for (const raw of group) {
|
|
320
|
+
const p = deref(root, raw);
|
|
321
|
+
if (typeof p.name !== "string" ||
|
|
322
|
+
!["query", "header", "path", "cookie"].includes(p.in))
|
|
323
|
+
fail("PARAMETER_INVALID");
|
|
324
|
+
if (p.content !== undefined || p.schema === undefined)
|
|
325
|
+
fail("PARAMETER_SCHEMA_REQUIRED");
|
|
326
|
+
if (p.required !== undefined && typeof p.required !== "boolean")
|
|
327
|
+
fail("PARAMETER_INVALID");
|
|
328
|
+
out.set(p.in + ":" + p.name, p);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
return out;
|
|
332
|
+
}
|
|
333
|
+
function media(old, now, path, direction) {
|
|
334
|
+
const oc = old.content === undefined ? {} : record(old.content), nc = now.content === undefined ? {} : record(now.content);
|
|
335
|
+
for (const type of Object.keys(oc)) {
|
|
336
|
+
if (!Object.hasOwn(nc, type)) {
|
|
337
|
+
add("media-type-removed", "A documented media type was removed.", path);
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
const x = record(oc[type]), y = record(nc[type]);
|
|
341
|
+
if (x.schema !== undefined && y.schema !== undefined)
|
|
342
|
+
compare(x.schema, y.schema, path + "/schema", direction);
|
|
343
|
+
else if (x.schema !== y.schema)
|
|
344
|
+
add("schema-presence-changed", "A documented schema was added or removed; review compatibility.", path);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
for (const path of Object.keys(bp).sort()) {
|
|
348
|
+
if (!path.startsWith("/") || path.length > 512)
|
|
349
|
+
fail("OPENAPI_PATH_INVALID");
|
|
350
|
+
if (!Object.hasOwn(cp, path)) {
|
|
351
|
+
add("endpoint-removed", "A documented endpoint was removed.", path);
|
|
352
|
+
continue;
|
|
353
|
+
}
|
|
354
|
+
const old = deref(b, bp[path]), now = deref(c, cp[path]);
|
|
355
|
+
for (const method of methods) {
|
|
356
|
+
if (old[method] === undefined)
|
|
357
|
+
continue;
|
|
358
|
+
const loc = method.toUpperCase() + " " + path;
|
|
359
|
+
if (now[method] === undefined) {
|
|
360
|
+
add("method-removed", "An HTTP method was removed.", loc);
|
|
361
|
+
continue;
|
|
362
|
+
}
|
|
363
|
+
const x = record(old[method]), y = record(now[method]);
|
|
364
|
+
if (stable(security(b, x)) !== stable(security(c, y)))
|
|
365
|
+
add("authentication-changed", "Authentication requirements changed.", loc);
|
|
366
|
+
const xp = parameters(b, old, x), yp = parameters(c, now, y);
|
|
367
|
+
for (const [key, p] of yp) {
|
|
368
|
+
const before = xp.get(key);
|
|
369
|
+
if ((p.required === true || p.in === "path") &&
|
|
370
|
+
(!before || (before.required !== true && before.in !== "path")))
|
|
371
|
+
add("required-parameter-added", "A new request parameter is required.", loc + "/parameters/" + key);
|
|
372
|
+
if (before)
|
|
373
|
+
compare(before.schema, p.schema, loc + "/parameters/" + key, "request");
|
|
374
|
+
}
|
|
375
|
+
for (const [key, p] of xp)
|
|
376
|
+
if (!yp.has(key))
|
|
377
|
+
add("parameter-removed", "A previously documented request parameter was removed.", loc + "/parameters/" + key, "medium");
|
|
378
|
+
if (y.requestBody !== undefined) {
|
|
379
|
+
const nr = deref(c, y.requestBody);
|
|
380
|
+
if (x.requestBody === undefined) {
|
|
381
|
+
if (nr.required === true)
|
|
382
|
+
add("request-body-required", "A request body became required.", loc);
|
|
383
|
+
}
|
|
384
|
+
else {
|
|
385
|
+
const br = deref(b, x.requestBody);
|
|
386
|
+
if (nr.required === true && br.required !== true)
|
|
387
|
+
add("request-body-required", "A request body became required.", loc);
|
|
388
|
+
media(br, nr, loc + "/requestBody", "request");
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
else if (x.requestBody !== undefined)
|
|
392
|
+
add("request-body-removed", "A documented request body was removed.", loc);
|
|
393
|
+
if (x.responses === undefined || y.responses === undefined)
|
|
394
|
+
fail("RESPONSES_REQUIRED");
|
|
395
|
+
const xr = record(x.responses), yr = record(y.responses);
|
|
396
|
+
for (const status of Object.keys(xr)) {
|
|
397
|
+
if (!Object.hasOwn(yr, status)) {
|
|
398
|
+
add("response-removed", "A documented response status was removed.", loc + "/responses/" + status);
|
|
399
|
+
continue;
|
|
400
|
+
}
|
|
401
|
+
media(deref(b, xr[status]), deref(c, yr[status]), loc + "/responses/" + status, "response");
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
const bs = record(b.components
|
|
406
|
+
?.securitySchemes ?? {}), cs = record(c.components
|
|
407
|
+
?.securitySchemes ?? {});
|
|
408
|
+
if (stable(bs) !== stable(cs))
|
|
409
|
+
add("authentication-scheme-changed", "Security scheme definitions changed.", "#/components/securitySchemes");
|
|
410
|
+
}
|
|
411
|
+
return {
|
|
412
|
+
kind,
|
|
413
|
+
compatible: !exceeds(findings, threshold),
|
|
414
|
+
findings,
|
|
415
|
+
limitations: [...limitations],
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
export const readContract = async (root, path) => parseJson(await readBounded(root, path));
|
|
419
|
+
export const renderMarkdown = (report) => markdown("API compatibility report", report.findings, report.limitations);
|
package/docs/API.md
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# API and compatibility policy
|
|
2
|
+
|
|
3
|
+
`compareContracts(baseline, candidate, options?)` accepts JSON objects and returns `{kind, compatible, findings, limitations}`. `kind` is `openapi` or `json-schema`; omitted kind detects OpenAPI from the baseline. Options accept `failOn` (default `high`) and a `severity` map keyed by finding ID. Severities in ascending order are `info`, `low`, `medium`, `high`, `critical`. Findings contain `id`, `severity`, fixed explanatory `message`, and a sanitized location `path`.
|
|
4
|
+
|
|
5
|
+
`readContract(root, relativePath)` reads bounded UTF-8 JSON below an explicit root. `renderMarkdown(report)` renders a report. `SafeInputError` has a fixed safe `code` and message. Limits: 4 MiB per file, 100,000 JSON nodes, depth 48, schema depth 32, and reference chain length 24.
|
|
6
|
+
|
|
7
|
+
Finding IDs: endpoint-removed, method-removed, required-parameter-added, parameter-removed, request-body-required, request-body-removed, response-removed, media-type-removed, schema-presence-changed, authentication-changed, authentication-scheme-changed, type-incompatible, required-field-changed, enum-incompatible, constraint-incompatible, constraint-changed, property-removed, additional-properties-restricted, additional-properties-changed and items-changed. Most default to high; removed request parameters default to medium.
|
|
8
|
+
|
|
9
|
+
Requests use input contravariance: newly required fields, narrower types/enums and tighter bounds may reject existing callers. Responses use output covariance: broader types/enums, weaker guaranteed fields and loosened bounds may violate consumer expectations. Integer is a subset of number. Authentication changes and format/pattern changes conservatively require review. Documentation-only changes are ignored.
|
|
10
|
+
|
|
11
|
+
This 0.1.0 API is the initial public candidate. Breaking changes require a documented version change. `compatible` means below the configured threshold for completed supported static checks, never proof of runtime compatibility.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Supported scope and limitations
|
|
2
|
+
|
|
3
|
+
Only JSON OpenAPI 3.0/3.1 and JSON Schema object schemas are accepted. YAML is not parsed. References must be local JSON Pointer fragments in the same document. Remote references, other local files, cycles, `$ref` siblings other than descriptions, boolean schemas, composition (`allOf`, `oneOf`, `anyOf`, `not`), conditionals, tuple schemas and unrecognized schema keywords are rejected, returning exit 2.
|
|
4
|
+
|
|
5
|
+
Supported schema checks include type/nullable, enum/const, required, properties, homogeneous items, additionalProperties, numeric bounds, length/item bounds, pattern and format changes, and readOnly/writeOnly changes. The checker does not evaluate regular expressions or validate instance data. OpenAPI parameter schemas are required; content-based parameters are rejected. OpenAPI 3.0 boolean exclusive bounds are outside this subset.
|
|
6
|
+
|
|
7
|
+
OpenAPI response-property removal is conservatively flagged even if the property was optional. Response headers, links, callbacks, webhooks, discriminator behavior, pagination semantics, authorization semantics, implementation behavior and external services are not verified. Security scheme definition changes are reported even if unused. Reports compare supported declared contracts, and do not certify full OpenAPI validity.
|
|
8
|
+
|
|
9
|
+
Source errors never quote input. Findings retain sanitized specification locations; avoid placing secrets in contract names or paths. No network requests, embedded code execution or artifact retrieval occurs. Hosted capabilities are blocked pending an approved public contract and authorized live acceptance tests.
|
package/docs/RELEASE.md
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Release process
|
|
2
|
+
|
|
3
|
+
Version 0.1.1 is distributed under [Apache-2.0](../LICENSE). [GitHub Releases](https://github.com/Zof-AI/zof-api-contract-check/releases) provides versioned source and reviewed build artifacts. Install `@zof-ai/api-contract-check@0.1.1` using the [npm quickstart](../README.md#install-from-npm). Refer to the release assets, commit SHA and recorded validation for the exact version.
|
|
4
|
+
|
|
5
|
+
Start with the [source installation](../README.md#install-from-github-source). Run `node scripts/bootstrap.mjs --allow-network`, `npm ci --ignore-scripts --prefer-offline`, `npm run build`, `npm test`, and `npm pack --ignore-scripts`. The bootstrap checks the exact dependency release SHA256 and byte sizes. Do not replace failed checks with an unverified dependency or a `file:` reference in published metadata.
|
|
6
|
+
|
|
7
|
+
Before releasing a new version, inspect source and Git history, dependency licenses, build outputs and every package entry. Resolve security findings; run independent negative and installation tests. Record artifact SHA256 and release commits. Maintain branch protection, private vulnerability reporting, least-privilege workflows and a protected release environment. The manual release-candidate workflow builds artifacts and does not publish automatically. Material release changes require a new review.
|
|
8
|
+
|
|
9
|
+
Publish the reviewed package tarball as a GitHub Release asset. npm registry publication requires independently verified `@zof-ai` scope ownership and an authenticated publisher. Verify the registry version and integrity, then run the README commands in a fresh project before recording registry acceptance.
|
|
10
|
+
|
|
11
|
+
After publication, retrieve assets anonymously, verify their checksums, install in a clean project, and execute documented examples. Check every intended distribution channel separately. Missing or unrun acceptance is not a pass. Hosted Zof integration remains unavailable until an approved public contract and real service acceptance exist.
|
|
12
|
+
|
|
13
|
+
CI targets Node 22/24 on Linux/macOS. File operations require Python 3.10 or newer and fail closed on Windows; in-memory JSON operations remain portable. Only completed jobs establish platform verification. Docker acceptance runs on Linux; editor acceptance exercises a real VS Code host.
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
# Validation and release evidence
|
|
2
|
+
|
|
3
|
+
Run `npm run check` for strict TypeScript compilation plus named unit, integration, end-to-end and security tests. Run `npm run test:package` to build a tarball, install it into a temporary clean project with scripts disabled, load the public package exports and invoke the installed CLI. Tests use synthetic local fixtures, never customer data or hosted endpoints.
|
|
4
|
+
|
|
5
|
+
CI targets Node 22/24 on Linux/macOS with Python 3.10 or newer; only completed jobs verify those platforms. Windows file operations fail closed. Local candidate validation is separate from anonymous public installation, registry ownership, protected release settings and independent security/QA review. Missing evidence remains unverified.
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"openapi": "3.1.0",
|
|
3
|
+
"info": {
|
|
4
|
+
"title": "Synthetic catalog",
|
|
5
|
+
"version": "1.0"
|
|
6
|
+
},
|
|
7
|
+
"paths": {
|
|
8
|
+
"/items": {
|
|
9
|
+
"get": {
|
|
10
|
+
"responses": {
|
|
11
|
+
"200": {
|
|
12
|
+
"description": "Catalog item",
|
|
13
|
+
"content": {
|
|
14
|
+
"application/json": {
|
|
15
|
+
"schema": {
|
|
16
|
+
"type": "object",
|
|
17
|
+
"required": [
|
|
18
|
+
"id"
|
|
19
|
+
],
|
|
20
|
+
"properties": {
|
|
21
|
+
"id": {
|
|
22
|
+
"type": "integer"
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
"security": [
|
|
34
|
+
{
|
|
35
|
+
"bearer": []
|
|
36
|
+
}
|
|
37
|
+
],
|
|
38
|
+
"components": {
|
|
39
|
+
"securitySchemes": {
|
|
40
|
+
"bearer": {
|
|
41
|
+
"type": "http",
|
|
42
|
+
"scheme": "bearer"
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"openapi": "3.1.0",
|
|
3
|
+
"info": {
|
|
4
|
+
"title": "Synthetic catalog",
|
|
5
|
+
"version": "1.0"
|
|
6
|
+
},
|
|
7
|
+
"paths": {
|
|
8
|
+
"/items": {
|
|
9
|
+
"get": {
|
|
10
|
+
"responses": {
|
|
11
|
+
"200": {
|
|
12
|
+
"description": "Catalog item",
|
|
13
|
+
"content": {
|
|
14
|
+
"application/json": {
|
|
15
|
+
"schema": {
|
|
16
|
+
"type": "object",
|
|
17
|
+
"required": [
|
|
18
|
+
"id"
|
|
19
|
+
],
|
|
20
|
+
"properties": {
|
|
21
|
+
"id": {
|
|
22
|
+
"type": "integer"
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"openapi": "3.1.0",
|
|
3
|
+
"info": {
|
|
4
|
+
"title": "Synthetic catalog",
|
|
5
|
+
"version": "1.0"
|
|
6
|
+
},
|
|
7
|
+
"paths": {
|
|
8
|
+
"/items": {
|
|
9
|
+
"get": {
|
|
10
|
+
"responses": {
|
|
11
|
+
"200": {
|
|
12
|
+
"description": "Catalog item",
|
|
13
|
+
"content": {
|
|
14
|
+
"application/json": {
|
|
15
|
+
"schema": {
|
|
16
|
+
"type": "object",
|
|
17
|
+
"required": [
|
|
18
|
+
"id"
|
|
19
|
+
],
|
|
20
|
+
"properties": {
|
|
21
|
+
"id": {
|
|
22
|
+
"type": "string"
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"openapi": "3.1.0",
|
|
3
|
+
"info": {
|
|
4
|
+
"title": "Synthetic catalog",
|
|
5
|
+
"version": "1.0"
|
|
6
|
+
},
|
|
7
|
+
"paths": {
|
|
8
|
+
"/items": {
|
|
9
|
+
"get": {
|
|
10
|
+
"responses": {
|
|
11
|
+
"200": {
|
|
12
|
+
"description": "Catalog item",
|
|
13
|
+
"content": {
|
|
14
|
+
"application/json": {
|
|
15
|
+
"schema": {
|
|
16
|
+
"type": "object",
|
|
17
|
+
"required": [
|
|
18
|
+
"id"
|
|
19
|
+
],
|
|
20
|
+
"properties": {
|
|
21
|
+
"id": {
|
|
22
|
+
"type": "integer"
|
|
23
|
+
},
|
|
24
|
+
"label": {
|
|
25
|
+
"type": "string"
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,62 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zof-ai/api-contract-check",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Bounded offline OpenAPI and JSON Schema compatibility analysis",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"bin": {
|
|
15
|
+
"zof-api-contract-check": "./dist/cli.js"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist",
|
|
19
|
+
"README.md",
|
|
20
|
+
"LICENSE",
|
|
21
|
+
"CHANGELOG.md",
|
|
22
|
+
"docs",
|
|
23
|
+
"examples",
|
|
24
|
+
"SECURITY.md",
|
|
25
|
+
"CONTRIBUTING.md",
|
|
26
|
+
"CODE_OF_CONDUCT.md"
|
|
27
|
+
],
|
|
28
|
+
"engines": {
|
|
29
|
+
"node": ">=22"
|
|
30
|
+
},
|
|
31
|
+
"license": "Apache-2.0",
|
|
32
|
+
"repository": {
|
|
33
|
+
"type": "git",
|
|
34
|
+
"url": "https://github.com/Zof-AI/zof-api-contract-check.git"
|
|
35
|
+
},
|
|
36
|
+
"homepage": "https://github.com/Zof-AI/zof-api-contract-check#readme",
|
|
37
|
+
"bugs": {
|
|
38
|
+
"url": "https://github.com/Zof-AI/zof-api-contract-check/issues"
|
|
39
|
+
},
|
|
40
|
+
"keywords": [
|
|
41
|
+
"verification",
|
|
42
|
+
"testing",
|
|
43
|
+
"zof"
|
|
44
|
+
],
|
|
45
|
+
"publishConfig": {
|
|
46
|
+
"access": "public",
|
|
47
|
+
"provenance": true
|
|
48
|
+
},
|
|
49
|
+
"scripts": {
|
|
50
|
+
"build": "tsc -p tsconfig.json",
|
|
51
|
+
"test": "node --test tests/*.test.mjs",
|
|
52
|
+
"check": "npm run build && npm test",
|
|
53
|
+
"test:package": "node tests/package.mjs"
|
|
54
|
+
},
|
|
55
|
+
"devDependencies": {
|
|
56
|
+
"typescript": "5.9.3",
|
|
57
|
+
"@types/node": "22.19.11"
|
|
58
|
+
},
|
|
59
|
+
"dependencies": {
|
|
60
|
+
"@zof-ai/evidence-spec": "0.1.1"
|
|
61
|
+
}
|
|
62
|
+
}
|