Consentimento
Ao terminar esta página você sabe o que um app alcança no filesystem da pessoa, quem concede esse alcance, onde a concessão mora, o que ela protege e o que ela não protege. O mesmo modelo governa o cofre de segredos, a impressão e os diálogos: o app pede, o shell mostra, e a pessoa decide.
Escolher é consentir
O modelo é o da File System Access API do W3C. O app não recebe a home da pessoa; ele recebe o que
ela escolheu num seletor. Um showDirectoryPicker() abre o gerenciador de arquivos do ambiente
em modo de escolha; a pasta escolhida passa a ser alcançável pelo app, e só ela, com o que há
dentro. Não há segunda confirmação: a escolha é o consentimento.
Abrir um arquivo com o app também é escolher. O duplo-clique no gerenciador de arquivos, o "Abrir
com" e um item que o app declarou no menu do arquivo entregam o caminho pelo evento abertura, e o
shell concede aquele arquivo antes de entregá-lo, como o seletor faria. Um editor aberto num
documento grava nele sem perguntar de novo onde. A concessão cobre o arquivo, e a pasta dele
continua fora. Uma abertura de pasta (tipo: 'pasta') não concede nada, porque ali a pessoa
escolheu onde o app começa.
Cada operação de arquivo do app atravessa a ponte e é conferida pelo shell contra a lista de
concessões (os grants) antes de chegar à API do portal. O app nunca fala com /api/fs/* direto.
Uma operação com dois caminhos (renomear, copiar) confere os dois: sem isso, um renomear
levaria um arquivo da pessoa para fora do que ela autorizou, e um copiar traria para dentro algo
que o app não podia ler, e as duas operações teriam sucesso sem erro nenhum. A recusa é um 403
que nomeia o caminho reprovado.
Onde o grant mora
O grant é do app, por servidor, e mora nas preferências da pessoa (appGrants, em
/api/user/settings). Ele viaja com ela: o mesmo app no mesmo servidor, aberto de outro computador,
encontra a concessão feita. O teto é de 64 caminhos por app; quem excede isso quase sempre quer um
caminho só, a raiz.
O handle da File System Access API é outra coisa: o polyfill o guarda no IndexedDB do navegador,
que é por perfil e não viaja. Daí a terceira resposta que um app precisa tratar: num computador
novo ele acorda com a permissão concedida e sem handle nenhum. Não é granted (não há por onde
ler) nem denied (ninguém negou). O app reconstrói o handle a partir do caminho concedido, sem
abrir seletor, porque a escolha já foi feita uma vez. O guia arquivos com
consentimento mostra o código.
Duas precisões sobre o que o shell responde:
queryPermission()responde de verdade, consultando o shell.requestPermission()reabre o seletor, e só a partir de um gesto da pessoa; sem gesto ele devolve'prompt'sem abrir nada, que é a regra do navegador;{ mode: 'read' | 'readwrite' }é aceito e repassado, e todo grant de hoje éreadwrite. Pedirreade recebergrantedestá correto.
A pessoa revoga em Permissões de arquivo, no menu de contexto da janela do app. 'denied' é um
estado normal, e um app o trata como tal.
O que o grant protege
O grant mantém o app dentro do que a pessoa escolheu e transforma um erro de programação numa
mensagem clara. Ele não é uma fronteira contra um app hostil: na web o app roda num iframe de
mesma origem que o portal, e o JS dele alcança /api/* com o cookie de sessão, com ou sem a tabela
de grants. A fronteira de segurança é outra, e é a instalação: um admin instalou o app, como root,
num servidor compartilhado, e por isso ele é confiável. O backend do app já lê qualquer arquivo que
a pessoa lê, porque roda como a conta dela.
Numa plataforma aberta como hiperlink o grant pesa mais. A plataforma mora em outra origem e não roda na estação, então os arquivos da pessoa chegam a ela pela ponte, e só o que o seletor concedeu passa. Abrir, abrir com, arrastar, copiar e imprimir pedem o mesmo grant, e a área de transferência de arquivos fica fora do alcance dela.
O que o ambiente ganha com o grant é uniformidade. Todo app pede do mesmo jeito, a pessoa concede
no mesmo seletor, revoga no mesmo lugar e vê a mesma lista. Um app que inventasse o próprio
caminho para a home (um campo de texto pedindo /home/ana/dados) teria o mesmo alcance e nenhuma
dessas propriedades.
O mesmo modelo, em três outros lugares
O cofre de segredos: o manifesto declara de que credenciais o app precisa; o valor nunca vem do
manifesto, e o gate de publicação recusa um value ali. Quem guarda é a pessoa, pelo shell, e o
valor mora em ~/.vssh-apps/<id>/secrets.json (modo 0600) no servidor dela. O portal escreve e
esquece: não há coluna de segredo no portal, e a tela nunca relê o valor. O app pede pelo nome
(vssh.segredos), o shell mostra o campo, e o valor chega ao backend como variável de ambiente
na próxima subida. Um backend que já estava de pé precisa reiniciar, porque o ambiente de um
processo é fixado no spawn, e o shell diz isso ao app.
A impressão: vssh.impressao abre a tela de impressão do ambiente com o arquivo pedido. Quem
escolhe a impressora e confirma é a pessoa, com o nome do arquivo na tela. A chamada resolve
quando a tela abre, e não quando ela imprime.
Os diálogos: vssh.dialogos desenha a pergunta com a aparência do ambiente, fora do iframe. Um
modal desenhado dentro do app fica preso no iframe e não bloqueia o resto da janela; o do shell
bloqueia a janela inteira e devolve a resposta.
O que não passa pelo consentimento, e por quê
O clipboard de texto e imagem é a API padrão do navegador, direto: o iframe recebe
allow="clipboard-read; clipboard-write", e clipboard.write() exige ativação transitória da
pessoa, que não atravessa postMessage. Mediar pelo shell quebraria o que a mediação existiria
para permitir. O clipboard de arquivos, que é do gerenciador de arquivos, passa pela ponte.
O arraste de arquivos nas duas direções não declara nada no manifesto: o app é servido na mesma
origem, então o drop cai no documento dele e o dataTransfer é legível direto. O backend já lê
o que a pessoa lê; não haveria o que guardar.