Segunda camada
A API, o formato, a cadeia
Tudo de que um comprovante é feito, e cada rota que cria ou serve um. Alguém te entregou um comprovante e você quer desmontá-lo sem a nossa ajuda: é para isso que esta página existe, e desmontá-lo sem a nossa ajuda é o projeto inteiro.
O acesso, com franqueza
Não há chave de API, nem cadastro, nem plano que libere um limite maior. Toda rota abaixo é pública ou protegida por um token que pertence a uma sessão ou a um comprovante e é entregue a quem a iniciou.
As rotas que carregam um documento ou o dinheiro recusam uma requisição cujo cabeçalho Origin nomeie um host que não é o nosso. Uma requisição que não manda cabeçalho Origin nenhum —um servidor chamando outro servidor— não é recusada, e preferimos dizer isso aqui a que alguém descubra e suponha que estava escondido. Não é uma superfície de integração suportada: não tem promessa de versionamento, nem chave para girar, nem limite de requisições pensado para ela, e pode mudar em qualquer implantação.
Se você quiser construir em cima disto a sério, escreva e diga. Uma superfície suportada e versionada, com chaves, é algo que construiríamos para alguém real e que não construímos para alguém hipotético.
Rotas
Toda resposta é JSON. As rotas de sessão são no-store e noindex; um token bem formado cuja sessão terminou responde 404 com expired, que é coisa diferente de um token que nunca existiu.
A tabela abaixo é o próprio protocolo e é publicada só em inglês.
| Método | Rota | O que faz | Responde |
|---|---|---|---|
| POST | /api/check | Start a check. Multipart with a file, or JSON {text, ref}. A ref of word-win, word-mac or word-web over a posted string records that the caller assembled the string out of an open document, and the receipt then names the add-in's extractor rather than the file reader's. — quem: same-origin | 202 {token, expires_at, arm} |
| GET | /api/check/:token | The session as it stands: every claim found, and each verdict as it lands. This is what the page polls while a check runs. — quem: the token | the session view |
| GET | /api/check/:token/status | The completion signal, read off the database row rather than out of memory — so a deploy in the middle of a payment does not tell somebody who has paid that their session is gone. — quem: the token | {receipt_id, sealed, checked_out, paid, closed, session} |
| GET | /api/check/:token/preview.json | The unsigned, unchained receipt-shaped preview of a check that has finished but has not been sealed. — quem: the token | a receipt-shaped object with no signature |
| GET | /api/check/:token/binding.json | The binding bundle: what turns the per-claim fingerprints in a receipt back into the claims they were computed from. — quem: the token | a downloaded file |
| POST | /api/check/:token/holds | Run the adversarial third level over one claim. Off by default, never sealed into a receipt, and it sends the sentence you wrote. — quem: the token | 202 |
| DELETE | /api/check/:token | End the session now and drop the preview. The session expires on its own either way. — quem: the token | {deleted: true} |
| POST | /api/checkout | Turn a finished check into an invoice. {token, email, tier}. — quem: same-origin | a payment URL |
| GET | /api/receipt/:id | The public projection of a sealed receipt: every cited URL removed, every verdict, count and hash kept, signed in its own right. | the stored canonical bytes |
| GET | /api/receipt/:id/proof | The inclusion proof pointing at the day's Merkle root. 202 while that day has not been rooted yet. | an audit path |
| GET | /r/:id/receipt.json | The full sealed receipt, cited URLs and all. This is the creator's file, and the reason the projection exists. — quem: the creator's link | the sealed bytes |
| GET | /r/:id/receipt.pdf | The printable receipt: the page somebody staples to a report and is asked about under oath. — quem: the creator's link | a PDF |
| GET | /r/:id/qr.png | The receipt's QR as a raster, for a printed page. | a PNG |
| GET | /chain/head | Where the chain is now. | {seq, self_hash, issued_at, last_root} |
| GET | /chain/root/:date | One UTC day's signed root file, exactly as it was stored and published. | the stored canonical bytes |
| GET | /chain/verify | Our own replay of the chain, and the first break in it if there is one. Rooted days are re-derived on every call; the answer is memoised for sixty seconds. | {ok, checked, head_seq, first_broken, …} |
| GET | /keys.json | Every public key this install has ever signed with, so a verifier can pin them rather than trust the key a receipt carries about itself. | the key set |
| GET | /healthz | Whether the service is up. | {ok} |
Uma rota de comprovante nunca confirma um identificador que ela não tem, e uma rota de administração sem o token dela responde byte a byte igual a um caminho que não tem rota nenhuma: mesmo status, mesmos cabeçalhos, mesmo corpo. É deliberado: a diferença entre as duas respostas seria, do contrário, um mapa do que existe.
Um servidor para agentes de IA
Ainda não existe, e este é o único lugar onde o anunciaríamos. A mudança que reordenou este produto colocou um servidor voltado a modelos na mesma segunda camada das rotas acima, e a posição honesta hoje é que aquelas rotas são toda a superfície para máquinas: uma conferência é um POST, o resultado dela é um GET e um comprovante é uma URL pública. Quando um servidor para agentes for construído, ele vai embrulhar justamente isso e mais nada, e esta seção vai dizê-lo com o nome e o transporte dentro. Até lá não há o que instalar nem o que configurar.
A assinatura
Um comprovante é JSON canônico. O self_hash dele é o SHA-256 dos bytes canônicos do comprovante com dois membros de primeiro nível removidos, self_hash e signatures, de modo que o hash vive fora de si mesmo e as assinaturas vivem fora do corpo sobre o qual o hash é calculado. É isso que permite contra-assinar um comprovante mais tarde sem mover o hash dele.
A assinatura é Ed25519 sobre os 32 bytes brutos obtidos ao decodificar self_hash de hexadecimal. Não sobre os 64 caracteres hexadecimais, nem sobre o JSON canônico, nem sobre um hash do hash: o Ed25519 aplica o próprio hash à entrada, então assinar o resumo diretamente é o esquema inteiro.
Todo valor base64 no formato é canônico: alfabeto padrão, preenchimento completo, sem espaços e com os bits baixos não usados do último símbolo em zero, de modo que uma chave tem uma única escrita e duas chaves podem ser comparadas como bytes ou como texto com a mesma resposta. Um decodificador que tolerasse um preenchimento ausente ou o alfabeto seguro para URL leria os mesmos bytes de várias escritas, e este formato admite uma.
Um comprovante carrega uma única assinatura cujo papel é issuer. Zero diz que ele nunca foi emitido; duas ou mais dizem que ele não consegue declarar por quem; as duas coisas são falhas. A assinatura é verificada contra a chave publicada para o identificador de chave dela, nunca contra a chave que o comprovante diz de si mesmo: um comprovante não pode responder por si.
A cadeia e as raízes
Há uma cadeia só. seq é densa e começa em 1, e prev_hash em seq n é o self_hash de seq n−1. Os dois ficam dentro do corpo assinado, então a posição de um comprovante é assinada junto com ele: mover um comprovante, ou reescrever qualquer coisa antes dele, muda o hash dele.
Os comprovantes encadeados de cada dia UTC formam uma árvore de Merkle, com o hash calculado tal como o RFC 6962 define: as folhas com um byte zero na frente e os nós interiores com um, de modo que uma folha nunca possa passar por nó. A raiz do dia é assinada com uma chave de raiz separada e publicada em um repositório público que não dá para editarmos em silêncio. Um dia vazio também ganha seu arquivo, então uma falha na série ficaria visível de fora.
A prova de inclusão de um comprovante é um caminho de auditoria até uma dessas raízes. Um comprovante selado hoje não tem prova até a raiz desta noite ser cortada, e a rota da prova diz pending em vez de inventar uma.
O que um comprovante público não carrega
O que quem abre o link de um comprovante vê é uma projeção: cada URL citada removida, cada localizador reduzido a {type, domain} e o identificador próprio do registro de decisões descartado junto, porque o conjunto desses identificadores seria a tabela de autoridades do documento. Todo o resto fica byte a byte, hashes e assinaturas inclusos, então quem tiver o comprovante completo consegue amarrar os dois. Uma projeção é assinada por si, com um membro a mais que o comprovante selado nunca tem, de modo que toda a superfície visível dela —veredictos, contagens, estados, domínios— fica coberta por uma assinatura, e não só pelo hash do comprovante.
O verificador
O verificador é licenciado sob MIT e está espelhado em um serviço público de código. Ele não faz requisição própria nenhuma: verificar é calcular hashes e conferir assinaturas sobre bytes que você já tem, que é por isso que o contador ao lado dele na página de verificação pode marcar zero com honestidade.
Ele roda aqui, no seu navegador, como um módulo compilado servido a partir deste site. Carregue a página de verificação uma vez, desconecte, e ela continua verificando: essa é a conferência a fazer antes de confiar em qualquer coisa disto, e é a conferência a refazer se um dia sumirmos.
A partir de um terminal
As mesmas conferências rodam a partir da linha de comando contra um comprovante no seu disco. O pacote ainda não está no registro público, então npx exhibitb hoje não resolve e não vamos imprimi-lo como se resolvesse. O que existe agora é a build para navegador acima, que é o mesmo verificador, e o código no serviço público, que compila para os dois. Quando o pacote for publicado, ele será nomeado no registro de mudanças no dia em que isso acontecer, não antes.
O que o suplemento envia
Um comprovante registra qual extrator produziu o texto que ele descreve, e esse campo não vale nada se um desconhecido com o mesmo documento não conseguir reconstruir a mesma cadeia de texto e obter o mesmo hash. Esta é essa especificação. É o contrato publicado por trás do rótulo exhibitb-word@1, e é por isso que um comprovante feito dentro de um documento pode ser amarrado ao documento.
A especificação abaixo é o próprio contrato publicado e é impressa só em inglês, para que uma reconstrução contra ela seja idêntica byte a byte em todo lugar.
exhibitb-word@1
exhibitb-word@1 — the string a receipt from the add-in describes
A receipt names the extractor that produced the text it was made from. When the check ran from inside Word, the text was assembled in the open document rather than parsed out of an uploaded file, and this is how. Rebuild the string from your own copy of the document, hash it, and it matches the text_sha256 in the receipt — or it does not, and you have learned something.
A ordem em que a cadeia de texto é construída
- Take the paragraphs of the document's main story, in the order Word lays them out. Paragraphs inside a table are part of that order: a table is read row by row, and each row cell by cell in reading order, at the point where the table sits.
- Then every footnote body, in footnote-number order.
- Then every endnote body, in endnote-number order.
- A paragraph's text is its own characters. A hyperlink contributes the words the reader sees; the address it points to is not inserted.
- The three groups are concatenated in that order — body, then footnotes, then endnotes — with nothing between them but the same separator that joins paragraphs.
O que as junta
Every paragraph, footnote body and endnote body is joined to the next by two line feeds (U+000A twice), which is one blank line.
O que nunca está dentro
- the .docx file itself — no part of the container is uploaded
- headers and footers
- comments, and the identity of whoever wrote them
- tracked changes: deleted-but-unaccepted text is not sent, insertions are sent as the paragraph reads with them applied, and no revision author is sent
- styles, fonts, numbering, colour and every other piece of formatting
- images, charts, embedded objects and their captions' pictures (a caption's own words are a paragraph like any other)
- the file name, the folder it sits in, and every piece of document metadata
- fields, in favour of the result they display
A limpeza, aplicada por último e a mais nada
- carriage returns become line feeds: and a lone both become
- control characters are removed (U+0000–U+0008, U+000B, U+000C, U+000E–U+001F, U+007F)
- soft hyphens (U+00AD) are removed
- spaces and tabs immediately before a line feed are removed
- three or more consecutive line feeds collapse to two
- leading and trailing whitespace is removed from the whole string
Conferindo a sua própria reconstrução
The cleaning is the same eight lines the uploaded-file path runs, and it is idempotent: running it twice changes nothing, so a rebuild that applies it once is comparable byte for byte. SHA-256 of the resulting UTF-8 bytes is the receipt's document.text_sha256; document.chars is that string's length in code units.
Por que os dois extratores contam diferente
The two paths do not read the same amount of a document, and the label is how a reader tells them apart. exhibitb-word@1 reads footnotes and endnotes. exhibitb-text@1 over an uploaded .docx reads the main story only, so a footnote-heavy report checked as a file will show fewer citations than the same report checked from inside Word. Neither number is wrong; they are counts of different strings, and the receipt says which one it made.
exhibitb-text@1
O outro rótulo. Um .docx enviado é lido por uma biblioteca que percorre os filhos do próprio documento, que são o corpo principal e as tabelas dentro dele; as notas de rodapé e as notas de fim ficam em outro lugar do contêiner e não são lidas. Todo o resto sobre a cadeia de texto é igual, inclusive a limpeza abaixo e o hash sobre ela.
O formato por inteiro
A forma canônica, o esquema do comprovante membro a membro, as regras da cadeia, a construção de Merkle, o arquivo da raiz, a prova de inclusão e a regra de projeção estão especificados em um documento só, com vetores de teste ao lado: o mesmo arquivo, byte a byte, que vai junto no repositório do próprio verificador. Todo número nele é um número que um terceiro consegue reproduzir.
Vetores de teste
- the canonical form of an object, with the worked examples that pin the whitespace and the number formatting
- self-hash vectors, including an object carrying a wrong self-hash and a nonsense signature array hashing to what it hashes to without them
- a signed receipt, its key set and its verification result
- Merkle roots for trees of 0, 1, 2, 3 and 7 leaves, with every audit path
Se algo nesta página estiver errado, ou se uma conferência que você escreveu contra ela falhar e você achar que não deveria, o endereço de suporte no rodapé chega a uma pessoa que vai olhar os bytes com você.