Commit graph

965 commits

Author SHA1 Message Date
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
dc06551967 docs: use make migration — never hand-write revision IDs 2026-04-06 14:20:13 -04:00
c6d03bd9ae docs: clarify prod rules — restarts ok, some servers ci-only not salt 2026-04-06 13:45:29 -04:00
08509629b6 docs: never operate on production directly — always CI/CD and Salt 2026-04-06 13:44:50 -04:00
c12365143d fix: replace bad migration revision ID with proper alembic-generated ID
The torrent_distribution_support migration was manually created with a
duplicate revision ID (a1b2c3d4e5f6) already in use by an older migration,
and pointed to the wrong down_revision (fd9f7e2f2b78 root instead of
9884324a48e3 current head). This caused a cycle in the revision map and
alembic upgrade head failed silently, leaving prod with new code but old
schema → 502 on all shop endpoints.

Deleted the broken file, regenerated with alembic revision to get a proper
unique ID (c0236e351476), set correct down_revision, preserved idempotent
_column_exists guards.
2026-04-06 13:40:36 -04:00
d7e8ec5bc2 mps: torrent backfill on enable + web seed + full test coverage
- backfill: enabling torrent on a shop auto-generates .torrent for all
  existing products that have a product file (no manual trigger needed)
- web seed (BEP 19): CDN url embedded in .torrent + magnet link so
  clients bootstrap via HTTP then seed to peers (no seeder process needed)
- torrent_file_url: stored on Product, shown as download link on content
  and edit pages alongside the magnet link
- migration: idempotent _column_exists guards on all add_column calls
- template: grid layout (not flex) for magnet/torrent buttons on edit page
- tests: 13 passing tests covering all new paths (unit + functional)
  including backfill trigger, web seed construction, visibility gating
2026-04-05 22:09:20 -04:00
e06cf1c230 mps: torrent web seed + torrent_file_url for CDN-bootstrapped swarm
- embed CDN URL as BEP 19 web seed in .torrent + magnet link
- store torrent_file_url (CDN path to .torrent) on Product
- migration adds torrent_file_url column alongside magnet_link
- content page shows Magnet + .torrent download buttons
- product edit shows Copy Magnet + Open + .torrent download
- cdn_endpoint flows from request.shop_cdn_endpoint through async thread
2026-04-05 21:31:09 -04:00
7d78dff6ac mps: torrent magnet links auto-generated on file upload
lib/torrent.py: generate_torrent_async() — daemon thread that fires after
upload, downloads file from S3, builds .torrent via torf, uploads
product.torrent to S3 (public-read), saves magnet link to DB.

views/product.py: trigger generate_torrent_async when file_key=='product'
and shop.torrent_enabled. Removed manual magnet link save — it's automatic.

templates/product_edit.j2: magnet link is read-only + copy button + Open
button when generated; 'upload the product file' hint when not yet generated.

tests: updated to reflect auto-generation model, added content page test.
2026-04-05 20:03:47 -04:00
d5f55f8131 mps: torrent distribution — shop toggle + per-product magnet link
Shop settings: new 'Torrent Distribution' section with single checkbox
(torrent_enabled, default False). When on, product edit shows a magnet
link field; content page shows a Torrent button alongside Download.

- models/shop.py: torrent_enabled (Boolean, default False)
- models/product.py: torrent_magnet_link (UnicodeText, nullable)
- views/shop.py: torrent-settings form section handler
- views/product.py: save/validate magnet link on product edit
- views/content.py: pass torrent_magnet_link to content template
- templates/shop_settings.j2: Torrent Distribution section
- templates/product_edit.j2: magnet link field (shown when torrent enabled)
- templates/content.j2: Torrent button beside Download
- migration: a1b2c3d4e5f6 adds both columns
- tests: 6 functional tests covering enable/disable, magnet save,
  invalid scheme rejection, and no-leak when disabled
2026-04-05 19:57:44 -04:00
41a8525953 feat: add Previous button left of Random in watch controls 2026-04-03 10:00:10 -04:00
6ec646a075 fix: restore logo full width on mobile — max-height 100px, max-width 100% 2026-04-03 09:58:34 -04:00
f77a8029c1 bump GIT_HASH 2026-03-31 20:10:39 -04:00
5ceeaf8a69 feat: share watch queue across tabs via localStorage + storage events
Queue was sessionStorage (per-tab only). Switch to localStorage so any
tab can add to our shared queue. Storage events fire in all other same-origin
tabs on mutation — renderQueue() wired to keep UI in sync automatically.
2026-03-31 20:10:19 -04:00
f12fa036a6 style: avoid "the", use "our" — writing style rule + sweep 2026-03-31 13:20:21 -04:00
422d462b46 bump GIT_HASH 2026-03-30 09:26:44 -04:00
1883329af1 docs: mark bleach CWE-407 as fixed in security section 2026-03-30 09:26:42 -04:00
79ecef66ef bump GIT_HASH 2026-03-30 09:26:17 -04:00
c71fd328c3 fix: CWE-407 — limit HTML nesting depth before bleach to prevent O(2^N)
Add limit_html_nesting() to sanitize_html.py. Flattens any HTML element
nested beyond depth 20 using html.parser (O(N)) before the content reaches
bleach/html5lib. N=35 attack drops from 12.8s to 0.04s.

Wire into markdown_to_html() in render.py — single enforcement point for
all callers: product descriptions, shop descriptions, privacy policy,
terms of service, markup preview.

No byte cap — books, long-form content, and deep table-of-contents
structures are fully supported. The depth limit (20 levels) prevents the
exponential zone while accommodating any legitimate nesting depth.
2026-03-30 09:26:07 -04:00
4ef3059c8d bump GIT_HASH 2026-03-29 21:48:57 -04:00
72626bf65a fix: CWE-407 — cap markup preview input + harden color regex
markup_editor_preview: reject requests where data exceeds 100KB
before passing to markdown/bleach pipeline (same class as
CWE-407 bleach O(2^N) finding — no input cap on AJAX preview).

add_shop_theme_classes: replace single compound regex with four
anchored non-backtracking patterns to eliminate ReDoS risk on
color validation. Practical risk was low (theme_link_color is
always derived from color_scale()), but the pattern was unsafe.
2026-03-29 21:48:52 -04:00
8b3c9a1456 bump GIT_HASH 2026-03-29 21:20:20 -04:00
389f80a730 docs: CWE-407 security section — bleach O(2^N) exposure and PoC
Add Security section to CLAUDE.md documenting both CWE-407 surfaces:
- Search/feed endpoints (fixed, commit f9cbebb)
- Bleach HTML sanitization: O(2^N) on crafted HTML, no input cap in MPS

Add docs/poc-cwe407.py: proof-of-concept timing harness covering
rbox-search, rbox-page, rbox-dump, mps-search, mps-sitemap vectors.
Authorized use only — run against own staging/dev instance.
2026-03-29 21:19:59 -04:00
7aaf189cbd ops: operation voyeur — credential opsec protocol 2026-03-29 15:46:08 -04:00
9131f5900e bump GIT_HASH 2026-03-28 18:25:43 -04:00
6c89b31830 fix: add X-Requested-With header to markup editor preview fetch — Pyramid xhr=True predicate was rejecting requests 2026-03-28 18:25:21 -04:00
84a21e1e7a bump GIT_HASH 2026-03-28 15:13:05 -04:00
f9cbebb6b5 fix: cap search keywords and feed queries to prevent CWE-407 amplification
- shop.py: strip empty tokens, cap keywords to 10 before passing to model
- product.py: add .limit(200) per keyword query — was unbounded .all()
- feeds.py: add .limit(1000) on product/content feed queries — was unbounded
2026-03-28 15:12:41 -04:00
eec598604b docs: 🔥 == 🔥 in auto-push rule 2026-03-28 11:46:08 -04:00
ba3a6e66b2 docs: auto-push without asking — remove human friction 2026-03-28 11:45:34 -04:00
2bd25672f9 fix: cap ring related items at 42 — was rendering entire ring with forward=len(ring) 2026-03-28 11:44:23 -04:00
01bd9086fe docs: wordpress import pipeline architecture with dot diagrams
Design doc for ingesting WordPress sites into MPS shops. Covers two
input modes (REST API + WXR XML), 4-phase HTML conversion pipeline,
content/media/comment mapping, CLI interface, competitive analysis,
and future enhancements. Includes rendered dot diagrams for the
architecture overview and HTML conversion detail flow.
2026-03-12 11:32:47 -04:00
8649e6aaae docs: karaoke pipeline architecture with dot diagrams
Add docs/karaoke-pipeline.md covering the full streaming pipeline from
MPS through unsandbox API to zerotrust container and back. Includes two
Graphviz dot diagrams (rendered to SVG):

- karaoke-pipeline.dot: full system flow across MPS, API, pool, container
- karaoke-ondemand.dot: watch mode on-demand user flow

Update architecture.md feature toggle matrix and related docs table.
Update CLAUDE.md karaoke section with streaming path and on-demand info.
2026-03-11 17:49:57 -04:00
6fd6bfd756 bump GIT_HASH to 4a12722 2026-03-11 17:20:51 -04:00
4a12722b4d feat: on-demand karaoke processing — button next to random triggers vocal isolation
Karaoke button (🎤) now always visible for audio/video products when the
shop has unsandbox API keys configured. Clicking it triggers on-demand
processing via POST /karaoke/{product_id} if tracks don't exist yet.

Server forks a detached child process (survives uWSGI recycling) to run
process_karaoke, then the existing 10s watch_json refresh picks up the
new URLs when processing completes. Button shows hourglass during
processing and auto-switches to instrumentals on completion.

Changes:
- New route + view: POST /karaoke/{product_id} (on-demand processing)
- watch_json + content.py: add karaoke_eligible flag
- content.j2: data-karaoke-eligible attribute on media container
- watch.js: show button when eligible, trigger processing, detect
  completion via URL refresh, auto-switch to instrumentals
- related_content.j2: mic emoji button, JS controls visibility
- CSS: disabled state for processing button
- 8 new functional tests covering eligibility, processing, edge cases
2026-03-11 17:20:26 -04:00
924921e232 bump GIT_HASH to 5e0d263 2026-03-11 13:03:35 -04:00
5e0d263b7c fix: 3 watch mode defects — crossfade race, filter-aware next, countdown position
1. Crossfade race condition: when a DJ crossfade is active and the user
   clicks a new song, cancelDjCrossfade resets djCrossfadeActive but the
   old song is still near its end — timeupdate immediately re-triggers
   startDjCrossfade, racing with the in-flight fetch. Added
   navigationPending flag to block DJ crossfade and ended handler while
   a user-initiated navigation is in progress.

2. getNextItem/getNextUnwatchedItem now respect media type filters.
   Previously, filtering to "video only" still showed an image in the
   countdown because the ring walked by position without checking
   data-media-type. Now skips filtered items.

3. Countdown overlay moved from position:absolute inside the video/audio
   container (covering native controls) to a flow-positioned element
   between the media and title. Removed watch-countdown-static class
   since all countdown instances now use the same in-flow layout.
2026-03-11 13:03:18 -04:00
a71a3d4c6e bump GIT_HASH to 92dc61f 2026-03-08 00:45:15 -05:00
92dc61fc50 fix: address 4 defects from CodeRabbit review
- reforge_discovery_ring_async: add non-production guard matching sync version
- is_trial_active/is_trial_expired: check trial_ended flag
- has_primary_s3: include primary_s3_region in validation
- gift card migration downgrade: add missing json_gift_cards drop + existence guards
2026-03-08 00:45:04 -05:00
df8f4993c6 Merge branch 'explore/design-tokens' into 'master'
feat: integrate design tokens from www.makepostsell.com styleguide

See merge request engineering/make-post-sell/make_post_sell!58
2026-03-08 04:28:22 +00:00
cca3172c9b bump GIT_HASH to e2169ec 2026-03-07 21:14:46 -05:00
e2169ecbbf sidebar: show full ring instead of 42-item window
The related content sidebar now shows all ring items from the current
position forward, matching content.py behavior. This means the sidebar
never "runs out" of items — Fresh mode hides watched items and reveals
the full remaining ring. Items beyond offset 7 still use the
related-content-overflow CSS class for mobile.
2026-03-07 21:14:25 -05:00
2bf4ab1736 bump GIT_HASH to 25d2554 2026-03-07 20:36:10 -05:00
25d255447e fix: seed discovery ring from server on initial page load
The ring was only populated from localStorage or during SPA navigation.
First visits (empty localStorage) or cross-shop visits (stale ring)
left ringProductIds empty, causing the related content sidebar to show
only the current product. Now:

- On page load, if localStorage ring is empty, fetch from watch JSON
- Invalidate stale rings when current product is not in the saved ring
2026-03-07 20:36:05 -05:00
5385e14a29 bump GIT_HASH to 84f96f9 2026-03-07 20:21:02 -05:00
84f96f90c8 fix remaining MPS-14/15/16 audit gaps
- Skip discovery ring reforge for non-production shops (MPS-14)
- Block settings POST when trial expired, except environment and
  bucket settings needed for onboarding (MPS-15)
- BYOB upload enforcement is a soft prompt (trial tip in settings)
  rather than hard block — new users need to upload during trial
2026-03-07 20:20:56 -05:00
63bdc3c589 bump GIT_HASH to 9c3e0e5 2026-03-07 19:36:34 -05:00
9c3e0e5e54 implement remaining MPS-14/15/16 gaps: trial enforcement, BYOB backfill, search exclusion
- Add trial_active_required decorator to product_new, product_edit, cart_checkout,
  all checkout completions, and gift_card_add_to_cart (MPS-15)
- Make karaoke backfill and s3_mirror backfill shop-aware for BYOB buckets (MPS-16)
- Auto-test BYOB bucket connection on save, like mirror does (MPS-16)
- Add trial onboarding tip in bucket settings for trial shops (MPS-16)
- Filter non-production shops from search results (MPS-14)
- Filter non-production shops from email digests (MPS-14)
- Fix environment allowance to allow minimum 2 non-prod shops (MPS-14)
- Add 12 integration tests (6 trial + 6 BYOB), 724 total tests pass
2026-03-07 19:36:16 -05:00
13fabb5d3f bump GIT_HASH to bfe2289 2026-03-07 19:02:07 -05:00
bfe2289313 docs: update CLAUDE.md testing requirements, architecture, and design system
- CLAUDE.md: add mandatory test coverage rule (all 3 layers required),
  document BYOB shop-aware S3 methods, update test count to 712
- architecture.md: mark MPS-14/15/16 complete, add environment/trial/BYOB
  to feature toggle matrix, add BYOB to S3 storage diagram
- design-system.md: add environment and trial banner components
2026-03-07 19:01:57 -05:00
395e703a20 bump GIT_HASH to 2cb1e79 2026-03-07 18:14:44 -05:00