@git-stunts/git-cas 1.6.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/CHANGELOG.md +105 -0
- package/LICENSE +200 -0
- package/README.md +111 -0
- package/bin/git-cas.js +135 -0
- package/index.js +290 -0
- package/package.json +81 -0
- package/src/domain/errors/CasError.js +20 -0
- package/src/domain/schemas/ManifestSchema.js +30 -0
- package/src/domain/services/CasService.js +403 -0
- package/src/domain/value-objects/Chunk.js +36 -0
- package/src/domain/value-objects/Manifest.js +52 -0
- package/src/infrastructure/adapters/BunCryptoAdapter.js +120 -0
- package/src/infrastructure/adapters/GitPersistenceAdapter.js +103 -0
- package/src/infrastructure/adapters/NodeCryptoAdapter.js +103 -0
- package/src/infrastructure/adapters/WebCryptoAdapter.js +194 -0
- package/src/infrastructure/codecs/CborCodec.js +22 -0
- package/src/infrastructure/codecs/JsonCodec.js +23 -0
- package/src/ports/CodecPort.js +31 -0
- package/src/ports/CryptoPort.js +54 -0
- package/src/ports/GitPersistencePort.js +41 -0
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { Policy } from '@git-stunts/alfred';
|
|
2
|
+
import GitPersistencePort from '../../ports/GitPersistencePort.js';
|
|
3
|
+
import CasError from '../../domain/errors/CasError.js';
|
|
4
|
+
|
|
5
|
+
/** Default resilience policy: 30 s timeout wrapping 2 retries with exponential backoff. */
|
|
6
|
+
const DEFAULT_POLICY = Policy.timeout(30_000).wrap(
|
|
7
|
+
Policy.retry({
|
|
8
|
+
retries: 2,
|
|
9
|
+
backoff: 'exponential',
|
|
10
|
+
delay: 100,
|
|
11
|
+
maxDelay: 2_000,
|
|
12
|
+
}),
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* {@link GitPersistencePort} implementation backed by `@git-stunts/plumbing`.
|
|
17
|
+
*
|
|
18
|
+
* All Git I/O is wrapped with a configurable resilience {@link Policy}
|
|
19
|
+
* (timeout + retry by default).
|
|
20
|
+
*/
|
|
21
|
+
export default class GitPersistenceAdapter extends GitPersistencePort {
|
|
22
|
+
/**
|
|
23
|
+
* @param {Object} options
|
|
24
|
+
* @param {import('@git-stunts/plumbing').default} options.plumbing - GitPlumbing instance.
|
|
25
|
+
* @param {import('@git-stunts/alfred').Policy} [options.policy] - Resilience policy (defaults to 30 s timeout + 2 retries).
|
|
26
|
+
*/
|
|
27
|
+
constructor({ plumbing, policy }) {
|
|
28
|
+
super();
|
|
29
|
+
this.plumbing = plumbing;
|
|
30
|
+
this.policy = policy ?? DEFAULT_POLICY;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** @override */
|
|
34
|
+
async writeBlob(content) {
|
|
35
|
+
return this.policy.execute(() =>
|
|
36
|
+
this.plumbing.execute({
|
|
37
|
+
args: ['hash-object', '-w', '--stdin'],
|
|
38
|
+
input: content,
|
|
39
|
+
}),
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** @override */
|
|
44
|
+
async writeTree(entries) {
|
|
45
|
+
return this.policy.execute(() =>
|
|
46
|
+
this.plumbing.execute({
|
|
47
|
+
args: ['mktree'],
|
|
48
|
+
input: `${entries.join('\n')}\n`,
|
|
49
|
+
}),
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** @override */
|
|
54
|
+
async readBlob(oid) {
|
|
55
|
+
return this.policy.execute(async () => {
|
|
56
|
+
const stream = await this.plumbing.executeStream({
|
|
57
|
+
args: ['cat-file', 'blob', oid],
|
|
58
|
+
});
|
|
59
|
+
const data = await stream.collect({ asString: false });
|
|
60
|
+
// Plumbing returns Uint8Array; ensure we return a Buffer for codec/crypto compat
|
|
61
|
+
return Buffer.from(data.buffer, data.byteOffset, data.byteLength);
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** @override */
|
|
66
|
+
async readTree(treeOid) {
|
|
67
|
+
return this.policy.execute(async () => {
|
|
68
|
+
const output = await this.plumbing.execute({
|
|
69
|
+
args: ['ls-tree', '-z', treeOid],
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
if (!output || output.length === 0) {
|
|
73
|
+
return [];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return output.split('\0').filter(Boolean).map((entry) => {
|
|
77
|
+
// Format: <mode> <type> <oid>\t<name>
|
|
78
|
+
const tabIndex = entry.indexOf('\t');
|
|
79
|
+
if (tabIndex === -1) {
|
|
80
|
+
throw new CasError(
|
|
81
|
+
`Malformed ls-tree entry: ${entry}`,
|
|
82
|
+
'TREE_PARSE_ERROR',
|
|
83
|
+
{ rawEntry: entry },
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
const meta = entry.slice(0, tabIndex).split(' ');
|
|
87
|
+
if (meta.length !== 3) {
|
|
88
|
+
throw new CasError(
|
|
89
|
+
`Malformed ls-tree entry: ${entry}`,
|
|
90
|
+
'TREE_PARSE_ERROR',
|
|
91
|
+
{ rawEntry: entry },
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
mode: meta[0],
|
|
96
|
+
type: meta[1],
|
|
97
|
+
oid: meta[2],
|
|
98
|
+
name: entry.slice(tabIndex + 1),
|
|
99
|
+
};
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { createHash, createCipheriv, createDecipheriv, randomBytes } from 'node:crypto';
|
|
2
|
+
import CryptoPort from '../../ports/CryptoPort.js';
|
|
3
|
+
import CasError from '../../domain/errors/CasError.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Node.js implementation of CryptoPort using node:crypto.
|
|
7
|
+
*/
|
|
8
|
+
export default class NodeCryptoAdapter extends CryptoPort {
|
|
9
|
+
/** @override */
|
|
10
|
+
sha256(buf) {
|
|
11
|
+
return createHash('sha256').update(buf).digest('hex');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** @override */
|
|
15
|
+
randomBytes(n) {
|
|
16
|
+
return randomBytes(n);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** @override */
|
|
20
|
+
encryptBuffer(buffer, key) {
|
|
21
|
+
this.#validateKey(key);
|
|
22
|
+
const nonce = randomBytes(12);
|
|
23
|
+
const cipher = createCipheriv('aes-256-gcm', key, nonce);
|
|
24
|
+
const enc = Buffer.concat([cipher.update(buffer), cipher.final()]);
|
|
25
|
+
const tag = cipher.getAuthTag();
|
|
26
|
+
return {
|
|
27
|
+
buf: enc,
|
|
28
|
+
meta: this.#buildMeta(nonce, tag),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** @override */
|
|
33
|
+
decryptBuffer(buffer, key, meta) {
|
|
34
|
+
const nonce = Buffer.from(meta.nonce, 'base64');
|
|
35
|
+
const tag = Buffer.from(meta.tag, 'base64');
|
|
36
|
+
const decipher = createDecipheriv('aes-256-gcm', key, nonce);
|
|
37
|
+
decipher.setAuthTag(tag);
|
|
38
|
+
return Buffer.concat([decipher.update(buffer), decipher.final()]);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** @override */
|
|
42
|
+
createEncryptionStream(key) {
|
|
43
|
+
this.#validateKey(key);
|
|
44
|
+
const nonce = randomBytes(12);
|
|
45
|
+
const cipher = createCipheriv('aes-256-gcm', key, nonce);
|
|
46
|
+
|
|
47
|
+
const encrypt = async function* (source) {
|
|
48
|
+
for await (const chunk of source) {
|
|
49
|
+
const encrypted = cipher.update(chunk);
|
|
50
|
+
if (encrypted.length > 0) {
|
|
51
|
+
yield encrypted;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
const final = cipher.final();
|
|
55
|
+
if (final.length > 0) {
|
|
56
|
+
yield final;
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const finalize = () => {
|
|
61
|
+
const tag = cipher.getAuthTag();
|
|
62
|
+
return this.#buildMeta(nonce, tag);
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
return { encrypt, finalize };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Validates that a key is a 32-byte Buffer.
|
|
70
|
+
* @param {Buffer} key
|
|
71
|
+
* @throws {CasError} INVALID_KEY_TYPE | INVALID_KEY_LENGTH
|
|
72
|
+
*/
|
|
73
|
+
#validateKey(key) {
|
|
74
|
+
if (!Buffer.isBuffer(key)) {
|
|
75
|
+
throw new CasError(
|
|
76
|
+
'Encryption key must be a Buffer',
|
|
77
|
+
'INVALID_KEY_TYPE',
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
if (key.length !== 32) {
|
|
81
|
+
throw new CasError(
|
|
82
|
+
`Encryption key must be 32 bytes, got ${key.length}`,
|
|
83
|
+
'INVALID_KEY_LENGTH',
|
|
84
|
+
{ expected: 32, actual: key.length },
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Builds the encryption metadata object.
|
|
91
|
+
* @param {Buffer} nonce - 12-byte AES-GCM nonce.
|
|
92
|
+
* @param {Buffer} tag - 16-byte GCM authentication tag.
|
|
93
|
+
* @returns {{ algorithm: string, nonce: string, tag: string, encrypted: boolean }}
|
|
94
|
+
*/
|
|
95
|
+
#buildMeta(nonce, tag) {
|
|
96
|
+
return {
|
|
97
|
+
algorithm: 'aes-256-gcm',
|
|
98
|
+
nonce: nonce.toString('base64'),
|
|
99
|
+
tag: tag.toString('base64'),
|
|
100
|
+
encrypted: true,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import CryptoPort from '../../ports/CryptoPort.js';
|
|
2
|
+
import CasError from '../../domain/errors/CasError.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* {@link CryptoPort} implementation using the Web Crypto API.
|
|
6
|
+
*
|
|
7
|
+
* Works in Deno, browsers, and other environments supporting `globalThis.crypto.subtle`.
|
|
8
|
+
* Note: streaming encryption buffers all data internally because Web Crypto's
|
|
9
|
+
* AES-GCM is a one-shot API (the GCM tag is computed over the entire plaintext).
|
|
10
|
+
*/
|
|
11
|
+
export default class WebCryptoAdapter extends CryptoPort {
|
|
12
|
+
/** @override */
|
|
13
|
+
async sha256(buf) {
|
|
14
|
+
const hashBuffer = await globalThis.crypto.subtle.digest('SHA-256', buf);
|
|
15
|
+
return Array.from(new Uint8Array(hashBuffer))
|
|
16
|
+
.map(b => b.toString(16).padStart(2, '0'))
|
|
17
|
+
.join('');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** @override */
|
|
21
|
+
randomBytes(n) {
|
|
22
|
+
const uint8 = globalThis.crypto.getRandomValues(new Uint8Array(n));
|
|
23
|
+
if (globalThis.Buffer) {
|
|
24
|
+
return Buffer.from(uint8.buffer, uint8.byteOffset, uint8.byteLength);
|
|
25
|
+
}
|
|
26
|
+
return uint8;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** @override */
|
|
30
|
+
async encryptBuffer(buffer, key) {
|
|
31
|
+
this.#validateKey(key);
|
|
32
|
+
const nonce = this.randomBytes(12);
|
|
33
|
+
const cryptoKey = await this.#importKey(key);
|
|
34
|
+
|
|
35
|
+
// AES-GCM in Web Crypto includes the tag at the end of the ciphertext
|
|
36
|
+
const encrypted = await globalThis.crypto.subtle.encrypt(
|
|
37
|
+
{ name: 'AES-GCM', iv: nonce },
|
|
38
|
+
cryptoKey,
|
|
39
|
+
buffer
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
const fullBuffer = new Uint8Array(encrypted);
|
|
43
|
+
const tagLength = 16;
|
|
44
|
+
const ciphertext = fullBuffer.slice(0, -tagLength);
|
|
45
|
+
const tag = fullBuffer.slice(-tagLength);
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
buf: Buffer.from(ciphertext),
|
|
49
|
+
meta: this.#buildMeta(nonce, tag),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** @override */
|
|
54
|
+
async decryptBuffer(buffer, key, meta) {
|
|
55
|
+
const nonce = this.#fromBase64(meta.nonce);
|
|
56
|
+
const tag = this.#fromBase64(meta.tag);
|
|
57
|
+
const cryptoKey = await this.#importKey(key);
|
|
58
|
+
|
|
59
|
+
// Reconstruct Web Crypto format (ciphertext + tag)
|
|
60
|
+
const combined = new Uint8Array(buffer.length + tag.length);
|
|
61
|
+
combined.set(new Uint8Array(buffer));
|
|
62
|
+
combined.set(tag, buffer.length);
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
const decrypted = await globalThis.crypto.subtle.decrypt(
|
|
66
|
+
{ name: 'AES-GCM', iv: nonce },
|
|
67
|
+
cryptoKey,
|
|
68
|
+
combined
|
|
69
|
+
);
|
|
70
|
+
return Buffer.from(decrypted);
|
|
71
|
+
} catch (err) {
|
|
72
|
+
throw new CasError('Decryption failed', 'INTEGRITY_ERROR', { originalError: err });
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** @override */
|
|
77
|
+
createEncryptionStream(key) {
|
|
78
|
+
this.#validateKey(key);
|
|
79
|
+
const nonce = this.randomBytes(12);
|
|
80
|
+
const cryptoKeyPromise = this.#importKey(key);
|
|
81
|
+
|
|
82
|
+
// Web Crypto doesn't have a native streaming AES-GCM API like Node
|
|
83
|
+
// We have to buffer for the one-shot call because GCM tag is computed over the whole thing.
|
|
84
|
+
// NOTE: This limits the "stream" to memory capacity, matching the project's
|
|
85
|
+
// current CasService.restore limitation.
|
|
86
|
+
const chunks = [];
|
|
87
|
+
let finalTag = null;
|
|
88
|
+
|
|
89
|
+
const encrypt = async function* (source) {
|
|
90
|
+
for await (const chunk of source) {
|
|
91
|
+
chunks.push(chunk);
|
|
92
|
+
// We can't yield partial encrypted chunks for GCM in Web Crypto
|
|
93
|
+
// without complex chunk-chaining which would break compatibility
|
|
94
|
+
// with the Node adapter's single-stream GCM.
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const buffer = Buffer.concat(chunks);
|
|
98
|
+
const cryptoKey = await cryptoKeyPromise;
|
|
99
|
+
const encrypted = await globalThis.crypto.subtle.encrypt(
|
|
100
|
+
{ name: 'AES-GCM', iv: nonce },
|
|
101
|
+
cryptoKey,
|
|
102
|
+
buffer
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
const fullBuffer = new Uint8Array(encrypted);
|
|
106
|
+
const tagLength = 16;
|
|
107
|
+
const ciphertext = fullBuffer.slice(0, -tagLength);
|
|
108
|
+
finalTag = fullBuffer.slice(-tagLength);
|
|
109
|
+
|
|
110
|
+
yield Buffer.from(ciphertext);
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const finalize = () => {
|
|
114
|
+
return this.#buildMeta(nonce, finalTag);
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
return { encrypt, finalize };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Imports a raw key for use with Web Crypto AES-GCM operations.
|
|
122
|
+
* @param {Buffer|Uint8Array} rawKey - 32-byte raw key material.
|
|
123
|
+
* @returns {Promise<CryptoKey>}
|
|
124
|
+
*/
|
|
125
|
+
async #importKey(rawKey) {
|
|
126
|
+
return globalThis.crypto.subtle.importKey(
|
|
127
|
+
'raw',
|
|
128
|
+
rawKey,
|
|
129
|
+
{ name: 'AES-GCM' },
|
|
130
|
+
false,
|
|
131
|
+
['encrypt', 'decrypt']
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Validates that a key is a 32-byte Buffer or Uint8Array.
|
|
137
|
+
* @param {Buffer|Uint8Array} key
|
|
138
|
+
* @throws {CasError} INVALID_KEY_TYPE | INVALID_KEY_LENGTH
|
|
139
|
+
*/
|
|
140
|
+
#validateKey(key) {
|
|
141
|
+
if (!globalThis.Buffer?.isBuffer(key) && !(key instanceof Uint8Array)) {
|
|
142
|
+
throw new CasError(
|
|
143
|
+
'Encryption key must be a Buffer or Uint8Array',
|
|
144
|
+
'INVALID_KEY_TYPE',
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
if (key.length !== 32) {
|
|
148
|
+
throw new CasError(
|
|
149
|
+
`Encryption key must be 32 bytes, got ${key.length}`,
|
|
150
|
+
'INVALID_KEY_LENGTH',
|
|
151
|
+
{ expected: 32, actual: key.length },
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Builds the encryption metadata object.
|
|
158
|
+
* @param {Uint8Array} nonce - 12-byte AES-GCM nonce.
|
|
159
|
+
* @param {Uint8Array} tag - 16-byte GCM authentication tag.
|
|
160
|
+
* @returns {{ algorithm: string, nonce: string, tag: string, encrypted: boolean }}
|
|
161
|
+
*/
|
|
162
|
+
#buildMeta(nonce, tag) {
|
|
163
|
+
return {
|
|
164
|
+
algorithm: 'aes-256-gcm',
|
|
165
|
+
nonce: this.#toBase64(nonce),
|
|
166
|
+
tag: this.#toBase64(tag),
|
|
167
|
+
encrypted: true,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Encodes binary data to base64, using Buffer when available.
|
|
173
|
+
* @param {Uint8Array} buf
|
|
174
|
+
* @returns {string}
|
|
175
|
+
*/
|
|
176
|
+
#toBase64(buf) {
|
|
177
|
+
if (globalThis.Buffer) {
|
|
178
|
+
return Buffer.from(buf).toString('base64');
|
|
179
|
+
}
|
|
180
|
+
return globalThis.btoa(String.fromCharCode(...new Uint8Array(buf)));
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Decodes a base64 string to binary, using Buffer when available.
|
|
185
|
+
* @param {string} str
|
|
186
|
+
* @returns {Buffer|Uint8Array}
|
|
187
|
+
*/
|
|
188
|
+
#fromBase64(str) {
|
|
189
|
+
if (globalThis.Buffer) {
|
|
190
|
+
return Buffer.from(str, 'base64');
|
|
191
|
+
}
|
|
192
|
+
return Uint8Array.from(globalThis.atob(str), c => c.charCodeAt(0));
|
|
193
|
+
}
|
|
194
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import CodecPort from '../../ports/CodecPort.js';
|
|
2
|
+
import { encode, decode } from 'cbor-x';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* {@link CodecPort} implementation that serializes manifests as CBOR (binary).
|
|
6
|
+
*/
|
|
7
|
+
export default class CborCodec extends CodecPort {
|
|
8
|
+
/** @override */
|
|
9
|
+
encode(data) {
|
|
10
|
+
return encode(data);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** @override */
|
|
14
|
+
decode(buffer) {
|
|
15
|
+
return decode(buffer);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** @override */
|
|
19
|
+
get extension() {
|
|
20
|
+
return 'cbor';
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import CodecPort from '../../ports/CodecPort.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* {@link CodecPort} implementation that serializes manifests as pretty-printed JSON.
|
|
5
|
+
*/
|
|
6
|
+
export default class JsonCodec extends CodecPort {
|
|
7
|
+
/** @override */
|
|
8
|
+
encode(data) {
|
|
9
|
+
// Determine if we need to handle Buffers specially for JSON
|
|
10
|
+
// For now, we assume data is JSON-safe or uses toJSON() methods
|
|
11
|
+
return JSON.stringify(data, null, 2);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** @override */
|
|
15
|
+
decode(buffer) {
|
|
16
|
+
return JSON.parse(buffer.toString('utf8'));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** @override */
|
|
20
|
+
get extension() {
|
|
21
|
+
return 'json';
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Abstract interface for encoding and decoding manifest data.
|
|
3
|
+
* @abstract
|
|
4
|
+
*/
|
|
5
|
+
export default class CodecPort {
|
|
6
|
+
/**
|
|
7
|
+
* Encodes data to a Buffer or string.
|
|
8
|
+
* @param {Object} data
|
|
9
|
+
* @returns {Buffer|string}
|
|
10
|
+
*/
|
|
11
|
+
encode(_data) {
|
|
12
|
+
throw new Error('Not implemented');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Decodes data from a Buffer or string.
|
|
17
|
+
* @param {Buffer|string} buffer
|
|
18
|
+
* @returns {Object}
|
|
19
|
+
*/
|
|
20
|
+
decode(_buffer) {
|
|
21
|
+
throw new Error('Not implemented');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Returns the file extension for this codec (e.g. 'json', 'cbor').
|
|
26
|
+
* @returns {string}
|
|
27
|
+
*/
|
|
28
|
+
get extension() {
|
|
29
|
+
throw new Error('Not implemented');
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Abstract port for cryptographic operations (hashing, random bytes, AES-256-GCM).
|
|
3
|
+
* @abstract
|
|
4
|
+
*/
|
|
5
|
+
export default class CryptoPort {
|
|
6
|
+
/**
|
|
7
|
+
* Returns the SHA-256 hex digest of a buffer.
|
|
8
|
+
* @param {Buffer} buf
|
|
9
|
+
* @returns {string} 64-char hex digest
|
|
10
|
+
*/
|
|
11
|
+
sha256(_buf) {
|
|
12
|
+
throw new Error('Not implemented');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Returns a Buffer of n cryptographically random bytes.
|
|
17
|
+
* @param {number} n
|
|
18
|
+
* @returns {Buffer}
|
|
19
|
+
*/
|
|
20
|
+
randomBytes(_n) {
|
|
21
|
+
throw new Error('Not implemented');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Encrypts a buffer using AES-256-GCM.
|
|
26
|
+
* @param {Buffer} buffer
|
|
27
|
+
* @param {Buffer} key - 32-byte encryption key
|
|
28
|
+
* @returns {{ buf: Buffer, meta: { algorithm: string, nonce: string, tag: string, encrypted: boolean } }}
|
|
29
|
+
*/
|
|
30
|
+
encryptBuffer(_buffer, _key) {
|
|
31
|
+
throw new Error('Not implemented');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Decrypts a buffer using AES-256-GCM.
|
|
36
|
+
* @param {Buffer} buffer
|
|
37
|
+
* @param {Buffer} key - 32-byte encryption key
|
|
38
|
+
* @param {{ algorithm: string, nonce: string, tag: string, encrypted: boolean }} meta
|
|
39
|
+
* @returns {Buffer}
|
|
40
|
+
* @throws on authentication failure
|
|
41
|
+
*/
|
|
42
|
+
decryptBuffer(_buffer, _key, _meta) {
|
|
43
|
+
throw new Error('Not implemented');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Creates a streaming encryption context.
|
|
48
|
+
* @param {Buffer} key - 32-byte encryption key
|
|
49
|
+
* @returns {{ encrypt: (source: AsyncIterable<Buffer>) => AsyncIterable<Buffer>, finalize: () => { algorithm: string, nonce: string, tag: string, encrypted: boolean } }}
|
|
50
|
+
*/
|
|
51
|
+
createEncryptionStream(_key) {
|
|
52
|
+
throw new Error('Not implemented');
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Abstract port for persisting data to Git's object database.
|
|
3
|
+
* @abstract
|
|
4
|
+
*/
|
|
5
|
+
export default class GitPersistencePort {
|
|
6
|
+
/**
|
|
7
|
+
* Writes content as a Git blob object.
|
|
8
|
+
* @param {Buffer|string} content - Data to store.
|
|
9
|
+
* @returns {Promise<string>} The Git OID of the stored blob.
|
|
10
|
+
*/
|
|
11
|
+
async writeBlob(_content) {
|
|
12
|
+
throw new Error('Not implemented');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Creates a Git tree object from formatted entries.
|
|
17
|
+
* @param {string[]} entries - Lines in `git mktree` format.
|
|
18
|
+
* @returns {Promise<string>} The Git OID of the created tree.
|
|
19
|
+
*/
|
|
20
|
+
async writeTree(_entries) {
|
|
21
|
+
throw new Error('Not implemented');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Reads a Git blob by its OID.
|
|
26
|
+
* @param {string} oid - Git object ID.
|
|
27
|
+
* @returns {Promise<Buffer>} The blob content.
|
|
28
|
+
*/
|
|
29
|
+
async readBlob(_oid) {
|
|
30
|
+
throw new Error('Not implemented');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Reads and parses a Git tree object.
|
|
35
|
+
* @param {string} treeOid - Git tree OID.
|
|
36
|
+
* @returns {Promise<Array<{ mode: string, type: string, oid: string, name: string }>>} Parsed tree entries.
|
|
37
|
+
*/
|
|
38
|
+
async readTree(_treeOid) {
|
|
39
|
+
throw new Error('Not implemented');
|
|
40
|
+
}
|
|
41
|
+
}
|