fix(ntn-files): propagate NOTION_API_KEY into ntn subprocesses
Some checks failed
Verify Skills / verify-skills (push) Has been cancelled
Some checks failed
Verify Skills / verify-skills (push) Has been cancelled
Force ntn to authenticate as the same integration that writes the page by injecting NOTION_API_TOKEN=$NOTION_API_KEY into every ntn subprocess env. Notion scopes file uploads to the creating integration, so a mismatch caused "Could not find file_upload with ID …" when attaching uploaded images. Added test_upload_passes_token_env (9 total in test_ntn_files.py) and updated code/CLAUDE.md to reflect that a separate ntn login is no longer required. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -195,9 +195,8 @@ Standalone local-image lines (``) are auto-uploaded to Notion
|
||||
|
||||
**Requirements:**
|
||||
- `ntn` CLI installed: `curl -fsSL https://ntn.dev | bash`
|
||||
- Logged in: `ntn login`
|
||||
|
||||
Uploads are workspace-scoped — the file lands in whichever workspace the `ntn` CLI is authenticated to. Ensure this matches the workspace the Notion API token targets, or the image block will not attach.
|
||||
Uploads run as the **same integration** that writes the page (`NOTION_API_KEY`). The script injects `NOTION_API_TOKEN=$NOTION_API_KEY` into every `ntn` subprocess, so the file upload and the page share one identity — no separate `ntn login` or workspace matching is needed. `ntn` only needs to be installed, not logged in.
|
||||
|
||||
### Engines
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ lifecycle (create -> send bytes -> complete) and prints the upload ID.
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
@@ -17,6 +18,19 @@ from typing import Dict, List, Optional, Callable, Any
|
||||
_WORKSPACE: Optional[Dict[str, str]] = None
|
||||
|
||||
|
||||
def _ntn_env():
|
||||
"""Env for ntn subprocesses: force ntn to authenticate as the SAME
|
||||
integration the script uses (NOTION_API_KEY/NOTION_TOKEN), so an uploaded
|
||||
file and the page that references it share one identity. Notion scopes
|
||||
file uploads to the creating integration, so a mismatch makes the upload
|
||||
un-attachable."""
|
||||
env = dict(os.environ)
|
||||
token = os.environ.get("NOTION_API_KEY") or os.environ.get("NOTION_TOKEN")
|
||||
if token:
|
||||
env["NOTION_API_TOKEN"] = token
|
||||
return env
|
||||
|
||||
|
||||
class NtnUploadError(Exception):
|
||||
"""Raised when `ntn` is unavailable or a file upload fails."""
|
||||
|
||||
@@ -45,7 +59,7 @@ def preflight() -> Dict[str, str]:
|
||||
|
||||
result = subprocess.run(
|
||||
["ntn", "api", "v1/users/me"],
|
||||
capture_output=True, text=True,
|
||||
capture_output=True, text=True, env=_ntn_env(),
|
||||
)
|
||||
if result.returncode != 0:
|
||||
raise NtnUploadError(
|
||||
@@ -73,7 +87,7 @@ def upload(path: Path) -> str:
|
||||
with open(path, "rb") as fh:
|
||||
result = subprocess.run(
|
||||
["ntn", "files", "create", "--plain"],
|
||||
stdin=fh, capture_output=True, text=True,
|
||||
stdin=fh, capture_output=True, text=True, env=_ntn_env(),
|
||||
)
|
||||
if result.returncode != 0:
|
||||
raise NtnUploadError(
|
||||
|
||||
@@ -116,6 +116,18 @@ def test_materialize_missing_file_raises():
|
||||
assert "nope.png" in str(e) or "nope.png" in e.path
|
||||
|
||||
|
||||
def test_upload_passes_token_env():
|
||||
_reset_cache()
|
||||
fake = subprocess.CompletedProcess(args=[], returncode=0,
|
||||
stdout="ID123\tphoto.png\tuploaded\n", stderr="")
|
||||
with mock.patch.dict("os.environ", {"NOTION_API_KEY": "tok-abc"}, clear=False), \
|
||||
mock.patch("builtins.open", mock.mock_open(read_data=b"x")), \
|
||||
mock.patch("subprocess.run", return_value=fake) as m:
|
||||
ntn_files.upload(Path("/tmp/p.png"))
|
||||
passed_env = m.call_args.kwargs["env"]
|
||||
assert passed_env["NOTION_API_TOKEN"] == "tok-abc"
|
||||
|
||||
|
||||
def run_all():
|
||||
tests = [
|
||||
test_preflight_missing_ntn,
|
||||
@@ -126,6 +138,7 @@ def run_all():
|
||||
test_materialize_local_uploaded,
|
||||
test_materialize_nested_children,
|
||||
test_materialize_missing_file_raises,
|
||||
test_upload_passes_token_env,
|
||||
]
|
||||
for t in tests:
|
||||
print(f"\n{t.__name__}")
|
||||
|
||||
Reference in New Issue
Block a user