@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,454 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Caret — Context Fragment Implementation
|
|
3
|
+
* @module @longarcstudios/caret/fragment
|
|
4
|
+
*
|
|
5
|
+
* The atomic unit of structured context.
|
|
6
|
+
* Immutable, sealed, attributable.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type {
|
|
10
|
+
ContextFragment,
|
|
11
|
+
FragmentId,
|
|
12
|
+
Timestamp,
|
|
13
|
+
SemanticUnit,
|
|
14
|
+
SemanticType,
|
|
15
|
+
TokenCount,
|
|
16
|
+
Constraint,
|
|
17
|
+
ProvenanceChain,
|
|
18
|
+
Attribution,
|
|
19
|
+
SourceURI,
|
|
20
|
+
TrustLevel,
|
|
21
|
+
} from './types.js';
|
|
22
|
+
import { sha256Object } from './crypto/hash.js';
|
|
23
|
+
import { sealFragment, verifyFragmentSeal } from './crypto/hmac.js';
|
|
24
|
+
import {
|
|
25
|
+
createProvenanceRecord,
|
|
26
|
+
createProvenanceChain,
|
|
27
|
+
extendProvenanceChain,
|
|
28
|
+
} from './provenance.js';
|
|
29
|
+
|
|
30
|
+
// ============================================================================
|
|
31
|
+
// UUID GENERATION
|
|
32
|
+
// ============================================================================
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Generate a UUID v4.
|
|
36
|
+
* Uses crypto.randomUUID if available, falls back to crypto.getRandomValues.
|
|
37
|
+
* SECURITY: Never falls back to Math.random — fails instead.
|
|
38
|
+
*/
|
|
39
|
+
function generateUUID(): FragmentId {
|
|
40
|
+
// Preferred: native randomUUID
|
|
41
|
+
if (typeof crypto !== 'undefined' && crypto.randomUUID) {
|
|
42
|
+
return crypto.randomUUID() as FragmentId;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Fallback: manual UUID from getRandomValues (still cryptographically secure)
|
|
46
|
+
if (typeof crypto !== 'undefined' && crypto.getRandomValues) {
|
|
47
|
+
const bytes = new Uint8Array(16);
|
|
48
|
+
crypto.getRandomValues(bytes);
|
|
49
|
+
|
|
50
|
+
// Set version (4) and variant (RFC4122)
|
|
51
|
+
bytes[6] = (bytes[6]! & 0x0f) | 0x40;
|
|
52
|
+
bytes[8] = (bytes[8]! & 0x3f) | 0x80;
|
|
53
|
+
|
|
54
|
+
const hex = Array.from(bytes, b => b.toString(16).padStart(2, '0')).join('');
|
|
55
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}` as FragmentId;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// SECURITY: Do not fall back to Math.random — fail secure
|
|
59
|
+
throw new Error('Cryptographically secure random number generator not available');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ============================================================================
|
|
63
|
+
// TOKEN COUNTING
|
|
64
|
+
// ============================================================================
|
|
65
|
+
|
|
66
|
+
/** Default tokenizer: approximate cl100k_base behavior */
|
|
67
|
+
function defaultTokenCount(text: string): TokenCount {
|
|
68
|
+
// Rough approximation: ~4 chars per token for English
|
|
69
|
+
// More accurate would require actual tokenizer
|
|
70
|
+
const charCount = text.length;
|
|
71
|
+
const estimate = Math.ceil(charCount / 4);
|
|
72
|
+
return estimate as TokenCount;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Tokenizer function type */
|
|
76
|
+
type Tokenizer = (content: unknown) => TokenCount;
|
|
77
|
+
|
|
78
|
+
/** Current tokenizer (can be replaced) */
|
|
79
|
+
let currentTokenizer: Tokenizer = (content: unknown): TokenCount => {
|
|
80
|
+
const text = typeof content === 'string'
|
|
81
|
+
? content
|
|
82
|
+
: JSON.stringify(content);
|
|
83
|
+
return defaultTokenCount(text);
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Set a custom tokenizer function.
|
|
88
|
+
*/
|
|
89
|
+
export function setTokenizer(tokenizer: Tokenizer): void {
|
|
90
|
+
currentTokenizer = tokenizer;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Count tokens in content using current tokenizer.
|
|
95
|
+
*/
|
|
96
|
+
export function countTokens(content: unknown): TokenCount {
|
|
97
|
+
return currentTokenizer(content);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ============================================================================
|
|
101
|
+
// SEMANTIC UNIT
|
|
102
|
+
// ============================================================================
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Create a SemanticUnit from content.
|
|
106
|
+
*/
|
|
107
|
+
export function createSemanticUnit<T>(
|
|
108
|
+
data: T,
|
|
109
|
+
options: {
|
|
110
|
+
type?: SemanticType;
|
|
111
|
+
compression_ratio?: number;
|
|
112
|
+
} = {}
|
|
113
|
+
): SemanticUnit<T> {
|
|
114
|
+
const type = options.type ?? inferSemanticType(data);
|
|
115
|
+
const token_count = countTokens(data);
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
type,
|
|
119
|
+
data,
|
|
120
|
+
token_count,
|
|
121
|
+
compression_ratio: options.compression_ratio,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Infer semantic type from data.
|
|
127
|
+
*/
|
|
128
|
+
function inferSemanticType(data: unknown): SemanticType {
|
|
129
|
+
if (typeof data === 'string') {
|
|
130
|
+
return 'text';
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (typeof data === 'object' && data !== null) {
|
|
134
|
+
// Check for special object patterns
|
|
135
|
+
if ('$ref' in data || 'uri' in data || 'url' in data) {
|
|
136
|
+
return 'reference';
|
|
137
|
+
}
|
|
138
|
+
return 'structured';
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return 'text';
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// ============================================================================
|
|
145
|
+
// FRAGMENT CREATION
|
|
146
|
+
// ============================================================================
|
|
147
|
+
|
|
148
|
+
/** Options for creating a fragment */
|
|
149
|
+
export interface CreateFragmentOptions<T = unknown> {
|
|
150
|
+
/** The content to store */
|
|
151
|
+
content: T;
|
|
152
|
+
|
|
153
|
+
/** Source URI for provenance */
|
|
154
|
+
source: SourceURI | string;
|
|
155
|
+
|
|
156
|
+
/** Attribution type */
|
|
157
|
+
attribution: Attribution;
|
|
158
|
+
|
|
159
|
+
/** Trust level (0-100, optional) */
|
|
160
|
+
trust_level?: number;
|
|
161
|
+
|
|
162
|
+
/** Constraints to apply */
|
|
163
|
+
constraints?: Constraint[];
|
|
164
|
+
|
|
165
|
+
/** Parent fragment for provenance chain */
|
|
166
|
+
parent?: ContextFragment;
|
|
167
|
+
|
|
168
|
+
/** Optional signature */
|
|
169
|
+
signature?: string;
|
|
170
|
+
|
|
171
|
+
/** Semantic type override */
|
|
172
|
+
semantic_type?: SemanticType;
|
|
173
|
+
|
|
174
|
+
/** Secret key for sealing */
|
|
175
|
+
seal_key: string;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Create a new ContextFragment.
|
|
180
|
+
* This is the primary factory function for fragments.
|
|
181
|
+
*/
|
|
182
|
+
export async function createFragment<T = unknown>(
|
|
183
|
+
options: CreateFragmentOptions<T>
|
|
184
|
+
): Promise<ContextFragment<T>> {
|
|
185
|
+
const id = generateUUID();
|
|
186
|
+
const sealed_at = new Date().toISOString() as Timestamp;
|
|
187
|
+
|
|
188
|
+
// Create semantic unit
|
|
189
|
+
const content = createSemanticUnit(options.content, {
|
|
190
|
+
type: options.semantic_type,
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
// Create provenance
|
|
194
|
+
const record = await createProvenanceRecord({
|
|
195
|
+
source: options.source,
|
|
196
|
+
attribution: options.attribution,
|
|
197
|
+
trust_level: options.trust_level,
|
|
198
|
+
parent_hash: options.parent?.hash ?? null,
|
|
199
|
+
signature: options.signature,
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
let provenance: ProvenanceChain;
|
|
203
|
+
if (options.parent) {
|
|
204
|
+
provenance = await extendProvenanceChain(options.parent.provenance, record);
|
|
205
|
+
} else {
|
|
206
|
+
provenance = await createProvenanceChain(record);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Compute content hash
|
|
210
|
+
const hash = await sha256Object({
|
|
211
|
+
content,
|
|
212
|
+
provenance,
|
|
213
|
+
sealed_at,
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
// Create seal
|
|
217
|
+
const fragmentData = {
|
|
218
|
+
id,
|
|
219
|
+
hash,
|
|
220
|
+
content,
|
|
221
|
+
provenance,
|
|
222
|
+
sealed_at,
|
|
223
|
+
constraints: options.constraints ?? [],
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
const seal = await sealFragment(fragmentData, options.seal_key);
|
|
227
|
+
|
|
228
|
+
return {
|
|
229
|
+
id,
|
|
230
|
+
hash,
|
|
231
|
+
content,
|
|
232
|
+
provenance,
|
|
233
|
+
sealed_at,
|
|
234
|
+
seal,
|
|
235
|
+
constraints: options.constraints ?? [],
|
|
236
|
+
version: 'v3.1',
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// ============================================================================
|
|
241
|
+
// FRAGMENT VERIFICATION
|
|
242
|
+
// ============================================================================
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Verify a fragment's integrity.
|
|
246
|
+
* Checks both the content hash and the HMAC seal.
|
|
247
|
+
*/
|
|
248
|
+
export async function verifyFragment(
|
|
249
|
+
fragment: ContextFragment,
|
|
250
|
+
seal_key: string
|
|
251
|
+
): Promise<{
|
|
252
|
+
valid: boolean;
|
|
253
|
+
errors: string[];
|
|
254
|
+
}> {
|
|
255
|
+
const errors: string[] = [];
|
|
256
|
+
|
|
257
|
+
// Verify content hash
|
|
258
|
+
const expectedHash = await sha256Object({
|
|
259
|
+
content: fragment.content,
|
|
260
|
+
provenance: fragment.provenance,
|
|
261
|
+
sealed_at: fragment.sealed_at,
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
if (expectedHash !== fragment.hash) {
|
|
265
|
+
errors.push('Content hash mismatch');
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Verify HMAC seal
|
|
269
|
+
const sealValid = await verifyFragmentSeal(
|
|
270
|
+
{
|
|
271
|
+
id: fragment.id,
|
|
272
|
+
hash: fragment.hash,
|
|
273
|
+
content: fragment.content,
|
|
274
|
+
provenance: fragment.provenance,
|
|
275
|
+
sealed_at: fragment.sealed_at,
|
|
276
|
+
constraints: fragment.constraints,
|
|
277
|
+
seal: fragment.seal,
|
|
278
|
+
},
|
|
279
|
+
seal_key
|
|
280
|
+
);
|
|
281
|
+
|
|
282
|
+
if (!sealValid) {
|
|
283
|
+
errors.push('HMAC seal verification failed');
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Verify version (accept both v3.1 and legacy 1.0)
|
|
287
|
+
if (fragment.version !== 'v3.1' && fragment.version !== '1.0') {
|
|
288
|
+
errors.push(`Unknown fragment version: ${fragment.version}`);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return {
|
|
292
|
+
valid: errors.length === 0,
|
|
293
|
+
errors,
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// ============================================================================
|
|
298
|
+
// FRAGMENT OPERATIONS
|
|
299
|
+
// ============================================================================
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Derive a new fragment from an existing one.
|
|
303
|
+
* Creates a new fragment with the parent's provenance extended.
|
|
304
|
+
*/
|
|
305
|
+
export async function deriveFragment<T, U = T>(
|
|
306
|
+
parent: ContextFragment<T>,
|
|
307
|
+
options: {
|
|
308
|
+
content: U;
|
|
309
|
+
source: SourceURI | string;
|
|
310
|
+
attribution?: Attribution;
|
|
311
|
+
trust_level?: number;
|
|
312
|
+
constraints?: Constraint[];
|
|
313
|
+
seal_key: string;
|
|
314
|
+
}
|
|
315
|
+
): Promise<ContextFragment<U>> {
|
|
316
|
+
return createFragment({
|
|
317
|
+
content: options.content,
|
|
318
|
+
source: options.source,
|
|
319
|
+
attribution: options.attribution ?? 'derived',
|
|
320
|
+
trust_level: options.trust_level,
|
|
321
|
+
constraints: [...(options.constraints ?? parent.constraints)],
|
|
322
|
+
parent,
|
|
323
|
+
seal_key: options.seal_key,
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Clone a fragment with new constraints.
|
|
329
|
+
* Creates a new fragment with the same content but different constraints.
|
|
330
|
+
*/
|
|
331
|
+
export async function withConstraints<T>(
|
|
332
|
+
fragment: ContextFragment<T>,
|
|
333
|
+
constraints: Constraint[],
|
|
334
|
+
seal_key: string
|
|
335
|
+
): Promise<ContextFragment<T>> {
|
|
336
|
+
return deriveFragment(fragment, {
|
|
337
|
+
content: fragment.content.data,
|
|
338
|
+
source: fragment.provenance.head.source,
|
|
339
|
+
attribution: 'derived',
|
|
340
|
+
constraints,
|
|
341
|
+
seal_key,
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Get the age of a fragment in milliseconds.
|
|
347
|
+
*/
|
|
348
|
+
export function getFragmentAge(fragment: ContextFragment): number {
|
|
349
|
+
const sealed = new Date(fragment.sealed_at).getTime();
|
|
350
|
+
return Date.now() - sealed;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Check if a fragment is expired based on time constraints.
|
|
355
|
+
*/
|
|
356
|
+
export function isFragmentExpired(fragment: ContextFragment): boolean {
|
|
357
|
+
for (const constraint of fragment.constraints) {
|
|
358
|
+
if (constraint.kind === 'time') {
|
|
359
|
+
// Check max_age
|
|
360
|
+
if (constraint.max_age_ms !== undefined) {
|
|
361
|
+
const age = getFragmentAge(fragment);
|
|
362
|
+
if (age > constraint.max_age_ms) {
|
|
363
|
+
return true;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Check not_after
|
|
368
|
+
if (constraint.not_after !== undefined) {
|
|
369
|
+
const deadline = new Date(constraint.not_after).getTime();
|
|
370
|
+
if (Date.now() > deadline) {
|
|
371
|
+
return true;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
return false;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Get the trust level of a fragment (minimum in chain).
|
|
382
|
+
*/
|
|
383
|
+
export function getFragmentTrust(fragment: ContextFragment): TrustLevel {
|
|
384
|
+
let min = fragment.provenance.head.trust_level;
|
|
385
|
+
let current = fragment.provenance.tail;
|
|
386
|
+
|
|
387
|
+
while (current !== null) {
|
|
388
|
+
if (current.head.trust_level < min) {
|
|
389
|
+
min = current.head.trust_level;
|
|
390
|
+
}
|
|
391
|
+
current = current.tail;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
return min;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// ============================================================================
|
|
398
|
+
// SERIALIZATION
|
|
399
|
+
// ============================================================================
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Serialize a fragment to JSON.
|
|
403
|
+
*/
|
|
404
|
+
export function serializeFragment(fragment: ContextFragment): string {
|
|
405
|
+
return JSON.stringify(fragment);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Sanitize an object to prevent prototype pollution.
|
|
410
|
+
* Removes __proto__, constructor, and prototype properties recursively.
|
|
411
|
+
*/
|
|
412
|
+
function sanitizeObject(obj: unknown): unknown {
|
|
413
|
+
if (obj === null || typeof obj !== 'object') {
|
|
414
|
+
return obj;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
if (Array.isArray(obj)) {
|
|
418
|
+
return obj.map(sanitizeObject);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
const sanitized: Record<string, unknown> = Object.create(null);
|
|
422
|
+
for (const key of Object.keys(obj)) {
|
|
423
|
+
// Block dangerous properties
|
|
424
|
+
if (key === '__proto__' || key === 'constructor' || key === 'prototype') {
|
|
425
|
+
continue;
|
|
426
|
+
}
|
|
427
|
+
sanitized[key] = sanitizeObject((obj as Record<string, unknown>)[key]);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
return sanitized;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Deserialize a fragment from JSON.
|
|
435
|
+
* Note: Does NOT verify integrity — call verifyFragment separately.
|
|
436
|
+
*/
|
|
437
|
+
export function deserializeFragment<T = unknown>(json: string): ContextFragment<T> {
|
|
438
|
+
const raw = JSON.parse(json);
|
|
439
|
+
|
|
440
|
+
// Sanitize to prevent prototype pollution
|
|
441
|
+
const parsed = sanitizeObject(raw) as Record<string, unknown>;
|
|
442
|
+
|
|
443
|
+
// Type validation
|
|
444
|
+
if (!parsed.id || !parsed.hash || !parsed.content || !parsed.provenance) {
|
|
445
|
+
throw new Error('Invalid fragment structure');
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
if (parsed.version !== 'v3.1' && parsed.version !== '1.0') {
|
|
449
|
+
// SECURITY: Do not leak version value in error message
|
|
450
|
+
throw new Error('Unsupported fragment version');
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
return parsed as unknown as ContextFragment<T>;
|
|
454
|
+
}
|