chore: normalize to bun monorepo conventions
CI / check (push) Failing after 43s

Applied monorepo normalization:
- Updated TypeScript to use composite project references with NodeNext
- Configured Biome for linting and formatting
- Standardized package.json metadata across all packages
- Set up changesets for version management and npm publishing
- Added vitest test infrastructure to all packages
- Created Gitea Actions CI pipeline
- Added solve-issue workflow

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-29 04:39:36 +00:00
parent 1ea058a7a6
commit 064c9afa1e
46 changed files with 1166 additions and 20 deletions
+2 -1
View File
@@ -6,7 +6,8 @@
"json-cas": "./src/index.ts"
},
"scripts": {
"test": "bun test",
"test": "vitest run --passWithNoTests",
"test:ci": "vitest run --passWithNoTests",
"prepublishOnly": "echo '请用 bun run release 从根目录发版' && exit 1"
},
"dependencies": {
+5 -1
View File
@@ -4,5 +4,9 @@
"rootDir": "src",
"outDir": "dist"
},
"include": ["src"]
"include": ["src"],
"references": [
{ "path": "../json-cas" },
{ "path": "../json-cas-fs" }
]
}
+7
View File
@@ -0,0 +1,7 @@
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
include: ["src/__tests__/**/*.test.ts"],
},
});
+2 -1
View File
@@ -15,7 +15,8 @@
"src"
],
"scripts": {
"test": "bun test",
"test": "vitest run --passWithNoTests",
"test:ci": "vitest run --passWithNoTests",
"prepublishOnly": "echo '请用 bun run release 从根目录发版' && exit 1"
},
"dependencies": {
+2
View File
@@ -0,0 +1,2 @@
export { createFsStore } from "./store.js";
//# sourceMappingURL=index.d.ts.map
+2
View File
@@ -0,0 +1,2 @@
export { createFsStore } from "./store.js";
//# sourceMappingURL=index.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"index.js","sourceRoot":"","sources":["index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC"}
+3
View File
@@ -0,0 +1,3 @@
import type { BootstrapCapableStore } from "@uncaged/json-cas";
export declare function createFsStore(dir: string): BootstrapCapableStore;
//# sourceMappingURL=store.d.ts.map
+138
View File
@@ -0,0 +1,138 @@
import { appendFileSync, existsSync, mkdirSync, readdirSync, readFileSync, renameSync, writeFileSync, } from "node:fs";
import { join } from "node:path";
import { BOOTSTRAP_STORE, cborEncode, computeHash, computeSelfHash, } from "@uncaged/json-cas";
import { decode } from "cborg";
const INDEX_DIR = "_index";
function loadDir(dir, data) {
let entries;
try {
entries = readdirSync(dir);
}
catch {
return;
}
for (const name of entries) {
if (!name.endsWith(".bin"))
continue;
const hash = name.slice(0, -4);
try {
const buf = readFileSync(join(dir, name));
const node = decode(new Uint8Array(buf));
data.set(hash, node);
}
catch {
// skip corrupted files
}
}
}
function parseIndexFile(content) {
if (content.length === 0)
return [];
return content.split("\n").filter((line) => line.length > 0);
}
function loadTypeIndex(indexDir) {
const typeIndex = new Map();
let entries;
try {
entries = readdirSync(indexDir);
}
catch {
return typeIndex;
}
for (const typeHash of entries) {
try {
const content = readFileSync(join(indexDir, typeHash), "utf8");
typeIndex.set(typeHash, parseIndexFile(content));
}
catch {
// skip unreadable index files
}
}
return typeIndex;
}
function buildTypeIndexFromNodes(data) {
const typeIndex = new Map();
for (const [hash, node] of data) {
const list = typeIndex.get(node.type) ?? [];
list.push(hash);
typeIndex.set(node.type, list);
}
return typeIndex;
}
function writeTypeIndex(indexDir, typeIndex) {
mkdirSync(indexDir, { recursive: true });
for (const [typeHash, hashes] of typeIndex) {
const body = hashes.length > 0 ? `${hashes.join("\n")}\n` : "";
writeFileSync(join(indexDir, typeHash), body, "utf8");
}
}
function loadOrMigrateTypeIndex(dir, data) {
const indexDir = join(dir, INDEX_DIR);
if (!existsSync(indexDir)) {
const typeIndex = buildTypeIndexFromNodes(data);
if (typeIndex.size > 0) {
writeTypeIndex(indexDir, typeIndex);
}
return typeIndex;
}
return loadTypeIndex(indexDir);
}
function appendToTypeIndex(indexDir, typeIndex, type, hash) {
mkdirSync(indexDir, { recursive: true });
appendFileSync(join(indexDir, type), `${hash}\n`, "utf8");
const list = typeIndex.get(type) ?? [];
list.push(hash);
typeIndex.set(type, list);
}
export function createFsStore(dir) {
const data = new Map();
loadDir(dir, data);
const indexDir = join(dir, INDEX_DIR);
const typeIndex = loadOrMigrateTypeIndex(dir, data);
async function putSelfReferencing(payload) {
const hash = await computeSelfHash(payload);
if (!data.has(hash)) {
const node = { type: hash, payload, timestamp: Date.now() };
data.set(hash, node);
mkdirSync(dir, { recursive: true });
const tmp = join(dir, `${hash}.tmp`);
const dest = join(dir, `${hash}.bin`);
writeFileSync(tmp, cborEncode({ type: hash, payload, timestamp: node.timestamp }));
renameSync(tmp, dest);
appendToTypeIndex(indexDir, typeIndex, hash, hash);
}
return hash;
}
const store = {
async put(typeHash, payload) {
const hash = await computeHash(typeHash, payload);
if (!data.has(hash)) {
const node = {
type: typeHash,
payload,
timestamp: Date.now(),
};
data.set(hash, node);
mkdirSync(dir, { recursive: true });
const tmp = join(dir, `${hash}.tmp`);
const dest = join(dir, `${hash}.bin`);
writeFileSync(tmp, cborEncode({ type: typeHash, payload, timestamp: node.timestamp }));
renameSync(tmp, dest);
appendToTypeIndex(indexDir, typeIndex, typeHash, hash);
}
return hash;
},
get(hash) {
return data.get(hash) ?? null;
},
has(hash) {
return data.has(hash);
},
listByType(typeHash) {
return typeIndex.get(typeHash) ?? [];
},
[BOOTSTRAP_STORE]: putSelfReferencing,
};
return store;
}
//# sourceMappingURL=store.js.map
File diff suppressed because one or more lines are too long
+7
View File
@@ -0,0 +1,7 @@
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
include: ["src/__tests__/**/*.test.ts"],
},
});
+2 -1
View File
@@ -15,7 +15,8 @@
"src"
],
"scripts": {
"test": "bun test",
"test": "vitest run --passWithNoTests",
"test:ci": "vitest run --passWithNoTests",
"prepublishOnly": "echo '请用 bun run release 从根目录发版' && exit 1"
},
"dependencies": {
+8
View File
@@ -0,0 +1,8 @@
import type { Hash, Store } from "./types.js";
/** @internal Store implementations attach this for bootstrap() only. */
export declare const BOOTSTRAP_STORE: unique symbol;
export type BootstrapCapableStore = Store & {
[BOOTSTRAP_STORE](payload: unknown): Promise<Hash>;
};
export declare function isBootstrapCapableStore(store: Store): store is BootstrapCapableStore;
//# sourceMappingURL=bootstrap-capable.d.ts.map
+6
View File
@@ -0,0 +1,6 @@
/** @internal Store implementations attach this for bootstrap() only. */
export const BOOTSTRAP_STORE = Symbol.for("@uncaged/json-cas/bootstrap-store");
export function isBootstrapCapableStore(store) {
return (typeof store[BOOTSTRAP_STORE] === "function");
}
//# sourceMappingURL=bootstrap-capable.js.map
@@ -0,0 +1 @@
{"version":3,"file":"bootstrap-capable.js","sourceRoot":"","sources":["bootstrap-capable.ts"],"names":[],"mappings":"AAEA,wEAAwE;AACxE,MAAM,CAAC,MAAM,eAAe,GAAG,MAAM,CAAC,GAAG,CAAC,mCAAmC,CAAC,CAAC;AAM/E,MAAM,UAAU,uBAAuB,CACrC,KAAY;IAEZ,OAAO,CACL,OAAQ,KAA+B,CAAC,eAAe,CAAC,KAAK,UAAU,CACxE,CAAC;AACJ,CAAC"}
+8
View File
@@ -0,0 +1,8 @@
import type { Hash, Store } from "./types.js";
/**
* Write the meta-schema seed node into the store.
* The returned hash equals the node's own type field (self-referencing).
* Idempotent: calling bootstrap multiple times returns the same hash.
*/
export declare function bootstrap(store: Store): Promise<Hash>;
//# sourceMappingURL=bootstrap.d.ts.map
+70
View File
@@ -0,0 +1,70 @@
import { BOOTSTRAP_STORE, isBootstrapCapableStore, } from "./bootstrap-capable.js";
const JSON_SCHEMA_TYPES = [
"string",
"number",
"integer",
"boolean",
"object",
"array",
"null",
];
/**
* Self-describing JSON Schema meta-schema for the supported schema subset.
* Stored as the bootstrap node's payload; its hash equals the node's type field.
*/
const BOOTSTRAP_PAYLOAD = {
type: "object",
additionalProperties: false,
description: "json-cas JSON Schema meta-schema",
properties: {
type: {
anyOf: [
{ type: "string", enum: [...JSON_SCHEMA_TYPES] },
{
type: "array",
items: { type: "string", enum: [...JSON_SCHEMA_TYPES] },
},
],
},
properties: {
type: "object",
additionalProperties: { type: "object", additionalProperties: false },
},
required: {
type: "array",
items: { type: "string" },
},
additionalProperties: {
anyOf: [
{ type: "boolean" },
{ type: "object", additionalProperties: false },
],
},
anyOf: {
type: "array",
items: { type: "object", additionalProperties: false },
},
oneOf: {
type: "array",
items: { type: "object", additionalProperties: false },
},
items: { type: "object", additionalProperties: false },
format: { type: "string" },
title: { type: "string" },
enum: { type: "array" },
const: {},
description: { type: "string" },
},
};
/**
* Write the meta-schema seed node into the store.
* The returned hash equals the node's own type field (self-referencing).
* Idempotent: calling bootstrap multiple times returns the same hash.
*/
export async function bootstrap(store) {
if (!isBootstrapCapableStore(store)) {
throw new Error("Store does not support bootstrap");
}
return store[BOOTSTRAP_STORE](BOOTSTRAP_PAYLOAD);
}
//# sourceMappingURL=bootstrap.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"bootstrap.js","sourceRoot":"","sources":["bootstrap.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,eAAe,EACf,uBAAuB,GACxB,MAAM,wBAAwB,CAAC;AAGhC,MAAM,iBAAiB,GAAG;IACxB,QAAQ;IACR,QAAQ;IACR,SAAS;IACT,SAAS;IACT,QAAQ;IACR,OAAO;IACP,MAAM;CACE,CAAC;AAEX;;;GAGG;AACH,MAAM,iBAAiB,GAAG;IACxB,IAAI,EAAE,QAAQ;IACd,oBAAoB,EAAE,KAAK;IAC3B,WAAW,EAAE,kCAAkC;IAC/C,UAAU,EAAE;QACV,IAAI,EAAE;YACJ,KAAK,EAAE;gBACL,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,GAAG,iBAAiB,CAAC,EAAE;gBAChD;oBACE,IAAI,EAAE,OAAO;oBACb,KAAK,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,GAAG,iBAAiB,CAAC,EAAE;iBACxD;aACF;SACF;QACD,UAAU,EAAE;YACV,IAAI,EAAE,QAAQ;YACd,oBAAoB,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,oBAAoB,EAAE,KAAK,EAAE;SACtE;QACD,QAAQ,EAAE;YACR,IAAI,EAAE,OAAO;YACb,KAAK,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE;SAC1B;QACD,oBAAoB,EAAE;YACpB,KAAK,EAAE;gBACL,EAAE,IAAI,EAAE,SAAS,EAAE;gBACnB,EAAE,IAAI,EAAE,QAAQ,EAAE,oBAAoB,EAAE,KAAK,EAAE;aAChD;SACF;QACD,KAAK,EAAE;YACL,IAAI,EAAE,OAAO;YACb,KAAK,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,oBAAoB,EAAE,KAAK,EAAE;SACvD;QACD,KAAK,EAAE;YACL,IAAI,EAAE,OAAO;YACb,KAAK,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,oBAAoB,EAAE,KAAK,EAAE;SACvD;QACD,KAAK,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,oBAAoB,EAAE,KAAK,EAAE;QACtD,MAAM,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE;QAC1B,KAAK,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE;QACzB,IAAI,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE;QACvB,KAAK,EAAE,EAAE;QACT,WAAW,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE;KAChC;CACO,CAAC;AAEX;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,SAAS,CAAC,KAAY;IAC1C,IAAI,CAAC,uBAAuB,CAAC,KAAK,CAAC,EAAE,CAAC;QACpC,MAAM,IAAI,KAAK,CAAC,kCAAkC,CAAC,CAAC;IACtD,CAAC;IACD,OAAO,KAAK,CAAC,eAAe,CAAC,CAAC,iBAAiB,CAAC,CAAC;AACnD,CAAC"}
+6
View File
@@ -0,0 +1,6 @@
/**
* Deterministic CBOR encoding per RFC 8949 (bytewise-sorted map keys,
* smallest-possible integer sizes).
*/
export declare function cborEncode(value: unknown): Uint8Array;
//# sourceMappingURL=cbor.d.ts.map
+9
View File
@@ -0,0 +1,9 @@
import { encode, rfc8949EncodeOptions } from "cborg";
/**
* Deterministic CBOR encoding per RFC 8949 (bytewise-sorted map keys,
* smallest-possible integer sizes).
*/
export function cborEncode(value) {
return encode(value, rfc8949EncodeOptions);
}
//# sourceMappingURL=cbor.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"cbor.js","sourceRoot":"","sources":["cbor.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,oBAAoB,EAAE,MAAM,OAAO,CAAC;AAErD;;;GAGG;AACH,MAAM,UAAU,UAAU,CAAC,KAAc;IACvC,OAAO,MAAM,CAAC,KAAK,EAAE,oBAAoB,CAAC,CAAC;AAC7C,CAAC"}
+12
View File
@@ -0,0 +1,12 @@
import type { Hash } from "./types.js";
/**
* hash = XXH64(utf8(typeHash) ++ CBOR_deterministic(payload))
* Used for all normal nodes.
*/
export declare function computeHash(typeHash: Hash, payload: unknown): Promise<Hash>;
/**
* hash = XXH64(CBOR_deterministic(payload))
* Used for self-referencing (bootstrap) nodes where type = hash.
*/
export declare function computeSelfHash(payload: unknown): Promise<Hash>;
//# sourceMappingURL=hash.d.ts.map
+59
View File
@@ -0,0 +1,59 @@
import xxhashFactory from "xxhash-wasm";
import { cborEncode } from "./cbor.js";
/** Crockford Base32 symbol table (32 characters, indices 0–31). */
const CROCKFORD = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
/** Encode a u64 BigInt as a 13-character Crockford Base32 string. */
function u64ToCrockford(n) {
let result = "";
let x = n;
for (let i = 0; i < 13; i++) {
result = CROCKFORD[Number(x & 31n)] + result;
x >>= 5n;
}
return result;
}
/** Encode an ASCII string as bytes without TextEncoder (all hashes are ASCII). */
function asciiToBytes(s) {
const bytes = new Uint8Array(s.length);
for (let i = 0; i < s.length; i++) {
bytes[i] = s.charCodeAt(i);
}
return bytes;
}
function concatBytes(a, b) {
const out = new Uint8Array(a.length + b.length);
out.set(a);
out.set(b, a.length);
return out;
}
let _instance = null;
let _pending = null;
async function getInstance() {
if (_instance !== null)
return _instance;
if (_pending === null) {
_pending = xxhashFactory().then((api) => {
_instance = api;
return api;
});
}
return _pending;
}
/**
* hash = XXH64(utf8(typeHash) ++ CBOR_deterministic(payload))
* Used for all normal nodes.
*/
export async function computeHash(typeHash, payload) {
const api = await getInstance();
const input = concatBytes(asciiToBytes(typeHash), cborEncode(payload));
return u64ToCrockford(api.h64Raw(input));
}
/**
* hash = XXH64(CBOR_deterministic(payload))
* Used for self-referencing (bootstrap) nodes where type = hash.
*/
export async function computeSelfHash(payload) {
const api = await getInstance();
return u64ToCrockford(api.h64Raw(cborEncode(payload)));
}
//# sourceMappingURL=hash.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"hash.js","sourceRoot":"","sources":["hash.ts"],"names":[],"mappings":"AACA,OAAO,aAAa,MAAM,aAAa,CAAC;AAExC,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAGvC,mEAAmE;AACnE,MAAM,SAAS,GAAG,kCAAkC,CAAC;AAErD,qEAAqE;AACrE,SAAS,cAAc,CAAC,CAAS;IAC/B,IAAI,MAAM,GAAG,EAAE,CAAC;IAChB,IAAI,CAAC,GAAG,CAAC,CAAC;IACV,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,EAAE,EAAE,CAAC;QAC5B,MAAM,GAAG,SAAS,CAAC,MAAM,CAAC,CAAC,GAAG,GAAG,CAAC,CAAC,GAAG,MAAM,CAAC;QAC7C,CAAC,KAAK,EAAE,CAAC;IACX,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,kFAAkF;AAClF,SAAS,YAAY,CAAC,CAAS;IAC7B,MAAM,KAAK,GAAG,IAAI,UAAU,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC;IACvC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QAClC,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC;IAC7B,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,SAAS,WAAW,CAAC,CAAa,EAAE,CAAa;IAC/C,MAAM,GAAG,GAAG,IAAI,UAAU,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,MAAM,CAAC,CAAC;IAChD,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;IACX,GAAG,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC;IACrB,OAAO,GAAG,CAAC;AACb,CAAC;AAED,IAAI,SAAS,GAAqB,IAAI,CAAC;AACvC,IAAI,QAAQ,GAA8B,IAAI,CAAC;AAE/C,KAAK,UAAU,WAAW;IACxB,IAAI,SAAS,KAAK,IAAI;QAAE,OAAO,SAAS,CAAC;IACzC,IAAI,QAAQ,KAAK,IAAI,EAAE,CAAC;QACtB,QAAQ,GAAG,aAAa,EAAE,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE;YACtC,SAAS,GAAG,GAAG,CAAC;YAChB,OAAO,GAAG,CAAC;QACb,CAAC,CAAC,CAAC;IACL,CAAC;IACD,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,WAAW,CAC/B,QAAc,EACd,OAAgB;IAEhB,MAAM,GAAG,GAAG,MAAM,WAAW,EAAE,CAAC;IAChC,MAAM,KAAK,GAAG,WAAW,CAAC,YAAY,CAAC,QAAQ,CAAC,EAAE,UAAU,CAAC,OAAO,CAAC,CAAC,CAAC;IACvE,OAAO,cAAc,CAAC,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;AAC3C,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,OAAgB;IACpD,MAAM,GAAG,GAAG,MAAM,WAAW,EAAE,CAAC;IAChC,OAAO,cAAc,CAAC,GAAG,CAAC,MAAM,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;AACzD,CAAC"}
+11
View File
@@ -0,0 +1,11 @@
export { bootstrap } from "./bootstrap.js";
export type { BootstrapCapableStore } from "./bootstrap-capable.js";
export { BOOTSTRAP_STORE } from "./bootstrap-capable.js";
export { cborEncode } from "./cbor.js";
export { computeHash, computeSelfHash } from "./hash.js";
export type { JSONSchema } from "./schema.js";
export { getSchema, putSchema, refs, SchemaValidationError, validate, walk, } from "./schema.js";
export { createMemoryStore } from "./store.js";
export type { CasNode, Hash, Store } from "./types.js";
export { verify } from "./verify.js";
//# sourceMappingURL=index.d.ts.map
+8
View File
@@ -0,0 +1,8 @@
export { bootstrap } from "./bootstrap.js";
export { BOOTSTRAP_STORE } from "./bootstrap-capable.js";
export { cborEncode } from "./cbor.js";
export { computeHash, computeSelfHash } from "./hash.js";
export { getSchema, putSchema, refs, SchemaValidationError, validate, walk, } from "./schema.js";
export { createMemoryStore } from "./store.js";
export { verify } from "./verify.js";
//# sourceMappingURL=index.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"index.js","sourceRoot":"","sources":["index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAE3C,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AACzD,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AACvC,OAAO,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AAEzD,OAAO,EACL,SAAS,EACT,SAAS,EACT,IAAI,EACJ,qBAAqB,EACrB,QAAQ,EACR,IAAI,GACL,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAE/C,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC"}
+32
View File
@@ -0,0 +1,32 @@
import type { CasNode, Hash, Store } from "./types.js";
export type JSONSchema = Record<string, unknown>;
export declare class SchemaValidationError extends Error {
readonly name = "SchemaValidationError";
}
/**
* Store a JSON Schema as a CAS node typed by the meta-schema hash.
* The returned hash becomes the typeHash for nodes that conform to this schema.
*/
export declare function putSchema(store: Store, jsonSchema: JSONSchema): Promise<Hash>;
/**
* Retrieve the JSON Schema payload for a given type hash.
* Returns null if no node exists at that hash.
*/
export declare function getSchema(store: Store, typeHash: Hash): JSONSchema | null;
/**
* Validate a node's payload against the schema identified by node.type.
* Returns false if the schema cannot be found or validation fails.
*/
export declare function validate(store: Store, node: CasNode): boolean;
/**
* Return all hashes referenced by this node via cas_ref fields in its schema.
* Null/undefined values are skipped.
*/
export declare function refs(store: Store, node: CasNode): Hash[];
/**
* BFS traversal starting from rootHash.
* Calls visitor(hash, node) for each reachable node exactly once.
* Handles cycles via a visited set.
*/
export declare function walk(store: Store, rootHash: Hash, visitor: (hash: Hash, node: CasNode) => void): void;
//# sourceMappingURL=schema.d.ts.map
+234
View File
@@ -0,0 +1,234 @@
import * as AjvModule from "ajv";
// ajv CJS default export: runtime `.default` holds the constructor,
// but tsc with verbatimModuleSyntax sees the namespace wrapper.
// biome-ignore lint/suspicious/noExplicitAny: CJS interop
const Ajv = (AjvModule.default ?? AjvModule);
import { bootstrap } from "./bootstrap.js";
export class SchemaValidationError extends Error {
name = "SchemaValidationError";
}
const ajv = new Ajv();
ajv.addFormat("cas_ref", /^[0-9A-HJKMNP-TV-Z]{13}$/);
const ALLOWED_SCHEMA_KEYS = new Set([
"type",
"properties",
"required",
"additionalProperties",
"anyOf",
"oneOf",
"items",
"format",
"title",
"enum",
"const",
"description",
]);
const JSON_SCHEMA_TYPES = new Set([
"string",
"number",
"integer",
"boolean",
"object",
"array",
"null",
]);
function isValidTypeValue(type) {
if (typeof type === "string") {
return JSON_SCHEMA_TYPES.has(type);
}
if (Array.isArray(type)) {
if (type.length === 0)
return false;
return type.every((entry) => typeof entry === "string" && JSON_SCHEMA_TYPES.has(entry));
}
return false;
}
function isValidSchema(value) {
if (value === null || typeof value !== "object" || Array.isArray(value)) {
return false;
}
const schema = value;
for (const key of Object.keys(schema)) {
if (!ALLOWED_SCHEMA_KEYS.has(key))
return false;
}
if ("type" in schema && !isValidTypeValue(schema.type))
return false;
if ("properties" in schema) {
const properties = schema.properties;
if (properties === null ||
typeof properties !== "object" ||
Array.isArray(properties)) {
return false;
}
for (const nested of Object.values(properties)) {
if (!isValidSchema(nested))
return false;
}
}
if ("required" in schema) {
if (!Array.isArray(schema.required))
return false;
for (const entry of schema.required) {
if (typeof entry !== "string")
return false;
}
}
if ("additionalProperties" in schema) {
const additionalProperties = schema.additionalProperties;
if (typeof additionalProperties === "boolean") {
// allowed
}
else if (!isValidSchema(additionalProperties)) {
return false;
}
}
if ("anyOf" in schema) {
if (!Array.isArray(schema.anyOf) || schema.anyOf.length === 0)
return false;
for (const entry of schema.anyOf) {
if (!isValidSchema(entry))
return false;
}
}
if ("oneOf" in schema) {
if (!Array.isArray(schema.oneOf) || schema.oneOf.length === 0)
return false;
for (const entry of schema.oneOf) {
if (!isValidSchema(entry))
return false;
}
}
if ("items" in schema && !isValidSchema(schema.items))
return false;
if ("format" in schema && typeof schema.format !== "string")
return false;
if ("title" in schema && typeof schema.title !== "string")
return false;
if ("description" in schema && typeof schema.description !== "string") {
return false;
}
if ("enum" in schema) {
if (!Array.isArray(schema.enum) || schema.enum.length === 0)
return false;
}
return true;
}
function isMetaSchemaNode(store, node) {
const schema = getSchema(store, node.type);
return schema !== null && schema === node.payload;
}
/**
* Store a JSON Schema as a CAS node typed by the meta-schema hash.
* The returned hash becomes the typeHash for nodes that conform to this schema.
*/
export async function putSchema(store, jsonSchema) {
const metaHash = await bootstrap(store);
if (!isValidSchema(jsonSchema)) {
throw new SchemaValidationError("Invalid schema: input does not conform to the json-cas JSON Schema meta-schema");
}
return store.put(metaHash, jsonSchema);
}
/**
* Retrieve the JSON Schema payload for a given type hash.
* Returns null if no node exists at that hash.
*/
export function getSchema(store, typeHash) {
const node = store.get(typeHash);
if (node === null)
return null;
return node.payload;
}
/**
* Validate a node's payload against the schema identified by node.type.
* Returns false if the schema cannot be found or validation fails.
*/
export function validate(store, node) {
const schema = getSchema(store, node.type);
if (schema === null)
return false;
if (isMetaSchemaNode(store, node)) {
return isValidSchema(node.payload);
}
return ajv.validate(schema, node.payload);
}
/**
* Recursively collect values of all properties whose schema has format: 'cas_ref'.
* Handles: direct format, anyOf (nullable refs), items (array refs),
* properties (nested objects), and additionalProperties (record refs).
*/
function collectRefs(schema, value) {
const result = [];
if (schema.format === "cas_ref") {
if (typeof value === "string") {
result.push(value);
}
return result;
}
if (Array.isArray(schema.anyOf)) {
for (const sub of schema.anyOf) {
result.push(...collectRefs(sub, value));
}
return result;
}
if (schema.type === "array" && schema.items && Array.isArray(value)) {
const itemSchema = schema.items;
for (const item of value) {
result.push(...collectRefs(itemSchema, item));
}
return result;
}
if (value !== null && typeof value === "object" && !Array.isArray(value)) {
if (schema.properties && typeof schema.properties === "object") {
const props = schema.properties;
const obj = value;
for (const [key, subSchema] of Object.entries(props)) {
result.push(...collectRefs(subSchema, obj[key]));
}
}
if (schema.additionalProperties &&
typeof schema.additionalProperties === "object") {
const addlSchema = schema.additionalProperties;
const obj = value;
for (const val of Object.values(obj)) {
result.push(...collectRefs(addlSchema, val));
}
}
}
return result;
}
/**
* Return all hashes referenced by this node via cas_ref fields in its schema.
* Null/undefined values are skipped.
*/
export function refs(store, node) {
const schema = getSchema(store, node.type);
if (schema === null)
return [];
return collectRefs(schema, node.payload);
}
/**
* BFS traversal starting from rootHash.
* Calls visitor(hash, node) for each reachable node exactly once.
* Handles cycles via a visited set.
*/
export function walk(store, rootHash, visitor) {
const visited = new Set();
const queue = [rootHash];
while (queue.length > 0) {
const hash = queue.shift();
if (visited.has(hash))
continue;
visited.add(hash);
const node = store.get(hash);
if (node === null)
continue;
visitor(hash, node);
for (const refHash of refs(store, node)) {
if (!visited.has(refHash)) {
queue.push(refHash);
}
}
}
}
//# sourceMappingURL=schema.js.map
File diff suppressed because one or more lines are too long
+3
View File
@@ -0,0 +1,3 @@
import { type BootstrapCapableStore } from "./bootstrap-capable.js";
export declare function createMemoryStore(): BootstrapCapableStore;
//# sourceMappingURL=store.d.ts.map
+45
View File
@@ -0,0 +1,45 @@
import { BOOTSTRAP_STORE, } from "./bootstrap-capable.js";
import { computeHash, computeSelfHash } from "./hash.js";
export function createMemoryStore() {
const data = new Map();
const byType = new Map();
function indexHash(type, hash) {
let set = byType.get(type);
if (!set) {
set = new Set();
byType.set(type, set);
}
set.add(hash);
}
async function putSelfReferencing(payload) {
const hash = await computeSelfHash(payload);
if (!data.has(hash)) {
data.set(hash, { type: hash, payload, timestamp: Date.now() });
indexHash(hash, hash);
}
return hash;
}
const store = {
async put(typeHash, payload) {
const hash = await computeHash(typeHash, payload);
if (!data.has(hash)) {
data.set(hash, { type: typeHash, payload, timestamp: Date.now() });
indexHash(typeHash, hash);
}
return hash;
},
get(hash) {
return data.get(hash) ?? null;
},
has(hash) {
return data.has(hash);
},
listByType(typeHash) {
const set = byType.get(typeHash);
return set ? [...set] : [];
},
[BOOTSTRAP_STORE]: putSelfReferencing,
};
return store;
}
//# sourceMappingURL=store.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"store.js","sourceRoot":"","sources":["store.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,eAAe,GAEhB,MAAM,wBAAwB,CAAC;AAChC,OAAO,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AAGzD,MAAM,UAAU,iBAAiB;IAC/B,MAAM,IAAI,GAAG,IAAI,GAAG,EAAiB,CAAC;IACtC,MAAM,MAAM,GAAG,IAAI,GAAG,EAAmB,CAAC;IAE1C,SAAS,SAAS,CAAC,IAAU,EAAE,IAAU;QACvC,IAAI,GAAG,GAAG,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QAC3B,IAAI,CAAC,GAAG,EAAE,CAAC;YACT,GAAG,GAAG,IAAI,GAAG,EAAE,CAAC;YAChB,MAAM,CAAC,GAAG,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;QACxB,CAAC;QACD,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAChB,CAAC;IAED,KAAK,UAAU,kBAAkB,CAAC,OAAgB;QAChD,MAAM,IAAI,GAAG,MAAM,eAAe,CAAC,OAAO,CAAC,CAAC;QAC5C,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC;YACpB,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;YAC/D,SAAS,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;QACxB,CAAC;QACD,OAAO,IAAI,CAAC;IACd,CAAC;IAED,MAAM,KAAK,GAA0B;QACnC,KAAK,CAAC,GAAG,CAAC,QAAc,EAAE,OAAgB;YACxC,MAAM,IAAI,GAAG,MAAM,WAAW,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;YAElD,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC;gBACpB,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;gBACnE,SAAS,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;YAC5B,CAAC;YAED,OAAO,IAAI,CAAC;QACd,CAAC;QAED,GAAG,CAAC,IAAU;YACZ,OAAO,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC;QAChC,CAAC;QAED,GAAG,CAAC,IAAU;YACZ,OAAO,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QACxB,CAAC;QAED,UAAU,CAAC,QAAc;YACvB,MAAM,GAAG,GAAG,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;YACjC,OAAO,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;QAC7B,CAAC;QAED,CAAC,eAAe,CAAC,EAAE,kBAAkB;KACtC,CAAC;IAEF,OAAO,KAAK,CAAC;AACf,CAAC"}
+26
View File
@@ -0,0 +1,26 @@
/**
* 13-character uppercase Crockford Base32 string produced by XXH64.
*/
export type Hash = string;
/**
* A content-addressed node with a typed payload.
* - type: Hash of the type descriptor node (or self for bootstrap)
* - payload: arbitrary data
* - timestamp: Unix epoch ms when the node was first stored
*/
export type CasNode<T = unknown> = {
type: Hash;
payload: T;
timestamp: number;
};
/**
* Content-addressable store interface.
* Self-referencing nodes are created only via bootstrap().
*/
export type Store = {
put(typeHash: Hash, payload: unknown): Promise<Hash>;
get(hash: Hash): CasNode | null;
has(hash: Hash): boolean;
listByType(typeHash: Hash): Hash[];
};
//# sourceMappingURL=types.d.ts.map
+2
View File
@@ -0,0 +1,2 @@
export {};
//# sourceMappingURL=types.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"types.js","sourceRoot":"","sources":["types.ts"],"names":[],"mappings":""}
+8
View File
@@ -0,0 +1,8 @@
import type { CasNode, Hash } from "./types.js";
/**
* Verify that a stored node matches the given hash.
* - Self-referencing nodes (type === hash): verified via CBOR-only hash.
* - Normal nodes: verified via XXH64(type_bytes ++ CBOR(payload)).
*/
export declare function verify(hash: Hash, node: CasNode): Promise<boolean>;
//# sourceMappingURL=verify.d.ts.map
+15
View File
@@ -0,0 +1,15 @@
import { computeHash, computeSelfHash } from "./hash.js";
/**
* Verify that a stored node matches the given hash.
* - Self-referencing nodes (type === hash): verified via CBOR-only hash.
* - Normal nodes: verified via XXH64(type_bytes ++ CBOR(payload)).
*/
export async function verify(hash, node) {
if (node.type === hash) {
const computed = await computeSelfHash(node.payload);
return computed === hash;
}
const computed = await computeHash(node.type, node.payload);
return computed === hash;
}
//# sourceMappingURL=verify.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"verify.js","sourceRoot":"","sources":["verify.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AAGzD;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,MAAM,CAAC,IAAU,EAAE,IAAa;IACpD,IAAI,IAAI,CAAC,IAAI,KAAK,IAAI,EAAE,CAAC;QACvB,MAAM,QAAQ,GAAG,MAAM,eAAe,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QACrD,OAAO,QAAQ,KAAK,IAAI,CAAC;IAC3B,CAAC;IACD,MAAM,QAAQ,GAAG,MAAM,WAAW,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC;IAC5D,OAAO,QAAQ,KAAK,IAAI,CAAC;AAC3B,CAAC"}
+7
View File
@@ -0,0 +1,7 @@
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
include: ["src/__tests__/**/*.test.ts"],
},
});