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 +105 -20
- package/package.json +1 -1
- package/server/server.js +328 -30
- package/server/templates/generic-recover-stripe.html +461 -547
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.
|
|
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
|
|
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
|
-
|
|
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/
|
|
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
|
|
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
|
-
|
|
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": "
|
|
376
|
+
"sessionless-node": "latest"
|
|
292
377
|
}
|
|
293
378
|
```
|