make_post_sell/docs/tickets/mps-17.md
russell@unturf.com b72fc6bf3b feat: REST API v1 — HMAC-signed product/content creation and file upload
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)
2026-04-06 15:23:30 -04:00

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_keymps_pub_{32 hex} — identifies the pair, safe to log
  • secret_keymps_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