@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 +689 -61
- package/package.json +5 -4
- package/src/commands/permissions.ts +269 -0
- package/src/commands/serve.ts +25 -62
- package/src/config.ts +5 -4
- package/src/context.ts +21 -1
- package/src/help.ts +82 -1
- package/src/main.ts +5 -0
- package/src/wallet-api/admin.ts +107 -0
- package/src/wallet-api/deny.ts +194 -0
- package/src/wallet-api/endpoint.ts +108 -0
- package/src/wallet-api/grants.ts +238 -0
- package/src/wallet-api/permission-store.ts +106 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@1sat/cli",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.121",
|
|
4
4
|
"description": "CLI for 1Sat Ordinals SDK",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/cli.js",
|
|
@@ -27,11 +27,12 @@
|
|
|
27
27
|
],
|
|
28
28
|
"license": "MIT",
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@1sat/actions": "0.0.
|
|
30
|
+
"@1sat/actions": "0.0.225",
|
|
31
31
|
"@1sat/client": "0.0.55",
|
|
32
32
|
"@1sat/types": "0.0.48",
|
|
33
|
-
"@1sat/wallet
|
|
34
|
-
"@1sat/wallet-
|
|
33
|
+
"@1sat/wallet": "0.0.112",
|
|
34
|
+
"@1sat/wallet-node": "0.0.79",
|
|
35
|
+
"@1sat/wallet-server": "0.0.56",
|
|
35
36
|
"@bopen-io/messagebox-server": "^1.3.2",
|
|
36
37
|
"@bsv/sdk": "^2.6.0",
|
|
37
38
|
"@bsv/wallet-toolbox": "npm:@bopen-io/wallet-toolbox@2.6.2-brc153.5",
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `1sat permissions` — what apps on `1sat serve wallet-api` may do.
|
|
3
|
+
*
|
|
4
|
+
* The endpoint never asks: a request it has no grant for is denied, and
|
|
5
|
+
* the error names the `1sat permissions grant …` command that would allow
|
|
6
|
+
* it. This command is the other half of that loop.
|
|
7
|
+
*
|
|
8
|
+
* Everything here works directly on the JSON grant store
|
|
9
|
+
* (`<dataDir>/permissions-<chain>.json`), so it needs no key, no wallet
|
|
10
|
+
* and no running server. The manager reads that file on every check, so a
|
|
11
|
+
* grant written while `1sat serve wallet-api` is running takes effect on
|
|
12
|
+
* the app's next call — no restart.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { normalizeOriginator } from '@1sat/wallet'
|
|
16
|
+
import type { StoredGrant } from '@1sat/wallet'
|
|
17
|
+
import type { GlobalFlags } from '../args.js'
|
|
18
|
+
import { extractFlag, hasFlag } from '../args.js'
|
|
19
|
+
import { ensureDataDir } from '../config.js'
|
|
20
|
+
import { printCommandHelp } from '../help.js'
|
|
21
|
+
import { fatal, formatLabel, formatValue, output } from '../output.js'
|
|
22
|
+
import {
|
|
23
|
+
type GrantFlags,
|
|
24
|
+
type GrantSpec,
|
|
25
|
+
describeKey,
|
|
26
|
+
grantCommand,
|
|
27
|
+
specId,
|
|
28
|
+
specsFromFlags,
|
|
29
|
+
} from '../wallet-api/grants.js'
|
|
30
|
+
import {
|
|
31
|
+
FilePermissionStore,
|
|
32
|
+
permissionStorePath,
|
|
33
|
+
} from '../wallet-api/permission-store.js'
|
|
34
|
+
|
|
35
|
+
export async function handlePermissionsCommand(
|
|
36
|
+
args: string[],
|
|
37
|
+
opts: GlobalFlags,
|
|
38
|
+
): Promise<void> {
|
|
39
|
+
const [subcommand, ...rest] = args
|
|
40
|
+
|
|
41
|
+
switch (subcommand) {
|
|
42
|
+
case 'list':
|
|
43
|
+
case 'ls': // deprecated alias
|
|
44
|
+
return permissionsList(rest, opts)
|
|
45
|
+
case 'grant':
|
|
46
|
+
return permissionsGrant(rest, opts)
|
|
47
|
+
case 'revoke':
|
|
48
|
+
return permissionsRevoke(rest, opts)
|
|
49
|
+
default:
|
|
50
|
+
printCommandHelp('permissions', opts.json)
|
|
51
|
+
if (subcommand && subcommand !== 'help') {
|
|
52
|
+
process.exit(1)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** The store `1sat serve wallet-api` reads on this chain. */
|
|
58
|
+
function openStore(opts: GlobalFlags): {
|
|
59
|
+
store: FilePermissionStore
|
|
60
|
+
path: string
|
|
61
|
+
} {
|
|
62
|
+
const path = permissionStorePath(ensureDataDir(), opts.chain)
|
|
63
|
+
return { store: new FilePermissionStore(path), path }
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** `1sat permissions grant`, with the global flags this invocation used. */
|
|
67
|
+
function commandPrefix(opts: GlobalFlags): string {
|
|
68
|
+
return opts.chain === 'test'
|
|
69
|
+
? '1sat --chain test permissions grant'
|
|
70
|
+
: '1sat permissions grant'
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function grantFlags(args: string[]): GrantFlags {
|
|
74
|
+
return {
|
|
75
|
+
protocol: extractFlag(args, '--protocol'),
|
|
76
|
+
level: extractFlag(args, '--level'),
|
|
77
|
+
counterparty:
|
|
78
|
+
extractFlag(args, '--counterparty') ?? extractFlag(args, '--verifier'),
|
|
79
|
+
privileged: hasFlag(args, '--privileged'),
|
|
80
|
+
basket: extractFlag(args, '--basket'),
|
|
81
|
+
label: extractFlag(args, '--label'),
|
|
82
|
+
certificate: extractFlag(args, '--certificate'),
|
|
83
|
+
fields: extractFlag(args, '--fields'),
|
|
84
|
+
spending: extractFlag(args, '--spending'),
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Flags of this command that take no value. */
|
|
89
|
+
const BOOLEAN_FLAGS = new Set(['--privileged', '--all'])
|
|
90
|
+
|
|
91
|
+
/** First non-flag argument, skipping the values flags consume. */
|
|
92
|
+
function firstPositional(args: string[]): string | undefined {
|
|
93
|
+
for (let i = 0; i < args.length; i++) {
|
|
94
|
+
const arg = args[i]
|
|
95
|
+
if (arg.startsWith('--')) {
|
|
96
|
+
if (!BOOLEAN_FLAGS.has(arg)) i++
|
|
97
|
+
continue
|
|
98
|
+
}
|
|
99
|
+
return arg
|
|
100
|
+
}
|
|
101
|
+
return undefined
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function toSpec(grant: StoredGrant): GrantSpec {
|
|
105
|
+
return grant.authorizedAmount != null
|
|
106
|
+
? { key: grant.key, authorizedAmount: grant.authorizedAmount }
|
|
107
|
+
: { key: grant.key }
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function permissionsList(
|
|
111
|
+
args: string[],
|
|
112
|
+
opts: GlobalFlags,
|
|
113
|
+
): Promise<void> {
|
|
114
|
+
const { store, path } = openStore(opts)
|
|
115
|
+
const origin = firstPositional(args)
|
|
116
|
+
const filter = origin ? { originator: normalizeOriginator(origin) } : {}
|
|
117
|
+
const grants = await store.listGrants(filter)
|
|
118
|
+
|
|
119
|
+
if (opts.json) {
|
|
120
|
+
output(grants, opts)
|
|
121
|
+
return
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (grants.length === 0) {
|
|
125
|
+
output(
|
|
126
|
+
origin
|
|
127
|
+
? `No grants for ${normalizeOriginator(origin)} in ${path}`
|
|
128
|
+
: `No grants in ${path}`,
|
|
129
|
+
opts,
|
|
130
|
+
)
|
|
131
|
+
return
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const byOrigin = new Map<string, StoredGrant[]>()
|
|
135
|
+
for (const grant of grants) {
|
|
136
|
+
const list = byOrigin.get(grant.key.originator) ?? []
|
|
137
|
+
list.push(grant)
|
|
138
|
+
byOrigin.set(grant.key.originator, list)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
for (const name of [...byOrigin.keys()].sort()) {
|
|
142
|
+
console.log(`\n${formatValue(name)}`)
|
|
143
|
+
for (const grant of byOrigin.get(name) ?? []) {
|
|
144
|
+
const expiry =
|
|
145
|
+
grant.expiry > 0
|
|
146
|
+
? ` expires ${new Date(grant.expiry * 1000).toISOString()}`
|
|
147
|
+
: ''
|
|
148
|
+
console.log(
|
|
149
|
+
` ${describeKey(toSpec(grant))}${formatLabel(expiry)}${formatLabel(
|
|
150
|
+
grant.reason ? ` — ${grant.reason}` : '',
|
|
151
|
+
)}`,
|
|
152
|
+
)
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
console.log(`\n ${grants.length} grant(s) in ${path}`)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async function permissionsGrant(
|
|
159
|
+
args: string[],
|
|
160
|
+
opts: GlobalFlags,
|
|
161
|
+
): Promise<void> {
|
|
162
|
+
const origin = firstPositional(args)
|
|
163
|
+
if (!origin) fatal('Missing <origin>. See `1sat permissions --help`')
|
|
164
|
+
|
|
165
|
+
let specs: GrantSpec[]
|
|
166
|
+
try {
|
|
167
|
+
specs = specsFromFlags(origin, grantFlags(args))
|
|
168
|
+
} catch (err) {
|
|
169
|
+
return fatal((err as Error).message)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const { store, path } = openStore(opts)
|
|
173
|
+
const now = Date.now()
|
|
174
|
+
for (const spec of specs) {
|
|
175
|
+
await store.putGrant({
|
|
176
|
+
key: spec.key,
|
|
177
|
+
expiry: 0,
|
|
178
|
+
grantedAt: now,
|
|
179
|
+
...(spec.authorizedAmount != null
|
|
180
|
+
? { authorizedAmount: spec.authorizedAmount }
|
|
181
|
+
: {}),
|
|
182
|
+
})
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (opts.json) {
|
|
186
|
+
output(
|
|
187
|
+
{
|
|
188
|
+
granted: specs.map((spec) => ({
|
|
189
|
+
id: specId(spec),
|
|
190
|
+
key: spec.key,
|
|
191
|
+
authorizedAmount: spec.authorizedAmount,
|
|
192
|
+
})),
|
|
193
|
+
store: path,
|
|
194
|
+
},
|
|
195
|
+
opts,
|
|
196
|
+
)
|
|
197
|
+
return
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
for (const spec of specs) {
|
|
201
|
+
console.log(`granted ${describeKey(spec)}`)
|
|
202
|
+
}
|
|
203
|
+
console.log(
|
|
204
|
+
`\n ${specs.length} grant(s) written to ${path}. A running \`1sat serve wallet-api\` picks them up on the app's next call.`,
|
|
205
|
+
)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
async function permissionsRevoke(
|
|
209
|
+
args: string[],
|
|
210
|
+
opts: GlobalFlags,
|
|
211
|
+
): Promise<void> {
|
|
212
|
+
const origin = firstPositional(args)
|
|
213
|
+
if (!origin) fatal('Missing <origin>. See `1sat permissions --help`')
|
|
214
|
+
const originator = normalizeOriginator(origin)
|
|
215
|
+
const { store, path } = openStore(opts)
|
|
216
|
+
|
|
217
|
+
if (hasFlag(args, '--all')) {
|
|
218
|
+
const removed = await store.deleteAllForOriginator(originator)
|
|
219
|
+
if (opts.json) {
|
|
220
|
+
output({ originator, removed, store: path }, opts)
|
|
221
|
+
return
|
|
222
|
+
}
|
|
223
|
+
output(`revoked ${removed} grant(s) for ${originator}`, opts)
|
|
224
|
+
return
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
let specs: GrantSpec[]
|
|
228
|
+
try {
|
|
229
|
+
specs = specsFromFlags(origin, grantFlags(args))
|
|
230
|
+
} catch (err) {
|
|
231
|
+
return fatal(
|
|
232
|
+
`${(err as Error).message}. Pass --all to revoke everything for an origin.`,
|
|
233
|
+
)
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const removed: GrantSpec[] = []
|
|
237
|
+
const missing: GrantSpec[] = []
|
|
238
|
+
for (const spec of specs) {
|
|
239
|
+
if (await store.findGrant(spec.key)) {
|
|
240
|
+
await store.deleteGrant(spec.key)
|
|
241
|
+
removed.push(spec)
|
|
242
|
+
} else {
|
|
243
|
+
missing.push(spec)
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (opts.json) {
|
|
248
|
+
output(
|
|
249
|
+
{
|
|
250
|
+
revoked: removed.map((spec) => ({ id: specId(spec), key: spec.key })),
|
|
251
|
+
notFound: missing.map((spec) => ({ id: specId(spec), key: spec.key })),
|
|
252
|
+
store: path,
|
|
253
|
+
},
|
|
254
|
+
opts,
|
|
255
|
+
)
|
|
256
|
+
return
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
for (const spec of removed) console.log(`revoked ${describeKey(spec)}`)
|
|
260
|
+
for (const spec of missing) {
|
|
261
|
+
console.log(
|
|
262
|
+
`no such grant: ${describeKey(spec)} (grant it with \`${grantCommand(
|
|
263
|
+
spec,
|
|
264
|
+
commandPrefix(opts),
|
|
265
|
+
)}\`)`,
|
|
266
|
+
)
|
|
267
|
+
}
|
|
268
|
+
console.log(`\n ${removed.length} grant(s) removed from ${path}`)
|
|
269
|
+
}
|
package/src/commands/serve.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* 1sat serve Host server (storage + accounts + paymail + messagebox) + monitor
|
|
5
5
|
* 1sat serve wallet Wallet storage server only
|
|
6
6
|
* 1sat serve monitor Monitor daemon only
|
|
7
|
+
* 1sat serve wallet-api App-facing BRC-100 endpoint (headless; grants come from `1sat permissions`)
|
|
7
8
|
*
|
|
8
9
|
* The server wraps the same wallet instance the CLI uses. Storage, active
|
|
9
10
|
* remote, and backups all come from `~/.1sat/cli/config.json` via the same
|
|
@@ -25,15 +26,12 @@ import {
|
|
|
25
26
|
createNodeWallet,
|
|
26
27
|
} from '@1sat/wallet-node'
|
|
27
28
|
import {
|
|
28
|
-
type BRC100WalletHandle,
|
|
29
29
|
KnexAccountStore,
|
|
30
30
|
KnexHandleCertStore,
|
|
31
31
|
KnexPendingStore,
|
|
32
32
|
createHostServer,
|
|
33
33
|
createWalletServer,
|
|
34
|
-
startBRC100Server,
|
|
35
34
|
} from '@1sat/wallet-server'
|
|
36
|
-
import type { Wallet, WalletInterface } from '@bsv/sdk'
|
|
37
35
|
import type { PrivateKey } from '@bsv/sdk'
|
|
38
36
|
import { initLogger } from 'evlog'
|
|
39
37
|
import knexLib from 'knex'
|
|
@@ -57,6 +55,9 @@ import {
|
|
|
57
55
|
createAccountsConfigLoader,
|
|
58
56
|
resolveRateProvider,
|
|
59
57
|
} from '../repricer/index.js'
|
|
58
|
+
import { startWalletApi } from '../wallet-api/endpoint.js'
|
|
59
|
+
import { GRANT_COMMAND } from '../wallet-api/grants.js'
|
|
60
|
+
import { permissionStorePath } from '../wallet-api/permission-store.js'
|
|
60
61
|
|
|
61
62
|
const DEFAULT_HOST = '127.0.0.1'
|
|
62
63
|
const DEFAULT_PORT = 8100
|
|
@@ -276,7 +277,7 @@ async function runWithStorage(
|
|
|
276
277
|
// wallet-api serves only the app-facing BRC-100 endpoint: no storage
|
|
277
278
|
// server, no accounts, no monitor.
|
|
278
279
|
if (mode === 'wallet-api') {
|
|
279
|
-
const dapp = await startDappEndpoint(walletResult)
|
|
280
|
+
const dapp = await startDappEndpoint(walletResult, resolved)
|
|
280
281
|
return {
|
|
281
282
|
stop: async () => {
|
|
282
283
|
await dapp.close()
|
|
@@ -562,36 +563,18 @@ function waitForShutdown(): Promise<void> {
|
|
|
562
563
|
const DEFAULT_DAPP_HOST = '127.0.0.1'
|
|
563
564
|
const DEFAULT_DAPP_PORT = 3321
|
|
564
565
|
|
|
565
|
-
/**
|
|
566
|
-
* Wrap the CLI's wallet (wallet-toolbox client) as a BRC100WalletHandle.
|
|
567
|
-
* The toolbox Wallet already implements the 28 BRC-100 methods with the
|
|
568
|
-
* `(args, originator)` signature the dApps send.
|
|
569
|
-
*/
|
|
570
|
-
function walletApiHandle(wallet: Wallet): BRC100WalletHandle {
|
|
571
|
-
return {
|
|
572
|
-
async call(method, args, origin) {
|
|
573
|
-
const iface = wallet as unknown as WalletInterface
|
|
574
|
-
const fn = (iface as unknown as Record<string, unknown>)[method] as (
|
|
575
|
-
a: unknown,
|
|
576
|
-
o: string,
|
|
577
|
-
) => Promise<unknown>
|
|
578
|
-
if (typeof fn !== 'function') {
|
|
579
|
-
throw new Error(`Method not available: ${method}`)
|
|
580
|
-
}
|
|
581
|
-
return await fn.call(iface, args, origin)
|
|
582
|
-
},
|
|
583
|
-
}
|
|
584
|
-
}
|
|
585
|
-
|
|
586
566
|
/**
|
|
587
567
|
* Start the app-facing BRC-100 HTTP endpoint (the interface dApps like
|
|
588
568
|
* wallet-desktop's :3321 server speak; see bitplan and other BRC-100
|
|
589
|
-
* clients).
|
|
590
|
-
*
|
|
591
|
-
*
|
|
569
|
+
* clients). See `../wallet-api/endpoint.ts` for what is served: a
|
|
570
|
+
* permissions manager around the CLI wallet, grants kept in
|
|
571
|
+
* `<dataDir>/permissions-<chain>.json`, and every ungranted request denied
|
|
572
|
+
* on the spot with the `1sat permissions grant` command that would allow
|
|
573
|
+
* it. Nothing is ever asked on this terminal.
|
|
592
574
|
*/
|
|
593
575
|
async function startDappEndpoint(
|
|
594
576
|
walletResult: NodeWalletResult,
|
|
577
|
+
resolved: ResolvedServe,
|
|
595
578
|
): Promise<{ close: () => Promise<void> }> {
|
|
596
579
|
const config = loadConfig()
|
|
597
580
|
const dapp = config.server?.dapp ?? {}
|
|
@@ -602,46 +585,26 @@ async function startDappEndpoint(
|
|
|
602
585
|
? Number(process.env.ONESAT_DAPP_PORT)
|
|
603
586
|
: DEFAULT_DAPP_PORT)
|
|
604
587
|
|
|
605
|
-
const
|
|
606
|
-
|
|
607
|
-
|
|
588
|
+
const storePath = permissionStorePath(resolved.dataDir, resolved.chain)
|
|
589
|
+
// Denials print the command that would allow them, so it has to carry
|
|
590
|
+
// the chain when this is not the default one.
|
|
591
|
+
const grantCommandPrefix =
|
|
592
|
+
resolved.chain === 'test'
|
|
593
|
+
? '1sat --chain test permissions grant'
|
|
594
|
+
: GRANT_COMMAND
|
|
595
|
+
const api = await startWalletApi({
|
|
596
|
+
wallet: walletResult.wallet,
|
|
597
|
+
storePath,
|
|
608
598
|
host,
|
|
609
599
|
port,
|
|
610
|
-
|
|
611
|
-
approvalPolicy: approve
|
|
612
|
-
? async ({ method, origin, args }) => {
|
|
613
|
-
const { confirm, isCancel } = await import('@clack/prompts')
|
|
614
|
-
console.log(`\n[brc-100] ${method} requested by ${origin}`)
|
|
615
|
-
console.log(` ${JSON.stringify(args).slice(0, 400)}`)
|
|
616
|
-
const ok = await confirm({
|
|
617
|
-
message: `Approve ${method}?`,
|
|
618
|
-
})
|
|
619
|
-
if (isCancel(ok) || !ok) {
|
|
620
|
-
throw new Error('User denied permission')
|
|
621
|
-
}
|
|
622
|
-
}
|
|
623
|
-
: undefined,
|
|
624
|
-
onEvent: (event) => {
|
|
625
|
-
if (event.event === 'brc100_sensitive') {
|
|
626
|
-
const trustNote = event.trusted
|
|
627
|
-
? ' (trusted)'
|
|
628
|
-
: approve
|
|
629
|
-
? ''
|
|
630
|
-
: ' (auto-approved)'
|
|
631
|
-
console.log(
|
|
632
|
-
`[brc-100] sensitive ${event.method} from ${event.origin}${trustNote}`,
|
|
633
|
-
)
|
|
634
|
-
}
|
|
635
|
-
},
|
|
600
|
+
grantCommandPrefix,
|
|
636
601
|
})
|
|
637
602
|
|
|
638
603
|
console.log(`[wallet-api] BRC-100 app endpoint on http://${host}:${port}`)
|
|
604
|
+
console.log(`[wallet-api] permission grants: ${storePath}`)
|
|
639
605
|
console.log(
|
|
640
|
-
|
|
641
|
-
? '[wallet-api] sensitive methods require TTY confirmation'
|
|
642
|
-
: '[wallet-api] sensitive methods are AUTO-APPROVED (headless). ' +
|
|
643
|
-
'Set server.dapp.approve=true to require confirmation.',
|
|
606
|
+
'[wallet-api] ungranted requests are denied; the error names the `1sat permissions grant` command that allows them',
|
|
644
607
|
)
|
|
645
608
|
|
|
646
|
-
return { close:
|
|
609
|
+
return { close: () => api.close() }
|
|
647
610
|
}
|
package/src/config.ts
CHANGED
|
@@ -156,14 +156,15 @@ export interface ServerConfig {
|
|
|
156
156
|
messagebox?: ServerMessageboxConfig
|
|
157
157
|
/**
|
|
158
158
|
* Application-facing BRC-100 endpoint (`1sat serve wallet-api`):
|
|
159
|
-
* dApp connectivity for the CLI wallet
|
|
160
|
-
*
|
|
159
|
+
* dApp connectivity for the CLI wallet. Defaults to 127.0.0.1:3321.
|
|
160
|
+
* Each app origin is limited to the permissions granted to it with
|
|
161
|
+
* `1sat permissions grant`; anything else is denied on the spot. The
|
|
162
|
+
* endpoint never prompts and there is no setting that approves
|
|
163
|
+
* without a grant.
|
|
161
164
|
*/
|
|
162
165
|
dapp?: {
|
|
163
166
|
host?: string
|
|
164
167
|
port?: number
|
|
165
|
-
/** Require TTY confirmation for sensitive methods. Default false. */
|
|
166
|
-
approve?: boolean
|
|
167
168
|
}
|
|
168
169
|
/** Paymail settings for the unified host. */
|
|
169
170
|
paymail?: ServerPaymailConfig
|
package/src/context.ts
CHANGED
|
@@ -5,6 +5,11 @@
|
|
|
5
5
|
* not run in-process (avoids toolbox/console noise on the TTY). After the
|
|
6
6
|
* wallet is destroyed, a detached `__monitor-once` child may run against the
|
|
7
7
|
* same storage with logs in <dataDir>/monitor.log.
|
|
8
|
+
*
|
|
9
|
+
* The context's wallet is the permissions manager called as the CLI's admin
|
|
10
|
+
* originator, not the raw toolbox wallet: checks are bypassed, transaction
|
|
11
|
+
* metadata is encrypted at rest and decrypted on the way back. See
|
|
12
|
+
* `wallet-api/admin.ts`.
|
|
8
13
|
*/
|
|
9
14
|
|
|
10
15
|
import { type OneSatContext, createContext } from '@1sat/actions'
|
|
@@ -12,6 +17,11 @@ import { type NodeWalletResult, createNodeWallet } from '@1sat/wallet-node'
|
|
|
12
17
|
import type { PrivateKey } from '@bsv/sdk'
|
|
13
18
|
import { ensureDataDir, loadConfig } from './config.js'
|
|
14
19
|
import { spawnDetachedMonitorOnce } from './monitor-once.js'
|
|
20
|
+
import { adminWallet } from './wallet-api/admin.js'
|
|
21
|
+
import {
|
|
22
|
+
FilePermissionStore,
|
|
23
|
+
permissionStorePath,
|
|
24
|
+
} from './wallet-api/permission-store.js'
|
|
15
25
|
|
|
16
26
|
/** Extended context that includes cleanup */
|
|
17
27
|
export interface CliContext {
|
|
@@ -52,7 +62,17 @@ export async function loadContext(
|
|
|
52
62
|
skipInitialMonitor: true,
|
|
53
63
|
})
|
|
54
64
|
|
|
55
|
-
|
|
65
|
+
// Commands talk to the wallet through the permissions manager as the
|
|
66
|
+
// admin originator: checks are bypassed, and transaction metadata is
|
|
67
|
+
// encrypted and decrypted the same way the served endpoint does it. The
|
|
68
|
+
// store is the same file `1sat permissions` and the endpoint use, so
|
|
69
|
+
// nothing here can disagree with what the endpoint enforces.
|
|
70
|
+
const wallet = adminWallet(
|
|
71
|
+
walletResult.wallet,
|
|
72
|
+
new FilePermissionStore(permissionStorePath(dataDir, opts.chain)),
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
const ctx = createContext(wallet, {
|
|
56
76
|
services: walletResult.services,
|
|
57
77
|
chain: opts.chain,
|
|
58
78
|
dataDir,
|
package/src/help.ts
CHANGED
|
@@ -757,7 +757,88 @@ export const COMMANDS: CommandSpec[] = [
|
|
|
757
757
|
{
|
|
758
758
|
name: 'wallet-api',
|
|
759
759
|
description:
|
|
760
|
-
'App-facing BRC-100 wallet endpoint only (headless, default port 3321)',
|
|
760
|
+
'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',
|
|
761
|
+
},
|
|
762
|
+
],
|
|
763
|
+
},
|
|
764
|
+
{
|
|
765
|
+
group: 'Server',
|
|
766
|
+
name: 'permissions',
|
|
767
|
+
description:
|
|
768
|
+
"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.",
|
|
769
|
+
subcommands: [
|
|
770
|
+
{
|
|
771
|
+
name: 'list',
|
|
772
|
+
description: 'Show grants, grouped by app origin',
|
|
773
|
+
positional: '[<origin>]',
|
|
774
|
+
},
|
|
775
|
+
{
|
|
776
|
+
name: 'grant',
|
|
777
|
+
description:
|
|
778
|
+
'Allow an app one or more permissions. Selectors may be combined in one call.',
|
|
779
|
+
positional: '<origin>',
|
|
780
|
+
args: [
|
|
781
|
+
{
|
|
782
|
+
flag: '--protocol',
|
|
783
|
+
values: '<name>',
|
|
784
|
+
description: 'Protocol name; requires --level',
|
|
785
|
+
},
|
|
786
|
+
{
|
|
787
|
+
flag: '--level',
|
|
788
|
+
values: '<0|1|2>',
|
|
789
|
+
description: 'Protocol security level',
|
|
790
|
+
},
|
|
791
|
+
{
|
|
792
|
+
flag: '--counterparty',
|
|
793
|
+
values: '<hex|self|anyone>',
|
|
794
|
+
description:
|
|
795
|
+
"Counterparty for a level 0/2 protocol (default self; ignored at level 1), or a certificate's verifier (also --verifier)",
|
|
796
|
+
},
|
|
797
|
+
{
|
|
798
|
+
flag: '--privileged',
|
|
799
|
+
description:
|
|
800
|
+
'Grant the privileged variant of a protocol/certificate',
|
|
801
|
+
},
|
|
802
|
+
{ flag: '--basket', values: '<name>', description: 'Basket access' },
|
|
803
|
+
{
|
|
804
|
+
flag: '--label',
|
|
805
|
+
values: '<name>',
|
|
806
|
+
description:
|
|
807
|
+
'Action label (a level-1 `action label <name>` protocol)',
|
|
808
|
+
},
|
|
809
|
+
{
|
|
810
|
+
flag: '--certificate',
|
|
811
|
+
values: '<type>',
|
|
812
|
+
description: 'Certificate type; requires --fields',
|
|
813
|
+
},
|
|
814
|
+
{
|
|
815
|
+
flag: '--fields',
|
|
816
|
+
values: '<a,b>',
|
|
817
|
+
description: 'Certificate fields that may be disclosed',
|
|
818
|
+
},
|
|
819
|
+
{
|
|
820
|
+
flag: '--spending',
|
|
821
|
+
values: '<satoshis>',
|
|
822
|
+
description:
|
|
823
|
+
'Monthly spending cap; month-to-date spend is read from action history on every check',
|
|
824
|
+
},
|
|
825
|
+
],
|
|
826
|
+
},
|
|
827
|
+
{
|
|
828
|
+
name: 'revoke',
|
|
829
|
+
description: 'Remove matching grants, or every grant for an origin',
|
|
830
|
+
positional: '<origin>',
|
|
831
|
+
args: [
|
|
832
|
+
{
|
|
833
|
+
flag: '--all',
|
|
834
|
+
description: 'Remove every grant for the origin',
|
|
835
|
+
},
|
|
836
|
+
{
|
|
837
|
+
flag: '--protocol',
|
|
838
|
+
values: '<name>',
|
|
839
|
+
description: 'Same selectors as `grant`',
|
|
840
|
+
},
|
|
841
|
+
],
|
|
761
842
|
},
|
|
762
843
|
],
|
|
763
844
|
},
|
package/src/main.ts
CHANGED
|
@@ -21,6 +21,7 @@ import { handleMcpProxyCommand } from './commands/mcp-proxy.js'
|
|
|
21
21
|
import { handleMessageboxCommand } from './commands/messagebox.js'
|
|
22
22
|
import { handleOpnsCommand } from './commands/opns.js'
|
|
23
23
|
import { handleOrdinalsCommand } from './commands/ordinals.js'
|
|
24
|
+
import { handlePermissionsCommand } from './commands/permissions.js'
|
|
24
25
|
import { handleRemoteCommand } from './commands/remote.js'
|
|
25
26
|
import { handleServeCommand } from './commands/serve.js'
|
|
26
27
|
import { handleSocialCommand } from './commands/social.js'
|
|
@@ -151,6 +152,10 @@ async function main(): Promise<void> {
|
|
|
151
152
|
await handleServeCommand(rest, flags)
|
|
152
153
|
break
|
|
153
154
|
|
|
155
|
+
case 'permissions':
|
|
156
|
+
await handlePermissionsCommand(rest, flags)
|
|
157
|
+
break
|
|
158
|
+
|
|
154
159
|
case 'storage':
|
|
155
160
|
await handleStorageCommand(rest, flags)
|
|
156
161
|
break
|