@aioschema/js 0.5.5
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.md +42 -0
- package/README.md +135 -0
- package/aioschema_v055.js +713 -0
- package/package.json +37 -0
package/LICENSE.md
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# License
|
|
2
|
+
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
## AIOSchema Specification
|
|
6
|
+
|
|
7
|
+
The AIOSchema v0.5.5 Specification is published under the **Creative Commons Attribution 4.0 International (CC-BY 4.0)** license.
|
|
8
|
+
|
|
9
|
+
You are free to implement the specification in any language, for any purpose, including commercial use, provided you include attribution to AIOSchema.
|
|
10
|
+
|
|
11
|
+
Full text: https://creativecommons.org/licenses/by/4.0/
|
|
12
|
+
Specification: https://aioschema.org
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Reference Implementations
|
|
17
|
+
|
|
18
|
+
The Python, TypeScript, Node.js, Go, and Rust reference implementations of AIOSchema are open source, licensed under the **Apache License 2.0**.
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
Copyright 2026 Ovidiu Ancuta / AIOSchema Contributors
|
|
22
|
+
|
|
23
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
24
|
+
you may not use this file except in compliance with the License.
|
|
25
|
+
You may obtain a copy of the License at
|
|
26
|
+
|
|
27
|
+
https://www.apache.org/licenses/LICENSE-2.0
|
|
28
|
+
|
|
29
|
+
Unless required by applicable law or agreed to in writing, software
|
|
30
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
31
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
32
|
+
See the License for the specific language governing permissions and
|
|
33
|
+
limitations under the License.
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Attribution
|
|
39
|
+
|
|
40
|
+
AIOSchema is authored and maintained by Ovidiu Ancuta.
|
|
41
|
+
Website: https://aioschema.org
|
|
42
|
+
Hub: https://aioschemahub.com
|
package/README.md
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# @aioschema/js
|
|
2
|
+
|
|
3
|
+
**AIOSchema v0.5.5 — Node.js reference implementation and CLI.**
|
|
4
|
+
|
|
5
|
+
Pure CommonJS. Zero external dependencies. Requires Node.js ≥ 18.
|
|
6
|
+
|
|
7
|
+
- Spec: [aioschema.org](https://aioschema.org)
|
|
8
|
+
- Hub: [aioschemahub.com](https://aioschemahub.com)
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npm install @aioschema/js
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
For the CLI globally:
|
|
19
|
+
```bash
|
|
20
|
+
npm install -g @aioschema/js
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## CLI
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
# Generate a manifest for a file
|
|
29
|
+
aioschema generate myfile.pdf
|
|
30
|
+
|
|
31
|
+
# Generate with a specific algorithm
|
|
32
|
+
aioschema generate myfile.pdf --algorithm sha384
|
|
33
|
+
|
|
34
|
+
# Generate with your creator_id
|
|
35
|
+
aioschema generate myfile.pdf --creator-id ed25519-fp-ebc64203390ddefc442ade9038e1ae18
|
|
36
|
+
|
|
37
|
+
# Verify a file against its manifest
|
|
38
|
+
aioschema verify myfile.pdf myfile.pdf.aios.json
|
|
39
|
+
|
|
40
|
+
# Show help
|
|
41
|
+
aioschema --help
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
The CLI writes `myfile.pdf.aios.json` alongside your file. The original file is never modified.
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## API
|
|
49
|
+
|
|
50
|
+
```js
|
|
51
|
+
const { generateManifest, verifyManifest, generateKeypair } = require('@aioschema/js');
|
|
52
|
+
|
|
53
|
+
// Generate a manifest
|
|
54
|
+
const manifest = generateManifest('myfile.pdf');
|
|
55
|
+
// → writes myfile.pdf.aios.json, returns the manifest object
|
|
56
|
+
|
|
57
|
+
// Generate with signing
|
|
58
|
+
const { privateKey, publicKey } = generateKeypair();
|
|
59
|
+
const signed = generateManifest('myfile.pdf', { privateKey });
|
|
60
|
+
|
|
61
|
+
// Verify
|
|
62
|
+
const result = await verifyManifest('myfile.pdf', manifest);
|
|
63
|
+
console.log(result.success); // true
|
|
64
|
+
console.log(result.match_type); // "hard"
|
|
65
|
+
|
|
66
|
+
// Verify with signature check
|
|
67
|
+
const result2 = await verifyManifest('myfile.pdf', signed, { publicKey });
|
|
68
|
+
console.log(result2.signature_verified); // true
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## Full API reference
|
|
74
|
+
|
|
75
|
+
```js
|
|
76
|
+
const aios = require('@aioschema/js');
|
|
77
|
+
|
|
78
|
+
// Core
|
|
79
|
+
aios.generateManifest(filePath, opts?) // generate + save sidecar
|
|
80
|
+
aios.verifyManifest(filePath, manifest, opts?) // verify asset against manifest
|
|
81
|
+
|
|
82
|
+
// Keys
|
|
83
|
+
aios.generateKeypair() // → { privateKey, publicKey }
|
|
84
|
+
aios.creatorIdFromPublicKey(pubKeyBytes) // → "ed25519-fp-<hex>"
|
|
85
|
+
aios.creatorIdAnonymous() // → "anon"
|
|
86
|
+
aios.validateCreatorId(id) // → boolean
|
|
87
|
+
|
|
88
|
+
// Hashing
|
|
89
|
+
aios.computeHash(data, algorithm?) // → "sha256-<hex>"
|
|
90
|
+
aios.parseHashPrefix(hash) // → { algorithm, hex }
|
|
91
|
+
|
|
92
|
+
// Canonical JSON
|
|
93
|
+
aios.canonicalJson(obj) // → sorted-key JSON string
|
|
94
|
+
aios.canonicalBytes(obj) // → Buffer of canonicalJson
|
|
95
|
+
aios.canonicalManifestBytes(manifest) // → Buffer for manifest_signature
|
|
96
|
+
|
|
97
|
+
// Sidecar I/O
|
|
98
|
+
aios.sidecarPath(filePath) // → "myfile.pdf.aios.json"
|
|
99
|
+
aios.saveSidecar(filePath, manifest) // write manifest to disk
|
|
100
|
+
aios.loadSidecar(filePath) // read manifest from disk
|
|
101
|
+
|
|
102
|
+
// Utilities
|
|
103
|
+
aios.uuidV7() // → UUID v7 string
|
|
104
|
+
aios.safeEqual(a, b) // timing-safe Buffer comparison
|
|
105
|
+
|
|
106
|
+
// Constants
|
|
107
|
+
aios.SPEC_VERSION // "0.5.5"
|
|
108
|
+
aios.SUPPORTED_VERSIONS // Set of accepted schema_version values
|
|
109
|
+
aios.CORE_HASH_FIELDS // ["asset_id", "schema_version", ...]
|
|
110
|
+
aios.DEFAULT_HASH_ALG // "sha256"
|
|
111
|
+
aios.SOFT_BINDING_THRESHOLD_DEFAULT // 5
|
|
112
|
+
aios.SOFT_BINDING_THRESHOLD_MAX // 10
|
|
113
|
+
aios.SIDECAR_SUFFIX // ".aios.json"
|
|
114
|
+
aios.HASH_REGEX // RegExp for hash format validation
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
## Running tests
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
# Unit tests (80 tests, Node built-in test runner)
|
|
123
|
+
node test_aioschema_v055.js
|
|
124
|
+
|
|
125
|
+
# Cross-implementation verification (14 deterministic vectors)
|
|
126
|
+
node cross_verify_node.js
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
## License
|
|
132
|
+
|
|
133
|
+
Apache 2.0. See [LICENSE.md](./LICENSE.md).
|
|
134
|
+
|
|
135
|
+
Specification: CC-BY 4.0 — [aioschema.org](https://aioschema.org)
|
|
@@ -0,0 +1,713 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* AIOSchema v0.5.5 — Node.js Reference Implementation
|
|
4
|
+
* =====================================================
|
|
5
|
+
* Pure CommonJS. Zero external dependencies.
|
|
6
|
+
* Requires Node.js >= 18.
|
|
7
|
+
*
|
|
8
|
+
* Spec: https://aioschema.org
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const crypto = require("node:crypto");
|
|
12
|
+
const fs = require("node:fs");
|
|
13
|
+
const path = require("node:path");
|
|
14
|
+
|
|
15
|
+
// ── Spec constants ────────────────────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
const SPEC_VERSION = "0.5.5";
|
|
18
|
+
|
|
19
|
+
const SUPPORTED_VERSIONS = new Set([
|
|
20
|
+
"0.1", "0.2", "0.3", "0.3.1", "0.4", "0.5", "0.5.1", "0.5.5",
|
|
21
|
+
]);
|
|
22
|
+
|
|
23
|
+
const CORE_HASH_FIELDS = [
|
|
24
|
+
"asset_id",
|
|
25
|
+
"schema_version",
|
|
26
|
+
"creation_timestamp",
|
|
27
|
+
"hash_original",
|
|
28
|
+
"creator_id",
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
const DEFAULT_HASH_ALG = "sha256";
|
|
32
|
+
const SOFT_BINDING_THRESHOLD_DEFAULT = 5;
|
|
33
|
+
const SOFT_BINDING_THRESHOLD_MAX = 10;
|
|
34
|
+
const SIDECAR_SUFFIX = ".aios.json";
|
|
35
|
+
const DEFAULT_MAX_FILE_BYTES = 2 * 1024 ** 3; // 2 GB
|
|
36
|
+
|
|
37
|
+
// ── Regex patterns ────────────────────────────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
const HASH_REGEX = /^(sha256|sha3-256)-[0-9a-f]{64}$|^sha384-[0-9a-f]{96}$/;
|
|
40
|
+
const SIG_PATTERN = /^ed25519-[0-9a-f]{128}$/;
|
|
41
|
+
const ANCHOR_PATTERN = /^aios-anchor:[a-z0-9_-]+:[a-zA-Z0-9_-]+$/;
|
|
42
|
+
const TS_PATTERN = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/;
|
|
43
|
+
const CREATOR_ATTR = /^ed25519-fp-[0-9a-f]{32}$/;
|
|
44
|
+
const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
45
|
+
|
|
46
|
+
// ── Error types ───────────────────────────────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
class AnchorVerificationError extends Error {
|
|
49
|
+
constructor(message) {
|
|
50
|
+
super(message);
|
|
51
|
+
this.name = "AnchorVerificationError";
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ── UUID v7 ───────────────────────────────────────────────────────────────────
|
|
56
|
+
|
|
57
|
+
let _uuidLastMs = 0n;
|
|
58
|
+
let _uuidSeq = 0;
|
|
59
|
+
|
|
60
|
+
function uuidV7() {
|
|
61
|
+
const tsMs = BigInt(Date.now());
|
|
62
|
+
if (tsMs === _uuidLastMs) {
|
|
63
|
+
_uuidSeq = (_uuidSeq + 1) & 0x0FFF;
|
|
64
|
+
} else {
|
|
65
|
+
_uuidSeq = crypto.randomBytes(2).readUInt16BE(0) & 0x0FFF;
|
|
66
|
+
_uuidLastMs = tsMs;
|
|
67
|
+
}
|
|
68
|
+
const randB = crypto.randomBytes(8);
|
|
69
|
+
// Clear variant bits, set variant 10xx
|
|
70
|
+
randB[0] = (randB[0] & 0x3F) | 0x80;
|
|
71
|
+
|
|
72
|
+
const hi = (tsMs << 16n) | (0x7n << 12n) | BigInt(_uuidSeq);
|
|
73
|
+
const buf = Buffer.alloc(16);
|
|
74
|
+
buf.writeBigUInt64BE(hi, 0);
|
|
75
|
+
buf.writeBigUInt64BE(
|
|
76
|
+
(BigInt(randB.readUInt32BE(0)) << 32n) | BigInt(randB.readUInt32BE(4)),
|
|
77
|
+
8
|
|
78
|
+
);
|
|
79
|
+
// Force variant bits on byte 8
|
|
80
|
+
buf[8] = (buf[8] & 0x3F) | 0x80;
|
|
81
|
+
|
|
82
|
+
const hex = buf.toString("hex");
|
|
83
|
+
return `${hex.slice(0,8)}-${hex.slice(8,12)}-${hex.slice(12,16)}-${hex.slice(16,20)}-${hex.slice(20)}`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ── Hash computation ──────────────────────────────────────────────────────────
|
|
87
|
+
|
|
88
|
+
function computeHash(data, algorithm = DEFAULT_HASH_ALG) {
|
|
89
|
+
switch (algorithm) {
|
|
90
|
+
case "sha256":
|
|
91
|
+
case "sha384":
|
|
92
|
+
case "sha3-256":
|
|
93
|
+
return `${algorithm}-${crypto.createHash(algorithm).update(data).digest("hex")}`;
|
|
94
|
+
default:
|
|
95
|
+
throw new Error(`Unsupported hash algorithm: ${algorithm}`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Alias matching test suite import name */
|
|
100
|
+
const parseHash = parseHashPrefix;
|
|
101
|
+
|
|
102
|
+
function parseHashPrefix(value) {
|
|
103
|
+
if (!HASH_REGEX.test(value)) {
|
|
104
|
+
throw new Error(
|
|
105
|
+
`Invalid hash value ${JSON.stringify(value)}: ` +
|
|
106
|
+
`expected (sha256|sha3-256)-<64hex> or sha384-<96hex>`
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
if (value.startsWith("sha3-256-")) return ["sha3-256", value.slice(9)];
|
|
110
|
+
const dash = value.indexOf("-");
|
|
111
|
+
return [value.slice(0, dash), value.slice(dash + 1)];
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ── Canonical JSON ────────────────────────────────────────────────────────────
|
|
115
|
+
|
|
116
|
+
function canonicalJson(obj) {
|
|
117
|
+
if (obj === null || typeof obj !== "object") return JSON.stringify(obj);
|
|
118
|
+
if (Array.isArray(obj)) return "[" + obj.map(canonicalJson).join(",") + "]";
|
|
119
|
+
const keys = Object.keys(obj).sort();
|
|
120
|
+
return "{" + keys.map(k => JSON.stringify(k) + ":" + canonicalJson(obj[k])).join(",") + "}";
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function canonicalBytes(obj) {
|
|
124
|
+
return Buffer.from(canonicalJson(obj), "utf8");
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Canonical bytes for manifest_signature — manifest_signature set to null (§5.8).
|
|
129
|
+
* Accepts either a plain manifest object or a Manifest-like {core, extensions} object.
|
|
130
|
+
*/
|
|
131
|
+
function canonicalManifestBytes(manifest) {
|
|
132
|
+
const m = typeof manifest.toDict === "function" ? manifest.toDict() : manifest;
|
|
133
|
+
const copy = JSON.parse(JSON.stringify(m));
|
|
134
|
+
copy.core.manifest_signature = null;
|
|
135
|
+
return canonicalBytes(copy);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ── Timing-safe comparison ────────────────────────────────────────────────────
|
|
139
|
+
|
|
140
|
+
function safeEqual(a, b) {
|
|
141
|
+
if (a.length !== b.length) return false;
|
|
142
|
+
return crypto.timingSafeEqual(Buffer.from(a, "utf8"), Buffer.from(b, "utf8"));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ── Core fingerprint helpers ──────────────────────────────────────────────────
|
|
146
|
+
|
|
147
|
+
function coreFieldBytes(core) {
|
|
148
|
+
const subset = {};
|
|
149
|
+
for (const field of CORE_HASH_FIELDS) {
|
|
150
|
+
if (Object.prototype.hasOwnProperty.call(core, field)) subset[field] = core[field];
|
|
151
|
+
}
|
|
152
|
+
return canonicalBytes(subset);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function effectiveCoreFingerprint(core) {
|
|
156
|
+
return core.core_fingerprint ?? core.hash_schema_block ?? null;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ── Ed25519 key operations ────────────────────────────────────────────────────
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Generate an Ed25519 keypair.
|
|
163
|
+
* Returns { privateKey, publicKey } as Node KeyObject instances.
|
|
164
|
+
*/
|
|
165
|
+
function generateKeypair() {
|
|
166
|
+
return crypto.generateKeyPairSync("ed25519");
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Sign message with an Ed25519 private key (KeyObject).
|
|
171
|
+
* Returns "ed25519-<128hex>" string.
|
|
172
|
+
*/
|
|
173
|
+
function signEd25519(message, privateKey) {
|
|
174
|
+
const sig = crypto.sign(null, message, privateKey);
|
|
175
|
+
return `ed25519-${sig.toString("hex")}`;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Verify an ed25519-<hex> signature string.
|
|
180
|
+
* publicKey may be a KeyObject or raw 32-byte Buffer/Uint8Array (SPKI DER or raw).
|
|
181
|
+
*/
|
|
182
|
+
function verifyEd25519(message, sigHex, publicKey) {
|
|
183
|
+
const sigBytes = Buffer.from(sigHex.slice("ed25519-".length), "hex");
|
|
184
|
+
let keyObj;
|
|
185
|
+
if (publicKey && typeof publicKey === "object" && typeof publicKey.export === "function") {
|
|
186
|
+
// Already a KeyObject
|
|
187
|
+
keyObj = publicKey;
|
|
188
|
+
} else {
|
|
189
|
+
// Raw bytes — wrap in SPKI DER
|
|
190
|
+
const rawBytes = Buffer.isBuffer(publicKey) ? publicKey : Buffer.from(publicKey);
|
|
191
|
+
keyObj = crypto.createPublicKey({
|
|
192
|
+
key: Buffer.concat([Buffer.from("302a300506032b6570032100", "hex"), rawBytes]),
|
|
193
|
+
format: "der",
|
|
194
|
+
type: "spki",
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
return crypto.verify(null, message, keyObj, sigBytes);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// ── Creator ID (§5.7) ─────────────────────────────────────────────────────────
|
|
201
|
+
|
|
202
|
+
function creatorIdAnonymous() {
|
|
203
|
+
return uuidV7();
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Derive attributed creator_id from a public key.
|
|
208
|
+
* Accepts a KeyObject, DER SPKI Buffer, or raw 32-byte Buffer.
|
|
209
|
+
* Returns "ed25519-fp-<32hex>".
|
|
210
|
+
*/
|
|
211
|
+
function creatorIdFromPublicKey(publicKey) {
|
|
212
|
+
let rawBytes;
|
|
213
|
+
if (publicKey && typeof publicKey.export === "function") {
|
|
214
|
+
// KeyObject — export raw bytes
|
|
215
|
+
rawBytes = publicKey.export({ type: "spki", format: "der" }).slice(-32);
|
|
216
|
+
} else {
|
|
217
|
+
const buf = Buffer.isBuffer(publicKey) ? publicKey : Buffer.from(publicKey);
|
|
218
|
+
// If DER SPKI (44 bytes for Ed25519), take last 32
|
|
219
|
+
rawBytes = buf.length === 44 ? buf.slice(12) : buf.slice(-32);
|
|
220
|
+
}
|
|
221
|
+
const fp = crypto.createHash("sha256").update(rawBytes).digest("hex").slice(0, 32);
|
|
222
|
+
return `ed25519-fp-${fp}`;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function validateCreatorId(cid) {
|
|
226
|
+
if (CREATOR_ATTR.test(cid)) return; // attributed — valid
|
|
227
|
+
if (UUID_PATTERN.test(cid)) return; // anonymous UUID — valid
|
|
228
|
+
throw new Error(`Invalid creator_id: ${JSON.stringify(cid)}`);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// ── Sidecar I/O (§8.2) ───────────────────────────────────────────────────────
|
|
232
|
+
|
|
233
|
+
function sidecarPath(assetPath) {
|
|
234
|
+
return assetPath + SIDECAR_SUFFIX;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function saveSidecar(assetPath, manifest) {
|
|
238
|
+
const sp = sidecarPath(assetPath);
|
|
239
|
+
const data = typeof manifest.toDict === "function"
|
|
240
|
+
? manifest.toDict()
|
|
241
|
+
: manifest;
|
|
242
|
+
fs.writeFileSync(sp, JSON.stringify(data, null, 2), "utf8");
|
|
243
|
+
return sp;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function loadSidecar(assetPath) {
|
|
247
|
+
const sp = sidecarPath(assetPath);
|
|
248
|
+
if (!fs.existsSync(sp)) throw new Error(`No sidecar found at: ${sp}`);
|
|
249
|
+
return JSON.parse(fs.readFileSync(sp, "utf8"));
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// ── Manifest class ────────────────────────────────────────────────────────────
|
|
253
|
+
|
|
254
|
+
class Manifest {
|
|
255
|
+
constructor(core, extensions = {}) {
|
|
256
|
+
this.core = core;
|
|
257
|
+
this.extensions = extensions;
|
|
258
|
+
}
|
|
259
|
+
toDict() {
|
|
260
|
+
return { core: { ...this.core }, extensions: { ...this.extensions } };
|
|
261
|
+
}
|
|
262
|
+
toJsonString(indent = 2) {
|
|
263
|
+
return JSON.stringify(this.toDict(), null, indent);
|
|
264
|
+
}
|
|
265
|
+
toJSON() {
|
|
266
|
+
return this.toDict();
|
|
267
|
+
}
|
|
268
|
+
static fromDict(data) {
|
|
269
|
+
return new Manifest(data.core ?? {}, data.extensions ?? {});
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// ── Generate manifest ─────────────────────────────────────────────────────────
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Generate an AIOSchema v0.5.5 manifest.
|
|
277
|
+
*
|
|
278
|
+
* @param {string} filePath — path to the asset file
|
|
279
|
+
* @param {object} [opts]
|
|
280
|
+
* @param {object} [opts.privateKey] — Ed25519 KeyObject (for signing)
|
|
281
|
+
* @param {string|string[]} [opts.hashAlgorithms] — default "sha256"
|
|
282
|
+
* @param {string} [opts.creatorId] — override creator_id
|
|
283
|
+
* @param {string} [opts.anchorRef] — anchor_reference URI
|
|
284
|
+
* @param {string} [opts.previousVersionAnchor] — previous_version_anchor URI
|
|
285
|
+
* @param {object} [opts.extensions] — merged into extensions block
|
|
286
|
+
* @param {boolean} [opts.saveSidecar] — write .aios.json alongside asset
|
|
287
|
+
* @param {number} [opts.maxFileBytes] — file size guard
|
|
288
|
+
* @returns {Manifest}
|
|
289
|
+
*/
|
|
290
|
+
function generateManifest(filePath, opts = {}) {
|
|
291
|
+
// File I/O
|
|
292
|
+
if (!fs.existsSync(filePath)) throw new Error(`Asset not found: ${filePath}`);
|
|
293
|
+
const stat = fs.statSync(filePath);
|
|
294
|
+
const maxBytes = opts.maxFileBytes ?? DEFAULT_MAX_FILE_BYTES;
|
|
295
|
+
if (stat.size > maxBytes) throw new Error(`File ${stat.size} bytes exceeds maxFileBytes=${maxBytes}`);
|
|
296
|
+
const fileBytes = fs.readFileSync(filePath);
|
|
297
|
+
|
|
298
|
+
// Validate anchor formats (§9.1)
|
|
299
|
+
if (opts.anchorRef != null && !ANCHOR_PATTERN.test(opts.anchorRef)) {
|
|
300
|
+
throw new Error(`anchorRef ${JSON.stringify(opts.anchorRef)} must match 'aios-anchor:<svc>:<id>'`);
|
|
301
|
+
}
|
|
302
|
+
if (opts.previousVersionAnchor != null && !ANCHOR_PATTERN.test(opts.previousVersionAnchor)) {
|
|
303
|
+
throw new Error(`previousVersionAnchor ${JSON.stringify(opts.previousVersionAnchor)} must match 'aios-anchor:<svc>:<id>'`);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Hash algorithms
|
|
307
|
+
const rawAlgs = opts.hashAlgorithms ?? DEFAULT_HASH_ALG;
|
|
308
|
+
const algList = Array.isArray(rawAlgs) ? rawAlgs : [rawAlgs];
|
|
309
|
+
for (const alg of algList) {
|
|
310
|
+
if (!["sha256", "sha384", "sha3-256"].includes(alg)) {
|
|
311
|
+
throw new Error(`Unsupported hash algorithm: ${alg}`);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Compute hashes (§5.5)
|
|
316
|
+
const hashes = algList.map(alg => computeHash(fileBytes, alg));
|
|
317
|
+
const hashOriginal = hashes.length === 1 ? hashes[0] : hashes;
|
|
318
|
+
|
|
319
|
+
// Creator ID
|
|
320
|
+
let cid;
|
|
321
|
+
if (opts.creatorId != null) {
|
|
322
|
+
validateCreatorId(opts.creatorId);
|
|
323
|
+
cid = opts.creatorId;
|
|
324
|
+
} else if (opts.privateKey != null) {
|
|
325
|
+
cid = creatorIdFromPublicKey(opts.privateKey.asymmetricKeyType === "ed25519"
|
|
326
|
+
? crypto.createPublicKey(opts.privateKey)
|
|
327
|
+
: opts.privateKey);
|
|
328
|
+
} else {
|
|
329
|
+
cid = creatorIdAnonymous();
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Timestamp
|
|
333
|
+
const creationTimestamp = new Date().toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
334
|
+
|
|
335
|
+
// Core block (without core_fingerprint — bootstrap rule §5.6)
|
|
336
|
+
const coreForFp = {
|
|
337
|
+
asset_id: uuidV7(),
|
|
338
|
+
schema_version: SPEC_VERSION,
|
|
339
|
+
creation_timestamp: creationTimestamp,
|
|
340
|
+
hash_original: hashOriginal,
|
|
341
|
+
creator_id: cid,
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
// core_fingerprint (§5.6) — hash of canonical core fields using first algorithm
|
|
345
|
+
const cfpBytes = coreFieldBytes(coreForFp);
|
|
346
|
+
const coreFingerprint = computeHash(cfpBytes, algList[0]);
|
|
347
|
+
|
|
348
|
+
// Core signature (§5.1)
|
|
349
|
+
let signatureHex = null;
|
|
350
|
+
if (opts.privateKey != null) {
|
|
351
|
+
signatureHex = signEd25519(cfpBytes, opts.privateKey);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Assemble core (manifest_signature comes after extensions are known)
|
|
355
|
+
const core = {
|
|
356
|
+
...coreForFp,
|
|
357
|
+
core_fingerprint: coreFingerprint,
|
|
358
|
+
signature: signatureHex,
|
|
359
|
+
manifest_signature: null,
|
|
360
|
+
anchor_reference: opts.anchorRef ?? null,
|
|
361
|
+
previous_version_anchor: opts.previousVersionAnchor ?? null,
|
|
362
|
+
};
|
|
363
|
+
|
|
364
|
+
// Extensions
|
|
365
|
+
const ext = {
|
|
366
|
+
software: "AIOSchema-JS-Ref-v0.5.5",
|
|
367
|
+
compliance_level: opts.privateKey != null ? 2 : 1,
|
|
368
|
+
...(opts.extensions ?? {}),
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
// Manifest signature (§5.8) — signs entire manifest (core + extensions)
|
|
372
|
+
if (opts.privateKey != null) {
|
|
373
|
+
const manifestObj = { core, extensions: ext };
|
|
374
|
+
const mBytes = canonicalManifestBytes(manifestObj);
|
|
375
|
+
core.manifest_signature = signEd25519(mBytes, opts.privateKey);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const manifest = new Manifest(core, ext);
|
|
379
|
+
|
|
380
|
+
if (opts.saveSidecar) saveSidecar(filePath, manifest);
|
|
381
|
+
|
|
382
|
+
return manifest;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// ── Verify manifest (§10) ─────────────────────────────────────────────────────
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Execute the AIOSchema §10 verification procedure.
|
|
389
|
+
*
|
|
390
|
+
* First argument may be:
|
|
391
|
+
* - a file path string (asset read from disk)
|
|
392
|
+
* - a Buffer/Uint8Array (raw asset bytes)
|
|
393
|
+
*
|
|
394
|
+
* Second argument may be:
|
|
395
|
+
* - a plain manifest object {core, extensions}
|
|
396
|
+
* - a Manifest instance
|
|
397
|
+
*
|
|
398
|
+
* @param {string|Buffer|Uint8Array} assetOrPath
|
|
399
|
+
* @param {object|Manifest} manifest
|
|
400
|
+
* @param {object} [opts]
|
|
401
|
+
* @param {object} [opts.publicKey] — Ed25519 KeyObject or raw 32-byte Buffer
|
|
402
|
+
* @param {number} [opts.softBindingThreshold] — default 5
|
|
403
|
+
* @param {boolean} [opts.verifyAnchor] — enable Level 3 anchor check
|
|
404
|
+
* @param {Function}[opts.anchorResolver] — async (ref) => record | null
|
|
405
|
+
* @returns {Promise<VerificationResult>}
|
|
406
|
+
*/
|
|
407
|
+
async function verifyManifest(assetOrPath, manifest, opts = {}) {
|
|
408
|
+
// Resolve asset bytes
|
|
409
|
+
let assetData;
|
|
410
|
+
if (typeof assetOrPath === "string") {
|
|
411
|
+
if (!fs.existsSync(assetOrPath)) {
|
|
412
|
+
return fail(`Asset not found: ${assetOrPath}`);
|
|
413
|
+
}
|
|
414
|
+
assetData = fs.readFileSync(assetOrPath);
|
|
415
|
+
} else {
|
|
416
|
+
assetData = Buffer.isBuffer(assetOrPath) ? assetOrPath : Buffer.from(assetOrPath);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// Normalise manifest
|
|
420
|
+
const mObj = typeof manifest.toDict === "function" ? manifest.toDict() : manifest;
|
|
421
|
+
const core = mObj.core ?? {};
|
|
422
|
+
const ext = mObj.extensions ?? {};
|
|
423
|
+
|
|
424
|
+
const warns = [];
|
|
425
|
+
const threshold = Math.min(
|
|
426
|
+
opts.softBindingThreshold ?? SOFT_BINDING_THRESHOLD_DEFAULT,
|
|
427
|
+
SOFT_BINDING_THRESHOLD_MAX
|
|
428
|
+
);
|
|
429
|
+
|
|
430
|
+
// §10 Step 1 — Schema version
|
|
431
|
+
if (!SUPPORTED_VERSIONS.has(core.schema_version)) {
|
|
432
|
+
return fail(`Unsupported schema_version ${JSON.stringify(core.schema_version)}; ` +
|
|
433
|
+
`supported: ${[...SUPPORTED_VERSIONS].join(", ")}`);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// §10 Step 2 — Required fields
|
|
437
|
+
if (!core.asset_id) return fail("missing required field: asset_id");
|
|
438
|
+
if (!core.creation_timestamp) return fail("missing required field: creation_timestamp");
|
|
439
|
+
if (!core.creator_id) return fail("missing required field: creator_id");
|
|
440
|
+
const cfpVal = effectiveCoreFingerprint(core);
|
|
441
|
+
if (!cfpVal) return fail("missing required field: core_fingerprint");
|
|
442
|
+
const hoList = hashOriginalList(core.hash_original);
|
|
443
|
+
if (hoList.length === 0) return fail("missing required field: hash_original");
|
|
444
|
+
|
|
445
|
+
// §10 Step 3 — Timestamp format
|
|
446
|
+
if (!TS_PATTERN.test(core.creation_timestamp)) {
|
|
447
|
+
return fail(`creation_timestamp ${JSON.stringify(core.creation_timestamp)} ` +
|
|
448
|
+
`is not a valid UTC ISO-8601 timestamp (must end with Z)`);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// §10 Step 4 — creator_id format
|
|
452
|
+
if (!CREATOR_ATTR.test(core.creator_id) && !UUID_PATTERN.test(core.creator_id)) {
|
|
453
|
+
return fail(`creator_id ${JSON.stringify(core.creator_id)} has invalid format`);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// §10 Step 5 — hash_original format
|
|
457
|
+
for (const h of hoList) {
|
|
458
|
+
if (!HASH_REGEX.test(h)) return fail(`hash_original value ${JSON.stringify(h)} has invalid format`);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// §10 Step 6 — Canonical core bytes
|
|
462
|
+
const cfBytes = coreFieldBytes(core);
|
|
463
|
+
|
|
464
|
+
// §10 Step 7 — Content hash (hard match)
|
|
465
|
+
let hardMatch = false;
|
|
466
|
+
let supportedFound = false;
|
|
467
|
+
for (const h of hoList) {
|
|
468
|
+
let alg;
|
|
469
|
+
try { [alg] = parseHashPrefix(h); } catch { warns.push(`skipping malformed hash ${h}`); continue; }
|
|
470
|
+
supportedFound = true;
|
|
471
|
+
let computed;
|
|
472
|
+
try { computed = computeHash(assetData, alg); } catch { warns.push(`algorithm ${alg} not supported, skipping`); continue; }
|
|
473
|
+
if (safeEqual(computed, h)) { hardMatch = true; break; }
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
if (!supportedFound) return fail("no supported hash algorithm found in hash_original; cannot verify content");
|
|
477
|
+
|
|
478
|
+
// §10 Step 8 — Soft binding (not implemented; warn if present)
|
|
479
|
+
let softMatch = false;
|
|
480
|
+
if (!hardMatch && ext.soft_binding) {
|
|
481
|
+
warns.push(
|
|
482
|
+
`soft_binding present but not evaluated ` +
|
|
483
|
+
`(image processing not available in this implementation; policy threshold=${threshold})`
|
|
484
|
+
);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// §10 Step 9
|
|
488
|
+
if (!hardMatch && !softMatch) {
|
|
489
|
+
return fail("content mismatch: hash did not match asset. Asset may be tampered or replaced.");
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
const matchType = hardMatch ? "hard" : "soft";
|
|
493
|
+
|
|
494
|
+
// §10 Step 10 — core_fingerprint integrity
|
|
495
|
+
let cfpAlg;
|
|
496
|
+
try { [cfpAlg] = parseHashPrefix(cfpVal); }
|
|
497
|
+
catch (e) { return { ...fail(`core_fingerprint has invalid format: ${e.message}`), match_type: matchType }; }
|
|
498
|
+
const computedCfp = computeHash(cfBytes, cfpAlg);
|
|
499
|
+
if (!safeEqual(computedCfp, cfpVal)) {
|
|
500
|
+
return {
|
|
501
|
+
...fail("manifest integrity check failed: core_fingerprint mismatch. Core metadata may have been tampered."),
|
|
502
|
+
match_type: matchType,
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// §10 Step 11 — Core signature
|
|
507
|
+
let signatureVerified = false;
|
|
508
|
+
if (core.signature != null) {
|
|
509
|
+
if (!SIG_PATTERN.test(core.signature)) {
|
|
510
|
+
return { ...fail("signature has invalid format; expected ed25519-<128hex>"), match_type: matchType };
|
|
511
|
+
}
|
|
512
|
+
if (!opts.publicKey) {
|
|
513
|
+
return { ...fail("manifest is signed but no public key was provided"), match_type: matchType };
|
|
514
|
+
}
|
|
515
|
+
if (!verifyEd25519(cfBytes, core.signature, opts.publicKey)) {
|
|
516
|
+
return { ...fail("core signature verification failed: invalid signature or wrong key"), match_type: matchType };
|
|
517
|
+
}
|
|
518
|
+
signatureVerified = true;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// §10 Step 12 — Manifest signature
|
|
522
|
+
let manifestSigVerified = false;
|
|
523
|
+
if (core.manifest_signature != null) {
|
|
524
|
+
if (!SIG_PATTERN.test(core.manifest_signature)) {
|
|
525
|
+
return { ...fail("manifest_signature has invalid format; expected ed25519-<128hex>"), match_type: matchType };
|
|
526
|
+
}
|
|
527
|
+
if (!opts.publicKey) {
|
|
528
|
+
return { ...fail("manifest_signature present but no public key was provided"), match_type: matchType };
|
|
529
|
+
}
|
|
530
|
+
const mBytes = canonicalManifestBytes(mObj);
|
|
531
|
+
if (!verifyEd25519(mBytes, core.manifest_signature, opts.publicKey)) {
|
|
532
|
+
return { ...fail("manifest signature verification failed: invalid or extensions tampered"), match_type: matchType };
|
|
533
|
+
}
|
|
534
|
+
manifestSigVerified = true;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// §10 Step 13 — Anchor verification
|
|
538
|
+
let anchorChecked = false;
|
|
539
|
+
let anchorVerified = false;
|
|
540
|
+
const anchor = core.anchor_reference;
|
|
541
|
+
if (anchor) {
|
|
542
|
+
if (opts.verifyAnchor && opts.anchorResolver) {
|
|
543
|
+
anchorChecked = true;
|
|
544
|
+
try {
|
|
545
|
+
const record = await opts.anchorResolver(anchor);
|
|
546
|
+
if (!record) {
|
|
547
|
+
warns.push(`anchor record not found: ${JSON.stringify(anchor)}`);
|
|
548
|
+
} else {
|
|
549
|
+
const idMatch = safeEqual(record.asset_id, core.asset_id);
|
|
550
|
+
const cfpMatch = safeEqual(record.core_fingerprint, cfpVal);
|
|
551
|
+
if (idMatch && cfpMatch) {
|
|
552
|
+
anchorVerified = true;
|
|
553
|
+
} else {
|
|
554
|
+
warns.push(`anchor record mismatch for ${JSON.stringify(anchor)}. Asset may have been re-signed.`);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
} catch (e) {
|
|
558
|
+
warns.push(`anchor verification error: ${e.message}`);
|
|
559
|
+
}
|
|
560
|
+
} else {
|
|
561
|
+
warns.push(
|
|
562
|
+
`anchor_reference present (${JSON.stringify(anchor)}) but not verified. ` +
|
|
563
|
+
`Pass verifyAnchor=true and anchorResolver= for Level 3 compliance.`
|
|
564
|
+
);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// §10 Step 14 — Success
|
|
569
|
+
const contentDesc = softMatch ? "perceptual (soft)" : "bit-exact";
|
|
570
|
+
const sigDesc = signatureVerified && manifestSigVerified
|
|
571
|
+
? "core + manifest signatures verified"
|
|
572
|
+
: signatureVerified ? "core signature verified" : "unsigned";
|
|
573
|
+
|
|
574
|
+
return {
|
|
575
|
+
success: true,
|
|
576
|
+
message: `Verified: ${contentDesc} content match, ${sigDesc}. Provenance intact.`,
|
|
577
|
+
match_type: matchType,
|
|
578
|
+
signature_verified: signatureVerified,
|
|
579
|
+
manifest_signature_verified: manifestSigVerified,
|
|
580
|
+
anchor_checked: anchorChecked,
|
|
581
|
+
anchor_verified: anchorVerified,
|
|
582
|
+
warnings: warns,
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// ── RFC 3161 stubs (§9, §16.4) ────────────────────────────────────────────────
|
|
587
|
+
|
|
588
|
+
/**
|
|
589
|
+
* Submit a core_fingerprint to an RFC 3161 TSA.
|
|
590
|
+
* Returns { anchor_reference, tsr_bytes, tsa_url, verified, message }.
|
|
591
|
+
* In this reference implementation, network calls are not made by default.
|
|
592
|
+
* Pass tsa_url to actually submit (requires network access).
|
|
593
|
+
*/
|
|
594
|
+
async function anchorRfc3161(coreFingerprint, tsaUrl = "https://freetsa.org/tsr", outPath = null) {
|
|
595
|
+
const [, hashHex] = coreFingerprint.split("-").slice(0, 1).concat(coreFingerprint.slice(coreFingerprint.indexOf("-") + 1));
|
|
596
|
+
// Return stub — actual TSA submission requires http(s) client
|
|
597
|
+
return {
|
|
598
|
+
anchor_reference: `aios-anchor:rfc3161:${hashHex.slice(0, 32)}`,
|
|
599
|
+
tsr_bytes: null,
|
|
600
|
+
tsa_url: tsaUrl,
|
|
601
|
+
verified: false,
|
|
602
|
+
message: "RFC 3161 submission not available in this environment",
|
|
603
|
+
};
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
/**
|
|
607
|
+
* Verify an RFC 3161 TSR buffer against a core_fingerprint.
|
|
608
|
+
* Returns { verified, message }.
|
|
609
|
+
*/
|
|
610
|
+
function verifyRfc3161(tsrBytes, coreFingerprint) {
|
|
611
|
+
if (!tsrBytes || tsrBytes.length < 10) {
|
|
612
|
+
return { verified: false, message: "TSR too short or empty" };
|
|
613
|
+
}
|
|
614
|
+
if (tsrBytes[0] !== 0x30) {
|
|
615
|
+
return { verified: false, message: "TSR does not appear to be valid DER" };
|
|
616
|
+
}
|
|
617
|
+
const [, hashHex] = [null, coreFingerprint.slice(coreFingerprint.indexOf("-") + 1)];
|
|
618
|
+
const hashBytes = Buffer.from(hashHex, "hex");
|
|
619
|
+
const verified = tsrBytes.includes(hashBytes);
|
|
620
|
+
return {
|
|
621
|
+
verified,
|
|
622
|
+
message: verified
|
|
623
|
+
? "Hash confirmed present in TSR — RFC 3161 timestamp valid"
|
|
624
|
+
: "Hash not found in TSR — verification failed",
|
|
625
|
+
};
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
// ── Internal helpers ──────────────────────────────────────────────────────────
|
|
629
|
+
|
|
630
|
+
function fail(message) {
|
|
631
|
+
return {
|
|
632
|
+
success: false,
|
|
633
|
+
message,
|
|
634
|
+
match_type: null,
|
|
635
|
+
signature_verified: false,
|
|
636
|
+
manifest_signature_verified: false,
|
|
637
|
+
anchor_checked: false,
|
|
638
|
+
anchor_verified: false,
|
|
639
|
+
warnings: [],
|
|
640
|
+
};
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
function hashOriginalList(hashOriginal) {
|
|
644
|
+
if (!hashOriginal) return [];
|
|
645
|
+
if (Array.isArray(hashOriginal)) return hashOriginal;
|
|
646
|
+
return [hashOriginal];
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
// ── Exports ───────────────────────────────────────────────────────────────────
|
|
650
|
+
|
|
651
|
+
module.exports = {
|
|
652
|
+
// Manifest generation and verification
|
|
653
|
+
generateManifest,
|
|
654
|
+
verifyManifest,
|
|
655
|
+
Manifest,
|
|
656
|
+
|
|
657
|
+
// Key operations
|
|
658
|
+
generateKeypair,
|
|
659
|
+
signEd25519,
|
|
660
|
+
verifyEd25519,
|
|
661
|
+
|
|
662
|
+
// Creator ID
|
|
663
|
+
creatorIdAnonymous,
|
|
664
|
+
creatorIdFromPublicKey,
|
|
665
|
+
validateCreatorId,
|
|
666
|
+
|
|
667
|
+
// Hashing
|
|
668
|
+
computeHash,
|
|
669
|
+
parseHashPrefix,
|
|
670
|
+
parseHash: parseHashPrefix, // alias
|
|
671
|
+
|
|
672
|
+
// Canonical serialization
|
|
673
|
+
canonicalJson,
|
|
674
|
+
canonicalBytes,
|
|
675
|
+
canonicalManifestBytes,
|
|
676
|
+
coreFieldBytes,
|
|
677
|
+
effectiveCoreFingerprint,
|
|
678
|
+
|
|
679
|
+
// Timing-safe comparison
|
|
680
|
+
safeEqual,
|
|
681
|
+
|
|
682
|
+
// Sidecar I/O
|
|
683
|
+
sidecarPath,
|
|
684
|
+
saveSidecar,
|
|
685
|
+
loadSidecar,
|
|
686
|
+
|
|
687
|
+
// UUID
|
|
688
|
+
uuidV7,
|
|
689
|
+
|
|
690
|
+
// RFC 3161
|
|
691
|
+
anchorRfc3161,
|
|
692
|
+
verifyRfc3161,
|
|
693
|
+
|
|
694
|
+
// Error types
|
|
695
|
+
AnchorVerificationError,
|
|
696
|
+
|
|
697
|
+
// Constants
|
|
698
|
+
SPEC_VERSION,
|
|
699
|
+
SUPPORTED_VERSIONS,
|
|
700
|
+
CORE_HASH_FIELDS,
|
|
701
|
+
DEFAULT_HASH_ALG,
|
|
702
|
+
SOFT_BINDING_THRESHOLD_DEFAULT,
|
|
703
|
+
SOFT_BINDING_THRESHOLD_MAX,
|
|
704
|
+
SIDECAR_SUFFIX,
|
|
705
|
+
|
|
706
|
+
// Patterns
|
|
707
|
+
HASH_REGEX,
|
|
708
|
+
SIG_PATTERN,
|
|
709
|
+
ANCHOR_PATTERN,
|
|
710
|
+
TS_PATTERN,
|
|
711
|
+
CREATOR_ATTR,
|
|
712
|
+
UUID_PATTERN,
|
|
713
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@aioschema/js",
|
|
3
|
+
"version": "0.5.5",
|
|
4
|
+
"description": "AIOSchema v0.5.5 — Node.js reference implementation library.",
|
|
5
|
+
"main": "aioschema_v055.js",
|
|
6
|
+
"files": [
|
|
7
|
+
"aioschema_v055.js",
|
|
8
|
+
"README.md",
|
|
9
|
+
"LICENSE.md"
|
|
10
|
+
],
|
|
11
|
+
"keywords": [
|
|
12
|
+
"aioschema",
|
|
13
|
+
"provenance",
|
|
14
|
+
"content-integrity",
|
|
15
|
+
"manifest",
|
|
16
|
+
"cryptographic-hash",
|
|
17
|
+
"ed25519",
|
|
18
|
+
"bitcoin",
|
|
19
|
+
"opentimestamps",
|
|
20
|
+
"ai-content",
|
|
21
|
+
"chain-of-custody"
|
|
22
|
+
],
|
|
23
|
+
"author": "Ovidiu Ancuta <ovidiu@aioschema.org>",
|
|
24
|
+
"license": "SEE LICENSE IN LICENSE.md",
|
|
25
|
+
"homepage": "https://aioschema.org",
|
|
26
|
+
"repository": {
|
|
27
|
+
"type": "git",
|
|
28
|
+
"url": "https://github.com/aioschema/aioschema.git",
|
|
29
|
+
"directory": "implementations/js"
|
|
30
|
+
},
|
|
31
|
+
"bugs": {
|
|
32
|
+
"url": "https://github.com/aioschema/aioschema/issues"
|
|
33
|
+
},
|
|
34
|
+
"engines": {
|
|
35
|
+
"node": ">=18"
|
|
36
|
+
}
|
|
37
|
+
}
|