@1sat/cli 0.0.119 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@1sat/cli",
3
- "version": "0.0.119",
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.223",
30
+ "@1sat/actions": "0.0.225",
31
31
  "@1sat/client": "0.0.55",
32
32
  "@1sat/types": "0.0.48",
33
- "@1sat/wallet-node": "0.0.78",
34
- "@1sat/wallet-server": "0.0.55",
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
+ }
@@ -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). Headless by design: sensitive methods are auto-approved with a
590
- * console line unless `server.dapp.approve` is set, in which case each one
591
- * must be confirmed on the TTY.
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 approve = dapp.approve === true
606
-
607
- const handle = await startBRC100Server({
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
- wallet: walletApiHandle(walletResult.wallet),
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
- approve
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: async () => await handle.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 (encrypt/decrypt/sign/create-action).
160
- * Defaults: 127.0.0.1:3321, headless auto-approval of sensitive methods.
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
- const ctx = createContext(walletResult.wallet, {
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