@sage-protocol/cli 0.3.7 → 0.3.10

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.
@@ -1,887 +1,35 @@
1
1
  const { Command } = require('commander');
2
- const { ethers } = require('ethers');
3
2
 
4
- function requireLit() {
5
- // Try resolving from normal Node resolution, then fall back to the cli/ directory
6
- try {
7
- const { LitNodeClient } = require('@lit-protocol/lit-node-client');
8
- const { decryptToUint8Array, encryptUint8Array } = require('@lit-protocol/encryption');
9
- return { LitNodeClient, decryptToUint8Array, encryptUint8Array };
10
- } catch (_) {
11
- try {
12
- const path = require('path');
13
- const { createRequire } = require('module');
14
- // Base at cli/ (two levels up from this file: cli/commands → cli)
15
- const cliBase = path.resolve(__dirname, '..', '..');
16
- const reqCli = createRequire(path.join(cliBase, 'package.json'));
17
- const { LitNodeClient } = reqCli('@lit-protocol/lit-node-client');
18
- const { decryptToUint8Array, encryptUint8Array } = reqCli('@lit-protocol/encryption');
19
- return { LitNodeClient, decryptToUint8Array, encryptUint8Array };
20
- } catch (e2) {
21
- const hint = 'Install Lit SDK at root with Yarn workspaces (yarn add -W @lit-protocol/lit-node-client) or inside ./cli (cd cli && npm i @lit-protocol/lit-node-client).';
22
- throw new Error(`Lit SDK not found in root or cli package. ${hint}`);
23
- }
24
- }
25
- }
26
-
27
- function requireLitAuthHelpers() {
28
- // Try resolving from normal Node resolution, then fall back to the cli/ directory
29
- try {
30
- const { LitAccessControlConditionResource } = require('@lit-protocol/auth-helpers');
31
- return { LitAccessControlConditionResource };
32
- } catch (_) {
33
- try {
34
- const path = require('path');
35
- const { createRequire } = require('module');
36
- const cliBase = path.resolve(__dirname, '..', '..');
37
- const reqCli = createRequire(path.join(cliBase, 'package.json'));
38
- const { LitAccessControlConditionResource } = reqCli('@lit-protocol/auth-helpers');
39
- return { LitAccessControlConditionResource };
40
- } catch (e2) {
41
- throw new Error('Lit auth-helpers not found. Install @lit-protocol/auth-helpers in root or cli package.');
42
- }
43
- }
44
- }
45
-
46
- function loadAuthSig(pathOrEnv) {
47
- const fs = require('fs'); const path = require('path');
48
- const candidate = pathOrEnv || process.env.LIT_AUTH_SIG;
49
- if (!candidate) throw new Error('AuthSig not provided. Pass --auth-sig <file> or set LIT_AUTH_SIG=<file>');
50
- const p = path.resolve(candidate);
51
- if (!fs.existsSync(p)) throw new Error(`AuthSig file not found: ${p}`);
52
- try { return JSON.parse(fs.readFileSync(p, 'utf8')); } catch (e) { throw new Error('Invalid AuthSig JSON'); }
53
- }
54
-
55
- function toB64(buf) { return Buffer.from(buf).toString('base64'); }
56
- function fromB64(b64) { return Buffer.from(b64, 'base64'); }
3
+ /**
4
+ * Premium PRE (Proxy Re-Encryption) CLI
5
+ *
6
+ * This command was part of the SubDAO-based premium prompts model.
7
+ * Use `sage personal` commands instead for personal premium.
8
+ *
9
+ * See docs/specs/premium-endorsement-model.md for the new architecture.
10
+ */
57
11
 
58
12
  function register(program) {
59
- const cmd = new Command('premium-pre').description('Premium encryption via PRE (Lit) with on-chain ERC-1155 gating (no on-chain key storage)');
60
-
61
- // Helpers
62
- const inferLitChain = async (provider, explicit) => {
63
- if (explicit && String(explicit).trim()) return explicit;
64
- try {
65
- const net = await provider.getNetwork();
66
- const id = Number(net.chainId);
67
- // Minimal mapping for common test/prod nets used here
68
- if (id === 84532) return 'baseSepolia';
69
- if (id === 8453) return 'base';
70
- if (id === 1) return 'ethereum';
71
- if (id === 137) return 'polygon';
72
- if (id === 80001) return 'mumbai';
73
- if (id === 11155111) return 'sepolia';
74
- } catch(_) {}
75
- return process.env.LIT_CHAIN || 'baseSepolia';
76
- };
77
-
78
- cmd
79
- .command('doctor')
80
- .description('Diagnose PRE/Lit v7 setup and manifests (chain key, tokenId decimal, conditions)')
81
- .option('--cid <manifestCID>', 'Manifest CID to inspect (optional)')
82
- .option('--chain <litChain>', 'Expected Lit chain key (default: infer from RPC or env)')
83
- .option('--json', 'Output JSON', false)
84
- .action(async (opts)=>{
85
- try {
86
- const out = { ok: true, notes: [], warnings: [], manifest: null };
87
- const WM = require('../wallet-manager'); const wm = new WM(); await wm.connect();
88
- const provider = wm.getProvider();
89
- const litChain = await inferLitChain(provider, opts.chain);
90
- out.notes.push(`Lit chain: ${litChain}`);
91
- const net = await provider.getNetwork(); out.notes.push(`RPC chainId: ${Number(net.chainId)}`);
92
- if (opts.cid) {
93
- const IPFSManager = require('../ipfs-manager');
94
- const ipfs = new IPFSManager(); await ipfs.initialize();
95
- const manifest = await ipfs.downloadJson(opts.cid);
96
- out.manifest = { cid: opts.cid };
97
- if (!manifest?.pre || manifest.pre.provider !== 'lit') {
98
- out.ok = false; out.warnings.push('Manifest missing PRE payload (pre.provider!=lit)');
99
- } else {
100
- const { normalizeCidV1Base32 } = require('../utils/cid');
101
- const { ethers } = require('ethers');
102
- const encCid = manifest.encryptedCID;
103
- const acc = manifest.pre.evmContractConditions;
104
- let chain = manifest.pre.chain || '';
105
- if (chain === 'base-sepolia') { out.warnings.push('Legacy chain key base-sepolia found; should be baseSepolia'); chain = 'baseSepolia'; }
106
- if (chain !== litChain) out.warnings.push(`Chain mismatch: manifest=${manifest.pre.chain} vs expected=${litChain}`);
107
- // tokenId decimal check
108
- const tok = String(manifest.pre.tokenId || '').trim();
109
- if (!tok || /^0x[0-9a-fA-F]+$/.test(tok)) {
110
- out.ok = false; out.warnings.push('pre.tokenId must be decimal for Lit v7. Republish to store decimal tokenId.');
111
- }
112
- // expected id derived from encryptedCID (CIDv1 base32 then keccak256 of utf8)
113
- if (encCid) {
114
- const tokenIdHex = ethers.keccak256(ethers.toUtf8Bytes(normalizeCidV1Base32(encCid)));
115
- const tokenIdDecExpected = BigInt(tokenIdHex).toString();
116
- if (tok && tok !== tokenIdDecExpected) {
117
- out.warnings.push(`tokenId mismatch: manifest=${tok}, expected=${tokenIdDecExpected} (derived from encryptedCID)`);
118
- }
119
- } else {
120
- out.warnings.push('Manifest missing encryptedCID');
121
- }
122
- // ACC param normalization
123
- if (Array.isArray(acc) && acc[0]?.functionName === 'balanceOf') {
124
- const p = acc[0].functionParams || [];
125
- const tokParam = String(p[1] || '');
126
- if (/^0x[0-9a-fA-F]+$/.test(tokParam)) out.warnings.push('Access condition tokenId should be decimal, not hex');
127
- if (acc[0].chain === 'base-sepolia') out.warnings.push('Access condition chain should be baseSepolia');
128
- } else {
129
- out.warnings.push('evmContractConditions missing or not in expected balanceOf format');
130
- }
131
- }
132
- }
133
- if (opts.json) { process.stdout.write(JSON.stringify(out, null, 2) + '\n'); return; }
134
- console.log('🔍 PRE Doctor');
135
- out.notes.forEach(n => console.log(' -', n));
136
- if (out.manifest) console.log(' - Manifest CID:', out.manifest.cid);
137
- if (out.warnings.length) {
138
- console.log('\nWarnings:'); out.warnings.forEach(w => console.log(' -', w));
139
- }
140
- console.log('\nTips: ensure decimal pre.tokenId, chain=baseSepolia, and session signatures enabled.');
141
- } catch (e) { console.error('❌ premium-pre doctor failed:', e.message); process.exit(1); }
142
- });
143
-
144
- cmd
145
- .command('publish')
146
- .description('Encrypt content; save AES key with Lit using ERC-1155 receipt gating; store PRE payload in manifest')
147
- .requiredOption('--in <path>', 'Path to plaintext file')
148
- .requiredOption('--name <name>', 'Name')
149
- .requiredOption('--description <text>', 'Description')
150
- .requiredOption('--subdao <address>', 'SubDAO to receive proceeds (for price registration via existing premium create flow)')
151
- .requiredOption('--price <usdc>', 'Price in USDC (6dp)')
152
- .option('--receipt <address>', 'PremiumReceipt address (defaults to PREMIUM_RECEIPT_ADDRESS)', process.env.PREMIUM_RECEIPT_ADDRESS)
153
- .option('--chain <name>', 'Lit EVM chain identifier (e.g., base-sepolia). Defaults inferred from connected RPC')
154
- .requiredOption('--auth-sig <path>', 'Path to Lit AuthSig JSON (SIWE signature)')
155
- .option('--no-auto-self-delegate', 'Do not attempt auto self-delegation on the voting token when preparing a proposal')
156
- .option('--lit-network <name>', 'Lit network: serrano (default) | datil-test | datil-dev', process.env.LIT_NETWORK || 'serrano')
157
- .action(async (opts) => {
158
- try {
159
- const fs = require('fs'); const path = require('path');
160
- const IPFSManager = require('../ipfs-manager');
161
- const { encryptAesGcm, randomBytes, toB64: b64 } = require('../utils/aes');
162
- const { normalizeCidV1Base32 } = require('../utils/cid');
163
- const ipfs = new IPFSManager(); await ipfs.initialize();
164
-
165
- if (!opts.receipt) throw new Error('PREMIUM_RECEIPT_ADDRESS not set; pass --receipt');
166
- const receipt = ethers.getAddress(opts.receipt);
167
-
168
- const content = fs.readFileSync(path.resolve(opts.in), 'utf8');
169
- const key = randomBytes(32);
170
- const { ciphertext, iv, tag } = encryptAesGcm(content, key);
171
- const encPayload = { type: 'sage-premium-encrypted', enc: 'aes-256-gcm', name: opts.name, description: opts.description, iv: b64(iv), tag: b64(tag), ciphertext: b64(ciphertext) };
172
- const cipherCID = await ipfs.uploadJson(encPayload, `${opts.name}.encrypted`);
173
- // Derive stable tokenId from encryptedCID to avoid self-referential manifest CID loops
174
- const tokenIdHex = ethers.keccak256(ethers.toUtf8Bytes(normalizeCidV1Base32(cipherCID)));
175
- // Convert to decimal for Lit Protocol compatibility
176
- const tokenId = BigInt(tokenIdHex).toString();
177
- const manifest = { type: 'sage-premium-manifest-pre', version: '1.0.0', encryptedCID: cipherCID, cipher: 'aes-256-gcm', name: opts.name, description: opts.description };
178
-
179
- // PRE: save AES key with Lit under ERC-1155 gating
180
- const { LitNodeClient, decryptToUint8Array } = requireLit();
181
- let litNetworkPub = opts.litNetwork || process.env.LIT_NETWORK || 'serrano';
182
- if (litNetworkPub === 'jalapeno') { console.log('⚠️ Lit network "jalapeno" is deprecated; using "serrano".'); litNetworkPub = 'serrano'; }
183
- const client = new LitNodeClient({ litNetwork: litNetworkPub });
184
- await client.connect();
185
- // Back-compat: AuthSig file may still be provided. We'll prefer sessionSigs below.
186
- const hasAuthSigFile = !!opts.authSig;
187
- const authSig = hasAuthSigFile ? loadAuthSig(opts.authSig) : null;
188
-
189
- // Infer Lit chain from connected provider if not provided
190
- const wm = new (require('../wallet-manager'))(); await wm.connect();
191
- const provider = wm.getProvider();
192
- const litChain = await inferLitChain(provider, opts.chain);
193
-
194
- // Upload a preliminary manifest (not used for id derivation anymore)
195
- const manifestCID = await ipfs.uploadJson(manifest, `${opts.name}.manifest`);
196
-
197
- // evmContractConditions for ERC-1155 balanceOf(address,uint256) > 0
198
- const evmContractConditions = [
199
- {
200
- contractAddress: receipt,
201
- functionName: 'balanceOf',
202
- functionParams: [':userAddress', tokenId],
203
- functionAbi: { name: 'balanceOf', type: 'function', stateMutability: 'view', inputs: [{ name: 'account', type: 'address' }, { name: 'id', type: 'uint256' }], outputs: [{ name: '', type: 'uint256' }] },
204
- chain: litChain,
205
- returnValueTest: { key: '', comparator: '>', value: '0' }
206
- }
207
- ];
208
-
209
- // Build a session for Lit v7 using session signatures (preferred over AuthSig)
210
- // Resource must reflect both ACC hash and the private data hash; derive via client helper
211
- const symmetricKey = new Uint8Array(key);
212
- const resource = await client.getLitResourceForEncryption({ evmContractConditions, chain: litChain, dataToEncrypt: symmetricKey });
13
+ const cmd = new Command('premium-pre')
14
+ .description('Premium PRE encryption - use "sage personal" instead')
15
+ .action(() => {
16
+ console.log(`
17
+ Premium PRE (Proxy Re-Encryption)
213
18
 
214
- // Create session signatures by signing a SIWE message via our CLI signer
215
- const sessionSigs = await client.getSessionSigs({
216
- chain: 'ethereum',
217
- resourceAbilityRequests: [
218
- {
219
- resource,
220
- ability: 'access-control-condition-decryption'
221
- }
222
- ],
223
- authNeededCallback: async ({ uri, expiration, resources, nonce, domain }) => {
224
- const signer = wm.getSigner();
225
- const address = await signer.getAddress();
226
- const siweDomain = domain || process.env.LIT_SIWE_DOMAIN || 'localhost';
227
- const siweUri = uri || 'lit:session';
228
- const issuedAt = new Date().toISOString();
229
- const chainId = 1; // ethereum for SIWE
230
- const statement = 'Authorize access to Sage premium content via Lit';
231
- const lines = [
232
- `${siweDomain} wants you to sign in with your Ethereum account:`,
233
- `${address}`,
234
- '',
235
- `${statement}`,
236
- '',
237
- `URI: ${siweUri}`,
238
- `Version: 1`,
239
- `Chain ID: ${chainId}`,
240
- `Nonce: ${nonce}`,
241
- `Issued At: ${issuedAt}`,
242
- `Expiration Time: ${expiration}`
243
- ];
244
- if (Array.isArray(resources) && resources.length) {
245
- lines.push('Resources:');
246
- for (const r of resources) lines.push(`- ${r}`);
247
- }
248
- const message = lines.join('\n');
249
- const sig = await signer.signMessage(message);
250
- return { sig, derivedVia: 'web3.eth.personal.sign', signedMessage: message, address };
251
- }
252
- });
253
-
254
- const { encryptUint8Array } = requireLit();
255
-
256
- // v7 API: encrypt the symmetric key with access conditions
257
- // Note: param name must be `dataToEncrypt` (not `data`)
258
- const encResp = await encryptUint8Array({
259
- evmContractConditions,
260
- chain: litChain,
261
- // Prefer sessionSigs; fall back to authSig if explicitly provided
262
- ...(sessionSigs ? { sessionSigs } : {}),
263
- ...(sessionSigs ? {} : (authSig ? { authSig } : {})),
264
- dataToEncrypt: symmetricKey
265
- }, client);
266
- // encResp: { ciphertext, dataToEncryptHash }
267
- const eskB64 = encResp.ciphertext;
268
- const dataToEncryptHash = encResp.dataToEncryptHash;
269
-
270
- // Write final manifest with PRE payload reference
271
- manifest.pre = {
272
- provider: 'lit',
273
- version: '1.0.0',
274
- chain: litChain,
275
- receipt,
276
- tokenId,
277
- evmContractConditions,
278
- encryptedSymmetricKey: eskB64,
279
- dataToEncryptHash
280
- };
281
- const manifestFinalCID = await ipfs.uploadJson(manifest, `${opts.name}.manifest`);
282
-
283
- // Register price / prompt via existing premium contract
284
- const promptsAddr = process.env.PREMIUM_PROMPTS_ADDRESS; if (!promptsAddr) throw new Error('PREMIUM_PROMPTS_ADDRESS not set');
285
- const signer = wm.getSigner(); const providerRO = wm.getProvider();
286
- const price = ethers.parseUnits(String(opts.price), 6);
287
- const ifaceV2 = new ethers.Interface(['function createPremiumPromptWithManifest(bytes32,address,uint256,string)']);
288
- // Register using cidHash = tokenIdHex (derived from encryptedCID) to match ERC-1155 gating
289
- const cidHash = tokenIdHex;
290
- const data = ifaceV2.encodeFunctionData('createPremiumPromptWithManifest', [cidHash, ethers.getAddress(opts.subdao), price, manifestFinalCID]);
291
- let sent = false;
292
- try {
293
- // Preflight: check MANAGER role (robust to contracts that revert on view)
294
- const prRO = new ethers.Contract(promptsAddr, ['function hasRole(bytes32,address) view returns (bool)'], providerRO);
295
- const MR = ethers.id('MANAGER_ROLE'); // keccak256("MANAGER_ROLE")
296
- const me = await signer.getAddress();
297
- let hasMgr = false;
298
- try { hasMgr = await prRO.hasRole(MR, me); } catch(_) { hasMgr = false; }
299
- if (!hasMgr) throw new Error('NO_MANAGER');
300
- const tx = await signer.sendTransaction({ to: promptsAddr, data });
301
- console.log('🧾 createPremiumPromptWithManifest tx:', tx.hash);
302
- sent = true;
303
- } catch (e) {
304
- const msg = String(e?.message||'');
305
- if (msg.includes('closed') || msg.includes('NO_MANAGER') || msg.includes('execution reverted')) {
306
- console.log('ℹ️ Creator path closed or missing MANAGER_ROLE. Falling back to governance proposal.');
307
- // Propose via GovernanceManager to ensure tuple is cached for reliable queue/execute
308
- const GM = require('../governance-manager');
309
- const gm = new GM();
310
- await gm.initialize(opts.subdao || null);
311
-
312
- // Preflight: ensure proposer meets threshold; auto self-delegate on voting token if needed
313
- try {
314
- const { ethers } = require('ethers');
315
- const provider = gm.provider; const signer = gm.signer;
316
- const me = await signer.getAddress();
317
- // Resolve governor address for minimal ABI calls
318
- const govAddr = (gm.governor.getAddress && typeof gm.governor.getAddress === 'function')
319
- ? await gm.governor.getAddress() : gm.governor.target;
320
- const govView = new ethers.Contract(govAddr, [
321
- 'function proposalThreshold() view returns (uint256)',
322
- 'function token() view returns (address)'
323
- ], provider);
324
- let votingToken = ethers.ZeroAddress;
325
- try { votingToken = await govView.token(); } catch(_) { votingToken = ethers.ZeroAddress; }
326
- if (votingToken === ethers.ZeroAddress) {
327
- // Fallback to SubDAO stake token (common for GOVERNANCE/HYBRID modes)
328
- try {
329
- const sub = new ethers.Contract(ethers.getAddress(opts.subdao), ['function stakeToken() view returns (address)'], provider);
330
- votingToken = await sub.stakeToken();
331
- } catch(_) {}
332
- }
333
- // Read threshold and current votes
334
- let threshold = 0n; let votes = 0n;
335
- try { threshold = await govView.proposalThreshold(); } catch(_) { threshold = 0n; }
336
- if (votingToken && votingToken !== ethers.ZeroAddress) {
337
- const vAbi = [
338
- 'function getVotes(address) view returns (uint256)',
339
- 'function delegates(address) view returns (address)',
340
- 'function delegate(address)'
341
- ];
342
- const vt = new ethers.Contract(votingToken, vAbi, signer);
343
- try { votes = await vt.getVotes(me); } catch(_) { votes = 0n; }
344
- if (threshold > 0n && votes < threshold) {
345
- // Auto self-delegate unless disabled via flag
346
- if (opts.autoSelfDelegate !== false) {
347
- try {
348
- let curDel = ethers.ZeroAddress;
349
- try { curDel = await vt.delegates(me); } catch(_) { curDel = ethers.ZeroAddress; }
350
- if (String(curDel).toLowerCase() !== me.toLowerCase()) {
351
- console.log('🪪 Auto self-delegating voting power on token', votingToken);
352
- const dtx = await vt.delegate(me);
353
- console.log('⏳ delegate tx:', dtx.hash);
354
- await dtx.wait();
355
- // Re-check votes post-delegation
356
- try { votes = await vt.getVotes(me); } catch(_) {}
357
- console.log('🗳️ Votes after delegation:', require('ethers').formatEther(votes));
358
- }
359
- } catch (delErr) {
360
- console.log('⚠️ Auto self-delegation skipped/failed:', String(delErr?.message||delErr));
361
- }
362
- }
363
- // Final advisory if still under threshold
364
- if (votes < threshold) {
365
- const fmt = (x)=>require('ethers').formatEther(x);
366
- console.log('❗ Proposer preflight: insufficient voting power.');
367
- console.log(' votes=', fmt(votes), ' threshold=', fmt(threshold));
368
- console.log(' Hint: stake into the SubDAO and self-delegate, or use timelock schedule-call.');
369
- }
370
- }
371
- }
372
- } catch (pfErr) {
373
- // Non-fatal; continue to attempt proposal (Governor will enforce)
374
- console.log('⚠️ Preflight check failed (continuing):', String(pfErr?.message||pfErr));
375
- }
376
-
377
- const targets = [promptsAddr]; const values = [0n]; const calldatas = [data];
378
- const desc = `Register premium prompt via PRE (Lit): ${opts.name}`;
379
-
380
- // If after preflight+auto-delegate we still have insufficient votes, offer Timelock route now
381
- try {
382
- const { ethers } = require('ethers');
383
- const provider = gm.provider; const signer = gm.signer; const me = await signer.getAddress();
384
- const govAddr = (gm.governor.getAddress && typeof gm.governor.getAddress === 'function') ? await gm.governor.getAddress() : gm.governor.target;
385
- const govView = new ethers.Contract(govAddr, ['function proposalThreshold() view returns (uint256)','function token() view returns (address)'], provider);
386
- let threshold = 0n; try { threshold = await govView.proposalThreshold(); } catch(_) {}
387
- if (threshold > 0n) {
388
- let votingToken = ethers.ZeroAddress;
389
- try { votingToken = await govView.token(); } catch(_) {}
390
- if (votingToken === ethers.ZeroAddress) {
391
- try { const sub = new ethers.Contract(ethers.getAddress(opts.subdao), ['function stakeToken() view returns (address)'], provider); votingToken = await sub.stakeToken(); } catch(_) {}
392
- }
393
- let votes = 0n; if (votingToken && votingToken !== ethers.ZeroAddress) { try { votes = await new ethers.Contract(votingToken, ['function getVotes(address) view returns (uint256)'], provider).getVotes(me); } catch(_) {} }
394
- if (votes < threshold) {
395
- const inquirer = (require('inquirer').default || require('inquirer'));
396
- const { route } = await inquirer.prompt([
397
- { type:'list', name:'route', message:'Insufficient voting power. Choose how to proceed:', choices:[
398
- { name:'Schedule via Timelock now (recommended)', value:'timelock' },
399
- { name:'Attempt creating a proposal anyway', value:'proposal' },
400
- { name:'Abort (I will stake/delegate first)', value:'abort' }
401
- ]}
402
- ]);
403
- if (route === 'timelock') {
404
- try {
405
- const tl = gm.timelock; if (!tl) throw new Error('Timelock not resolved');
406
- const tlAddr = (tl.getAddress && typeof tl.getAddress === 'function') ? await tl.getAddress() : tl.target;
407
- const PROPOSER = await tl.PROPOSER_ROLE();
408
- const has = await tl.hasRole(PROPOSER, me).catch(()=>false);
409
- if (!has) throw new Error('Current signer lacks PROPOSER_ROLE on Timelock');
410
- const delay = await tl.getMinDelay().catch(()=>0n);
411
- const salt = ethers.hexlify(ethers.randomBytes(32));
412
- const predecessor = ethers.ZeroHash;
413
- console.log('⏳ Scheduling via Timelock...', { timelock: tlAddr, target: promptsAddr, delay: String(delay) });
414
- const stx = await tl.schedule(promptsAddr, 0, data, predecessor, salt, delay);
415
- console.log('📨 schedule tx:', stx.hash); { const { waitForReceipt } = require('../utils/tx-wait'); const { ethers } = require('ethers'); const pr = new ethers.JsonRpcProvider(process.env.RPC_URL || process.env.BASE_SEPOLIA_RPC || process.env.BASE_RPC_URL || 'https://base-sepolia.publicnode.com'); await waitForReceipt(pr, stx, Number(process.env.SAGE_TX_WAIT_MS || 60000)); } console.log('✅ Scheduled');
416
- if (delay === 0n) {
417
- try { const etx = await tl.execute(promptsAddr, 0, data, predecessor, salt); console.log('⚡ execute tx:', etx.hash); { const { waitForReceipt } = require('../utils/tx-wait'); const { ethers } = require('ethers'); const pr = new ethers.JsonRpcProvider(process.env.RPC_URL || process.env.BASE_SEPOLIA_RPC || process.env.BASE_RPC_URL || 'https://base-sepolia.publicnode.com'); await waitForReceipt(pr, etx, Number(process.env.SAGE_TX_WAIT_MS || 60000)); } console.log('✅ Executed'); }
418
- catch(exErr) { console.log('⚠️ Execute failed or not ready:', String(exErr?.message||exErr)); }
419
- } else {
420
- console.log('ℹ️ Execute after delay:');
421
- console.log(` sage timelock execute --subdao ${ethers.getAddress(opts.subdao)} --target ${promptsAddr} --value 0 --data ${data} --predecessor 0x${'0'.repeat(64)} --salt ${salt}`);
422
- }
423
- // Output identifiers and exit early (timelock route)
424
- console.log('🔗 Identifiers:');
425
- console.log(' encryptedCID:', cipherCID);
426
- console.log(' tokenId :', tokenId);
427
- console.log(' cidHash :', cidHash);
428
- console.log(' manifestCID :', manifestFinalCID);
429
- console.log(JSON.stringify({ manifestCID: manifestFinalCID, encryptedCID: cipherCID, cidHash, route: 'timelock', timelock: tlAddr }, null, 2));
430
- return;
431
- } catch (timErr) {
432
- console.log('❌ Timelock scheduling failed:', String(timErr?.message||timErr));
433
- console.log(' You can retry manually with:');
434
- console.log(` sage timelock schedule-call --subdao ${ethers.getAddress(opts.subdao)} --to ${promptsAddr} --sig 'createPremiumPromptWithManifest(bytes32,address,uint256,string)' --args '${cidHash},${ethers.getAddress(opts.subdao)},${price.toString()},${manifestFinalCID}'`);
435
- }
436
- } else if (route === 'abort') {
437
- throw new Error('Aborted by user due to insufficient voting power.');
438
- }
439
- }
440
- }
441
- } catch(_) {}
442
- try {
443
- const pid = await gm.createProposal(targets, values, calldatas, desc);
444
- console.log('🗳️ proposal id:', pid);
445
- try { const { execSync } = require('child_process'); console.log(`➡️ Watching proposal ${pid} for queue/execute...`); console.log(execSync(['node','packages/cli/src/index.js','governance','watch',String(pid),'--subdao',String(opts.subdao)].join(' '), { encoding:'utf8' })); } catch(_) {}
446
- } catch (propErr) {
447
- const em = String(propErr?.message||'');
448
- if (em.includes('Operator mode detected') || em.toLowerCase().includes('not a timelock proposer')) {
449
- console.log('ℹ️ Operator mode detected; scheduling on Timelock.');
450
- const { resolveGovContext } = require('../utils/gov-context');
451
- const ctx2 = await resolveGovContext({ govOpt: null, subdaoOpt: opts.subdao, provider: providerRO });
452
- const tlAddr = ctx2.timelock || process.env.TIMELOCK || process.env.TIMELOCK_ADDRESS;
453
- if (!tlAddr) throw new Error('Timelock not resolved for SubDAO');
454
- const tlIface = new ethers.Interface(['function schedule(address,uint256,bytes,bytes32,bytes32,uint256)','function execute(address,uint256,bytes,bytes32,bytes32)','function getMinDelay() view returns (uint256)']);
455
- const tl = new ethers.Contract(tlAddr, tlIface, signer);
456
- const predecessor = ethers.ZeroHash; const salt = ethers.hexlify(ethers.randomBytes(32));
457
- const delay = await tl.getMinDelay().catch(()=>0n);
458
- const stx = await tl.schedule(promptsAddr, 0, data, predecessor, salt, delay);
459
- console.log('⏳ Timelock schedule tx:', stx.hash); { const { waitForReceipt } = require('../utils/tx-wait'); const { ethers } = require('ethers'); const pr = new ethers.JsonRpcProvider(process.env.RPC_URL || process.env.BASE_SEPOLIA_RPC || process.env.BASE_RPC_URL || 'https://base-sepolia.publicnode.com'); await waitForReceipt(pr, stx, Number(process.env.SAGE_TX_WAIT_MS || 60000)); } console.log('✅ Scheduled');
460
- if (delay === 0n) { const etx = await tl.execute(promptsAddr, 0, data, predecessor, salt); console.log('⚡ execute tx:', etx.hash); { const { waitForReceipt } = require('../utils/tx-wait'); const { ethers } = require('ethers'); const pr = new ethers.JsonRpcProvider(process.env.RPC_URL || process.env.BASE_SEPOLIA_RPC || process.env.BASE_RPC_URL || 'https://base-sepolia.publicnode.com'); await waitForReceipt(pr, etx, Number(process.env.SAGE_TX_WAIT_MS || 60000)); } console.log('✅ Executed'); }
461
- } else { throw propErr; }
462
- }
463
- // Fall through to final JSON output
464
- } else {
465
- throw e;
466
- }
467
- }
468
- console.log('🔗 Identifiers:');
469
- console.log(' encryptedCID:', cipherCID);
470
- console.log(' tokenId :', tokenId);
471
- console.log(' cidHash :', cidHash);
472
- console.log(' manifestCID :', manifestFinalCID);
473
- console.log(JSON.stringify({ manifestCID: manifestFinalCID, encryptedCID: cipherCID, cidHash, route: sent?'direct':'governance', pre: { provider: 'lit', chain: litChain, receipt, tokenId, encryptedSymmetricKey: eskB64 } }, null, 2));
474
- } catch (e) { console.error('❌ premium-pre publish failed:', e.message); process.exit(1); }
475
- });
476
-
477
- cmd
478
- .command('manifest')
479
- .description('Resolve the manifest CID for a PRE (Lit) premium from subgraph/logs/cache or raw calldata')
480
- .option('--proposal <id>', 'Proposal ID that registered the premium (uses ./.sage/proposals.json)')
481
- .option('--gov <address>', 'Governor address (overrides --subdao/context)')
482
- .option('--subdao <address>', 'SubDAO address to derive Governor from')
483
- .option('--calldata <0xdata>', 'Raw calldata to decode (skips lookups)')
484
- .option('--cid-hash <hex>', 'cidHash (bytes32 hex) to search via subgraph/logs')
485
- .option('--subgraph <url>', 'Subgraph URL (defaults to SUBGRAPH_URL env)')
486
- .option('--prompts <address>', 'PremiumPrompts contract address for logs fallback')
487
- .option('--from <block>', 'Logs scan start block (default: current-100000)')
488
- .option('--json', 'Output JSON', false)
489
- .action(async (opts) => {
490
- try {
491
- const fs = require('fs'); const path = require('path'); const axios = require('axios');
492
- let dataHex = null; let used = '';
493
- if (opts.calldata && String(opts.calldata).startsWith('0x')) {
494
- dataHex = String(opts.calldata); used = 'calldata';
495
- } else {
496
- // 1) Subgraph fast-path
497
- const subgraphUrl = String(opts.subgraph || process.env.SUBGRAPH_URL || '').trim();
498
- if (subgraphUrl && (opts['cid-hash'] || opts.subdao)) {
499
- try {
500
- if (opts['cid-hash']) {
501
- const q = { query: 'query($id: ID!){ premiumPrompt(id:$id){ id manifestCID subdao price } }', variables: { id: String(opts['cid-hash']).toLowerCase() } };
502
- const { data } = await axios.post(subgraphUrl, q, { headers: { 'content-type': 'application/json' } });
503
- const manifestCID = data?.data?.premiumPrompt?.manifestCID || null;
504
- if (manifestCID) { if (opts.json) console.log(JSON.stringify({ manifestCID, source: 'subgraph:id' }, null, 2)); else console.log('Manifest CID:', manifestCID); return; }
505
- } else if (opts.subdao) {
506
- const q = { query: 'query($sub: Bytes!){ premiumPrompts(where:{ subdao:$sub }, orderBy:blockTimestamp, orderDirection: desc, first: 20){ id manifestCID subdao price } }', variables: { sub: String(opts.subdao).toLowerCase() } };
507
- const { data } = await axios.post(subgraphUrl, q, { headers: { 'content-type': 'application/json' } });
508
- const list = data?.data?.premiumPrompts || [];
509
- if (list.length) { if (opts.json) console.log(JSON.stringify({ results: list, source: 'subgraph:subdao' }, null, 2)); else { console.log('Premium prompts (latest 20):'); for (const it of list) console.log(`- cidHash: ${it.id} manifestCID: ${it.manifestCID || 'n/a'} price: ${it.price}`); } return; }
510
- }
511
- } catch (_) { /* fallthrough */ }
512
- }
513
-
514
- // 2) Logs fallback if prompts address provided
515
- if (opts.prompts) {
516
- const { ethers } = require('ethers');
517
- const provider = new ethers.JsonRpcProvider(process.env.RPC_URL || 'https://base-sepolia.publicnode.com');
518
- const ifaceEv = new ethers.Interface(['event PremiumPromptCreatedV2(bytes32 indexed cidHash, address indexed subdao, uint256 price, string manifestCID)']);
519
- const topic0 = ifaceEv.getEvent('PremiumPromptCreatedV2').topicHash;
520
- const topics = [topic0];
521
- if (opts['cid-hash']) topics.push(ethers.zeroPadValue(String(opts['cid-hash']), 32));
522
- const current = await provider.getBlockNumber();
523
- const fromBlock = opts.from ? Number(opts.from) : Math.max(0, current - 100000);
524
- const logs = await provider.getLogs({ address: ethers.getAddress(opts.prompts), topics, fromBlock, toBlock: 'latest' });
525
- if (logs.length) {
526
- const last = logs[logs.length-1];
527
- const parsed = ifaceEv.parseLog(last);
528
- const manifestCID = String(parsed?.args?.manifestCID || '');
529
- if (manifestCID) { if (opts.json) console.log(JSON.stringify({ manifestCID, source: `logs:${opts.prompts}` }, null, 2)); else console.log('Manifest CID:', manifestCID); return; }
530
- }
531
- }
532
-
533
- // 3) Proposals cache fallback
534
- const { ethers } = require('ethers');
535
- const { resolveGovContext } = require('../utils/gov-context');
536
- const provider = new ethers.JsonRpcProvider(process.env.RPC_URL || 'https://base-sepolia.publicnode.com');
537
- const ctx = await resolveGovContext({ govOpt: opts.gov, subdaoOpt: opts.subdao, provider });
538
- const govAddr = (ctx.governor || process.env.GOV || '').toLowerCase();
539
- const cachePath = path.join(process.cwd(), '.sage', 'proposals.json');
540
- if (fs.existsSync(cachePath) && govAddr) {
541
- const cache = JSON.parse(fs.readFileSync(cachePath, 'utf8')||'{}');
542
- const entries = cache[govAddr] || {};
543
- let pid = opts.proposal ? String(opts.proposal) : null;
544
- if (!pid) { const keys = Object.keys(entries); if (keys.length) pid = keys[keys.length-1]; }
545
- const t = entries[pid || ''] || null;
546
- if (t && Array.isArray(t.calldatas) && t.calldatas.length) { dataHex = String(t.calldatas[0]); used = `cache:${ctx.governor}:${pid}`; }
547
- }
548
- if (!dataHex) throw new Error('No source available. Provide --calldata, or set SUBGRAPH_URL and use --cid-hash/--subdao, or pass --prompts for logs.');
549
- }
550
-
551
- // Decode calldata as createPremiumPromptWithManifest(bytes32,address,uint256,string)
552
- const { ethers: E } = require('ethers');
553
- const iface = new E.Interface(['function createPremiumPromptWithManifest(bytes32,address,uint256,string)']);
554
- let cid = null;
555
- try { const decoded = iface.decodeFunctionData('createPremiumPromptWithManifest', dataHex); cid = String(decoded?.[3] || ''); }
556
- catch (_) {
557
- try { const buf = Buffer.from(dataHex.replace(/^0x/, ''), 'hex'); const s = buf.toString('utf8'); const m = s.match(/Qm[1-9A-HJ-NP-Za-km-z]{44,}/); if (m) cid = m[0]; } catch (_) {}
558
- }
559
- if (!cid) throw new Error('Could not extract manifest CID from calldata');
560
- if (opts.json) console.log(JSON.stringify({ manifestCID: cid, source: used || 'decoded-calldata' }, null, 2));
561
- else console.log('Manifest CID:', cid);
562
- } catch (e) { console.error('❌ manifest resolve failed:', e.message); process.exit(1); }
563
- });
564
-
565
- cmd
566
- .command('view')
567
- .description('For buyers: request key from Lit via ERC-1155 gating and decrypt locally (no centralized reveal)')
568
- .requiredOption('--cid <manifestCID>', 'Manifest CID')
569
- .option('--auth-sig <path>', 'Path to Lit AuthSig JSON (buyer wallet SIWE signature)')
570
- .option('--auto-auth', 'Auto-generate AuthSig from a keystore that owns the receipt', false)
571
- .option('--chain <litChain>', 'Override Lit chain key (e.g., baseSepolia)')
572
- .action(async (opts) => {
573
- try {
574
- const IPFSManager = require('../ipfs-manager');
575
- const { fromB64, decryptAesGcm } = require('../utils/aes');
576
- const ipfs = new IPFSManager(); await ipfs.initialize();
577
- const manifest = await ipfs.downloadJson(opts.cid);
578
- if (!manifest?.encryptedCID || !manifest?.pre || manifest.pre.provider !== 'lit') throw new Error('Manifest missing PRE payload');
579
- const enc = await ipfs.downloadJson(manifest.encryptedCID);
580
- const eskB64 = manifest.pre.encryptedSymmetricKey;
581
- const dataToEncryptHash = manifest.pre.dataToEncryptHash;
582
- let evmContractConditions = manifest.pre.evmContractConditions;
583
- let chain = opts.chain || manifest.pre.chain || process.env.LIT_CHAIN || 'baseSepolia';
584
- // Normalize chain key used historically
585
- if (chain === 'base-sepolia') chain = 'baseSepolia';
586
- // Normalize tokenId param to decimal string for Lit nodes AND update chain in access conditions
587
- try {
588
- if (Array.isArray(evmContractConditions) && evmContractConditions.length) {
589
- const cond = evmContractConditions[0];
590
- let updatedCond = { ...cond };
591
-
592
- // Update chain identifier to normalized version
593
- if (updatedCond.chain === 'base-sepolia') {
594
- updatedCond.chain = 'baseSepolia';
595
- }
596
-
597
- // Update tokenId to decimal format
598
- if (cond?.functionName === 'balanceOf' && Array.isArray(cond.functionParams) && cond.functionParams.length >= 2) {
599
- const p1 = String(cond.functionParams[1] || '').trim();
600
- if (/^0x[0-9a-fA-F]+$/.test(p1)) {
601
- const dec = BigInt(p1).toString();
602
- updatedCond.functionParams = [cond.functionParams[0], dec];
603
- }
604
- }
605
-
606
- evmContractConditions = [updatedCond];
607
- }
608
- } catch (_) {}
609
- // Strong preflight: confirm tokenId matches on-chain receipt id expected by purchase path
610
- try {
611
- const { ethers } = require('ethers');
612
- const { normalizeCidV1Base32 } = require('../utils/cid');
613
- const expectedId = ethers.keccak256(ethers.toUtf8Bytes(normalizeCidV1Base32(manifest.encryptedCID)));
614
- if (manifest.pre.tokenId?.toLowerCase?.() !== expectedId.toLowerCase()) {
615
- console.log('⚠️ Warning: Manifest tokenId does not match encryptedCID-derived id.');
616
- console.log(' manifest.pre.tokenId:', manifest.pre.tokenId);
617
- console.log(' expected:', expectedId);
618
- console.log(' If decryption fails, republish so tokenId is derived from encryptedCID.');
619
- }
620
- } catch(_) {}
621
- const { LitNodeClient, decryptToUint8Array } = requireLit();
622
- let litNetworkView = opts.litNetwork || process.env.LIT_NETWORK || 'serrano';
623
- if (litNetworkView === 'jalapeno') { console.log('⚠️ Lit network "jalapeno" is deprecated; using "serrano".'); litNetworkView = 'serrano'; }
624
- const client = new LitNodeClient({ litNetwork: litNetworkView });
625
- await client.connect();
626
- let authSig;
627
- if (opts.authSig) {
628
- authSig = loadAuthSig(opts.authSig);
629
- } else if (opts.autoAuth) {
630
- // Find an address with receipt balance>0 and sign SIWE via Cast keystore
631
- const { ethers } = require('ethers');
632
- const receipt = manifest.pre.receipt; const tokenId = manifest.pre.tokenId;
633
- const provider = new ethers.JsonRpcProvider(process.env.RPC_URL || 'https://base-sepolia.publicnode.com');
634
- const rcpt = new ethers.Contract(receipt, ['function balanceOf(address,uint256) view returns (uint256)'], provider);
635
- const Cast = require('../cast-wallet-manager'); const cast = new Cast(); await cast.connect();
636
- const ksDir = cast.keystoreDir; const fs = require('fs'); const path = require('path'); const { spawnSync } = require('child_process');
637
- if (!fs.existsSync(ksDir)) throw new Error('No Cast keystores found. Provide --auth-sig <file>.');
638
- const files = fs.readdirSync(ksDir);
639
- let match = null;
640
- for (const f of files) {
641
- const p = path.join(ksDir, f);
642
- const res = spawnSync(cast.castCommand, ['wallet', 'address', p], { encoding: 'utf8' });
643
- const addr = (res.stdout || '').trim();
644
- if (!addr) continue;
645
- try { const bal = await rcpt.balanceOf(addr, tokenId); if (BigInt(bal.toString()) > 0n) { match = { addr, ks: p }; break; } } catch (_) {}
646
- }
647
- if (!match) throw new Error('No keystore address holds the required receipt. Generate an AuthSig for the buyer wallet.');
648
- // Build SIWE
649
- const net = await provider.getNetwork(); const chainId = Number(net.chainId);
650
- const nonce = Math.random().toString(36).slice(2); const issuedAt = new Date().toISOString();
651
- const message = [`localhost wants you to sign in with your Ethereum account:`, `${match.addr}`, '', `Authorize with Lit Protocol`, '', `URI: http://localhost`, `Version: 1`, `Chain ID: ${chainId}`, `Nonce: ${nonce}`, `Issued At: ${issuedAt}`].join('\n');
652
- const pw = await cast.promptForPassword();
653
- const sigRes = spawnSync(cast.castCommand, ['wallet', 'sign', '--keystore', match.ks, '--password', pw, message], { encoding: 'utf8' });
654
- if (sigRes.status !== 0) throw new Error(sigRes.stderr || 'cast wallet sign failed');
655
- authSig = { sig: (sigRes.stdout||'').trim(), derivedVia: 'cast.personal_sign', signedMessage: message, address: match.addr };
656
- } else {
657
- throw new Error('Provide --auth-sig <file> or use --auto-auth');
658
- }
659
- // Build session signatures for Lit v7 (preferred over AuthSig)
660
- // Compute the resource manually: hashOfConditions/hashOfPrivateData, using the proper Resource class
661
- const { hashEVMContractConditions } = require('@lit-protocol/access-control-conditions');
662
- const { uint8arrayToString } = require('@lit-protocol/uint8arrays');
663
- const { LitAccessControlConditionResource } = requireLitAuthHelpers();
664
- const condHashBuf = await hashEVMContractConditions(evmContractConditions);
665
- const condHash = uint8arrayToString(new Uint8Array(condHashBuf), 'base16');
666
- const resource = new LitAccessControlConditionResource(`${condHash}/${dataToEncryptHash}`);
667
-
668
- // Get wallet manager for session signatures
669
- const WalletManager = require('../wallet-manager');
670
- const wm = new WalletManager();
671
- await wm.connect();
672
-
673
- // Create session signatures by signing a SIWE message via our CLI signer
674
- const sessionSigs = await client.getSessionSigs({
675
- chain: 'ethereum',
676
- resourceAbilityRequests: [
677
- {
678
- resource,
679
- ability: 'access-control-condition-decryption'
680
- }
681
- ],
682
- authNeededCallback: async ({ uri, expiration, resources, nonce, domain }) => {
683
- const signer = wm.getSigner();
684
- const address = await signer.getAddress();
685
- const siweDomain = domain || process.env.LIT_SIWE_DOMAIN || 'localhost';
686
- const siweUri = uri || 'lit:session';
687
- const issuedAt = new Date().toISOString();
688
- const chainId = 1; // ethereum for SIWE
689
- const statement = 'Authorize access to Sage premium content via Lit';
690
- const lines = [
691
- `${siweDomain} wants you to sign in with your Ethereum account:`,
692
- `${address}`,
693
- '',
694
- `${statement}`,
695
- '',
696
- `URI: ${siweUri}`,
697
- `Version: 1`,
698
- `Chain ID: ${chainId}`,
699
- `Nonce: ${nonce}`,
700
- `Issued At: ${issuedAt}`,
701
- `Expiration Time: ${expiration}`
702
- ];
703
- if (Array.isArray(resources) && resources.length) {
704
- lines.push('Resources:');
705
- for (const r of resources) lines.push(`- ${r}`);
706
- }
707
- const message = lines.join('\n');
708
- const sig = await signer.signMessage(message);
709
- return { sig, derivedVia: 'web3.eth.personal.sign', signedMessage: message, address };
710
- }
711
- });
712
-
713
- // Optional preflight: ensure buyer holds ERC-1155
714
- try {
715
- const { ethers } = require('ethers');
716
- const receipt = manifest.pre.receipt; const tokenId = manifest.pre.tokenId;
717
- const provider = new ethers.JsonRpcProvider(process.env.RPC_URL || 'https://base-sepolia.publicnode.com');
718
- const rcpt = new ethers.Contract(receipt, ['function balanceOf(address,uint256) view returns (uint256)'], provider);
719
- const owner = (authSig.address || authSig.siwe?.address || '').toString();
720
- if (owner && owner.startsWith('0x')) {
721
- const bal = await rcpt.balanceOf(owner, tokenId).catch(()=>null);
722
- if (bal !== null && BigInt(bal.toString()) === 0n) {
723
- console.log('❌ AuthSig address does not own the required receipt for this manifest.');
724
- console.log(' Address:', owner);
725
- console.log(' TokenId:', tokenId);
726
- console.log(' Hint: generate an AuthSig for the buyer wallet that holds the receipt.');
727
- console.log(' Example: sage wallet use <0xBuyer>; sage premium-pre authsig --account <0xBuyer> --out ./authSig.buyer.json');
728
- }
729
- }
730
- } catch(_) {}
731
-
732
- // Try multiple tokenId formats and chain combinations for backward compatibility
733
- const tokenId = manifest.pre.tokenId;
734
- // Preserve original tokenId format from manifest for exact condition matching
735
- const originalTokenId = String(tokenId);
736
- // Also prepare decimal format for legacy compatibility
737
- const decimalTokenId = (/^0x[0-9a-fA-F]+$/.test(originalTokenId)) ? BigInt(originalTokenId).toString() : originalTokenId;
738
- const attempts = [
739
- // Primary: baseSepolia + original tokenId format from manifest (preserves publishing format)
740
- { evmContractConditions: evmContractConditions.map(cond => ({
741
- ...cond,
742
- functionParams: [cond.functionParams[0], originalTokenId] // Use exact tokenId format from manifest
743
- })), chain: 'baseSepolia' },
744
- // Fallback: baseSepolia + decimal tokenId (for legacy compatibility)
745
- { evmContractConditions, chain: 'baseSepolia' },
746
- // Legacy: base-sepolia + original tokenId format
747
- { evmContractConditions: evmContractConditions.map(cond => ({
748
- ...cond,
749
- functionParams: [cond.functionParams[0], originalTokenId],
750
- chain: 'base-sepolia'
751
- })), chain: 'base-sepolia' }
752
- ];
753
-
754
- let symmetricKey = null;
755
- let lastError = null;
756
-
757
- for (let i = 0; i < attempts.length; i++) {
758
- const attempt = attempts[i];
759
- try {
760
- console.log(`🔍 Attempt ${i + 1}/${attempts.length}: chain=${attempt.chain}, tokenId=${attempt.evmContractConditions[0].functionParams[1]}`);
761
-
762
- // v7 API: decrypt using session signatures
763
- const decryptedKey = await decryptToUint8Array(
764
- {
765
- evmContractConditions: attempt.evmContractConditions,
766
- chain: attempt.chain,
767
- ciphertext: eskB64,
768
- dataToEncryptHash,
769
- // Prefer sessionSigs; fall back to authSig if explicitly provided
770
- ...(sessionSigs ? { sessionSigs } : {}),
771
- ...(sessionSigs ? {} : (authSig ? { authSig } : {}))
772
- },
773
- client
774
- );
775
-
776
- symmetricKey = decryptedKey;
777
- console.log(`✅ Success with attempt ${i + 1}`);
778
- break;
779
- } catch (err) {
780
- lastError = err;
781
- console.log(`❌ Attempt ${i + 1} failed: ${err.message}`);
782
- if (i === attempts.length - 1) {
783
- throw new Error(`All decryption attempts failed. Last error: ${lastError.message}`);
784
- }
785
- }
786
- }
787
-
788
- // symmetricKey is Uint8Array
789
- const keyBytes = Buffer.from(symmetricKey);
790
- const plaintext = decryptAesGcm(fromB64(enc.ciphertext), keyBytes, fromB64(enc.iv), fromB64(enc.tag));
791
- console.log(plaintext);
792
- } catch (e) { console.error('❌ premium-pre view failed:', e.message); process.exit(1); }
793
- });
19
+ This command has been replaced by personal premium.
794
20
 
795
- cmd
796
- .command('authsig')
797
- .description('Generate a Lit AuthSig (SIWE-style)')
798
- .option('--out <path>', 'Output JSON file', './authSig.json')
799
- .option('--domain <domain>', 'SIWE domain', 'localhost')
800
- .option('--uri <uri>', 'SIWE URI', 'http://localhost')
801
- .option('--statement <text>', 'SIWE statement', 'Authorize with Lit Protocol')
802
- .option('--chain-id <id>', 'Chain ID for SIWE (defaults to connected RPC)')
803
- .option('--account <address>', 'Explicit account address to sign with (Cast keystore)')
804
- .option('--wallet-type <type>', 'Signer type: cast|pk (default: cast)', 'cast')
805
- .option('--private-key <hex>', 'Private key (0x...) when --wallet-type=pk')
806
- .action(async (opts) => {
807
- try {
808
- const fs = require('fs'); const path = require('path');
809
- const { ethers } = require('ethers');
810
- const rpcUrl = process.env.RPC_URL || 'https://base-sepolia.publicnode.com';
811
- const provider = new ethers.JsonRpcProvider(rpcUrl);
812
- const net = await provider.getNetwork();
813
- const chainId = opts['chain-id'] ? Number(opts['chain-id']) : Number(net.chainId);
21
+ For personal premium prompts:
814
22
 
815
- // Helper to build SIWE
816
- const buildSiwe = (addr) => {
817
- const nonce = Math.random().toString(36).slice(2);
818
- const issuedAt = new Date().toISOString();
819
- const message = [
820
- `${opts.domain} wants you to sign in with your Ethereum account:`,
821
- `${addr}`,
822
- '',
823
- `${opts.statement}`,
824
- '',
825
- `URI: ${opts.uri}`,
826
- `Version: 1`,
827
- `Chain ID: ${chainId}`,
828
- `Nonce: ${nonce}`,
829
- `Issued At: ${issuedAt}`
830
- ].join('\n');
831
- return message;
832
- };
23
+ sage personal sell <key> <price> --encrypt --file <path> # List encrypted content
24
+ sage personal buy <creator> <key> # Purchase license
25
+ sage personal access <creator> <key> # Decrypt purchased content
833
26
 
834
- let address = null; let signature = null; let derivedVia = '';
835
- if (opts.walletType === 'pk') {
836
- if (!opts.privateKey) throw new Error('Provide --private-key when using --wallet-type=pk');
837
- const pk = opts.privateKey.startsWith('0x') ? opts.privateKey : ('0x' + opts.privateKey);
838
- const signer = new ethers.Wallet(pk, provider);
839
- address = await signer.getAddress();
840
- const msg = buildSiwe(address);
841
- signature = await signer.signMessage(msg);
842
- derivedVia = 'pk.personal_sign';
843
- const outPath = path.resolve(opts.out);
844
- fs.writeFileSync(outPath, JSON.stringify({ sig: signature, derivedVia, signedMessage: msg, address }, null, 2));
845
- console.log('✅ Wrote AuthSig to', outPath);
846
- return;
847
- }
27
+ Enable personal commands:
28
+ export SAGE_ENABLE_PERSONAL=1
848
29
 
849
- // Cast keystore path
850
- const Cast = require('../cast-wallet-manager');
851
- const cast = new Cast();
852
- await cast.connect();
853
- let targetAddr = cast.account;
854
- let keystorePath = null;
855
- if (opts.account && opts.account.startsWith('0x')) {
856
- // Try resolve a keystore file matching the requested account
857
- const ksDir = cast.keystoreDir;
858
- if (!fs.existsSync(ksDir)) throw new Error('Cast keystore directory not found');
859
- const files = fs.readdirSync(ksDir);
860
- const { spawnSync } = require('child_process');
861
- for (const f of files) {
862
- const p = path.join(ksDir, f);
863
- const res = spawnSync(cast.castCommand, ['wallet', 'address', p], { encoding: 'utf8' });
864
- const addr = (res.stdout || '').trim();
865
- if (addr && addr.toLowerCase() === opts.account.toLowerCase()) { keystorePath = p; targetAddr = addr; break; }
866
- }
867
- if (!keystorePath) throw new Error('No keystore found for --account');
868
- } else {
869
- // Use saved keystore from cast manager
870
- const saved = cast.loadCastWallet();
871
- if (!saved?.keystorePath) throw new Error('No Cast wallet configured');
872
- keystorePath = saved.keystorePath; targetAddr = saved.address;
873
- }
874
- const msg = buildSiwe(targetAddr);
875
- // Prompt for password
876
- const pw = await cast.promptForPassword();
877
- const { spawnSync } = require('child_process');
878
- const res = spawnSync(cast.castCommand, ['wallet', 'sign', '--keystore', keystorePath, '--password', pw, msg], { encoding: 'utf8' });
879
- if (res.status !== 0) throw new Error(res.stderr || 'cast wallet sign failed');
880
- signature = (res.stdout || '').trim(); derivedVia = 'cast.personal_sign';
881
- const outPath = path.resolve(opts.out);
882
- fs.writeFileSync(outPath, JSON.stringify({ sig: signature, derivedVia, signedMessage: msg, address: targetAddr }, null, 2));
883
- console.log('✅ Wrote AuthSig to', outPath);
884
- } catch (e) { console.error('❌ authsig failed:', e.message); process.exit(1); }
30
+ For more information:
31
+ docs/specs/premium-endorsement-model.md
32
+ `);
885
33
  });
886
34
 
887
35
  program.addCommand(cmd);