@softize/opus 24.0.1 → 24.1.0

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/CHANGELOG.md CHANGED
@@ -7,6 +7,16 @@ Depois de qualquer bump, rode os gates (`typecheck` · `test` · `opus check` ·
7
7
  `opus copy --check` · `base copy check` · `manifest:check`) — eles apontam o que a
8
8
  mudança cobra do seu código.
9
9
 
10
+ ## 24.1.0 — 2026-10-02
11
+
12
+ O `Markdown` deixa de carregar imagem com endereço absoluto (`https:`, `http:` ou `//host`). No
13
+ lugar dela fica o texto alternativo, num `span` com `data-slot="markdown-external-image"`, sem link.
14
+ O conteúdo vem de modelo, e um `<img>` é buscado pelo navegador sem clique: um modelo induzido por
15
+ injeção de prompt poderia levar dados da conversa no endereço da imagem (ADR 0050). Imagem com
16
+ endereço relativo e as imagens `data:` que o markdown-it já aceitava continuam renderizando. A
17
+ regra vale para a instância compartilhada, então chat, painéis e documentação mudam juntos. Quem
18
+ exibia imagem absoluta da própria origem passa a usar o endereço relativo.
19
+
10
20
  ## 24.0.1 — 2026-10-01
11
21
 
12
22
  Em `DialogHeader` e `DrawerHeader` com navegação e título, os dois passam a formar um grupo com
@@ -0,0 +1,64 @@
1
+ # ADR 0050 — Markdown não carrega imagem externa
2
+
3
+ - **Status:** aceita.
4
+ - **Data:** 2026-10-02.
5
+ - **Complementa:** a decisão registrada em `markdown.tsx` de renderizar com `html: false`, porque o
6
+ conteúdo vem de pessoas e de modelos.
7
+
8
+ ## Contexto
9
+
10
+ O `Markdown` é o renderer único do pacote: o chat, os painéis e a documentação usam a mesma
11
+ instância do markdown-it. Ele já escapa HTML do fonte e o markdown-it já recusa links
12
+ `javascript:` e `data:text/html`. Faltava uma terceira via: `![](https://host/x.png?d=…)` virava um
13
+ `<img>` que o navegador busca sozinho, sem nenhum clique.
14
+
15
+ Isso importa porque boa parte desse conteúdo é escrita por um modelo. Um modelo induzido por
16
+ injeção de prompt (um documento, uma resposta de ferramenta, uma mensagem de terceiro) pode montar
17
+ o endereço de uma imagem com dados da conversa e deixar que o navegador de quem lê os entregue ao
18
+ host. Num relatório salvo, isso acontece também com quem abre o relatório depois.
19
+
20
+ Nenhum consumidor conhecido depende de imagem externa no Markdown: a documentação do pacote não usa
21
+ imagem em Markdown, e a plataforma Grand Brasil não tem imagem externa em documentos, Base de
22
+ conhecimento nem relatórios.
23
+
24
+ ## Decisão
25
+
26
+ 1. O `Markdown` não renderiza `<img>` para endereço absoluto, com esquema (`https:`, `http:`) ou
27
+ relativo ao protocolo (`//host`). No lugar da imagem fica o texto alternativo dela, num
28
+ `span` com `data-slot="markdown-external-image"`, sem link: seguir o endereço exigiria um
29
+ clique, mas um link montado pelo modelo não deve ser oferecido como se fosse da imagem.
30
+ 2. Endereço relativo (`/caminho`, `./arquivo.png`) continua renderizando a imagem, porque é servido
31
+ pela própria aplicação. As imagens em `data:` que o markdown-it já aceita (`png`, `gif`, `jpeg`,
32
+ `webp`) também continuam: não fazem requisição.
33
+ 3. A regra vale para a instância compartilhada `markdownIt`, então chat, painéis e documentação
34
+ seguem o mesmo comportamento. Não há opção para liberar imagem externa; um consumidor que
35
+ precise disso traz o caso concreto para uma decisão nova.
36
+
37
+ ## Consequências
38
+
39
+ - Uma resposta ou um relatório com imagem externa mostra o texto alternativo no lugar da imagem.
40
+ - A defesa não depende de cada aplicação configurar uma Content Security Policy com `img-src`,
41
+ embora uma CSP continue sendo uma segunda camada recomendada.
42
+ - Imagem absoluta da própria origem também deixa de carregar. Não há uso conhecido; se aparecer,
43
+ o caminho é usar o endereço relativo.
44
+ - A imagem relativa continua fazendo uma requisição à própria origem, com os cookies de quem lê. Se
45
+ a aplicação tiver um redirecionamento aberto ou um proxy de imagem, o mesmo vetor volta por ali.
46
+ É mais um motivo para manter a CSP com `img-src` como segunda camada.
47
+
48
+ ## Alternativas consideradas
49
+
50
+ - **Bloquear toda imagem.** Fecharia também o resíduo da imagem relativa, mas tiraria imagens que a
51
+ própria aplicação serve (anexos, ilustrações de documentação) sem um vetor conhecido que o
52
+ justifique hoje.
53
+ - **Transformar a imagem externa em link.** Mantém o endereço acessível, mas oferece com aparência
54
+ legítima um link montado pelo modelo; o texto alternativo basta para a leitura.
55
+ - **Lista de origens permitidas ou proxy de imagem.** Exigiriam configuração por aplicação e um
56
+ serviço novo para um uso que não existe. Ficam para quando um consumidor trouxer o caso.
57
+ - **Só CSP na aplicação.** Depende de cada consumidor configurar e não vale fora do navegador da
58
+ aplicação (por exemplo, na documentação publicada). Fica como segunda camada, não como a defesa.
59
+
60
+ ## Verificação
61
+
62
+ - Teste do `Markdown`: imagem `https:`, `//host`, com maiúsculas, com esquema em entidade, por
63
+ referência, dentro de link e em célula de tabela não geram `<img>` e mostram o texto alternativo;
64
+ imagem relativa e `data:image/png` continuam gerando `<img>`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@softize/opus",
3
- "version": "24.0.1",
3
+ "version": "24.1.0",
4
4
  "description": "End-to-end action protocol for TypeScript. Single package with subpath exports (core + adapters).",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -17,6 +17,23 @@ import MarkdownIt from 'markdown-it'
17
17
  * opções próprias divergiriam no primeiro edge case, e "mesmo motor" viraria promessa. */
18
18
  export const markdownIt = new MarkdownIt({ html: false, linkify: true })
19
19
 
20
+ /**
21
+ * Imagem com endereço absoluto não carrega (ADR 0050). O conteúdo vem de modelo, e um `<img>` é
22
+ * buscado pelo navegador sem clique: um modelo induzido por injeção de prompt poderia levar dados
23
+ * da conversa no endereço. Fica o texto alternativo; imagem relativa e `data:` seguem normais.
24
+ */
25
+ const EXTERNAL_IMAGE = /^(?:[a-z][a-z0-9+.-]*:|\/\/)/i
26
+ const renderImage = markdownIt.renderer.rules.image
27
+ markdownIt.renderer.rules.image = (tokens, idx, options, env, self) => {
28
+ const token = tokens[idx]!
29
+ const src = token.attrGet('src') ?? ''
30
+ if (EXTERNAL_IMAGE.test(src) && !src.toLowerCase().startsWith('data:')) {
31
+ const alt = self.renderInlineAsText(token.children ?? [], options, env)
32
+ return `<span data-slot="markdown-external-image">${markdownIt.utils.escapeHtml(alt)}</span>`
33
+ }
34
+ return renderImage!(tokens, idx, options, env, self)
35
+ }
36
+
20
37
  export interface MarkdownProps {
21
38
  content: string
22
39
  className?: string