@1sat/cli 0.0.120 → 0.0.121

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -386,6 +386,120 @@ var init_monitor_once = __esm(() => {
386
386
  init_monitor_lock();
387
387
  });
388
388
 
389
+ // src/wallet-api/admin.ts
390
+ import { PERMISSION_SCHEME_IDS } from "@1sat/types";
391
+ import {
392
+ LocalWalletPermissionsManager
393
+ } from "@1sat/wallet";
394
+ function adminWallet(wallet, store) {
395
+ const manager = new LocalWalletPermissionsManager(wallet, CLI_ADMIN_ORIGINATOR, { permissionModules: adminPermissionModules }, { store });
396
+ return withAdminOriginator(manager);
397
+ }
398
+ function withAdminOriginator(manager) {
399
+ return new Proxy(manager, {
400
+ get(target, property, receiver) {
401
+ const value = Reflect.get(target, property, receiver);
402
+ if (typeof value !== "function")
403
+ return value;
404
+ return (args) => value.call(target, args, CLI_ADMIN_ORIGINATOR);
405
+ }
406
+ });
407
+ }
408
+ var CLI_ADMIN_ORIGINATOR = "1sat-cli.internal", adminPermissionModules;
409
+ var init_admin = __esm(() => {
410
+ adminPermissionModules = Object.fromEntries(PERMISSION_SCHEME_IDS.map((scheme) => [
411
+ scheme,
412
+ {
413
+ onRequest: async (req) => ({ args: req.args }),
414
+ onResponse: async (res) => res
415
+ }
416
+ ]));
417
+ });
418
+
419
+ // src/wallet-api/permission-store.ts
420
+ import {
421
+ existsSync as existsSync3,
422
+ mkdirSync as mkdirSync2,
423
+ readFileSync as readFileSync4,
424
+ renameSync,
425
+ writeFileSync as writeFileSync4
426
+ } from "node:fs";
427
+ import { dirname, join as join5 } from "node:path";
428
+ import {
429
+ permissionKeyToString
430
+ } from "@1sat/wallet";
431
+ function permissionStorePath(dataDir, chain) {
432
+ return join5(dataDir, `permissions-${chain}.json`);
433
+ }
434
+
435
+ class FilePermissionStore {
436
+ path;
437
+ constructor(path) {
438
+ this.path = path;
439
+ }
440
+ async findGrant(key) {
441
+ return this.load().grants[permissionKeyToString(key)] ?? null;
442
+ }
443
+ async putGrant(grant) {
444
+ const file = this.load();
445
+ file.grants[permissionKeyToString(grant.key)] = grant;
446
+ this.save(file);
447
+ }
448
+ async deleteGrant(key) {
449
+ const file = this.load();
450
+ const id = permissionKeyToString(key);
451
+ if (!(id in file.grants))
452
+ return;
453
+ delete file.grants[id];
454
+ this.save(file);
455
+ }
456
+ async deleteAllForOriginator(originator) {
457
+ const file = this.load();
458
+ let removed = 0;
459
+ for (const [id, grant] of Object.entries(file.grants)) {
460
+ if (grant.key.originator === originator) {
461
+ delete file.grants[id];
462
+ removed++;
463
+ }
464
+ }
465
+ if (removed > 0)
466
+ this.save(file);
467
+ return removed;
468
+ }
469
+ async listGrants(filter) {
470
+ const out = [];
471
+ for (const grant of Object.values(this.load().grants)) {
472
+ if (filter?.originator && grant.key.originator !== filter.originator)
473
+ continue;
474
+ if (filter?.type && grant.key.type !== filter.type)
475
+ continue;
476
+ out.push(grant);
477
+ }
478
+ return out;
479
+ }
480
+ load() {
481
+ if (!existsSync3(this.path))
482
+ return { version: FILE_VERSION, grants: {} };
483
+ const raw = readFileSync4(this.path, "utf8");
484
+ const parsed = JSON.parse(raw);
485
+ if (parsed.version !== FILE_VERSION) {
486
+ throw new Error(`Unsupported permission store version ${String(parsed.version)} in ${this.path}`);
487
+ }
488
+ return { version: FILE_VERSION, grants: parsed.grants ?? {} };
489
+ }
490
+ save(file) {
491
+ const dir = dirname(this.path);
492
+ if (!existsSync3(dir))
493
+ mkdirSync2(dir, { recursive: true, mode: 448 });
494
+ const tmp = `${this.path}.${process.pid}.tmp`;
495
+ writeFileSync4(tmp, `${JSON.stringify(file, null, 2)}
496
+ `, { mode: 384 });
497
+ renameSync(tmp, this.path);
498
+ }
499
+ }
500
+ var FILE_VERSION = 1;
501
+ var init_permission_store = () => {};
502
+
389
503
  // src/context.ts
390
504
  import { createContext } from "@1sat/actions";
391
505
  import { createNodeWallet as createNodeWallet2 } from "@1sat/wallet-node";
@@ -405,7 +519,8 @@ async function loadContext(privateKey, opts) {
405
519
  backups: config.backups,
406
520
  skipInitialMonitor: true
407
521
  });
408
- const ctx = createContext(walletResult.wallet, {
522
+ const wallet = adminWallet(walletResult.wallet, new FilePermissionStore(permissionStorePath(dataDir, opts.chain)));
523
+ const ctx = createContext(wallet, {
409
524
  services: walletResult.services,
410
525
  chain: opts.chain,
411
526
  dataDir,
@@ -428,6 +543,8 @@ async function loadContext(privateKey, opts) {
428
543
  var init_context = __esm(() => {
429
544
  init_config();
430
545
  init_monitor_once();
546
+ init_admin();
547
+ init_permission_store();
431
548
  });
432
549
 
433
550
  // src/output.ts
@@ -563,7 +680,7 @@ var init_action = __esm(() => {
563
680
  });
564
681
 
565
682
  // src/help.ts
566
- import { readFileSync as readFileSync4 } from "node:fs";
683
+ import { readFileSync as readFileSync5 } from "node:fs";
567
684
  import chalk2 from "chalk";
568
685
  function getVersion() {
569
686
  return VERSION;
@@ -717,7 +834,7 @@ var VERSION, GLOBAL_OPTIONS, ENV_VARS, COMMANDS;
717
834
  var init_help = __esm(() => {
718
835
  VERSION = (() => {
719
836
  try {
720
- const pkg = JSON.parse(readFileSync4(new URL("../package.json", import.meta.url), "utf8"));
837
+ const pkg = JSON.parse(readFileSync5(new URL("../package.json", import.meta.url), "utf8"));
721
838
  return pkg.version || "0.0.11";
722
839
  } catch {
723
840
  return "0.0.11";
@@ -1390,7 +1507,82 @@ var init_help = __esm(() => {
1390
1507
  { name: "monitor", description: "Monitor daemon only" },
1391
1508
  {
1392
1509
  name: "wallet-api",
1393
- description: "App-facing BRC-100 wallet endpoint only (headless, default port 3321)"
1510
+ description: "App-facing BRC-100 wallet endpoint only (headless, default port 3321). Ungranted requests are denied with the `1sat permissions grant` command that would allow them"
1511
+ }
1512
+ ]
1513
+ },
1514
+ {
1515
+ group: "Server",
1516
+ name: "permissions",
1517
+ description: "What apps on `1sat serve wallet-api` may do. Grants live in <dataDir>/permissions-<chain>.json; the endpoint reads the file on every check, so a grant applies to the app's next call without a restart. Works with the server stopped or running and needs no wallet key.",
1518
+ subcommands: [
1519
+ {
1520
+ name: "list",
1521
+ description: "Show grants, grouped by app origin",
1522
+ positional: "[<origin>]"
1523
+ },
1524
+ {
1525
+ name: "grant",
1526
+ description: "Allow an app one or more permissions. Selectors may be combined in one call.",
1527
+ positional: "<origin>",
1528
+ args: [
1529
+ {
1530
+ flag: "--protocol",
1531
+ values: "<name>",
1532
+ description: "Protocol name; requires --level"
1533
+ },
1534
+ {
1535
+ flag: "--level",
1536
+ values: "<0|1|2>",
1537
+ description: "Protocol security level"
1538
+ },
1539
+ {
1540
+ flag: "--counterparty",
1541
+ values: "<hex|self|anyone>",
1542
+ description: "Counterparty for a level 0/2 protocol (default self; ignored at level 1), or a certificate's verifier (also --verifier)"
1543
+ },
1544
+ {
1545
+ flag: "--privileged",
1546
+ description: "Grant the privileged variant of a protocol/certificate"
1547
+ },
1548
+ { flag: "--basket", values: "<name>", description: "Basket access" },
1549
+ {
1550
+ flag: "--label",
1551
+ values: "<name>",
1552
+ description: "Action label (a level-1 `action label <name>` protocol)"
1553
+ },
1554
+ {
1555
+ flag: "--certificate",
1556
+ values: "<type>",
1557
+ description: "Certificate type; requires --fields"
1558
+ },
1559
+ {
1560
+ flag: "--fields",
1561
+ values: "<a,b>",
1562
+ description: "Certificate fields that may be disclosed"
1563
+ },
1564
+ {
1565
+ flag: "--spending",
1566
+ values: "<satoshis>",
1567
+ description: "Monthly spending cap; month-to-date spend is read from action history on every check"
1568
+ }
1569
+ ]
1570
+ },
1571
+ {
1572
+ name: "revoke",
1573
+ description: "Remove matching grants, or every grant for an origin",
1574
+ positional: "<origin>",
1575
+ args: [
1576
+ {
1577
+ flag: "--all",
1578
+ description: "Remove every grant for the origin"
1579
+ },
1580
+ {
1581
+ flag: "--protocol",
1582
+ values: "<name>",
1583
+ description: "Same selectors as `grant`"
1584
+ }
1585
+ ]
1394
1586
  }
1395
1587
  ]
1396
1588
  },
@@ -1573,7 +1765,7 @@ var init_authfetch_request = __esm(() => {
1573
1765
  });
1574
1766
 
1575
1767
  // src/commands/authfetch.ts
1576
- import { readFileSync as readFileSync5 } from "node:fs";
1768
+ import { readFileSync as readFileSync6 } from "node:fs";
1577
1769
  import { confirm, isCancel as isCancel2 } from "@clack/prompts";
1578
1770
  async function handleAuthfetchCommand(args, opts) {
1579
1771
  if (!args[0] || args[0] === "help" || args.includes("--help") || args.includes("-h")) {
@@ -1632,7 +1824,7 @@ Unknown or missing method: ${method ?? ""}`);
1632
1824
  }
1633
1825
  let body;
1634
1826
  if (bodyRaw !== undefined) {
1635
- body = bodyRaw.startsWith("@") ? readFileSync5(bodyRaw.slice(1), "utf8") : bodyRaw;
1827
+ body = bodyRaw.startsWith("@") ? readFileSync6(bodyRaw.slice(1), "utf8") : bodyRaw;
1636
1828
  if (!headers["Content-Type"] && !headers["content-type"]) {
1637
1829
  headers["Content-Type"] = "application/json";
1638
1830
  }
@@ -2077,7 +2269,7 @@ var init_init = __esm(() => {
2077
2269
  });
2078
2270
 
2079
2271
  // src/beef.ts
2080
- import { readFileSync as readFileSync6 } from "node:fs";
2272
+ import { readFileSync as readFileSync7 } from "node:fs";
2081
2273
  import { Utils } from "@bsv/sdk";
2082
2274
  function parseBeefFlag(value) {
2083
2275
  if (!value)
@@ -2086,7 +2278,7 @@ function parseBeefFlag(value) {
2086
2278
  if (!value.includes(`
2087
2279
  `) && !value.includes(" ") && value.length < 4096) {
2088
2280
  try {
2089
- const buf = readFileSync6(value);
2281
+ const buf = readFileSync7(value);
2090
2282
  return Array.from(buf);
2091
2283
  } catch {}
2092
2284
  }
@@ -2227,7 +2419,7 @@ var init_locks = __esm(() => {
2227
2419
  });
2228
2420
 
2229
2421
  // src/commands/mcp-proxy.ts
2230
- import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "node:fs";
2422
+ import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync8, writeFileSync as writeFileSync5 } from "node:fs";
2231
2423
  import { Hash, KeyDeriver, PrivateKey as PrivateKey3, Signature, Utils as Utils2 } from "@bsv/sdk";
2232
2424
  import { createLogger, initLogger } from "evlog";
2233
2425
  function generateNonce() {
@@ -2236,12 +2428,12 @@ function generateNonce() {
2236
2428
  return toBase64(Array.from(bytes));
2237
2429
  }
2238
2430
  function getClientKey() {
2239
- mkdirSync2(KEY_DIR, { recursive: true });
2240
- if (existsSync3(CLIENT_KEY_PATH)) {
2241
- return PrivateKey3.fromWif(readFileSync7(CLIENT_KEY_PATH, "utf-8").trim());
2431
+ mkdirSync3(KEY_DIR, { recursive: true });
2432
+ if (existsSync4(CLIENT_KEY_PATH)) {
2433
+ return PrivateKey3.fromWif(readFileSync8(CLIENT_KEY_PATH, "utf-8").trim());
2242
2434
  }
2243
2435
  const key = PrivateKey3.fromRandom();
2244
- writeFileSync4(CLIENT_KEY_PATH, key.toWif(), { mode: 384 });
2436
+ writeFileSync5(CLIENT_KEY_PATH, key.toWif(), { mode: 384 });
2245
2437
  const log = createLogger({ context: "startup" });
2246
2438
  log.set({ event: "key_generated", path: CLIENT_KEY_PATH });
2247
2439
  log.emit();
@@ -2739,7 +2931,7 @@ var init_opns = __esm(() => {
2739
2931
  });
2740
2932
 
2741
2933
  // src/commands/ordinals.ts
2742
- import { readFileSync as readFileSync8 } from "node:fs";
2934
+ import { readFileSync as readFileSync9 } from "node:fs";
2743
2935
  import { basename, extname } from "node:path";
2744
2936
  import {
2745
2937
  burnOrdinals,
@@ -2857,7 +3049,7 @@ async function ordinalsInscribe(args, opts) {
2857
3049
  }
2858
3050
  let fileBytes;
2859
3051
  try {
2860
- fileBytes = readFileSync8(file);
3052
+ fileBytes = readFileSync9(file);
2861
3053
  } catch (err) {
2862
3054
  fatal(`Failed to read file: ${err.message}`);
2863
3055
  }
@@ -3045,6 +3237,335 @@ var init_ordinals = __esm(() => {
3045
3237
  };
3046
3238
  });
3047
3239
 
3240
+ // src/wallet-api/grants.ts
3241
+ import {
3242
+ normalizeOriginator,
3243
+ permissionKeyToString as permissionKeyToString2
3244
+ } from "@1sat/wallet";
3245
+ function shellQuote(value) {
3246
+ if (value !== "" && /^[A-Za-z0-9_@%+=:,./-]+$/.test(value))
3247
+ return value;
3248
+ return `"${value.replace(/(["\\$`])/g, "\\$1")}"`;
3249
+ }
3250
+ function grantCommand(spec, prefix = GRANT_COMMAND) {
3251
+ const key = spec.key;
3252
+ const parts = [prefix, shellQuote(key.originator)];
3253
+ switch (key.type) {
3254
+ case "protocol": {
3255
+ if (key.protocolLevel === 1 && key.protocolName.startsWith(LABEL_PROTOCOL_PREFIX)) {
3256
+ parts.push("--label", shellQuote(key.protocolName.slice(LABEL_PROTOCOL_PREFIX.length)));
3257
+ break;
3258
+ }
3259
+ parts.push("--protocol", shellQuote(key.protocolName));
3260
+ parts.push("--level", String(key.protocolLevel));
3261
+ if (key.protocolLevel !== 1 && key.counterparty) {
3262
+ parts.push("--counterparty", shellQuote(key.counterparty));
3263
+ }
3264
+ if (key.privileged)
3265
+ parts.push("--privileged");
3266
+ break;
3267
+ }
3268
+ case "basket":
3269
+ parts.push("--basket", shellQuote(key.basket));
3270
+ break;
3271
+ case "certificate":
3272
+ parts.push("--certificate", shellQuote(key.certType));
3273
+ parts.push("--fields", shellQuote(key.fields.join(",")));
3274
+ if (key.verifier) {
3275
+ parts.push("--counterparty", shellQuote(key.verifier));
3276
+ }
3277
+ if (key.privileged)
3278
+ parts.push("--privileged");
3279
+ break;
3280
+ case "spending":
3281
+ parts.push("--spending", String(spec.authorizedAmount ?? 0));
3282
+ break;
3283
+ }
3284
+ return parts.join(" ");
3285
+ }
3286
+ function describeKey(spec) {
3287
+ const key = spec.key;
3288
+ switch (key.type) {
3289
+ case "protocol": {
3290
+ if (key.protocolLevel === 1 && key.protocolName.startsWith(LABEL_PROTOCOL_PREFIX)) {
3291
+ return `label ${key.protocolName.slice(LABEL_PROTOCOL_PREFIX.length)}`;
3292
+ }
3293
+ const bits = [`"${key.protocolName}" (level ${key.protocolLevel}`];
3294
+ if (key.protocolLevel !== 1 && key.counterparty) {
3295
+ bits.push(`, counterparty ${key.counterparty}`);
3296
+ }
3297
+ if (key.privileged)
3298
+ bits.push(", privileged");
3299
+ return `protocol ${bits.join("")})`;
3300
+ }
3301
+ case "basket":
3302
+ return `basket "${key.basket}"`;
3303
+ case "certificate":
3304
+ return `cert "${key.certType}" fields [${key.fields.join(", ")}]${key.verifier ? ` to ${key.verifier}` : ""}${key.privileged ? " (privileged)" : ""}`;
3305
+ case "spending":
3306
+ return `spending up to ${spec.authorizedAmount ?? 0} sat per month`;
3307
+ }
3308
+ }
3309
+ function specsFromFlags(origin, flags) {
3310
+ const originator = normalizeOriginator(origin);
3311
+ if (!originator)
3312
+ throw new Error("An app origin is required");
3313
+ const specs = [];
3314
+ if (flags.protocol !== undefined || flags.label !== undefined) {
3315
+ if (flags.protocol !== undefined && flags.label !== undefined) {
3316
+ throw new Error("Use either --protocol or --label, not both");
3317
+ }
3318
+ const isLabel = flags.label !== undefined;
3319
+ const name = isLabel ? `${LABEL_PROTOCOL_PREFIX}${flags.label}` : flags.protocol;
3320
+ if (!name.trim() || name === LABEL_PROTOCOL_PREFIX) {
3321
+ throw new Error(isLabel ? "--label requires a name" : "--protocol requires a name");
3322
+ }
3323
+ const level = isLabel ? 1 : parseLevel(flags.level);
3324
+ specs.push({
3325
+ key: {
3326
+ type: "protocol",
3327
+ originator,
3328
+ privileged: flags.privileged,
3329
+ protocolLevel: level,
3330
+ protocolName: name,
3331
+ counterparty: level === 1 ? "" : flags.counterparty ?? "self"
3332
+ }
3333
+ });
3334
+ }
3335
+ if (flags.basket !== undefined) {
3336
+ if (!flags.basket.trim())
3337
+ throw new Error("--basket requires a name");
3338
+ specs.push({
3339
+ key: { type: "basket", originator, basket: flags.basket }
3340
+ });
3341
+ }
3342
+ if (flags.certificate !== undefined) {
3343
+ if (!flags.certificate.trim()) {
3344
+ throw new Error("--certificate requires a certificate type");
3345
+ }
3346
+ const fields = (flags.fields ?? "").split(",").map((f) => f.trim()).filter((f) => f.length > 0);
3347
+ if (fields.length === 0) {
3348
+ throw new Error("--certificate requires --fields <a,b>");
3349
+ }
3350
+ specs.push({
3351
+ key: {
3352
+ type: "certificate",
3353
+ originator,
3354
+ privileged: flags.privileged,
3355
+ verifier: flags.counterparty ?? "",
3356
+ certType: flags.certificate,
3357
+ fields: fields.sort()
3358
+ }
3359
+ });
3360
+ }
3361
+ if (flags.spending !== undefined) {
3362
+ const satoshis = Number(flags.spending);
3363
+ if (!Number.isInteger(satoshis) || satoshis <= 0) {
3364
+ throw new Error("--spending requires a positive whole number of satoshis");
3365
+ }
3366
+ specs.push({
3367
+ key: { type: "spending", originator },
3368
+ authorizedAmount: satoshis
3369
+ });
3370
+ }
3371
+ if (specs.length === 0) {
3372
+ throw new Error("Nothing selected. Pass --protocol/--label, --basket, --certificate or --spending");
3373
+ }
3374
+ return specs;
3375
+ }
3376
+ function parseLevel(value) {
3377
+ if (value === undefined) {
3378
+ throw new Error("--protocol requires --level <0|1|2>");
3379
+ }
3380
+ if (value !== "0" && value !== "1" && value !== "2") {
3381
+ throw new Error(`--level must be 0, 1 or 2 (got ${value})`);
3382
+ }
3383
+ return Number(value);
3384
+ }
3385
+ function specId(spec) {
3386
+ return permissionKeyToString2(spec.key);
3387
+ }
3388
+ var LABEL_PROTOCOL_PREFIX = "action label ", GRANT_COMMAND = "1sat permissions grant";
3389
+ var init_grants = () => {};
3390
+
3391
+ // src/commands/permissions.ts
3392
+ import { normalizeOriginator as normalizeOriginator2 } from "@1sat/wallet";
3393
+ async function handlePermissionsCommand(args, opts) {
3394
+ const [subcommand, ...rest] = args;
3395
+ switch (subcommand) {
3396
+ case "list":
3397
+ case "ls":
3398
+ return permissionsList(rest, opts);
3399
+ case "grant":
3400
+ return permissionsGrant(rest, opts);
3401
+ case "revoke":
3402
+ return permissionsRevoke(rest, opts);
3403
+ default:
3404
+ printCommandHelp("permissions", opts.json);
3405
+ if (subcommand && subcommand !== "help") {
3406
+ process.exit(1);
3407
+ }
3408
+ }
3409
+ }
3410
+ function openStore(opts) {
3411
+ const path = permissionStorePath(ensureDataDir(), opts.chain);
3412
+ return { store: new FilePermissionStore(path), path };
3413
+ }
3414
+ function commandPrefix(opts) {
3415
+ return opts.chain === "test" ? "1sat --chain test permissions grant" : "1sat permissions grant";
3416
+ }
3417
+ function grantFlags(args) {
3418
+ return {
3419
+ protocol: extractFlag(args, "--protocol"),
3420
+ level: extractFlag(args, "--level"),
3421
+ counterparty: extractFlag(args, "--counterparty") ?? extractFlag(args, "--verifier"),
3422
+ privileged: hasFlag(args, "--privileged"),
3423
+ basket: extractFlag(args, "--basket"),
3424
+ label: extractFlag(args, "--label"),
3425
+ certificate: extractFlag(args, "--certificate"),
3426
+ fields: extractFlag(args, "--fields"),
3427
+ spending: extractFlag(args, "--spending")
3428
+ };
3429
+ }
3430
+ function firstPositional(args) {
3431
+ for (let i = 0;i < args.length; i++) {
3432
+ const arg = args[i];
3433
+ if (arg.startsWith("--")) {
3434
+ if (!BOOLEAN_FLAGS.has(arg))
3435
+ i++;
3436
+ continue;
3437
+ }
3438
+ return arg;
3439
+ }
3440
+ return;
3441
+ }
3442
+ function toSpec(grant) {
3443
+ return grant.authorizedAmount != null ? { key: grant.key, authorizedAmount: grant.authorizedAmount } : { key: grant.key };
3444
+ }
3445
+ async function permissionsList(args, opts) {
3446
+ const { store, path } = openStore(opts);
3447
+ const origin = firstPositional(args);
3448
+ const filter = origin ? { originator: normalizeOriginator2(origin) } : {};
3449
+ const grants = await store.listGrants(filter);
3450
+ if (opts.json) {
3451
+ output(grants, opts);
3452
+ return;
3453
+ }
3454
+ if (grants.length === 0) {
3455
+ output(origin ? `No grants for ${normalizeOriginator2(origin)} in ${path}` : `No grants in ${path}`, opts);
3456
+ return;
3457
+ }
3458
+ const byOrigin = new Map;
3459
+ for (const grant of grants) {
3460
+ const list = byOrigin.get(grant.key.originator) ?? [];
3461
+ list.push(grant);
3462
+ byOrigin.set(grant.key.originator, list);
3463
+ }
3464
+ for (const name of [...byOrigin.keys()].sort()) {
3465
+ console.log(`
3466
+ ${formatValue(name)}`);
3467
+ for (const grant of byOrigin.get(name) ?? []) {
3468
+ const expiry = grant.expiry > 0 ? ` expires ${new Date(grant.expiry * 1000).toISOString()}` : "";
3469
+ console.log(` ${describeKey(toSpec(grant))}${formatLabel(expiry)}${formatLabel(grant.reason ? ` — ${grant.reason}` : "")}`);
3470
+ }
3471
+ }
3472
+ console.log(`
3473
+ ${grants.length} grant(s) in ${path}`);
3474
+ }
3475
+ async function permissionsGrant(args, opts) {
3476
+ const origin = firstPositional(args);
3477
+ if (!origin)
3478
+ fatal("Missing <origin>. See `1sat permissions --help`");
3479
+ let specs;
3480
+ try {
3481
+ specs = specsFromFlags(origin, grantFlags(args));
3482
+ } catch (err) {
3483
+ return fatal(err.message);
3484
+ }
3485
+ const { store, path } = openStore(opts);
3486
+ const now = Date.now();
3487
+ for (const spec of specs) {
3488
+ await store.putGrant({
3489
+ key: spec.key,
3490
+ expiry: 0,
3491
+ grantedAt: now,
3492
+ ...spec.authorizedAmount != null ? { authorizedAmount: spec.authorizedAmount } : {}
3493
+ });
3494
+ }
3495
+ if (opts.json) {
3496
+ output({
3497
+ granted: specs.map((spec) => ({
3498
+ id: specId(spec),
3499
+ key: spec.key,
3500
+ authorizedAmount: spec.authorizedAmount
3501
+ })),
3502
+ store: path
3503
+ }, opts);
3504
+ return;
3505
+ }
3506
+ for (const spec of specs) {
3507
+ console.log(`granted ${describeKey(spec)}`);
3508
+ }
3509
+ console.log(`
3510
+ ${specs.length} grant(s) written to ${path}. A running \`1sat serve wallet-api\` picks them up on the app's next call.`);
3511
+ }
3512
+ async function permissionsRevoke(args, opts) {
3513
+ const origin = firstPositional(args);
3514
+ if (!origin)
3515
+ fatal("Missing <origin>. See `1sat permissions --help`");
3516
+ const originator = normalizeOriginator2(origin);
3517
+ const { store, path } = openStore(opts);
3518
+ if (hasFlag(args, "--all")) {
3519
+ const removed = await store.deleteAllForOriginator(originator);
3520
+ if (opts.json) {
3521
+ output({ originator, removed, store: path }, opts);
3522
+ return;
3523
+ }
3524
+ output(`revoked ${removed} grant(s) for ${originator}`, opts);
3525
+ return;
3526
+ }
3527
+ let specs;
3528
+ try {
3529
+ specs = specsFromFlags(origin, grantFlags(args));
3530
+ } catch (err) {
3531
+ return fatal(`${err.message}. Pass --all to revoke everything for an origin.`);
3532
+ }
3533
+ const removed = [];
3534
+ const missing = [];
3535
+ for (const spec of specs) {
3536
+ if (await store.findGrant(spec.key)) {
3537
+ await store.deleteGrant(spec.key);
3538
+ removed.push(spec);
3539
+ } else {
3540
+ missing.push(spec);
3541
+ }
3542
+ }
3543
+ if (opts.json) {
3544
+ output({
3545
+ revoked: removed.map((spec) => ({ id: specId(spec), key: spec.key })),
3546
+ notFound: missing.map((spec) => ({ id: specId(spec), key: spec.key })),
3547
+ store: path
3548
+ }, opts);
3549
+ return;
3550
+ }
3551
+ for (const spec of removed)
3552
+ console.log(`revoked ${describeKey(spec)}`);
3553
+ for (const spec of missing) {
3554
+ console.log(`no such grant: ${describeKey(spec)} (grant it with \`${grantCommand(spec, commandPrefix(opts))}\`)`);
3555
+ }
3556
+ console.log(`
3557
+ ${removed.length} grant(s) removed from ${path}`);
3558
+ }
3559
+ var BOOLEAN_FLAGS;
3560
+ var init_permissions = __esm(() => {
3561
+ init_config();
3562
+ init_help();
3563
+ init_output();
3564
+ init_grants();
3565
+ init_permission_store();
3566
+ BOOLEAN_FLAGS = new Set(["--privileged", "--all"]);
3567
+ });
3568
+
3048
3569
  // src/commands/remote.ts
3049
3570
  import { StorageClient } from "@1sat/wallet-node";
3050
3571
  import { WalletServerClient, topUpStorage } from "@1sat/wallet-server";
@@ -3533,8 +4054,135 @@ var init_repricer = __esm(() => {
3533
4054
  init_providers();
3534
4055
  });
3535
4056
 
4057
+ // src/wallet-api/deny.ts
4058
+ import { AsyncLocalStorage } from "node:async_hooks";
4059
+ import {
4060
+ normalizeOriginator as normalizeOriginator3,
4061
+ permissionKeyFromRequest,
4062
+ permissionKeysFromGroup
4063
+ } from "@1sat/wallet";
4064
+ function bindPermissionDenials(manager, options = {}) {
4065
+ const prefix = options.grantCommandPrefix ?? GRANT_COMMAND;
4066
+ const log = options.log ?? ((line) => console.log(line));
4067
+ const record = (origin, commands) => {
4068
+ const message = denialMessage(origin, commands);
4069
+ inFlight.getStore()?.push(message);
4070
+ log(`[wallet-api] ${message}`);
4071
+ };
4072
+ const single = async (request) => {
4073
+ const origin = displayOrigin(request);
4074
+ try {
4075
+ record(origin, [grantCommand(specFromRequest(request), prefix)]);
4076
+ } catch (err) {
4077
+ log(`[wallet-api] could not describe request: ${err.message}`);
4078
+ }
4079
+ await safely(log, "deny", () => manager.denyPermission(request.requestID));
4080
+ };
4081
+ const grouped = async (request) => {
4082
+ const origin = normalizeOriginator3(request.originator);
4083
+ try {
4084
+ record(origin, specsFromGroupedRequest(request).map((spec) => grantCommand(spec, prefix)));
4085
+ } catch (err) {
4086
+ log(`[wallet-api] could not describe request: ${err.message}`);
4087
+ }
4088
+ await safely(log, "deny", () => manager.denyGroupedPermission(request.requestID));
4089
+ };
4090
+ manager.bindCallback("onProtocolPermissionRequested", single);
4091
+ manager.bindCallback("onBasketAccessRequested", single);
4092
+ manager.bindCallback("onCertificateAccessRequested", single);
4093
+ manager.bindCallback("onSpendingAuthorizationRequested", single);
4094
+ manager.bindCallback("onGroupedPermissionRequested", grouped);
4095
+ }
4096
+ async function withDenialMessage(fn) {
4097
+ const messages = [];
4098
+ try {
4099
+ return await inFlight.run(messages, fn);
4100
+ } catch (err) {
4101
+ const code = err?.code;
4102
+ if (code !== DENIED_CODE || messages.length === 0)
4103
+ throw err;
4104
+ const denied = new Error(messages.join(" "));
4105
+ denied.code = DENIED_CODE;
4106
+ throw denied;
4107
+ }
4108
+ }
4109
+ function denialMessage(origin, commands) {
4110
+ const quoted = commands.map((c) => `\`${c}\``);
4111
+ const list = quoted.length > 1 ? `${quoted.slice(0, -1).join(", ")} and ${quoted[quoted.length - 1]}` : quoted[0];
4112
+ return `permission denied for ${origin}: run ${list} and retry`;
4113
+ }
4114
+ function specFromRequest(request) {
4115
+ const key = permissionKeyFromRequest(request);
4116
+ return key.type === "spending" ? { key, authorizedAmount: request.spending?.satoshis ?? 0 } : { key };
4117
+ }
4118
+ function specsFromGroupedRequest(request) {
4119
+ const amount = request.permissions.spendingAuthorization?.amount ?? 0;
4120
+ return permissionKeysFromGroup(request.originator, request.permissions).map((key) => key.type === "spending" ? { key, authorizedAmount: amount } : { key });
4121
+ }
4122
+ function displayOrigin(request) {
4123
+ return normalizeOriginator3(request.originator);
4124
+ }
4125
+ async function safely(log, what, fn) {
4126
+ try {
4127
+ await fn();
4128
+ } catch (err) {
4129
+ log(`[wallet-api] ${what} failed: ${err.message}`);
4130
+ }
4131
+ }
4132
+ var inFlight, DENIED_CODE = "ERR_PERMISSION_DENIED";
4133
+ var init_deny = __esm(() => {
4134
+ init_grants();
4135
+ inFlight = new AsyncLocalStorage;
4136
+ });
4137
+
4138
+ // src/wallet-api/endpoint.ts
4139
+ import { LocalWalletPermissionsManager as LocalWalletPermissionsManager2 } from "@1sat/wallet";
4140
+ import {
4141
+ startBRC100Server
4142
+ } from "@1sat/wallet-server";
4143
+ function walletApiHandle(wallet) {
4144
+ return {
4145
+ async call(method, args, origin) {
4146
+ const fn = wallet[method];
4147
+ if (typeof fn !== "function") {
4148
+ throw new Error(`Method not available: ${method}`);
4149
+ }
4150
+ return withDenialMessage(() => fn.call(wallet, args, origin));
4151
+ }
4152
+ };
4153
+ }
4154
+ async function startWalletApi(options) {
4155
+ const log = options.log ?? ((line) => console.log(line));
4156
+ const manager = new LocalWalletPermissionsManager2(options.wallet, CLI_ADMIN_ORIGINATOR, { ...options.managerConfig }, { store: new FilePermissionStore(options.storePath) });
4157
+ bindPermissionDenials(manager, {
4158
+ grantCommandPrefix: options.grantCommandPrefix,
4159
+ log
4160
+ });
4161
+ const server = await startBRC100Server({
4162
+ host: options.host,
4163
+ port: options.port,
4164
+ adminOriginator: CLI_ADMIN_ORIGINATOR,
4165
+ wallet: walletApiHandle(manager),
4166
+ onEvent: (event) => {
4167
+ if (event.event !== "brc100_call")
4168
+ return;
4169
+ const line = `[wallet-api] ${event.method} from ${event.origin || "(no origin)"} -> ${event.status}`;
4170
+ log(event.error ? `${line}: ${event.error}` : line);
4171
+ }
4172
+ });
4173
+ return {
4174
+ server,
4175
+ close: () => server.close()
4176
+ };
4177
+ }
4178
+ var init_endpoint = __esm(() => {
4179
+ init_admin();
4180
+ init_deny();
4181
+ init_permission_store();
4182
+ });
4183
+
3536
4184
  // src/commands/serve.ts
3537
- import { join as join5 } from "node:path";
4185
+ import { join as join6 } from "node:path";
3538
4186
  import {
3539
4187
  createNodeWallet as createNodeWallet3
3540
4188
  } from "@1sat/wallet-node";
@@ -3543,8 +4191,7 @@ import {
3543
4191
  KnexHandleCertStore,
3544
4192
  KnexPendingStore,
3545
4193
  createHostServer,
3546
- createWalletServer,
3547
- startBRC100Server
4194
+ createWalletServer
3548
4195
  } from "@1sat/wallet-server";
3549
4196
  import { initLogger as initLogger2 } from "evlog";
3550
4197
  import knexLib from "knex";
@@ -3620,7 +4267,7 @@ async function resolveServe(opts) {
3620
4267
  };
3621
4268
  }
3622
4269
  function deriveSqliteFilename(dataDir, chain) {
3623
- return join5(dataDir, `wallet-${chain}.db`);
4270
+ return join6(dataDir, `wallet-${chain}.db`);
3624
4271
  }
3625
4272
  function resolveMonitorEnabled(configured) {
3626
4273
  const fromEnv = process.env.ONESAT_MONITOR;
@@ -3674,7 +4321,7 @@ async function runWithStorage(resolved, mode) {
3674
4321
  skipInitialMonitor: true
3675
4322
  });
3676
4323
  if (mode === "wallet-api") {
3677
- const dapp = await startDappEndpoint(walletResult);
4324
+ const dapp = await startDappEndpoint(walletResult, resolved);
3678
4325
  return {
3679
4326
  stop: async () => {
3680
4327
  await dapp.close();
@@ -3812,7 +4459,7 @@ async function startWalletServer(resolved, walletResult, accounts, mode) {
3812
4459
  }
3813
4460
  function createMessageboxKnex(storage, messagebox, resolved) {
3814
4461
  if (storage.provider === "bun-sqlite") {
3815
- const filename = messagebox?.dbPath ?? join5(resolved.dataDir, `messagebox-${resolved.chain}.db`);
4462
+ const filename = messagebox?.dbPath ?? join6(resolved.dataDir, `messagebox-${resolved.chain}.db`);
3816
4463
  return knexLib({
3817
4464
  client: "sqlite3",
3818
4465
  connection: { filename },
@@ -3870,50 +4517,24 @@ function waitForShutdown() {
3870
4517
  process.once("SIGTERM", () => handler("SIGTERM"));
3871
4518
  });
3872
4519
  }
3873
- function walletApiHandle(wallet) {
3874
- return {
3875
- async call(method, args, origin) {
3876
- const iface = wallet;
3877
- const fn = iface[method];
3878
- if (typeof fn !== "function") {
3879
- throw new Error(`Method not available: ${method}`);
3880
- }
3881
- return await fn.call(iface, args, origin);
3882
- }
3883
- };
3884
- }
3885
- async function startDappEndpoint(walletResult) {
4520
+ async function startDappEndpoint(walletResult, resolved) {
3886
4521
  const config = loadConfig();
3887
4522
  const dapp = config.server?.dapp ?? {};
3888
4523
  const host = dapp.host ?? DEFAULT_DAPP_HOST;
3889
4524
  const port = dapp.port ?? (process.env.ONESAT_DAPP_PORT ? Number(process.env.ONESAT_DAPP_PORT) : DEFAULT_DAPP_PORT);
3890
- const approve = dapp.approve === true;
3891
- const handle = await startBRC100Server({
4525
+ const storePath = permissionStorePath(resolved.dataDir, resolved.chain);
4526
+ const grantCommandPrefix = resolved.chain === "test" ? "1sat --chain test permissions grant" : GRANT_COMMAND;
4527
+ const api = await startWalletApi({
4528
+ wallet: walletResult.wallet,
4529
+ storePath,
3892
4530
  host,
3893
4531
  port,
3894
- wallet: walletApiHandle(walletResult.wallet),
3895
- approvalPolicy: approve ? async ({ method, origin, args }) => {
3896
- const { confirm, isCancel } = await import("@clack/prompts");
3897
- console.log(`
3898
- [brc-100] ${method} requested by ${origin}`);
3899
- console.log(` ${JSON.stringify(args).slice(0, 400)}`);
3900
- const ok = await confirm({
3901
- message: `Approve ${method}?`
3902
- });
3903
- if (isCancel(ok) || !ok) {
3904
- throw new Error("User denied permission");
3905
- }
3906
- } : undefined,
3907
- onEvent: (event) => {
3908
- if (event.event === "brc100_sensitive") {
3909
- const trustNote = event.trusted ? " (trusted)" : approve ? "" : " (auto-approved)";
3910
- console.log(`[brc-100] sensitive ${event.method} from ${event.origin}${trustNote}`);
3911
- }
3912
- }
4532
+ grantCommandPrefix
3913
4533
  });
3914
4534
  console.log(`[wallet-api] BRC-100 app endpoint on http://${host}:${port}`);
3915
- console.log(approve ? "[wallet-api] sensitive methods require TTY confirmation" : "[wallet-api] sensitive methods are AUTO-APPROVED (headless). " + "Set server.dapp.approve=true to require confirmation.");
3916
- return { close: async () => await handle.close() };
4535
+ console.log(`[wallet-api] permission grants: ${storePath}`);
4536
+ console.log("[wallet-api] ungranted requests are denied; the error names the `1sat permissions grant` command that allows them");
4537
+ return { close: () => api.close() };
3917
4538
  }
3918
4539
  var DEFAULT_HOST = "127.0.0.1", DEFAULT_PORT = 8100, DEFAULT_STACK_URL = "https://api.1sat.app", DEFAULT_BASELINE_BYTES, DEFAULT_PURCHASE_UNIT_BYTES = 1073741824, DEFAULT_SATS_PER_UNIT = 1e6, DEFAULT_DURATION_BLOCKS = 4383, DEFAULT_STORAGE_IDENTITY_KEY = "1sat-cli-default", DEFAULT_PG_SCHEMA = "messagebox", DEFAULT_DAPP_HOST = "127.0.0.1", DEFAULT_DAPP_PORT = 3321;
3919
4540
  var init_serve = __esm(() => {
@@ -3924,6 +4545,9 @@ var init_serve = __esm(() => {
3924
4545
  init_monitor_lock();
3925
4546
  init_output();
3926
4547
  init_repricer();
4548
+ init_endpoint();
4549
+ init_grants();
4550
+ init_permission_store();
3927
4551
  DEFAULT_BASELINE_BYTES = 1024 * 1024 * 1024;
3928
4552
  });
3929
4553
 
@@ -3978,7 +4602,7 @@ var init_social = __esm(() => {
3978
4602
  });
3979
4603
 
3980
4604
  // src/commands/storage.ts
3981
- import { join as join6 } from "node:path";
4605
+ import { join as join7 } from "node:path";
3982
4606
  import { StorageBunSqlite, nominateInvalidReqs } from "@1sat/wallet-node";
3983
4607
  import { StorageProvider } from "@bsv/wallet-toolbox";
3984
4608
  async function handleStorageCommand(args, opts) {
@@ -4048,7 +4672,7 @@ async function openServeStorage(opts) {
4048
4672
  } else {
4049
4673
  storage = new StorageBunSqlite({
4050
4674
  ...baseOptions,
4051
- filename: join6(ensureDataDir(), `wallet-${chain}.db`)
4675
+ filename: join7(ensureDataDir(), `wallet-${chain}.db`)
4052
4676
  });
4053
4677
  }
4054
4678
  await storage.makeAvailable();
@@ -5447,13 +6071,13 @@ var init_wallet = __esm(() => {
5447
6071
  });
5448
6072
 
5449
6073
  // src/main.ts
5450
- import { existsSync as existsSync4 } from "node:fs";
6074
+ import { existsSync as existsSync5 } from "node:fs";
5451
6075
  import { resolve } from "node:path";
5452
6076
  import { config as loadEnv } from "dotenv";
5453
6077
  function applyEnvFiles(paths) {
5454
6078
  for (const p of paths) {
5455
6079
  const abs = resolve(p);
5456
- if (!existsSync4(abs)) {
6080
+ if (!existsSync5(abs)) {
5457
6081
  throw new Error(`Env file not found: ${p}`);
5458
6082
  }
5459
6083
  const result = loadEnv({ path: abs, quiet: true, override: true });
@@ -5537,6 +6161,9 @@ async function main() {
5537
6161
  case "serve":
5538
6162
  await handleServeCommand(rest, flags);
5539
6163
  break;
6164
+ case "permissions":
6165
+ await handlePermissionsCommand(rest, flags);
6166
+ break;
5540
6167
  case "storage":
5541
6168
  await handleStorageCommand(rest, flags);
5542
6169
  break;
@@ -5564,6 +6191,7 @@ var init_main = __esm(() => {
5564
6191
  init_messagebox();
5565
6192
  init_opns();
5566
6193
  init_ordinals();
6194
+ init_permissions();
5567
6195
  init_remote();
5568
6196
  init_serve();
5569
6197
  init_social();