@juspay/neurolink 11.29.2 → 11.30.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.
Files changed (50) hide show
  1. package/CHANGELOG.md +3 -3
  2. package/dist/auth/anthropicOAuth.d.ts +50 -0
  3. package/dist/auth/anthropicOAuth.js +78 -0
  4. package/dist/browser/neurolink.min.js +393 -393
  5. package/dist/cli/commands/proxy.d.ts +2 -0
  6. package/dist/cli/commands/proxy.js +284 -4
  7. package/dist/cli/commands/proxyExpose.d.ts +35 -0
  8. package/dist/cli/commands/proxyExpose.js +252 -0
  9. package/dist/cli/commands/proxyPeer.d.ts +29 -0
  10. package/dist/cli/commands/proxyPeer.js +738 -0
  11. package/dist/cli/commands/proxyShare.d.ts +37 -0
  12. package/dist/cli/commands/proxyShare.js +1080 -0
  13. package/dist/cli/parser.js +7 -1
  14. package/dist/proxy/peerStore.d.ts +52 -0
  15. package/dist/proxy/peerStore.js +324 -0
  16. package/dist/proxy/peerTransport.d.ts +38 -0
  17. package/dist/proxy/peerTransport.js +242 -0
  18. package/dist/proxy/proxyPaths.d.ts +8 -0
  19. package/dist/proxy/proxyPaths.js +55 -17
  20. package/dist/proxy/requestLogger.js +8 -0
  21. package/dist/proxy/residentGrants.d.ts +57 -0
  22. package/dist/proxy/residentGrants.js +393 -0
  23. package/dist/proxy/shareAudit.d.ts +81 -0
  24. package/dist/proxy/shareAudit.js +280 -0
  25. package/dist/proxy/shareContext.d.ts +38 -0
  26. package/dist/proxy/shareContext.js +92 -0
  27. package/dist/proxy/shareGate.d.ts +64 -0
  28. package/dist/proxy/shareGate.js +216 -0
  29. package/dist/proxy/shareGrants.d.ts +115 -0
  30. package/dist/proxy/shareGrants.js +590 -0
  31. package/dist/proxy/shareLease.d.ts +101 -0
  32. package/dist/proxy/shareLease.js +192 -0
  33. package/dist/proxy/shareLedger.d.ts +105 -0
  34. package/dist/proxy/shareLedger.js +406 -0
  35. package/dist/proxy/shareListener.d.ts +60 -0
  36. package/dist/proxy/shareListener.js +143 -0
  37. package/dist/proxy/shareNotes.d.ts +97 -0
  38. package/dist/proxy/shareNotes.js +234 -0
  39. package/dist/proxy/sharePolicy.d.ts +110 -0
  40. package/dist/proxy/sharePolicy.js +366 -0
  41. package/dist/proxy/shareProvisioning.d.ts +110 -0
  42. package/dist/proxy/shareProvisioning.js +237 -0
  43. package/dist/proxy/shareReceipts.d.ts +99 -0
  44. package/dist/proxy/shareReceipts.js +303 -0
  45. package/dist/proxy/shareSigning.d.ts +40 -0
  46. package/dist/proxy/shareSigning.js +78 -0
  47. package/dist/server/routes/claudeProxyRoutes.js +1066 -3
  48. package/dist/types/cli.d.ts +61 -0
  49. package/dist/types/proxy.d.ts +781 -0
  50. package/package.json +2 -1
@@ -0,0 +1,1080 @@
1
+ /**
2
+ * `neurolink proxy share` — the lender's controls.
3
+ *
4
+ * Every mutation here lands in the running proxy without a restart: the grant
5
+ * store re-reads its file when the mtime moves, so `share pause` takes effect on
6
+ * the borrower's next request rather than at the next deploy.
7
+ *
8
+ * @module cli/commands/proxyShare
9
+ */
10
+ import { attachLeaseMaterial, createShareGrant, getNodePublicUrl, setNodePublicUrl, deleteShareGrant, findShareGrantByPeer, initShareGrants, listShareGrants, rotateShareGrantToken, setShareGrantState, updateShareGrant, } from "../../proxy/shareGrants.js";
11
+ import { resolveProxyGrantsPath, resolveProxyLedgerPath, resolveProxyPaths, resolveProxyNotesPath, resolveProxyProvisioningPath, resolveProxyReceiptsPath, resolveProxyShareAuditPath, } from "../../proxy/proxyPaths.js";
12
+ import { initShareLedger, summarizeGrantUsage, } from "../../proxy/shareLedger.js";
13
+ import { clearAuditDrift, getAuditRecord, initShareAudit, } from "../../proxy/shareAudit.js";
14
+ const ACTIONS = [
15
+ "create",
16
+ "provision",
17
+ "url",
18
+ "list",
19
+ "status",
20
+ "pause",
21
+ "resume",
22
+ "revoke",
23
+ "topup",
24
+ "set",
25
+ "link",
26
+ "rotate",
27
+ "level",
28
+ "note",
29
+ "notes",
30
+ "receipts",
31
+ "delete",
32
+ ];
33
+ /** Parse `7d`, `12h`, `90m` or a bare number of days into milliseconds. */
34
+ export function parseDurationMs(value) {
35
+ const match = /^(\d+(?:\.\d+)?)\s*([smhdw]?)$/i.exec(value.trim());
36
+ if (!match) {
37
+ return undefined;
38
+ }
39
+ const amount = Number(match[1]);
40
+ const unit = (match[2] || "d").toLowerCase();
41
+ const scale = {
42
+ s: 1000,
43
+ m: 60_000,
44
+ h: 3_600_000,
45
+ d: 86_400_000,
46
+ w: 604_800_000,
47
+ };
48
+ const factor = scale[unit];
49
+ return factor === undefined ? undefined : amount * factor;
50
+ }
51
+ /**
52
+ * Parse a window-slice expression.
53
+ *
54
+ * Accepts `20` (both windows), or `5h=20,7d=15` to set them apart. Both windows
55
+ * matter: a borrower can be harmless inside any single 5-hour session and still
56
+ * drain the week.
57
+ */
58
+ export function parseWindowSlice(value) {
59
+ const trimmed = value.trim();
60
+ if (!trimmed) {
61
+ return undefined;
62
+ }
63
+ if (/^\d+(?:\.\d+)?$/.test(trimmed)) {
64
+ const pct = Number(trimmed);
65
+ return { session5hPct: pct, weekly7dPct: pct };
66
+ }
67
+ const slice = {};
68
+ for (const part of trimmed.split(",")) {
69
+ const [rawKey, rawValue] = part.split("=");
70
+ if (!rawKey || !rawValue) {
71
+ return undefined;
72
+ }
73
+ const pct = Number(rawValue.trim());
74
+ if (!Number.isFinite(pct)) {
75
+ return undefined;
76
+ }
77
+ const key = rawKey.trim().toLowerCase();
78
+ if (key === "5h" || key === "session") {
79
+ slice.session5hPct = pct;
80
+ }
81
+ else if (key === "7d" || key === "weekly" || key === "week") {
82
+ slice.weekly7dPct = pct;
83
+ }
84
+ else {
85
+ return undefined;
86
+ }
87
+ }
88
+ return Object.keys(slice).length > 0 ? slice : undefined;
89
+ }
90
+ /** Parse `12h<60` / `12h<60@25` — window, utilization threshold, slice cap. */
91
+ export function parseSpillover(value) {
92
+ const match = /^(\d+(?:\.\d+)?)h?\s*<\s*(\d+(?:\.\d+)?)(?:@(\d+(?:\.\d+)?))?$/i.exec(value.trim());
93
+ if (!match) {
94
+ return undefined;
95
+ }
96
+ return {
97
+ beforeResetHours: Number(match[1]),
98
+ whenUtilizationBelowPct: Number(match[2]),
99
+ ...(match[3] !== undefined ? { maxSlicePct: Number(match[3]) } : {}),
100
+ };
101
+ }
102
+ /** Parse `21-9` into an hour-of-day admission window. */
103
+ export function parseSchedule(value) {
104
+ const match = /^(\d{1,2})\s*-\s*(\d{1,2})$/.exec(value.trim());
105
+ if (!match) {
106
+ return undefined;
107
+ }
108
+ const fromHour = Number(match[1]);
109
+ const toHour = Number(match[2]);
110
+ if (fromHour > 23 || toHour > 23) {
111
+ return undefined;
112
+ }
113
+ return { fromHour, toHour };
114
+ }
115
+ /** Parse `100/week` or `50/session` into a refill policy. */
116
+ export function parseRefill(value) {
117
+ const [rawAmount, rawPeriod] = value.split("/");
118
+ const amount = Number(rawAmount?.trim());
119
+ const period = rawPeriod?.trim().toLowerCase();
120
+ if (!Number.isFinite(amount) || amount <= 0) {
121
+ return undefined;
122
+ }
123
+ if (period === "week" || period === "weekly" || period === "7d") {
124
+ return { amount, per: "week" };
125
+ }
126
+ if (period === "session" || period === "5h") {
127
+ return { amount, per: "session" };
128
+ }
129
+ return undefined;
130
+ }
131
+ /** Parse `20/min` or a bare number into a per-minute request ceiling. */
132
+ export function parseRate(value) {
133
+ const match = /^(\d+)(?:\s*\/\s*min(?:ute)?)?$/i.exec(value.trim());
134
+ return match ? Number(match[1]) : undefined;
135
+ }
136
+ /**
137
+ * Presets fill the gate set; they are not a separate concept.
138
+ * Every field a preset sets can be overridden by an explicit flag, so
139
+ * `--preset spare --reserve 50` is exactly the preset with a tighter floor.
140
+ */
141
+ const PRESETS = {
142
+ spare: {
143
+ gates: {
144
+ reserveFloor: { session5hPct: 30, weekly7dPct: 30 },
145
+ maxSlice: { session5hPct: 20, weekly7dPct: 20 },
146
+ },
147
+ ledger: "unlimited",
148
+ },
149
+ spillover: {
150
+ gates: {
151
+ spillover: {
152
+ beforeResetHours: 12,
153
+ whenUtilizationBelowPct: 60,
154
+ maxSlicePct: 25,
155
+ },
156
+ },
157
+ ledger: "unlimited",
158
+ },
159
+ metered: {
160
+ gates: { maxSlice: { session5hPct: 25 } },
161
+ ledger: "coins",
162
+ },
163
+ open: {
164
+ // The most permissive preset still carries a rate ceiling. "Open" means no
165
+ // window slice and no ledger, not "hammer the pool as fast as you can" —
166
+ // and a borrower with a runaway loop is the likeliest way an open share
167
+ // costs its lender a window.
168
+ gates: {
169
+ reserveFloor: { session5hPct: 10 },
170
+ rate: { perMinute: 60 },
171
+ },
172
+ ledger: "unlimited",
173
+ },
174
+ };
175
+ function isPresetName(value) {
176
+ return (value === "spare" ||
177
+ value === "spillover" ||
178
+ value === "metered" ||
179
+ value === "open");
180
+ }
181
+ /**
182
+ * Build the link a borrower consumes with `peer add --link`.
183
+ *
184
+ * The token rides in the fragment because fragments are not transmitted: a link
185
+ * pasted into anything that resolves the URL leaks the host, never the secret.
186
+ */
187
+ export function buildShareLink(publicUrl, token, receiptSecret) {
188
+ const trimmed = publicUrl.replace(/\/+$/, "");
189
+ const host = trimmed.replace(/^https?:\/\//, "");
190
+ // https is the default, so only a plaintext origin needs to say so. Without
191
+ // this the scheme is lost and a LAN or loopback peer silently becomes an
192
+ // https URL that nothing is listening on.
193
+ const insecure = /^http:\/\//i.test(trimmed);
194
+ // The receipt secret rides in the same fragment as the token: it is no more
195
+ // sensitive, and a borrower without it cannot check a single charge. A "."
196
+ // separates them because neither half's alphabet contains one.
197
+ const fragment = receiptSecret ? `${token}.${receiptSecret}` : token;
198
+ return `neurolink://share/${host}${insecure ? "?scheme=http" : ""}#${fragment}`;
199
+ }
200
+ /**
201
+ * The gate-only listener port, when the running proxy has one.
202
+ *
203
+ * Read from the proxy's own state rather than derived, because the operator may
204
+ * have moved it with `--share-port` and the answer has to match what is actually
205
+ * listening.
206
+ */
207
+ async function readSharePort() {
208
+ try {
209
+ const { StateFileManager } = await import("../utils/serverUtils.js");
210
+ const state = new StateFileManager("proxy-state.json").load();
211
+ return state?.sharePort;
212
+ }
213
+ catch {
214
+ return undefined;
215
+ }
216
+ }
217
+ function printToken(token, publicUrl, peerLabel, receiptSecret) {
218
+ console.info("");
219
+ console.info(" Share token (shown once — it is not stored anywhere):");
220
+ console.info(` ${token}`);
221
+ if (publicUrl) {
222
+ console.info("");
223
+ console.info(" Send the peer this link:");
224
+ console.info(` ${buildShareLink(publicUrl, token, receiptSecret)}`);
225
+ console.info("");
226
+ console.info(" They add it with:");
227
+ console.info(` neurolink proxy peer add --name <your-name> --link "${buildShareLink(publicUrl, token, receiptSecret)}"`);
228
+ }
229
+ else {
230
+ console.info("");
231
+ console.info(" Re-run with --public-url <your exposed URL> to get a link the peer can paste.");
232
+ console.info(" Until then they can add it by hand:");
233
+ console.info(` neurolink proxy peer add --name <your-name> --url <your-url> --token ${token}` +
234
+ (receiptSecret ? ` --receipt-secret ${receiptSecret}` : ""));
235
+ }
236
+ void peerLabel;
237
+ }
238
+ function formatPct(value) {
239
+ return value === undefined ? "—" : `${value}%`;
240
+ }
241
+ function describeGates(gates) {
242
+ const lines = [];
243
+ if (gates.reserveFloor) {
244
+ lines.push(` reserve floor 5h ${formatPct(gates.reserveFloor.session5hPct)} 7d ${formatPct(gates.reserveFloor.weekly7dPct)}`);
245
+ }
246
+ if (gates.maxSlice) {
247
+ lines.push(` max slice 5h ${formatPct(gates.maxSlice.session5hPct)} 7d ${formatPct(gates.maxSlice.weekly7dPct)} (of the pool)`);
248
+ }
249
+ if (gates.maxSlicePerAccount) {
250
+ lines.push(` per-account cap 5h ${formatPct(gates.maxSlicePerAccount.session5hPct)} 7d ${formatPct(gates.maxSlicePerAccount.weekly7dPct)}`);
251
+ }
252
+ if (gates.spillover) {
253
+ lines.push(` spillover last ${gates.spillover.beforeResetHours}h when under ${gates.spillover.whenUtilizationBelowPct}%` +
254
+ (gates.spillover.maxSlicePct !== undefined
255
+ ? ` (cap ${gates.spillover.maxSlicePct}%)`
256
+ : ""));
257
+ }
258
+ if (gates.models?.length) {
259
+ lines.push(` models ${gates.models.join(", ")}`);
260
+ }
261
+ if (gates.accounts?.length) {
262
+ lines.push(` accounts ${gates.accounts.join(", ")}`);
263
+ }
264
+ if (gates.rate) {
265
+ const parts = [];
266
+ if (gates.rate.perMinute !== undefined) {
267
+ parts.push(`${gates.rate.perMinute}/min`);
268
+ }
269
+ if (gates.rate.concurrency !== undefined) {
270
+ parts.push(`${gates.rate.concurrency} concurrent`);
271
+ }
272
+ lines.push(` rate ${parts.join(", ")}`);
273
+ }
274
+ if (gates.schedule) {
275
+ lines.push(` schedule ${String(gates.schedule.fromHour).padStart(2, "0")}:00–${String(gates.schedule.toHour).padStart(2, "0")}:00 local`);
276
+ }
277
+ if (gates.notAfter !== undefined) {
278
+ lines.push(` expires ${new Date(gates.notAfter).toISOString()}`);
279
+ }
280
+ return lines;
281
+ }
282
+ function describeGrant(grant) {
283
+ const entitlement = grant.entitlement.ledger === "coins"
284
+ ? `${Math.floor(grant.entitlement.coins ?? 0)} coins`
285
+ : "unlimited";
286
+ const refill = grant.entitlement.refill
287
+ ? ` (+${grant.entitlement.refill.amount}/${grant.entitlement.refill.per})`
288
+ : "";
289
+ const lines = [
290
+ `${grant.peerLabel} [${grant.id}]`,
291
+ ` level ${grant.level}`,
292
+ ` state ${grant.state}`,
293
+ ` entitlement ${entitlement}${refill}`,
294
+ ...describeGates(grant.gates),
295
+ ];
296
+ if (grant.lastUsedAt) {
297
+ lines.push(` last used ${new Date(grant.lastUsedAt).toISOString()}`);
298
+ }
299
+ return lines.join("\n");
300
+ }
301
+ /**
302
+ * Collect gate overrides from the flags actually supplied.
303
+ * Returns `undefined` for anything unset so a `set` never silently clears a
304
+ * control the operator did not mention.
305
+ */
306
+ function gatesFromArgs(argv) {
307
+ const gates = {};
308
+ const errors = [];
309
+ if (argv.reserve !== undefined) {
310
+ const parsed = parseWindowSlice(argv.reserve);
311
+ if (!parsed) {
312
+ errors.push(`--reserve: cannot parse "${argv.reserve}" (try 30 or 5h=30,7d=20)`);
313
+ }
314
+ else {
315
+ gates.reserveFloor = parsed;
316
+ }
317
+ }
318
+ if (argv.maxSlicePerAccount !== undefined) {
319
+ const parsed = parseWindowSlice(argv.maxSlicePerAccount);
320
+ if (!parsed) {
321
+ errors.push(`--max-slice-per-account: cannot parse "${argv.maxSlicePerAccount}"`);
322
+ }
323
+ else {
324
+ gates.maxSlicePerAccount = parsed;
325
+ }
326
+ }
327
+ if (argv.maxSlice !== undefined) {
328
+ const parsed = parseWindowSlice(argv.maxSlice);
329
+ if (!parsed) {
330
+ errors.push(`--max-slice: cannot parse "${argv.maxSlice}" (try 20 or 5h=20,7d=15)`);
331
+ }
332
+ else {
333
+ gates.maxSlice = parsed;
334
+ }
335
+ }
336
+ if (argv.spillover !== undefined) {
337
+ const parsed = parseSpillover(argv.spillover);
338
+ if (!parsed) {
339
+ errors.push(`--spillover: cannot parse "${argv.spillover}" (try 12h<60@25)`);
340
+ }
341
+ else {
342
+ gates.spillover = parsed;
343
+ }
344
+ }
345
+ if (argv.models?.length) {
346
+ gates.models = argv.models;
347
+ }
348
+ if (argv.accounts?.length) {
349
+ gates.accounts = argv.accounts;
350
+ }
351
+ if (argv.rate !== undefined || argv.concurrency !== undefined) {
352
+ const rate = {};
353
+ if (argv.rate !== undefined) {
354
+ const parsed = parseRate(argv.rate);
355
+ if (parsed === undefined) {
356
+ errors.push(`--rate: cannot parse "${argv.rate}" (try 20/min)`);
357
+ }
358
+ else {
359
+ rate.perMinute = parsed;
360
+ }
361
+ }
362
+ if (argv.concurrency !== undefined) {
363
+ rate.concurrency = argv.concurrency;
364
+ }
365
+ if (Object.keys(rate).length > 0) {
366
+ gates.rate = rate;
367
+ }
368
+ }
369
+ if (argv.schedule !== undefined) {
370
+ const parsed = parseSchedule(argv.schedule);
371
+ if (!parsed) {
372
+ errors.push(`--schedule: cannot parse "${argv.schedule}" (try 21-9)`);
373
+ }
374
+ else {
375
+ gates.schedule = parsed;
376
+ }
377
+ }
378
+ if (argv.expires !== undefined) {
379
+ const ms = parseDurationMs(argv.expires);
380
+ if (ms === undefined) {
381
+ errors.push(`--expires: cannot parse "${argv.expires}" (try 7d or 48h)`);
382
+ }
383
+ else {
384
+ gates.notAfter = Date.now() + ms;
385
+ }
386
+ }
387
+ return { gates, errors };
388
+ }
389
+ async function resolvePeerOrFail(peer) {
390
+ if (!peer) {
391
+ throw new Error("A peer name or grant id is required for this action.");
392
+ }
393
+ const grant = await findShareGrantByPeer(peer);
394
+ if (!grant) {
395
+ throw new Error(`No share grant found for "${peer}".`);
396
+ }
397
+ return grant;
398
+ }
399
+ /**
400
+ * `share create` — mint a grant and print its one-time token.
401
+ *
402
+ * The token is shown here and never again: nothing stores it, so a lost link is
403
+ * replaced by `share rotate`, not reprinted.
404
+ */
405
+ async function handleShareCreate(argv, publicUrl) {
406
+ if (!argv.peer) {
407
+ throw new Error("--peer is required to create a share grant.");
408
+ }
409
+ const presetName = argv.preset ?? "spare";
410
+ if (!isPresetName(presetName)) {
411
+ throw new Error(`Unknown preset "${presetName}". Use spare, spillover, metered or open.`);
412
+ }
413
+ const preset = PRESETS[presetName];
414
+ const { gates: overrides, errors } = gatesFromArgs(argv);
415
+ if (errors.length > 0) {
416
+ throw new Error(errors.join("\n"));
417
+ }
418
+ const level = argv.level === "complete" ? "complete" : "live";
419
+ const ledger = argv.ledger === "coins" || argv.coins !== undefined
420
+ ? "coins"
421
+ : argv.ledger === "unlimited"
422
+ ? "unlimited"
423
+ : preset.ledger;
424
+ const entitlement = { ledger };
425
+ if (ledger === "coins") {
426
+ entitlement.coins = argv.coins ?? 0;
427
+ }
428
+ if (argv.refill) {
429
+ const refill = parseRefill(argv.refill);
430
+ if (!refill) {
431
+ throw new Error(`--refill: cannot parse "${argv.refill}" (try 100/week or 50/session)`);
432
+ }
433
+ entitlement.refill = refill;
434
+ }
435
+ const issued = await createShareGrant({
436
+ peerLabel: argv.peer,
437
+ level,
438
+ entitlement,
439
+ gates: { ...preset.gates, ...overrides },
440
+ ...(argv.note ? { note: argv.note } : {}),
441
+ });
442
+ if (argv.json) {
443
+ console.info(JSON.stringify({ grant: issued.grant, token: issued.token }, null, 2));
444
+ return;
445
+ }
446
+ console.info(describeGrant(issued.grant));
447
+ printToken(issued.token, publicUrl, issued.grant.peerLabel, issued.grant.receiptSecret);
448
+ // The gate-only listener comes up on the first active grant, so this is
449
+ // exactly the moment an operator needs to know which port to expose —
450
+ // and that their own client keeps using the main one.
451
+ const sharePort = await readSharePort();
452
+ console.info("");
453
+ if (sharePort !== undefined) {
454
+ console.info(` Peers connect on the share listener, port ${sharePort}.`);
455
+ console.info(" It refuses any request without a token, so expose that port — not the");
456
+ console.info(" main one, which still serves your own client untokened.");
457
+ }
458
+ else {
459
+ console.info(" The gate-only share listener starts within a few seconds of this grant,");
460
+ console.info(" on your proxy port + 1. Expose that port; `neurolink proxy expose` picks");
461
+ console.info(" it automatically.");
462
+ }
463
+ console.info("");
464
+ console.info(" Note: sharing subscription capacity with other people is likely outside");
465
+ console.info(" your provider's consumer terms, and the account carrying the traffic is");
466
+ console.info(" the one exposed. Share deliberately.");
467
+ return;
468
+ }
469
+ /** `share status` — spend, reach and audit standing, per grant. */
470
+ async function handleShareStatus(argv) {
471
+ const grants = argv.peer
472
+ ? [await resolvePeerOrFail(argv.peer)]
473
+ : await listShareGrants();
474
+ const withUsage = await Promise.all(grants.map(async (grant) => ({
475
+ grant,
476
+ usage: await summarizeGrantUsage(grant.id),
477
+ audit: await getAuditRecord(grant.id),
478
+ })));
479
+ if (argv.json) {
480
+ console.info(JSON.stringify(withUsage, null, 2));
481
+ return;
482
+ }
483
+ if (withUsage.length === 0) {
484
+ console.info("No share grants issued.");
485
+ return;
486
+ }
487
+ for (const { grant, usage, audit } of withUsage) {
488
+ console.info(describeGrant(grant));
489
+ console.info(` spent ${usage.coinsSpent.toFixed(1)} coins over ${usage.requests} request(s)`);
490
+ if (usage.accounts > 0) {
491
+ console.info(` drew on ${usage.accounts} account(s)`);
492
+ }
493
+ if (grant.level === "complete") {
494
+ // Spend on a complete share is the borrower's own word. Show what the
495
+ // account's real usage makes of it so the two are never confused.
496
+ const verdict = audit?.autoPausedAt
497
+ ? `auto-paused on usage drift ${new Date(audit.autoPausedAt).toISOString()}`
498
+ : audit && audit.driftStreak > 0
499
+ ? `drifting (${audit.driftStreak} consecutive)`
500
+ : audit?.lastObservation
501
+ ? "consistent with the account's own usage"
502
+ : "no check-in observed yet";
503
+ console.info(` audit ${verdict}`);
504
+ if (audit?.lastDriftDetail && audit.driftStreak > 0) {
505
+ console.info(` ${audit.lastDriftDetail}`);
506
+ }
507
+ }
508
+ console.info("");
509
+ }
510
+ return;
511
+ }
512
+ /** `share note` — mint a bearer coin note against this node. */
513
+ async function handleShareNote(argv, paths, publicUrl) {
514
+ const { initShareNotes, issueShareNote, encodeShareNote } = await import("../../proxy/shareNotes.js");
515
+ initShareNotes(resolveProxyNotesPath(paths));
516
+ if (argv.coins === undefined || argv.coins <= 0) {
517
+ throw new Error("--coins is required, and must be positive.");
518
+ }
519
+ const ttlMs = argv.ttl ? parseDurationMs(argv.ttl) : undefined;
520
+ if (argv.ttl && ttlMs === undefined) {
521
+ throw new Error(`--ttl: cannot parse "${argv.ttl}" (try 30d or 48h)`);
522
+ }
523
+ const note = await issueShareNote({
524
+ issuer: publicUrl ?? "this node",
525
+ coins: argv.coins,
526
+ ...(ttlMs !== undefined ? { ttlMs } : {}),
527
+ ...(argv.memo ? { memo: argv.memo } : {}),
528
+ });
529
+ const encoded = encodeShareNote(note);
530
+ if (argv.json) {
531
+ console.info(JSON.stringify({ note, encoded }, null, 2));
532
+ return;
533
+ }
534
+ console.info(`Minted a ${argv.coins}-coin note against this node.`);
535
+ console.info("");
536
+ console.info(` ${encoded}`);
537
+ console.info("");
538
+ console.info(` Redeemable once, by whoever holds it, until ${new Date(note.notAfter).toISOString()}.`);
539
+ console.info(" They need a grant with you to redeem it into:");
540
+ console.info(" neurolink proxy peer redeem --name <you> --coin-note <the note>");
541
+ console.info(" Anyone holding it can check it without spending it, with --check.");
542
+ return;
543
+ }
544
+ /** `share notes` — what this node has minted, and whether it was redeemed. */
545
+ async function handleShareNotes(argv, paths) {
546
+ const { initShareNotes, listShareNotes } = await import("../../proxy/shareNotes.js");
547
+ initShareNotes(resolveProxyNotesPath(paths));
548
+ const minted = await listShareNotes();
549
+ if (argv.json) {
550
+ console.info(JSON.stringify(minted, null, 2));
551
+ return;
552
+ }
553
+ if (minted.length === 0) {
554
+ console.info("No coin notes minted.");
555
+ return;
556
+ }
557
+ const now = Date.now();
558
+ for (const record of minted) {
559
+ const state = record.redeemedAt
560
+ ? `redeemed ${new Date(record.redeemedAt).toISOString()}`
561
+ : record.notAfter <= now
562
+ ? "expired"
563
+ : `valid until ${new Date(record.notAfter).toISOString()}`;
564
+ console.info(`${record.noteId} ${String(record.coins).padStart(6)} coins ${state}${record.memo ? ` — ${record.memo}` : ""}`);
565
+ }
566
+ return;
567
+ }
568
+ /**
569
+ * `share provision` — authorize a borrower's own credential.
570
+ *
571
+ * Split PKCE: the borrower holds the verifier, so the code relayed through here
572
+ * is worthless to the lender and to anyone who intercepts it.
573
+ */
574
+ async function handleShareProvision(argv, paths, publicUrl) {
575
+ const grant = await resolvePeerOrFail(argv.peer);
576
+ const { generateLeaseSecret } = await import("../../proxy/shareLease.js");
577
+ const { authorizeProvisionRequest, getProvisionRequest, initShareProvisioning, } = await import("../../proxy/shareProvisioning.js");
578
+ initShareProvisioning(resolveProxyProvisioningPath(paths));
579
+ // The borrower goes first. Its verifier is the only thing that can turn
580
+ // the code this command produces into a credential, so there is nothing
581
+ // to authorize until it has lodged the matching challenge.
582
+ const pending = await getProvisionRequest(grant.id);
583
+ if (!pending) {
584
+ throw new Error(`${grant.peerLabel} has not asked to be provisioned yet.\n` +
585
+ " On their machine, with your share token already added:\n" +
586
+ " neurolink proxy peer request --name <your-name>\n" +
587
+ " Then run this command again. Their request is valid for 15 minutes.");
588
+ }
589
+ const leasePolicy = {
590
+ ttlMs: parseDurationMs(argv.leaseTtl ?? "7d") ?? 604_800_000,
591
+ heartbeatEveryMs: parseDurationMs(argv.heartbeat ?? "15m") ?? 900_000,
592
+ offlineGraceMs: parseDurationMs(argv.offlineGrace ?? "24h") ?? 86_400_000,
593
+ };
594
+ const prepared = await updateShareGrant(grant.id, { level: "complete" });
595
+ if (!prepared) {
596
+ throw new Error(`No share grant found for "${argv.peer}".`);
597
+ }
598
+ // Which of the lender's accounts this credential is minted from. The
599
+ // drift audit compares that account's real utilization against what the
600
+ // borrower reports, so without it complete mode has reporting but no
601
+ // verification.
602
+ const fromAccount = argv.fromAccount ?? prepared.gates.accounts?.[0];
603
+ const withLease = await attachLeaseMaterial(prepared.id, prepared.leaseSecret ?? generateLeaseSecret(), leasePolicy, fromAccount);
604
+ if (!withLease) {
605
+ throw new Error("Could not prepare the grant for provisioning.");
606
+ }
607
+ console.info(`Authorizing an independent credential for ${withLease.peerLabel}.`);
608
+ console.info("");
609
+ console.info(" This runs a SEPARATE authorization on your account — it does not copy");
610
+ console.info(" your own tokens. Copying them would put two devices on one rotating");
611
+ console.info(" refresh chain, and the loser of that race gets disabled — yours.");
612
+ console.info("");
613
+ console.info(" You will never hold a token for this credential. The borrower keeps");
614
+ console.info(" the PKCE verifier; you only relay a code that is worthless without it.");
615
+ console.info("");
616
+ console.info(` Once provisioned, ${withLease.peerLabel} calls the provider directly. You keep`);
617
+ console.info(` control through the lease: they stop ${argv.offlineGrace ?? "24h"} after you become`);
618
+ console.info(" unreachable, immediately once a heartbeat reaches them, and at the");
619
+ console.info(" lease's hard expiry regardless.");
620
+ console.info("");
621
+ console.info(" Note: a credential on someone else's machine can be extracted by them.");
622
+ console.info(" Complete sharing is enforced cooperatively and audited after the fact —");
623
+ console.info(" use it for people you would trust with the account.");
624
+ console.info("");
625
+ const { buildSubscriptionAuthUrl } = await import("../../auth/anthropicOAuth.js");
626
+ const authUrl = buildSubscriptionAuthUrl({
627
+ codeChallenge: pending.codeChallenge,
628
+ state: pending.state,
629
+ });
630
+ console.info(" Open this URL, sign in, and authorize:");
631
+ console.info("");
632
+ console.info(` ${authUrl}`);
633
+ console.info("");
634
+ console.info(" Anthropic then shows an authorization code. Paste it back:");
635
+ console.info("");
636
+ console.info(` neurolink proxy share provision --peer ${withLease.peerLabel} --code <code>`);
637
+ if (!argv.code) {
638
+ console.info("");
639
+ console.info(" Nothing has been authorized yet — re-run with --code to finish.");
640
+ return;
641
+ }
642
+ // Anthropic renders the code as `code#state`. Only the code half is ours
643
+ // to relay; the state came from the borrower and is already on file.
644
+ const code = argv.code.trim().split("#")[0];
645
+ const authorized = await authorizeProvisionRequest({
646
+ grantId: withLease.id,
647
+ code,
648
+ ...(fromAccount ? { accountLabel: fromAccount } : {}),
649
+ });
650
+ if (!authorized.ok) {
651
+ throw new Error(`Could not record that code: ${authorized.reason}`);
652
+ }
653
+ console.info("");
654
+ console.info(`Authorized. ${withLease.peerLabel} can collect it now:`);
655
+ console.info("");
656
+ console.info(" neurolink proxy peer request --name <your-name> --claim");
657
+ console.info("");
658
+ console.info(" The code is single-use and expires with their request. If they miss");
659
+ console.info(" the window, they ask again and you authorize again.");
660
+ if (!publicUrl) {
661
+ console.info("");
662
+ console.info(" No public URL is recorded, so the lease will carry no heartbeat");
663
+ console.info(" address and they will stop at the offline grace. Set one with:");
664
+ console.info(" neurolink proxy share url https://your-proxy");
665
+ }
666
+ return;
667
+ }
668
+ /** `share url` — read, set or clear the address links are minted against. */
669
+ async function handleShareUrl(argv) {
670
+ const positional = argv.value?.trim();
671
+ if (argv.clear || positional === "clear") {
672
+ await setNodePublicUrl(undefined);
673
+ console.info("Public URL cleared. Share links now need an explicit --public-url.");
674
+ return;
675
+ }
676
+ // `get` prints the bare value and nothing else, so it can be captured in
677
+ // a shell variable without post-processing. Absent means empty output and
678
+ // a non-zero exit, which is what a script expects.
679
+ if (positional === "get") {
680
+ const current = await getNodePublicUrl();
681
+ if (!current) {
682
+ process.exitCode = 1;
683
+ return;
684
+ }
685
+ console.info(current);
686
+ return;
687
+ }
688
+ const target = positional && positional !== "show" ? positional : argv.publicUrl;
689
+ if (!target) {
690
+ const current = await getNodePublicUrl();
691
+ console.info(current
692
+ ? `This node is shared at ${current}`
693
+ : "No public URL recorded. Set one with: neurolink proxy share url https://proxy.example.com");
694
+ return;
695
+ }
696
+ await setNodePublicUrl(target);
697
+ console.info(`Share links will be minted against ${target}`);
698
+ const { probeProxyGate } = await import("./proxyExpose.js");
699
+ try {
700
+ const parsed = new URL(target);
701
+ const port = Number(parsed.port || (parsed.protocol === "https:" ? 443 : 80));
702
+ const probe = await probeProxyGate(parsed.hostname, port, parsed.protocol === "https:" ? "https" : "http");
703
+ if (probe.reachable && !probe.gated) {
704
+ console.info("");
705
+ console.info(" ⚠ That address served a request carrying no share token.");
706
+ console.info(" Anyone who finds it can spend your subscription. Point that address");
707
+ console.info(" at the gate-only share listener instead — it refuses every untokened");
708
+ console.info(" request, and your own client keeps using the main port:");
709
+ const sharePort = await readSharePort();
710
+ console.info(sharePort !== undefined
711
+ ? ` port ${sharePort} on this machine`
712
+ : " your proxy port + 1, once at least one grant is active");
713
+ console.info("");
714
+ console.info(" Or gate this port itself, which also refuses your own client:");
715
+ console.info(" NEUROLINK_PROXY_REQUIRE_GRANT=1 neurolink proxy start");
716
+ }
717
+ }
718
+ catch {
719
+ // A URL we cannot probe is not an error — it may not be up yet.
720
+ }
721
+ return;
722
+ }
723
+ async function runShareCommand(argv) {
724
+ const paths = resolveProxyPaths(argv.dev ?? false);
725
+ initShareGrants(resolveProxyGrantsPath(paths));
726
+ initShareLedger(resolveProxyLedgerPath(paths));
727
+ initShareAudit(resolveProxyShareAuditPath(paths));
728
+ // An explicit flag wins; otherwise use whatever address this node was told it
729
+ // lives at. Most operators front the proxy with a domain they already own,
730
+ // and retyping it on every mint is how share links go stale.
731
+ const publicUrl = argv.publicUrl ?? (await getNodePublicUrl());
732
+ switch (argv.action) {
733
+ case "create":
734
+ await handleShareCreate(argv, publicUrl);
735
+ return;
736
+ case "list": {
737
+ const grants = await listShareGrants();
738
+ if (argv.json) {
739
+ console.info(JSON.stringify(grants, null, 2));
740
+ return;
741
+ }
742
+ if (grants.length === 0) {
743
+ console.info("No share grants issued.");
744
+ return;
745
+ }
746
+ for (const grant of grants) {
747
+ console.info(describeGrant(grant));
748
+ console.info("");
749
+ }
750
+ return;
751
+ }
752
+ case "status":
753
+ await handleShareStatus(argv);
754
+ return;
755
+ case "pause":
756
+ case "resume": {
757
+ const grant = await resolvePeerOrFail(argv.peer);
758
+ const nextState = argv.action === "pause" ? "paused" : "active";
759
+ const updated = await setShareGrantState(grant.id, nextState);
760
+ if (nextState === "active") {
761
+ // A grant the drift audit paused carries a marker that stops it firing
762
+ // twice. Resuming without clearing it would leave the audit permanently
763
+ // disarmed for this grant, which is the opposite of what resume means.
764
+ await clearAuditDrift(grant.id);
765
+ }
766
+ console.info(`${updated?.peerLabel ?? grant.peerLabel} is now ${nextState}. ` +
767
+ "Takes effect on the borrower's next request.");
768
+ return;
769
+ }
770
+ case "revoke": {
771
+ const grant = await resolvePeerOrFail(argv.peer);
772
+ await setShareGrantState(grant.id, "revoked");
773
+ console.info(`${grant.peerLabel} revoked. The token no longer serves traffic.`);
774
+ if (grant.level === "complete") {
775
+ console.info(" This grant is complete-level: the borrower holds a credential on your");
776
+ console.info(" account. Revocation stops the lease at their next heartbeat and at");
777
+ console.info(" lease expiry — it does not reach out and delete the credential.");
778
+ }
779
+ return;
780
+ }
781
+ case "note":
782
+ await handleShareNote(argv, paths, publicUrl);
783
+ return;
784
+ case "notes":
785
+ await handleShareNotes(argv, paths);
786
+ return;
787
+ case "receipts": {
788
+ const { initShareReceipts, listShareReceipts, nettedCoinsFor } = await import("../../proxy/shareReceipts.js");
789
+ initShareReceipts(resolveProxyReceiptsPath(paths));
790
+ const grant = await resolvePeerOrFail(argv.peer);
791
+ const issuedReceipts = await listShareReceipts(grant.id);
792
+ const forgiven = await nettedCoinsFor(grant.id);
793
+ if (argv.json) {
794
+ console.info(JSON.stringify({ receipts: issuedReceipts, netted: forgiven }, null, 2));
795
+ return;
796
+ }
797
+ if (issuedReceipts.length === 0) {
798
+ console.info(`No receipts issued to ${grant.peerLabel} yet.`);
799
+ return;
800
+ }
801
+ const charged = issuedReceipts.reduce((sum, receipt) => sum + receipt.coins, 0);
802
+ console.info(`${grant.peerLabel}: ${issuedReceipts.length} receipt(s), ${charged.toFixed(1)} coins charged`);
803
+ if (forgiven > 0) {
804
+ console.info(` ${forgiven.toFixed(1)} coins forgiven by reciprocal netting`);
805
+ }
806
+ for (const receipt of issuedReceipts.slice(-10)) {
807
+ console.info(` #${String(receipt.sequence).padStart(4)} ${receipt.coins.toFixed(2).padStart(8)} coins ${receipt.model ?? "unknown model"} ${new Date(receipt.settledAt).toISOString()}`);
808
+ }
809
+ return;
810
+ }
811
+ case "delete": {
812
+ const grant = await resolvePeerOrFail(argv.peer);
813
+ const deleted = await deleteShareGrant(grant.id);
814
+ // The audit trail is keyed by grant id and would otherwise outlive the
815
+ // grant it describes, forever.
816
+ const { clearAuditRecord } = await import("../../proxy/shareAudit.js");
817
+ await clearAuditRecord(grant.id);
818
+ // An outstanding challenge outliving its grant would still be claimable
819
+ // by whoever holds the now-dead token, so it goes with it.
820
+ const { clearProvisionRequest, initShareProvisioning } = await import("../../proxy/shareProvisioning.js");
821
+ initShareProvisioning(resolveProxyProvisioningPath(paths));
822
+ await clearProvisionRequest(grant.id);
823
+ const { clearShareReceipts, initShareReceipts } = await import("../../proxy/shareReceipts.js");
824
+ initShareReceipts(resolveProxyReceiptsPath(paths));
825
+ await clearShareReceipts(grant.id);
826
+ console.info(deleted
827
+ ? `${grant.peerLabel} deleted.`
828
+ : `${grant.peerLabel} was already gone.`);
829
+ return;
830
+ }
831
+ case "rotate": {
832
+ const grant = await resolvePeerOrFail(argv.peer);
833
+ const issued = await rotateShareGrantToken(grant.id);
834
+ if (!issued) {
835
+ throw new Error(`No share grant found for "${argv.peer}".`);
836
+ }
837
+ console.info(`${grant.peerLabel} token rotated. The previous token is dead.`);
838
+ printToken(issued.token, publicUrl, grant.peerLabel, issued.grant.receiptSecret);
839
+ return;
840
+ }
841
+ case "topup": {
842
+ const grant = await resolvePeerOrFail(argv.peer);
843
+ if (argv.coins === undefined) {
844
+ throw new Error("--coins is required for topup.");
845
+ }
846
+ // An unlimited grant has no balance to add to, and writing `ledger:
847
+ // "coins"` here would quietly meter a peer the operator had deliberately
848
+ // left unmetered. Changing the ledger is `set`'s job, and it says so.
849
+ if (grant.entitlement.ledger !== "coins") {
850
+ throw new Error(`${grant.peerLabel} is an unlimited grant — there is no balance to top up.\n` +
851
+ " To start metering it instead:\n" +
852
+ ` neurolink proxy share set --peer ${grant.peerLabel} --coins ${argv.coins}`);
853
+ }
854
+ const balance = Math.max(0, (grant.entitlement.coins ?? 0) + argv.coins);
855
+ const updated = await updateShareGrant(grant.id, {
856
+ entitlement: { ledger: "coins", coins: balance },
857
+ });
858
+ console.info(`${grant.peerLabel} balance is now ${Math.floor(updated?.entitlement.coins ?? balance)} coins.`);
859
+ return;
860
+ }
861
+ case "set": {
862
+ const grant = await resolvePeerOrFail(argv.peer);
863
+ const { gates, errors } = gatesFromArgs(argv);
864
+ if (errors.length > 0) {
865
+ throw new Error(errors.join("\n"));
866
+ }
867
+ const entitlement = {};
868
+ if (argv.ledger === "coins" || argv.ledger === "unlimited") {
869
+ entitlement.ledger = argv.ledger;
870
+ }
871
+ if (argv.coins !== undefined) {
872
+ entitlement.ledger = "coins";
873
+ entitlement.coins = Math.max(0, argv.coins);
874
+ }
875
+ if (argv.refill !== undefined) {
876
+ const refill = parseRefill(argv.refill);
877
+ if (!refill) {
878
+ throw new Error(`--refill: cannot parse "${argv.refill}"`);
879
+ }
880
+ entitlement.refill = refill;
881
+ }
882
+ const updated = await updateShareGrant(grant.id, {
883
+ ...(Object.keys(gates).length > 0 ? { gates } : {}),
884
+ ...(Object.keys(entitlement).length > 0 ? { entitlement } : {}),
885
+ });
886
+ console.info(updated ? describeGrant(updated) : "No change.");
887
+ return;
888
+ }
889
+ case "provision":
890
+ await handleShareProvision(argv, paths, publicUrl);
891
+ return;
892
+ case "url":
893
+ await handleShareUrl(argv);
894
+ return;
895
+ case "level": {
896
+ const grant = await resolvePeerOrFail(argv.peer);
897
+ const target = argv.to;
898
+ if (target !== "live" && target !== "complete") {
899
+ throw new Error("--to must be live or complete.");
900
+ }
901
+ const updated = await updateShareGrant(grant.id, { level: target });
902
+ if (!updated) {
903
+ throw new Error(`Could not change the level for "${argv.peer}".`);
904
+ }
905
+ console.info(`${grant.peerLabel} is now a ${target} share.`);
906
+ if (target === "complete") {
907
+ console.info(" Run `neurolink proxy share provision --peer " +
908
+ `${grant.peerLabel}\` to mint the borrower's own credential.`);
909
+ }
910
+ return;
911
+ }
912
+ case "link": {
913
+ const grant = await resolvePeerOrFail(argv.peer);
914
+ // Nothing stores the token, by design — so a link genuinely cannot be
915
+ // reprinted. Rotating is the honest answer, and it also invalidates
916
+ // whatever copy went missing.
917
+ console.info(`Tokens are never stored, so a link cannot be reprinted for ${grant.peerLabel}.`);
918
+ console.info(` neurolink proxy share rotate --peer ${grant.peerLabel}` +
919
+ (publicUrl ? "" : " --public-url <your-url>"));
920
+ return;
921
+ }
922
+ default:
923
+ throw new Error(`Unknown share action: ${String(argv.action)}`);
924
+ }
925
+ }
926
+ export const proxyShareCommand = {
927
+ command: "share <action> [value]",
928
+ describe: "Lend pool capacity to a peer and control how much they may take",
929
+ builder: (yargs) => yargs
930
+ .positional("action", {
931
+ type: "string",
932
+ choices: [...ACTIONS],
933
+ describe: "Share action",
934
+ })
935
+ .positional("value", {
936
+ type: "string",
937
+ describe: "Action argument, e.g. the URL for `share url`",
938
+ })
939
+ .option("peer", {
940
+ type: "string",
941
+ description: "Peer name or grant id",
942
+ })
943
+ .option("level", {
944
+ type: "string",
945
+ choices: ["live", "complete"],
946
+ description: "live = borrower proxies through you; complete = borrower holds its own credential on your account",
947
+ })
948
+ .option("preset", {
949
+ type: "string",
950
+ choices: ["spare", "spillover", "metered", "open"],
951
+ description: "Starting gate set; individual flags override it",
952
+ })
953
+ .option("ledger", {
954
+ type: "string",
955
+ choices: ["coins", "unlimited"],
956
+ description: "Metered against a coin balance, or uncapped",
957
+ })
958
+ .option("coins", {
959
+ type: "number",
960
+ description: "Coin balance to set (create/set) or add (topup)",
961
+ })
962
+ .option("refill", {
963
+ type: "string",
964
+ description: "Standing allowance, e.g. 100/week or 50/session",
965
+ })
966
+ .option("max-slice", {
967
+ type: "string",
968
+ alias: "maxSlice",
969
+ description: "Hard ceiling as a share of each window: 20, or 5h=20,7d=15",
970
+ })
971
+ .option("max-slice-per-account", {
972
+ type: "string",
973
+ alias: "maxSlicePerAccount",
974
+ description: "Ceiling applied to each account independently, instead of the pool",
975
+ })
976
+ .option("reserve", {
977
+ type: "string",
978
+ description: "Headroom you keep for yourself: 30, or 5h=30,7d=20",
979
+ })
980
+ .option("spillover", {
981
+ type: "string",
982
+ description: "Lend only near a reset when little was used: 12h<60@25",
983
+ })
984
+ .option("models", {
985
+ type: "string",
986
+ array: true,
987
+ description: "Model tiers this peer may use, e.g. sonnet haiku",
988
+ })
989
+ .option("accounts", {
990
+ type: "string",
991
+ array: true,
992
+ description: "Which of your accounts are lendable under this grant",
993
+ })
994
+ .option("rate", {
995
+ type: "string",
996
+ description: "Request ceiling, e.g. 20/min",
997
+ })
998
+ .option("concurrency", {
999
+ type: "number",
1000
+ description: "Maximum concurrent borrowed requests",
1001
+ })
1002
+ .option("schedule", {
1003
+ type: "string",
1004
+ description: "Hours this share is open, e.g. 21-9",
1005
+ })
1006
+ .option("expires", {
1007
+ type: "string",
1008
+ description: "Grant lifetime, e.g. 7d or 48h",
1009
+ })
1010
+ .option("note", {
1011
+ type: "string",
1012
+ description: "Free-text note kept with the grant",
1013
+ })
1014
+ .option("to", {
1015
+ type: "string",
1016
+ choices: ["live", "complete"],
1017
+ description: "Target level for `share level`",
1018
+ })
1019
+ .option("from-account", {
1020
+ type: "string",
1021
+ alias: "fromAccount",
1022
+ description: "Which of your accounts the complete share draws on (enables drift auditing)",
1023
+ })
1024
+ .option("clear", {
1025
+ type: "boolean",
1026
+ default: false,
1027
+ description: "With `share url`: forget the recorded public address",
1028
+ })
1029
+ .option("code", {
1030
+ type: "string",
1031
+ description: "Authorization code from your browser, to finish `share provision`",
1032
+ })
1033
+ .option("ttl", {
1034
+ type: "string",
1035
+ description: "With `share note`: how long the note stays redeemable",
1036
+ })
1037
+ .option("memo", {
1038
+ type: "string",
1039
+ description: "With `share note`: a note carried on the coin note",
1040
+ })
1041
+ .option("offline-grace", {
1042
+ type: "string",
1043
+ alias: "offlineGrace",
1044
+ description: "How long a complete-share borrower may run unheard-from (default 24h)",
1045
+ })
1046
+ .option("heartbeat", {
1047
+ type: "string",
1048
+ description: "Complete-share check-in interval (default 15m)",
1049
+ })
1050
+ .option("lease-ttl", {
1051
+ type: "string",
1052
+ alias: "leaseTtl",
1053
+ description: "Complete-share lease lifetime (default 7d)",
1054
+ })
1055
+ .option("public-url", {
1056
+ type: "string",
1057
+ alias: "publicUrl",
1058
+ description: "Public URL for this node; defaults to the one saved by `share url`",
1059
+ })
1060
+ .option("json", {
1061
+ type: "boolean",
1062
+ default: false,
1063
+ description: "Emit JSON instead of formatted text",
1064
+ })
1065
+ .option("dev", {
1066
+ type: "boolean",
1067
+ default: false,
1068
+ description: "Use the isolated dev-mode state directory",
1069
+ }),
1070
+ handler: async (argv) => {
1071
+ try {
1072
+ await runShareCommand(argv);
1073
+ }
1074
+ catch (error) {
1075
+ console.error(error instanceof Error ? error.message : String(error));
1076
+ process.exitCode = 1;
1077
+ }
1078
+ },
1079
+ };
1080
+ //# sourceMappingURL=proxyShare.js.map