@pcguest/atb-sdk 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +40 -0
- package/dist/index.d.mts +95 -0
- package/dist/index.d.ts +95 -0
- package/dist/index.js +194 -0
- package/dist/index.mjs +162 -0
- package/package.json +46 -0
- package/src/bundle.ts +111 -0
- package/src/canonicalize.ts +66 -0
- package/src/hash.ts +43 -0
- package/src/index.ts +19 -0
- package/src/types.ts +27 -0
- package/tsconfig.json +19 -0
package/README.md
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# @atb-dev/sdk — ATB TypeScript SDK
|
|
2
|
+
|
|
3
|
+
The official TypeScript/JavaScript SDK for [ATB (Agent Trace Bundle)](https://github.com/pcguest/atb).
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @atb-dev/sdk
|
|
9
|
+
# or
|
|
10
|
+
pnpm add @atb-dev/sdk
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Quick Start
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
import { Bundle } from "@atb-dev/sdk";
|
|
17
|
+
|
|
18
|
+
const bundle = new Bundle();
|
|
19
|
+
|
|
20
|
+
bundle.append("dev.session", {
|
|
21
|
+
date: "2025-01-15",
|
|
22
|
+
featuresBuilt: ["hash chaining", "CLI init"],
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
bundle.append("decision", {
|
|
26
|
+
choice: "Go over Rust for CLI",
|
|
27
|
+
reason: "Solo founder velocity",
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
bundle.save(); // Writes to run.atb/bundle.atb
|
|
31
|
+
|
|
32
|
+
// Later — reload and verify
|
|
33
|
+
const loaded = Bundle.load();
|
|
34
|
+
loaded.verify(); // Throws ATBVerificationError if tampered
|
|
35
|
+
console.log(`Verified ${loaded.length} events — chain intact.`);
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## License
|
|
39
|
+
|
|
40
|
+
MIT
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core type definitions for the ATB TypeScript SDK.
|
|
3
|
+
*/
|
|
4
|
+
/** A single auditable event in an ATB bundle. */
|
|
5
|
+
interface ATBEvent {
|
|
6
|
+
/** 1-based sequence number within the bundle. */
|
|
7
|
+
seq: number;
|
|
8
|
+
/** Hex-encoded SHA-256 hash of the preceding event (or GENESIS_HASH). */
|
|
9
|
+
prevHash: string;
|
|
10
|
+
/** Dot-namespaced event type identifier. */
|
|
11
|
+
type: string;
|
|
12
|
+
/** Arbitrary JSON-serialisable payload. */
|
|
13
|
+
data: unknown;
|
|
14
|
+
}
|
|
15
|
+
/** A single record in an ATB bundle file (event + its hash). */
|
|
16
|
+
interface ATBRecord {
|
|
17
|
+
event: ATBEvent;
|
|
18
|
+
hash: string;
|
|
19
|
+
}
|
|
20
|
+
/** Options for creating a Bundle. */
|
|
21
|
+
interface BundleOptions {
|
|
22
|
+
/** Path to the bundle file. Defaults to `run.atb/bundle.atb`. */
|
|
23
|
+
path?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* ATB Bundle — the primary interface for creating and managing ATB trace bundles.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
declare class ATBVerificationError extends Error {
|
|
31
|
+
readonly eventIndex: number;
|
|
32
|
+
readonly expectedHash: string;
|
|
33
|
+
readonly computedHash: string;
|
|
34
|
+
constructor(message: string, eventIndex: number, expectedHash: string, computedHash: string);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* An in-memory ATB bundle.
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* ```ts
|
|
41
|
+
* import { Bundle } from "@atb-dev/sdk";
|
|
42
|
+
*
|
|
43
|
+
* const bundle = new Bundle();
|
|
44
|
+
* bundle.append("dev.session", { date: "2025-01-15" });
|
|
45
|
+
* bundle.save();
|
|
46
|
+
*
|
|
47
|
+
* const loaded = Bundle.load();
|
|
48
|
+
* loaded.verify();
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
51
|
+
declare class Bundle {
|
|
52
|
+
readonly records: ATBRecord[];
|
|
53
|
+
private readonly path;
|
|
54
|
+
constructor(options?: BundleOptions);
|
|
55
|
+
/** Append a new event to the bundle. */
|
|
56
|
+
append(type: string, data: unknown): ATBRecord;
|
|
57
|
+
/** Verify the integrity of the entire bundle. Throws on tampering. */
|
|
58
|
+
verify(): void;
|
|
59
|
+
/** Save the bundle to disk in NDJSON format. */
|
|
60
|
+
save(path?: string): void;
|
|
61
|
+
/** Load a bundle from disk. */
|
|
62
|
+
static load(path?: string): Bundle;
|
|
63
|
+
get length(): number;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* ATB hash-chaining implementation for TypeScript/Node.js.
|
|
68
|
+
*
|
|
69
|
+
* Each event hash is computed as:
|
|
70
|
+
* SHA256(prevHash + canonicalJSON(event))
|
|
71
|
+
*/
|
|
72
|
+
|
|
73
|
+
/** The sentinel previous-hash used for the first event in a bundle. */
|
|
74
|
+
declare const GENESIS_HASH: string;
|
|
75
|
+
/**
|
|
76
|
+
* Compute the SHA-256 hash for an event given the previous hash.
|
|
77
|
+
*/
|
|
78
|
+
declare function computeHash(event: ATBEvent, prevHash: string): string;
|
|
79
|
+
/**
|
|
80
|
+
* Compute and assign hashes for a sequence of events.
|
|
81
|
+
* Mutates each event's `seq` and `prevHash` fields in-place.
|
|
82
|
+
*
|
|
83
|
+
* @returns Array of hex-encoded SHA-256 hashes, one per event.
|
|
84
|
+
*/
|
|
85
|
+
declare function chainEvents(events: ATBEvent[]): string[];
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* RFC 8785 JSON Canonicalization Scheme (JCS) implementation.
|
|
89
|
+
*
|
|
90
|
+
* Produces a deterministic, canonical UTF-8 string for any JSON value,
|
|
91
|
+
* suitable for use in cryptographic hash computation.
|
|
92
|
+
*/
|
|
93
|
+
declare function canonicalize(value: unknown): string;
|
|
94
|
+
|
|
95
|
+
export { type ATBEvent, type ATBRecord, ATBVerificationError, Bundle, type BundleOptions, GENESIS_HASH, canonicalize, chainEvents, computeHash };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core type definitions for the ATB TypeScript SDK.
|
|
3
|
+
*/
|
|
4
|
+
/** A single auditable event in an ATB bundle. */
|
|
5
|
+
interface ATBEvent {
|
|
6
|
+
/** 1-based sequence number within the bundle. */
|
|
7
|
+
seq: number;
|
|
8
|
+
/** Hex-encoded SHA-256 hash of the preceding event (or GENESIS_HASH). */
|
|
9
|
+
prevHash: string;
|
|
10
|
+
/** Dot-namespaced event type identifier. */
|
|
11
|
+
type: string;
|
|
12
|
+
/** Arbitrary JSON-serialisable payload. */
|
|
13
|
+
data: unknown;
|
|
14
|
+
}
|
|
15
|
+
/** A single record in an ATB bundle file (event + its hash). */
|
|
16
|
+
interface ATBRecord {
|
|
17
|
+
event: ATBEvent;
|
|
18
|
+
hash: string;
|
|
19
|
+
}
|
|
20
|
+
/** Options for creating a Bundle. */
|
|
21
|
+
interface BundleOptions {
|
|
22
|
+
/** Path to the bundle file. Defaults to `run.atb/bundle.atb`. */
|
|
23
|
+
path?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* ATB Bundle — the primary interface for creating and managing ATB trace bundles.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
declare class ATBVerificationError extends Error {
|
|
31
|
+
readonly eventIndex: number;
|
|
32
|
+
readonly expectedHash: string;
|
|
33
|
+
readonly computedHash: string;
|
|
34
|
+
constructor(message: string, eventIndex: number, expectedHash: string, computedHash: string);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* An in-memory ATB bundle.
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* ```ts
|
|
41
|
+
* import { Bundle } from "@atb-dev/sdk";
|
|
42
|
+
*
|
|
43
|
+
* const bundle = new Bundle();
|
|
44
|
+
* bundle.append("dev.session", { date: "2025-01-15" });
|
|
45
|
+
* bundle.save();
|
|
46
|
+
*
|
|
47
|
+
* const loaded = Bundle.load();
|
|
48
|
+
* loaded.verify();
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
51
|
+
declare class Bundle {
|
|
52
|
+
readonly records: ATBRecord[];
|
|
53
|
+
private readonly path;
|
|
54
|
+
constructor(options?: BundleOptions);
|
|
55
|
+
/** Append a new event to the bundle. */
|
|
56
|
+
append(type: string, data: unknown): ATBRecord;
|
|
57
|
+
/** Verify the integrity of the entire bundle. Throws on tampering. */
|
|
58
|
+
verify(): void;
|
|
59
|
+
/** Save the bundle to disk in NDJSON format. */
|
|
60
|
+
save(path?: string): void;
|
|
61
|
+
/** Load a bundle from disk. */
|
|
62
|
+
static load(path?: string): Bundle;
|
|
63
|
+
get length(): number;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* ATB hash-chaining implementation for TypeScript/Node.js.
|
|
68
|
+
*
|
|
69
|
+
* Each event hash is computed as:
|
|
70
|
+
* SHA256(prevHash + canonicalJSON(event))
|
|
71
|
+
*/
|
|
72
|
+
|
|
73
|
+
/** The sentinel previous-hash used for the first event in a bundle. */
|
|
74
|
+
declare const GENESIS_HASH: string;
|
|
75
|
+
/**
|
|
76
|
+
* Compute the SHA-256 hash for an event given the previous hash.
|
|
77
|
+
*/
|
|
78
|
+
declare function computeHash(event: ATBEvent, prevHash: string): string;
|
|
79
|
+
/**
|
|
80
|
+
* Compute and assign hashes for a sequence of events.
|
|
81
|
+
* Mutates each event's `seq` and `prevHash` fields in-place.
|
|
82
|
+
*
|
|
83
|
+
* @returns Array of hex-encoded SHA-256 hashes, one per event.
|
|
84
|
+
*/
|
|
85
|
+
declare function chainEvents(events: ATBEvent[]): string[];
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* RFC 8785 JSON Canonicalization Scheme (JCS) implementation.
|
|
89
|
+
*
|
|
90
|
+
* Produces a deterministic, canonical UTF-8 string for any JSON value,
|
|
91
|
+
* suitable for use in cryptographic hash computation.
|
|
92
|
+
*/
|
|
93
|
+
declare function canonicalize(value: unknown): string;
|
|
94
|
+
|
|
95
|
+
export { type ATBEvent, type ATBRecord, ATBVerificationError, Bundle, type BundleOptions, GENESIS_HASH, canonicalize, chainEvents, computeHash };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
ATBVerificationError: () => ATBVerificationError,
|
|
24
|
+
Bundle: () => Bundle,
|
|
25
|
+
GENESIS_HASH: () => GENESIS_HASH,
|
|
26
|
+
canonicalize: () => canonicalize,
|
|
27
|
+
chainEvents: () => chainEvents,
|
|
28
|
+
computeHash: () => computeHash
|
|
29
|
+
});
|
|
30
|
+
module.exports = __toCommonJS(index_exports);
|
|
31
|
+
|
|
32
|
+
// src/bundle.ts
|
|
33
|
+
var import_node_fs = require("fs");
|
|
34
|
+
var import_node_path = require("path");
|
|
35
|
+
|
|
36
|
+
// src/hash.ts
|
|
37
|
+
var import_node_crypto = require("crypto");
|
|
38
|
+
|
|
39
|
+
// src/canonicalize.ts
|
|
40
|
+
function canonicalize(value) {
|
|
41
|
+
if (value === null) return "null";
|
|
42
|
+
if (typeof value === "boolean") return value ? "true" : "false";
|
|
43
|
+
if (typeof value === "number") return serializeNumber(value);
|
|
44
|
+
if (typeof value === "string") return serializeString(value);
|
|
45
|
+
if (Array.isArray(value)) {
|
|
46
|
+
return "[" + value.map(canonicalize).join(",") + "]";
|
|
47
|
+
}
|
|
48
|
+
if (typeof value === "object") {
|
|
49
|
+
const obj = value;
|
|
50
|
+
const sortedKeys = Object.keys(obj).sort(utf16Compare);
|
|
51
|
+
const pairs = sortedKeys.map(
|
|
52
|
+
(k) => `${serializeString(k)}:${canonicalize(obj[k])}`
|
|
53
|
+
);
|
|
54
|
+
return "{" + pairs.join(",") + "}";
|
|
55
|
+
}
|
|
56
|
+
throw new TypeError(`canonicalize: unsupported type ${typeof value}`);
|
|
57
|
+
}
|
|
58
|
+
function serializeNumber(n) {
|
|
59
|
+
if (!isFinite(n)) throw new RangeError(`canonicalize: non-finite number ${n}`);
|
|
60
|
+
return String(n);
|
|
61
|
+
}
|
|
62
|
+
var ESCAPE_MAP = {
|
|
63
|
+
'"': '\\"',
|
|
64
|
+
"\\": "\\\\",
|
|
65
|
+
"\b": "\\b",
|
|
66
|
+
"\f": "\\f",
|
|
67
|
+
"\n": "\\n",
|
|
68
|
+
"\r": "\\r",
|
|
69
|
+
" ": "\\t"
|
|
70
|
+
};
|
|
71
|
+
function serializeString(s) {
|
|
72
|
+
let result = '"';
|
|
73
|
+
for (const ch of s) {
|
|
74
|
+
const code = ch.charCodeAt(0);
|
|
75
|
+
if (ch in ESCAPE_MAP) {
|
|
76
|
+
result += ESCAPE_MAP[ch];
|
|
77
|
+
} else if (code < 32) {
|
|
78
|
+
result += `\\u${code.toString(16).padStart(4, "0")}`;
|
|
79
|
+
} else {
|
|
80
|
+
result += ch;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return result + '"';
|
|
84
|
+
}
|
|
85
|
+
function utf16Compare(a, b) {
|
|
86
|
+
for (let i = 0; i < Math.max(a.length, b.length); i++) {
|
|
87
|
+
const ca = a.charCodeAt(i) || -1;
|
|
88
|
+
const cb = b.charCodeAt(i) || -1;
|
|
89
|
+
if (ca !== cb) return ca - cb;
|
|
90
|
+
}
|
|
91
|
+
return 0;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// src/hash.ts
|
|
95
|
+
var GENESIS_HASH = "0".repeat(64);
|
|
96
|
+
function computeHash(event, prevHash) {
|
|
97
|
+
const canonical = canonicalize(event);
|
|
98
|
+
return (0, import_node_crypto.createHash)("sha256").update(prevHash, "utf8").update(canonical, "utf8").digest("hex");
|
|
99
|
+
}
|
|
100
|
+
function chainEvents(events) {
|
|
101
|
+
const hashes = [];
|
|
102
|
+
let prev = GENESIS_HASH;
|
|
103
|
+
for (let i = 0; i < events.length; i++) {
|
|
104
|
+
events[i].seq = i + 1;
|
|
105
|
+
events[i].prevHash = prev;
|
|
106
|
+
const h = computeHash(events[i], prev);
|
|
107
|
+
hashes.push(h);
|
|
108
|
+
prev = h;
|
|
109
|
+
}
|
|
110
|
+
return hashes;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// src/bundle.ts
|
|
114
|
+
var DEFAULT_PATH = "run.atb/bundle.atb";
|
|
115
|
+
var ATBVerificationError = class extends Error {
|
|
116
|
+
constructor(message, eventIndex, expectedHash, computedHash) {
|
|
117
|
+
super(message);
|
|
118
|
+
this.eventIndex = eventIndex;
|
|
119
|
+
this.expectedHash = expectedHash;
|
|
120
|
+
this.computedHash = computedHash;
|
|
121
|
+
this.name = "ATBVerificationError";
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
var Bundle = class _Bundle {
|
|
125
|
+
records = [];
|
|
126
|
+
path;
|
|
127
|
+
constructor(options = {}) {
|
|
128
|
+
this.path = options.path ?? DEFAULT_PATH;
|
|
129
|
+
}
|
|
130
|
+
/** Append a new event to the bundle. */
|
|
131
|
+
append(type, data) {
|
|
132
|
+
const prevHash = this.records.length > 0 ? this.records[this.records.length - 1].hash : GENESIS_HASH;
|
|
133
|
+
const event = {
|
|
134
|
+
seq: this.records.length + 1,
|
|
135
|
+
prevHash,
|
|
136
|
+
type,
|
|
137
|
+
data
|
|
138
|
+
};
|
|
139
|
+
const hash = computeHash(event, prevHash);
|
|
140
|
+
const record = { event, hash };
|
|
141
|
+
this.records.push(record);
|
|
142
|
+
return record;
|
|
143
|
+
}
|
|
144
|
+
/** Verify the integrity of the entire bundle. Throws on tampering. */
|
|
145
|
+
verify() {
|
|
146
|
+
let prev = GENESIS_HASH;
|
|
147
|
+
for (let i = 0; i < this.records.length; i++) {
|
|
148
|
+
const record = this.records[i];
|
|
149
|
+
const event = { ...record.event, seq: i + 1, prevHash: prev };
|
|
150
|
+
const computed = computeHash(event, prev);
|
|
151
|
+
if (computed !== record.hash) {
|
|
152
|
+
throw new ATBVerificationError(
|
|
153
|
+
`Tamper detected at event ${i + 1}: expected ${record.hash}, computed ${computed}`,
|
|
154
|
+
i,
|
|
155
|
+
record.hash,
|
|
156
|
+
computed
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
prev = computed;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
/** Save the bundle to disk in NDJSON format. */
|
|
163
|
+
save(path) {
|
|
164
|
+
const target = path ?? this.path;
|
|
165
|
+
(0, import_node_fs.mkdirSync)((0, import_node_path.dirname)(target), { recursive: true });
|
|
166
|
+
const lines = this.records.map((r) => JSON.stringify(r));
|
|
167
|
+
(0, import_node_fs.writeFileSync)(target, lines.join("\n") + "\n", "utf8");
|
|
168
|
+
}
|
|
169
|
+
/** Load a bundle from disk. */
|
|
170
|
+
static load(path) {
|
|
171
|
+
const target = path ?? DEFAULT_PATH;
|
|
172
|
+
const bundle = new _Bundle({ path: target });
|
|
173
|
+
const content = (0, import_node_fs.readFileSync)(target, "utf8");
|
|
174
|
+
for (const line of content.split("\n")) {
|
|
175
|
+
const trimmed = line.trim();
|
|
176
|
+
if (!trimmed) continue;
|
|
177
|
+
const record = JSON.parse(trimmed);
|
|
178
|
+
bundle.records.push(record);
|
|
179
|
+
}
|
|
180
|
+
return bundle;
|
|
181
|
+
}
|
|
182
|
+
get length() {
|
|
183
|
+
return this.records.length;
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
187
|
+
0 && (module.exports = {
|
|
188
|
+
ATBVerificationError,
|
|
189
|
+
Bundle,
|
|
190
|
+
GENESIS_HASH,
|
|
191
|
+
canonicalize,
|
|
192
|
+
chainEvents,
|
|
193
|
+
computeHash
|
|
194
|
+
});
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
// src/bundle.ts
|
|
2
|
+
import { readFileSync, writeFileSync, mkdirSync } from "fs";
|
|
3
|
+
import { dirname } from "path";
|
|
4
|
+
|
|
5
|
+
// src/hash.ts
|
|
6
|
+
import { createHash } from "crypto";
|
|
7
|
+
|
|
8
|
+
// src/canonicalize.ts
|
|
9
|
+
function canonicalize(value) {
|
|
10
|
+
if (value === null) return "null";
|
|
11
|
+
if (typeof value === "boolean") return value ? "true" : "false";
|
|
12
|
+
if (typeof value === "number") return serializeNumber(value);
|
|
13
|
+
if (typeof value === "string") return serializeString(value);
|
|
14
|
+
if (Array.isArray(value)) {
|
|
15
|
+
return "[" + value.map(canonicalize).join(",") + "]";
|
|
16
|
+
}
|
|
17
|
+
if (typeof value === "object") {
|
|
18
|
+
const obj = value;
|
|
19
|
+
const sortedKeys = Object.keys(obj).sort(utf16Compare);
|
|
20
|
+
const pairs = sortedKeys.map(
|
|
21
|
+
(k) => `${serializeString(k)}:${canonicalize(obj[k])}`
|
|
22
|
+
);
|
|
23
|
+
return "{" + pairs.join(",") + "}";
|
|
24
|
+
}
|
|
25
|
+
throw new TypeError(`canonicalize: unsupported type ${typeof value}`);
|
|
26
|
+
}
|
|
27
|
+
function serializeNumber(n) {
|
|
28
|
+
if (!isFinite(n)) throw new RangeError(`canonicalize: non-finite number ${n}`);
|
|
29
|
+
return String(n);
|
|
30
|
+
}
|
|
31
|
+
var ESCAPE_MAP = {
|
|
32
|
+
'"': '\\"',
|
|
33
|
+
"\\": "\\\\",
|
|
34
|
+
"\b": "\\b",
|
|
35
|
+
"\f": "\\f",
|
|
36
|
+
"\n": "\\n",
|
|
37
|
+
"\r": "\\r",
|
|
38
|
+
" ": "\\t"
|
|
39
|
+
};
|
|
40
|
+
function serializeString(s) {
|
|
41
|
+
let result = '"';
|
|
42
|
+
for (const ch of s) {
|
|
43
|
+
const code = ch.charCodeAt(0);
|
|
44
|
+
if (ch in ESCAPE_MAP) {
|
|
45
|
+
result += ESCAPE_MAP[ch];
|
|
46
|
+
} else if (code < 32) {
|
|
47
|
+
result += `\\u${code.toString(16).padStart(4, "0")}`;
|
|
48
|
+
} else {
|
|
49
|
+
result += ch;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return result + '"';
|
|
53
|
+
}
|
|
54
|
+
function utf16Compare(a, b) {
|
|
55
|
+
for (let i = 0; i < Math.max(a.length, b.length); i++) {
|
|
56
|
+
const ca = a.charCodeAt(i) || -1;
|
|
57
|
+
const cb = b.charCodeAt(i) || -1;
|
|
58
|
+
if (ca !== cb) return ca - cb;
|
|
59
|
+
}
|
|
60
|
+
return 0;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// src/hash.ts
|
|
64
|
+
var GENESIS_HASH = "0".repeat(64);
|
|
65
|
+
function computeHash(event, prevHash) {
|
|
66
|
+
const canonical = canonicalize(event);
|
|
67
|
+
return createHash("sha256").update(prevHash, "utf8").update(canonical, "utf8").digest("hex");
|
|
68
|
+
}
|
|
69
|
+
function chainEvents(events) {
|
|
70
|
+
const hashes = [];
|
|
71
|
+
let prev = GENESIS_HASH;
|
|
72
|
+
for (let i = 0; i < events.length; i++) {
|
|
73
|
+
events[i].seq = i + 1;
|
|
74
|
+
events[i].prevHash = prev;
|
|
75
|
+
const h = computeHash(events[i], prev);
|
|
76
|
+
hashes.push(h);
|
|
77
|
+
prev = h;
|
|
78
|
+
}
|
|
79
|
+
return hashes;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// src/bundle.ts
|
|
83
|
+
var DEFAULT_PATH = "run.atb/bundle.atb";
|
|
84
|
+
var ATBVerificationError = class extends Error {
|
|
85
|
+
constructor(message, eventIndex, expectedHash, computedHash) {
|
|
86
|
+
super(message);
|
|
87
|
+
this.eventIndex = eventIndex;
|
|
88
|
+
this.expectedHash = expectedHash;
|
|
89
|
+
this.computedHash = computedHash;
|
|
90
|
+
this.name = "ATBVerificationError";
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
var Bundle = class _Bundle {
|
|
94
|
+
records = [];
|
|
95
|
+
path;
|
|
96
|
+
constructor(options = {}) {
|
|
97
|
+
this.path = options.path ?? DEFAULT_PATH;
|
|
98
|
+
}
|
|
99
|
+
/** Append a new event to the bundle. */
|
|
100
|
+
append(type, data) {
|
|
101
|
+
const prevHash = this.records.length > 0 ? this.records[this.records.length - 1].hash : GENESIS_HASH;
|
|
102
|
+
const event = {
|
|
103
|
+
seq: this.records.length + 1,
|
|
104
|
+
prevHash,
|
|
105
|
+
type,
|
|
106
|
+
data
|
|
107
|
+
};
|
|
108
|
+
const hash = computeHash(event, prevHash);
|
|
109
|
+
const record = { event, hash };
|
|
110
|
+
this.records.push(record);
|
|
111
|
+
return record;
|
|
112
|
+
}
|
|
113
|
+
/** Verify the integrity of the entire bundle. Throws on tampering. */
|
|
114
|
+
verify() {
|
|
115
|
+
let prev = GENESIS_HASH;
|
|
116
|
+
for (let i = 0; i < this.records.length; i++) {
|
|
117
|
+
const record = this.records[i];
|
|
118
|
+
const event = { ...record.event, seq: i + 1, prevHash: prev };
|
|
119
|
+
const computed = computeHash(event, prev);
|
|
120
|
+
if (computed !== record.hash) {
|
|
121
|
+
throw new ATBVerificationError(
|
|
122
|
+
`Tamper detected at event ${i + 1}: expected ${record.hash}, computed ${computed}`,
|
|
123
|
+
i,
|
|
124
|
+
record.hash,
|
|
125
|
+
computed
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
prev = computed;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
/** Save the bundle to disk in NDJSON format. */
|
|
132
|
+
save(path) {
|
|
133
|
+
const target = path ?? this.path;
|
|
134
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
135
|
+
const lines = this.records.map((r) => JSON.stringify(r));
|
|
136
|
+
writeFileSync(target, lines.join("\n") + "\n", "utf8");
|
|
137
|
+
}
|
|
138
|
+
/** Load a bundle from disk. */
|
|
139
|
+
static load(path) {
|
|
140
|
+
const target = path ?? DEFAULT_PATH;
|
|
141
|
+
const bundle = new _Bundle({ path: target });
|
|
142
|
+
const content = readFileSync(target, "utf8");
|
|
143
|
+
for (const line of content.split("\n")) {
|
|
144
|
+
const trimmed = line.trim();
|
|
145
|
+
if (!trimmed) continue;
|
|
146
|
+
const record = JSON.parse(trimmed);
|
|
147
|
+
bundle.records.push(record);
|
|
148
|
+
}
|
|
149
|
+
return bundle;
|
|
150
|
+
}
|
|
151
|
+
get length() {
|
|
152
|
+
return this.records.length;
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
export {
|
|
156
|
+
ATBVerificationError,
|
|
157
|
+
Bundle,
|
|
158
|
+
GENESIS_HASH,
|
|
159
|
+
canonicalize,
|
|
160
|
+
chainEvents,
|
|
161
|
+
computeHash
|
|
162
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@pcguest/atb-sdk",
|
|
3
|
+
"version": "0.1.2",
|
|
4
|
+
"description": "ATB (Agent Trace Bundle) TypeScript SDK — tamper-evident audit trails for AI agent workflows",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"module": "dist/index.mjs",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.mjs",
|
|
12
|
+
"require": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"scripts": {
|
|
16
|
+
"build": "tsup src/index.ts --format cjs,esm --dts",
|
|
17
|
+
"test": "vitest run",
|
|
18
|
+
"lint": "eslint src --ext .ts",
|
|
19
|
+
"typecheck": "tsc --noEmit"
|
|
20
|
+
},
|
|
21
|
+
"keywords": [
|
|
22
|
+
"audit",
|
|
23
|
+
"ai",
|
|
24
|
+
"agents",
|
|
25
|
+
"tracing",
|
|
26
|
+
"observability",
|
|
27
|
+
"atb"
|
|
28
|
+
],
|
|
29
|
+
"author": "Paddy Guest <patrickcguest@proton.me>",
|
|
30
|
+
"license": "MIT",
|
|
31
|
+
"repository": {
|
|
32
|
+
"type": "git",
|
|
33
|
+
"url": "https://github.com/pcguest/atb.git",
|
|
34
|
+
"directory": "sdk/typescript"
|
|
35
|
+
},
|
|
36
|
+
"homepage": "https://atb.dev",
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@types/node": "^20.0.0",
|
|
39
|
+
"tsup": "^8.0.0",
|
|
40
|
+
"typescript": "^5.0.0",
|
|
41
|
+
"vitest": "^1.0.0"
|
|
42
|
+
},
|
|
43
|
+
"engines": {
|
|
44
|
+
"node": ">=18"
|
|
45
|
+
}
|
|
46
|
+
}
|
package/src/bundle.ts
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ATB Bundle — the primary interface for creating and managing ATB trace bundles.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
6
|
+
import { dirname } from "node:path";
|
|
7
|
+
import { GENESIS_HASH, computeHash } from "./hash.js";
|
|
8
|
+
import type { ATBEvent, ATBRecord, BundleOptions } from "./types.js";
|
|
9
|
+
|
|
10
|
+
const DEFAULT_PATH = "run.atb/bundle.atb";
|
|
11
|
+
|
|
12
|
+
export class ATBVerificationError extends Error {
|
|
13
|
+
constructor(
|
|
14
|
+
message: string,
|
|
15
|
+
public readonly eventIndex: number,
|
|
16
|
+
public readonly expectedHash: string,
|
|
17
|
+
public readonly computedHash: string
|
|
18
|
+
) {
|
|
19
|
+
super(message);
|
|
20
|
+
this.name = "ATBVerificationError";
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* An in-memory ATB bundle.
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* ```ts
|
|
29
|
+
* import { Bundle } from "@atb-dev/sdk";
|
|
30
|
+
*
|
|
31
|
+
* const bundle = new Bundle();
|
|
32
|
+
* bundle.append("dev.session", { date: "2025-01-15" });
|
|
33
|
+
* bundle.save();
|
|
34
|
+
*
|
|
35
|
+
* const loaded = Bundle.load();
|
|
36
|
+
* loaded.verify();
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
export class Bundle {
|
|
40
|
+
readonly records: ATBRecord[] = [];
|
|
41
|
+
private readonly path: string;
|
|
42
|
+
|
|
43
|
+
constructor(options: BundleOptions = {}) {
|
|
44
|
+
this.path = options.path ?? DEFAULT_PATH;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Append a new event to the bundle. */
|
|
48
|
+
append(type: string, data: unknown): ATBRecord {
|
|
49
|
+
const prevHash =
|
|
50
|
+
this.records.length > 0
|
|
51
|
+
? this.records[this.records.length - 1].hash
|
|
52
|
+
: GENESIS_HASH;
|
|
53
|
+
|
|
54
|
+
const event: ATBEvent = {
|
|
55
|
+
seq: this.records.length + 1,
|
|
56
|
+
prevHash,
|
|
57
|
+
type,
|
|
58
|
+
data,
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const hash = computeHash(event, prevHash);
|
|
62
|
+
const record: ATBRecord = { event, hash };
|
|
63
|
+
this.records.push(record);
|
|
64
|
+
return record;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Verify the integrity of the entire bundle. Throws on tampering. */
|
|
68
|
+
verify(): void {
|
|
69
|
+
let prev = GENESIS_HASH;
|
|
70
|
+
for (let i = 0; i < this.records.length; i++) {
|
|
71
|
+
const record = this.records[i];
|
|
72
|
+
const event = { ...record.event, seq: i + 1, prevHash: prev };
|
|
73
|
+
const computed = computeHash(event, prev);
|
|
74
|
+
if (computed !== record.hash) {
|
|
75
|
+
throw new ATBVerificationError(
|
|
76
|
+
`Tamper detected at event ${i + 1}: expected ${record.hash}, computed ${computed}`,
|
|
77
|
+
i,
|
|
78
|
+
record.hash,
|
|
79
|
+
computed
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
prev = computed;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Save the bundle to disk in NDJSON format. */
|
|
87
|
+
save(path?: string): void {
|
|
88
|
+
const target = path ?? this.path;
|
|
89
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
90
|
+
const lines = this.records.map((r) => JSON.stringify(r));
|
|
91
|
+
writeFileSync(target, lines.join("\n") + "\n", "utf8");
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Load a bundle from disk. */
|
|
95
|
+
static load(path?: string): Bundle {
|
|
96
|
+
const target = path ?? DEFAULT_PATH;
|
|
97
|
+
const bundle = new Bundle({ path: target });
|
|
98
|
+
const content = readFileSync(target, "utf8");
|
|
99
|
+
for (const line of content.split("\n")) {
|
|
100
|
+
const trimmed = line.trim();
|
|
101
|
+
if (!trimmed) continue;
|
|
102
|
+
const record = JSON.parse(trimmed) as ATBRecord;
|
|
103
|
+
bundle.records.push(record);
|
|
104
|
+
}
|
|
105
|
+
return bundle;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
get length(): number {
|
|
109
|
+
return this.records.length;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RFC 8785 JSON Canonicalization Scheme (JCS) implementation.
|
|
3
|
+
*
|
|
4
|
+
* Produces a deterministic, canonical UTF-8 string for any JSON value,
|
|
5
|
+
* suitable for use in cryptographic hash computation.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export function canonicalize(value: unknown): string {
|
|
9
|
+
if (value === null) return "null";
|
|
10
|
+
if (typeof value === "boolean") return value ? "true" : "false";
|
|
11
|
+
if (typeof value === "number") return serializeNumber(value);
|
|
12
|
+
if (typeof value === "string") return serializeString(value);
|
|
13
|
+
if (Array.isArray(value)) {
|
|
14
|
+
return "[" + value.map(canonicalize).join(",") + "]";
|
|
15
|
+
}
|
|
16
|
+
if (typeof value === "object") {
|
|
17
|
+
const obj = value as Record<string, unknown>;
|
|
18
|
+
// RFC 8785 §3.2.3 — sort keys by UTF-16 code unit sequence.
|
|
19
|
+
const sortedKeys = Object.keys(obj).sort(utf16Compare);
|
|
20
|
+
const pairs = sortedKeys.map(
|
|
21
|
+
(k) => `${serializeString(k)}:${canonicalize(obj[k])}`
|
|
22
|
+
);
|
|
23
|
+
return "{" + pairs.join(",") + "}";
|
|
24
|
+
}
|
|
25
|
+
throw new TypeError(`canonicalize: unsupported type ${typeof value}`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function serializeNumber(n: number): string {
|
|
29
|
+
if (!isFinite(n)) throw new RangeError(`canonicalize: non-finite number ${n}`);
|
|
30
|
+
// Use JavaScript's default number serialisation, which matches ES6.
|
|
31
|
+
return String(n);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const ESCAPE_MAP: Record<string, string> = {
|
|
35
|
+
'"': '\\"',
|
|
36
|
+
"\\": "\\\\",
|
|
37
|
+
"\b": "\\b",
|
|
38
|
+
"\f": "\\f",
|
|
39
|
+
"\n": "\\n",
|
|
40
|
+
"\r": "\\r",
|
|
41
|
+
"\t": "\\t",
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
function serializeString(s: string): string {
|
|
45
|
+
let result = '"';
|
|
46
|
+
for (const ch of s) {
|
|
47
|
+
const code = ch.charCodeAt(0);
|
|
48
|
+
if (ch in ESCAPE_MAP) {
|
|
49
|
+
result += ESCAPE_MAP[ch];
|
|
50
|
+
} else if (code < 0x20) {
|
|
51
|
+
result += `\\u${code.toString(16).padStart(4, "0")}`;
|
|
52
|
+
} else {
|
|
53
|
+
result += ch;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return result + '"';
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function utf16Compare(a: string, b: string): number {
|
|
60
|
+
for (let i = 0; i < Math.max(a.length, b.length); i++) {
|
|
61
|
+
const ca = a.charCodeAt(i) || -1;
|
|
62
|
+
const cb = b.charCodeAt(i) || -1;
|
|
63
|
+
if (ca !== cb) return ca - cb;
|
|
64
|
+
}
|
|
65
|
+
return 0;
|
|
66
|
+
}
|
package/src/hash.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ATB hash-chaining implementation for TypeScript/Node.js.
|
|
3
|
+
*
|
|
4
|
+
* Each event hash is computed as:
|
|
5
|
+
* SHA256(prevHash + canonicalJSON(event))
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { createHash } from "node:crypto";
|
|
9
|
+
import { canonicalize } from "./canonicalize.js";
|
|
10
|
+
import type { ATBEvent } from "./types.js";
|
|
11
|
+
|
|
12
|
+
/** The sentinel previous-hash used for the first event in a bundle. */
|
|
13
|
+
export const GENESIS_HASH = "0".repeat(64);
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Compute the SHA-256 hash for an event given the previous hash.
|
|
17
|
+
*/
|
|
18
|
+
export function computeHash(event: ATBEvent, prevHash: string): string {
|
|
19
|
+
const canonical = canonicalize(event);
|
|
20
|
+
return createHash("sha256")
|
|
21
|
+
.update(prevHash, "utf8")
|
|
22
|
+
.update(canonical, "utf8")
|
|
23
|
+
.digest("hex");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Compute and assign hashes for a sequence of events.
|
|
28
|
+
* Mutates each event's `seq` and `prevHash` fields in-place.
|
|
29
|
+
*
|
|
30
|
+
* @returns Array of hex-encoded SHA-256 hashes, one per event.
|
|
31
|
+
*/
|
|
32
|
+
export function chainEvents(events: ATBEvent[]): string[] {
|
|
33
|
+
const hashes: string[] = [];
|
|
34
|
+
let prev = GENESIS_HASH;
|
|
35
|
+
for (let i = 0; i < events.length; i++) {
|
|
36
|
+
events[i].seq = i + 1;
|
|
37
|
+
events[i].prevHash = prev;
|
|
38
|
+
const h = computeHash(events[i], prev);
|
|
39
|
+
hashes.push(h);
|
|
40
|
+
prev = h;
|
|
41
|
+
}
|
|
42
|
+
return hashes;
|
|
43
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @atb-dev/sdk — ATB (Agent Trace Bundle) TypeScript SDK
|
|
3
|
+
*
|
|
4
|
+
* Tamper-evident, replayable audit trails for AI agent workflows.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```ts
|
|
8
|
+
* import { Bundle } from "@atb-dev/sdk";
|
|
9
|
+
*
|
|
10
|
+
* const bundle = new Bundle();
|
|
11
|
+
* bundle.append("dev.session", { date: "2025-01-15" });
|
|
12
|
+
* bundle.save();
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
export { Bundle, ATBVerificationError } from "./bundle.js";
|
|
17
|
+
export { computeHash, chainEvents, GENESIS_HASH } from "./hash.js";
|
|
18
|
+
export { canonicalize } from "./canonicalize.js";
|
|
19
|
+
export type { ATBEvent, ATBRecord, BundleOptions } from "./types.js";
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core type definitions for the ATB TypeScript SDK.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/** A single auditable event in an ATB bundle. */
|
|
6
|
+
export interface ATBEvent {
|
|
7
|
+
/** 1-based sequence number within the bundle. */
|
|
8
|
+
seq: number;
|
|
9
|
+
/** Hex-encoded SHA-256 hash of the preceding event (or GENESIS_HASH). */
|
|
10
|
+
prevHash: string;
|
|
11
|
+
/** Dot-namespaced event type identifier. */
|
|
12
|
+
type: string;
|
|
13
|
+
/** Arbitrary JSON-serialisable payload. */
|
|
14
|
+
data: unknown;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** A single record in an ATB bundle file (event + its hash). */
|
|
18
|
+
export interface ATBRecord {
|
|
19
|
+
event: ATBEvent;
|
|
20
|
+
hash: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Options for creating a Bundle. */
|
|
24
|
+
export interface BundleOptions {
|
|
25
|
+
/** Path to the bundle file. Defaults to `run.atb/bundle.atb`. */
|
|
26
|
+
path?: string;
|
|
27
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "NodeNext",
|
|
5
|
+
"moduleResolution": "NodeNext",
|
|
6
|
+
"lib": ["ES2022"],
|
|
7
|
+
"outDir": "dist",
|
|
8
|
+
"rootDir": "src",
|
|
9
|
+
"declaration": true,
|
|
10
|
+
"declarationMap": true,
|
|
11
|
+
"sourceMap": true,
|
|
12
|
+
"strict": true,
|
|
13
|
+
"esModuleInterop": true,
|
|
14
|
+
"skipLibCheck": true,
|
|
15
|
+
"forceConsistentCasingInFileNames": true
|
|
16
|
+
},
|
|
17
|
+
"include": ["src/**/*"],
|
|
18
|
+
"exclude": ["node_modules", "dist"]
|
|
19
|
+
}
|