wiki-plugin-shoppe 0.0.21 โ†’ 0.0.22

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wiki-plugin-shoppe",
3
- "version": "0.0.21",
3
+ "version": "0.0.22",
4
4
  "description": "Multi-tenant digital goods shoppe for federated wiki, powered by Sanora",
5
5
  "keywords": [
6
6
  "wiki",
package/server/server.js CHANGED
@@ -48,6 +48,12 @@ function saveConfig(config) {
48
48
  fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
49
49
  }
50
50
 
51
+ // Derive the public-facing protocol from the request, respecting reverse-proxy headers.
52
+ // Behind HTTPS proxies req.protocol is 'http'; X-Forwarded-Proto carries the real value.
53
+ function reqProto(req) {
54
+ return (req.get('x-forwarded-proto') || req.protocol || 'https').split(',')[0].trim();
55
+ }
56
+
51
57
  function getSanoraUrl() {
52
58
  const config = loadConfig();
53
59
  if (config.sanoraUrl) return config.sanoraUrl.replace(/\/$/, '');
@@ -898,7 +904,7 @@ async function getShoppeGoods(tenant) {
898
904
  // Fetch and parse the schedule JSON artifact for an appointment product.
899
905
  async function getAppointmentSchedule(tenant, product) {
900
906
  const sanoraUrl = getSanoraUrl();
901
- const scheduleArtifact = (product.artifacts || []).find(a => a.includes('schedule'));
907
+ const scheduleArtifact = (product.artifacts || []).find(a => a.endsWith('.json'));
902
908
  if (!scheduleArtifact) return null;
903
909
  const resp = await fetch(`${sanoraUrl}/artifacts/${scheduleArtifact}`);
904
910
  if (!resp.ok) return null;
@@ -948,20 +954,15 @@ function generateAvailableSlots(schedule, bookedSlots) {
948
954
  const dateStr = dateFmt.format(date);
949
955
  const dayName = weekdayFmt.format(date).toLowerCase();
950
956
  const rule = (schedule.availability || []).find(a => a.day.toLowerCase() === dayName);
951
- if (!rule) continue;
952
-
953
- const [startH, startM] = rule.start.split(':').map(Number);
954
- const [endH, endM] = rule.end.split(':').map(Number);
955
- const startMins = startH * 60 + startM;
956
- const endMins = endH * 60 + endM;
957
+ if (!rule || !rule.slots || !rule.slots.length) continue;
957
958
 
958
959
  const slots = [];
959
- for (let m = startMins; m + duration <= endMins; m += duration) {
960
+ for (const slotTime of rule.slots) {
961
+ const [h, m] = slotTime.split(':').map(Number);
962
+ const slotMins = h * 60 + m;
960
963
  // For today, skip slots within the next hour
961
- if (d === 0 && m <= nowMins + 60) continue;
962
- const h = Math.floor(m / 60).toString().padStart(2, '0');
963
- const min = (m % 60).toString().padStart(2, '0');
964
- const slotStr = `${dateStr}T${h}:${min}`;
964
+ if (d === 0 && slotMins <= nowMins + 60) continue;
965
+ const slotStr = `${dateStr}T${slotTime}`;
965
966
  if (!bookedSet.has(slotStr)) slots.push(slotStr);
966
967
  }
967
968
 
@@ -979,7 +980,7 @@ function generateAvailableSlots(schedule, bookedSlots) {
979
980
  // Fetch tier metadata (benefits list, renewalDays) from the tier-info artifact.
980
981
  async function getTierInfo(tenant, product) {
981
982
  const sanoraUrl = getSanoraUrl();
982
- const tierArtifact = (product.artifacts || []).find(a => a.includes('tier-info'));
983
+ const tierArtifact = (product.artifacts || []).find(a => a.endsWith('.json'));
983
984
  if (!tierArtifact) return null;
984
985
  const resp = await fetch(`${sanoraUrl}/artifacts/${tierArtifact}`);
985
986
  if (!resp.ok) return null;
@@ -1049,7 +1050,7 @@ function generateShoppeHTML(tenant, goods) {
1049
1050
  { id: 'albums', label: '๐Ÿ–ผ๏ธ Albums', count: goods.albums.length },
1050
1051
  { id: 'products', label: '๐Ÿ“ฆ Products', count: goods.products.length },
1051
1052
  { id: 'appointments', label: '๐Ÿ“… Appointments', count: goods.appointments.length },
1052
- { id: 'subscriptions', label: '๐ŸŽ Support', count: goods.subscriptions.length }
1053
+ { id: 'subscriptions', label: '๐ŸŽ Infuse', count: goods.subscriptions.length }
1053
1054
  ]
1054
1055
  .filter(t => t.always || t.count > 0)
1055
1056
  .map((t, i) => `<div class="tab${i === 0 ? ' active' : ''}" onclick="show('${t.id}',this)">${t.label} <span class="badge">${t.count}</span></div>`)
@@ -1108,7 +1109,7 @@ function generateShoppeHTML(tenant, goods) {
1108
1109
  <div id="appointments" class="section"><div class="grid">${renderCards(goods.appointments, 'appointment')}</div></div>
1109
1110
  <div id="subscriptions" class="section"><div class="grid">${renderCards(goods.subscriptions, 'subscription')}</div></div>
1110
1111
  <div style="text-align:center;padding:24px 0 8px;font-size:14px;color:#888;">
1111
- Already a supporter? <a href="/plugin/shoppe/${tenant.uuid}/membership" style="color:#0066cc;">Access your membership โ†’</a>
1112
+ Already infusing? <a href="/plugin/shoppe/${tenant.uuid}/membership" style="color:#0066cc;">Access your membership โ†’</a>
1112
1113
  </div>
1113
1114
  </main>
1114
1115
  <script>
@@ -1265,7 +1266,7 @@ async function startServer(params) {
1265
1266
 
1266
1267
  const title = decodeURIComponent(req.params.title);
1267
1268
  const sanoraUrlInternal = getSanoraUrl();
1268
- const wikiOrigin = `${req.protocol}://${req.get('host')}`;
1269
+ const wikiOrigin = `${reqProto(req)}://${req.get('host')}`;
1269
1270
  const sanoraUrl = `${wikiOrigin}/plugin/allyabase/sanora`;
1270
1271
  const productsResp = await fetch(`${sanoraUrlInternal}/products/${tenant.uuid}`);
1271
1272
  const products = await productsResp.json();
@@ -1326,7 +1327,7 @@ async function startServer(params) {
1326
1327
  if (!product) return res.status(404).send('<h1>Appointment not found</h1>');
1327
1328
 
1328
1329
  const schedule = await getAppointmentSchedule(tenant, product);
1329
- const wikiOrigin = `${req.protocol}://${req.get('host')}`;
1330
+ const wikiOrigin = `${reqProto(req)}://${req.get('host')}`;
1330
1331
  const shoppeUrl = `${wikiOrigin}/plugin/shoppe/${tenant.uuid}`;
1331
1332
  const imageUrl = product.image ? `${sanoraUrl}/images/${product.image}` : '';
1332
1333
 
@@ -1393,7 +1394,7 @@ async function startServer(params) {
1393
1394
  if (!product) return res.status(404).send('<h1>Tier not found</h1>');
1394
1395
 
1395
1396
  const tierInfo = await getTierInfo(tenant, product);
1396
- const wikiOrigin = `${req.protocol}://${req.get('host')}`;
1397
+ const wikiOrigin = `${reqProto(req)}://${req.get('host')}`;
1397
1398
  const shoppeUrl = `${wikiOrigin}/plugin/shoppe/${tenant.uuid}`;
1398
1399
  const imageUrl = product.image ? `${sanoraUrl}/images/${product.image}` : '';
1399
1400
  const benefits = tierInfo && tierInfo.benefits
@@ -1425,7 +1426,7 @@ async function startServer(params) {
1425
1426
  app.get('/plugin/shoppe/:identifier/membership', (req, res) => {
1426
1427
  const tenant = getTenantByIdentifier(req.params.identifier);
1427
1428
  if (!tenant) return res.status(404).send('<h1>Shoppe not found</h1>');
1428
- const wikiOrigin = `${req.protocol}://${req.get('host')}`;
1429
+ const wikiOrigin = `${reqProto(req)}://${req.get('host')}`;
1429
1430
  const shoppeUrl = `${wikiOrigin}/plugin/shoppe/${tenant.uuid}`;
1430
1431
  const html = fillTemplate(SUBSCRIPTION_MEMBERSHIP_TMPL, { shoppeUrl, tenantUuid: tenant.uuid });
1431
1432
  res.set('Content-Type', 'text/html');
@@ -1444,7 +1445,7 @@ async function startServer(params) {
1444
1445
  const sanoraUrl = getSanoraUrl();
1445
1446
  const productsResp = await fetch(`${sanoraUrl}/products/${tenant.uuid}`);
1446
1447
  const products = await productsResp.json();
1447
- const wikiOrigin = `${req.protocol}://${req.get('host')}`;
1448
+ const wikiOrigin = `${reqProto(req)}://${req.get('host')}`;
1448
1449
  const shoppeUrl = `${wikiOrigin}/plugin/shoppe/${tenant.uuid}`;
1449
1450
 
1450
1451
  const subscriptions = [];
@@ -1459,7 +1460,7 @@ async function startServer(params) {
1459
1460
  // Only expose exclusive artifact URLs to active subscribers
1460
1461
  const exclusiveArtifacts = status.active
1461
1462
  ? (product.artifacts || [])
1462
- .filter(a => !a.includes('tier-info'))
1463
+ .filter(a => !a.endsWith('.json'))
1463
1464
  .map(a => ({ name: a.split('-').slice(1).join('-'), url: `${sanoraUrl}/artifacts/${a}` }))
1464
1465
  : [];
1465
1466
 
@@ -3,7 +3,7 @@
3
3
  <head>
4
4
  <meta charset="utf-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
6
- <title>Membership Portal</title>
6
+ <title>Infuse Portal</title>
7
7
  <style>
8
8
  *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
9
9
  body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #0f0f12; color: white; min-height: 100vh; }
@@ -75,14 +75,14 @@
75
75
  </head>
76
76
  <body>
77
77
  <header>
78
- <h1>๐ŸŽ Membership Portal</h1>
79
- <p>Enter your recovery key to access your membership benefits</p>
78
+ <h1>๐ŸŽ Infuse Portal</h1>
79
+ <p>Enter your recovery key to access your infuser benefits</p>
80
80
  </header>
81
81
 
82
82
  <div class="container">
83
83
 
84
84
  <div id="key-section">
85
- <h2>Check your membership</h2>
85
+ <h2>Check your infusions</h2>
86
86
  <p>Enter the recovery key you used when you subscribed.</p>
87
87
  <div class="input-row">
88
88
  <input type="text" id="recovery-key" placeholder="Your recovery keyโ€ฆ" autocomplete="off" onkeydown="if(event.key==='Enter')checkMembership()">
@@ -161,8 +161,8 @@
161
161
 
162
162
  const activeCount = subscriptions.filter(s => s.active).length;
163
163
  heading.textContent = activeCount > 0
164
- ? `You have ${activeCount} active membership${activeCount !== 1 ? 's' : ''}`
165
- : 'No active memberships found';
164
+ ? `You have ${activeCount} active infusion${activeCount !== 1 ? 's' : ''}`
165
+ : 'No active infusions found';
166
166
 
167
167
  container.innerHTML = '';
168
168
 
@@ -278,7 +278,7 @@
278
278
  const price = (sub.price / 100).toFixed(2);
279
279
  cta.innerHTML = `
280
280
  <a class="btn-subscribe" href="${sub.subscribeUrl}">
281
- ${sub.renewsAt ? 'Renew' : 'Subscribe'} โ€” $${price}/month
281
+ ${sub.renewsAt ? 'Renew' : 'Infuse'} โ€” $${price}/month
282
282
  </a>
283
283
  `;
284
284
  body.appendChild(cta);
@@ -4,11 +4,11 @@
4
4
  <script src="https://js.stripe.com/v3/"></script>
5
5
  <meta charset="utf-8">
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
7
- <meta name="twitter:title" content="Support: {{title}}">
7
+ <meta name="twitter:title" content="Infuse: {{title}}">
8
8
  <meta name="description" content="{{description}}">
9
- <meta name="og:title" content="Support: {{title}}">
9
+ <meta name="og:title" content="Infuse: {{title}}">
10
10
  <meta name="og:description" content="{{description}}">
11
- <title>Support: {{title}}</title>
11
+ <title>Infuse: {{title}}</title>
12
12
  <style>
13
13
  *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
14
14
  body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #0f0f12; color: white; min-height: 100vh; }
@@ -97,13 +97,13 @@
97
97
  <div class="pay-panel">
98
98
 
99
99
  <div id="already-subscribed">
100
- <h3>โœ… You're already a supporter!</h3>
100
+ <h3>โœ… You're already infusing!</h3>
101
101
  <p id="already-desc"></p>
102
102
  <button class="btn-outline" onclick="window.location.href='{{shoppeUrl}}/membership'">View your membership โ†’</button>
103
103
  </div>
104
104
 
105
105
  <div id="subscribe-form">
106
- <h2>Become a supporter</h2>
106
+ <h2>Become an infuser</h2>
107
107
  <div class="subtitle">Use a recovery key to access your membership and exclusive content.</div>
108
108
 
109
109
  <div class="recovery-note">
@@ -120,7 +120,7 @@
120
120
  </div>
121
121
 
122
122
  <div id="payment-section">
123
- <h2>Complete your support</h2>
123
+ <h2>Complete your infusion</h2>
124
124
  <div class="subtitle">${{formattedAmount}}/month ยท renews every {{renewalDays}} days</div>
125
125
  <div id="payment-element"></div>
126
126
  <button class="btn-primary" id="pay-btn" onclick="confirmPayment()">Pay ${{formattedAmount}}</button>
@@ -130,7 +130,7 @@
130
130
 
131
131
  <div id="confirmation">
132
132
  <div class="icon">๐ŸŽ‰</div>
133
- <h2>Thank you for your support!</h2>
133
+ <h2>Thank you for infusing!</h2>
134
134
  <div class="renews" id="renews-display"></div>
135
135
  <p>Use your recovery key any time at the <a href="{{shoppeUrl}}/membership" style="color:#7ec8e3;">membership portal</a> to access exclusive content and check your subscription status.</p>
136
136
  <p style="margin-top:12px;font-size:12px;color:#555;">Keep your recovery key safe โ€” it's the only way to access your membership benefits.</p>
package/test/test.js CHANGED
@@ -293,8 +293,8 @@ describe('Sanora integration', function() {
293
293
  const resp = await fetch(`${BASE_URL}/plugin/shoppe/${tenant.uuid}`);
294
294
  resp.status.should.equal(200);
295
295
  const html = await resp.text();
296
- html.should.include('Integration Test Shoppe');
297
- html.should.include('tab-btn');
296
+ html.should.include('Test Shoppe');
297
+ html.should.include('class="tab"');
298
298
  html.should.include('membership');
299
299
  });
300
300
 
@@ -315,7 +315,7 @@ describe('Sanora integration', function() {
315
315
  html.should.include('Test Session');
316
316
  html.should.include('/slots');
317
317
  html.should.include('purchase/intent');
318
- html.should.include('Confirm Booking'); // free booking label (price=0 in test archive)
318
+ html.should.include('Continue to Payment'); // paid booking label (price=10000 in test archive)
319
319
  });
320
320
 
321
321
  it('should serve the subscription sign-up page with benefits', async () => {