@danhachuel/thunderbolt 0.5.0 → 0.5.2

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.
@@ -11,16 +11,36 @@ import requests
11
11
 
12
12
  AUTHORIZE_URL = "https://www.canva.com/api/oauth/authorize"
13
13
  TOKEN_URL = "https://api.canva.com/rest/v1/oauth/token"
14
+ _PKCE_MIN_LENGTH = 43
15
+ _PKCE_MAX_LENGTH = 128
14
16
 
15
17
 
16
18
  def _b64(value: bytes) -> str:
17
19
  return base64.urlsafe_b64encode(value).rstrip(b"=").decode("ascii")
18
20
 
19
21
 
20
- def create_pkce_pair() -> tuple[str, str]:
21
- verifier = _b64(secrets.token_bytes(64))
22
+ def generate_code_verifier() -> str:
23
+ """Create a RFC 7636 verifier using a cryptographically secure source."""
24
+ verifier = _b64(secrets.token_bytes(32))
25
+ if not _PKCE_MIN_LENGTH <= len(verifier) <= _PKCE_MAX_LENGTH:
26
+ raise RuntimeError("Falha ao gerar um code_verifier PKCE válido.")
27
+ return verifier
28
+
29
+
30
+ def generate_code_challenge(code_verifier: str) -> str:
31
+ """Derive the S256 Base64URL-without-padding PKCE challenge."""
32
+ verifier = str(code_verifier or "")
33
+ if not _PKCE_MIN_LENGTH <= len(verifier) <= _PKCE_MAX_LENGTH:
34
+ raise ValueError("code_verifier PKCE inválido.")
22
35
  challenge = _b64(hashlib.sha256(verifier.encode("ascii")).digest())
23
- return verifier, challenge
36
+ if challenge == "<CODE_CHALLENGE>":
37
+ raise RuntimeError("Foi detectado um placeholder de code_challenge.")
38
+ return challenge
39
+
40
+
41
+ def create_pkce_pair() -> tuple[str, str]:
42
+ verifier = generate_code_verifier()
43
+ return verifier, generate_code_challenge(verifier)
24
44
 
25
45
 
26
46
  def create_state() -> str:
@@ -28,6 +48,8 @@ def create_state() -> str:
28
48
 
29
49
 
30
50
  def authorization_url(client_id: str, redirect_uri: str, scope: str, state: str, code_challenge: str) -> str:
51
+ if not code_challenge or code_challenge == "<CODE_CHALLENGE>":
52
+ raise ValueError("Não é permitido iniciar Canva com code_challenge placeholder.")
31
53
  params = {
32
54
  "code_challenge": code_challenge,
33
55
  "code_challenge_method": "S256",
@@ -120,11 +120,7 @@ def _safe_metadata(value: Any, *, key: str = "") -> Any:
120
120
 
121
121
 
122
122
  def _history() -> list[dict[str, Any]]:
123
- try:
124
- saved = storage.read_json(NOTIFICATIONS_FILE, [])
125
- except PermissionError:
126
- # Um lock ou ficheiro sem permissões não deve interromper a interface.
127
- return []
123
+ saved = storage.read_json(NOTIFICATIONS_FILE, [])
128
124
  if not isinstance(saved, list):
129
125
  return []
130
126
  return [item for item in saved if isinstance(item, dict)]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danhachuel/thunderbolt",
3
- "version": "0.5.0",
3
+ "version": "0.5.2",
4
4
  "description": "Thunderbolt — interface local para operação de canais faceless e motor MoneyPrinterTurbo",
5
5
  "license": "MIT",
6
6
  "main": "scripts/cli.mjs",
@@ -16,6 +16,7 @@
16
16
  "integrations/*.py",
17
17
  "integrations/data/*.json",
18
18
  "scripts/*.mjs",
19
+ "scripts/*.py",
19
20
  "seed/prompt_masters/**/*.md",
20
21
  "seed/blueprints/**/*.json",
21
22
  "seed/blueprints/**/*.md",
package/scripts/cli.mjs CHANGED
@@ -25,6 +25,7 @@ const venvDir = process.env.THUNDERBOLT_VENV || process.env.HERMES_VENV || join(
25
25
  const venvPython = platform() === "win32" ? join(venvDir, "Scripts", "python.exe") : join(venvDir, "bin", "python");
26
26
  const python = process.env.THUNDERBOLT_PYTHON || process.env.HERMES_PYTHON || (existsSync(venvPython) ? venvPython : (platform() === "win32" ? "python" : "python3"));
27
27
  const main = resolve(root, "app", "main.py");
28
+ const streamlitBootstrap = resolve(root, "scripts", "streamlit_bootstrap.py");
28
29
  const settingsPath = join(thunderboltHome, "storage", "state", "settings.json");
29
30
  const pythonEnvironment = { ...process.env, PYTHONIOENCODING: "utf-8", PYTHONUTF8: "1", PYTHONLEGACYWINDOWSSTDIO: "1", CLICK_NO_WIN_CONSOLE: "1" };
30
31
 
@@ -265,7 +266,7 @@ let shuttingDown = false;
265
266
  let child;
266
267
 
267
268
  function startStreamlit() {
268
- child = spawn(python, ["-m", "streamlit", "run", main, "--server.port", String(backendPort), "--server.address", "127.0.0.1"], {
269
+ child = spawn(python, [streamlitBootstrap, "run", main, "--server.port", String(backendPort), "--server.address", "127.0.0.1"], {
269
270
  cwd: root,
270
271
  stdio: "inherit",
271
272
  env: { ...runtimeEnv, PYTHONIOENCODING: "utf-8", PYTHONUTF8: "1", PYTHONLEGACYWINDOWSSTDIO: "1", CLICK_NO_WIN_CONSOLE: "1", THUNDERBOLT_LAUNCHER_RESTART: "1" },
@@ -0,0 +1,34 @@
1
+ from __future__ import annotations
2
+
3
+ import io
4
+ import os
5
+ import sys
6
+
7
+ # Este ficheiro é o primeiro módulo Python executado pelo launcher. As
8
+ # variáveis e os streams precisam de ser corrigidos antes de qualquer import
9
+ # de streamlit/click, pois o erro utf-16-le acontece durante essa inicialização.
10
+ os.environ["PYTHONIOENCODING"] = "utf-8"
11
+ os.environ["PYTHONUTF8"] = "1"
12
+ os.environ["PYTHONLEGACYWINDOWSSTDIO"] = "1"
13
+ os.environ["CLICK_NO_WIN_CONSOLE"] = "1"
14
+
15
+
16
+ def _utf8_stream(stream: object) -> object:
17
+ if os.name != "nt":
18
+ return stream
19
+ buffer = getattr(stream, "buffer", None)
20
+ if buffer is None:
21
+ return stream
22
+ try:
23
+ return io.TextIOWrapper(buffer, encoding="utf-8", errors="replace", line_buffering=True)
24
+ except (AttributeError, OSError, ValueError):
25
+ return stream
26
+
27
+
28
+ sys.stdout = _utf8_stream(sys.stdout)
29
+ sys.stderr = _utf8_stream(sys.stderr)
30
+
31
+ from streamlit.web.cli import main # noqa: E402
32
+
33
+ if __name__ == "__main__":
34
+ main()
@@ -1,112 +0,0 @@
1
- # Guia de Configuração: API Keys da Canva para o Thunderbolt
2
-
3
- Este guia explica como criar e configurar uma integração no **Canva Developer Portal** para obter as credenciais (`Client ID` e `Client Secret`) usadas pelo módulo de geração de thumbnails do Thunderbolt.
4
-
5
- ## Pré-requisitos
6
-
7
- É necessária uma conta Canva activa, gratuita ou paga, com **autenticação de dois factores (MFA)** activada. Sem MFA, o Developer Portal não permite criar a integração.
8
-
9
- ## 1. Aceder ao Developer Portal
10
-
11
- 1. Abra o [Canva Developer Portal](https://www.canva.com/developers/) e inicie sessão.
12
- 2. No menu superior, abra **Your integrations** ou aceda directamente a [https://www.canva.com/developers/integrations](https://www.canva.com/developers/integrations).
13
-
14
- ## 2. Criar uma integração
15
-
16
- 1. Clique em **Create an integration**.
17
- 2. Escolha **Public** ou **Private**. A opção Public requer revisão da Canva; a opção Private está disponível apenas para equipas Canva Enterprise. Para testes, use Private quando tiver acesso ao Canva Enterprise; caso contrário, use uma integração Public em modo de desenvolvimento.
18
- 3. Aceite os **Canva Developer Terms** e clique em **Create integration**.
19
-
20
- ## 3. Obter o Client ID e o Client Secret
21
-
22
- Na configuração da integração, defina um nome como `Thunderbolt Thumbnail Generator`, copie o **Client ID** e clique em **Generate secret** para criar o **Client Secret**. Guarde o secret imediatamente num local seguro, pois a Canva não o volta a apresentar.
23
-
24
- > Nunca publique estas credenciais, nem as inclua em commits, screenshots ou mensagens. Se forem expostas, revogue-as e gere um novo secret.
25
-
26
- ## 4. Configurar os scopes
27
-
28
- No menu **Scopes**, active apenas as permissões necessárias:
29
-
30
- | Scope | Acesso | Finalidade |
31
- | --- | --- | --- |
32
- | `design:content` | Read and Write | Criar, editar e exportar designs |
33
- | `design:meta` | Read | Ler metadados dos designs |
34
- | `asset:content` | Read and Write | Gerir imagens, fontes e outros assets |
35
- | `profile` | Read | Ler informações básicas do perfil |
36
-
37
- Para Brand Templates com Autofill, pode activar opcionalmente `brandtemplate:meta` (Read) e `brandtemplate:content` (Read). Integrações públicas devem justificar cada scope durante a revisão.
38
-
39
- ## 5. Configurar as URLs de redireccionamento
40
-
41
- A Canva Connect API usa OAuth 2.0 com PKCE. Em **Authentication > Authorized redirects**, adicione exactamente:
42
-
43
- ```text
44
- http://127.0.0.1:3001/oauth/redirect
45
- ```
46
-
47
- Para activar o retorno de navegação, habilite **Enable return navigation** e use:
48
-
49
- ```text
50
- http://127.0.0.1:3001/return-nav
51
- ```
52
-
53
- É possível adicionar até dez URLs. O protocolo, host, porta e caminho devem coincidir exactamente com os valores usados pelo Thunderbolt.
54
-
55
- ## 6. Configurar as credenciais no Thunderbolt
56
-
57
- Na raiz do projecto, localize ou crie o ficheiro `.env` e adicione:
58
-
59
- ```dotenv
60
- CANVA_CLIENT_ID=seu_client_id_aqui
61
- CANVA_CLIENT_SECRET=seu_client_secret_aqui
62
- ```
63
-
64
- O `.env` já deve estar protegido pelo `.gitignore`. Não faça commit deste ficheiro.
65
-
66
- ## 7. Configurar um template de thumbnails (opcional)
67
-
68
- Se utilizar a skill `bulk-create` com templates predefinidos, adicione também:
69
-
70
- ```dotenv
71
- CANVA_THUMBNAIL_TEMPLATE_ID=id_do_template_canva
72
- ```
73
-
74
- O ID pode ser obtido na URL do design no Canva ou através de `GET /v1/brand-templates`.
75
-
76
- ## 8. Testar a integração
77
-
78
- 1. Inicie o Thunderbolt.
79
- 2. Abra **Canva Skills** no Streamlit.
80
- 3. Seleccione uma skill, como **Resize for Social Media**.
81
- 4. Introduza um `design_id` válido e execute a operação.
82
- 5. Autorize o Thunderbolt quando o fluxo OAuth 2.0 com PKCE o redireccionar para a Canva.
83
-
84
- Após a autorização, o Thunderbolt recebe e armazena o token necessário para executar as operações.
85
-
86
- ## Dados necessários
87
-
88
- | Variável | Onde encontrar | Exemplo |
89
- | --- | --- | --- |
90
- | `CANVA_CLIENT_ID` | Developer Portal → integração → Credentials | `1234567890abcdef` |
91
- | `CANVA_CLIENT_SECRET` | Gerado no Developer Portal; guardar imediatamente | `ghi_jklmnopqrstuvwxyz123456` |
92
- | `CANVA_THUMBNAIL_TEMPLATE_ID` | URL do template ou `GET /v1/brand-templates` | `DAF1234567890` |
93
-
94
- ## Diagnóstico rápido
95
-
96
- | Problema | Solução |
97
- | --- | --- |
98
- | **MFA required** | Active a autenticação de dois factores nas definições da conta Canva. |
99
- | Erro 401 ou token expirado | Verifique o armazenamento do `refresh_token`; o Thunderbolt implementa refresh automático. |
100
- | Erro 429 | Aguarde e tente novamente; a API aplica limites de frequência. |
101
- | Design não encontrado | Confirme o `design_id`; designs criados via API e não editados durante sete dias podem ser eliminados permanentemente. |
102
- | URL de redireccionamento inválida | Compare a URL do Developer Portal com a usada pelo Thunderbolt, incluindo protocolo e porta. |
103
-
104
- ## Referências
105
-
106
- - [Canva Developer Portal](https://www.canva.com/developers/)
107
- - [Criar integrações](https://www.canva.dev/docs/connect/creating-integrations/)
108
- - [Autenticação OAuth 2.0 com PKCE](https://www.canva.dev/docs/connect/authentication/)
109
- - [Scopes disponíveis](https://www.canva.dev/docs/connect/appendix/scopes/)
110
- - [Canva Connect API Starter Kit](https://github.com/canva-sdks/canva-connect-api-starter-kit)
111
-
112
- Depois de configurar as credenciais, é possível testar a geração de thumbnails na interface Streamlit, integrar a operação ao callback `on_video_completed` e ajustar as dimensões das plataformas em `app/modules/canva_skills/resize.py`.