Adds a public/private key pair authentication system and REST API endpoints
for programmatic product and content management. Designed for CI/CD pipelines
(permacomputer.com image hosting).
Auth: HMAC-SHA256 signed requests using public/private key pairs.
The secret key never travels over the wire. Replay window: 300 seconds.
Endpoints:
POST /api/v1/products create product (fiat/crypto priced)
POST /api/v1/content create content (free)
GET /api/v1/products/{id} get product
GET /api/v1/content/{id} get content
POST /api/v1/products/{id}/upload-url presigned S3 POST for direct upload
POST /api/v1/content/{id}/upload-url presigned S3 POST for direct upload
POST /api/v1/products/{id}/files/confirm confirm upload, register metadata
POST /api/v1/content/{id}/files/confirm confirm upload, register metadata
Key management UI in shop settings. Secret shown once on generation.
Migration: mps_api_key table (id, shop_id, public_key, secret_key, label,
created_timestamp, last_used_timestamp, is_active)
Tests: 12 MpsApiKey unit tests, 8 REST API functional tests (269 total passing)
4.5 KiB
4.5 KiB
MPS-17: REST API v1 — HMAC-signed product/content creation + file upload
Purpose
Enable CI/CD pipelines (e.g. permacomputer.com) to programmatically:
- Create products (fiat/crypto priced) or content (free)
- Upload files directly to Spaces via presigned POST
- Confirm uploads and get CDN URLs
Auth — HMAC public/private key pairs
Each shop has API key pairs. A key pair is:
public_key—mps_pub_{32 hex}— identifies the pair, safe to logsecret_key—mps_sec_{64 hex}— signs requests, shown once on creation
No bearer tokens. The secret never travels over the wire. Every request is signed with HMAC-SHA256. Replay window: ±300 seconds.
Signing scheme
string_to_sign = "{METHOD}\n{PATH}\n{TIMESTAMP}\n{SHA256_OF_BODY_HEX}"
signature = hmac_sha256(secret_key, string_to_sign).hexdigest()
Request headers:
X-MPS-Key: mps_pub_abc123...
X-MPS-Timestamp: 1712345678
X-MPS-Signature: sha256=abcdef...
Shell example (for CI)
METHOD=POST
PATH=/api/v1/products
TIMESTAMP=$(date +%s)
BODY='{"title":"Debian permacomputer","description":"...","price":"0.00"}'
BODY_HASH=$(echo -n "$BODY" | sha256sum | awk '{print $1}')
STRING_TO_SIGN="${METHOD}\n${PATH}\n${TIMESTAMP}\n${BODY_HASH}"
SIG=$(echo -n "$STRING_TO_SIGN" | openssl dgst -sha256 -hmac "$MPS_SECRET_KEY" | awk '{print $2}')
curl -X POST https://my.makepostsell.com/api/v1/products \
-H "X-MPS-Key: $MPS_PUBLIC_KEY" \
-H "X-MPS-Timestamp: $TIMESTAMP" \
-H "X-MPS-Signature: sha256=$SIG" \
-H "Content-Type: application/json" \
-d "$BODY"
API Endpoints
POST /api/v1/products create product (is_sellable=True)
POST /api/v1/content create content (is_sellable=False)
GET /api/v1/products/{id} get product
GET /api/v1/content/{id} get content
POST /api/v1/products/{id}/upload-url get presigned POST URL for file upload
POST /api/v1/content/{id}/upload-url get presigned POST URL for file upload
POST /api/v1/products/{id}/files/confirm confirm S3 upload, register file, get CDN URL
POST /api/v1/content/{id}/files/confirm confirm S3 upload, register file, get CDN URL
POST /api/v1/products
Request:
{
"title": "Debian permacomputer 6.12 amd64",
"description": "Debian bookworm with CWE-407 patched Linux 6.12 kernel",
"price": "9.99",
"visibility": "public"
}
Response 201:
{
"id": "abc123...",
"url": "https://my.makepostsell.com/p/abc123/debian-permacomputer",
"edit_url": "https://my.makepostsell.com/p/abc123/edit"
}
POST /api/v1/content
Same body minus price. Response 201 same shape with /c/ URL.
POST /api/v1/products/{id}/upload-url
Request:
{
"filename": "debian-permacomputer-6.12-amd64.qcow2",
"content_type": "application/octet-stream"
}
Response 200:
{
"upload_url": "https://nyc3.digitaloceanspaces.com/...",
"fields": { "key": "...", "AWSAccessKeyId": "...", ... },
"confirm_path": "/api/v1/products/{id}/files/confirm",
"key": "products/shop_id/product_id/product.qcow2"
}
POST /api/v1/products/{id}/files/confirm
Request:
{
"key": "products/shop_id/product_id/product.qcow2",
"filename": "debian-permacomputer-6.12-amd64.qcow2"
}
Response 200:
{
"cdn_url": "https://plan-period-files.nyc3.cdn.digitaloceanspaces.com/..."
}
Shop Settings UI
New section at bottom of /s/{shop_id}/settings:
- List active key pairs (label, public key, created date, last used)
- "Generate new key pair" form (label input)
- Secret shown once in a flash-style
<output>element after generation - Per-key revoke button
Routes:
POST /s/{shop_id}/api-keys/generate
POST /s/{shop_id}/api-keys/{key_id}/revoke
Files
| File | Change |
|---|---|
models/api_key.py |
New: MpsApiKey model |
models/__init__.py |
Import MpsApiKey |
models/meta.py |
Add MpsApiKey to CLASS_TO_TABLE |
views/api/__init__.py |
HMAC auth: api_key_required decorator |
views/api/items.py |
All API endpoints |
routes.py |
Add API + key management routes |
views/shop.py |
api_keys_generate, api_key_revoke handlers |
templates/shop_settings.j2 |
API Keys section |
alembic/versions/ |
Migration: mps_api_key table |
tests/test_models.py |
MpsApiKey unit tests |
tests/test_integration.py |
HMAC signing integration tests |
tests/test_functional.py |
API endpoint functional tests |