Skip to main content

max / makenotwork

Update docs: sandbox guide, tips/bundles/splits, Stripe naming, fuzz findings New guide pages: sandbox.md, tips.md, bundles.md, splits.md. Payouts/payments: replace "payment processor" with "Stripe" throughout, add direct links to Stripe docs (global, currencies, payouts, refunds). Getting started: add sandbox link, creator application criteria, Stripe naming. Content protection: disclose text fingerprinting, scope watermark claims. Mailing lists: reword import as roadmap item. Docs index: add sandbox to Getting Started subcategory. Todo: document round 3 fuzz findings (20 fixed), creator trust audit resolutions (doc contradictions, missing docs, trust gaps). MT todo: record code fuzz findings (cross-community auth bypasses, SSRF, panics). Cargo.lock: version bump to 0.4.2.
Co-Authored-By
Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Author: Max J. <87768334+MaxJMath@users.noreply.github.com> · 2026-04-26 19:46 UTC
Commit: 80f412740202c9295ecb0cacfdc16368b70839ba
Parent: be9c4b0
13 files changed, +337 insertions, -40 deletions
@@ -3385,7 +3385,7 @@
3385 3385
3386 3386 [[package]]
3387 3387 name = "makenotwork"
3388 - version = "0.4.0"
3388 + version = "0.4.2"
3389 3389 dependencies = [
3390 3390 "anyhow",
3391 3391 "argon2",
@@ -6,6 +6,25 @@
6 6
7 7 ---
8 8
9 + ## Code Fuzz Findings (2026-04-26)
10 +
11 + ### Critical
12 + - [ ] Cross-community post removal via flag: `remove_flagged_post_handler` and `dismiss_flag_handler` verify mod/owner of the URL slug community but act on any `flag_id` without checking it belongs to that community. A mod of community-A can remove posts from community-B. Fix: after fetching the flag row, join through posts→threads→categories to get community_id and verify it matches. (`src/routes/flagging.rs:101,128`)
13 +
14 + ### Serious
15 + - [ ] Cross-community category edit: `edit_category_handler` verifies owner of slug community, then updates any category by UUID. Fix: add `AND community_id = $2` to the UPDATE in `update_category`. (`src/routes/settings.rs:231`, `crates/mt-db/src/mutations.rs:349`)
16 + - [ ] Cross-community tag delete: `delete_tag_handler` verifies owner of slug community, then deletes any tag by UUID. Fix: add `AND community_id = $2` to the DELETE in `delete_tag`. (`src/routes/settings.rs:363`, `crates/mt-db/src/mutations.rs:666`)
17 + - [ ] Search query panics on multi-byte UTF-8: `&q[..200]` panics if byte 200 is mid-character. Fix: use `is_char_boundary` loop to find a safe truncation point. (`src/routes/search.rs:32`)
18 + - [ ] SSRF bypass in link preview via alternative IP encodings (octal, decimal, hex, shorthand, IPv6-mapped IPv4). String-based host check doesn't catch them. Fix: parse host with `std::net::IpAddr` to normalize all IP representations before checking private ranges. (`src/link_preview.rs:13`)
19 +
20 + ### Minor
21 + - [ ] `parse_duration` silently defaults unknown strings to permanent ban (e.g. typo "3d" → permanent). Fix: return error on unrecognized values. (`src/routes/helpers.rs:275`)
22 + - [ ] `/internal/threads/{id}/stats` is unauthenticated and publicly accessible (thread UUIDs visible in URLs). Fix: add `InternalAuth` extractor or accept the info leak. (`src/routes/internal.rs:276`)
23 + - [ ] `auto_hide_if_threshold_met` records the flagger's user_id as `removed_by`, not a system/mod account. Misleading audit trail. (`src/routes/flagging.rs:82`)
24 + - [ ] `/search` endpoint has no rate limiting (GET in read_routes). Full-text + trigram similarity queries are expensive. Fix: add per-IP rate limit or move to write_routes group. (`src/routes/search.rs`)
25 +
26 + ---
27 +
9 28 ## Code Review Remediation — Remaining
10 29 - [ ] Transitive dep advisories: rand 0.8/0.9 (RUSTSEC-2026-0097), rsa (RUSTSEC-2023-0071), lru (RUSTSEC-2026-0002) — no direct fix available, monitor upstream
11 30 - [ ] Add partial index on `posts.removed_at` (`WHERE removed_at IS NOT NULL`) when data volume warrants it
@@ -120,7 +120,7 @@
120 120
121 121 ## Code Fuzz Findings (2026-04-25)
122 122
123 - Two rounds of adversarial code review. 31 findings total: 30 fixed, 1 accepted risk, 1 deferred.
123 + Three rounds of adversarial code review. 51 findings total: 50 fixed, 1 accepted risk, 1 deferred.
124 124
125 125 ### Accepted Risk
126 126 - Idempotency check not atomic with operation — concurrent requests both execute (`db/idempotency.rs`). Safe because underlying ops are themselves idempotent.
@@ -131,6 +131,46 @@
131 131 ### Resolved (28 findings)
132 132 All critical, serious, and minor findings from rounds 1 and 2 are fixed. See git history for details.
133 133
134 + ## Code Fuzz Findings -- Round 3 (2026-04-25)
135 +
136 + ### Fixed
137 + - [x] SubscriptionStatus missing Trialing/Incomplete/IncompleteExpired variants (db/enums.rs)
138 + - [x] SyncKit auth does not block deactivated users (synckit_auth.rs)
139 + - [x] Build token comparison not constant-time (routes/builds.rs)
140 + - [x] Subscription access checks ignore paused_at (db/subscriptions.rs)
141 + - [x] Cover/MediaImage uploads bypass storage cap with i64::MAX (db/creator_tiers.rs)
142 + - [x] Promo code use_count consumed even when user already owns item (db/transactions.rs)
143 + - [x] Idempotency middleware caches empty string instead of real body (metrics.rs)
144 + - [x] OAuth: no dummy hash on user-not-found path (timing leak, routes/oauth.rs)
145 + - [x] OAuth: no password length cap (Argon2 DoS, routes/oauth.rs)
146 + - [x] OAuth: suspended/deactivated users can still authorize (routes/oauth.rs)
147 + - [x] OAuth session validation: result.valid not checked before suspension (routes/oauth.rs)
148 + - [x] OAuth legacy session path ignores deactivated flag (routes/oauth.rs)
149 + - [x] Subscription test fixture missing paused_at field (db/models/subscription.rs)
150 + - [x] Git routes have no rate limiting (routes/git/mod.rs)
151 + - [x] No Content-Security-Policy header (lib.rs)
152 + - [x] /metrics endpoint unprotected (lib.rs)
153 + - [x] Upload confirm: no idempotency, no S3 cleanup on storage error (routes/storage/uploads.rs)
154 + - [x] Version confirm: no idempotency, no S3 cleanup on storage error (routes/storage/versions.rs)
155 + - [x] Image confirm: storage increment after DB write, no idempotency (routes/storage/images.rs)
156 + - [x] Media confirm: no content_type/extension re-validation, no S3 cleanup (routes/storage/media.rs)
157 +
158 + - [x] CSRF token not rotated after login -- now regenerated in login_user (auth.rs, csrf.rs)
159 + - [x] Read-modify-write race on sort_order/position -- atomic INSERT...SELECT (db/custom_links.rs, db/collections.rs)
160 + - [x] N+1 reorder loops without transactions -- wrapped in transactions (db/custom_links.rs, db/collections.rs)
161 + - [x] Race condition in custom domain creation -- SELECT FOR UPDATE in transaction (db/custom_domains.rs)
162 +
163 + - [x] Double-purchase TOCTOU -- partial unique index on (buyer_id, item_id/project_id) WHERE status='pending' prevents concurrent checkouts (migration 073, checkout handlers)
164 +
165 + - [x] Promo code max_uses bypassable on paid path -- use_count now reserved at checkout time, released by scheduler on stale cleanup (migration 074, checkout handlers, scheduler)
166 +
167 + ### Deferred
168 + - Rate limit IP extraction trusts X-Forwarded-For when traffic bypasses Cloudflare (helpers.rs). Fix requires splitting rate limit extraction by path: CF-Connecting-IP for public web routes, peer socket for internal/CLI/git. Needs careful routing since CLI, git smart HTTP, and SyncKit all hit the same server but some bypass Cloudflare.
169 + - S3 key/file size UPDATE queries lack ownership in SQL -- defense-in-depth; callers verify ownership (db/items.rs)
170 +
171 + ### Accepted
172 + - Revoked session usable for up to 30s -- session cache IS cleared by revoke_session and revoke_other_sessions handlers; window only applies to direct DB manipulation (admin)
173 +
134 174 ---
135 175
136 176 ## Creator Trust Audit (2026-04-25)
@@ -140,8 +180,24 @@
140 180 ### Resolved (20+ findings)
141 181 All doc/code fixes, trust gaps, security issues, and doc clarity items are complete. Key changes: subscription export endpoint, offsite backups with WAM alerting, API key hashing, security headers, fan subscription pause on suspension, account limbo state, support ticket portal, expanded tax/payout/discovery/storage docs, privacy policy updates. See git history.
142 182
143 - ### Remaining
144 - - [ ] No incident post-mortems or public historical incident log (process, not code)
183 + ### Doc/Code Contradictions (all resolved)
184 + - [x] Payout minimum: aligned payouts.md with Stripe reality ($1 minimum), named Stripe explicitly
185 + - [x] Content protection watermarking: scoped claim to "audio and image files delivered without watermarks", disclosed text fingerprinting
186 + - [x] Mailing list import: reworded as roadmap item, linked to migration guide
187 + - [x] IP retention: added daily scheduler job scrubbing IPs from user_sessions, download_fingerprints, streaming_sessions at 30 days
188 +
189 + ### Missing Creator-Facing Documentation
190 + - [x] Named Stripe explicitly in payouts.md and payments.md (replaced all "payment processor" references)
191 + - [x] Creator application criteria added to getting-started.md (what we look for, what gets rejected)
192 + - [x] Tips documented (new guide/tips.md)
193 + - [x] Bundles documented (new guide/bundles.md)
194 + - [x] Revenue splits documented (new guide/splits.md)
195 + - [x] Currencies/countries: linked to Stripe's own docs (46+ countries, 135+ currencies) rather than maintaining stale tables; fixed grammar from bulk find-replace
196 +
197 + ### Trust Gaps
198 + - [x] 30-day post-termination export window -- migration 076 adds terminated_at; admin terminate route (requires prior suspension); scheduler deletes after 30 days; Stripe subscriptions canceled; termination email sent with export instructions
199 + - [x] Per-item content removal admin action -- migration 075, admin routes (remove/restore), email notifications, publish guards prevent re-publishing removed items
200 + - ~~No incident post-mortems or public historical incident log~~ — will publish as posts in the MNW Changelog blog project
145 201
146 202 ### Competitive Positioning (acknowledged, not bugs)
147 203 - No free tier — deliberate tradeoff. Earn-back credit program planned.
@@ -2,6 +2,8 @@
2 2
3 3 Your first 15 minutes on Makenot.work — from sign-up to your first published item.
4 4
5 + **Not ready to commit?** [Try sandbox mode](./sandbox.md) to explore the full creator dashboard without signing up. It lasts 1 hour and requires no account.
6 +
5 7 ## Create Your Account
6 8
7 9 1. Visit the homepage and click **Join**
@@ -26,16 +28,18 @@
26 28
27 29 Most applications are approved within a few days. You'll get an email when you're in.
28 30
31 + **What we look for:** We approve anyone who makes original creative work -- music, writing, software, art, video, courses. We don't require a minimum audience or existing sales history. Applications are rejected only for content that violates our [acceptable use policy](../legal/acceptable-use.md) (e.g., reselling others' work, prohibited content).
32 +
29 33 ## Connect Payments
30 34
31 - Once approved as a creator, connect your payment account to receive fan payments:
35 + Once approved as a creator, connect Stripe to receive fan payments:
32 36
33 37 1. Go to your **Dashboard**
34 38 2. Click **Connect Payments**
35 - 3. Follow the payment onboarding flow
36 - 4. Complete identity verification (payment processor requirement)
39 + 3. Follow the Stripe onboarding flow
40 + 4. Complete identity verification (Stripe requirement)
37 41
38 - Payments go directly to your payment account. We never hold or touch your revenue.
42 + Payments go directly to your Stripe account. We never hold or touch your revenue.
39 43
40 44 ## Create Your First Project
41 45
@@ -53,7 +53,7 @@
53 53
54 54 ## Importing Subscribers
55 55
56 - You can import external email addresses (people without Makenot.work accounts) to your mailing lists. This is useful when migrating from another platform.
56 + Subscriber import from other platforms (CSV upload with consent emails) is on the roadmap but not yet available. When migrating, use the [data import tools](./migration.md) to bring over your content; your audience can follow your new project to subscribe.
57 57
58 58 ## See Also
59 59
@@ -4,14 +4,14 @@
4 4
5 5 We use a direct-deposit payment model. When you sign up:
6 6
7 - 1. Connect or create a payment account
8 - 2. Verify your identity (payment processor requirement)
7 + 1. Connect or create a Stripe account
8 + 2. Verify your identity (Stripe requirement)
9 9 3. Link your bank account
10 10 4. Payments flow directly to you
11 11
12 12 ## Payout Schedule
13 13
14 - You control this in your payment processor's dashboard:
14 + You control this in your [Stripe dashboard](https://dashboard.stripe.com):
15 15
16 16 - **Instant**: Available immediately (small fee)
17 17 - **Daily**: Next business day
@@ -22,11 +22,11 @@
22 22
23 23 ## Minimum Payout
24 24
25 - No minimum. Even $0.01 will transfer (though processing fees make tiny amounts impractical).
25 + Stripe requires a minimum balance before payout (typically $1). You can set a higher threshold if you prefer larger, less frequent payouts. Processing fees make very small amounts impractical regardless of the minimum.
26 26
27 27 ## International Creators
28 28
29 - Our payment processor supports 40+ countries. Payouts are converted to your local currency by the payment processor when your country's currency differs from the purchase currency (USD). Conversion rates and any currency conversion fees are set by the payment processor, not by us.
29 + Stripe supports creators in [46+ countries](https://stripe.com/global). Payouts are converted to your local currency by Stripe when your country's currency differs from the purchase currency (USD). Conversion rates and any currency conversion fees are set by Stripe, not by us.
30 30
31 31 If your country isn't supported, contact us — we're working on alternatives.
32 32
@@ -36,19 +36,19 @@
36 36
37 37 ### US Creators
38 38
39 - - The payment processor issues a **1099-K** if your gross payments exceed IRS reporting thresholds ($600/year as of 2024)
39 + - Stripe issues a **1099-K** if your gross payments exceed IRS [reporting thresholds](https://support.stripe.com/topics/1099-tax-forms) ($600/year as of 2024)
40 40 - You are responsible for reporting all income, including amounts below the 1099-K threshold
41 - - We do not issue 1099-MISC or W-2 forms — your relationship is with the payment processor, not with us as an employer
41 + - We do not issue 1099-MISC or W-2 forms — your relationship is with Stripe, not with us as an employer
42 42 - Keep your own records: your dashboard shows revenue charts and full transaction history, and you can export sales as CSV at any time
43 43 - Consult a tax professional if you are unsure about your obligations — this is not tax advice
44 44
45 45 ### Non-US Creators
46 46
47 47 - Follow your local tax laws for self-employment or platform income
48 - - The payment processor provides transaction records and may issue tax forms required by your jurisdiction
48 + - Stripe provides transaction records and may issue tax forms required by your jurisdiction
49 49 - VAT/GST collection is your responsibility — we do not currently collect or remit taxes on your behalf
50 50 - We provide annual revenue summaries on request (email billing@makenot.work)
51 - - If your country requires tax withholding on cross-border payments, the payment processor handles this based on your W-8BEN or equivalent tax form
51 + - If your country requires tax withholding on cross-border payments, Stripe handles this based on your W-8BEN or equivalent tax form
52 52
53 53 <!-- PENDING: This section should be reviewed by a tax professional. -->
54 54 <!-- Specific areas: 1099-K threshold accuracy, international withholding guidance, VAT/GST obligations. -->
@@ -62,15 +62,15 @@
62 62 - Per-project and per-item breakdowns
63 63 - Full transaction history
64 64
65 - Your payment processor's dashboard provides additional payout detail (pending transfers, completed payouts, payout schedule).
65 + Your [Stripe dashboard](https://dashboard.stripe.com) provides additional payout detail (pending transfers, completed payouts, payout schedule).
66 66
67 67 ## Refunds
68 68
69 69 If a fan requests a refund and you approve:
70 70
71 71 - Funds deducted from your next payout
72 - - Or from your payment account balance
73 - - Refund fees may apply (payment processor policy)
72 + - Or from your Stripe balance
73 + - Refund fees may apply ([Stripe refund policy](https://docs.stripe.com/refunds))
74 74
75 75 You have full control over refund decisions.
76 76
@@ -78,3 +78,5 @@
78 78
79 79 - [Analytics & Dashboard](./analytics.md) — Revenue charts and transaction history
80 80 - [Pricing & Monetization](./03-selling.md) — Setting prices and payment flow
81 + - [Stripe Global](https://stripe.com/global) — Supported countries
82 + - [Stripe Payouts](https://docs.stripe.com/payouts) — Payout schedules and timing
@@ -6,7 +6,7 @@
6 6
7 7 ## The Short Version
8 8
9 - Our payment processor handles all payments. You're the merchant of record. We don't take a cut of your revenue. Fans handle refunds with you or dispute via their bank.
9 + Stripe handles all payments. You're the merchant of record. We don't take a cut of your revenue. Fans handle refunds with you or dispute via their bank.
10 10
11 11 ---
12 12
@@ -16,8 +16,8 @@
16 16
17 17 When fans buy your content or subscribe:
18 18
19 - 1. Fan pays via payment processor (~2.9% + $0.30 per transaction)
20 - 2. Funds go directly to your connected payment account
19 + 1. Fan pays via Stripe (~2.9% + $0.30 per transaction)
20 + 2. Funds go directly to your connected Stripe account
21 21 3. Deposits to your bank on your chosen schedule
22 22 4. We never touch or hold your money
23 23
@@ -33,12 +33,12 @@
33 33
34 34 ## You're the Merchant of Record
35 35
36 - When you connect payments, the processor creates an account for you. This means:
36 + When you connect Stripe, it creates an account for you. This means:
37 37
38 38 - **You are the merchant** - Fans are paying you, not us
39 39 - **You set prices** - Including pay-what-you-want options
40 40 - **You handle disputes** - Chargebacks and refunds are your responsibility
41 - - **Your payment account is yours** - If you leave, it stays with you
41 + - **Your Stripe account is yours** - If you leave, it stays with you
42 42
43 43 This keeps your revenue in your hands. The tradeoff: you absorb the (typically rare) chargeback risk.
44 44
@@ -46,7 +46,7 @@
46 46
47 47 ## Payouts
48 48
49 - You control when funds move from your payment account balance to your bank:
49 + You control when funds move from your Stripe balance to your bank:
50 50
51 51 | Schedule | Timing | Notes |
52 52 |----------|--------|-------|
@@ -75,7 +75,7 @@
75 75
76 76 Fans should contact you directly. You can:
77 77
78 - - Issue a full or partial refund via your payment processor's dashboard
78 + - Issue a full or partial refund via your [Stripe dashboard](https://dashboard.stripe.com)
79 79 - Offer alternative resolution (access fix, different content)
80 80 - Decline if the purchase was delivered as promised
81 81
@@ -96,7 +96,7 @@
96 96 If a fan disputes a charge with their bank:
97 97
98 98 1. **Your balance is debited** - Disputed amount plus fee (~$15)
99 - 2. **You respond through the payment processor** - Provide evidence the charge was legitimate
99 + 2. **You respond through Stripe** - Provide evidence the charge was legitimate
100 100 3. **Bank decides** - You either get the money back or lose it
101 101
102 102 ### Why This Model
@@ -134,7 +134,7 @@
134 134 ### What We Provide
135 135
136 136 - **Transaction records** - Exportable history of all payments
137 - - **1099-K** (US creators) - Issued by the payment processor if you meet thresholds
137 + - **1099-K** (US creators) - Issued by Stripe if you meet [reporting thresholds](https://support.stripe.com/topics/1099-tax-forms)
138 138 - **Invoices** - For your platform subscription
139 139
140 140 ### Your Responsibilities
@@ -153,22 +153,22 @@
153 153
154 154 ## International Payments
155 155
156 - Our payment processor supports 40+ countries. Creators outside the US:
156 + Stripe supports creators in [46+ countries](https://stripe.com/global). Creators outside the US:
157 157
158 - - Set up a payment account in your country
159 - - Receive payments in multiple currencies
160 - - The payment processor handles currency conversion
158 + - Set up a Stripe account in your country
159 + - Receive payments in [135+ currencies](https://docs.stripe.com/currencies)
160 + - Stripe handles currency conversion automatically
161 161
162 - Conversion rates and timing vary by country. Check your payment processor's documentation for specifics.
162 + Conversion rates and timing vary by country. See [Stripe's payout documentation](https://docs.stripe.com/payouts) for specifics.
163 163
164 164 ---
165 165
166 166 ## What We Don't Do
167 167
168 - - **Hold creator funds** - Money goes directly to your payment account
168 + - **Hold creator funds** - Money goes directly to your Stripe account
169 169 - **Take a percentage** - Our fee is flat monthly subscription only
170 170 - **Control your pricing** - Set whatever prices you want
171 - - **Process payments ourselves** - Our payment processor handles everything
171 + - **Process payments ourselves** - Stripe handles everything
172 172 - **Provide tax advice** - Consult a professional
173 173
174 174 ---
@@ -177,4 +177,6 @@
177 177
178 178 - [Creator Guarantees](../about/guarantees.md) - our commitments on revenue
179 179 - [Pricing Tiers](../guide/tiers.md) - tier features and pricing
180 - - [Infrastructure & Vendors](../tech/infrastructure.md) - current payment processor details
180 + - [Stripe Global](https://stripe.com/global) - supported countries
181 + - [Stripe Currencies](https://docs.stripe.com/currencies) - supported currencies
182 + - [Stripe Connect](https://stripe.com/connect) - how the payment model works
@@ -12,7 +12,9 @@
12 12
13 13 ## What We Do Instead
14 14
15 - We give fans clean, unencumbered files. Presigned download URLs are time-limited and tied to authenticated accounts, so casual link-sharing doesn't work. But we don't embed watermarks, restrict playback, or limit how fans use files they've paid for.
15 + We give fans clean, unencumbered files. Presigned download URLs are time-limited and tied to authenticated accounts, so casual link-sharing doesn't work. We don't restrict playback or limit how fans use files they've paid for.
16 +
17 + For text files (README, LICENSE, liner notes), we may embed an invisible fingerprint tied to the buyer's purchase. This lets creators trace leaked files back to a specific download without affecting the content's usability. Audio and image files are delivered without watermarks.
16 18
17 19 If you need stronger protection than this, we may not be the right platform for your content. We'd rather be honest about that upfront.
18 20