wiki-plugin-shoppe 0.0.34 → 0.0.36

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CLAUDE.md CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  Multi-tenant digital goods shoppe for Federated Wiki, powered by Sanora.
4
4
 
5
+ **Current version**: 0.0.35
6
+
5
7
  ## Architecture
6
8
 
7
9
  Follows the Service-Bundling Plugin Pattern. Each tenant (seller/creator) gets their own Sanora user account, identified by a UUID and an 8-emoji emojicode (same format as BDO: 3 base + 5 unique from the EMOJI_PALETTE).
@@ -99,25 +101,16 @@ my-shoppe.zip
99
101
  "uuid": "your-uuid-from-registration",
100
102
  "emojicode": "🛍️🎨🎁🌟💎🐉📚🔥",
101
103
  "name": "My Shoppe",
102
- "keywords": ["digital goods", "indie creator", "music", "books"]
104
+ "keywords": ["digital goods", "indie creator", "music", "books"],
105
+ "lightMode": false
103
106
  }
104
107
  ```
105
108
 
106
- `keywords` is optional. When present, it is stored in the tenant record and rendered as a `<meta name="keywords">` tag on the main shoppe page.
109
+ `keywords` is optional. Stored in the tenant record and rendered as a `<meta name="keywords">` tag.
107
110
 
108
- `redirects` is optional. Each key is a content category (`books`, `music`, `posts`, `albums`, `products`, `appointments`, `subscriptions`) and the value is an external URL. When set, clicking any card in that category sends visitors to that URL instead of the plugin's built-in purchase/download pages. Example:
111
+ `redirects` is optional. Each key is a content category and the value is an external URL. Clicking any card in that category sends visitors to that URL instead of the plugin's built-in pages.
109
112
 
110
- ```json
111
- {
112
- "uuid": "...",
113
- "emojicode": "...",
114
- "name": "My Shoppe",
115
- "redirects": {
116
- "books": "https://myauthorsite.com/books",
117
- "music": "https://mybandcamp.com"
118
- }
119
- }
120
- ```
113
+ `lightMode` is optional (default `false`). When `true`, the shoppe page uses light mode styling (white cards, `#f5f5f7` background, `#0066cc` accent). Default is dark mode (`#0f0f12` background, `#7ec8e3` accent). Stored in `tenants.json` and applied on every page load — no re-upload needed once set.
121
114
 
122
115
  ### books/*/info.json
123
116
 
@@ -183,6 +176,8 @@ preview = "ocean.jpg"
183
176
 
184
177
  The hero image is resolved automatically: `hero.jpg` or `hero.png` is used if present, otherwise the first image in the folder. Folder numeric prefix (`01-`, `02-`, …) sets display order.
185
178
 
179
+ **Note:** If the image upload fails (e.g. 413 from nginx), the product metadata is still recorded in Sanora and the upload result still counts as a success with a warning. Re-uploading the archive will push the image without duplicating the product entry.
180
+
186
181
  ### appointments/*/info.json
187
182
 
188
183
  ```json
@@ -226,13 +221,15 @@ The hero image is resolved automatically: `hero.jpg` or `hero.png` is used if pr
226
221
  |--------|------|------|-------------|
227
222
  | `POST` | `/plugin/shoppe/register` | Owner | Register new tenant |
228
223
  | `GET` | `/plugin/shoppe/tenants` | Owner | List all tenants |
229
- | `POST` | `/plugin/shoppe/upload` | UUID+emojicode in archive | Upload goods archive |
224
+ | `POST` | `/plugin/shoppe/upload` | UUID+emojicode in archive | Upload goods archive (returns `{ jobId }` immediately) |
225
+ | `GET` | `/plugin/shoppe/upload/progress/:jobId` | Public | SSE stream of upload progress events |
230
226
  | `GET` | `/plugin/shoppe/:id` | Public | Shoppe HTML page |
231
227
  | `GET` | `/plugin/shoppe/:id/goods` | Public | Goods JSON |
232
228
  | `GET` | `/plugin/shoppe/:id/goods?category=books` | Public | Filtered goods JSON |
233
- | `GET` | `/plugin/shoppe/:id/book/:title` | Public | Appointment booking page |
229
+ | `GET` | `/plugin/shoppe/:id/music/feed` | Public | Music feed `{ albums, tracks }` built from Sanora products |
230
+ | `GET` | `/plugin/shoppe/:id/book/:title` | Public | Appointment booking page (standalone) |
234
231
  | `GET` | `/plugin/shoppe/:id/book/:title/slots` | Public | Available slots JSON |
235
- | `GET` | `/plugin/shoppe/:id/subscribe/:title` | Public | Subscription sign-up page |
232
+ | `GET` | `/plugin/shoppe/:id/subscribe/:title` | Public | Subscription sign-up page (standalone) |
236
233
  | `GET` | `/plugin/shoppe/:id/membership` | Public | Membership portal |
237
234
  | `POST` | `/plugin/shoppe/:id/membership/check` | Public | Check subscription status |
238
235
  | `POST` | `/plugin/shoppe/:id/purchase/intent` | Public | Create Stripe payment intent |
@@ -245,12 +242,88 @@ The hero image is resolved automatically: `hero.jpg` or `hero.png` is used if pr
245
242
 
246
243
  `:id` accepts either UUID or emojicode.
247
244
 
245
+ ## Upload Flow (SSE Progress)
246
+
247
+ The upload endpoint is non-blocking. The client POSTs the archive and immediately gets `{ jobId }`. It then opens an `EventSource` to `/plugin/shoppe/upload/progress/:jobId` and receives a stream of events:
248
+
249
+ | Event | Data |
250
+ |-------|------|
251
+ | `start` | `{ total, name }` — total item count and shoppe name |
252
+ | `progress` | `{ current, total, label }` — item number and human-readable label |
253
+ | `warning` | `{ message }` — non-fatal issue (e.g. image upload failed) |
254
+ | `complete` | `{ success, books, music, posts, … }` — final result counts |
255
+ | `error` | `{ message }` — fatal upload failure |
256
+
257
+ The progress stream is buffered for late-connecting clients. Jobs are cleaned up after 15 minutes.
258
+
259
+ ## Shoppe Page UI
260
+
261
+ The shoppe page is a single-page app generated server-side by `generateShoppeHTML`. All tabs are lazy-initialized on first open.
262
+
263
+ ### Tabs and their UI patterns
264
+
265
+ | Tab | Pattern |
266
+ |-----|---------|
267
+ | All | Card grid of everything |
268
+ | Books | Card grid → buy page |
269
+ | Music | Album grid → track list → fixed player bar |
270
+ | Posts | Series cards → numbered parts list; standalones below |
271
+ | Albums | Card grid |
272
+ | Products | Card grid → buy page |
273
+ | Videos | Card grid with inline player modal |
274
+ | Appointments | Inline date strip → slot picker → booking form → Stripe |
275
+ | Infuse | Inline tier cards with benefits → recovery key → Stripe |
276
+
277
+ ### Music Player
278
+
279
+ The music tab fetches `/music/feed` on first open (lazy). The feed is built from Sanora products with `category: 'music'`. Products with multiple audio artifacts are treated as albums; single-artifact products are standalone tracks.
280
+
281
+ The player bar is fixed at the bottom of the page and is always dark regardless of `lightMode`. Track titles default to "Track 1", "Track 2", etc. because Sanora stores artifacts by UUID (original filenames are not preserved).
282
+
283
+ ### Posts Hierarchy
284
+
285
+ Post series are detected server-side by `category: 'post-series'` products. Parts are linked by `series:SeriesTitle` and `part:N` tags. The hierarchy is pre-computed in `generateShoppeHTML` and embedded as `_postsRaw` JSON so the client does no extra fetching.
286
+
287
+ ### Inline Subscriptions and Appointments
288
+
289
+ Subscriptions and appointments are fully handled inline on the shoppe page — no navigation to separate pages. The full payment flow (recovery key → Stripe Elements → confirmation) runs inside expanding panels under each tier/appointment card. Data (`productId`, `renewalDays`, `benefits`, `timezone`, `duration`) is fetched from Sanora artifact JSON during `getShoppeGoods` and embedded in the page.
290
+
291
+ ## Theming
292
+
293
+ The shoppe page is **dark mode by default**. All colors use CSS custom properties defined in `:root`:
294
+
295
+ | Variable | Dark | Light |
296
+ |----------|------|-------|
297
+ | `--bg` | `#0f0f12` | `#f5f5f7` |
298
+ | `--card-bg` | `#18181c` | `white` |
299
+ | `--accent` | `#7ec8e3` | `#0066cc` |
300
+ | `--text` | `#e8e8ea` | `#1d1d1f` |
301
+ | `--border` | `#333` | `#ddd` |
302
+
303
+ Set `"lightMode": true` in `manifest.json` and re-upload to switch a shoppe to light mode. The flag is stored in `tenants.json` so it persists across re-uploads. The music player bar is always dark in both modes.
304
+
305
+ ## UUID Alias / Redis Reset Recovery
306
+
307
+ If Sanora's Redis is cleared, the tenant's UUID changes on the next `sanoraEnsureUser` call. The server handles this automatically:
308
+
309
+ 1. The old UUID is kept in `tenants.json` as a forwarding alias: `{ "old-uuid": "new-uuid-string", "new-uuid": { ...fullRecord } }`
310
+ 2. `getTenantByIdentifier` follows string values as aliases
311
+ 3. All subsequent uploads and page loads use the new UUID transparently
312
+
313
+ If you encounter `Unknown UUID` errors after a Redis reset, manually add `"old-uuid": "new-uuid"` to `~/.shoppe/tenants.json`.
314
+
315
+ ## Resilience Features
316
+
317
+ - **`sanoraCreateProductResilient`** — wraps `sanoraCreateProduct`. On 404/not-found mid-upload (Redis cleared), calls `sanoraEnsureUser`, updates `tenant.uuid`, and retries once.
318
+ - **`fetchWithRetry`** — wraps `fetch`. On 429 Too Many Requests, backs off exponentially (1s → 2s → 4s) up to 3 retries.
319
+ - **Image upload isolation** — product image upload failures are caught independently; the product entry is always recorded even if the image fails. A warning is emitted so the user can re-upload to fix just the image.
320
+
248
321
  ## Payment / Transfer Flow
249
322
 
250
323
  1. Buyer calls `POST /purchase/intent` → shoppe creates a buyer Addie user and calls `PUT /user/:buyerUuid/processor/stripe/intent` on Addie → returns `{ clientSecret, publishableKey }`
251
324
  2. Stripe.js confirms payment client-side (no redirect)
252
325
  3. Client extracts `paymentIntentId` from `clientSecret` (`clientSecret.split('_secret_')[0]`) and posts to `POST /purchase/complete` with `paymentIntentId`
253
- 4. Server records the order in Sanora, then fires a **fire-and-forget** `POST ${addieUrl}/payment/${paymentIntentId}/process-transfers` — Addie splits the payment and routes it to the tenant's Stripe account (no auth required on this Addie endpoint)
326
+ 4. Server records the order in Sanora, then fires a **fire-and-forget** `POST ${addieUrl}/payment/${paymentIntentId}/process-transfers` — Addie splits the payment and routes it to the tenant's Stripe account
254
327
 
255
328
  **Important:** Transfers only flow to the owner after `node shoppe-sign.js payouts` has been run and Stripe Connect onboarding is complete.
256
329
 
@@ -264,6 +337,16 @@ export SHOPPE_BASE_EMOJI="🏪🎪🎁"
264
337
  export SANORA_PORT=7243
265
338
  ```
266
339
 
340
+ ## nginx Requirements
341
+
342
+ The allyabase server's nginx must allow large uploads for books, audio, and images:
343
+
344
+ ```nginx
345
+ client_max_body_size 50M;
346
+ ```
347
+
348
+ Without this, epub/audio artifact uploads and large cover images will fail with 413. Apply to the relevant `server {}` block and reload: `sudo nginx -t && sudo systemctl reload nginx`.
349
+
267
350
  ## Supported File Types
268
351
 
269
352
  | Category | Extensions |
@@ -276,7 +359,9 @@ export SANORA_PORT=7243
276
359
 
277
360
  ## Storage
278
361
 
279
- Tenant registry: `.shoppe-tenants.json` (gitignored contains private keys)
362
+ - `~/.shoppe/tenants.json` — tenant registry (private keys + UUID aliases — gitignored)
363
+ - `~/.shoppe/buyers.json` — buyer Addie keys, keyed by `recoveryKey + productId`
364
+ - `~/.shoppe/config.json` — plugin config (sanoraUrl)
280
365
 
281
366
  Each tenant's goods are stored in Sanora under their own UUID.
282
367
 
@@ -288,6 +373,6 @@ Each tenant's goods are stored in Sanora under their own UUID.
288
373
  "form-data": "^4.0.0",
289
374
  "multer": "^1.4.5-lts.1",
290
375
  "node-fetch": "^2.6.1",
291
- "sessionless-node": "^0.9.12"
376
+ "sessionless-node": "latest"
292
377
  }
293
378
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wiki-plugin-shoppe",
3
- "version": "0.0.34",
3
+ "version": "0.0.36",
4
4
  "description": "Multi-tenant digital goods shoppe for federated wiki, powered by Sanora",
5
5
  "keywords": [
6
6
  "wiki",