withub-cli 0.1.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/LICENSE +201 -0
- package/README.md +41 -0
- package/dist/commands/account.js +109 -0
- package/dist/commands/checkout.js +213 -0
- package/dist/commands/clone.js +263 -0
- package/dist/commands/commit.js +150 -0
- package/dist/commands/diff.js +159 -0
- package/dist/commands/fetch.js +169 -0
- package/dist/commands/init.js +125 -0
- package/dist/commands/invite.js +72 -0
- package/dist/commands/list.js +214 -0
- package/dist/commands/plumbing.js +98 -0
- package/dist/commands/pull.js +160 -0
- package/dist/commands/push.js +371 -0
- package/dist/commands/registerCommands.js +183 -0
- package/dist/commands/removeUser.js +63 -0
- package/dist/commands/stub.js +11 -0
- package/dist/commands/transfer.js +46 -0
- package/dist/commands/walrusBlob.js +50 -0
- package/dist/commands/walrusQuilt.js +282 -0
- package/dist/commands/workspace.js +260 -0
- package/dist/index.js +46 -0
- package/dist/lib/config.js +49 -0
- package/dist/lib/constants.js +6 -0
- package/dist/lib/fs.js +154 -0
- package/dist/lib/keys.js +224 -0
- package/dist/lib/manifest.js +37 -0
- package/dist/lib/quilt.js +38 -0
- package/dist/lib/repo.js +70 -0
- package/dist/lib/schema.js +53 -0
- package/dist/lib/seal.js +157 -0
- package/dist/lib/serialize.js +30 -0
- package/dist/lib/state.js +57 -0
- package/dist/lib/suiRepo.js +220 -0
- package/dist/lib/ui.js +51 -0
- package/dist/lib/validate.js +13 -0
- package/dist/lib/walrus.js +237 -0
- package/package.json +57 -0
package/dist/lib/keys.js
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.KEY_HOME = void 0;
|
|
7
|
+
exports.keyPathFor = keyPathFor;
|
|
8
|
+
exports.createSigner = createSigner;
|
|
9
|
+
exports.loadSigner = loadSigner;
|
|
10
|
+
exports.checkResources = checkResources;
|
|
11
|
+
exports.normalizeAddress = normalizeAddress;
|
|
12
|
+
exports.listStoredKeys = listStoredKeys;
|
|
13
|
+
exports.readActiveAddress = readActiveAddress;
|
|
14
|
+
exports.setActiveAddress = setActiveAddress;
|
|
15
|
+
const promises_1 = __importDefault(require("fs/promises"));
|
|
16
|
+
const os_1 = __importDefault(require("os"));
|
|
17
|
+
const path_1 = __importDefault(require("path"));
|
|
18
|
+
const ed25519_1 = require("@mysten/sui/keypairs/ed25519");
|
|
19
|
+
const client_1 = require("@mysten/sui/client");
|
|
20
|
+
const walrus_1 = require("./walrus");
|
|
21
|
+
exports.KEY_HOME = process.env.WIT_KEY_HOME || path_1.default.join(os_1.default.homedir(), '.wit', 'keys');
|
|
22
|
+
const GLOBAL_CONFIG = path_1.default.join(os_1.default.homedir(), '.witconfig');
|
|
23
|
+
const SUI_COIN = '0x2::sui::SUI';
|
|
24
|
+
// WAL CoinType map (9 decimals). Default to testnet when unknown.
|
|
25
|
+
const WAL_COIN_BY_NETWORK = {
|
|
26
|
+
testnet: '0x8270feb7375eee355e64fdb69c50abb6b5f9393a722883c1cf45f8e26048810a::wal::WAL',
|
|
27
|
+
mainnet: '0x356a26eb9e012a68958082340d4c4116e7f55615cf27affcff209cf0ae544f59::wal::WAL',
|
|
28
|
+
};
|
|
29
|
+
const MIN_SUI_BALANCE = 100000000n; // 0.1 SUI (in MIST) for smoother dev flow
|
|
30
|
+
const MIN_WAL_BALANCE = 1000000000n; // 1 WAL (assuming 9 decimals)
|
|
31
|
+
function keyPathFor(address) {
|
|
32
|
+
return path_1.default.join(exports.KEY_HOME, `${normalizeAddress(address)}.key`);
|
|
33
|
+
}
|
|
34
|
+
async function createSigner(alias = 'default') {
|
|
35
|
+
const keypair = ed25519_1.Ed25519Keypair.generate();
|
|
36
|
+
const address = keypair.getPublicKey().toSuiAddress();
|
|
37
|
+
const file = keyPathFor(address);
|
|
38
|
+
await promises_1.default.mkdir(path_1.default.dirname(file), { recursive: true });
|
|
39
|
+
const payload = {
|
|
40
|
+
scheme: 'ED25519',
|
|
41
|
+
privateKey: keypair.getSecretKey(),
|
|
42
|
+
address,
|
|
43
|
+
publicKey: Buffer.from(keypair.getPublicKey().toRawBytes()).toString('base64'),
|
|
44
|
+
createdAt: new Date().toISOString(),
|
|
45
|
+
alias,
|
|
46
|
+
};
|
|
47
|
+
await promises_1.default.writeFile(file, JSON.stringify(payload, null, 2) + '\n', { encoding: 'utf8', mode: 0o600 });
|
|
48
|
+
await upsertGlobalConfig((cfg) => ({
|
|
49
|
+
...cfg,
|
|
50
|
+
active_address: address,
|
|
51
|
+
key_alias: alias || cfg.key_alias || 'default',
|
|
52
|
+
author: cfg.author && cfg.author !== 'unknown' ? cfg.author : address,
|
|
53
|
+
}));
|
|
54
|
+
return { signer: keypair, address, file };
|
|
55
|
+
}
|
|
56
|
+
async function loadSigner(address) {
|
|
57
|
+
const globalCfg = await readGlobalConfig();
|
|
58
|
+
const resolvedAddress = normalizeAddress(address || globalCfg.active_address || guessAddress(globalCfg.author));
|
|
59
|
+
if (!resolvedAddress) {
|
|
60
|
+
throw new Error('No active address configured. Generate a key with createSigner() or set active_address in ~/.witconfig.');
|
|
61
|
+
}
|
|
62
|
+
const file = keyPathFor(resolvedAddress);
|
|
63
|
+
const stored = await readKeyFile(file);
|
|
64
|
+
const signer = ed25519_1.Ed25519Keypair.fromSecretKey(stored.privateKey);
|
|
65
|
+
const derived = signer.getPublicKey().toSuiAddress();
|
|
66
|
+
if (stored.address && stored.address !== derived) {
|
|
67
|
+
// eslint-disable-next-line no-console
|
|
68
|
+
console.warn(`Warning: address mismatch for ${file}. Using derived address ${derived}.`);
|
|
69
|
+
}
|
|
70
|
+
return { signer, address: derived, file };
|
|
71
|
+
}
|
|
72
|
+
async function checkResources(address, opts) {
|
|
73
|
+
const minSui = opts?.minSui ?? MIN_SUI_BALANCE;
|
|
74
|
+
const minWal = opts?.minWal ?? MIN_WAL_BALANCE;
|
|
75
|
+
const resolved = await safeResolveWalrusConfig();
|
|
76
|
+
let rpcUrl = opts?.rpcUrl;
|
|
77
|
+
if (!rpcUrl) {
|
|
78
|
+
try {
|
|
79
|
+
rpcUrl = resolved?.suiRpcUrl;
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
rpcUrl = (0, client_1.getFullnodeUrl)('testnet');
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if (!rpcUrl) {
|
|
86
|
+
rpcUrl = (0, client_1.getFullnodeUrl)('testnet');
|
|
87
|
+
}
|
|
88
|
+
const client = new client_1.SuiClient({ url: rpcUrl });
|
|
89
|
+
try {
|
|
90
|
+
const respSui = await client.getBalance({ owner: address, coinType: SUI_COIN, signal: opts?.signal });
|
|
91
|
+
const rawSui = respSui?.balance?.balance ?? respSui?.totalBalance ?? respSui?.balance;
|
|
92
|
+
const suiBalance = typeof rawSui === 'string' ? BigInt(rawSui) : BigInt(rawSui || 0);
|
|
93
|
+
const walCoin = opts?.walCoin || (resolved?.network ? WAL_COIN_BY_NETWORK[resolved.network] : undefined) || WAL_COIN_BY_NETWORK.testnet;
|
|
94
|
+
let walBalance;
|
|
95
|
+
let hasMinWal = null;
|
|
96
|
+
let walError;
|
|
97
|
+
try {
|
|
98
|
+
const respWal = await client.getBalance({ owner: address, coinType: walCoin, signal: opts?.signal });
|
|
99
|
+
const rawWal = respWal?.balance?.balance ?? respWal?.totalBalance ?? respWal?.balance;
|
|
100
|
+
walBalance = typeof rawWal === 'string' ? BigInt(rawWal) : BigInt(rawWal || 0);
|
|
101
|
+
hasMinWal = walBalance >= minWal;
|
|
102
|
+
}
|
|
103
|
+
catch (err) {
|
|
104
|
+
walError = err?.message || String(err);
|
|
105
|
+
}
|
|
106
|
+
return {
|
|
107
|
+
suiBalance,
|
|
108
|
+
hasMinSui: suiBalance >= minSui,
|
|
109
|
+
minSui,
|
|
110
|
+
walBalance,
|
|
111
|
+
hasMinWal,
|
|
112
|
+
minWal,
|
|
113
|
+
walError,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
catch (err) {
|
|
117
|
+
return { hasMinSui: null, minSui, hasMinWal: null, minWal, error: err?.message || String(err) };
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
async function readKeyFile(file) {
|
|
121
|
+
try {
|
|
122
|
+
const raw = await promises_1.default.readFile(file, 'utf8');
|
|
123
|
+
const parsed = JSON.parse(raw);
|
|
124
|
+
if (!parsed.privateKey) {
|
|
125
|
+
throw new Error(`Missing privateKey in ${file}`);
|
|
126
|
+
}
|
|
127
|
+
return parsed;
|
|
128
|
+
}
|
|
129
|
+
catch (err) {
|
|
130
|
+
if (err?.code === 'ENOENT') {
|
|
131
|
+
throw new Error(`Key file not found: ${file}`);
|
|
132
|
+
}
|
|
133
|
+
throw err;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
async function readGlobalConfig() {
|
|
137
|
+
try {
|
|
138
|
+
const raw = await promises_1.default.readFile(GLOBAL_CONFIG, 'utf8');
|
|
139
|
+
return JSON.parse(raw);
|
|
140
|
+
}
|
|
141
|
+
catch (err) {
|
|
142
|
+
if (err?.code === 'ENOENT')
|
|
143
|
+
return {};
|
|
144
|
+
// eslint-disable-next-line no-console
|
|
145
|
+
console.warn(`Warning: could not read ${GLOBAL_CONFIG}: ${err.message}`);
|
|
146
|
+
return {};
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
async function upsertGlobalConfig(mutator) {
|
|
150
|
+
const cfg = await readGlobalConfig();
|
|
151
|
+
const next = mutator(cfg);
|
|
152
|
+
await promises_1.default.writeFile(GLOBAL_CONFIG, JSON.stringify(next, null, 2) + '\n', 'utf8');
|
|
153
|
+
}
|
|
154
|
+
function normalizeAddress(addr) {
|
|
155
|
+
if (!addr)
|
|
156
|
+
return '';
|
|
157
|
+
const normalized = addr.toLowerCase();
|
|
158
|
+
return normalized.startsWith('0x') ? normalized : `0x${normalized}`;
|
|
159
|
+
}
|
|
160
|
+
function guessAddress(author) {
|
|
161
|
+
if (!author)
|
|
162
|
+
return undefined;
|
|
163
|
+
if (/^0x[0-9a-fA-F]+$/.test(author.trim()))
|
|
164
|
+
return author.trim();
|
|
165
|
+
return undefined;
|
|
166
|
+
}
|
|
167
|
+
async function listStoredKeys() {
|
|
168
|
+
let entries;
|
|
169
|
+
try {
|
|
170
|
+
entries = await promises_1.default.readdir(exports.KEY_HOME, { withFileTypes: true });
|
|
171
|
+
}
|
|
172
|
+
catch (err) {
|
|
173
|
+
if (err?.code === 'ENOENT')
|
|
174
|
+
return [];
|
|
175
|
+
throw err;
|
|
176
|
+
}
|
|
177
|
+
const keys = [];
|
|
178
|
+
for (const entry of entries) {
|
|
179
|
+
if (!entry.isFile() || !entry.name.endsWith('.key'))
|
|
180
|
+
continue;
|
|
181
|
+
const file = path_1.default.join(exports.KEY_HOME, entry.name);
|
|
182
|
+
try {
|
|
183
|
+
const parsed = await readKeyFile(file);
|
|
184
|
+
const derived = parsed.address || ed25519_1.Ed25519Keypair.fromSecretKey(parsed.privateKey).getPublicKey().toSuiAddress();
|
|
185
|
+
keys.push({
|
|
186
|
+
address: normalizeAddress(derived),
|
|
187
|
+
alias: parsed.alias,
|
|
188
|
+
file,
|
|
189
|
+
createdAt: parsed.createdAt,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
catch (err) {
|
|
193
|
+
// eslint-disable-next-line no-console
|
|
194
|
+
console.warn(`Skipping key ${file}: ${err.message}`);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return keys.sort((a, b) => (a.createdAt || '').localeCompare(b.createdAt || ''));
|
|
198
|
+
}
|
|
199
|
+
async function readActiveAddress() {
|
|
200
|
+
const cfg = await readGlobalConfig();
|
|
201
|
+
if (cfg.active_address)
|
|
202
|
+
return normalizeAddress(cfg.active_address);
|
|
203
|
+
const guessed = guessAddress(cfg.author);
|
|
204
|
+
return guessed ? normalizeAddress(guessed) : null;
|
|
205
|
+
}
|
|
206
|
+
async function setActiveAddress(address, opts) {
|
|
207
|
+
await upsertGlobalConfig((cfg) => {
|
|
208
|
+
const next = { ...cfg, active_address: normalizeAddress(address) };
|
|
209
|
+
if (opts?.alias)
|
|
210
|
+
next.key_alias = opts.alias;
|
|
211
|
+
if (opts?.updateAuthorIfUnknown && (!cfg.author || cfg.author === 'unknown')) {
|
|
212
|
+
next.author = normalizeAddress(address);
|
|
213
|
+
}
|
|
214
|
+
return next;
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
async function safeResolveWalrusConfig() {
|
|
218
|
+
try {
|
|
219
|
+
return await (0, walrus_1.resolveWalrusConfig)();
|
|
220
|
+
}
|
|
221
|
+
catch {
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.computeRootHash = computeRootHash;
|
|
4
|
+
exports.buildManifest = buildManifest;
|
|
5
|
+
const fs_1 = require("./fs");
|
|
6
|
+
const serialize_1 = require("./serialize");
|
|
7
|
+
const schema_1 = require("./schema");
|
|
8
|
+
function computeRootHash(index) {
|
|
9
|
+
const entries = Object.keys(index)
|
|
10
|
+
.sort()
|
|
11
|
+
.map((rel) => {
|
|
12
|
+
const meta = index[rel];
|
|
13
|
+
return {
|
|
14
|
+
path: (0, fs_1.pathToPosix)(rel),
|
|
15
|
+
hash: meta.hash,
|
|
16
|
+
size: meta.size,
|
|
17
|
+
mode: meta.mode,
|
|
18
|
+
mtime: meta.mtime,
|
|
19
|
+
};
|
|
20
|
+
});
|
|
21
|
+
const serialized = (0, serialize_1.canonicalStringify)(entries);
|
|
22
|
+
return (0, serialize_1.sha256Base64)(serialized);
|
|
23
|
+
}
|
|
24
|
+
function buildManifest(index, quiltId) {
|
|
25
|
+
const files = {};
|
|
26
|
+
for (const rel of Object.keys(index).sort()) {
|
|
27
|
+
const posix = (0, fs_1.pathToPosix)(rel);
|
|
28
|
+
files[posix] = index[rel];
|
|
29
|
+
}
|
|
30
|
+
const manifest = {
|
|
31
|
+
version: 1,
|
|
32
|
+
quilt_id: quiltId,
|
|
33
|
+
root_hash: computeRootHash(index),
|
|
34
|
+
files,
|
|
35
|
+
};
|
|
36
|
+
return schema_1.ManifestSchema.parse(manifest);
|
|
37
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.fetchQuiltFileById = fetchQuiltFileById;
|
|
4
|
+
exports.listQuiltIdentifiers = listQuiltIdentifiers;
|
|
5
|
+
const walrus_1 = require("./walrus");
|
|
6
|
+
async function fetchQuiltFileById(quiltId, identifier) {
|
|
7
|
+
const svc = await walrus_1.WalrusService.fromRepo();
|
|
8
|
+
// Prefer aggregator fast path; fallback to relay blob/files
|
|
9
|
+
try {
|
|
10
|
+
const bytes = await svc.readQuiltFile(quiltId, identifier);
|
|
11
|
+
return { bytes, tags: {} };
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
// fallback to relay
|
|
15
|
+
}
|
|
16
|
+
const blob = await svc.getClient().getBlob({ blobId: quiltId });
|
|
17
|
+
const files = await blob.files({ identifiers: [identifier] });
|
|
18
|
+
if (files.length) {
|
|
19
|
+
const file = files[0];
|
|
20
|
+
return { bytes: await file.bytes(), tags: await file.getTags() };
|
|
21
|
+
}
|
|
22
|
+
throw new Error(`Identifier not found in quilt: ${identifier}`);
|
|
23
|
+
}
|
|
24
|
+
async function listQuiltIdentifiers(quiltId) {
|
|
25
|
+
const svc = await walrus_1.WalrusService.fromRepo();
|
|
26
|
+
try {
|
|
27
|
+
const blob = await svc.getClient().getBlob({ blobId: quiltId });
|
|
28
|
+
const files = await blob.files();
|
|
29
|
+
const ids = await Promise.all(files.map((f) => f.getIdentifier()));
|
|
30
|
+
return ids.filter((i) => !!i).sort();
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
// fallback: getFiles and list identifiers
|
|
34
|
+
const files = await svc.getClient().getFiles({ ids: [quiltId] });
|
|
35
|
+
const ids = await Promise.all(files.map((f) => f.getIdentifier()));
|
|
36
|
+
return ids.filter((i) => !!i).sort();
|
|
37
|
+
}
|
|
38
|
+
}
|
package/dist/lib/repo.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.requireWitDir = requireWitDir;
|
|
7
|
+
exports.readRepoConfig = readRepoConfig;
|
|
8
|
+
exports.writeRepoConfig = writeRepoConfig;
|
|
9
|
+
exports.readRemoteState = readRemoteState;
|
|
10
|
+
exports.writeRemoteState = writeRemoteState;
|
|
11
|
+
exports.readRemoteRef = readRemoteRef;
|
|
12
|
+
exports.writeRemoteRef = writeRemoteRef;
|
|
13
|
+
const promises_1 = __importDefault(require("fs/promises"));
|
|
14
|
+
const path_1 = __importDefault(require("path"));
|
|
15
|
+
const DEFAULT_REMOTE_REF = path_1.default.join('refs', 'remotes', 'main');
|
|
16
|
+
async function requireWitDir(cwd = process.cwd()) {
|
|
17
|
+
const dir = path_1.default.join(cwd, '.wit');
|
|
18
|
+
try {
|
|
19
|
+
await promises_1.default.access(dir);
|
|
20
|
+
return dir;
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
throw new Error('Not a wit repository (missing .wit). Run `wit init` first.');
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
async function readRepoConfig(witPath) {
|
|
27
|
+
const file = path_1.default.join(witPath, 'config.json');
|
|
28
|
+
const raw = await promises_1.default.readFile(file, 'utf8');
|
|
29
|
+
return JSON.parse(raw);
|
|
30
|
+
}
|
|
31
|
+
async function writeRepoConfig(witPath, cfg) {
|
|
32
|
+
const file = path_1.default.join(witPath, 'config.json');
|
|
33
|
+
await promises_1.default.writeFile(file, JSON.stringify(cfg, null, 2) + '\n', 'utf8');
|
|
34
|
+
}
|
|
35
|
+
async function readRemoteState(witPath) {
|
|
36
|
+
const file = path_1.default.join(witPath, 'state', 'remote.json');
|
|
37
|
+
try {
|
|
38
|
+
const raw = await promises_1.default.readFile(file, 'utf8');
|
|
39
|
+
return JSON.parse(raw);
|
|
40
|
+
}
|
|
41
|
+
catch (err) {
|
|
42
|
+
if (err?.code === 'ENOENT')
|
|
43
|
+
return null;
|
|
44
|
+
throw err;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
async function writeRemoteState(witPath, state) {
|
|
48
|
+
const file = path_1.default.join(witPath, 'state', 'remote.json');
|
|
49
|
+
await promises_1.default.mkdir(path_1.default.dirname(file), { recursive: true });
|
|
50
|
+
await promises_1.default.writeFile(file, JSON.stringify(state, null, 2) + '\n', 'utf8');
|
|
51
|
+
}
|
|
52
|
+
async function readRemoteRef(witPath) {
|
|
53
|
+
const file = path_1.default.join(witPath, DEFAULT_REMOTE_REF);
|
|
54
|
+
try {
|
|
55
|
+
const raw = await promises_1.default.readFile(file, 'utf8');
|
|
56
|
+
const val = raw.trim();
|
|
57
|
+
return val.length ? val : null;
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
if (err?.code === 'ENOENT')
|
|
61
|
+
return null;
|
|
62
|
+
throw err;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
async function writeRemoteRef(witPath, commitId) {
|
|
66
|
+
const file = path_1.default.join(witPath, DEFAULT_REMOTE_REF);
|
|
67
|
+
await promises_1.default.mkdir(path_1.default.dirname(file), { recursive: true });
|
|
68
|
+
const val = commitId ? `${commitId}\n` : '';
|
|
69
|
+
await promises_1.default.writeFile(file, val, 'utf8');
|
|
70
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.QuiltSchema = exports.QuiltEntrySchema = exports.CommitSchema = exports.ManifestSchema = void 0;
|
|
4
|
+
const zod_1 = require("zod");
|
|
5
|
+
const fileMeta = zod_1.z.object({
|
|
6
|
+
hash: zod_1.z.string().min(1),
|
|
7
|
+
size: zod_1.z.number().int().nonnegative(),
|
|
8
|
+
mode: zod_1.z.string().regex(/^\d{6}$/),
|
|
9
|
+
mtime: zod_1.z.number().int().nonnegative(),
|
|
10
|
+
id: zod_1.z.string().min(1).optional(),
|
|
11
|
+
enc: zod_1.z
|
|
12
|
+
.object({
|
|
13
|
+
alg: zod_1.z.union([zod_1.z.literal('aes-256-gcm'), zod_1.z.literal('seal-aes-256-gcm')]),
|
|
14
|
+
iv: zod_1.z.string().min(1),
|
|
15
|
+
tag: zod_1.z.string().min(1),
|
|
16
|
+
policy: zod_1.z.string().min(1).optional(),
|
|
17
|
+
policy_id: zod_1.z.string().min(1).optional(),
|
|
18
|
+
package_id: zod_1.z.string().min(1).optional(),
|
|
19
|
+
sealed_session_key: zod_1.z.string().min(1).optional(),
|
|
20
|
+
cipher_size: zod_1.z.number().int().nonnegative().optional(),
|
|
21
|
+
})
|
|
22
|
+
.optional(),
|
|
23
|
+
});
|
|
24
|
+
exports.ManifestSchema = zod_1.z.object({
|
|
25
|
+
version: zod_1.z.literal(1),
|
|
26
|
+
quilt_id: zod_1.z.string().min(1),
|
|
27
|
+
root_hash: zod_1.z.string().min(1),
|
|
28
|
+
files: zod_1.z.record(fileMeta),
|
|
29
|
+
});
|
|
30
|
+
exports.CommitSchema = zod_1.z.object({
|
|
31
|
+
tree: zod_1.z.object({
|
|
32
|
+
quilt_id: zod_1.z.string().min(1).nullable(),
|
|
33
|
+
manifest_id: zod_1.z.string().min(1).nullable(),
|
|
34
|
+
root_hash: zod_1.z.string().min(1),
|
|
35
|
+
files: zod_1.z.record(fileMeta).optional(),
|
|
36
|
+
}),
|
|
37
|
+
parent: zod_1.z.string().min(1).nullable(),
|
|
38
|
+
author: zod_1.z.string().min(1),
|
|
39
|
+
message: zod_1.z.string().min(1),
|
|
40
|
+
timestamp: zod_1.z.number().int().nonnegative(),
|
|
41
|
+
extras: zod_1.z.object({
|
|
42
|
+
patch_id: zod_1.z.string().nullable(),
|
|
43
|
+
tags: zod_1.z.record(zod_1.z.string()),
|
|
44
|
+
}),
|
|
45
|
+
});
|
|
46
|
+
exports.QuiltEntrySchema = zod_1.z.object({
|
|
47
|
+
identifier: zod_1.z.string().min(1),
|
|
48
|
+
contents: zod_1.z.instanceof(Uint8Array).or(zod_1.z.any()),
|
|
49
|
+
tags: zod_1.z.record(zod_1.z.string()).optional(),
|
|
50
|
+
});
|
|
51
|
+
exports.QuiltSchema = zod_1.z.object({
|
|
52
|
+
blobs: zod_1.z.array(exports.QuiltEntrySchema),
|
|
53
|
+
});
|
package/dist/lib/seal.js
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.getSealClient = getSealClient;
|
|
7
|
+
exports.encryptWithSeal = encryptWithSeal;
|
|
8
|
+
exports.decryptWithSeal = decryptWithSeal;
|
|
9
|
+
const seal_1 = require("@mysten/seal");
|
|
10
|
+
const seal_2 = require("@mysten/seal");
|
|
11
|
+
const transactions_1 = require("@mysten/sui/transactions");
|
|
12
|
+
const crypto_1 = __importDefault(require("crypto"));
|
|
13
|
+
// Seal Testnet Servers
|
|
14
|
+
const SEAL_SERVERS = [
|
|
15
|
+
{
|
|
16
|
+
objectId: '0x73d05d62c18d9374e3ea529e8e0ed6161da1a141a94d3f76ae3fe4e99356db75',
|
|
17
|
+
weight: 1,
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
objectId: '0xf5d14a81a982144ae441cd7d64b09027f116a468bd36e7eca494f750591623c8',
|
|
21
|
+
weight: 1,
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
objectId: '0x6068c0acb197dddbacd4746a9de7f025b2ed5a5b6c1b1ab44dade4426d141da2',
|
|
25
|
+
weight: 1,
|
|
26
|
+
}
|
|
27
|
+
];
|
|
28
|
+
let _sealClient = null;
|
|
29
|
+
function getSealClient(suiClient) {
|
|
30
|
+
if (!_sealClient) {
|
|
31
|
+
_sealClient = new seal_1.SealClient({
|
|
32
|
+
serverConfigs: SEAL_SERVERS,
|
|
33
|
+
suiClient: suiClient, // Cast to compatible client
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
return _sealClient;
|
|
37
|
+
}
|
|
38
|
+
// WAIT! If I use Seal to encrypt the whole file, I rely on Seal's DEM (Data Encapsulation Mechanism).
|
|
39
|
+
// Is it efficient for large files (GBs)?
|
|
40
|
+
// It likely uses AES-GCM or Chacha20Poly1305.
|
|
41
|
+
// But if I want to use my own chunking or streaming, I might want to encrypt a session key only.
|
|
42
|
+
// If I encrypt a session key (32 bytes) with Seal.
|
|
43
|
+
// I get `encryptedObject` (small).
|
|
44
|
+
// And `key` (the key used to encrypt the session key).
|
|
45
|
+
// This `key` is NOT the session key I passed!
|
|
46
|
+
// It's the key derived from KEM.
|
|
47
|
+
// So:
|
|
48
|
+
// 1. Generate `mySessionKey` (32 bytes).
|
|
49
|
+
// 2. Call `client.encrypt({ data: mySessionKey })`.
|
|
50
|
+
// 3. Get `res.encryptedObject` (Sealed Session Key).
|
|
51
|
+
// 4. Use `mySessionKey` to encrypt the file using AES-GCM.
|
|
52
|
+
// This is the standard "Envelope Encryption" pattern.
|
|
53
|
+
// And `decrypt`:
|
|
54
|
+
// 1. Call `client.decrypt({ data: sealedSessionKey })`.
|
|
55
|
+
// 2. Get `decryptedData` (which is `mySessionKey`).
|
|
56
|
+
// 3. Use `mySessionKey` to decrypt the file.
|
|
57
|
+
// This is better because it decouples file encryption from Seal SDK (which might change or have overhead).
|
|
58
|
+
// And allows me to use standard AES-GCM.
|
|
59
|
+
/**
|
|
60
|
+
* Encrypts data using Seal.
|
|
61
|
+
* 1. Seal SDK generates a symmetric key and encrypts it for the policy.
|
|
62
|
+
* 2. We use that symmetric key to encrypt the data (AES-GCM).
|
|
63
|
+
*/
|
|
64
|
+
async function encryptWithSeal(plain, policyId, packageId, suiClient) {
|
|
65
|
+
const client = getSealClient(suiClient);
|
|
66
|
+
// 1. Generate Session Key
|
|
67
|
+
const sessionKey = crypto_1.default.randomBytes(32);
|
|
68
|
+
const iv = crypto_1.default.randomBytes(12);
|
|
69
|
+
// 2. Seal the Session Key
|
|
70
|
+
const res = await client.encrypt({
|
|
71
|
+
packageId,
|
|
72
|
+
id: policyId,
|
|
73
|
+
data: sessionKey,
|
|
74
|
+
threshold: 1,
|
|
75
|
+
});
|
|
76
|
+
// 3. Encrypt Data with Session Key
|
|
77
|
+
const cipher = crypto_1.default.createCipheriv('aes-256-gcm', sessionKey, iv);
|
|
78
|
+
const cipherBuf = Buffer.concat([cipher.update(plain), cipher.final()]);
|
|
79
|
+
const tag = cipher.getAuthTag();
|
|
80
|
+
return {
|
|
81
|
+
cipher: cipherBuf,
|
|
82
|
+
meta: {
|
|
83
|
+
alg: 'seal-aes-256-gcm',
|
|
84
|
+
policy_id: policyId,
|
|
85
|
+
package_id: packageId,
|
|
86
|
+
sealed_session_key: Buffer.from(res.encryptedObject).toString('base64'),
|
|
87
|
+
iv: iv.toString('base64'),
|
|
88
|
+
tag: tag.toString('base64'),
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
// Rename to encryptWithSeal for compatibility
|
|
93
|
+
/**
|
|
94
|
+
* Decrypts data using Seal.
|
|
95
|
+
*/
|
|
96
|
+
async function decryptWithSeal(cipher, meta, signer, suiClient) {
|
|
97
|
+
if (meta.alg !== 'seal-aes-256-gcm') {
|
|
98
|
+
throw new Error(`Unsupported encryption alg: ${meta.alg}`);
|
|
99
|
+
}
|
|
100
|
+
try {
|
|
101
|
+
const client = getSealClient(suiClient);
|
|
102
|
+
const sealedKey = Buffer.from(meta.sealed_session_key, 'base64');
|
|
103
|
+
// 1. Create Ephemeral Session Key for Seal Protocol
|
|
104
|
+
// This is NOT the AES key. This is for the decryption request.
|
|
105
|
+
const address = signer.getPublicKey().toSuiAddress();
|
|
106
|
+
const sessionKey = await seal_2.SessionKey.create({
|
|
107
|
+
address: address,
|
|
108
|
+
packageId: meta.package_id,
|
|
109
|
+
ttlMin: 10,
|
|
110
|
+
signer,
|
|
111
|
+
suiClient: suiClient,
|
|
112
|
+
});
|
|
113
|
+
// 2. Construct Transaction for seal_approve
|
|
114
|
+
const tx = new transactions_1.Transaction();
|
|
115
|
+
// Helper to convert hex to bytes
|
|
116
|
+
const policyIdBytes = fromHex(meta.policy_id);
|
|
117
|
+
tx.moveCall({
|
|
118
|
+
target: `${meta.package_id}::whitelist::seal_approve`,
|
|
119
|
+
arguments: [
|
|
120
|
+
tx.pure.vector('u8', policyIdBytes),
|
|
121
|
+
tx.object(meta.policy_id), // The Whitelist object itself
|
|
122
|
+
],
|
|
123
|
+
});
|
|
124
|
+
// 3. Build the transaction to get txBytes
|
|
125
|
+
// Important: Use onlyTransactionKind: true as per Seal examples
|
|
126
|
+
const txBytes = await tx.build({ client: suiClient, onlyTransactionKind: true });
|
|
127
|
+
// 4. Decrypt the sealed session key
|
|
128
|
+
const sessionKeyBytes = await client.decrypt({
|
|
129
|
+
data: sealedKey,
|
|
130
|
+
sessionKey,
|
|
131
|
+
txBytes: Buffer.from(txBytes),
|
|
132
|
+
});
|
|
133
|
+
const decryptedSessionKey = Buffer.from(sessionKeyBytes);
|
|
134
|
+
// 5. Decrypt Data with the session key
|
|
135
|
+
const iv = Buffer.from(meta.iv, 'base64');
|
|
136
|
+
const tag = Buffer.from(meta.tag, 'base64');
|
|
137
|
+
const decipher = crypto_1.default.createDecipheriv('aes-256-gcm', decryptedSessionKey, iv);
|
|
138
|
+
decipher.setAuthTag(tag);
|
|
139
|
+
return Buffer.concat([decipher.update(cipher), decipher.final()]);
|
|
140
|
+
}
|
|
141
|
+
catch (err) {
|
|
142
|
+
// Handle specific error types with user-friendly messages
|
|
143
|
+
if (err.name === 'NoAccessError' || err.message?.includes('does not have access')) {
|
|
144
|
+
throw new Error('NoAccess: User is not whitelisted for this repository');
|
|
145
|
+
}
|
|
146
|
+
if (err.name === 'TimeoutError' || err.message?.includes('timeout')) {
|
|
147
|
+
throw new Error('Timeout: Unable to connect to Seal servers');
|
|
148
|
+
}
|
|
149
|
+
// For other errors, throw with original message
|
|
150
|
+
throw new Error(`Seal decryption failed: ${err.message || err}`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
function fromHex(hex) {
|
|
154
|
+
if (hex.startsWith('0x'))
|
|
155
|
+
hex = hex.slice(2);
|
|
156
|
+
return Array.from(Buffer.from(hex, 'hex'));
|
|
157
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.canonicalize = canonicalize;
|
|
7
|
+
exports.canonicalStringify = canonicalStringify;
|
|
8
|
+
exports.sha256Base64 = sha256Base64;
|
|
9
|
+
const crypto_1 = __importDefault(require("crypto"));
|
|
10
|
+
function canonicalize(value) {
|
|
11
|
+
if (Array.isArray(value)) {
|
|
12
|
+
return value.map(canonicalize);
|
|
13
|
+
}
|
|
14
|
+
if (value && typeof value === 'object') {
|
|
15
|
+
const sortedKeys = Object.keys(value).sort();
|
|
16
|
+
const result = {};
|
|
17
|
+
for (const key of sortedKeys) {
|
|
18
|
+
result[key] = canonicalize(value[key]);
|
|
19
|
+
}
|
|
20
|
+
return result;
|
|
21
|
+
}
|
|
22
|
+
return value;
|
|
23
|
+
}
|
|
24
|
+
function canonicalStringify(value) {
|
|
25
|
+
return JSON.stringify(canonicalize(value)) + '\n';
|
|
26
|
+
}
|
|
27
|
+
function sha256Base64(data) {
|
|
28
|
+
const buf = Buffer.isBuffer(data) ? data : Buffer.from(data);
|
|
29
|
+
return 'sha256-' + crypto_1.default.createHash('sha256').update(buf).digest('base64');
|
|
30
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.readHeadRefPath = readHeadRefPath;
|
|
7
|
+
exports.readRef = readRef;
|
|
8
|
+
exports.readCommitById = readCommitById;
|
|
9
|
+
exports.idToFileName = idToFileName;
|
|
10
|
+
exports.readCommitIdMap = readCommitIdMap;
|
|
11
|
+
exports.writeCommitIdMap = writeCommitIdMap;
|
|
12
|
+
const promises_1 = __importDefault(require("fs/promises"));
|
|
13
|
+
const path_1 = __importDefault(require("path"));
|
|
14
|
+
async function readHeadRefPath(witPath) {
|
|
15
|
+
const headFile = path_1.default.join(witPath, 'HEAD');
|
|
16
|
+
const raw = await promises_1.default.readFile(headFile, 'utf8');
|
|
17
|
+
const ref = raw.trim() || 'refs/heads/main';
|
|
18
|
+
return path_1.default.join(witPath, ref);
|
|
19
|
+
}
|
|
20
|
+
async function readRef(refPath) {
|
|
21
|
+
try {
|
|
22
|
+
const raw = await promises_1.default.readFile(refPath, 'utf8');
|
|
23
|
+
const val = raw.trim();
|
|
24
|
+
return val.length ? val : null;
|
|
25
|
+
}
|
|
26
|
+
catch (err) {
|
|
27
|
+
if (err?.code === 'ENOENT')
|
|
28
|
+
return null;
|
|
29
|
+
throw err;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
async function readCommitById(witPath, commitId) {
|
|
33
|
+
const file = path_1.default.join(witPath, 'objects', 'commits', `${idToFileName(commitId)}.json`);
|
|
34
|
+
const raw = await promises_1.default.readFile(file, 'utf8');
|
|
35
|
+
return JSON.parse(raw);
|
|
36
|
+
}
|
|
37
|
+
function idToFileName(id) {
|
|
38
|
+
return id.replace(/\//g, '_').replace(/\+/g, '-');
|
|
39
|
+
}
|
|
40
|
+
const MAP_REL = path_1.default.join('objects', 'maps', 'commit_id_map.json');
|
|
41
|
+
async function readCommitIdMap(witPath) {
|
|
42
|
+
const file = path_1.default.join(witPath, MAP_REL);
|
|
43
|
+
try {
|
|
44
|
+
const raw = await promises_1.default.readFile(file, 'utf8');
|
|
45
|
+
return JSON.parse(raw);
|
|
46
|
+
}
|
|
47
|
+
catch (err) {
|
|
48
|
+
if (err?.code === 'ENOENT')
|
|
49
|
+
return {};
|
|
50
|
+
throw err;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
async function writeCommitIdMap(witPath, map) {
|
|
54
|
+
const file = path_1.default.join(witPath, MAP_REL);
|
|
55
|
+
await promises_1.default.mkdir(path_1.default.dirname(file), { recursive: true });
|
|
56
|
+
await promises_1.default.writeFile(file, JSON.stringify(map, null, 2) + '\n', 'utf8');
|
|
57
|
+
}
|