@readystack/cra-annex-ii-docs-lint 1.0.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/LICENSE.txt ADDED
@@ -0,0 +1,16 @@
1
+ CRA Annex II User Docs Lint — Licence
2
+
3
+ Free scope
4
+ Lints the documentation file you have open and lists every Annex II item that is missing, vague or already expired, with line numbers. is free for any use, personal or commercial, for as long as you keep the
5
+ extension installed, and the online check at the product page is free without registration.
6
+
7
+ Paid scope
8
+ Scans every documentation file in the workspace at once and exports a dated Annex II evidence report (Markdown + CSV) you keep with your technical file. is the paid part. It asks for a licence key issued at purchase (one seat, one key;
9
+ 7-day full refund, no questions). The key is checked against the payment provider and
10
+ cached locally for 30 days so it keeps working offline.
11
+
12
+ Redistribution
13
+ You may not resell, re-host or bundle this extension or its rule set. The rule set and the
14
+ engine are provided as-is; check results are advisory and do not constitute legal advice.
15
+
16
+ (c) ReadyStack — https://getreadystack.com
package/README.md ADDED
@@ -0,0 +1,54 @@
1
+ # CRA Annex II User Docs Lint
2
+
3
+ ![CRA Annex II User Docs Lint](https://getreadystack.com/img/promo/sku79782_result_card.jpg)
4
+
5
+ 19 rules over the user documentation that ships with your product: support-period end date, vulnerability reporting contact, EU declaration of conformity address, secure decommissioning.
6
+
7
+ ## Install
8
+
9
+ ```
10
+ npx @readystack/cra-annex-ii-docs-lint file
11
+ ```
12
+
13
+ Node 18+. The same 19 rules as the VS Code extension, from a terminal or CI.
14
+
15
+ ## Free
16
+
17
+ - Lints the documentation file you have open with all 19 rules and puts every missing, vague or expired Annex II item on its line - 15 findings (11 errors, 4 warnings) on the bundled dirty fixture, 0 on the clean one. No key, no upload, finished on its own.
18
+ - `--rules` lists every rule
19
+
20
+ ## With a licence ($29 once)
21
+
22
+ - Scope and ownership: one command sweeps every documentation file in the workspace and exports a dated Annex II evidence report (Markdown + CSV) you keep with your technical file. $29 once, one licence key per person or team seat, 7-day full refund.
23
+
24
+ ```
25
+ @readystack/cra-annex-ii-docs-lint --dir ./templates --report html --out report.html
26
+ ```
27
+
28
+ A compliance consultant reviewing one technical file bills about EUR 150 per hour.
29
+
30
+ ## Use from an AI agent (MCP)
31
+
32
+ Claude Code · Cursor · Windsurf · any MCP client - add to your MCP config:
33
+
34
+ ```json
35
+ { "mcpServers": { "cra-annex-ii-docs-lint": { "command": "npx", "args": ["-y", "@readystack/cra-annex-ii-docs-lint", "--mcp"] } } }
36
+ ```
37
+
38
+ Tools: `check_text` and `check_file` (free) · `check_dir` (licence; the full sweep is free for 7 days). The agent gets every finding with the line number.
39
+
40
+ ## Use in CI
41
+
42
+ ```yaml
43
+ - name: CRA Annex II User Docs Lint
44
+ run: npx -y @readystack/cra-annex-ii-docs-lint --dir . --ci
45
+ ```
46
+
47
+ (container: `docker run --rm -v "$PWD:/work" getreadystack/cra-annex-ii-docs-lint --dir /work --ci`)
48
+
49
+ Try the full run free for 7 days — no key needed. Then one licence, 7-day refund, no questions. Set `READYSTACK_LICENSE=<key>` or run `--license <key>` once.
50
+
51
+ [Get a licence](https://buy.polar.sh/polar_cl_mS0E5k9PqMWkUFlrIn7VEYPbBJmAX5ybAjvyM2dC7Zw)
52
+
53
+
54
+ <!-- cra annex ii docs lint -->
package/cli.js ADDED
@@ -0,0 +1,120 @@
1
+ #!/usr/bin/env node
2
+ // cli.js — the same rules as the VS Code extension, run from a terminal or a container. Generated by adapters.py.
3
+ 'use strict';
4
+ const fs = require('fs'), path = require('path');
5
+ const RULES = require('./rules.json');
6
+ const S = require('./strings.json');
7
+ const lic = require('./license.js');
8
+ const ENGINE = require('./engine.js'); // s144 — scaffold products: the same engine the VS Code extension runs
9
+ const RULE_LIST = Array.isArray(ENGINE.RULES) ? ENGINE.RULES : (Array.isArray(RULES) ? RULES : []);
10
+ const RULE_N = ENGINE.RULE_COUNT || RULE_LIST.length;
11
+ function scan(text, file) {
12
+ const r = ENGINE.engine.check(String(text), { today: new Date().toISOString().slice(0, 10), path: file || '' });
13
+ return (r && r.findings || []).map((f) => ({ line: parseInt(f.line, 10) || 1, msg: String(f.msg || f.message || f.check || ''), fix: f.fix || null, sev: (String(f.sev || 'error').toLowerCase().startsWith('err') ? 'error' : 'warn') }));
14
+ }
15
+ function isText(p) { try { const b = fs.readFileSync(p); if (b.length > 2 * 1024 * 1024) return false; const s = b.subarray(0, 4096); for (const x of s) if (x === 0) return false; return true; } catch (e) { return false; } }
16
+ function walk(dir, exts, out) {
17
+ let ents = []; try { ents = fs.readdirSync(dir, { withFileTypes: true }); } catch (e) { return out; }
18
+ for (const e of ents) {
19
+ if (e.name === 'node_modules' || e.name === '.git' || e.name.startsWith('.')) continue;
20
+ const p = path.join(dir, e.name);
21
+ if (e.isDirectory()) walk(p, exts, out);
22
+ else if ((!exts.length || exts.includes(path.extname(e.name).toLowerCase())) && isText(p)) out.push(p);
23
+ }
24
+ return out;
25
+ }
26
+ function esc(s) { return String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;'); }
27
+ function csvq(s) { return '"' + String(s == null ? '' : s).replace(/"/g, '""') + '"'; }
28
+ function render(rows, fmt) {
29
+ if (fmt === 'json') return JSON.stringify({ tool: S.name, rules: RULE_N, files: rows }, null, 1);
30
+ if (fmt === 'csv') { const o = ['file,line,severity,message,fix']; for (const r of rows) for (const h of r.hits) o.push([csvq(r.file), h.line, h.sev, csvq(h.msg), csvq(h.fix)].join(',')); return o.join('\n') + '\n'; }
31
+ if (fmt === 'html') {
32
+ const n = rows.reduce((a, r) => a + r.hits.length, 0);
33
+ let o = '<!doctype html><meta charset="utf-8"><title>' + esc(S.name) + ' report</title><style>body{font:14px system-ui;margin:24px}table{border-collapse:collapse}td,th{border:1px solid #ddd;padding:4px 8px;font-size:13px}code{font-family:ui-monospace,monospace}</style>';
34
+ o += '<h1>' + esc(S.name) + '</h1><p>' + rows.length + ' files · ' + n + ' findings · ' + RULE_N + ' rules</p><table><tr><th>file</th><th>line</th><th>sev</th><th>message</th><th>fix</th></tr>';
35
+ for (const r of rows) for (const h of r.hits) o += '<tr><td><code>' + esc(r.file) + '</code></td><td>' + h.line + '</td><td>' + esc(h.sev) + '</td><td>' + esc(h.msg) + '</td><td>' + esc(h.fix || '') + '</td></tr>';
36
+ return o + '</table>';
37
+ }
38
+ let o = ''; for (const r of rows) { o += r.file + '\n'; for (const h of r.hits) o += ' ' + String(h.line).padStart(5) + ' ' + h.sev.padEnd(5) + ' ' + h.msg + (h.fix ? '\n -> ' + h.fix : '') + '\n'; if (!r.hits.length) o += ' (no findings)\n'; }
39
+ return o;
40
+ }
41
+ function trialState() {
42
+ if (process.env.READYSTACK_NO_TRIAL) return { active: false };
43
+ try {
44
+ const p = path.join(path.dirname(lic.storePath()), S.bin + '.trial.json');
45
+ let t = null; try { t = JSON.parse(fs.readFileSync(p, 'utf8')); } catch (e) {}
46
+ if (!t || !t.until) { t = { until: new Date(Date.now() + 7 * 24 * 3600 * 1000).toISOString().slice(0, 10) }; fs.mkdirSync(path.dirname(p), { recursive: true }); fs.writeFileSync(p, JSON.stringify(t)); }
47
+ return { active: new Date(t.until + 'T23:59:59Z').getTime() > Date.now(), until: t.until };
48
+ } catch (e) { return { active: false }; }
49
+ }
50
+ function mcpServe() {
51
+ // s144 — MCP server over stdio (newline-delimited JSON-RPC · no dependencies). Free: check_text · check_file. Licence (7-day trial): check_dir.
52
+ const rl = require('readline').createInterface({ input: process.stdin });
53
+ const send = (o) => process.stdout.write(JSON.stringify(o) + '\n');
54
+ const tools = [
55
+ { name: 'check_text', description: S.name + ' - run all ' + RULE_N + ' checks on a text (free)', inputSchema: { type: 'object', properties: { text: { type: 'string', description: 'file contents' }, path: { type: 'string', description: 'optional file name for context' } }, required: ['text'] } },
56
+ { name: 'check_file', description: S.name + ' - run all checks on one file by path (free)', inputSchema: { type: 'object', properties: { path: { type: 'string' } }, required: ['path'] } },
57
+ { name: 'check_dir', description: S.name + ' - sweep a folder and return every finding (licence; the full run is free for 7 days)', inputSchema: { type: 'object', properties: { dir: { type: 'string' }, ext: { type: 'string', description: 'optional extension filter, e.g. .html' } }, required: ['dir'] } }
58
+ ];
59
+ const result = (id, rows) => send({ jsonrpc: '2.0', id, result: { content: [{ type: 'text', text: render(rows, 'text') }], structuredContent: { tool: S.name, rules: RULE_N, files: rows } } });
60
+ const fail = (id, text) => send({ jsonrpc: '2.0', id, result: { content: [{ type: 'text', text }], isError: true } });
61
+ rl.on('line', async (line) => {
62
+ let m; try { m = JSON.parse(line); } catch (e) { return; }
63
+ const id = m.id, method = m.method;
64
+ if (method === 'initialize') return send({ jsonrpc: '2.0', id, result: { protocolVersion: '2025-06-18', capabilities: { tools: {} }, serverInfo: { name: '@readystack/' + S.bin, version: '1.0.0' } } });
65
+ if (method === 'notifications/initialized' || method === 'ping') { if (id !== undefined) send({ jsonrpc: '2.0', id, result: {} }); return; }
66
+ if (method === 'tools/list') return send({ jsonrpc: '2.0', id, result: { tools } });
67
+ if (method === 'tools/call') {
68
+ const name = (m.params || {}).name, args = (m.params || {}).arguments || {};
69
+ try {
70
+ if (name === 'check_text') return result(id, [{ file: args.path || '(text)', hits: scan(String(args.text || ''), args.path || '') }]);
71
+ if (name === 'check_file') return result(id, [{ file: args.path, hits: scan(fs.readFileSync(args.path, 'utf8'), args.path) }]);
72
+ if (name === 'check_dir') {
73
+ const r = await lic.ensure();
74
+ if (!r.ok && !trialState().active) return fail(id, S.need_key + ' Get a licence ($' + S.price + ', once, 7-day refund): ' + lic.BUY_URL);
75
+ const files = walk(args.dir, args.ext ? [args.ext] : (S.exts || []), []);
76
+ return result(id, files.map((f) => { let t = ''; try { t = fs.readFileSync(f, 'utf8'); } catch (e) { return { file: f, hits: [], error: String(e.message) }; } return { file: f, hits: scan(t, f) }; }));
77
+ }
78
+ return send({ jsonrpc: '2.0', id, error: { code: -32601, message: 'unknown tool ' + name } });
79
+ } catch (e) { return fail(id, String(e && e.message || e)); }
80
+ }
81
+ if (id !== undefined) send({ jsonrpc: '2.0', id, error: { code: -32601, message: 'method not found: ' + method } });
82
+ });
83
+ }
84
+ function help() {
85
+ return [S.name + ' - ' + S.subtitle, '', 'Usage: ' + S.bin + ' <file> [more files] check the files you name (free, every rule)',
86
+ ' ' + S.bin + ' --dir <folder> [--ext .html] scan a whole folder (licence)',
87
+ ' ' + S.bin + ' ... --report csv|json|html [--out file] export a report (licence)',
88
+ ' ' + S.bin + ' ... --ci exit 1 when an error-level finding exists (licence)',
89
+ ' ' + S.bin + ' --license <key> store your licence key (or set READYSTACK_LICENSE)',
90
+ ' ' + S.bin + ' --rules list the ' + RULE_N + ' rules',
91
+ ' ' + S.bin + ' --mcp run as an MCP server (stdio) for Claude Code / Cursor / Windsurf - free checks, folder sweep needs a licence', '',
92
+ 'Free: ' + S.free, 'Licence ($' + S.price + ', once, 7-day refund): ' + S.paid, 'Get a licence: ' + lic.BUY_URL, ''].join('\n');
93
+ }
94
+ (async function main() {
95
+ try { const _feed = await lic.pullFeed(); if (_feed && Array.isArray(_feed.rules)) { if (Array.isArray(ENGINE.RULES)) for (const r of _feed.rules) ENGINE.RULES.push(r); } } catch (e) {} // ★s134 구독 피드 병합 (키 있는 손님만)
96
+ const a = process.argv.slice(2);
97
+ const get = (k) => { const i = a.indexOf(k); return i >= 0 ? a[i + 1] : null; };
98
+ if (a.includes('--mcp')) { mcpServe(); return; }
99
+ if (!a.length || a.includes('--help') || a.includes('-h')) { process.stdout.write(help()); return; }
100
+ if (a.includes('--rules')) { process.stdout.write((RULE_LIST.length ? RULE_LIST.map((r, i) => String(i + 1).padStart(3) + ' [' + (r.sev || 'warn') + '] ' + (r.message || r.msg || r.id || '')) : Array.from({ length: RULE_N }, (_, i) => String(i + 1).padStart(3) + ' [engine] check ' + (i + 1))).join('\n') + '\n'); return; }
101
+ if (a.includes('--license')) { const r = await lic.ensure(get('--license')); process.stdout.write(r.ok ? 'Licence stored: ' + lic.storePath() + '\n' : 'Licence not accepted (' + r.why + '). Get one: ' + lic.BUY_URL + '\n'); process.exit(r.ok ? 0 : 2); }
102
+ const dir = get('--dir'), fmt = get('--report'), out = get('--out'), ci = a.includes('--ci');
103
+ const exts = a.includes('--ext') ? [get('--ext')] : (S.exts || []);
104
+ const paid = !!(dir || fmt || ci);
105
+ if (paid) {
106
+ const r = await lic.ensure();
107
+ if (!r.ok) {
108
+ const t = trialState(); // s144 reverse trial: the full run is free for 7 days from the first paid use, then the key
109
+ if (t.active) process.stderr.write('Trial: the full run is free until ' + t.until + ' — after that $' + S.price + ' once (7-day refund). Get a licence: ' + lic.BUY_URL + '\n');
110
+ else { process.stderr.write(S.need_key + '\n set READYSTACK_LICENSE=<key> or ' + S.bin + ' --license <key>\n Get a licence ($' + S.price + ', once): ' + lic.BUY_URL + '\n'); process.exit(2); }
111
+ }
112
+ }
113
+ const files = dir ? walk(dir, exts, []) : a.filter((x, i) => !x.startsWith('--') && !['--dir', '--report', '--out', '--ext', '--license'].includes(a[i - 1]));
114
+ if (!files.length) { process.stderr.write('No files. ' + S.bin + ' --help\n'); process.exit(2); }
115
+ const rows = files.map((f) => { let t = ''; try { t = fs.readFileSync(f, 'utf8'); } catch (e) { return { file: f, hits: [], error: String(e.message) }; } return { file: f, hits: scan(t) }; });
116
+ const text = render(rows, fmt || 'text');
117
+ if (out) fs.writeFileSync(out, text); else process.stdout.write(text.endsWith('\n') ? text : text + '\n');
118
+ const errors = rows.reduce((n, r) => n + r.hits.filter((h) => h.sev === 'error').length, 0);
119
+ if (ci && errors) process.exit(1);
120
+ })().catch((e) => { process.stderr.write(String(e && e.stack || e) + '\n'); process.exit(3); });
package/engine.js ADDED
@@ -0,0 +1,92 @@
1
+ /* CRA Annex II User Docs Lint - engine. Same file runs in Node (VS Code) and in the browser. */
2
+ (function () {
3
+ var RULES = (typeof module !== 'undefined' && module.exports) ? require('./rules.json') : window.CRADOCS_RULES;
4
+
5
+ var MONTHS = {jan:1, feb:2, mar:3, apr:4, may:5, jun:6, jul:7, aug:8, sep:9, oct:10, nov:11, dec:12};
6
+ var SUPPORT_CTX = /(support period|security update|security support|end of support|supported until|patches until)/i;
7
+ var DATE_ISO = /(\d{4})-(\d{2})-(\d{2})/;
8
+ var DATE_DMY = /(\d{1,2})\s+([A-Za-z]{3,9})\.?\s+(\d{4})/;
9
+ var DATE_MDY = /([A-Za-z]{3,9})\.?\s+(\d{1,2}),?\s+(\d{4})/;
10
+
11
+ function pad(n) { return (n < 10 ? '0' : '') + n; }
12
+ function monthNum(w) { return MONTHS[String(w).slice(0, 3).toLowerCase()] || 0; }
13
+
14
+ function dateIn(line) {
15
+ var m = DATE_ISO.exec(line);
16
+ if (m) { return m[1] + '-' + m[2] + '-' + m[3]; }
17
+ m = DATE_DMY.exec(line);
18
+ if (m && monthNum(m[2])) { return m[3] + '-' + pad(monthNum(m[2])) + '-' + pad(parseInt(m[1], 10)); }
19
+ m = DATE_MDY.exec(line);
20
+ if (m && monthNum(m[1])) { return m[3] + '-' + pad(monthNum(m[1])) + '-' + pad(parseInt(m[2], 10)); }
21
+ return null;
22
+ }
23
+
24
+ function plusYears(iso, n) {
25
+ var p = iso.split('-');
26
+ return (parseInt(p[0], 10) + n) + '-' + p[1] + '-' + p[2];
27
+ }
28
+
29
+ function byCheck(name) {
30
+ for (var i = 0; i < RULES.length; i++) { if (RULES[i].check === name) { return RULES[i]; } }
31
+ return null;
32
+ }
33
+
34
+ function push(out, rule, line, extra) {
35
+ if (!rule) { return; }
36
+ out.push({check: rule.check, sev: rule.sev, msg: extra ? rule.msg + ' ' + extra : rule.msg, line: line || 1});
37
+ }
38
+
39
+ function check(text, opts) {
40
+ text = String(text == null ? '' : text);
41
+ opts = opts || {};
42
+ var today = /^\d{4}-\d{2}-\d{2}$/.test(opts.today || '') ? opts.today : '2026-09-16';
43
+ var lines = text.split(/\r?\n/);
44
+ var findings = [];
45
+ var i, j, k, r, hit, m;
46
+
47
+ for (i = 0; i < RULES.length; i++) {
48
+ r = RULES[i];
49
+ if (r.kind === 'require_any') {
50
+ hit = false;
51
+ for (j = 0; j < r.need.length; j++) { if (new RegExp(r.need[j], 'i').test(text)) { hit = true; break; } }
52
+ if (!hit) { push(findings, r, 1); }
53
+ } else if (r.kind === 'require_all') {
54
+ hit = true;
55
+ for (j = 0; j < r.need.length; j++) { if (!new RegExp(r.need[j], 'i').test(text)) { hit = false; break; } }
56
+ if (!hit) { push(findings, r, 1); }
57
+ } else if (r.kind === 'forbid') {
58
+ for (j = 0; j < lines.length; j++) {
59
+ for (k = 0; k < r.need.length; k++) {
60
+ m = new RegExp(r.need[k], 'i').exec(lines[j]);
61
+ if (m) { push(findings, r, j + 1, '(reads "' + String(m[0]).slice(0, 44) + '")'); break; }
62
+ }
63
+ }
64
+ }
65
+ }
66
+
67
+ var ctxLine = 0, endDate = null;
68
+ for (j = 0; j < lines.length; j++) {
69
+ if (SUPPORT_CTX.test(lines[j])) {
70
+ if (!ctxLine) { ctxLine = j + 1; }
71
+ var d = dateIn(lines[j]);
72
+ if (d) { endDate = d; ctxLine = j + 1; break; }
73
+ }
74
+ }
75
+ if (!endDate) {
76
+ push(findings, byCheck('support_end_date'), ctxLine);
77
+ } else if (endDate < today) {
78
+ push(findings, byCheck('support_end_expired'), ctxLine, '(found ' + endDate + ', today is ' + today + ')');
79
+ } else if (endDate < plusYears(today, 5)) {
80
+ push(findings, byCheck('support_period_short'), ctxLine, '(ends ' + endDate + ')');
81
+ }
82
+
83
+ var errors = 0, warnings = 0;
84
+ for (i = 0; i < findings.length; i++) { if (findings[i].sev === 'error') { errors++; } else { warnings++; } }
85
+ return {findings: findings, ok: findings.length === 0, errors: errors, warnings: warnings,
86
+ rules_checked: RULES.length, support_end_date: endDate, today: today};
87
+ }
88
+
89
+ var api = {engine: {check: check}, RULES: RULES, RULE_COUNT: RULES.length};
90
+ if (typeof module !== 'undefined' && module.exports) { module.exports = api; }
91
+ if (typeof window !== 'undefined') { window.CRADOCS = api; }
92
+ })();
package/license.js ADDED
@@ -0,0 +1,64 @@
1
+ // license.js — Polar licence check for the CLI / container. Generated by adapters.py; do not edit by hand.
2
+ 'use strict';
3
+ const https = require('https'), fs = require('fs'), path = require('path'), os = require('os');
4
+ const ORG_ID = 'a5cdf664-d8e7-4f87-8895-056717aaba17';
5
+ const BENEFIT_ID = 'ff8acc4e-ec44-47e8-8ec3-298424197b31'; // s142 — this product's Polar benefit (s140 law: ask with benefit_id, or one key opens every product)
6
+ const BUY_URL = 'https://buy.polar.sh/polar_cl_mS0E5k9PqMWkUFlrIn7VEYPbBJmAX5ybAjvyM2dC7Zw';
7
+ const SLUG = 'cra-annex-ii-docs-lint';
8
+ const GRACE_MS = 30 * 24 * 3600 * 1000; // after a successful check, 30 days work offline
9
+ const RECHECK_MS = 7 * 24 * 3600 * 1000; // re-ask Polar every 7 days (refunds / cancellations)
10
+ function storePath() { return path.join(process.env.READYSTACK_HOME || path.join(os.homedir(), '.config', 'readystack'), SLUG + '.json'); }
11
+ function load() { try { return JSON.parse(fs.readFileSync(storePath(), 'utf8')); } catch (e) { return {}; } }
12
+ function save(o) { try { fs.mkdirSync(path.dirname(storePath()), { recursive: true }); fs.writeFileSync(storePath(), JSON.stringify(o)); } catch (e) { /* read-only home: still works for this run */ } }
13
+ function validate(key) {
14
+ return new Promise(function (resolve) {
15
+ if (!ORG_ID) return resolve({ ok: false, offline: false });
16
+ const body = JSON.stringify(/^[0-9a-f-]{36}$/.test(BENEFIT_ID) ? { key: key, organization_id: ORG_ID, benefit_id: BENEFIT_ID } : { key: key, organization_id: ORG_ID });
17
+ const req = https.request({ hostname: 'api.polar.sh', path: '/v1/customer-portal/license-keys/validate', method: 'POST', timeout: 8000,
18
+ headers: { 'content-type': 'application/json', 'polar-version': '2026-04', 'content-length': Buffer.byteLength(body) } }, function (res) {
19
+ let buf = ''; res.on('data', function (d) { buf += d; });
20
+ res.on('end', function () {
21
+ if (res.statusCode !== 200) return resolve({ ok: false, offline: false });
22
+ try { const j = JSON.parse(buf); resolve({ ok: j && (j.status === 'granted' || j.valid === true || !!j.id), offline: false }); }
23
+ catch (e) { resolve({ ok: false, offline: false }); }
24
+ });
25
+ });
26
+ req.on('timeout', function () { req.destroy(); resolve({ ok: false, offline: true }); });
27
+ req.on('error', function () { resolve({ ok: false, offline: true }); });
28
+ req.write(body); req.end();
29
+ });
30
+ }
31
+ async function ensure(explicitKey) {
32
+ const st = load();
33
+ const key = explicitKey || process.env.READYSTACK_LICENSE || st.key;
34
+ if (!key) return { ok: false, why: 'no_key' };
35
+ const age = Date.now() - (st.okAt || 0);
36
+ if (!explicitKey && st.key === key && age < RECHECK_MS) return { ok: true, cached: true };
37
+ const r = await validate(String(key).trim());
38
+ if (r.ok) { save({ key: String(key).trim(), okAt: Date.now() }); return { ok: true }; }
39
+ if (r.offline && st.key === key && age < GRACE_MS) return { ok: true, offline: true };
40
+ return { ok: false, why: r.offline ? 'offline' : 'invalid' };
41
+ }
42
+ // ★s134 — 구독 규칙 피드(층3 "바뀌면 업데이트"): 키 있는 손님만 · 7일마다 · 오프라인은 캐시. 워커 GET /api/rules/<slug>?key=
43
+ const FEED_URL = 'https://getreadystack.com/api/rules/';
44
+ function pullFeed() {
45
+ const st = load(); const key = process.env.READYSTACK_LICENSE || st.key; const cached = st.feed || null;
46
+ if (!key) return Promise.resolve(cached);
47
+ if (cached && (Date.now() - (st.feedAt || 0)) < RECHECK_MS) return Promise.resolve(cached);
48
+ return new Promise(function (resolve) {
49
+ let req;
50
+ try {
51
+ req = https.get(FEED_URL + encodeURIComponent(SLUG) + '?key=' + encodeURIComponent(key), { timeout: 8000, headers: { 'user-agent': 'readystack-cli' } }, function (res) {
52
+ let buf = ''; res.on('data', function (d) { buf += d; });
53
+ res.on('end', function () {
54
+ if (res.statusCode !== 200) return resolve(cached);
55
+ try { const j = JSON.parse(buf); if (!j || !Array.isArray(j.rules)) return resolve(cached); save(Object.assign(load(), { feed: j, feedAt: Date.now() })); resolve(j); }
56
+ catch (e) { resolve(cached); }
57
+ });
58
+ });
59
+ } catch (e) { return resolve(cached); }
60
+ req.on('timeout', function () { req.destroy(); resolve(cached); });
61
+ req.on('error', function () { resolve(cached); });
62
+ });
63
+ }
64
+ module.exports = { ensure, BUY_URL, storePath, pullFeed };
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "@readystack/cra-annex-ii-docs-lint",
3
+ "version": "1.0.0",
4
+ "description": "19 rules over the user documentation that ships with your product: support-period end date, vulnerability reporting contact, EU declaration of conformity address, secure decommissioning.",
5
+ "license": "SEE LICENSE IN LICENSE.txt",
6
+ "publishConfig": {
7
+ "access": "public"
8
+ },
9
+ "keywords": [
10
+ "cyber resilience act",
11
+ "cra",
12
+ "compliance",
13
+ "documentation",
14
+ "ce marking",
15
+ "annex ii",
16
+ "markdown"
17
+ ],
18
+ "homepage": "https://getreadystack.com",
19
+ "funding": "https://buy.polar.sh/polar_cl_mS0E5k9PqMWkUFlrIn7VEYPbBJmAX5ybAjvyM2dC7Zw",
20
+ "bin": {
21
+ "cra-annex-ii-docs-lint": "cli.js"
22
+ },
23
+ "engines": {
24
+ "node": ">=18"
25
+ },
26
+ "mcpName": "io.github.jmshinhwa/cra-annex-ii-docs-lint",
27
+ "repository": {
28
+ "type": "git",
29
+ "url": "https://github.com/jmshinhwa/readystack-themes.git",
30
+ "directory": "cra-annex-ii-docs-lint"
31
+ },
32
+ "files": [
33
+ "cli.js",
34
+ "license.js",
35
+ "rules.json",
36
+ "strings.json",
37
+ "README.md",
38
+ "LICENSE.txt",
39
+ "engine.js"
40
+ ]
41
+ }
package/rules.json ADDED
@@ -0,0 +1,189 @@
1
+ [
2
+ {
3
+ "check": "manufacturer_identity",
4
+ "sev": "error",
5
+ "kind": "require_all",
6
+ "need": [
7
+ "\\b(manufacturer|manufactured by)\\b",
8
+ "(address|registered office|postal address)\\s*:"
9
+ ],
10
+ "msg": "Annex II(1): name the manufacturer and give a postal address ('Manufacturer:' plus 'Address:')."
11
+ },
12
+ {
13
+ "check": "manufacturer_email",
14
+ "sev": "error",
15
+ "kind": "require_any",
16
+ "need": [
17
+ "[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}"
18
+ ],
19
+ "msg": "Annex II(1): give an electronic contact address for the manufacturer."
20
+ },
21
+ {
22
+ "check": "vulnerability_contact",
23
+ "sev": "error",
24
+ "kind": "require_any",
25
+ "need": [
26
+ "report[^\\n]{0,60}vulnerabilit",
27
+ "vulnerabilit[^\\n]{0,60}(report|contact)"
28
+ ],
29
+ "msg": "Annex II(2): name a single point of contact where vulnerabilities can be reported."
30
+ },
31
+ {
32
+ "check": "disclosure_policy",
33
+ "sev": "warning",
34
+ "kind": "require_any",
35
+ "need": [
36
+ "coordinated vulnerability disclosure",
37
+ "disclosure policy",
38
+ "SECURITY\\.md",
39
+ "security\\.txt"
40
+ ],
41
+ "msg": "Annex II(2): say where the coordinated vulnerability disclosure policy can be found."
42
+ },
43
+ {
44
+ "check": "product_identification",
45
+ "sev": "error",
46
+ "kind": "require_any",
47
+ "need": [
48
+ "(model|type designation|batch|serial|firmware version|product version|version)\\s*:"
49
+ ],
50
+ "msg": "Annex II(3): identify the product by type, batch, model or version."
51
+ },
52
+ {
53
+ "check": "intended_purpose",
54
+ "sev": "error",
55
+ "kind": "require_any",
56
+ "need": [
57
+ "intended purpose",
58
+ "intended use"
59
+ ],
60
+ "msg": "Annex II(4): state the intended purpose of the product."
61
+ },
62
+ {
63
+ "check": "security_environment",
64
+ "sev": "error",
65
+ "kind": "require_any",
66
+ "need": [
67
+ "security environment",
68
+ "intended (operational|operating) environment",
69
+ "operational environment"
70
+ ],
71
+ "msg": "Annex II(4): describe the intended security and operational environment."
72
+ },
73
+ {
74
+ "check": "essential_functions",
75
+ "sev": "warning",
76
+ "kind": "require_any",
77
+ "need": [
78
+ "essential function",
79
+ "security (properties|features)",
80
+ "security functions?"
81
+ ],
82
+ "msg": "Annex II(4): list the essential functions and the security properties."
83
+ },
84
+ {
85
+ "check": "known_risks",
86
+ "sev": "error",
87
+ "kind": "require_any",
88
+ "need": [
89
+ "(known|foreseeable)[^\\n]{0,60}(risk|circumstanc)",
90
+ "risk[^\\n]{0,40}(misuse|foreseeable)"
91
+ ],
92
+ "msg": "Annex II(5): describe known or foreseeable circumstances that lead to significant cybersecurity risk."
93
+ },
94
+ {
95
+ "check": "doc_address",
96
+ "sev": "error",
97
+ "kind": "require_all",
98
+ "need": [
99
+ "declaration of conformity",
100
+ "https?://"
101
+ ],
102
+ "msg": "Annex II(6): give the internet address where the EU declaration of conformity can be accessed."
103
+ },
104
+ {
105
+ "check": "support_end_date",
106
+ "sev": "error",
107
+ "kind": "support_date",
108
+ "msg": "Annex II(7): give the end of the support period as a calendar date (for example 2032-12-31)."
109
+ },
110
+ {
111
+ "check": "support_end_vague",
112
+ "sev": "error",
113
+ "kind": "forbid",
114
+ "need": [
115
+ "(support|updates?)[^\\n]{0,80}(as long as|for the foreseeable future|indefinitely|until further notice|where possible|on an ongoing basis)",
116
+ "(as long as|indefinitely)[^\\n]{0,80}(support|updates?)"
117
+ ],
118
+ "msg": "Annex II(7): an open-ended support promise is not an end date."
119
+ },
120
+ {
121
+ "check": "support_end_expired",
122
+ "sev": "error",
123
+ "kind": "support_date",
124
+ "msg": "Annex II(7): the support period end date in this document has already passed."
125
+ },
126
+ {
127
+ "check": "support_period_short",
128
+ "sev": "warning",
129
+ "kind": "support_date",
130
+ "msg": "Annex II(7): the support period ends less than five years from today."
131
+ },
132
+ {
133
+ "check": "support_type",
134
+ "sev": "warning",
135
+ "kind": "require_any",
136
+ "need": [
137
+ "type of[^\\n]{0,30}support",
138
+ "security support[^\\n]{0,40}(includes|covers|means)",
139
+ "security patches"
140
+ ],
141
+ "msg": "Annex II(7): say what the technical security support covers."
142
+ },
143
+ {
144
+ "check": "update_instructions",
145
+ "sev": "error",
146
+ "kind": "require_any",
147
+ "need": [
148
+ "(install|apply|download)[^\\n]{0,40}(security )?updates?",
149
+ "update[^\\n]{0,30}instructions"
150
+ ],
151
+ "msg": "Annex II(8): give instructions for installing security updates."
152
+ },
153
+ {
154
+ "check": "secure_decommissioning",
155
+ "sev": "error",
156
+ "kind": "require_all",
157
+ "need": [
158
+ "(decommission|secure disposal|end of (use|life))",
159
+ "(remove|delete|erase|wipe)[^\\n]{0,60}data"
160
+ ],
161
+ "msg": "Annex II(8): cover secure decommissioning, including how user data is erased."
162
+ },
163
+ {
164
+ "check": "sbom_access",
165
+ "sev": "warning",
166
+ "kind": "require_any",
167
+ "need": [
168
+ "software bill of materials",
169
+ "\\bSBOM\\b",
170
+ "CycloneDX",
171
+ "SPDX"
172
+ ],
173
+ "msg": "Annex II(9): say how the machine-readable software bill of materials can be accessed."
174
+ },
175
+ {
176
+ "check": "template_placeholder",
177
+ "sev": "error",
178
+ "kind": "forbid",
179
+ "need": [
180
+ "\\[(company|product|insert|your)[^\\]]*\\]",
181
+ "<insert[^>]*>",
182
+ "\\bTODO\\b",
183
+ "XX/XX",
184
+ "YYYY-MM-DD",
185
+ "lorem ipsum"
186
+ ],
187
+ "msg": "A template placeholder is still in the shipped documentation."
188
+ }
189
+ ]
package/strings.json ADDED
@@ -0,0 +1,10 @@
1
+ {
2
+ "name": "CRA Annex II User Docs Lint",
3
+ "subtitle": "19 rules over the user documentation that ships with your product: support-period end date, vulnerability reporting contact, EU declaration of conformity address, secure decommissioning.",
4
+ "bin": "cra-annex-ii-docs-lint",
5
+ "price": 29,
6
+ "free": "Lints the documentation file you have open with all 19 rules and puts every missing, vague or expired Annex II item on its line - 15 findings (11 errors, 4 warnings) on the bundled dirty fixture, 0 on the clean one. No key, no upload, finished on its own.",
7
+ "paid": "Scope and ownership: one command sweeps every documentation file in the workspace and exports a dated Annex II evidence report (Markdown + CSV) you keep with your technical file. $29 once, one licence key per person or team seat, 7-day full refund.",
8
+ "need_key": "This option needs a licence (CRA Annex II User Docs Lint).",
9
+ "exts": []
10
+ }