generated-signature 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/eslint/rules/valid.js +1 -1
- package/dist/lib/manifest.js +3 -1
- package/dist/lib/merge.d.ts +3 -0
- package/dist/lib/merge.js +1 -1
- package/dist/lib/sections.js +2 -2
- package/dist/lib/signature.js +17 -16
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@ Sign generated files so hand edits are detected, while keeping manual sections e
|
|
|
8
8
|
|
|
9
9
|
</div>
|
|
10
10
|
|
|
11
|
-
Guides and the full reference are on the [docs site](https://jacobley.github.io/leyman/generated-signature/).
|
|
11
|
+
Guides and the full reference are on the [docs site](https://jacobley.github.io/leyman/packages/generated-signature/).
|
|
12
12
|
|
|
13
13
|
- **Offline check**: `verify()` needs only the file, not the generator, so it is cheap enough for a lint rule or a pre-commit hook.
|
|
14
14
|
- **Manual sections**: mark regions a person may edit. Their content is left out of the signature and carried over when the file is regenerated.
|
|
@@ -91,7 +91,7 @@ export class AutoGenerated {
|
|
|
91
91
|
}
|
|
92
92
|
```
|
|
93
93
|
|
|
94
|
-
- **Signature token**: `@generated-signature v1:` followed by 32 lowercase hex characters. It must appear exactly once, anywhere in the file. `sign()` inserts it as the first line (after a shebang
|
|
94
|
+
- **Signature token**: `@generated-signature v1:` followed by 32 lowercase hex characters. It must appear exactly once, anywhere in the file. `sign()` inserts it as the first line (after a shebang, an XML declaration, or Markdown front matter) unless the text already has one, so a generator with its own header can place it there with [`signatureToken()`](#signaturetoken).
|
|
95
95
|
- **Manual sections**: a line containing `@manual-start <id>` and a later line containing `@manual-end <id>`. Ids are letters, digits, `_`, `.` and `-`, unique per file. Sections do not nest. The marker must be at the start of the line or after whitespace.
|
|
96
96
|
- **Hash**: SHA-256 (first 128 bits) of the file with `\r\n` normalized to `\n`, the token's hash replaced by zeros, and every section body removed. The marker lines are hashed, so adding, removing or renaming a section is detected. `v1` versions this algorithm.
|
|
97
97
|
|
|
@@ -4,7 +4,7 @@ export const valid = {
|
|
|
4
4
|
type: 'problem',
|
|
5
5
|
docs: {
|
|
6
6
|
description: 'Disallow editing generated files outside of their manual sections',
|
|
7
|
-
url: 'https://jacobley.github.io/leyman/generated-signature/guides/eslint#generated-signature-valid'
|
|
7
|
+
url: 'https://jacobley.github.io/leyman/packages/generated-signature/guides/eslint#generated-signature-valid'
|
|
8
8
|
},
|
|
9
9
|
messages: {
|
|
10
10
|
invalid: 'Generated file was edited outside of its manual sections. Regenerate it instead.',
|
package/dist/lib/manifest.js
CHANGED
|
@@ -59,14 +59,16 @@ export const parseManifest = (text)=>{
|
|
|
59
59
|
if (typeof files !== 'object' || files === null || Array.isArray(files)) {
|
|
60
60
|
throw new GeneratedSignatureManifestError('files is not an object');
|
|
61
61
|
}
|
|
62
|
+
const entries = {};
|
|
62
63
|
for (const [path, entry] of Object.entries(files)){
|
|
63
64
|
if (!isEntry(entry)) {
|
|
64
65
|
throw new GeneratedSignatureManifestError(`invalid entry for ${path}`);
|
|
65
66
|
}
|
|
67
|
+
entries[path] = entry;
|
|
66
68
|
}
|
|
67
69
|
return {
|
|
68
70
|
version,
|
|
69
|
-
files:
|
|
71
|
+
files: entries
|
|
70
72
|
};
|
|
71
73
|
};
|
|
72
74
|
const sortKeys = (record)=>Object.fromEntries(Object.entries(record).toSorted(([a], [b])=>a < b ? -1 : 1));
|
package/dist/lib/merge.d.ts
CHANGED
package/dist/lib/merge.js
CHANGED
|
@@ -24,7 +24,7 @@ export const manualSection = ({ id, body = '', indent = '', ...options })=>{
|
|
|
24
24
|
throw new GeneratedSignatureSectionIdError(id);
|
|
25
25
|
}
|
|
26
26
|
const style = resolveCommentStyle(options);
|
|
27
|
-
const lines = body === '' ? [] : body.replace(/\n$/
|
|
27
|
+
const lines = body === '' ? [] : body.replace(/\n$/v, '').split('\n');
|
|
28
28
|
return [
|
|
29
29
|
renderComment(style, `@manual-start ${id}`),
|
|
30
30
|
...lines,
|
package/dist/lib/sections.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { GeneratedSignatureMalformedError } from './errors.js';
|
|
2
|
-
const markerPattern = /(?:^|\s)@manual-(?<kind>start
|
|
3
|
-
const idPattern = /^[\w
|
|
2
|
+
const markerPattern = /(?:^|\s)@manual-(?<kind>end|start)\b(?:\s+(?<id>\S+))?/v;
|
|
3
|
+
const idPattern = /^[\w\-.]+$/v;
|
|
4
4
|
export const isValidSectionId = (id)=>idPattern.test(id);
|
|
5
5
|
export const scanSections = (text)=>{
|
|
6
6
|
const sections = [];
|
package/dist/lib/signature.js
CHANGED
|
@@ -6,10 +6,11 @@ const VERSION = '1';
|
|
|
6
6
|
const HASH_LENGTH = 32;
|
|
7
7
|
const PLACEHOLDER = '0'.repeat(HASH_LENGTH);
|
|
8
8
|
const TOKEN_NAME = '@generated-signature';
|
|
9
|
-
const tokenPattern = new RegExp(String.raw`${TOKEN_NAME} v(?<version>\d+):(?<hash>[\da-f]{${HASH_LENGTH}})`, '
|
|
9
|
+
const tokenPattern = new RegExp(String.raw`${TOKEN_NAME} v(?<version>\d+):(?<hash>[\da-f]{${HASH_LENGTH}})`, 'gv');
|
|
10
10
|
const DEFAULT_MESSAGE = 'Generated file. Do not edit outside of manual sections.';
|
|
11
|
+
const renderToken = (hash)=>`${TOKEN_NAME} v${VERSION}:${hash}`;
|
|
11
12
|
const lineOf = (text, offset)=>text.slice(0, offset).split('\n').length;
|
|
12
|
-
const
|
|
13
|
+
const findHash = (text)=>{
|
|
13
14
|
const [first, second] = text.matchAll(tokenPattern);
|
|
14
15
|
if (!first) {
|
|
15
16
|
return null;
|
|
@@ -29,20 +30,14 @@ const findToken = (text)=>{
|
|
|
29
30
|
detail: `v${version} is not supported, expected v${VERSION}`
|
|
30
31
|
});
|
|
31
32
|
}
|
|
32
|
-
return
|
|
33
|
-
hash,
|
|
34
|
-
hashStart: first.index + first[0].length - HASH_LENGTH
|
|
35
|
-
};
|
|
33
|
+
return hash;
|
|
36
34
|
};
|
|
37
|
-
const replaceHash = (text, token, hash)=>text.slice(0, token.hashStart) + hash + text.slice(token.hashStart + HASH_LENGTH);
|
|
38
35
|
const computeHash = (text)=>{
|
|
39
|
-
const
|
|
40
|
-
const token = findToken(normalized);
|
|
41
|
-
const withPlaceholder = replaceHash(normalized, token, PLACEHOLDER);
|
|
36
|
+
const withPlaceholder = text.replaceAll('\r\n', '\n').replaceAll(tokenPattern, ()=>renderToken(PLACEHOLDER));
|
|
42
37
|
const hashable = replaceBodies(withPlaceholder, scanSections(withPlaceholder), ()=>'');
|
|
43
38
|
return createHash('sha256').update(hashable).digest('hex').slice(0, HASH_LENGTH);
|
|
44
39
|
};
|
|
45
|
-
export const signatureToken = ()
|
|
40
|
+
export const signatureToken = ()=>renderToken(PLACEHOLDER);
|
|
46
41
|
const insertHeader = (text, options)=>{
|
|
47
42
|
const style = resolveCommentStyle(options);
|
|
48
43
|
const lineBreak = text.includes('\r\n') ? '\r\n' : '\n';
|
|
@@ -51,28 +46,34 @@ const insertHeader = (text, options)=>{
|
|
|
51
46
|
const firstLineEnd = text.indexOf('\n');
|
|
52
47
|
return firstLineEnd === -1 ? text + lineBreak + header : text.slice(0, firstLineEnd + 1) + header + text.slice(firstLineEnd + 1);
|
|
53
48
|
}
|
|
49
|
+
const frontMatter = style.start === '<!--' ? /^---\r?\n(?:[^\n]*\n)*?---\r?(?:\n|$)/v.exec(text) : null;
|
|
50
|
+
if (frontMatter) {
|
|
51
|
+
const [fenced] = frontMatter;
|
|
52
|
+
return fenced.endsWith('\n') ? fenced + header + text.slice(fenced.length) : fenced + lineBreak + header;
|
|
53
|
+
}
|
|
54
54
|
return header + text;
|
|
55
55
|
};
|
|
56
56
|
export const sign = (text, options)=>{
|
|
57
57
|
let withToken = text;
|
|
58
|
-
if (
|
|
58
|
+
if (findHash(text) === null) {
|
|
59
59
|
if (!options) {
|
|
60
60
|
throw new TypeError('Text has no signature token, provide `comment` or `filePath`');
|
|
61
61
|
}
|
|
62
62
|
withToken = insertHeader(text, options);
|
|
63
63
|
}
|
|
64
|
-
|
|
64
|
+
const hash = computeHash(withToken);
|
|
65
|
+
return withToken.replaceAll(tokenPattern, ()=>renderToken(hash));
|
|
65
66
|
};
|
|
66
67
|
export const verify = (text)=>{
|
|
67
68
|
try {
|
|
68
|
-
const
|
|
69
|
-
if (
|
|
69
|
+
const hash = findHash(text.replaceAll('\r\n', '\n'));
|
|
70
|
+
if (hash === null) {
|
|
70
71
|
return {
|
|
71
72
|
status: 'unsigned'
|
|
72
73
|
};
|
|
73
74
|
}
|
|
74
75
|
return {
|
|
75
|
-
status: computeHash(text) ===
|
|
76
|
+
status: computeHash(text) === hash ? 'valid' : 'invalid'
|
|
76
77
|
};
|
|
77
78
|
} catch (err) {
|
|
78
79
|
return {
|