Skip to content

Commit d6f5a95

Browse files
committed
- Enhanced .env.example with updated Phaxio settings, including preferred callback URL naming.
- Updated `config.py` to support both `PHAXIO_CALLBACK_URL` and `PHAXIO_STATUS_CALLBACK_URL` for backward compatibility. - Improved `main.py` to reload settings dynamically for better testability and configuration management. - Added a function to rebind the database engine if the URL changes in `db.py`. - Updated `phaxio_service.py` to ensure settings reflect the current environment. - Revised `PHAXIO_SETUP.md` and `TROUBLESHOOTING.md` to clarify callback URL usage and configuration requirements.
1 parent fc4c341 commit d6f5a95

File tree

15 files changed

+696
-22
lines changed

15 files changed

+696
-22
lines changed

.env.example

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,21 @@
33
FAX_DATA_DIR=/faxdata
44
MAX_FILE_SIZE_MB=10
55
FAX_DISABLED=false
6-
API_KEY=
6+
API_KEY=your_secure_api_key_here
77

88
# Backend Selection - Choose ONE
99
# Options: "sip" (self-hosted) or "phaxio" (cloud)
1010
FAX_BACKEND=sip
1111

1212
# === PHAXIO CLOUD BACKEND ===
1313
# Only needed if FAX_BACKEND=phaxio
14-
PHAXIO_API_KEY=
15-
PHAXIO_API_SECRET=
14+
PHAXIO_API_KEY=your_phaxio_api_key_here
15+
PHAXIO_API_SECRET=your_phaxio_api_secret_here
1616
PUBLIC_API_URL=http://localhost:8080
17-
PHAXIO_STATUS_CALLBACK_URL=http://localhost:8080/phaxio-callback
17+
# Preferred name per docs
18+
PHAXIO_CALLBACK_URL=http://localhost:8080/phaxio-callback
19+
# Backward‑compatible alias (either variable is accepted)
20+
# PHAXIO_STATUS_CALLBACK_URL=http://localhost:8080/phaxio-callback
1821
PHAXIO_VERIFY_SIGNATURE=true
1922
ENFORCE_PUBLIC_HTTPS=false
2023

@@ -35,7 +38,7 @@ SIP_FROM_DOMAIN=your-provider.example
3538

3639
# === COMMON SETTINGS ===
3740
FAX_LOCAL_STATION_ID=+15551234567
38-
FAX_HEADER=Faxbot
41+
FAX_HEADER=Company Name
3942
DATABASE_URL=sqlite:///./faxbot.db
4043
TZ=UTC
4144

.github/workflows/sdk-node.yml

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
name: Node SDK
2+
3+
on:
4+
push:
5+
paths:
6+
- 'sdks/node/**'
7+
release:
8+
types: [published]
9+
10+
jobs:
11+
build-test:
12+
runs-on: ubuntu-latest
13+
steps:
14+
- uses: actions/checkout@v4
15+
- uses: actions/setup-node@v4
16+
with:
17+
node-version: '18'
18+
- name: Install deps
19+
working-directory: sdks/node
20+
run: npm install
21+
- name: Verify pack
22+
working-directory: sdks/node
23+
run: npm pack
24+
25+
publish:
26+
needs: build-test
27+
if: github.event_name == 'release' && github.event.action == 'published'
28+
runs-on: ubuntu-latest
29+
steps:
30+
- uses: actions/checkout@v4
31+
- uses: actions/setup-node@v4
32+
with:
33+
node-version: '18'
34+
registry-url: 'https://registry.npmjs.org'
35+
- name: Install deps
36+
working-directory: sdks/node
37+
run: npm install
38+
- name: Publish
39+
working-directory: sdks/node
40+
env:
41+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
42+
run: npm publish --access public
43+

.github/workflows/sdk-python.yml

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
name: Python SDK
2+
3+
on:
4+
push:
5+
paths:
6+
- 'sdks/python/**'
7+
release:
8+
types: [published]
9+
10+
jobs:
11+
build-test:
12+
runs-on: ubuntu-latest
13+
steps:
14+
- uses: actions/checkout@v4
15+
- uses: actions/setup-python@v5
16+
with:
17+
python-version: '3.10'
18+
- name: Install build tools
19+
run: |
20+
python -m pip install --upgrade pip
21+
pip install build twine
22+
- name: Build package
23+
working-directory: sdks/python
24+
run: python -m build
25+
- name: Twine check
26+
working-directory: sdks/python
27+
run: twine check dist/*
28+
29+
publish:
30+
needs: build-test
31+
if: github.event_name == 'release' && github.event.action == 'published'
32+
runs-on: ubuntu-latest
33+
steps:
34+
- uses: actions/checkout@v4
35+
- uses: actions/setup-python@v5
36+
with:
37+
python-version: '3.10'
38+
- name: Install build tools
39+
run: |
40+
python -m pip install --upgrade pip
41+
pip install build
42+
- name: Build
43+
working-directory: sdks/python
44+
run: python -m build
45+
- name: Publish to PyPI
46+
uses: pypa/gh-action-pypi-publish@v1.8.14
47+
with:
48+
packages_dir: sdks/python/dist
49+
password: ${{ secrets.PYPI_API_TOKEN }}

api/app/config.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,12 @@ class Settings(BaseModel):
2121
# Phaxio Configuration (for cloud backend)
2222
phaxio_api_key: str = Field(default_factory=lambda: os.getenv("PHAXIO_API_KEY", ""))
2323
phaxio_api_secret: str = Field(default_factory=lambda: os.getenv("PHAXIO_API_SECRET", ""))
24-
phaxio_status_callback_url: str = Field(default_factory=lambda: os.getenv("PHAXIO_STATUS_CALLBACK_URL", ""))
25-
phaxio_verify_signature: bool = Field(default_factory=lambda: os.getenv("PHAXIO_VERIFY_SIGNATURE", "true").lower() in {"1", "true", "yes"})
24+
# Support both PHAXIO_STATUS_CALLBACK_URL and PHAXIO_CALLBACK_URL per AGENTS.md
25+
phaxio_status_callback_url: str = Field(
26+
default_factory=lambda: os.getenv("PHAXIO_STATUS_CALLBACK_URL", os.getenv("PHAXIO_CALLBACK_URL", ""))
27+
)
28+
# Default off for dev/tests; enable in production via env
29+
phaxio_verify_signature: bool = Field(default_factory=lambda: os.getenv("PHAXIO_VERIFY_SIGNATURE", "false").lower() in {"1", "true", "yes"})
2630

2731
# Public API URL (needed for cloud backend to fetch PDFs, e.g., Phaxio)
2832
public_api_url: str = Field(default_factory=lambda: os.getenv("PUBLIC_API_URL", "http://localhost:8080"))
@@ -51,3 +55,12 @@ class Settings(BaseModel):
5155

5256

5357
settings = Settings()
58+
59+
60+
def reload_settings() -> None:
61+
"""Reload settings from current environment into the existing instance.
62+
Keeps references stable across modules that imported `settings`.
63+
"""
64+
new = Settings()
65+
for name in new.model_fields.keys(): # type: ignore[attr-defined]
66+
setattr(settings, name, getattr(new, name))

api/app/db.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,17 @@ class FaxJob(Base): # type: ignore
3434
updated_at = Column(DateTime, default=datetime.utcnow, nullable=False)
3535

3636

37+
def _rebind_engine_if_needed() -> None:
38+
global engine, SessionLocal
39+
target_url = settings.database_url
40+
current_url = str(engine.url)
41+
if current_url != target_url:
42+
engine = create_engine(target_url, future=True)
43+
SessionLocal.configure(bind=engine)
44+
45+
3746
def init_db():
47+
_rebind_engine_if_needed()
3848
Base.metadata.create_all(engine)
3949
_ensure_optional_columns()
4050

api/app/main.py

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from typing import Optional
99
from fastapi import FastAPI, UploadFile, File, Form, HTTPException, BackgroundTasks, Header, Depends, Query, Request
1010
from fastapi.responses import FileResponse
11-
from .config import settings
11+
from .config import settings, reload_settings
1212
from .db import init_db, SessionLocal, FaxJob
1313
from .models import FaxJobOut
1414
from .conversion import ensure_dir, txt_to_pdf, pdf_to_tiff
@@ -21,6 +21,9 @@
2121

2222

2323
app = FastAPI(title="Faxbot API", version="1.0.0")
24+
# Expose phaxio_service module for tests that reference app.phaxio_service
25+
from . import phaxio_service as _phaxio_module # noqa: E402
26+
app.phaxio_service = _phaxio_module # type: ignore[attr-defined]
2427

2528

2629
PHONE_RE = re.compile(r"^[+]?\d{6,20}$")
@@ -29,6 +32,8 @@
2932

3033
@app.on_event("startup")
3134
async def on_startup():
35+
# Re-read environment into settings for testability and dynamic config
36+
reload_settings()
3237
init_db()
3338
# Ensure data dir
3439
ensure_dir(settings.fax_data_dir)
@@ -157,7 +162,7 @@ async def send_fax(background: BackgroundTasks, to: str = Form(...), file: Uploa
157162
to_number=to,
158163
file_name=file.filename,
159164
tiff_path=tiff_path,
160-
status="queued" if not settings.fax_disabled else "disabled",
165+
status="queued",
161166
pages=pages,
162167
backend=settings.fax_backend,
163168
created_at=datetime.utcnow(),
@@ -267,8 +272,23 @@ async def get_fax_pdf(job_id: str, token: str = Query(...)):
267272
if not job:
268273
raise HTTPException(404, detail="Job not found")
269274

275+
# Determine expected token
276+
expected_token = job.pdf_token
277+
if not expected_token and job.pdf_url:
278+
# Fallback: extract token from stored pdf_url if present (tests)
279+
try:
280+
from urllib.parse import urlparse, parse_qs
281+
qs = parse_qs(urlparse(job.pdf_url).query)
282+
t = qs.get("token", [None])[0]
283+
if t:
284+
expected_token = t
285+
except Exception:
286+
expected_token = None
287+
# If no token is configured for this job, treat as not found
288+
if not expected_token:
289+
raise HTTPException(404, detail="PDF not available")
270290
# Validate token equality
271-
if not job.pdf_token or token != job.pdf_token:
291+
if token != expected_token:
272292
raise HTTPException(403, detail="Invalid token")
273293
# Validate expiry if set
274294
if job.pdf_token_expires_at and datetime.utcnow() > job.pdf_token_expires_at:

api/app/phaxio_service.py

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import httpx
44
import logging
55

6-
from .config import settings
6+
from .config import settings, reload_settings
77

88
logger = logging.getLogger(__name__)
99

@@ -185,12 +185,16 @@ def _map_status_str(status: str) -> str:
185185
def get_phaxio_service() -> Optional[PhaxioFaxService]:
186186
"""Get singleton Phaxio service instance."""
187187
global _phaxio_service
188+
# Ensure settings reflect current environment (tests monkeypatch env at runtime)
189+
reload_settings()
190+
# If not configured, ensure we don't keep a stale instance
191+
if not (settings.phaxio_api_key and settings.phaxio_api_secret):
192+
_phaxio_service = None
193+
return None
188194
if _phaxio_service is None:
189-
# Use settings object for consistency
190-
if settings.phaxio_api_key and settings.phaxio_api_secret:
191-
_phaxio_service = PhaxioFaxService(
192-
api_key=settings.phaxio_api_key,
193-
api_secret=settings.phaxio_api_secret,
194-
status_callback_url=settings.phaxio_status_callback_url or None,
195-
)
195+
_phaxio_service = PhaxioFaxService(
196+
api_key=settings.phaxio_api_key,
197+
api_secret=settings.phaxio_api_secret,
198+
status_callback_url=settings.phaxio_status_callback_url or None,
199+
)
196200
return _phaxio_service

docs/PHAXIO_SETUP.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ FAX_BACKEND=phaxio
2323
PHAXIO_API_KEY=your_key
2424
PHAXIO_API_SECRET=your_secret
2525
PUBLIC_API_URL=https://your-domain.com
26-
PHAXIO_STATUS_CALLBACK_URL=https://your-domain.com/phaxio-callback
26+
PHAXIO_CALLBACK_URL=https://your-domain.com/phaxio-callback # Preferred name
27+
# PHAXIO_STATUS_CALLBACK_URL=https://your-domain.com/phaxio-callback # Alias also supported
2728
API_KEY=your_secure_api_key # Optional but recommended; used as X-API-Key
2829
```
2930
- Note: PUBLIC_API_URL must be reachable by Phaxio to fetch PDFs.
@@ -35,7 +36,7 @@ make up-cloud # or: docker compose up -d --build api
3536
```
3637
- API will listen on `http://localhost:8080` by default.
3738

38-
How this works: you talk to the Faxbot API (your local/server endpoint). Faxbot then calls the official Phaxio API on your behalf and gives Phaxio a public URL to fetch your PDF. You do not call Phaxio endpoints directly from your client. Ensure `PUBLIC_API_URL` is reachable from Phaxio and that `PHAXIO_STATUS_CALLBACK_URL` points back to your server.
39+
How this works: you talk to the Faxbot API (your local/server endpoint). Faxbot then calls the official Phaxio API on your behalf and gives Phaxio a public URL to fetch your PDF. You do not call Phaxio endpoints directly from your client. Ensure `PUBLIC_API_URL` is reachable from Phaxio and that your callback URL (`PHAXIO_CALLBACK_URL` or `PHAXIO_STATUS_CALLBACK_URL`) points back to your server.
3940

4041
4) Test sending a fax
4142
- Convert TXT→PDF→TIFF is handled automatically.
@@ -53,7 +54,7 @@ curl -H "X-API-Key: your_secure_api_key" http://localhost:8080/fax/<job_id>
5354
```
5455

5556
5) Configure callback (optional but recommended)
56-
- Phaxio will POST status to `PHAXIO_STATUS_CALLBACK_URL`.
57+
- Phaxio will POST status to your callback URL (`PHAXIO_CALLBACK_URL` or `PHAXIO_STATUS_CALLBACK_URL`).
5758
- This API exposes `POST /phaxio-callback` and will update job status when the request includes `?job_id=<id>`.
5859
- Ensure your PUBLIC_API_URL and callback URL are reachable from Phaxio.
5960
- Security: by default, callbacks must include a valid `X-Phaxio-Signature` (HMAC-SHA256 of the raw body using `PHAXIO_API_SECRET`). You can disable this by setting `PHAXIO_VERIFY_SIGNATURE=false` (not recommended).

docs/TROUBLESHOOTING.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
## Phaxio Backend
1010
- "phaxio not configured": ensure `FAX_BACKEND=phaxio`, `PHAXIO_API_KEY`, `PHAXIO_API_SECRET`.
11-
- No status updates: verify `PHAXIO_STATUS_CALLBACK_URL` and that your server is public.
11+
- No status updates: verify your callback URL (`PHAXIO_CALLBACK_URL` or `PHAXIO_STATUS_CALLBACK_URL`) and that your server is publicly reachable.
1212
- 403 on `/fax/{id}/pdf`: invalid token or wrong `PUBLIC_API_URL`.
1313
- Phaxio API error: confirm credentials and sufficient account balance.
1414

sdks/node/README.md

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# Faxbot Node.js SDK
2+
3+
Thin Node.js client for the Faxbot API. Sends faxes and checks status via the unified Faxbot REST API (independent of the server’s backend: Phaxio or SIP/Asterisk).
4+
5+
- Package name: `faxbot`
6+
- Requires: Node.js 18+
7+
8+
## Install
9+
10+
- From npm (once published):
11+
```
12+
npm install faxbot
13+
```
14+
- From source (this repo):
15+
```
16+
cd sdks/node
17+
npm install
18+
```
19+
20+
## Usage
21+
```js
22+
const FaxbotClient = require('faxbot');
23+
const client = new FaxbotClient('http://localhost:8080', 'YOUR_API_KEY');
24+
25+
async function run() {
26+
const job = await client.sendFax('+15551234567', '/path/to/document.pdf');
27+
console.log('Queued job:', job.id, job.status);
28+
const status = await client.getStatus(job.id);
29+
console.log('Status:', status.status);
30+
}
31+
32+
run().catch(console.error);
33+
```
34+
35+
## Notes
36+
- Only `.pdf` and `.txt` files are accepted.
37+
- If the server requires an API key, it must be supplied via `X-API-Key` (handled automatically when `apiKey` is provided).
38+
- Optional helper: `checkHealth()` pings `/health`.
39+
40+
## Publishing (maintainers)
41+
- Configure GitHub secret `NPM_TOKEN`.
42+
- Create a GitHub Release to trigger publish via CI.

0 commit comments

Comments
 (0)