@noeos/verification-engine 0.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/CHANGELOG.md +8 -0
- package/LICENSE +201 -0
- package/NOTICE +11 -0
- package/README.md +7 -0
- package/dist/cjs/api/engine.js +271 -0
- package/dist/cjs/api/types.js +10 -0
- package/dist/cjs/chains/chain-builder.js +541 -0
- package/dist/cjs/chains/duplicate-detector.js +208 -0
- package/dist/cjs/chains/verify-chain.js +656 -0
- package/dist/cjs/contracts/generated.js +512 -0
- package/dist/cjs/contracts/schema-assets.js +6 -0
- package/dist/cjs/contracts/vector-assets.js +6 -0
- package/dist/cjs/domain/algorithm-id.js +5 -0
- package/dist/cjs/domain/byte-view.js +7 -0
- package/dist/cjs/domain/chain.js +3 -0
- package/dist/cjs/domain/diagnostic.js +6 -0
- package/dist/cjs/domain/digest.js +37 -0
- package/dist/cjs/domain/duplicate-policy.js +3 -0
- package/dist/cjs/domain/evidence.js +10 -0
- package/dist/cjs/domain/identifiers.js +52 -0
- package/dist/cjs/domain/json-value.js +3 -0
- package/dist/cjs/domain/limits.js +24 -0
- package/dist/cjs/domain/operation-result.js +19 -0
- package/dist/cjs/domain/position.js +16 -0
- package/dist/cjs/domain/protocol-version.js +5 -0
- package/dist/cjs/domain/rule.js +3 -0
- package/dist/cjs/domain/stream.js +3 -0
- package/dist/cjs/domain/version.js +16 -0
- package/dist/cjs/evidence/digest-evidence.js +22 -0
- package/dist/cjs/evidence/evidence-parser.js +288 -0
- package/dist/cjs/framing/frame-builders.js +124 -0
- package/dist/cjs/framing/frame-encoder.js +238 -0
- package/dist/cjs/framing/frame-parser.js +173 -0
- package/dist/cjs/framing/frame-types.js +3 -0
- package/dist/cjs/hashing/hash-adapter.js +90 -0
- package/dist/cjs/hashing/hash-frame.js +16 -0
- package/dist/cjs/index.js +16 -0
- package/dist/cjs/normalization/byte-sink.js +70 -0
- package/dist/cjs/normalization/jcs-profile.js +154 -0
- package/dist/cjs/normalization/normalize.js +56 -0
- package/dist/cjs/normalization/profile-registry.js +66 -0
- package/dist/cjs/normalization/profile.js +3 -0
- package/dist/cjs/normalization/raw-bytes-profile.js +27 -0
- package/dist/cjs/package.json +3 -0
- package/dist/cjs/profiles.js +9 -0
- package/dist/cjs/records/record-service.js +278 -0
- package/dist/cjs/rules/rule-set.js +211 -0
- package/dist/cjs/schemas.js +6 -0
- package/dist/cjs/validation/algorithm-validation.js +12 -0
- package/dist/cjs/validation/diagnostic-collector.js +159 -0
- package/dist/cjs/validation/digest-validation.js +48 -0
- package/dist/cjs/validation/identifier-validation.js +33 -0
- package/dist/cjs/validation/json-text-parser.js +297 -0
- package/dist/cjs/validation/json-value-validation.js +155 -0
- package/dist/cjs/validation/limit-validation.js +75 -0
- package/dist/cjs/validation/object-inspection.js +60 -0
- package/dist/cjs/validation/position-validation.js +13 -0
- package/dist/cjs/validation/result-helpers.js +9 -0
- package/dist/cjs/validation/utf8-validation.js +69 -0
- package/dist/cjs/validation/version-validation.js +31 -0
- package/dist/cjs/vectors.js +6 -0
- package/dist/esm/api/engine.js +266 -0
- package/dist/esm/api/types.js +4 -0
- package/dist/esm/chains/chain-builder.js +536 -0
- package/dist/esm/chains/duplicate-detector.js +204 -0
- package/dist/esm/chains/verify-chain.js +652 -0
- package/dist/esm/contracts/generated.js +509 -0
- package/dist/esm/contracts/schema-assets.js +2 -0
- package/dist/esm/contracts/vector-assets.js +2 -0
- package/dist/esm/domain/algorithm-id.js +2 -0
- package/dist/esm/domain/byte-view.js +4 -0
- package/dist/esm/domain/chain.js +2 -0
- package/dist/esm/domain/diagnostic.js +3 -0
- package/dist/esm/domain/digest.js +32 -0
- package/dist/esm/domain/duplicate-policy.js +2 -0
- package/dist/esm/domain/evidence.js +7 -0
- package/dist/esm/domain/identifiers.js +45 -0
- package/dist/esm/domain/json-value.js +2 -0
- package/dist/esm/domain/limits.js +21 -0
- package/dist/esm/domain/operation-result.js +15 -0
- package/dist/esm/domain/position.js +12 -0
- package/dist/esm/domain/protocol-version.js +2 -0
- package/dist/esm/domain/rule.js +2 -0
- package/dist/esm/domain/stream.js +2 -0
- package/dist/esm/domain/version.js +12 -0
- package/dist/esm/evidence/digest-evidence.js +19 -0
- package/dist/esm/evidence/evidence-parser.js +281 -0
- package/dist/esm/framing/frame-builders.js +114 -0
- package/dist/esm/framing/frame-encoder.js +234 -0
- package/dist/esm/framing/frame-parser.js +170 -0
- package/dist/esm/framing/frame-types.js +2 -0
- package/dist/esm/hashing/hash-adapter.js +84 -0
- package/dist/esm/hashing/hash-frame.js +12 -0
- package/dist/esm/index.js +5 -0
- package/dist/esm/normalization/byte-sink.js +66 -0
- package/dist/esm/normalization/jcs-profile.js +151 -0
- package/dist/esm/normalization/normalize.js +52 -0
- package/dist/esm/normalization/profile-registry.js +62 -0
- package/dist/esm/normalization/profile.js +2 -0
- package/dist/esm/normalization/raw-bytes-profile.js +24 -0
- package/dist/esm/profiles.js +6 -0
- package/dist/esm/records/record-service.js +270 -0
- package/dist/esm/rules/rule-set.js +206 -0
- package/dist/esm/schemas.js +2 -0
- package/dist/esm/validation/algorithm-validation.js +9 -0
- package/dist/esm/validation/diagnostic-collector.js +154 -0
- package/dist/esm/validation/digest-validation.js +43 -0
- package/dist/esm/validation/identifier-validation.js +27 -0
- package/dist/esm/validation/json-text-parser.js +294 -0
- package/dist/esm/validation/json-value-validation.js +152 -0
- package/dist/esm/validation/limit-validation.js +71 -0
- package/dist/esm/validation/object-inspection.js +54 -0
- package/dist/esm/validation/position-validation.js +10 -0
- package/dist/esm/validation/result-helpers.js +6 -0
- package/dist/esm/validation/utf8-validation.js +63 -0
- package/dist/esm/validation/version-validation.js +26 -0
- package/dist/esm/vectors.js +2 -0
- package/dist/types/api/engine.d.ts +8 -0
- package/dist/types/api/types.d.ts +124 -0
- package/dist/types/chains/chain-builder.d.ts +42 -0
- package/dist/types/chains/duplicate-detector.d.ts +18 -0
- package/dist/types/chains/verify-chain.d.ts +9 -0
- package/dist/types/contracts/generated.d.ts +442 -0
- package/dist/types/contracts/schema-assets.d.ts +2 -0
- package/dist/types/contracts/vector-assets.d.ts +2 -0
- package/dist/types/domain/algorithm-id.d.ts +4 -0
- package/dist/types/domain/byte-view.d.ts +1 -0
- package/dist/types/domain/chain.d.ts +43 -0
- package/dist/types/domain/diagnostic.d.ts +23 -0
- package/dist/types/domain/digest.d.ts +12 -0
- package/dist/types/domain/duplicate-policy.d.ts +26 -0
- package/dist/types/domain/evidence.d.ts +100 -0
- package/dist/types/domain/identifiers.d.ts +29 -0
- package/dist/types/domain/json-value.d.ts +6 -0
- package/dist/types/domain/limits.d.ts +13 -0
- package/dist/types/domain/operation-result.d.ts +12 -0
- package/dist/types/domain/position.d.ts +8 -0
- package/dist/types/domain/protocol-version.d.ts +2 -0
- package/dist/types/domain/rule.d.ts +34 -0
- package/dist/types/domain/stream.d.ts +5 -0
- package/dist/types/domain/version.d.ts +8 -0
- package/dist/types/evidence/digest-evidence.d.ts +4 -0
- package/dist/types/evidence/evidence-parser.d.ts +8 -0
- package/dist/types/framing/frame-builders.d.ts +40 -0
- package/dist/types/framing/frame-encoder.d.ts +6 -0
- package/dist/types/framing/frame-parser.d.ts +4 -0
- package/dist/types/framing/frame-types.d.ts +27 -0
- package/dist/types/hashing/hash-adapter.d.ts +12 -0
- package/dist/types/hashing/hash-frame.d.ts +6 -0
- package/dist/types/index.d.ts +8 -0
- package/dist/types/normalization/byte-sink.d.ts +19 -0
- package/dist/types/normalization/jcs-profile.d.ts +3 -0
- package/dist/types/normalization/normalize.d.ts +11 -0
- package/dist/types/normalization/profile-registry.d.ts +26 -0
- package/dist/types/normalization/profile.d.ts +22 -0
- package/dist/types/normalization/raw-bytes-profile.d.ts +2 -0
- package/dist/types/profiles.d.ts +8 -0
- package/dist/types/records/record-service.d.ts +38 -0
- package/dist/types/rules/rule-set.d.ts +13 -0
- package/dist/types/schemas.d.ts +2 -0
- package/dist/types/validation/algorithm-validation.d.ts +3 -0
- package/dist/types/validation/diagnostic-collector.d.ts +24 -0
- package/dist/types/validation/digest-validation.d.ts +6 -0
- package/dist/types/validation/identifier-validation.d.ts +6 -0
- package/dist/types/validation/json-text-parser.d.ts +4 -0
- package/dist/types/validation/json-value-validation.d.ts +4 -0
- package/dist/types/validation/limit-validation.d.ts +4 -0
- package/dist/types/validation/object-inspection.d.ts +11 -0
- package/dist/types/validation/position-validation.d.ts +3 -0
- package/dist/types/validation/result-helpers.d.ts +3 -0
- package/dist/types/validation/utf8-validation.d.ts +5 -0
- package/dist/types/validation/version-validation.d.ts +9 -0
- package/dist/types/vectors.d.ts +2 -0
- package/package.json +61 -0
- package/schemas/chain-summary.v1.schema.json +156 -0
- package/schemas/diagnostic.v1.schema.json +107 -0
- package/schemas/link-evidence.v1.schema.json +165 -0
- package/schemas/record-evidence.v1.schema.json +91 -0
- package/schemas/vector-set.v1.schema.json +52 -0
- package/vectors/evidence.json +26 -0
- package/vectors/framing.json +136 -0
- package/vectors/hashing.json +58 -0
- package/vectors/invalid.json +56 -0
- package/vectors/manifest.json +46 -0
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
import { types } from "node:util";
|
|
3
|
+
import { failure } from "../domain/operation-result.js";
|
|
4
|
+
import { DiagnosticCollector } from "../validation/diagnostic-collector.js";
|
|
5
|
+
import { decodeUtf8 } from "../validation/utf8-validation.js";
|
|
6
|
+
const MAGIC = "NOEOSVE1";
|
|
7
|
+
const HEADER_LENGTH = 13;
|
|
8
|
+
const KIND_NAMES = Object.freeze({
|
|
9
|
+
1: "content",
|
|
10
|
+
2: "record",
|
|
11
|
+
3: "link",
|
|
12
|
+
4: "evidence",
|
|
13
|
+
});
|
|
14
|
+
const TYPE_NAMES = Object.freeze({
|
|
15
|
+
1: "bytes",
|
|
16
|
+
2: "utf8",
|
|
17
|
+
3: "uint64",
|
|
18
|
+
4: "none",
|
|
19
|
+
});
|
|
20
|
+
export function parseFrame(input, limits) {
|
|
21
|
+
if (types.isProxy(input) || !(input instanceof Uint8Array))
|
|
22
|
+
return frameFailure("FRAME_LENGTH_INVALID", limits);
|
|
23
|
+
if (input.length > limits.maxPayloadBytes)
|
|
24
|
+
return frameFailure("INPUT_LIMIT_EXCEEDED", limits);
|
|
25
|
+
if (input.length < HEADER_LENGTH)
|
|
26
|
+
return frameFailure("FRAME_LENGTH_INVALID", limits);
|
|
27
|
+
const view = new DataView(input.buffer, input.byteOffset, input.byteLength);
|
|
28
|
+
for (let index = 0; index < MAGIC.length; index += 1) {
|
|
29
|
+
if (view.getUint8(index) !== MAGIC.charCodeAt(index))
|
|
30
|
+
return frameFailure("FRAME_MAGIC_INVALID", limits);
|
|
31
|
+
}
|
|
32
|
+
const kind = KIND_NAMES[view.getUint8(8)];
|
|
33
|
+
if (kind === undefined)
|
|
34
|
+
return frameFailure("FRAME_KIND_UNKNOWN", limits);
|
|
35
|
+
if (view.getUint8(9) !== 0 || view.getUint8(10) !== 1)
|
|
36
|
+
return frameFailure("FRAME_VERSION_UNSUPPORTED", limits);
|
|
37
|
+
const fieldCount = view.getUint16(11);
|
|
38
|
+
let offset = HEADER_LENGTH;
|
|
39
|
+
let previousTag = 0;
|
|
40
|
+
const fields = [];
|
|
41
|
+
for (let index = 0; index < fieldCount; index += 1) {
|
|
42
|
+
if (offset + 11 > input.length)
|
|
43
|
+
return frameFailure("FRAME_LENGTH_INVALID", limits);
|
|
44
|
+
const tag = view.getUint16(offset);
|
|
45
|
+
offset += 2;
|
|
46
|
+
const expected = expectedField(kind, tag);
|
|
47
|
+
if (tag === 0 || expected === undefined)
|
|
48
|
+
return frameFailure("FRAME_FIELD_UNKNOWN", limits);
|
|
49
|
+
if (tag === previousTag)
|
|
50
|
+
return frameFailure("FRAME_FIELD_DUPLICATE", limits);
|
|
51
|
+
if (tag < previousTag)
|
|
52
|
+
return frameFailure("FRAME_FIELD_ORDER_INVALID", limits);
|
|
53
|
+
previousTag = tag;
|
|
54
|
+
const typeCode = view.getUint8(offset);
|
|
55
|
+
offset += 1;
|
|
56
|
+
const type = TYPE_NAMES[typeCode];
|
|
57
|
+
if (type === undefined)
|
|
58
|
+
return frameFailure("FRAME_FIELD_TYPE_INVALID", limits);
|
|
59
|
+
const length = readUint64(view, offset);
|
|
60
|
+
if (length === undefined)
|
|
61
|
+
return frameFailure("FRAME_LENGTH_INVALID", limits);
|
|
62
|
+
offset += 8;
|
|
63
|
+
if (length > input.length - offset)
|
|
64
|
+
return frameFailure("FRAME_LENGTH_INVALID", limits);
|
|
65
|
+
if (!expected.types.includes(type))
|
|
66
|
+
return frameFailure("FRAME_FIELD_TYPE_INVALID", limits);
|
|
67
|
+
const value = readValue(tag, type, input.subarray(offset, offset + length), limits);
|
|
68
|
+
if (!value.ok)
|
|
69
|
+
return value;
|
|
70
|
+
fields.push(value.value);
|
|
71
|
+
offset += length;
|
|
72
|
+
}
|
|
73
|
+
if (offset !== input.length)
|
|
74
|
+
return frameFailure("FRAME_TRAILING_DATA", limits);
|
|
75
|
+
const expectedFields = expectedFieldsFor(kind);
|
|
76
|
+
if (fields.length !== expectedFields.length) {
|
|
77
|
+
return frameFailure("FRAME_FIELD_MISSING", limits);
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
ok: true,
|
|
81
|
+
value: Object.freeze({ kind, protocolVersion: 1, fields: Object.freeze(fields) }),
|
|
82
|
+
diagnostics: Object.freeze([]),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
function expectedFieldsFor(kind) {
|
|
86
|
+
const text = ["utf8"];
|
|
87
|
+
const bytes = ["bytes"];
|
|
88
|
+
const uint64 = ["uint64"];
|
|
89
|
+
if (kind === "content")
|
|
90
|
+
return [
|
|
91
|
+
{ tag: 1, types: text },
|
|
92
|
+
{ tag: 2, types: text },
|
|
93
|
+
{ tag: 3, types: text },
|
|
94
|
+
{ tag: 4, types: bytes },
|
|
95
|
+
];
|
|
96
|
+
if (kind === "record")
|
|
97
|
+
return [
|
|
98
|
+
{ tag: 1, types: text },
|
|
99
|
+
{ tag: 2, types: text },
|
|
100
|
+
{ tag: 3, types: text },
|
|
101
|
+
{ tag: 4, types: text },
|
|
102
|
+
{ tag: 5, types: text },
|
|
103
|
+
{ tag: 6, types: uint64 },
|
|
104
|
+
{ tag: 7, types: bytes },
|
|
105
|
+
];
|
|
106
|
+
if (kind === "link")
|
|
107
|
+
return [
|
|
108
|
+
{ tag: 1, types: text },
|
|
109
|
+
{ tag: 2, types: text },
|
|
110
|
+
{ tag: 3, types: text },
|
|
111
|
+
{ tag: 4, types: uint64 },
|
|
112
|
+
{ tag: 5, types: text },
|
|
113
|
+
{ tag: 6, types: bytes },
|
|
114
|
+
{ tag: 7, types: ["none", "bytes"] },
|
|
115
|
+
];
|
|
116
|
+
return [
|
|
117
|
+
{ tag: 1, types: text },
|
|
118
|
+
{ tag: 2, types: text },
|
|
119
|
+
{ tag: 3, types: bytes },
|
|
120
|
+
];
|
|
121
|
+
}
|
|
122
|
+
function expectedField(kind, tag) {
|
|
123
|
+
return expectedFieldsFor(kind).find((field) => field.tag === tag);
|
|
124
|
+
}
|
|
125
|
+
function readValue(tag, type, value, limits) {
|
|
126
|
+
if (type === "none") {
|
|
127
|
+
if (value.length !== 0)
|
|
128
|
+
return frameFailure("FRAME_LENGTH_INVALID", limits);
|
|
129
|
+
return { ok: true, value: { tag, type: "none" }, diagnostics: Object.freeze([]) };
|
|
130
|
+
}
|
|
131
|
+
if (type === "bytes")
|
|
132
|
+
return {
|
|
133
|
+
ok: true,
|
|
134
|
+
value: { tag, type: "bytes", value: Uint8Array.from(value) },
|
|
135
|
+
diagnostics: Object.freeze([]),
|
|
136
|
+
};
|
|
137
|
+
if (type === "utf8") {
|
|
138
|
+
const decoded = decodeUtf8(value);
|
|
139
|
+
if (!decoded.ok)
|
|
140
|
+
return frameFailure("UTF8_INVALID", limits);
|
|
141
|
+
return {
|
|
142
|
+
ok: true,
|
|
143
|
+
value: { tag, type: "utf8", value: decoded.value },
|
|
144
|
+
diagnostics: Object.freeze([]),
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
if (value.length !== 8)
|
|
148
|
+
return frameFailure("FRAME_LENGTH_INVALID", limits);
|
|
149
|
+
const number = readUint64(new DataView(value.buffer, value.byteOffset, value.byteLength), 0);
|
|
150
|
+
if (number === undefined)
|
|
151
|
+
return frameFailure("FRAME_LENGTH_INVALID", limits);
|
|
152
|
+
return {
|
|
153
|
+
ok: true,
|
|
154
|
+
value: { tag, type: "uint64", value: number },
|
|
155
|
+
diagnostics: Object.freeze([]),
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
function readUint64(view, offset) {
|
|
159
|
+
let value = 0n;
|
|
160
|
+
for (let index = 0; index < 8; index += 1)
|
|
161
|
+
value = (value << 8n) | BigInt(view.getUint8(offset + index));
|
|
162
|
+
if (value > BigInt(Number.MAX_SAFE_INTEGER))
|
|
163
|
+
return undefined;
|
|
164
|
+
return Number(value);
|
|
165
|
+
}
|
|
166
|
+
function frameFailure(code, limits) {
|
|
167
|
+
const collector = new DiagnosticCollector(limits);
|
|
168
|
+
collector.addCode(code, "input");
|
|
169
|
+
return failure(collector.finish());
|
|
170
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { strict as assert } from "node:assert";
|
|
4
|
+
import { types } from "node:util";
|
|
5
|
+
import { failure, success } from "../domain/operation-result.js";
|
|
6
|
+
import { DiagnosticCollector } from "../validation/diagnostic-collector.js";
|
|
7
|
+
import { digestLength } from "../validation/digest-validation.js";
|
|
8
|
+
import { validateAlgorithmId } from "../validation/algorithm-validation.js";
|
|
9
|
+
import { Digest as DigestValue } from "../domain/digest.js";
|
|
10
|
+
const NODE_ALGORITHMS = Object.freeze({
|
|
11
|
+
"sha-256": "sha256",
|
|
12
|
+
"sha-384": "sha384",
|
|
13
|
+
"sha-512": "sha512",
|
|
14
|
+
});
|
|
15
|
+
export function createHashSink(algorithm, limits) {
|
|
16
|
+
void limits;
|
|
17
|
+
const validated = validateAlgorithmId(algorithm);
|
|
18
|
+
if (!validated.ok)
|
|
19
|
+
return validated;
|
|
20
|
+
return success(new NodeHashSink(validated.value));
|
|
21
|
+
}
|
|
22
|
+
export function hashBytes(algorithm, input, limits) {
|
|
23
|
+
const sinkResult = createHashSink(algorithm, limits);
|
|
24
|
+
if (!sinkResult.ok)
|
|
25
|
+
return sinkResult;
|
|
26
|
+
if (types.isProxy(input) || !(input instanceof Uint8Array))
|
|
27
|
+
return hashFailure("INPUT_TYPE_INVALID", limits);
|
|
28
|
+
if (input.length > limits.maxPayloadBytes)
|
|
29
|
+
return hashFailure("INPUT_LIMIT_EXCEEDED", limits);
|
|
30
|
+
sinkResult.value.update(input);
|
|
31
|
+
return success(sinkResult.value.digest());
|
|
32
|
+
}
|
|
33
|
+
export function hashValidatedBytes(algorithm, input) {
|
|
34
|
+
const sink = new NodeHashSink(algorithm);
|
|
35
|
+
sink.update(input);
|
|
36
|
+
return success(sink.digest());
|
|
37
|
+
}
|
|
38
|
+
export function hashChunks(algorithm, chunks, limits) {
|
|
39
|
+
const sinkResult = createHashSink(algorithm, limits);
|
|
40
|
+
if (!sinkResult.ok)
|
|
41
|
+
return sinkResult;
|
|
42
|
+
let total = 0;
|
|
43
|
+
try {
|
|
44
|
+
for (const chunk of chunks) {
|
|
45
|
+
if (types.isProxy(chunk) || !(chunk instanceof Uint8Array))
|
|
46
|
+
return hashFailure("INPUT_TYPE_INVALID", limits);
|
|
47
|
+
if (chunk.length > limits.maxPayloadBytes - total)
|
|
48
|
+
return hashFailure("INPUT_LIMIT_EXCEEDED", limits);
|
|
49
|
+
total += chunk.length;
|
|
50
|
+
sinkResult.value.update(chunk);
|
|
51
|
+
}
|
|
52
|
+
return success(sinkResult.value.digest());
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return hashFailure("NORMALIZATION_FAILED", limits);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
class NodeHashSink {
|
|
59
|
+
algorithm;
|
|
60
|
+
hash;
|
|
61
|
+
finalized = false;
|
|
62
|
+
constructor(algorithm) {
|
|
63
|
+
this.algorithm = algorithm;
|
|
64
|
+
this.hash = createHash(NODE_ALGORITHMS[algorithm]);
|
|
65
|
+
}
|
|
66
|
+
update(value) {
|
|
67
|
+
if (this.finalized)
|
|
68
|
+
throw new Error("hash already finalized");
|
|
69
|
+
this.hash.update(value);
|
|
70
|
+
}
|
|
71
|
+
digest() {
|
|
72
|
+
if (this.finalized)
|
|
73
|
+
throw new Error("hash already finalized");
|
|
74
|
+
this.finalized = true;
|
|
75
|
+
const bytes = this.hash.digest();
|
|
76
|
+
assert.equal(bytes.length, digestLength(this.algorithm), "unexpected digest length");
|
|
77
|
+
return DigestValue.fromValidated(this.algorithm, bytes);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
function hashFailure(code, limits) {
|
|
81
|
+
const collector = new DiagnosticCollector(limits);
|
|
82
|
+
collector.addCode(code, "input");
|
|
83
|
+
return failure(collector.finish());
|
|
84
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
import { hashBytes, hashValidatedBytes } from "./hash-adapter.js";
|
|
3
|
+
export function hashFrame(algorithm, frame, limits) {
|
|
4
|
+
if (!frame.ok)
|
|
5
|
+
return frame;
|
|
6
|
+
return hashBytes(algorithm, frame.value, limits);
|
|
7
|
+
}
|
|
8
|
+
export function hashTrustedFrame(algorithm, frame) {
|
|
9
|
+
if (!frame.ok)
|
|
10
|
+
return frame;
|
|
11
|
+
return hashValidatedBytes(algorithm, frame.value);
|
|
12
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
export { DIAGNOSTIC_CODES } from "./contracts/generated.js";
|
|
3
|
+
export { createEngine, EngineConfigurationError } from "./api/engine.js";
|
|
4
|
+
export { BUILTIN_PROFILES } from "./profiles.js";
|
|
5
|
+
export { CHAIN_SUMMARY_EVIDENCE_SCHEMA, DIAGNOSTIC_SCHEMA, LINK_EVIDENCE_SCHEMA, RECORD_EVIDENCE_SCHEMA, } from "./api/types.js";
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
import { copyBytes } from "../domain/byte-view.js";
|
|
3
|
+
const UTF8_ENCODER = new TextEncoder();
|
|
4
|
+
export class ByteArraySink {
|
|
5
|
+
maximum;
|
|
6
|
+
buffer = new Uint8Array(0);
|
|
7
|
+
written = 0;
|
|
8
|
+
constructor(maximum, initialCapacity = 0) {
|
|
9
|
+
this.maximum = maximum;
|
|
10
|
+
if (!Number.isSafeInteger(initialCapacity) || initialCapacity < 0) {
|
|
11
|
+
throw new RangeError("invalid initial sink capacity");
|
|
12
|
+
}
|
|
13
|
+
const capacity = Math.min(maximum, Math.max(0, initialCapacity));
|
|
14
|
+
if (capacity > 0)
|
|
15
|
+
this.buffer = new Uint8Array(capacity);
|
|
16
|
+
}
|
|
17
|
+
get byteLength() {
|
|
18
|
+
return this.written;
|
|
19
|
+
}
|
|
20
|
+
write(value) {
|
|
21
|
+
this.ensureCapacity(value.length);
|
|
22
|
+
this.buffer.set(value, this.written);
|
|
23
|
+
this.written += value.length;
|
|
24
|
+
}
|
|
25
|
+
writeByte(value) {
|
|
26
|
+
if (!Number.isInteger(value) || value < 0 || value > 0xff)
|
|
27
|
+
throw new RangeError("byte value out of range");
|
|
28
|
+
this.ensureCapacity(1);
|
|
29
|
+
this.buffer[this.written++] = value;
|
|
30
|
+
}
|
|
31
|
+
writeAscii(value) {
|
|
32
|
+
this.ensureCapacity(value.length);
|
|
33
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
34
|
+
const code = value.charCodeAt(index);
|
|
35
|
+
if (code > 0x7f)
|
|
36
|
+
throw new RangeError("non-ASCII value");
|
|
37
|
+
this.buffer[this.written++] = code;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
writeText(value) {
|
|
41
|
+
this.write(UTF8_ENCODER.encode(value));
|
|
42
|
+
}
|
|
43
|
+
toBytes() {
|
|
44
|
+
return copyBytes(this.buffer.subarray(0, this.written));
|
|
45
|
+
}
|
|
46
|
+
/** Internal ownership transfer; the returned view is never exposed to callers. */
|
|
47
|
+
takeBytes() {
|
|
48
|
+
const bytes = this.buffer.subarray(0, this.written);
|
|
49
|
+
this.buffer = new Uint8Array(0);
|
|
50
|
+
this.written = 0;
|
|
51
|
+
return bytes;
|
|
52
|
+
}
|
|
53
|
+
ensureCapacity(additional) {
|
|
54
|
+
if (additional > this.maximum - this.written)
|
|
55
|
+
throw new RangeError("byte sink limit exceeded");
|
|
56
|
+
const required = this.written + additional;
|
|
57
|
+
if (required <= this.buffer.length)
|
|
58
|
+
return;
|
|
59
|
+
let capacity = Math.max(256, this.buffer.length);
|
|
60
|
+
while (capacity < required)
|
|
61
|
+
capacity = Math.min(this.maximum, capacity * 2);
|
|
62
|
+
const expanded = new Uint8Array(capacity);
|
|
63
|
+
expanded.set(this.buffer.subarray(0, this.written));
|
|
64
|
+
this.buffer = expanded;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
import { ProfileId } from "../domain/identifiers.js";
|
|
3
|
+
import { ProfileVersion } from "../domain/version.js";
|
|
4
|
+
import { success } from "../domain/operation-result.js";
|
|
5
|
+
import { validateJsonValue } from "../validation/json-value-validation.js";
|
|
6
|
+
import { ByteArraySink } from "./byte-sink.js";
|
|
7
|
+
const JCS_PROFILE_ID = ProfileId.fromValidated("dev.noeos.jcs");
|
|
8
|
+
const JCS_PROFILE_VERSION = ProfileVersion.fromValidated("1.0.0");
|
|
9
|
+
const EMPTY = "";
|
|
10
|
+
export const jcsProfile = Object.freeze({
|
|
11
|
+
id: JCS_PROFILE_ID,
|
|
12
|
+
version: JCS_PROFILE_VERSION,
|
|
13
|
+
inputKind: "json",
|
|
14
|
+
validate(input, limits) {
|
|
15
|
+
return validateJsonValue(input, limits);
|
|
16
|
+
},
|
|
17
|
+
normalize(input, sink) {
|
|
18
|
+
writeValue(input, sink);
|
|
19
|
+
return success(Object.freeze({ byteLength: sink.byteLength }));
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
function writeValue(value, sink) {
|
|
23
|
+
if (value === null) {
|
|
24
|
+
writeAscii("null", sink);
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
if (typeof value === "boolean") {
|
|
28
|
+
writeAscii(value ? "true" : "false", sink);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
if (typeof value === "number") {
|
|
32
|
+
writeText(numberToJcs(value), sink);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
if (typeof value === "string") {
|
|
36
|
+
writeJsonString(value, sink);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
if (isJsonArray(value)) {
|
|
40
|
+
writeArray(value, sink);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
writeObject(value, sink);
|
|
44
|
+
}
|
|
45
|
+
function isJsonArray(value) {
|
|
46
|
+
return Array.isArray(value);
|
|
47
|
+
}
|
|
48
|
+
function writeArray(value, sink) {
|
|
49
|
+
writeAscii("[", sink);
|
|
50
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
51
|
+
if (index > 0)
|
|
52
|
+
writeAscii(",", sink);
|
|
53
|
+
const item = value[index];
|
|
54
|
+
if (item === undefined)
|
|
55
|
+
throw new TypeError("invalid JSON array");
|
|
56
|
+
writeValue(item, sink);
|
|
57
|
+
}
|
|
58
|
+
writeAscii("]", sink);
|
|
59
|
+
}
|
|
60
|
+
function writeObject(value, sink) {
|
|
61
|
+
const keys = Object.keys(value).sort(compareUtf16);
|
|
62
|
+
writeAscii("{", sink);
|
|
63
|
+
let first = true;
|
|
64
|
+
for (const key of keys) {
|
|
65
|
+
if (!first)
|
|
66
|
+
writeAscii(",", sink);
|
|
67
|
+
first = false;
|
|
68
|
+
const item = value[key];
|
|
69
|
+
if (item === undefined)
|
|
70
|
+
throw new TypeError("invalid JSON object");
|
|
71
|
+
writeJsonString(key, sink);
|
|
72
|
+
writeAscii(":", sink);
|
|
73
|
+
writeValue(item, sink);
|
|
74
|
+
}
|
|
75
|
+
writeAscii("}", sink);
|
|
76
|
+
}
|
|
77
|
+
function writeJsonString(value, sink) {
|
|
78
|
+
let escaped = false;
|
|
79
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
80
|
+
const codeUnit = value.charCodeAt(index);
|
|
81
|
+
if (codeUnit <= 0x1f || codeUnit === 0x22 || codeUnit === 0x5c) {
|
|
82
|
+
escaped = true;
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (!escaped) {
|
|
87
|
+
writeAscii('"', sink);
|
|
88
|
+
writeText(value, sink);
|
|
89
|
+
writeAscii('"', sink);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
writeText(stringToJcs(value), sink);
|
|
93
|
+
}
|
|
94
|
+
function stringToJcs(value) {
|
|
95
|
+
let output = '"';
|
|
96
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
97
|
+
const codeUnit = value.charCodeAt(index);
|
|
98
|
+
switch (codeUnit) {
|
|
99
|
+
case 0x08:
|
|
100
|
+
output += "\\b";
|
|
101
|
+
break;
|
|
102
|
+
case 0x09:
|
|
103
|
+
output += "\\t";
|
|
104
|
+
break;
|
|
105
|
+
case 0x0a:
|
|
106
|
+
output += "\\n";
|
|
107
|
+
break;
|
|
108
|
+
case 0x0c:
|
|
109
|
+
output += "\\f";
|
|
110
|
+
break;
|
|
111
|
+
case 0x0d:
|
|
112
|
+
output += "\\r";
|
|
113
|
+
break;
|
|
114
|
+
case 0x22:
|
|
115
|
+
output += '\\"';
|
|
116
|
+
break;
|
|
117
|
+
case 0x5c:
|
|
118
|
+
output += "\\\\";
|
|
119
|
+
break;
|
|
120
|
+
default:
|
|
121
|
+
if (codeUnit <= 0x1f)
|
|
122
|
+
output += `\\u${codeUnit.toString(16).padStart(4, "0")}`;
|
|
123
|
+
else
|
|
124
|
+
output += value.charAt(index);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return `${output}"`;
|
|
128
|
+
}
|
|
129
|
+
function numberToJcs(value) {
|
|
130
|
+
if (Object.is(value, -0))
|
|
131
|
+
return "0";
|
|
132
|
+
if (!Number.isFinite(value) || (Number.isInteger(value) && !Number.isSafeInteger(value))) {
|
|
133
|
+
throw new TypeError("invalid JSON number");
|
|
134
|
+
}
|
|
135
|
+
return JSON.stringify(value);
|
|
136
|
+
}
|
|
137
|
+
function writeText(value, sink) {
|
|
138
|
+
if (sink instanceof ByteArraySink)
|
|
139
|
+
sink.writeText(value);
|
|
140
|
+
else
|
|
141
|
+
sink.write(new TextEncoder().encode(value));
|
|
142
|
+
}
|
|
143
|
+
function writeAscii(value, sink) {
|
|
144
|
+
if (sink instanceof ByteArraySink)
|
|
145
|
+
sink.writeAscii(value);
|
|
146
|
+
else
|
|
147
|
+
sink.write(new TextEncoder().encode(value || EMPTY));
|
|
148
|
+
}
|
|
149
|
+
function compareUtf16(left, right) {
|
|
150
|
+
return left < right ? -1 : 1;
|
|
151
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
import { failure } from "../domain/operation-result.js";
|
|
3
|
+
import { ByteArraySink } from "./byte-sink.js";
|
|
4
|
+
import { DiagnosticCollector } from "../validation/diagnostic-collector.js";
|
|
5
|
+
export function normalizeToBytes(profile, input, limits) {
|
|
6
|
+
const validated = profile.validate(input, limits);
|
|
7
|
+
if (!validated.ok)
|
|
8
|
+
return validated;
|
|
9
|
+
const sink = new ByteArraySink(limits.maxPayloadBytes, initialCapacity(input));
|
|
10
|
+
try {
|
|
11
|
+
const normalized = profile.normalize(validated.value, sink, limits);
|
|
12
|
+
if (!normalized.ok)
|
|
13
|
+
return normalized;
|
|
14
|
+
if (normalized.value.byteLength !== sink.byteLength)
|
|
15
|
+
return normalizationFailure(limits);
|
|
16
|
+
const bytes = sink.takeBytes();
|
|
17
|
+
return {
|
|
18
|
+
ok: true,
|
|
19
|
+
value: Object.freeze({ bytes, stats: normalized.value }),
|
|
20
|
+
diagnostics: normalized.diagnostics,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return normalizationFailure(limits);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export function normalizeRegisteredToBytes(profile, input, limits) {
|
|
28
|
+
const sink = new ByteArraySink(limits.maxPayloadBytes, initialCapacity(input));
|
|
29
|
+
try {
|
|
30
|
+
const normalized = (profile.normalizeValidated ?? profile.normalize)(input, sink, limits);
|
|
31
|
+
if (!normalized.ok)
|
|
32
|
+
return normalized;
|
|
33
|
+
if (normalized.value.byteLength !== sink.byteLength)
|
|
34
|
+
return normalizationFailure(limits);
|
|
35
|
+
return {
|
|
36
|
+
ok: true,
|
|
37
|
+
value: Object.freeze({ bytes: sink.takeBytes(), stats: normalized.value }),
|
|
38
|
+
diagnostics: normalized.diagnostics,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return normalizationFailure(limits);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function normalizationFailure(limits) {
|
|
46
|
+
const collector = new DiagnosticCollector(limits);
|
|
47
|
+
collector.addCode("NORMALIZATION_FAILED", "normalization");
|
|
48
|
+
return failure(collector.finish());
|
|
49
|
+
}
|
|
50
|
+
function initialCapacity(input) {
|
|
51
|
+
return input instanceof Uint8Array ? input.length : 0;
|
|
52
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
import { failure, success } from "../domain/operation-result.js";
|
|
3
|
+
import { createDiagnostic } from "../validation/diagnostic-collector.js";
|
|
4
|
+
import { jcsProfile } from "./jcs-profile.js";
|
|
5
|
+
import { rawBytesProfile } from "./raw-bytes-profile.js";
|
|
6
|
+
export class ProfileRegistry {
|
|
7
|
+
profiles = new Map();
|
|
8
|
+
constructor(additional = []) {
|
|
9
|
+
this.registerBuiltin(rawBytesProfile);
|
|
10
|
+
this.registerBuiltin(jcsProfile);
|
|
11
|
+
for (const profile of additional) {
|
|
12
|
+
const result = this.register(profile);
|
|
13
|
+
if (!result.ok)
|
|
14
|
+
throw new Error("profile registration conflict");
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
register(profile) {
|
|
18
|
+
const key = profileKey(profile.id, profile.version);
|
|
19
|
+
if (this.profiles.has(key)) {
|
|
20
|
+
return failure([createDiagnostic("PROFILE_VERSION_CONFLICT", "input")]);
|
|
21
|
+
}
|
|
22
|
+
this.profiles.set(key, eraseProfile(profile));
|
|
23
|
+
return success(undefined);
|
|
24
|
+
}
|
|
25
|
+
resolve(id, version) {
|
|
26
|
+
const profile = this.profiles.get(profileKey(id, version));
|
|
27
|
+
if (profile === undefined)
|
|
28
|
+
return failure([createDiagnostic("PROFILE_UNKNOWN", "input")]);
|
|
29
|
+
return success(profile);
|
|
30
|
+
}
|
|
31
|
+
resolveValidated(reference) {
|
|
32
|
+
const profile = this.profiles.get(`${reference.id}\u0000${reference.version}`);
|
|
33
|
+
if (profile === undefined)
|
|
34
|
+
return failure([createDiagnostic("PROFILE_UNKNOWN", "input")]);
|
|
35
|
+
return success(profile);
|
|
36
|
+
}
|
|
37
|
+
registerBuiltin(profile) {
|
|
38
|
+
this.profiles.set(profileKey(profile.id, profile.version), eraseProfile(profile));
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function eraseProfile(profile) {
|
|
42
|
+
return Object.freeze({
|
|
43
|
+
id: profile.id,
|
|
44
|
+
version: profile.version,
|
|
45
|
+
inputKind: profile.inputKind,
|
|
46
|
+
validate: (input, limits) => profile.validate(input, limits),
|
|
47
|
+
normalize: (input, sink, limits) => {
|
|
48
|
+
const validated = profile.validate(input, limits);
|
|
49
|
+
if (!validated.ok)
|
|
50
|
+
return validated;
|
|
51
|
+
return profile.normalize(validated.value, sink, limits);
|
|
52
|
+
},
|
|
53
|
+
normalizeValidated: (input, sink, limits) => {
|
|
54
|
+
// The record service invokes this only after the profile validator succeeds.
|
|
55
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- erased generic at the trusted registry boundary
|
|
56
|
+
return profile.normalize(input, sink, limits);
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
function profileKey(id, version) {
|
|
61
|
+
return `${id.value}\u0000${version.value}`;
|
|
62
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
import { types } from "node:util";
|
|
3
|
+
import { ProfileId } from "../domain/identifiers.js";
|
|
4
|
+
import { ProfileVersion } from "../domain/version.js";
|
|
5
|
+
import { success } from "../domain/operation-result.js";
|
|
6
|
+
import { failureWithCode } from "../validation/result-helpers.js";
|
|
7
|
+
const RAW_PROFILE_ID = ProfileId.fromValidated("dev.noeos.raw-bytes");
|
|
8
|
+
const RAW_PROFILE_VERSION = ProfileVersion.fromValidated("1.0.0");
|
|
9
|
+
export const rawBytesProfile = Object.freeze({
|
|
10
|
+
id: RAW_PROFILE_ID,
|
|
11
|
+
version: RAW_PROFILE_VERSION,
|
|
12
|
+
inputKind: "bytes",
|
|
13
|
+
validate(input, limits) {
|
|
14
|
+
if (types.isProxy(input) || !(input instanceof Uint8Array))
|
|
15
|
+
return failureWithCode("INPUT_TYPE_INVALID", "input");
|
|
16
|
+
if (input.length > limits.maxPayloadBytes)
|
|
17
|
+
return failureWithCode("INPUT_LIMIT_EXCEEDED", "input");
|
|
18
|
+
return success(input);
|
|
19
|
+
},
|
|
20
|
+
normalize(input, sink) {
|
|
21
|
+
sink.write(input);
|
|
22
|
+
return success(Object.freeze({ byteLength: input.length }));
|
|
23
|
+
},
|
|
24
|
+
});
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
/** @public */
|
|
3
|
+
export const BUILTIN_PROFILES = Object.freeze([
|
|
4
|
+
Object.freeze({ id: "dev.noeos.raw-bytes", version: "1.0.0", inputKind: "bytes" }),
|
|
5
|
+
Object.freeze({ id: "dev.noeos.jcs", version: "1.0.0", inputKind: "json" }),
|
|
6
|
+
]);
|