@sentinel-atl/registry 0.3.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/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@sentinel-atl/registry",
3
+ "version": "0.3.0",
4
+ "description": "Trust Registry API — publish, query, and verify Sentinel Trust Certificates",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "bin": {
9
+ "sentinel-registry": "dist/cli.js"
10
+ },
11
+ "exports": {
12
+ ".": {
13
+ "types": "./dist/index.d.ts",
14
+ "import": "./dist/index.js"
15
+ }
16
+ },
17
+ "scripts": {
18
+ "build": "tsc",
19
+ "test": "vitest run",
20
+ "lint": "tsc --noEmit",
21
+ "clean": "rm -rf dist"
22
+ },
23
+ "dependencies": {
24
+ "@sentinel-atl/scanner": "*",
25
+ "@sentinel-atl/hardening": "*",
26
+ "@sentinel-atl/store": "*"
27
+ },
28
+ "devDependencies": {
29
+ "vitest": "^3.2.4",
30
+ "typescript": "^5.7.0",
31
+ "@types/node": "^20.0.0"
32
+ },
33
+ "license": "Apache-2.0",
34
+ "repository": {
35
+ "type": "git",
36
+ "url": "https://github.com/sentinel-atl/project-sentinel.git",
37
+ "directory": "packages/registry"
38
+ },
39
+ "keywords": [
40
+ "sentinel",
41
+ "trust",
42
+ "registry",
43
+ "mcp",
44
+ "certificate",
45
+ "badge"
46
+ ]
47
+ }
@@ -0,0 +1,304 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import { CertificateStore, RegistryServer, gradeBadge, scoreBadge, verifiedBadge, notFoundBadge } from '../index.js';
3
+ import { InMemoryKeyProvider, publicKeyToDid } from '@sentinel-atl/core';
4
+ import { scan, issueSTC, type SentinelTrustCertificate } from '@sentinel-atl/scanner';
5
+ import { mkdtemp, writeFile, mkdir } from 'node:fs/promises';
6
+ import { tmpdir } from 'node:os';
7
+ import { join } from 'node:path';
8
+
9
+ // ─── Helpers ──────────────────────────────────────────────────────────
10
+
11
+ async function createTempPackage(files: Record<string, string>): Promise<string> {
12
+ const dir = await mkdtemp(join(tmpdir(), 'sentinel-reg-'));
13
+ for (const [name, content] of Object.entries(files)) {
14
+ const dirPath = join(dir, name.split('/').slice(0, -1).join('/'));
15
+ if (name.includes('/')) await mkdir(dirPath, { recursive: true });
16
+ await writeFile(join(dir, name), content, 'utf-8');
17
+ }
18
+ return dir;
19
+ }
20
+
21
+ let kp: InMemoryKeyProvider;
22
+ let identity: { keyId: string; did: string };
23
+
24
+ async function makeTestSTC(pkgName: string, version = '1.0.0'): Promise<SentinelTrustCertificate> {
25
+ const dir = await createTempPackage({
26
+ 'package.json': JSON.stringify({ name: pkgName, version }),
27
+ 'src/index.ts': 'export function handler() { return "ok"; }\n',
28
+ });
29
+ const report = await scan({ packagePath: dir, skipDependencies: true });
30
+ return issueSTC(kp, {
31
+ scanReport: report,
32
+ codeHash: `hash-${pkgName}-${version}`,
33
+ issuerDid: identity.did,
34
+ issuerKeyId: identity.keyId,
35
+ });
36
+ }
37
+
38
+ beforeEach(async () => {
39
+ kp = new InMemoryKeyProvider();
40
+ await kp.generate('test-key');
41
+ const pubKey = await kp.getPublicKey('test-key');
42
+ identity = { keyId: 'test-key', did: publicKeyToDid(pubKey) };
43
+ });
44
+
45
+ // ─── Store Tests ──────────────────────────────────────────────────────
46
+
47
+ describe('CertificateStore', () => {
48
+ it('registers and retrieves a certificate', async () => {
49
+ const store = new CertificateStore();
50
+ const stc = await makeTestSTC('@test/pkg-a');
51
+ const entry = await store.register(stc);
52
+
53
+ expect(entry.id).toBe(stc.id);
54
+ expect(entry.verified).toBe(true);
55
+ expect(entry.packageName).toBe('@test/pkg-a');
56
+
57
+ const retrieved = store.get(stc.id);
58
+ expect(retrieved).toBeDefined();
59
+ expect(retrieved!.trustScore).toBe(stc.trustScore.overall);
60
+ });
61
+
62
+ it('gets latest for package', async () => {
63
+ const store = new CertificateStore();
64
+ const stc1 = await makeTestSTC('@test/pkg-b', '1.0.0');
65
+ const stc2 = await makeTestSTC('@test/pkg-b', '2.0.0');
66
+
67
+ await store.register(stc1);
68
+ await store.register(stc2);
69
+
70
+ const latest = store.getLatestForPackage('@test/pkg-b');
71
+ expect(latest).toBeDefined();
72
+ expect(latest!.packageVersion).toBe('2.0.0');
73
+ });
74
+
75
+ it('queries with filters', async () => {
76
+ const store = new CertificateStore();
77
+ await store.register(await makeTestSTC('@test/a'));
78
+ await store.register(await makeTestSTC('@test/b'));
79
+
80
+ const all = store.query({});
81
+ expect(all.length).toBe(2);
82
+
83
+ const filtered = store.query({ packageName: '@test/a' });
84
+ expect(filtered.length).toBe(1);
85
+ expect(filtered[0].packageName).toBe('@test/a');
86
+ });
87
+
88
+ it('removes a certificate', async () => {
89
+ const store = new CertificateStore();
90
+ const stc = await makeTestSTC('@test/removable');
91
+ await store.register(stc);
92
+
93
+ expect(store.count()).toBe(1);
94
+ const removed = await store.remove(stc.id);
95
+ expect(removed).toBe(true);
96
+ expect(store.count()).toBe(0);
97
+ expect(store.get(stc.id)).toBeUndefined();
98
+ });
99
+
100
+ it('provides stats', async () => {
101
+ const store = new CertificateStore();
102
+ await store.register(await makeTestSTC('@test/stats-a'));
103
+ await store.register(await makeTestSTC('@test/stats-b'));
104
+
105
+ const stats = store.getStats();
106
+ expect(stats.totalCertificates).toBe(2);
107
+ expect(stats.verifiedCertificates).toBe(2);
108
+ expect(stats.uniquePackages).toBe(2);
109
+ expect(stats.averageScore).toBeGreaterThan(0);
110
+ });
111
+ });
112
+
113
+ // ─── Badge Tests ──────────────────────────────────────────────────────
114
+
115
+ describe('Badge SVG', () => {
116
+ it('generates grade badge', () => {
117
+ const svg = gradeBadge('A');
118
+ expect(svg).toContain('<svg');
119
+ expect(svg).toContain('Grade A');
120
+ expect(svg).toContain('#4c1'); // green
121
+ });
122
+
123
+ it('generates score badge', () => {
124
+ const svg = scoreBadge(87);
125
+ expect(svg).toContain('87/100');
126
+ });
127
+
128
+ it('generates verified badge', () => {
129
+ const svg = verifiedBadge(true);
130
+ expect(svg).toContain('Verified');
131
+ });
132
+
133
+ it('generates not-found badge', () => {
134
+ const svg = notFoundBadge();
135
+ expect(svg).toContain('Not Found');
136
+ expect(svg).toContain('#9f9f9f'); // gray
137
+ });
138
+
139
+ it('supports flat-square style', () => {
140
+ const svg = gradeBadge('B', 'flat-square');
141
+ expect(svg).toContain('rx="0"'); // no border radius
142
+ });
143
+ });
144
+
145
+ // ─── Server Tests ─────────────────────────────────────────────────────
146
+
147
+ let nextPort = 18200;
148
+ function getPort() { return nextPort++; }
149
+
150
+ describe('RegistryServer', () => {
151
+ let server: RegistryServer | null = null;
152
+
153
+ afterEach(async () => {
154
+ if (server) { await server.stop(); server = null; }
155
+ });
156
+
157
+ it('starts and responds to /health', async () => {
158
+ const port = getPort();
159
+ server = new RegistryServer({ port });
160
+ await server.start();
161
+
162
+ const res = await fetch(`http://localhost:${port}/health`);
163
+ expect(res.status).toBe(200);
164
+ const body = await res.json() as any;
165
+ expect(body.status).toBe('ok');
166
+ });
167
+
168
+ it('registers and retrieves a certificate via API', async () => {
169
+ const port = getPort();
170
+ server = new RegistryServer({ port });
171
+ await server.start();
172
+
173
+ const stc = await makeTestSTC('@test/api-pkg');
174
+
175
+ // Register
176
+ const postRes = await fetch(`http://localhost:${port}/api/v1/certificates`, {
177
+ method: 'POST',
178
+ headers: { 'Content-Type': 'application/json' },
179
+ body: JSON.stringify(stc),
180
+ });
181
+ expect(postRes.status).toBe(201);
182
+ const postBody = await postRes.json() as any;
183
+ expect(postBody.id).toBe(stc.id);
184
+ expect(postBody.verified).toBe(true);
185
+
186
+ // Get by ID
187
+ const getRes = await fetch(`http://localhost:${port}/api/v1/certificates/${encodeURIComponent(stc.id)}`);
188
+ expect(getRes.status).toBe(200);
189
+ const getBody = await getRes.json() as any;
190
+ expect(getBody.packageName).toBe('@test/api-pkg');
191
+ });
192
+
193
+ it('rejects duplicate registration', async () => {
194
+ const port = getPort();
195
+ server = new RegistryServer({ port });
196
+ await server.start();
197
+
198
+ const stc = await makeTestSTC('@test/dup-pkg');
199
+
200
+ await fetch(`http://localhost:${port}/api/v1/certificates`, {
201
+ method: 'POST',
202
+ headers: { 'Content-Type': 'application/json' },
203
+ body: JSON.stringify(stc),
204
+ });
205
+
206
+ const res2 = await fetch(`http://localhost:${port}/api/v1/certificates`, {
207
+ method: 'POST',
208
+ headers: { 'Content-Type': 'application/json' },
209
+ body: JSON.stringify(stc),
210
+ });
211
+ expect(res2.status).toBe(409);
212
+ });
213
+
214
+ it('serves grade badge for a package', async () => {
215
+ const port = getPort();
216
+ server = new RegistryServer({ port });
217
+ await server.start();
218
+
219
+ const stc = await makeTestSTC('@test/badge-pkg');
220
+ await fetch(`http://localhost:${port}/api/v1/certificates`, {
221
+ method: 'POST',
222
+ headers: { 'Content-Type': 'application/json' },
223
+ body: JSON.stringify(stc),
224
+ });
225
+
226
+ const badgeRes = await fetch(`http://localhost:${port}/api/v1/packages/${encodeURIComponent('@test/badge-pkg')}/badge`);
227
+ expect(badgeRes.status).toBe(200);
228
+ expect(badgeRes.headers.get('content-type')).toBe('image/svg+xml');
229
+ const svg = await badgeRes.text();
230
+ expect(svg).toContain('<svg');
231
+ expect(svg).toContain('Grade');
232
+ });
233
+
234
+ it('serves not-found badge for unknown package', async () => {
235
+ const port = getPort();
236
+ server = new RegistryServer({ port });
237
+ await server.start();
238
+
239
+ const badgeRes = await fetch(`http://localhost:${port}/api/v1/packages/${encodeURIComponent('@test/unknown')}/badge`);
240
+ expect(badgeRes.status).toBe(200);
241
+ const svg = await badgeRes.text();
242
+ expect(svg).toContain('Not Found');
243
+ });
244
+
245
+ it('queries certificates with filters', async () => {
246
+ const port = getPort();
247
+ server = new RegistryServer({ port });
248
+ await server.start();
249
+
250
+ await fetch(`http://localhost:${port}/api/v1/certificates`, {
251
+ method: 'POST',
252
+ headers: { 'Content-Type': 'application/json' },
253
+ body: JSON.stringify(await makeTestSTC('@test/q1')),
254
+ });
255
+ await fetch(`http://localhost:${port}/api/v1/certificates`, {
256
+ method: 'POST',
257
+ headers: { 'Content-Type': 'application/json' },
258
+ body: JSON.stringify(await makeTestSTC('@test/q2')),
259
+ });
260
+
261
+ const res = await fetch(`http://localhost:${port}/api/v1/certificates?package=@test/q1`);
262
+ const body = await res.json() as any;
263
+ expect(body.count).toBe(1);
264
+ expect(body.certificates[0].packageName).toBe('@test/q1');
265
+ });
266
+
267
+ it('deletes a certificate', async () => {
268
+ const port = getPort();
269
+ server = new RegistryServer({ port });
270
+ await server.start();
271
+
272
+ const stc = await makeTestSTC('@test/del-pkg');
273
+ await fetch(`http://localhost:${port}/api/v1/certificates`, {
274
+ method: 'POST',
275
+ headers: { 'Content-Type': 'application/json' },
276
+ body: JSON.stringify(stc),
277
+ });
278
+
279
+ const delRes = await fetch(`http://localhost:${port}/api/v1/certificates/${encodeURIComponent(stc.id)}`, {
280
+ method: 'DELETE',
281
+ });
282
+ expect(delRes.status).toBe(200);
283
+
284
+ const getRes = await fetch(`http://localhost:${port}/api/v1/certificates/${encodeURIComponent(stc.id)}`);
285
+ expect(getRes.status).toBe(404);
286
+ });
287
+
288
+ it('returns stats', async () => {
289
+ const port = getPort();
290
+ server = new RegistryServer({ port });
291
+ await server.start();
292
+
293
+ await fetch(`http://localhost:${port}/api/v1/certificates`, {
294
+ method: 'POST',
295
+ headers: { 'Content-Type': 'application/json' },
296
+ body: JSON.stringify(await makeTestSTC('@test/stats-pkg')),
297
+ });
298
+
299
+ const res = await fetch(`http://localhost:${port}/api/v1/stats`);
300
+ const stats = await res.json() as any;
301
+ expect(stats.totalCertificates).toBe(1);
302
+ expect(stats.verifiedCertificates).toBe(1);
303
+ });
304
+ });
package/src/badge.ts ADDED
@@ -0,0 +1,139 @@
1
+ /**
2
+ * Badge SVG generator — produces shields.io-style trust badges.
3
+ *
4
+ * Badge formats:
5
+ * - Trust grade badge: "Sentinel | A" (green)
6
+ * - Trust score badge: "Trust Score | 87/100" (green)
7
+ * - Verified badge: "Sentinel Verified | ✓" (green)
8
+ * - Not found badge: "Sentinel | Not Found" (gray)
9
+ */
10
+
11
+ // ─── Types ───────────────────────────────────────────────────────────
12
+
13
+ export type BadgeStyle = 'flat' | 'flat-square';
14
+
15
+ export interface BadgeOptions {
16
+ /** Left label text */
17
+ label?: string;
18
+ /** Right value text */
19
+ value: string;
20
+ /** Color of the right side */
21
+ color: string;
22
+ /** Badge style */
23
+ style?: BadgeStyle;
24
+ }
25
+
26
+ // ─── Color Mapping ───────────────────────────────────────────────────
27
+
28
+ const GRADE_COLORS: Record<string, string> = {
29
+ A: '#4c1', // bright green
30
+ B: '#97ca00', // yellow-green
31
+ C: '#dfb317', // yellow
32
+ D: '#fe7d37', // orange
33
+ F: '#e05d44', // red
34
+ };
35
+
36
+ function scoreColor(score: number): string {
37
+ if (score >= 90) return GRADE_COLORS.A;
38
+ if (score >= 75) return GRADE_COLORS.B;
39
+ if (score >= 60) return GRADE_COLORS.C;
40
+ if (score >= 40) return GRADE_COLORS.D;
41
+ return GRADE_COLORS.F;
42
+ }
43
+
44
+ // ─── SVG Generator ───────────────────────────────────────────────────
45
+
46
+ function escapeXml(str: string): string {
47
+ return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
48
+ }
49
+
50
+ function textWidth(text: string): number {
51
+ // Approximate character widths for Verdana 11px
52
+ return text.length * 6.5 + 10;
53
+ }
54
+
55
+ function renderBadge(options: BadgeOptions): string {
56
+ const label = options.label ?? 'Sentinel';
57
+ const { value, color } = options;
58
+ const isSquare = options.style === 'flat-square';
59
+
60
+ const labelWidth = textWidth(label);
61
+ const valueWidth = textWidth(value);
62
+ const totalWidth = labelWidth + valueWidth;
63
+ const radius = isSquare ? 0 : 3;
64
+
65
+ const escapedLabel = escapeXml(label);
66
+ const escapedValue = escapeXml(value);
67
+
68
+ return `<svg xmlns="http://www.w3.org/2000/svg" width="${totalWidth}" height="20" role="img" aria-label="${escapedLabel}: ${escapedValue}">
69
+ <title>${escapedLabel}: ${escapedValue}</title>
70
+ <linearGradient id="s" x2="0" y2="100%">
71
+ <stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
72
+ <stop offset="1" stop-opacity=".1"/>
73
+ </linearGradient>
74
+ <clipPath id="r">
75
+ <rect width="${totalWidth}" height="20" rx="${radius}" fill="#fff"/>
76
+ </clipPath>
77
+ <g clip-path="url(#r)">
78
+ <rect width="${labelWidth}" height="20" fill="#555"/>
79
+ <rect x="${labelWidth}" width="${valueWidth}" height="20" fill="${color}"/>
80
+ <rect width="${totalWidth}" height="20" fill="url(#s)"/>
81
+ </g>
82
+ <g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="11">
83
+ <text aria-hidden="true" x="${labelWidth / 2}" y="15" fill="#010101" fill-opacity=".3">${escapedLabel}</text>
84
+ <text x="${labelWidth / 2}" y="14">${escapedLabel}</text>
85
+ <text aria-hidden="true" x="${labelWidth + valueWidth / 2}" y="15" fill="#010101" fill-opacity=".3">${escapedValue}</text>
86
+ <text x="${labelWidth + valueWidth / 2}" y="14">${escapedValue}</text>
87
+ </g>
88
+ </svg>`;
89
+ }
90
+
91
+ // ─── Public API ──────────────────────────────────────────────────────
92
+
93
+ /**
94
+ * Generate a trust grade badge SVG.
95
+ */
96
+ export function gradeBadge(grade: string, style?: BadgeStyle): string {
97
+ return renderBadge({
98
+ label: 'Sentinel',
99
+ value: `Grade ${grade}`,
100
+ color: GRADE_COLORS[grade] ?? '#9f9f9f',
101
+ style,
102
+ });
103
+ }
104
+
105
+ /**
106
+ * Generate a trust score badge SVG.
107
+ */
108
+ export function scoreBadge(score: number, style?: BadgeStyle): string {
109
+ return renderBadge({
110
+ label: 'Trust Score',
111
+ value: `${score}/100`,
112
+ color: scoreColor(score),
113
+ style,
114
+ });
115
+ }
116
+
117
+ /**
118
+ * Generate a "Sentinel Verified" badge SVG.
119
+ */
120
+ export function verifiedBadge(verified: boolean, style?: BadgeStyle): string {
121
+ return renderBadge({
122
+ label: 'Sentinel',
123
+ value: verified ? 'Verified ✓' : 'Unverified',
124
+ color: verified ? '#4c1' : '#9f9f9f',
125
+ style,
126
+ });
127
+ }
128
+
129
+ /**
130
+ * Generate a "not found" badge SVG.
131
+ */
132
+ export function notFoundBadge(style?: BadgeStyle): string {
133
+ return renderBadge({
134
+ label: 'Sentinel',
135
+ value: 'Not Found',
136
+ color: '#9f9f9f',
137
+ style,
138
+ });
139
+ }
package/src/cli.ts ADDED
@@ -0,0 +1,74 @@
1
+ #!/usr/bin/env node
2
+
3
+ process.on('unhandledRejection', (err) => {
4
+ console.error('Unhandled rejection:', err);
5
+ process.exit(1);
6
+ });
7
+ process.on('uncaughtException', (err) => {
8
+ console.error('Uncaught exception:', err);
9
+ process.exit(1);
10
+ });
11
+
12
+ /**
13
+ * sentinel-registry — CLI for running the Trust Registry API server.
14
+ *
15
+ * Usage:
16
+ * sentinel-registry Start on default port 3200
17
+ * sentinel-registry --port 8080 Start on custom port
18
+ */
19
+
20
+ import { RegistryServer } from './server.js';
21
+ import { authConfigFromEnv, corsConfigFromEnv, tlsConfigFromEnv } from '@sentinel-atl/hardening';
22
+
23
+ const args = process.argv.slice(2);
24
+
25
+ if (args.includes('--help') || args.includes('-h')) {
26
+ console.log('🗂️ Sentinel Trust Registry');
27
+ console.log();
28
+ console.log('Usage:');
29
+ console.log(' sentinel-registry Start on port 3200');
30
+ console.log(' sentinel-registry --port <port> Start on custom port');
31
+ console.log();
32
+ console.log('Endpoints:');
33
+ console.log(' POST /api/v1/certificates Register an STC');
34
+ console.log(' GET /api/v1/certificates/:id Get by ID');
35
+ console.log(' GET /api/v1/certificates Query certificates');
36
+ console.log(' DELETE /api/v1/certificates/:id Remove');
37
+ console.log(' GET /api/v1/packages/:name Latest cert for package');
38
+ console.log(' GET /api/v1/packages/:name/badge SVG trust badge');
39
+ console.log(' GET /api/v1/stats Registry stats');
40
+ console.log(' GET /health Health check');
41
+ process.exit(0);
42
+ }
43
+
44
+ const portIdx = args.indexOf('--port');
45
+ const port = portIdx !== -1 ? parseInt(args[portIdx + 1]) : 3200;
46
+
47
+ const server = new RegistryServer({
48
+ port,
49
+ auth: authConfigFromEnv(),
50
+ cors: corsConfigFromEnv(),
51
+ tls: tlsConfigFromEnv(),
52
+ });
53
+
54
+ console.log('🗂️ Sentinel Trust Registry');
55
+ const { port: actualPort } = await server.start();
56
+ const proto = server.isTLS() ? 'https' : 'http';
57
+ console.log(` Listening on ${proto}://localhost:${actualPort}`);
58
+ console.log();
59
+ console.log(' Endpoints:');
60
+ console.log(' POST /api/v1/certificates');
61
+ console.log(' GET /api/v1/certificates/:id');
62
+ console.log(' GET /api/v1/packages/:name');
63
+ console.log(' GET /api/v1/packages/:name/badge');
64
+ console.log(' GET /api/v1/stats');
65
+ console.log(' GET /health');
66
+ console.log();
67
+
68
+ const shutdown = async () => {
69
+ console.log('\n Shutting down...');
70
+ await server.stop();
71
+ process.exit(0);
72
+ };
73
+ process.on('SIGINT', shutdown);
74
+ process.on('SIGTERM', shutdown);
package/src/index.ts ADDED
@@ -0,0 +1,31 @@
1
+ /**
2
+ * @sentinel-atl/registry — Trust Registry API
3
+ *
4
+ * Publish, query, and verify Sentinel Trust Certificates.
5
+ * Serves SVG trust badges for READMEs and dashboards.
6
+ *
7
+ * Usage:
8
+ * sentinel-registry --port 3200
9
+ */
10
+
11
+ export {
12
+ CertificateStore,
13
+ type CertificateStoreOptions,
14
+ type RegistryEntry,
15
+ type RegistryQuery,
16
+ type RegistryStats,
17
+ } from './store.js';
18
+
19
+ export {
20
+ RegistryServer,
21
+ type RegistryServerOptions,
22
+ } from './server.js';
23
+
24
+ export {
25
+ gradeBadge,
26
+ scoreBadge,
27
+ verifiedBadge,
28
+ notFoundBadge,
29
+ type BadgeStyle,
30
+ type BadgeOptions,
31
+ } from './badge.js';