wiki-plugin-shoppe 0.0.42 → 0.0.45

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,7 +2,7 @@
2
2
 
3
3
  Multi-tenant digital goods shoppe for Federated Wiki, powered by Sanora.
4
4
 
5
- **Current version**: 0.0.35
5
+ **Current version**: 0.0.43
6
6
 
7
7
  ## Architecture
8
8
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wiki-plugin-shoppe",
3
- "version": "0.0.42",
3
+ "version": "0.0.45",
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
@@ -8,6 +8,15 @@ const multer = require('multer');
8
8
  const FormData = require('form-data');
9
9
  const AdmZip = require('adm-zip');
10
10
  const sessionless = require('sessionless-node');
11
+ const { secp256k1: _secp256k1 } = require('ethereum-cryptography/secp256k1');
12
+ const { keccak256: _keccak256 } = require('ethereum-cryptography/keccak.js');
13
+ const { utf8ToBytes: _utf8ToBytes } = require('ethereum-cryptography/utils.js');
14
+
15
+ // Race-safe signing: bypasses the shared sessionless.getKeys singleton.
16
+ function signMessage(message, privateKey) {
17
+ const hash = _keccak256(_utf8ToBytes(message));
18
+ return _secp256k1.sign(hash, privateKey).toCompactHex();
19
+ }
11
20
 
12
21
  // Stripe is used directly for Terminal (card-present) payments.
13
22
  // Set STRIPE_SECRET_KEY in the environment. If absent, Terminal endpoints return 503.
@@ -75,10 +84,24 @@ function getSanoraUrl() {
75
84
  }
76
85
 
77
86
  function getAddieUrl() {
78
- try { return new URL(getSanoraUrl()).origin + '/plugin/allyabase/addie'; } catch { /* fall through */ }
87
+ const sanora = getSanoraUrl();
88
+ try {
89
+ const url = new URL(sanora);
90
+ // Only derive from origin when sanora is a wiki proxy URL (has a path component).
91
+ // A bare host:port URL (e.g. http://localhost:7243) means Addie is on its own port.
92
+ if (url.pathname && url.pathname !== '/') {
93
+ return url.origin + '/plugin/allyabase/addie';
94
+ }
95
+ } catch { /* fall through */ }
79
96
  return `http://localhost:${process.env.ADDIE_PORT || 3005}`;
80
97
  }
81
98
 
99
+ // Returns the public-facing Sanora URL (via wiki proxy) for browser-visible resource URLs.
100
+ // Always constructed from the current request so it matches the host the browser is using.
101
+ function getSanoraPublicUrl(req) {
102
+ return `${reqProto(req)}://${req.get('host')}/plugin/allyabase/sanora`;
103
+ }
104
+
82
105
  function getLucilleUrl() {
83
106
  const config = loadConfig();
84
107
  if (config.lucilleUrl) return config.lucilleUrl.replace(/\/$/, '');
@@ -111,9 +134,8 @@ async function getOrCreateAffiliateAddieUser(shopperePublicKey) {
111
134
  if (affiliates[shopperePublicKey]) return affiliates[shopperePublicKey];
112
135
 
113
136
  const addieKeys = await sessionless.generateKeys(() => {}, () => null);
114
- sessionless.getKeys = () => addieKeys;
115
137
  const timestamp = Date.now().toString();
116
- const signature = await sessionless.sign(timestamp + addieKeys.pubKey);
138
+ const signature = signMessage(timestamp + addieKeys.pubKey, addieKeys.privateKey);
117
139
 
118
140
  const resp = await fetch(`${getAddieUrl()}/user/create`, {
119
141
  method: 'PUT',
@@ -142,10 +164,9 @@ async function getOrCreateBuyerAddieUser(recoveryKey, productId) {
142
164
  if (buyers[buyerKey]) return buyers[buyerKey];
143
165
 
144
166
  const addieKeys = await sessionless.generateKeys(() => {}, () => null);
145
- sessionless.getKeys = () => addieKeys;
146
167
  const timestamp = Date.now().toString();
147
168
  const message = timestamp + addieKeys.pubKey;
148
- const signature = await sessionless.sign(message);
169
+ const signature = signMessage(message, addieKeys.privateKey);
149
170
 
150
171
  const resp = await fetch(`${getAddieUrl()}/user/create`, {
151
172
  method: 'PUT',
@@ -183,10 +204,9 @@ async function getOrCreateBuyerAddieUserByPubKey(pubKey, productId) {
183
204
  if (buyers[buyerKey]) return buyers[buyerKey];
184
205
 
185
206
  const addieKeys = await sessionless.generateKeys(() => {}, () => null);
186
- sessionless.getKeys = () => addieKeys;
187
207
  const timestamp = Date.now().toString();
188
208
  const message = timestamp + addieKeys.pubKey;
189
- const signature = await sessionless.sign(message);
209
+ const signature = signMessage(message, addieKeys.privateKey);
190
210
 
191
211
  const resp = await fetch(`${getAddieUrl()}/user/create`, {
192
212
  method: 'PUT',
@@ -208,9 +228,8 @@ async function getOrCreateBuyerAddieUserByPubKey(pubKey, productId) {
208
228
  async function hasPurchasedByPubKey(tenant, pubKey, productId) {
209
229
  const orderKey = crypto.createHash('sha256').update(pubKey + productId).digest('hex');
210
230
  const sanoraUrl = getSanoraUrl();
211
- sessionless.getKeys = () => tenant.keys;
212
231
  const timestamp = Date.now().toString();
213
- const signature = await sessionless.sign(timestamp + tenant.uuid);
232
+ const signature = signMessage(timestamp + tenant.uuid, tenant.keys.privateKey);
214
233
  try {
215
234
  const resp = await fetch(
216
235
  `${sanoraUrl}/user/${tenant.uuid}/orders/${encodeURIComponent(productId)}` +
@@ -479,10 +498,9 @@ function generateEmojicode(tenants) {
479
498
 
480
499
  async function addieCreateUser() {
481
500
  const addieKeys = await sessionless.generateKeys(() => {}, () => null);
482
- sessionless.getKeys = () => addieKeys;
483
501
  const timestamp = Date.now().toString();
484
502
  const message = timestamp + addieKeys.pubKey;
485
- const signature = await sessionless.sign(message);
503
+ const signature = signMessage(message, addieKeys.privateKey);
486
504
 
487
505
  const resp = await fetch(`${getAddieUrl()}/user/create`, {
488
506
  method: 'PUT',
@@ -501,10 +519,9 @@ async function registerTenant(name) {
501
519
 
502
520
  // Create a dedicated Sanora user for this tenant
503
521
  const keys = await sessionless.generateKeys(() => {}, () => null);
504
- sessionless.getKeys = () => keys;
505
522
  const timestamp = Date.now().toString();
506
523
  const message = timestamp + keys.pubKey;
507
- const signature = await sessionless.sign(message);
524
+ const signature = signMessage(message, keys.privateKey);
508
525
 
509
526
  const resp = await fetch(`${getSanoraUrl()}/user/create`, {
510
527
  method: 'PUT',
@@ -605,8 +622,7 @@ async function sanoraEnsureUser(tenant) {
605
622
  const { keys } = tenant;
606
623
  const timestamp = Date.now().toString();
607
624
  const message = timestamp + keys.pubKey;
608
- sessionless.getKeys = () => keys;
609
- const signature = await sessionless.sign(message);
625
+ const signature = signMessage(message, keys.privateKey);
610
626
 
611
627
  const resp = await fetch(`${getSanoraUrl()}/user/create`, {
612
628
  method: 'PUT',
@@ -656,8 +672,7 @@ async function sanoraCreateProduct(tenant, title, category, description, price,
656
672
  const safePrice = price || 0;
657
673
  const message = timestamp + uuid + title + (description || '') + safePrice;
658
674
 
659
- sessionless.getKeys = () => keys;
660
- const signature = await sessionless.sign(message);
675
+ const signature = signMessage(message, keys.privateKey);
661
676
 
662
677
  const resp = await fetchWithRetry(
663
678
  `${getSanoraUrl()}/user/${uuid}/product/${encodeURIComponent(title)}`,
@@ -707,9 +722,8 @@ async function sanoraCreateProductResilient(tenant, title, category, description
707
722
  async function sanoraUploadArtifact(tenant, title, fileBuffer, filename, artifactType) {
708
723
  const { uuid, keys } = tenant;
709
724
  const timestamp = Date.now().toString();
710
- sessionless.getKeys = () => keys;
711
725
  const message = timestamp + uuid + title;
712
- const signature = await sessionless.sign(message);
726
+ const signature = signMessage(message, keys.privateKey);
713
727
 
714
728
  const form = new FormData();
715
729
  form.append('artifact', fileBuffer, { filename, contentType: getMimeType(filename) });
@@ -741,9 +755,8 @@ async function sanoraUploadArtifact(tenant, title, fileBuffer, filename, artifac
741
755
  async function sanoraUploadImage(tenant, title, imageBuffer, filename) {
742
756
  const { uuid, keys } = tenant;
743
757
  const timestamp = Date.now().toString();
744
- sessionless.getKeys = () => keys;
745
758
  const message = timestamp + uuid + title;
746
- const signature = await sessionless.sign(message);
759
+ const signature = signMessage(message, keys.privateKey);
747
760
 
748
761
  const form = new FormData();
749
762
  form.append('image', imageBuffer, { filename, contentType: getMimeType(filename) });
@@ -778,8 +791,7 @@ async function sanoraDeleteProduct(tenant, title) {
778
791
  const timestamp = Date.now().toString();
779
792
  const message = timestamp + uuid + title;
780
793
 
781
- sessionless.getKeys = () => keys;
782
- const signature = await sessionless.sign(message);
794
+ const signature = signMessage(message, keys.privateKey);
783
795
 
784
796
  await fetch(
785
797
  `${getSanoraUrl()}/user/${uuid}/product/${encodeURIComponent(title)}?timestamp=${timestamp}&signature=${encodeURIComponent(signature)}`,
@@ -792,10 +804,9 @@ async function sanoraDeleteProduct(tenant, title) {
792
804
  async function lucilleCreateUser(lucilleUrl) {
793
805
  const url = lucilleUrl || getLucilleUrl();
794
806
  const keys = await sessionless.generateKeys(() => {}, () => null);
795
- sessionless.getKeys = () => keys;
796
807
  const timestamp = Date.now().toString();
797
808
  const message = timestamp + keys.pubKey;
798
- const signature = await sessionless.sign(message);
809
+ const signature = signMessage(message, keys.privateKey);
799
810
 
800
811
  const resp = await fetch(`${url}/user/create`, {
801
812
  method: 'PUT',
@@ -824,8 +835,7 @@ async function lucilleRegisterVideo(tenant, title, description, tags, lucilleUrl
824
835
  const { lucilleKeys } = tenant;
825
836
  if (!lucilleKeys) throw new Error('Tenant has no Lucille user — re-register to enable video uploads');
826
837
  const timestamp = Date.now().toString();
827
- sessionless.getKeys = () => lucilleKeys;
828
- const signature = await sessionless.sign(timestamp + lucilleKeys.pubKey);
838
+ const signature = signMessage(timestamp + lucilleKeys.pubKey, lucilleKeys.privateKey);
829
839
 
830
840
  const resp = await fetch(
831
841
  `${url}/user/${lucilleKeys.uuid}/video/${encodeURIComponent(title)}`,
@@ -846,8 +856,7 @@ async function lucilleUploadVideo(tenant, title, fileBuffer, filename, lucilleUr
846
856
  const { lucilleKeys } = tenant;
847
857
  if (!lucilleKeys) throw new Error('Tenant has no Lucille user');
848
858
  const timestamp = Date.now().toString();
849
- sessionless.getKeys = () => lucilleKeys;
850
- const signature = await sessionless.sign(timestamp + lucilleKeys.pubKey);
859
+ const signature = signMessage(timestamp + lucilleKeys.pubKey, lucilleKeys.privateKey);
851
860
 
852
861
  const form = new FormData();
853
862
  form.append('video', fileBuffer, { filename, contentType: getMimeType(filename) });
@@ -1506,7 +1515,7 @@ async function processArchive(zipPath, onProgress = () => {}) {
1506
1515
  // PORTFOLIO PAGE GENERATION
1507
1516
  // ============================================================
1508
1517
 
1509
- async function getShoppeGoods(tenant) {
1518
+ async function getShoppeGoods(tenant, imageBaseUrl) {
1510
1519
  let products = {};
1511
1520
  try {
1512
1521
  const resp = await fetch(`${getSanoraUrl()}/products/${tenant.uuid}`, { timeout: 15000 });
@@ -1561,7 +1570,7 @@ async function getShoppeGoods(tenant) {
1561
1570
  description: product.description || '',
1562
1571
  price: product.price || 0,
1563
1572
  shipping: product.shipping || 0,
1564
- image: product.image ? `${getSanoraUrl()}/images/${product.image}` : null,
1573
+ image: product.image ? `${imageBaseUrl || getSanoraUrl()}/images/${product.image}` : null,
1565
1574
  url: resolvedUrl,
1566
1575
  ...(isPost && { category: product.category, tags: product.tags || '' }),
1567
1576
  ...(lucillePlayerUrl && { lucillePlayerUrl }),
@@ -1616,9 +1625,8 @@ async function getAppointmentSchedule(tenant, product) {
1616
1625
  async function getBookedSlots(tenant, productId) {
1617
1626
  const sanoraUrl = getSanoraUrl();
1618
1627
  const tenantKeys = tenant.keys;
1619
- sessionless.getKeys = () => tenantKeys;
1620
1628
  const timestamp = Date.now().toString();
1621
- const signature = await sessionless.sign(timestamp + tenant.uuid);
1629
+ const signature = signMessage(timestamp + tenant.uuid, tenantKeys.privateKey);
1622
1630
  const resp = await fetch(
1623
1631
  `${sanoraUrl}/user/${tenant.uuid}/orders/${encodeURIComponent(productId)}?timestamp=${timestamp}&signature=${encodeURIComponent(signature)}`
1624
1632
  );
@@ -1695,9 +1703,8 @@ async function getSubscriptionStatus(tenant, productId, recoveryKey) {
1695
1703
  const orderKey = crypto.createHash('sha256').update(recoveryKey + productId).digest('hex');
1696
1704
  const sanoraUrl = getSanoraUrl();
1697
1705
  const tenantKeys = tenant.keys;
1698
- sessionless.getKeys = () => tenantKeys;
1699
1706
  const timestamp = Date.now().toString();
1700
- const signature = await sessionless.sign(timestamp + tenant.uuid);
1707
+ const signature = signMessage(timestamp + tenant.uuid, tenantKeys.privateKey);
1701
1708
  try {
1702
1709
  const resp = await fetch(
1703
1710
  `${sanoraUrl}/user/${tenant.uuid}/orders/${encodeURIComponent(productId)}?timestamp=${timestamp}&signature=${encodeURIComponent(signature)}`
@@ -1748,12 +1755,11 @@ async function getAllOrders(tenant) {
1748
1755
  console.warn(`[shoppe] getAllOrders: Sanora unreachable — ${err.message}`);
1749
1756
  }
1750
1757
 
1751
- sessionless.getKeys = () => tenant.keys;
1752
1758
 
1753
1759
  const results = [];
1754
1760
  for (const [title, product] of Object.entries(products)) {
1755
1761
  const timestamp = Date.now().toString();
1756
- const signature = await sessionless.sign(timestamp + tenant.uuid);
1762
+ const signature = signMessage(timestamp + tenant.uuid, tenant.keys.privateKey);
1757
1763
  try {
1758
1764
  const resp = await fetch(
1759
1765
  `${sanoraUrl}/user/${tenant.uuid}/orders/${encodeURIComponent(product.productId)}` +
@@ -3215,7 +3221,7 @@ async function startServer(params) {
3215
3221
  const product = products[title] || Object.values(products).find(p => p.title === title);
3216
3222
  if (!product) return res.status(404).send('<h1>Product not found</h1>');
3217
3223
 
3218
- const imageUrl = product.image ? `${sanoraUrlInternal}/images/${product.image}` : '';
3224
+ const imageUrl = product.image ? `${sanoraUrl}/images/${product.image}` : '';
3219
3225
  const ebookUrl = `${wikiOrigin}/plugin/shoppe/${tenant.uuid}/download/${encodeURIComponent(title)}`;
3220
3226
  const shoppeUrl = `${wikiOrigin}/plugin/shoppe/${tenant.uuid}`;
3221
3227
  const payees = tenant.addieKeys
@@ -3286,7 +3292,7 @@ async function startServer(params) {
3286
3292
  const schedule = await getAppointmentSchedule(tenant, product);
3287
3293
  const wikiOrigin = `${reqProto(req)}://${req.get('host')}`;
3288
3294
  const shoppeUrl = `${wikiOrigin}/plugin/shoppe/${tenant.uuid}`;
3289
- const imageUrl = product.image ? `${sanoraUrl}/images/${product.image}` : '';
3295
+ const imageUrl = product.image ? `${getSanoraPublicUrl(req)}/images/${product.image}` : '';
3290
3296
 
3291
3297
  const price = product.price || 0;
3292
3298
  const html = fillTemplate(APPOINTMENT_BOOKING_TMPL, {
@@ -3354,7 +3360,7 @@ async function startServer(params) {
3354
3360
  const tierInfo = await getTierInfo(tenant, product);
3355
3361
  const wikiOrigin = `${reqProto(req)}://${req.get('host')}`;
3356
3362
  const shoppeUrl = `${wikiOrigin}/plugin/shoppe/${tenant.uuid}`;
3357
- const imageUrl = product.image ? `${sanoraUrl}/images/${product.image}` : '';
3363
+ const imageUrl = product.image ? `${getSanoraPublicUrl(req)}/images/${product.image}` : '';
3358
3364
  const benefits = tierInfo && tierInfo.benefits
3359
3365
  ? tierInfo.benefits.map(b => `<li>${escHtml(b)}</li>`).join('')
3360
3366
  : '';
@@ -3426,10 +3432,9 @@ async function startServer(params) {
3426
3432
  }
3427
3433
 
3428
3434
  const addieKeys = { pubKey: tenant.addieKeys.pubKey, privateKey: tenant.addieKeys.privateKey };
3429
- sessionless.getKeys = () => addieKeys;
3430
3435
  const timestamp = Date.now().toString();
3431
3436
  const message = timestamp + tenant.addieKeys.uuid;
3432
- const signature = await sessionless.sign(message);
3437
+ const signature = signMessage(message, addieKeys.privateKey);
3433
3438
 
3434
3439
  const wikiOrigin = `${reqProto(req)}://${req.get('host')}`;
3435
3440
  const returnUrl = `${wikiOrigin}/plugin/shoppe/${tenant.uuid}/payouts/return`;
@@ -3536,7 +3541,7 @@ async function startServer(params) {
3536
3541
  productId: product.productId,
3537
3542
  description: product.description || '',
3538
3543
  price: product.price || 0,
3539
- image: product.image ? `${sanoraUrl}/images/${product.image}` : null,
3544
+ image: product.image ? `${getSanoraPublicUrl(req)}/images/${product.image}` : null,
3540
3545
  benefits: tierInfo ? (tierInfo.benefits || []) : [],
3541
3546
  renewalDays: tierInfo ? (tierInfo.renewalDays || 30) : 30,
3542
3547
  active: status.active,
@@ -3673,9 +3678,8 @@ async function startServer(params) {
3673
3678
  : tenant.addieKeys ? [{ pubKey: tenant.addieKeys.pubKey, amount }] : [];
3674
3679
  }
3675
3680
  const buyerKeys = { pubKey: buyer.pubKey, privateKey: buyer.privateKey };
3676
- sessionless.getKeys = () => buyerKeys;
3677
3681
  const intentTimestamp = Date.now().toString();
3678
- const intentSignature = await sessionless.sign(intentTimestamp + buyer.uuid + amount + 'USD');
3682
+ const intentSignature = signMessage(intentTimestamp + buyer.uuid + amount + 'USD', buyerKeys.privateKey);
3679
3683
  const intentResp = await fetch(`${getAddieUrl()}/user/${buyer.uuid}/processor/stripe/intent`, {
3680
3684
  method: 'POST',
3681
3685
  headers: { 'Content-Type': 'application/json' },
@@ -3728,9 +3732,8 @@ async function startServer(params) {
3728
3732
  // Shoppere subscription: orderKey = sha256(pubKey + productId)
3729
3733
  const orderKey = crypto.createHash('sha256').update(pubKey + productId).digest('hex');
3730
3734
  const tenantKeys = tenant.keys;
3731
- sessionless.getKeys = () => tenantKeys;
3732
3735
  const ts = Date.now().toString();
3733
- const sig = await sessionless.sign(ts + tenant.uuid);
3736
+ const sig = signMessage(ts + tenant.uuid, tenantKeys.privateKey);
3734
3737
  const order = { orderKey, pubKey, paidAt: Date.now(), title, productId, renewalDays: renewalDays || 30, status: 'active' };
3735
3738
  await fetch(`${sanoraUrlInternal}/user/${tenant.uuid}/orders`, {
3736
3739
  method: 'PUT',
@@ -3745,9 +3748,8 @@ async function startServer(params) {
3745
3748
  // Shoppere appointment: record booking with pubKey credential
3746
3749
  const orderKey = crypto.createHash('sha256').update(pubKey + productId).digest('hex');
3747
3750
  const tenantKeys = tenant.keys;
3748
- sessionless.getKeys = () => tenantKeys;
3749
3751
  const bookingTimestamp = Date.now().toString();
3750
- const bookingSignature = await sessionless.sign(bookingTimestamp + tenant.uuid);
3752
+ const bookingSignature = signMessage(bookingTimestamp + tenant.uuid, tenantKeys.privateKey);
3751
3753
  const order = {
3752
3754
  orderKey,
3753
3755
  pubKey,
@@ -3770,9 +3772,8 @@ async function startServer(params) {
3770
3772
  // Shoppere digital product: record purchase with pubKey as credential
3771
3773
  const orderKey = crypto.createHash('sha256').update(pubKey + productId).digest('hex');
3772
3774
  const tenantKeys = tenant.keys;
3773
- sessionless.getKeys = () => tenantKeys;
3774
3775
  const ts = Date.now().toString();
3775
- const sig = await sessionless.sign(ts + tenant.uuid);
3776
+ const sig = signMessage(ts + tenant.uuid, tenantKeys.privateKey);
3776
3777
  const order = { orderKey, pubKey, paidAt: Date.now(), title, productId, status: 'purchased' };
3777
3778
  await fetch(`${sanoraUrlInternal}/user/${tenant.uuid}/orders`, {
3778
3779
  method: 'PUT',
@@ -3798,9 +3799,8 @@ async function startServer(params) {
3798
3799
  // The recovery key itself is never stored; orderKey = sha256(recoveryKey + productId).
3799
3800
  const orderKey = crypto.createHash('sha256').update(recoveryKey + productId).digest('hex');
3800
3801
  const tenantKeys = tenant.keys;
3801
- sessionless.getKeys = () => tenantKeys;
3802
3802
  const ts = Date.now().toString();
3803
- const sig = await sessionless.sign(ts + tenant.uuid);
3803
+ const sig = signMessage(ts + tenant.uuid, tenantKeys.privateKey);
3804
3804
  const order = { orderKey, paidAt: Date.now(), title, productId, renewalDays: renewalDays || 30, status: 'active' };
3805
3805
  await fetch(`${sanoraUrlInternal}/user/${tenant.uuid}/orders`, {
3806
3806
  method: 'PUT',
@@ -3819,9 +3819,8 @@ async function startServer(params) {
3819
3819
 
3820
3820
  // Record the booking in Sanora (contact info flows through the server, never direct from browser)
3821
3821
  const tenantKeys = tenant.keys;
3822
- sessionless.getKeys = () => tenantKeys;
3823
3822
  const bookingTimestamp = Date.now().toString();
3824
- const bookingSignature = await sessionless.sign(bookingTimestamp + tenant.uuid);
3823
+ const bookingSignature = signMessage(bookingTimestamp + tenant.uuid, tenantKeys.privateKey);
3825
3824
  const order = {
3826
3825
  productId,
3827
3826
  title,
@@ -3851,9 +3850,8 @@ async function startServer(params) {
3851
3850
  // Physical product — record order in Sanora signed by the tenant.
3852
3851
  // The shippingAddress is collected here (post-payment) and sent once, server-side.
3853
3852
  const tenantKeys = tenant.keys;
3854
- sessionless.getKeys = () => tenantKeys;
3855
3853
  const orderTimestamp = Date.now().toString();
3856
- const orderSignature = await sessionless.sign(orderTimestamp + tenant.uuid);
3854
+ const orderSignature = signMessage(orderTimestamp + tenant.uuid, tenantKeys.privateKey);
3857
3855
  const order = {
3858
3856
  productId,
3859
3857
  title,
@@ -3972,8 +3970,7 @@ async function startServer(params) {
3972
3970
  // Record order in Sanora (fire-and-forget)
3973
3971
  if (productId && tenant.keys) {
3974
3972
  const ts = Date.now().toString();
3975
- sessionless.getKeys = () => tenant.keys;
3976
- const orderSig = await sessionless.sign(ts + tenant.uuid).catch(() => null);
3973
+ const orderSig = signMessage(ts + tenant.uuid).catch(() => null, tenant.keys.privateKey);
3977
3974
  if (orderSig) {
3978
3975
  const order = {
3979
3976
  productId,
@@ -4053,14 +4050,15 @@ async function startServer(params) {
4053
4050
  if (!purchased) return res.status(403).send('<h1>No purchase found for this key</h1>');
4054
4051
  }
4055
4052
 
4056
- const imageUrl = product.image ? `${sanoraUrl}/images/${product.image}` : '';
4053
+ const sanoraPublicUrl = getSanoraPublicUrl(req);
4054
+ const imageUrl = product.image ? `${sanoraPublicUrl}/images/${product.image}` : '';
4057
4055
 
4058
4056
  // Map artifact UUIDs to download paths by extension
4059
4057
  let epubPath = '', pdfPath = '', mobiPath = '';
4060
4058
  (product.artifacts || []).forEach(artifact => {
4061
- if (artifact.includes('epub')) epubPath = `${sanoraUrl}/artifacts/${artifact}`;
4062
- if (artifact.includes('pdf')) pdfPath = `${sanoraUrl}/artifacts/${artifact}`;
4063
- if (artifact.includes('mobi')) mobiPath = `${sanoraUrl}/artifacts/${artifact}`;
4059
+ if (artifact.includes('epub')) epubPath = `${sanoraPublicUrl}/artifacts/${artifact}`;
4060
+ if (artifact.includes('pdf')) pdfPath = `${sanoraPublicUrl}/artifacts/${artifact}`;
4061
+ if (artifact.includes('mobi')) mobiPath = `${sanoraPublicUrl}/artifacts/${artifact}`;
4064
4062
  });
4065
4063
 
4066
4064
  const html = fillTemplate(EBOOK_DOWNLOAD_TMPL, {
@@ -4106,7 +4104,7 @@ async function startServer(params) {
4106
4104
  const fm = parseFrontMatter(mdContent);
4107
4105
  const postTitle = fm.title || title;
4108
4106
  const postDate = fm.date || '';
4109
- const imageUrl = product.image ? `${getSanoraUrl()}/images/${product.image}` : null;
4107
+ const imageUrl = product.image ? `${getSanoraPublicUrl(req)}/images/${product.image}` : null;
4110
4108
 
4111
4109
  res.set('Content-Type', 'text/html');
4112
4110
  res.send(generatePostHTML(tenant, postTitle, postDate, imageUrl, fm.body || mdContent));
@@ -4135,8 +4133,7 @@ async function startServer(params) {
4135
4133
  const { uuid: lucilleUuid, pubKey, privateKey } = tenant.lucilleKeys;
4136
4134
 
4137
4135
  const timestamp = Date.now().toString();
4138
- sessionless.getKeys = () => ({ pubKey, privateKey });
4139
- const signature = await sessionless.sign(timestamp + pubKey);
4136
+ const signature = signMessage(timestamp + pubKey, privateKey);
4140
4137
 
4141
4138
  const uploadUrl = `${lucilleBase}/user/${lucilleUuid}/video/${encodeURIComponent(title)}/file`;
4142
4139
  res.json({ uploadUrl, timestamp, signature });
@@ -4182,7 +4179,7 @@ async function startServer(params) {
4182
4179
  productId: product.productId,
4183
4180
  title: product.title,
4184
4181
  category: product.category,
4185
- image: product.image ? `${sanoraUrl}/images/${product.image}` : null,
4182
+ image: product.image ? `${getSanoraPublicUrl(req)}/images/${product.image}` : null,
4186
4183
  price: product.price,
4187
4184
  paidAt: match.paidAt,
4188
4185
  status: match.status,
@@ -4208,7 +4205,7 @@ async function startServer(params) {
4208
4205
  try {
4209
4206
  const tenant = getTenantByIdentifier(req.params.identifier);
4210
4207
  if (!tenant) return res.status(404).json({ error: 'Shoppe not found' });
4211
- const goods = await getShoppeGoods(tenant);
4208
+ const goods = await getShoppeGoods(tenant, getSanoraPublicUrl(req));
4212
4209
  const cat = req.query.category;
4213
4210
  res.json({ success: true, goods: (cat && goods[cat]) ? goods[cat] : goods });
4214
4211
  } catch (err) {
@@ -4230,7 +4227,7 @@ async function startServer(params) {
4230
4227
  const tracks = [];
4231
4228
  for (const [key, product] of Object.entries(products)) {
4232
4229
  if (product.category !== 'music') continue;
4233
- const cover = product.image ? `${sanoraUrl}/images/${product.image}` : null;
4230
+ const cover = product.image ? `${getSanoraPublicUrl(req)}/images/${product.image}` : null;
4234
4231
  const artifacts = product.artifacts || [];
4235
4232
  if (artifacts.length > 1) {
4236
4233
  albums.push({
@@ -4264,7 +4261,7 @@ async function startServer(params) {
4264
4261
  try {
4265
4262
  const tenant = getTenantByIdentifier(req.params.identifier);
4266
4263
  if (!tenant) return res.status(404).send('<h1>Shoppe not found</h1>');
4267
- const goods = await getShoppeGoods(tenant);
4264
+ const goods = await getShoppeGoods(tenant, getSanoraPublicUrl(req));
4268
4265
 
4269
4266
  // Check if the request carries a valid owner signature — if so, embed auth
4270
4267
  // params in the page so the upload button can authenticate with upload-info.
@@ -193,7 +193,7 @@
193
193
  return p;
194
194
  }).filter(p => p.pubKey);
195
195
  }
196
- const URL_PAYEES = parseUrlPayees();
196
+ var URL_PAYEES = parseUrlPayees();
197
197
  </script>
198
198
 
199
199
  <!-- ── Shoppere (pubKey) path ────────────────────────────────────────────── -->
@@ -202,7 +202,7 @@
202
202
  const _buyerPubKey = '{{buyerPubKey}}' || new URLSearchParams(window.location.search).get('pubKey') || '';
203
203
  const _buyerTimestamp = '{{buyerTimestamp}}' || new URLSearchParams(window.location.search).get('timestamp') || '';
204
204
  const _buyerSignature = '{{buyerSignature}}' || new URLSearchParams(window.location.search).get('signature') || '';
205
- const IS_SHOPPERE = !!(_buyerPubKey && _buyerTimestamp && _buyerSignature);
205
+ var IS_SHOPPERE = !!(_buyerPubKey && _buyerTimestamp && _buyerSignature);
206
206
  // Affiliate (NFC proximity charge / referral link) param — present when buyer arrived via a referred link
207
207
  const _affiliatePubKey = new URLSearchParams(window.location.search).get('affiliatePubKey') || '';
208
208
  const IS_AFFILIATE_CHARGE = !!_affiliatePubKey;