scxq2-cc 1.0.0
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 +340 -0
- package/dist/base64.js +83 -0
- package/dist/canon.js +60 -0
- package/dist/cli.mjs +192 -0
- package/dist/engine.js +753 -0
- package/dist/index.d.ts +426 -0
- package/dist/index.js +48 -0
- package/dist/sha.js +71 -0
- package/dist/verify.js +480 -0
- package/dist/wasm-decoder.js +232 -0
- package/package.json +64 -0
- package/src/base64.js +83 -0
- package/src/canon.js +60 -0
- package/src/engine.js +753 -0
- package/src/index.js +48 -0
- package/src/sha.js +71 -0
package/README.md
ADDED
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="scxq2-logo.svg" alt="SCXQ2 Logo" width="200" height="200">
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
<h1 align="center">SCXQ2</h1>
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
<strong>Compression Calculus Engine</strong><br>
|
|
9
|
+
Deterministic, Proof-Generating, Content-Addressable Language Packs
|
|
10
|
+
</p>
|
|
11
|
+
|
|
12
|
+
<p align="center">
|
|
13
|
+
<a href="#installation">Installation</a> •
|
|
14
|
+
<a href="#quick-start">Quick Start</a> •
|
|
15
|
+
<a href="#api">API</a> •
|
|
16
|
+
<a href="#specification">Specification</a> •
|
|
17
|
+
<a href="#security">Security</a>
|
|
18
|
+
</p>
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Overview
|
|
23
|
+
|
|
24
|
+
SCXQ2 is a **frozen, deterministic compression calculus** that produces **content-addressable language packs**. It implements CC-v1 (Compression Calculus v1) operators to create self-verifying artifacts with cryptographic proofs of reversibility.
|
|
25
|
+
|
|
26
|
+
### Key Features
|
|
27
|
+
|
|
28
|
+
- **Deterministic** - Same input always produces identical output
|
|
29
|
+
- **Content-Addressable** - SHA-256 identity hashes for all artifacts
|
|
30
|
+
- **Proof-Generating** - Every compression includes reversibility proof
|
|
31
|
+
- **Universal Runtime** - Works in Node.js, browsers, and workers
|
|
32
|
+
- **Multi-Lane** - Compress multiple sources with shared dictionary
|
|
33
|
+
- **Type-Safe** - Full TypeScript definitions included
|
|
34
|
+
|
|
35
|
+
### What SCXQ2 Is
|
|
36
|
+
|
|
37
|
+
- A **representation algebra** for compressing text
|
|
38
|
+
- A **language artifact** format (dict + block + proof)
|
|
39
|
+
- A **deterministic encoding** (DICT16-B64)
|
|
40
|
+
|
|
41
|
+
### What SCXQ2 Is NOT
|
|
42
|
+
|
|
43
|
+
- ❌ Not encryption
|
|
44
|
+
- ❌ Not a file format
|
|
45
|
+
- ❌ Not a transport protocol
|
|
46
|
+
- ❌ Not an execution language
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## Installation
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
npm install @asx/scxq2-cc
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
**Requirements:** Node.js 18+ or modern browser with WebCrypto
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## Quick Start
|
|
61
|
+
|
|
62
|
+
### Basic Compression
|
|
63
|
+
|
|
64
|
+
```javascript
|
|
65
|
+
import { ccCompress, ccDecompress } from '@asx/scxq2-cc';
|
|
66
|
+
|
|
67
|
+
// Compress source code
|
|
68
|
+
const source = `
|
|
69
|
+
function hello() {
|
|
70
|
+
console.log("Hello, World!");
|
|
71
|
+
}
|
|
72
|
+
`;
|
|
73
|
+
|
|
74
|
+
const pack = await ccCompress(source, { maxDict: 512 });
|
|
75
|
+
|
|
76
|
+
console.log(pack.proof.ok); // true - roundtrip verified
|
|
77
|
+
console.log(pack.dict.dict.length); // number of dictionary entries
|
|
78
|
+
console.log(pack.audit.sizes.ratio); // compression ratio
|
|
79
|
+
|
|
80
|
+
// Decompress
|
|
81
|
+
const roundtrip = ccDecompress(pack.dict, pack.block);
|
|
82
|
+
console.log(roundtrip === source); // true
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Multi-Lane Compression
|
|
86
|
+
|
|
87
|
+
```javascript
|
|
88
|
+
import { ccCompressLanes, ccDecompress } from '@asx/scxq2-cc';
|
|
89
|
+
|
|
90
|
+
const pack = await ccCompressLanes({
|
|
91
|
+
lanes: [
|
|
92
|
+
{ lane_id: 'index', text: 'export * from "./utils";' },
|
|
93
|
+
{ lane_id: 'utils', text: 'export function utils() { return 42; }' },
|
|
94
|
+
{ lane_id: 'types', text: 'export interface Config { value: number; }' }
|
|
95
|
+
]
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// All lanes share the same dictionary
|
|
99
|
+
console.log(pack.dict.dict.length); // shared dictionary size
|
|
100
|
+
console.log(pack.lanes.length); // 3 blocks
|
|
101
|
+
|
|
102
|
+
// Decompress each lane
|
|
103
|
+
for (const block of pack.lanes) {
|
|
104
|
+
const text = ccDecompress(pack.dict, block);
|
|
105
|
+
console.log(`Lane ${block.lane_id}: ${text.length} chars`);
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### Synchronous API (Node.js only)
|
|
110
|
+
|
|
111
|
+
```javascript
|
|
112
|
+
import { ccCompressSync, ccCompressLanesSync } from '@asx/scxq2-cc';
|
|
113
|
+
|
|
114
|
+
// Sync single-lane
|
|
115
|
+
const pack = ccCompressSync(source);
|
|
116
|
+
|
|
117
|
+
// Sync multi-lane
|
|
118
|
+
const multiPack = ccCompressLanesSync({ lanes: [...] });
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
## API
|
|
124
|
+
|
|
125
|
+
### Core Functions
|
|
126
|
+
|
|
127
|
+
| Function | Description |
|
|
128
|
+
|----------|-------------|
|
|
129
|
+
| `ccCompress(input, opts?)` | Async compression with proof |
|
|
130
|
+
| `ccCompressSync(input, opts?)` | Sync compression (Node.js only) |
|
|
131
|
+
| `ccCompressLanes(input, opts?)` | Async multi-lane compression |
|
|
132
|
+
| `ccCompressLanesSync(input, opts?)` | Sync multi-lane (Node.js only) |
|
|
133
|
+
| `ccDecompress(dict, block)` | Decompress block using dictionary |
|
|
134
|
+
| `verifyPack(dict, block)` | Verify pack structure |
|
|
135
|
+
|
|
136
|
+
### Compression Options
|
|
137
|
+
|
|
138
|
+
```typescript
|
|
139
|
+
interface CCCompressOptions {
|
|
140
|
+
maxDict?: number; // Max dictionary entries (1-65535, default: 1024)
|
|
141
|
+
minLen?: number; // Min token length (2-128, default: 3)
|
|
142
|
+
noStrings?: boolean; // Skip string literal tokens
|
|
143
|
+
noWS?: boolean; // Skip whitespace tokens
|
|
144
|
+
noPunct?: boolean; // Skip punctuation tokens
|
|
145
|
+
enableFieldOps?: boolean; // Enable JSON key extraction
|
|
146
|
+
enableEdgeOps?: boolean; // Enable edge witnesses
|
|
147
|
+
created_utc?: string; // ISO timestamp
|
|
148
|
+
source_file?: string; // Source identifier
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### Result Objects
|
|
153
|
+
|
|
154
|
+
```typescript
|
|
155
|
+
interface CCResult {
|
|
156
|
+
dict: SCXQ2Dict; // Dictionary with token array
|
|
157
|
+
block: SCXQ2Block; // Encoded block with b64 payload
|
|
158
|
+
proof: CCProof; // Reversibility proof
|
|
159
|
+
audit: CCAudit; // Compression metrics
|
|
160
|
+
}
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### Utility Functions
|
|
164
|
+
|
|
165
|
+
```javascript
|
|
166
|
+
import {
|
|
167
|
+
canon, // Canonical JSON serialization
|
|
168
|
+
sha256HexUtf8, // Async SHA-256 hash
|
|
169
|
+
bytesToBase64, // Encode bytes to base64
|
|
170
|
+
base64ToBytes // Decode base64 to bytes
|
|
171
|
+
} from '@asx/scxq2-cc';
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
---
|
|
175
|
+
|
|
176
|
+
## Encoding Format
|
|
177
|
+
|
|
178
|
+
SCXQ2 uses a simple bytecode format:
|
|
179
|
+
|
|
180
|
+
| Byte | Meaning |
|
|
181
|
+
|------|---------|
|
|
182
|
+
| `0x00-0x7F` | ASCII literal (1 byte) |
|
|
183
|
+
| `0x80 [hi] [lo]` | Dictionary reference (3 bytes) |
|
|
184
|
+
| `0x81 [hi] [lo]` | UTF-16 code unit (3 bytes) |
|
|
185
|
+
|
|
186
|
+
### Dictionary Properties
|
|
187
|
+
|
|
188
|
+
- Maximum 65,535 entries (16-bit index)
|
|
189
|
+
- Ordered longest-first for greedy matching
|
|
190
|
+
- UTF-16 code-unit indexed
|
|
191
|
+
- Immutable once sealed
|
|
192
|
+
|
|
193
|
+
---
|
|
194
|
+
|
|
195
|
+
## Specification
|
|
196
|
+
|
|
197
|
+
SCXQ2 implements the frozen **CC-v1 (Compression Calculus v1)** specification.
|
|
198
|
+
|
|
199
|
+
### Invariants (Non-Negotiable)
|
|
200
|
+
|
|
201
|
+
1. **Deterministic Canonical Form** - Canonical JSON, stable UTF-8
|
|
202
|
+
2. **Reversibility** - Every block losslessly decodable
|
|
203
|
+
3. **Single-Hash Identity** - One SHA-256 identifies entire pack
|
|
204
|
+
4. **No Runtime Authority** - No execution, IO, or environment semantics
|
|
205
|
+
5. **Lane Isolation** - Blocks independent except shared dictionary
|
|
206
|
+
6. **Proof-Bound** - Proof inseparable from content
|
|
207
|
+
7. **Compression-Only** - Never introduces meaning, only representation
|
|
208
|
+
|
|
209
|
+
### Pack Structure
|
|
210
|
+
|
|
211
|
+
```
|
|
212
|
+
SCXQ2 PACK
|
|
213
|
+
├── Dictionary (shared)
|
|
214
|
+
├── Blocks[] (lanes)
|
|
215
|
+
│ ├── Encoded byte stream (b64)
|
|
216
|
+
│ ├── Optional lane_id
|
|
217
|
+
│ └── Optional edges (EDGE witnesses)
|
|
218
|
+
├── Proof
|
|
219
|
+
└── pack_sha256_canon (identity)
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
### CC Operators
|
|
223
|
+
|
|
224
|
+
| Operator | Purpose |
|
|
225
|
+
|----------|---------|
|
|
226
|
+
| `CC.NORM` | Normalize newlines, optional whitespace policy |
|
|
227
|
+
| `CC.DICT` | Extract dictionary from token stream |
|
|
228
|
+
| `CC.FIELD` | Structural JSON key augmentation |
|
|
229
|
+
| `CC.LANE` | Multi-lane product construction |
|
|
230
|
+
| `CC.EDGE` | Adjacency witnesses for analysis |
|
|
231
|
+
|
|
232
|
+
---
|
|
233
|
+
|
|
234
|
+
## Security
|
|
235
|
+
|
|
236
|
+
SCXQ2 is **compression representation**, not encryption.
|
|
237
|
+
|
|
238
|
+
### Threat Mitigations
|
|
239
|
+
|
|
240
|
+
- **Memory Safety** - No out-of-bounds access, bounded allocations
|
|
241
|
+
- **Time Safety** - O(n) decode time, predictable worst-case
|
|
242
|
+
- **Deterministic Failure** - Stable error codes, fail-closed
|
|
243
|
+
- **Integrity** - SHA-256 identity prevents silent mutation
|
|
244
|
+
|
|
245
|
+
### Decompression Bomb Protection
|
|
246
|
+
|
|
247
|
+
Set `maxOutputUnits` to limit decoded output size:
|
|
248
|
+
|
|
249
|
+
```javascript
|
|
250
|
+
// Verifier with output limit
|
|
251
|
+
const result = await ccCompress(input, {
|
|
252
|
+
maxDict: 1024
|
|
253
|
+
// Implementation can add maxOutputUnits for decode limits
|
|
254
|
+
});
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
### Security Non-Goals
|
|
258
|
+
|
|
259
|
+
SCXQ2 does NOT provide:
|
|
260
|
+
- Confidentiality
|
|
261
|
+
- Authentication
|
|
262
|
+
- Authorization
|
|
263
|
+
- Tamper-proofing against hash recomputation
|
|
264
|
+
|
|
265
|
+
---
|
|
266
|
+
|
|
267
|
+
## Error Codes
|
|
268
|
+
|
|
269
|
+
| Code | Phase | Description |
|
|
270
|
+
|------|-------|-------------|
|
|
271
|
+
| `scxq2.error.pack_*` | pack | Pack structure errors |
|
|
272
|
+
| `scxq2.error.dict_*` | dict | Dictionary errors |
|
|
273
|
+
| `scxq2.error.block_*` | block | Block errors |
|
|
274
|
+
| `scxq2.error.decode_*` | decode | Decoding errors |
|
|
275
|
+
| `scxq2.error.proof_*` | proof | Proof verification errors |
|
|
276
|
+
|
|
277
|
+
---
|
|
278
|
+
|
|
279
|
+
## Project Structure
|
|
280
|
+
|
|
281
|
+
```
|
|
282
|
+
scxq2-cc/
|
|
283
|
+
├── package.json
|
|
284
|
+
├── README.md
|
|
285
|
+
├── BRAND.md # Brand guidelines
|
|
286
|
+
├── SCXQ2_language.md # Full language specification
|
|
287
|
+
├── SCXQ2_CC_ENGINE_V1.md # Engine specification
|
|
288
|
+
├── NPM.md # NPM module documentation
|
|
289
|
+
├── scxq2-logo.svg # Logo
|
|
290
|
+
├── src/
|
|
291
|
+
│ ├── index.js # Main entry point
|
|
292
|
+
│ ├── engine.js # Core CC engine
|
|
293
|
+
│ ├── canon.js # Canonical JSON
|
|
294
|
+
│ ├── sha.js # SHA-256 utilities
|
|
295
|
+
│ └── base64.js # Base64 utilities
|
|
296
|
+
└── dist/
|
|
297
|
+
├── index.js # Built entry point
|
|
298
|
+
├── index.d.ts # TypeScript definitions
|
|
299
|
+
└── ...
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
---
|
|
303
|
+
|
|
304
|
+
## Constants
|
|
305
|
+
|
|
306
|
+
```javascript
|
|
307
|
+
import { CC_ENGINE, SCXQ2_ENCODING, CC_OPS } from '@asx/scxq2-cc';
|
|
308
|
+
|
|
309
|
+
console.log(CC_ENGINE['@id']);
|
|
310
|
+
// "asx://cc/engine/scxq2.v1"
|
|
311
|
+
|
|
312
|
+
console.log(SCXQ2_ENCODING);
|
|
313
|
+
// { mode: "SCXQ2-DICT16-B64", encoding: "SCXQ2-1" }
|
|
314
|
+
|
|
315
|
+
console.log(CC_OPS);
|
|
316
|
+
// { NORM: "cc.norm.v1", DICT: "cc.dict.v1", ... }
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
---
|
|
320
|
+
|
|
321
|
+
## Final Law
|
|
322
|
+
|
|
323
|
+
> **If two SCXQ2 packs have the same `pack_sha256_canon`, they are the same language object.**
|
|
324
|
+
|
|
325
|
+
Everything else is projection.
|
|
326
|
+
|
|
327
|
+
---
|
|
328
|
+
|
|
329
|
+
## License
|
|
330
|
+
|
|
331
|
+
MIT
|
|
332
|
+
|
|
333
|
+
---
|
|
334
|
+
|
|
335
|
+
## Links
|
|
336
|
+
|
|
337
|
+
- [SCXQ2 Language Specification](./SCXQ2_language.md)
|
|
338
|
+
- [CC Engine Specification](./SCXQ2_CC_ENGINE_V1.md)
|
|
339
|
+
- [NPM Module Documentation](./NPM.md)
|
|
340
|
+
- [Brand Guidelines](./BRAND.md)
|
package/dist/base64.js
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SCXQ2 Base64 Utilities
|
|
3
|
+
*
|
|
4
|
+
* Universal base64 encoding/decoding that works in Node.js, browsers, and workers.
|
|
5
|
+
* Handles the "base64:" prefix format used in some SCXQ2 contexts.
|
|
6
|
+
*
|
|
7
|
+
* @module @asx/scxq2-cc/base64
|
|
8
|
+
* @version 1.0.0
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Encodes bytes to base64 string.
|
|
13
|
+
*
|
|
14
|
+
* @param {Uint8Array|number[]} bytes - Bytes to encode
|
|
15
|
+
* @returns {string} Base64-encoded string
|
|
16
|
+
*/
|
|
17
|
+
export function bytesToBase64(bytes) {
|
|
18
|
+
// Ensure we have a proper array-like
|
|
19
|
+
const arr = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
|
|
20
|
+
|
|
21
|
+
// Node.js Buffer
|
|
22
|
+
if (typeof Buffer !== "undefined" && Buffer.from) {
|
|
23
|
+
return Buffer.from(arr).toString("base64");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Browser/Worker: use btoa
|
|
27
|
+
if (typeof btoa === "function") {
|
|
28
|
+
let binary = "";
|
|
29
|
+
for (let i = 0; i < arr.length; i++) {
|
|
30
|
+
binary += String.fromCharCode(arr[i]);
|
|
31
|
+
}
|
|
32
|
+
return btoa(binary);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
throw new Error("SCXQ2: no base64 encoder available");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Decodes base64 string to bytes.
|
|
40
|
+
* Automatically strips "base64:" prefix if present.
|
|
41
|
+
*
|
|
42
|
+
* @param {string} b64 - Base64-encoded string
|
|
43
|
+
* @returns {Uint8Array} Decoded bytes
|
|
44
|
+
*/
|
|
45
|
+
export function base64ToBytes(b64) {
|
|
46
|
+
// Strip optional "base64:" prefix
|
|
47
|
+
const clean = String(b64).startsWith("base64:")
|
|
48
|
+
? String(b64).slice(7)
|
|
49
|
+
: String(b64);
|
|
50
|
+
|
|
51
|
+
// Node.js Buffer
|
|
52
|
+
if (typeof Buffer !== "undefined" && Buffer.from) {
|
|
53
|
+
return new Uint8Array(Buffer.from(clean, "base64"));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Browser/Worker: use atob
|
|
57
|
+
if (typeof atob === "function") {
|
|
58
|
+
const binary = atob(clean);
|
|
59
|
+
const bytes = new Uint8Array(binary.length);
|
|
60
|
+
for (let i = 0; i < binary.length; i++) {
|
|
61
|
+
bytes[i] = binary.charCodeAt(i);
|
|
62
|
+
}
|
|
63
|
+
return bytes;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
throw new Error("SCXQ2: no base64 decoder available");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Validates that a string is valid base64.
|
|
71
|
+
*
|
|
72
|
+
* @param {string} b64 - String to validate
|
|
73
|
+
* @returns {boolean} True if valid base64
|
|
74
|
+
*/
|
|
75
|
+
export function isValidBase64(b64) {
|
|
76
|
+
const clean = String(b64).startsWith("base64:")
|
|
77
|
+
? String(b64).slice(7)
|
|
78
|
+
: String(b64);
|
|
79
|
+
|
|
80
|
+
// Standard base64 regex
|
|
81
|
+
const regex = /^[A-Za-z0-9+/]*={0,2}$/;
|
|
82
|
+
return regex.test(clean) && clean.length % 4 === 0;
|
|
83
|
+
}
|
package/dist/canon.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SCXQ2 Canonical JSON Utilities
|
|
3
|
+
*
|
|
4
|
+
* Provides deterministic JSON serialization with sorted keys for
|
|
5
|
+
* content-addressable hashing and reproducible pack identities.
|
|
6
|
+
*
|
|
7
|
+
* @module @asx/scxq2-cc/canon
|
|
8
|
+
* @version 1.0.0
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Recursively sorts object keys for deterministic JSON output.
|
|
13
|
+
* Arrays are preserved in order, objects have keys sorted alphabetically.
|
|
14
|
+
*
|
|
15
|
+
* @param {*} value - Any JSON-serializable value
|
|
16
|
+
* @returns {*} Value with all nested object keys sorted
|
|
17
|
+
*/
|
|
18
|
+
export function sortKeysDeep(value) {
|
|
19
|
+
if (Array.isArray(value)) {
|
|
20
|
+
return value.map(sortKeysDeep);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (value !== null && typeof value === "object") {
|
|
24
|
+
const sorted = {};
|
|
25
|
+
const keys = Object.keys(value).sort();
|
|
26
|
+
for (const key of keys) {
|
|
27
|
+
sorted[key] = sortKeysDeep(value[key]);
|
|
28
|
+
}
|
|
29
|
+
return sorted;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return value;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Produces canonical JSON string with sorted keys.
|
|
37
|
+
* This is the required serialization for all SCXQ2 hash computations.
|
|
38
|
+
*
|
|
39
|
+
* @param {*} obj - Object to serialize
|
|
40
|
+
* @returns {string} Canonical JSON string
|
|
41
|
+
*/
|
|
42
|
+
export function canon(obj) {
|
|
43
|
+
return JSON.stringify(sortKeysDeep(obj));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Creates a shallow copy of an object with specified fields removed.
|
|
48
|
+
* Used for computing hashes that exclude the hash field itself.
|
|
49
|
+
*
|
|
50
|
+
* @param {Object} obj - Source object
|
|
51
|
+
* @param {string[]} fields - Fields to exclude
|
|
52
|
+
* @returns {Object} New object without excluded fields
|
|
53
|
+
*/
|
|
54
|
+
export function strip(obj, fields) {
|
|
55
|
+
const copy = { ...obj };
|
|
56
|
+
for (const field of fields) {
|
|
57
|
+
delete copy[field];
|
|
58
|
+
}
|
|
59
|
+
return copy;
|
|
60
|
+
}
|
package/dist/cli.mjs
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* SCXQ2 CLI
|
|
4
|
+
*
|
|
5
|
+
* Commands:
|
|
6
|
+
* verify <pack.json> - Verify pack integrity
|
|
7
|
+
* decode <pack.json> [--lane ID] - Decode block to stdout
|
|
8
|
+
* inspect <pack.json> - Print pack summary
|
|
9
|
+
*
|
|
10
|
+
* @version 1.0.0
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import fs from "fs";
|
|
14
|
+
import path from "path";
|
|
15
|
+
import { scxq2PackVerify, scxq2DecodeUtf16, SCXQ2_DEFAULT_POLICY } from "./verify.js";
|
|
16
|
+
|
|
17
|
+
/* =============================================================================
|
|
18
|
+
Helpers
|
|
19
|
+
============================================================================= */
|
|
20
|
+
|
|
21
|
+
function usage() {
|
|
22
|
+
console.log(`
|
|
23
|
+
scxq2 <command> [args]
|
|
24
|
+
|
|
25
|
+
Commands:
|
|
26
|
+
verify <pack.json> Verify pack (default policy)
|
|
27
|
+
decode <pack.json> [--lane ID] Decode first block or matching lane_id
|
|
28
|
+
inspect <pack.json> Print pack summary (hashes, lanes, sizes)
|
|
29
|
+
|
|
30
|
+
Flags:
|
|
31
|
+
--no-roundtrip Skip roundtrip verification
|
|
32
|
+
--no-proof Skip proof verification
|
|
33
|
+
--maxOutputUnits N Set max output code units
|
|
34
|
+
|
|
35
|
+
Examples:
|
|
36
|
+
scxq2 verify mypack.json
|
|
37
|
+
scxq2 decode mypack.json --lane main
|
|
38
|
+
scxq2 inspect mypack.json
|
|
39
|
+
`);
|
|
40
|
+
process.exit(2);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function readJson(fp) {
|
|
44
|
+
try {
|
|
45
|
+
const s = fs.readFileSync(fp, "utf8");
|
|
46
|
+
return JSON.parse(s);
|
|
47
|
+
} catch (e) {
|
|
48
|
+
console.error(`Error reading ${fp}: ${e.message}`);
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function parseArgs(argv) {
|
|
54
|
+
const out = { _: [] };
|
|
55
|
+
for (let i = 0; i < argv.length; i++) {
|
|
56
|
+
const a = argv[i];
|
|
57
|
+
if (!a.startsWith("--")) {
|
|
58
|
+
out._.push(a);
|
|
59
|
+
} else {
|
|
60
|
+
const k = a.slice(2);
|
|
61
|
+
const v = (i + 1 < argv.length && !argv[i + 1].startsWith("--"))
|
|
62
|
+
? argv[++i]
|
|
63
|
+
: true;
|
|
64
|
+
out[k] = v;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return out;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function applyPolicyFlags(args) {
|
|
71
|
+
const p = { ...SCXQ2_DEFAULT_POLICY };
|
|
72
|
+
if (args["no-roundtrip"]) p.requireRoundtrip = false;
|
|
73
|
+
if (args["no-proof"]) p.requireProof = false;
|
|
74
|
+
if (args.maxOutputUnits) p.maxOutputUnits = Number(args.maxOutputUnits);
|
|
75
|
+
return p;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function b64ToBytes(b64) {
|
|
79
|
+
return Buffer.from(b64, "base64");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function formatBytes(bytes) {
|
|
83
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
84
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
85
|
+
return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/* =============================================================================
|
|
89
|
+
Commands
|
|
90
|
+
============================================================================= */
|
|
91
|
+
|
|
92
|
+
function cmdVerify(pack, policy) {
|
|
93
|
+
const res = scxq2PackVerify(pack, policy);
|
|
94
|
+
if (!res.ok) {
|
|
95
|
+
console.error(JSON.stringify(res, null, 2));
|
|
96
|
+
process.exit(1);
|
|
97
|
+
}
|
|
98
|
+
console.log(JSON.stringify(res, null, 2));
|
|
99
|
+
process.exit(0);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function cmdInspect(pack) {
|
|
103
|
+
const blocks = pack.blocks || [];
|
|
104
|
+
const lanes = blocks.map((b, i) => ({
|
|
105
|
+
index: i,
|
|
106
|
+
lane_id: b.lane_id ?? null,
|
|
107
|
+
b64_bytes: b.b64 ? Buffer.from(b.b64, "base64").length : null,
|
|
108
|
+
original_bytes: b.original_bytes_utf8 ?? null,
|
|
109
|
+
block_sha: b.block_sha256_canon?.slice(0, 16) + "..." ?? null
|
|
110
|
+
}));
|
|
111
|
+
|
|
112
|
+
const dictSize = pack.dict?.dict?.length ?? 0;
|
|
113
|
+
const totalB64 = blocks.reduce((acc, b) => acc + (b.b64 ? Buffer.from(b.b64, "base64").length : 0), 0);
|
|
114
|
+
|
|
115
|
+
const summary = {
|
|
116
|
+
pack_sha256_canon: pack.pack_sha256_canon ?? null,
|
|
117
|
+
dict_sha256_canon: pack.dict?.dict_sha256_canon ?? null,
|
|
118
|
+
dict_entries: dictSize,
|
|
119
|
+
blocks: lanes.length,
|
|
120
|
+
total_encoded_bytes: totalB64,
|
|
121
|
+
total_encoded_display: formatBytes(totalB64),
|
|
122
|
+
lanes
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
console.log(JSON.stringify(summary, null, 2));
|
|
126
|
+
process.exit(0);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function cmdDecode(pack, policy, laneId) {
|
|
130
|
+
const blocks = pack.blocks || [];
|
|
131
|
+
|
|
132
|
+
if (!blocks.length) {
|
|
133
|
+
console.error("Error: no blocks in pack");
|
|
134
|
+
process.exit(1);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
let block = blocks[0];
|
|
138
|
+
|
|
139
|
+
if (laneId != null && laneId !== true) {
|
|
140
|
+
const hit = blocks.find(x => String(x.lane_id) === String(laneId));
|
|
141
|
+
if (!hit) {
|
|
142
|
+
console.error(`Error: lane not found: ${laneId}`);
|
|
143
|
+
console.error(`Available lanes: ${blocks.map(b => b.lane_id ?? "(unnamed)").join(", ")}`);
|
|
144
|
+
process.exit(1);
|
|
145
|
+
}
|
|
146
|
+
block = hit;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const bytes = b64ToBytes(block.b64);
|
|
150
|
+
const dec = scxq2DecodeUtf16(pack.dict.dict, bytes, { maxOutputUnits: policy.maxOutputUnits });
|
|
151
|
+
|
|
152
|
+
if (!dec.ok) {
|
|
153
|
+
console.error(JSON.stringify(dec, null, 2));
|
|
154
|
+
process.exit(1);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
process.stdout.write(dec.value);
|
|
158
|
+
process.exit(0);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/* =============================================================================
|
|
162
|
+
Main
|
|
163
|
+
============================================================================= */
|
|
164
|
+
|
|
165
|
+
const args = parseArgs(process.argv.slice(2));
|
|
166
|
+
const cmd = args._[0];
|
|
167
|
+
const file = args._[1];
|
|
168
|
+
|
|
169
|
+
if (!cmd) usage();
|
|
170
|
+
if (cmd === "help" || cmd === "--help" || cmd === "-h") usage();
|
|
171
|
+
if (!file) {
|
|
172
|
+
console.error("Error: missing pack file argument");
|
|
173
|
+
usage();
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const pack = readJson(file);
|
|
177
|
+
const policy = applyPolicyFlags(args);
|
|
178
|
+
|
|
179
|
+
switch (cmd) {
|
|
180
|
+
case "verify":
|
|
181
|
+
cmdVerify(pack, policy);
|
|
182
|
+
break;
|
|
183
|
+
case "inspect":
|
|
184
|
+
cmdInspect(pack);
|
|
185
|
+
break;
|
|
186
|
+
case "decode":
|
|
187
|
+
cmdDecode(pack, policy, args.lane);
|
|
188
|
+
break;
|
|
189
|
+
default:
|
|
190
|
+
console.error(`Unknown command: ${cmd}`);
|
|
191
|
+
usage();
|
|
192
|
+
}
|