@longarc/mdash 3.1.2 → 3.1.3
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 +86 -23
- package/SECURITY.md +254 -0
- package/dist/accountability/engine.d.ts +27 -0
- package/dist/accountability/engine.d.ts.map +1 -0
- package/dist/accountability/engine.js +148 -0
- package/dist/accountability/engine.js.map +1 -0
- package/dist/accountability/types.d.ts +46 -0
- package/dist/accountability/types.d.ts.map +1 -0
- package/dist/accountability/types.js +8 -0
- package/dist/accountability/types.js.map +1 -0
- package/dist/checkpoint/engine.d.ts.map +1 -1
- package/dist/checkpoint/engine.js +4 -0
- package/dist/checkpoint/engine.js.map +1 -1
- package/dist/context/compose.d.ts +62 -0
- package/dist/context/compose.d.ts.map +1 -0
- package/dist/context/compose.js +286 -0
- package/dist/context/compose.js.map +1 -0
- package/dist/context/crypto/hash.d.ts +100 -0
- package/dist/context/crypto/hash.d.ts.map +1 -0
- package/dist/context/crypto/hash.js +248 -0
- package/dist/context/crypto/hash.js.map +1 -0
- package/dist/context/crypto/hmac.d.ts +80 -0
- package/dist/context/crypto/hmac.d.ts.map +1 -0
- package/dist/context/crypto/hmac.js +192 -0
- package/dist/context/crypto/hmac.js.map +1 -0
- package/dist/context/crypto/index.d.ts +7 -0
- package/dist/context/crypto/index.d.ts.map +1 -0
- package/dist/context/crypto/index.js +7 -0
- package/dist/context/crypto/index.js.map +1 -0
- package/dist/context/engine-v3.0-backup.d.ts +197 -0
- package/dist/context/engine-v3.0-backup.d.ts.map +1 -0
- package/dist/context/engine-v3.0-backup.js +392 -0
- package/dist/context/engine-v3.0-backup.js.map +1 -0
- package/dist/context/fragment.d.ts +99 -0
- package/dist/context/fragment.d.ts.map +1 -0
- package/dist/context/fragment.js +316 -0
- package/dist/context/fragment.js.map +1 -0
- package/dist/context/index.d.ts +99 -0
- package/dist/context/index.d.ts.map +1 -0
- package/dist/context/index.js +180 -0
- package/dist/context/index.js.map +1 -0
- package/dist/context/provenance.d.ts +80 -0
- package/dist/context/provenance.d.ts.map +1 -0
- package/dist/context/provenance.js +294 -0
- package/dist/context/provenance.js.map +1 -0
- package/dist/context/resolve.d.ts +106 -0
- package/dist/context/resolve.d.ts.map +1 -0
- package/dist/context/resolve.js +440 -0
- package/dist/context/resolve.js.map +1 -0
- package/dist/context/store.d.ts +156 -0
- package/dist/context/store.d.ts.map +1 -0
- package/dist/context/store.js +396 -0
- package/dist/context/store.js.map +1 -0
- package/dist/context/types.d.ts +463 -0
- package/dist/context/types.d.ts.map +1 -0
- package/dist/context/types.js +94 -0
- package/dist/context/types.js.map +1 -0
- package/dist/context/utils/atomic.d.ts +76 -0
- package/dist/context/utils/atomic.d.ts.map +1 -0
- package/dist/context/utils/atomic.js +159 -0
- package/dist/context/utils/atomic.js.map +1 -0
- package/dist/context/utils/credit.d.ts +65 -0
- package/dist/context/utils/credit.d.ts.map +1 -0
- package/dist/context/utils/credit.js +164 -0
- package/dist/context/utils/credit.js.map +1 -0
- package/dist/context/utils/index.d.ts +13 -0
- package/dist/context/utils/index.d.ts.map +1 -0
- package/dist/context/utils/index.js +13 -0
- package/dist/context/utils/index.js.map +1 -0
- package/dist/context/utils/utility.d.ts +63 -0
- package/dist/context/utils/utility.d.ts.map +1 -0
- package/dist/context/utils/utility.js +141 -0
- package/dist/context/utils/utility.js.map +1 -0
- package/dist/core/commitment.d.ts +25 -2
- package/dist/core/commitment.d.ts.map +1 -1
- package/dist/core/commitment.js +44 -6
- package/dist/core/commitment.js.map +1 -1
- package/dist/core/crypto.d.ts +2 -0
- package/dist/core/crypto.d.ts.map +1 -1
- package/dist/core/crypto.js +12 -0
- package/dist/core/crypto.js.map +1 -1
- package/dist/index.d.ts +11 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +35 -10
- package/dist/index.js.map +1 -1
- package/dist/mcca/engine.d.ts.map +1 -1
- package/dist/mcca/engine.js +5 -4
- package/dist/mcca/engine.js.map +1 -1
- package/dist/physics/engine.d.ts +1 -0
- package/dist/physics/engine.d.ts.map +1 -1
- package/dist/physics/engine.js +36 -2
- package/dist/physics/engine.js.map +1 -1
- package/dist/provenance/api-handler.d.ts +45 -0
- package/dist/provenance/api-handler.d.ts.map +1 -0
- package/dist/provenance/api-handler.js +223 -0
- package/dist/provenance/api-handler.js.map +1 -0
- package/dist/provenance/api-types.d.ts +108 -0
- package/dist/provenance/api-types.d.ts.map +1 -0
- package/dist/provenance/api-types.js +9 -0
- package/dist/provenance/api-types.js.map +1 -0
- package/dist/provenance/index.d.ts +6 -0
- package/dist/provenance/index.d.ts.map +1 -0
- package/dist/provenance/index.js +3 -0
- package/dist/provenance/index.js.map +1 -0
- package/dist/provenance/provenance-engine.d.ts +63 -0
- package/dist/provenance/provenance-engine.d.ts.map +1 -0
- package/dist/provenance/provenance-engine.js +311 -0
- package/dist/provenance/provenance-engine.js.map +1 -0
- package/dist/provenance/types.d.ts +193 -0
- package/dist/provenance/types.d.ts.map +1 -0
- package/dist/provenance/types.js +9 -0
- package/dist/provenance/types.js.map +1 -0
- package/dist/tee/engine.d.ts.map +1 -1
- package/dist/tee/engine.js +14 -0
- package/dist/tee/engine.js.map +1 -1
- package/dist/warrant/engine.d.ts +24 -1
- package/dist/warrant/engine.d.ts.map +1 -1
- package/dist/warrant/engine.js +76 -1
- package/dist/warrant/engine.js.map +1 -1
- package/dist/zk/engine.d.ts.map +1 -1
- package/dist/zk/engine.js +7 -4
- package/dist/zk/engine.js.map +1 -1
- package/docs/SECURITY-PATCHES.md +170 -0
- package/package.json +17 -5
- package/src/__tests__/accountability.test.ts +308 -0
- package/src/__tests__/l1-verification-modes.test.ts +424 -0
- package/src/__tests__/phase1.benchmark.test.ts +94 -0
- package/src/__tests__/phase1.test.ts +0 -77
- package/src/__tests__/phase2-4.benchmark.test.ts +60 -0
- package/src/__tests__/phase2-4.test.ts +1 -52
- package/src/__tests__/provenance/api-handler.test.ts +356 -0
- package/src/__tests__/provenance/provenance-engine.test.ts +628 -0
- package/src/__tests__/sa-2026-008.test.ts +45 -0
- package/src/__tests__/sa-2026-009.test.ts +86 -0
- package/src/__tests__/sa-2026-010.test.ts +72 -0
- package/src/__tests__/sa-2026-012.test.ts +65 -0
- package/src/__tests__/sa-2026-nfc.test.ts +40 -0
- package/src/__tests__/security.test.ts +786 -0
- package/src/accountability/engine.ts +230 -0
- package/src/accountability/types.ts +58 -0
- package/src/checkpoint/engine.ts +4 -0
- package/src/context/__tests__/caret-v0.2.0.test.ts +860 -0
- package/src/context/__tests__/integration.test.ts +356 -0
- package/src/context/compose.ts +388 -0
- package/src/context/crypto/hash.ts +277 -0
- package/src/context/crypto/hmac.ts +253 -0
- package/src/context/crypto/index.ts +29 -0
- package/src/context/engine-v3.0-backup.ts +598 -0
- package/src/context/fragment.ts +454 -0
- package/src/context/index.ts +427 -0
- package/src/context/provenance.ts +380 -0
- package/src/context/resolve.ts +581 -0
- package/src/context/store.ts +503 -0
- package/src/context/types.ts +679 -0
- package/src/context/utils/atomic.ts +207 -0
- package/src/context/utils/credit.ts +224 -0
- package/src/context/utils/index.ts +13 -0
- package/src/context/utils/utility.ts +200 -0
- package/src/core/commitment.ts +129 -67
- package/src/core/crypto.ts +13 -0
- package/src/index.ts +62 -10
- package/src/mcca/engine.ts +5 -4
- package/src/physics/engine.ts +40 -3
- package/src/provenance/api-handler.ts +248 -0
- package/src/provenance/api-types.ts +112 -0
- package/src/provenance/index.ts +19 -0
- package/src/provenance/provenance-engine.ts +387 -0
- package/src/provenance/types.ts +211 -0
- package/src/tee/engine.ts +16 -0
- package/src/warrant/engine.ts +89 -1
- package/src/zk/engine.ts +8 -4
- package/tsconfig.json +1 -1
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Caret — Manifold Composition Engine
|
|
3
|
+
* @module @longarcstudios/caret/compose
|
|
4
|
+
*
|
|
5
|
+
* Compose multiple context fragments into a single manifold.
|
|
6
|
+
* Handles token budgeting, semantic compression, and provenance merging.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type {
|
|
10
|
+
ContextFragment,
|
|
11
|
+
Manifold,
|
|
12
|
+
FragmentId,
|
|
13
|
+
Timestamp,
|
|
14
|
+
Seal,
|
|
15
|
+
TokenCount,
|
|
16
|
+
ComposeOptions,
|
|
17
|
+
CompositionStrategy,
|
|
18
|
+
TrustLevel,
|
|
19
|
+
} from './types.js';
|
|
20
|
+
import { sha256Object } from './crypto/hash.js';
|
|
21
|
+
import { hmacSeal } from './crypto/hmac.js';
|
|
22
|
+
import { mergeChains, getMinTrust } from './provenance.js';
|
|
23
|
+
import { createSemanticUnit, getFragmentTrust } from './fragment.js';
|
|
24
|
+
|
|
25
|
+
// ============================================================================
|
|
26
|
+
// TOKEN PARSING
|
|
27
|
+
// ============================================================================
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Parse a token ceiling specification.
|
|
31
|
+
* Accepts number or string like "8k tokens", "16k", "4096"
|
|
32
|
+
*/
|
|
33
|
+
export function parseTokenCeiling(ceiling: TokenCount | string): TokenCount {
|
|
34
|
+
if (typeof ceiling === 'number') {
|
|
35
|
+
return ceiling as TokenCount;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const str = ceiling.toLowerCase().replace(/\s+/g, '').replace('tokens', '');
|
|
39
|
+
|
|
40
|
+
// Handle k/K suffix
|
|
41
|
+
if (str.endsWith('k')) {
|
|
42
|
+
const num = parseFloat(str.slice(0, -1));
|
|
43
|
+
return Math.floor(num * 1000) as TokenCount;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Handle m/M suffix (millions)
|
|
47
|
+
if (str.endsWith('m')) {
|
|
48
|
+
const num = parseFloat(str.slice(0, -1));
|
|
49
|
+
return Math.floor(num * 1000000) as TokenCount;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return parseInt(str, 10) as TokenCount;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ============================================================================
|
|
56
|
+
// COMPOSITION STRATEGIES
|
|
57
|
+
// ============================================================================
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Sort fragments by strategy.
|
|
61
|
+
*/
|
|
62
|
+
function sortByStrategy(
|
|
63
|
+
fragments: readonly ContextFragment[],
|
|
64
|
+
strategy: CompositionStrategy
|
|
65
|
+
): ContextFragment[] {
|
|
66
|
+
const copy = [...fragments];
|
|
67
|
+
|
|
68
|
+
switch (strategy) {
|
|
69
|
+
case 'concatenate':
|
|
70
|
+
// Keep original order
|
|
71
|
+
return copy;
|
|
72
|
+
|
|
73
|
+
case 'priority':
|
|
74
|
+
// Sort by trust level (highest first)
|
|
75
|
+
return copy.sort((a, b) => getFragmentTrust(b) - getFragmentTrust(a));
|
|
76
|
+
|
|
77
|
+
case 'recency':
|
|
78
|
+
// Sort by sealed_at (most recent first)
|
|
79
|
+
return copy.sort((a, b) =>
|
|
80
|
+
new Date(b.sealed_at).getTime() - new Date(a.sealed_at).getTime()
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
case 'semantic':
|
|
84
|
+
// For now, same as priority — true semantic dedup would need embeddings
|
|
85
|
+
return copy.sort((a, b) => getFragmentTrust(b) - getFragmentTrust(a));
|
|
86
|
+
|
|
87
|
+
default:
|
|
88
|
+
return copy;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Select fragments that fit within token ceiling.
|
|
94
|
+
*/
|
|
95
|
+
function selectFragments(
|
|
96
|
+
fragments: readonly ContextFragment[],
|
|
97
|
+
ceiling: TokenCount,
|
|
98
|
+
minTrust?: TrustLevel
|
|
99
|
+
): {
|
|
100
|
+
included: ContextFragment[];
|
|
101
|
+
excluded: ContextFragment[];
|
|
102
|
+
totalTokens: TokenCount;
|
|
103
|
+
} {
|
|
104
|
+
const included: ContextFragment[] = [];
|
|
105
|
+
const excluded: ContextFragment[] = [];
|
|
106
|
+
let totalTokens = 0 as TokenCount;
|
|
107
|
+
|
|
108
|
+
for (const fragment of fragments) {
|
|
109
|
+
// Check trust level filter
|
|
110
|
+
if (minTrust !== undefined && getFragmentTrust(fragment) < minTrust) {
|
|
111
|
+
excluded.push(fragment);
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const fragmentTokens = fragment.content.token_count;
|
|
116
|
+
|
|
117
|
+
// Check if adding this fragment would exceed ceiling
|
|
118
|
+
if ((totalTokens as number) + (fragmentTokens as number) <= (ceiling as number)) {
|
|
119
|
+
included.push(fragment);
|
|
120
|
+
totalTokens = ((totalTokens as number) + (fragmentTokens as number)) as TokenCount;
|
|
121
|
+
} else {
|
|
122
|
+
excluded.push(fragment);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return { included, excluded, totalTokens };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ============================================================================
|
|
130
|
+
// MANIFOLD CREATION
|
|
131
|
+
// ============================================================================
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Generate a UUID v4 for manifold ID.
|
|
135
|
+
* SECURITY: Never falls back to Math.random — fails instead.
|
|
136
|
+
*/
|
|
137
|
+
function generateManifoldId(): FragmentId {
|
|
138
|
+
if (typeof crypto !== 'undefined' && crypto.randomUUID) {
|
|
139
|
+
return crypto.randomUUID() as FragmentId;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (typeof crypto !== 'undefined' && crypto.getRandomValues) {
|
|
143
|
+
const bytes = new Uint8Array(16);
|
|
144
|
+
crypto.getRandomValues(bytes);
|
|
145
|
+
|
|
146
|
+
bytes[6] = (bytes[6]! & 0x0f) | 0x40;
|
|
147
|
+
bytes[8] = (bytes[8]! & 0x3f) | 0x80;
|
|
148
|
+
|
|
149
|
+
const hex = Array.from(bytes, b => b.toString(16).padStart(2, '0')).join('');
|
|
150
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}` as FragmentId;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
throw new Error('Cryptographically secure random number generator not available');
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Compose multiple fragments into a manifold.
|
|
158
|
+
*
|
|
159
|
+
* @param fragments - The fragments to compose
|
|
160
|
+
* @param options - Composition options
|
|
161
|
+
* @param seal_key - Secret key for sealing
|
|
162
|
+
* @returns Promise resolving to the composed manifold
|
|
163
|
+
*
|
|
164
|
+
* @example
|
|
165
|
+
* ```ts
|
|
166
|
+
* const manifold = await compose([
|
|
167
|
+
* policyFragment,
|
|
168
|
+
* userContextFragment,
|
|
169
|
+
* agentStateFragment
|
|
170
|
+
* ], { ceiling: '8k tokens' }, 'secret-key');
|
|
171
|
+
* ```
|
|
172
|
+
*/
|
|
173
|
+
export async function compose<T = unknown>(
|
|
174
|
+
fragments: readonly ContextFragment[],
|
|
175
|
+
options: ComposeOptions,
|
|
176
|
+
seal_key: string
|
|
177
|
+
): Promise<Manifold<T>> {
|
|
178
|
+
if (fragments.length === 0) {
|
|
179
|
+
throw new Error('Cannot compose empty fragment array');
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const ceiling = parseTokenCeiling(options.ceiling);
|
|
183
|
+
const strategy = options.strategy ?? 'priority';
|
|
184
|
+
const preserveProvenance = options.preserve_provenance ?? true;
|
|
185
|
+
const minTrust = options.min_trust as TrustLevel | undefined;
|
|
186
|
+
|
|
187
|
+
// Sort fragments by strategy
|
|
188
|
+
const sorted = sortByStrategy(fragments, strategy);
|
|
189
|
+
|
|
190
|
+
// Select fragments within ceiling
|
|
191
|
+
const { included, excluded, totalTokens } = selectFragments(
|
|
192
|
+
sorted,
|
|
193
|
+
ceiling,
|
|
194
|
+
minTrust
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
if (included.length === 0) {
|
|
198
|
+
throw new Error('No fragments fit within token ceiling');
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Compose content
|
|
202
|
+
const composedData = composeContent(included);
|
|
203
|
+
const content = createSemanticUnit<T>(composedData as T, {
|
|
204
|
+
type: 'composite',
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
// Merge provenance chains
|
|
208
|
+
const firstIncluded = included[0];
|
|
209
|
+
if (!firstIncluded) {
|
|
210
|
+
throw new Error('No fragments to compose');
|
|
211
|
+
}
|
|
212
|
+
const provenance = preserveProvenance
|
|
213
|
+
? await mergeChains(included.map(f => f.provenance))
|
|
214
|
+
: firstIncluded.provenance;
|
|
215
|
+
|
|
216
|
+
// Generate manifold ID
|
|
217
|
+
const id = generateManifoldId();
|
|
218
|
+
|
|
219
|
+
// Compute hash
|
|
220
|
+
const hash = await sha256Object({
|
|
221
|
+
id,
|
|
222
|
+
content,
|
|
223
|
+
sources: included.map(f => f.id),
|
|
224
|
+
provenance,
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
// Create composition metadata
|
|
228
|
+
const composition = {
|
|
229
|
+
strategy,
|
|
230
|
+
ceiling,
|
|
231
|
+
actual_tokens: totalTokens,
|
|
232
|
+
fragments_included: included.length,
|
|
233
|
+
fragments_excluded: excluded.length,
|
|
234
|
+
compression_applied: false, // TODO: implement compression
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
const created_at = new Date().toISOString() as Timestamp;
|
|
238
|
+
|
|
239
|
+
// Create seal
|
|
240
|
+
const seal = await hmacSeal({
|
|
241
|
+
id,
|
|
242
|
+
hash,
|
|
243
|
+
content,
|
|
244
|
+
sources: included.map(f => f.id),
|
|
245
|
+
provenance,
|
|
246
|
+
composition,
|
|
247
|
+
created_at,
|
|
248
|
+
}, seal_key) as Seal;
|
|
249
|
+
|
|
250
|
+
return {
|
|
251
|
+
id,
|
|
252
|
+
hash,
|
|
253
|
+
content,
|
|
254
|
+
sources: included.map(f => f.id),
|
|
255
|
+
provenance,
|
|
256
|
+
composition,
|
|
257
|
+
created_at,
|
|
258
|
+
seal,
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Compose content from multiple fragments.
|
|
264
|
+
* For now, creates a structured object with all content.
|
|
265
|
+
* More sophisticated merging could be added later.
|
|
266
|
+
*/
|
|
267
|
+
function composeContent(fragments: ContextFragment[]): unknown {
|
|
268
|
+
// For text fragments, concatenate
|
|
269
|
+
const textFragments = fragments.filter(f => f.content.type === 'text');
|
|
270
|
+
if (textFragments.length === fragments.length) {
|
|
271
|
+
return textFragments.map(f => f.content.data).join('\n\n---\n\n');
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// For mixed types, create structured object
|
|
275
|
+
return {
|
|
276
|
+
fragments: fragments.map(f => ({
|
|
277
|
+
id: f.id,
|
|
278
|
+
type: f.content.type,
|
|
279
|
+
data: f.content.data,
|
|
280
|
+
source: f.provenance.head.source,
|
|
281
|
+
trust: f.provenance.head.trust_level,
|
|
282
|
+
})),
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// ============================================================================
|
|
287
|
+
// MANIFOLD OPERATIONS
|
|
288
|
+
// ============================================================================
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Verify a manifold's integrity.
|
|
292
|
+
*/
|
|
293
|
+
export async function verifyManifold(
|
|
294
|
+
manifold: Manifold,
|
|
295
|
+
seal_key: string
|
|
296
|
+
): Promise<{
|
|
297
|
+
valid: boolean;
|
|
298
|
+
errors: string[];
|
|
299
|
+
}> {
|
|
300
|
+
const errors: string[] = [];
|
|
301
|
+
|
|
302
|
+
// Verify hash
|
|
303
|
+
const expectedHash = await sha256Object({
|
|
304
|
+
id: manifold.id,
|
|
305
|
+
content: manifold.content,
|
|
306
|
+
sources: manifold.sources,
|
|
307
|
+
provenance: manifold.provenance,
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
if (expectedHash !== manifold.hash) {
|
|
311
|
+
errors.push('Manifold hash mismatch');
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Verify seal
|
|
315
|
+
const expectedSeal = await hmacSeal({
|
|
316
|
+
id: manifold.id,
|
|
317
|
+
hash: manifold.hash,
|
|
318
|
+
content: manifold.content,
|
|
319
|
+
sources: manifold.sources,
|
|
320
|
+
provenance: manifold.provenance,
|
|
321
|
+
composition: manifold.composition,
|
|
322
|
+
created_at: manifold.created_at,
|
|
323
|
+
}, seal_key);
|
|
324
|
+
|
|
325
|
+
if (expectedSeal !== manifold.seal) {
|
|
326
|
+
errors.push('Manifold seal verification failed');
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
return {
|
|
330
|
+
valid: errors.length === 0,
|
|
331
|
+
errors,
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Get summary statistics for a manifold.
|
|
337
|
+
*/
|
|
338
|
+
export function getManifoldStats(manifold: Manifold): {
|
|
339
|
+
tokens: TokenCount;
|
|
340
|
+
fragments: number;
|
|
341
|
+
utilization: number;
|
|
342
|
+
minTrust: TrustLevel;
|
|
343
|
+
} {
|
|
344
|
+
return {
|
|
345
|
+
tokens: manifold.composition.actual_tokens,
|
|
346
|
+
fragments: manifold.composition.fragments_included,
|
|
347
|
+
utilization: (manifold.composition.actual_tokens as number) / (manifold.composition.ceiling as number),
|
|
348
|
+
minTrust: getMinTrust(manifold.provenance),
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Check if a manifold is within a given token budget.
|
|
354
|
+
*/
|
|
355
|
+
export function fitsInBudget(manifold: Manifold, budget: TokenCount | string): boolean {
|
|
356
|
+
const budgetTokens = parseTokenCeiling(budget);
|
|
357
|
+
return (manifold.content.token_count as number) <= (budgetTokens as number);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// ============================================================================
|
|
361
|
+
// RECOMPOSITION
|
|
362
|
+
// ============================================================================
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Recompose a manifold with a different token ceiling.
|
|
366
|
+
* Requires access to original fragments.
|
|
367
|
+
*/
|
|
368
|
+
export async function recompose<T = unknown>(
|
|
369
|
+
originalFragments: readonly ContextFragment[],
|
|
370
|
+
newOptions: ComposeOptions,
|
|
371
|
+
seal_key: string
|
|
372
|
+
): Promise<Manifold<T>> {
|
|
373
|
+
return compose(originalFragments, newOptions, seal_key);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Extend a manifold with additional fragments.
|
|
378
|
+
* Creates a new manifold (immutable).
|
|
379
|
+
*/
|
|
380
|
+
export async function extendManifold<T = unknown>(
|
|
381
|
+
originalFragments: readonly ContextFragment[],
|
|
382
|
+
additionalFragments: readonly ContextFragment[],
|
|
383
|
+
options: ComposeOptions,
|
|
384
|
+
seal_key: string
|
|
385
|
+
): Promise<Manifold<T>> {
|
|
386
|
+
const allFragments = [...originalFragments, ...additionalFragments];
|
|
387
|
+
return compose(allFragments, options, seal_key);
|
|
388
|
+
}
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Caret — Cryptographic Hash Utilities
|
|
3
|
+
* @module @longarcstudios/caret/crypto/hash
|
|
4
|
+
*
|
|
5
|
+
* SHA-256 implementation using Web Crypto API (browser + Node compatible).
|
|
6
|
+
* All hashing is deterministic — same input always produces same output.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { Hash } from '../types.js';
|
|
10
|
+
|
|
11
|
+
// ============================================================================
|
|
12
|
+
// CONSTANTS
|
|
13
|
+
// ============================================================================
|
|
14
|
+
|
|
15
|
+
/** SHA-256 produces 256 bits = 64 hex characters */
|
|
16
|
+
const HASH_LENGTH = 64;
|
|
17
|
+
|
|
18
|
+
/** Algorithm identifier for Web Crypto */
|
|
19
|
+
const ALGORITHM = 'SHA-256';
|
|
20
|
+
|
|
21
|
+
// ============================================================================
|
|
22
|
+
// TEXT ENCODER
|
|
23
|
+
// ============================================================================
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Get TextEncoder instance.
|
|
27
|
+
* Works in both browser and Node.js environments.
|
|
28
|
+
*/
|
|
29
|
+
const getEncoder = (): TextEncoder => {
|
|
30
|
+
if (typeof TextEncoder !== 'undefined') {
|
|
31
|
+
return new TextEncoder();
|
|
32
|
+
}
|
|
33
|
+
// Node.js fallback (though TextEncoder is available in Node 11+)
|
|
34
|
+
throw new Error('TextEncoder not available');
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// ============================================================================
|
|
38
|
+
// CRYPTO IMPLEMENTATION
|
|
39
|
+
// ============================================================================
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Get the crypto implementation for the current environment.
|
|
43
|
+
* Supports browser (window.crypto) and Node.js (crypto.subtle).
|
|
44
|
+
*/
|
|
45
|
+
async function getCrypto(): Promise<SubtleCrypto> {
|
|
46
|
+
// Browser environment
|
|
47
|
+
if (typeof window !== 'undefined' && window.crypto?.subtle) {
|
|
48
|
+
return window.crypto.subtle;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Node.js environment
|
|
52
|
+
if (typeof globalThis !== 'undefined' && (globalThis as any).crypto?.subtle) {
|
|
53
|
+
return (globalThis as any).crypto.subtle;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Node.js < 19 fallback
|
|
57
|
+
try {
|
|
58
|
+
const crypto = await import('crypto');
|
|
59
|
+
if (crypto.webcrypto?.subtle) {
|
|
60
|
+
return crypto.webcrypto.subtle as SubtleCrypto;
|
|
61
|
+
}
|
|
62
|
+
} catch {
|
|
63
|
+
// Import failed
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
throw new Error('No crypto implementation available');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Convert ArrayBuffer to hexadecimal string.
|
|
71
|
+
*/
|
|
72
|
+
function bufferToHex(buffer: ArrayBuffer): string {
|
|
73
|
+
const bytes = new Uint8Array(buffer);
|
|
74
|
+
let hex = '';
|
|
75
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
76
|
+
const byte = bytes[i];
|
|
77
|
+
if (byte !== undefined) {
|
|
78
|
+
hex += byte.toString(16).padStart(2, '0');
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return hex;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Convert hexadecimal string to ArrayBuffer.
|
|
86
|
+
*/
|
|
87
|
+
export function hexToBuffer(hex: string): ArrayBuffer {
|
|
88
|
+
if (hex.length % 2 !== 0) {
|
|
89
|
+
throw new Error('Invalid hex string length');
|
|
90
|
+
}
|
|
91
|
+
const bytes = new Uint8Array(hex.length / 2);
|
|
92
|
+
for (let i = 0; i < hex.length; i += 2) {
|
|
93
|
+
bytes[i / 2] = parseInt(hex.slice(i, i + 2), 16);
|
|
94
|
+
}
|
|
95
|
+
return bytes.buffer;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ============================================================================
|
|
99
|
+
// PUBLIC API
|
|
100
|
+
// ============================================================================
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Compute SHA-256 hash of a string.
|
|
104
|
+
*
|
|
105
|
+
* @param input - The string to hash
|
|
106
|
+
* @returns Promise resolving to the hash as a branded Hash type
|
|
107
|
+
*
|
|
108
|
+
* @example
|
|
109
|
+
* ```ts
|
|
110
|
+
* const hash = await sha256('hello world');
|
|
111
|
+
* // => 'b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9'
|
|
112
|
+
* ```
|
|
113
|
+
*/
|
|
114
|
+
export async function sha256(input: string): Promise<Hash> {
|
|
115
|
+
const crypto = await getCrypto();
|
|
116
|
+
const encoder = getEncoder();
|
|
117
|
+
const data = encoder.encode(input);
|
|
118
|
+
const hashBuffer = await crypto.digest(ALGORITHM, data);
|
|
119
|
+
return bufferToHex(hashBuffer) as Hash;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Compute SHA-256 hash of binary data.
|
|
124
|
+
*
|
|
125
|
+
* @param data - The binary data to hash
|
|
126
|
+
* @returns Promise resolving to the hash
|
|
127
|
+
*/
|
|
128
|
+
export async function sha256Binary(data: ArrayBuffer | Uint8Array): Promise<Hash> {
|
|
129
|
+
const crypto = await getCrypto();
|
|
130
|
+
const buffer = data instanceof Uint8Array ? new Uint8Array(data).buffer as ArrayBuffer : data;
|
|
131
|
+
const hashBuffer = await crypto.digest(ALGORITHM, buffer);
|
|
132
|
+
return bufferToHex(hashBuffer) as Hash;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Compute SHA-256 hash of a JSON-serializable object.
|
|
137
|
+
* Uses deterministic JSON serialization (sorted keys).
|
|
138
|
+
*
|
|
139
|
+
* @param obj - The object to hash
|
|
140
|
+
* @returns Promise resolving to the hash
|
|
141
|
+
*
|
|
142
|
+
* @example
|
|
143
|
+
* ```ts
|
|
144
|
+
* const hash = await sha256Object({ b: 2, a: 1 });
|
|
145
|
+
* // Same as sha256Object({ a: 1, b: 2 }) — deterministic
|
|
146
|
+
* ```
|
|
147
|
+
*/
|
|
148
|
+
export async function sha256Object(obj: unknown): Promise<Hash> {
|
|
149
|
+
const json = deterministicStringify(obj);
|
|
150
|
+
return sha256(json);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Compute a rolling hash combining multiple hashes.
|
|
155
|
+
* Used for chain verification in provenance tracking.
|
|
156
|
+
*
|
|
157
|
+
* @param hashes - Array of hashes to combine
|
|
158
|
+
* @returns Promise resolving to the combined hash
|
|
159
|
+
*/
|
|
160
|
+
export async function rollingHash(hashes: readonly Hash[]): Promise<Hash> {
|
|
161
|
+
if (hashes.length === 0) {
|
|
162
|
+
// Hash of empty string for empty input
|
|
163
|
+
return sha256('');
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (hashes.length === 1) {
|
|
167
|
+
const first = hashes[0];
|
|
168
|
+
if (!first) return sha256('');
|
|
169
|
+
return first;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Concatenate all hashes and hash the result
|
|
173
|
+
const combined = hashes.join('');
|
|
174
|
+
return sha256(combined);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Verify that a hash matches expected content.
|
|
179
|
+
*
|
|
180
|
+
* @param content - The content to verify
|
|
181
|
+
* @param expectedHash - The expected hash
|
|
182
|
+
* @returns Promise resolving to boolean
|
|
183
|
+
*/
|
|
184
|
+
export async function verifyHash(content: string, expectedHash: Hash): Promise<boolean> {
|
|
185
|
+
const actualHash = await sha256(content);
|
|
186
|
+
return constantTimeEqual(actualHash, expectedHash);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Verify that an object's hash matches expected.
|
|
191
|
+
*
|
|
192
|
+
* @param obj - The object to verify
|
|
193
|
+
* @param expectedHash - The expected hash
|
|
194
|
+
* @returns Promise resolving to boolean
|
|
195
|
+
*/
|
|
196
|
+
export async function verifyObjectHash(obj: unknown, expectedHash: Hash): Promise<boolean> {
|
|
197
|
+
const actualHash = await sha256Object(obj);
|
|
198
|
+
return constantTimeEqual(actualHash, expectedHash);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// ============================================================================
|
|
202
|
+
// UTILITY FUNCTIONS
|
|
203
|
+
// ============================================================================
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Deterministic JSON stringify with sorted keys.
|
|
207
|
+
* Ensures same object always produces same string.
|
|
208
|
+
*/
|
|
209
|
+
export function deterministicStringify(obj: unknown): string {
|
|
210
|
+
return JSON.stringify(obj, (_, value) => {
|
|
211
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
212
|
+
// Sort object keys
|
|
213
|
+
const sorted: Record<string, unknown> = {};
|
|
214
|
+
const keys = Object.keys(value).sort();
|
|
215
|
+
for (const key of keys) {
|
|
216
|
+
sorted[key] = value[key];
|
|
217
|
+
}
|
|
218
|
+
return sorted;
|
|
219
|
+
}
|
|
220
|
+
return value;
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Constant-time string comparison to prevent timing attacks.
|
|
226
|
+
*
|
|
227
|
+
* @param a - First string
|
|
228
|
+
* @param b - Second string
|
|
229
|
+
* @returns true if equal, false otherwise
|
|
230
|
+
*/
|
|
231
|
+
export function constantTimeEqual(a: string, b: string): boolean {
|
|
232
|
+
// Use the longer length to prevent early exit timing leak
|
|
233
|
+
const len = Math.max(a.length, b.length);
|
|
234
|
+
|
|
235
|
+
// Track both the XOR result and length mismatch
|
|
236
|
+
let result = a.length ^ b.length; // Will be non-zero if lengths differ
|
|
237
|
+
|
|
238
|
+
for (let i = 0; i < len; i++) {
|
|
239
|
+
// Use 0 for out-of-bounds access (safe because result already tracks length diff)
|
|
240
|
+
const charA = i < a.length ? a.charCodeAt(i) : 0;
|
|
241
|
+
const charB = i < b.length ? b.charCodeAt(i) : 0;
|
|
242
|
+
result |= charA ^ charB;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return result === 0;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Validate that a string is a valid SHA-256 hash.
|
|
250
|
+
*
|
|
251
|
+
* @param value - The value to validate
|
|
252
|
+
* @returns true if valid hash format
|
|
253
|
+
*/
|
|
254
|
+
export function isValidHash(value: string): value is Hash {
|
|
255
|
+
return typeof value === 'string' &&
|
|
256
|
+
value.length === HASH_LENGTH &&
|
|
257
|
+
/^[a-f0-9]+$/i.test(value);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Create a Hash type from a raw string (unsafe — no validation).
|
|
262
|
+
* Use only when you've already validated the input.
|
|
263
|
+
*/
|
|
264
|
+
export function unsafeHash(value: string): Hash {
|
|
265
|
+
return value as Hash;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Parse a string as a Hash, throwing if invalid.
|
|
270
|
+
*/
|
|
271
|
+
export function parseHash(value: string): Hash {
|
|
272
|
+
if (!isValidHash(value)) {
|
|
273
|
+
// SECURITY: Do not leak input value in error message
|
|
274
|
+
throw new Error('Invalid hash format');
|
|
275
|
+
}
|
|
276
|
+
return value;
|
|
277
|
+
}
|