wiki-plugin-shoppe 0.0.41 → 0.0.43

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.41",
3
+ "version": "0.0.43",
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.
@@ -111,9 +120,8 @@ async function getOrCreateAffiliateAddieUser(shopperePublicKey) {
111
120
  if (affiliates[shopperePublicKey]) return affiliates[shopperePublicKey];
112
121
 
113
122
  const addieKeys = await sessionless.generateKeys(() => {}, () => null);
114
- sessionless.getKeys = () => addieKeys;
115
123
  const timestamp = Date.now().toString();
116
- const signature = await sessionless.sign(timestamp + addieKeys.pubKey);
124
+ const signature = signMessage(timestamp + addieKeys.pubKey, addieKeys.privateKey);
117
125
 
118
126
  const resp = await fetch(`${getAddieUrl()}/user/create`, {
119
127
  method: 'PUT',
@@ -142,10 +150,9 @@ async function getOrCreateBuyerAddieUser(recoveryKey, productId) {
142
150
  if (buyers[buyerKey]) return buyers[buyerKey];
143
151
 
144
152
  const addieKeys = await sessionless.generateKeys(() => {}, () => null);
145
- sessionless.getKeys = () => addieKeys;
146
153
  const timestamp = Date.now().toString();
147
154
  const message = timestamp + addieKeys.pubKey;
148
- const signature = await sessionless.sign(message);
155
+ const signature = signMessage(message, addieKeys.privateKey);
149
156
 
150
157
  const resp = await fetch(`${getAddieUrl()}/user/create`, {
151
158
  method: 'PUT',
@@ -183,10 +190,9 @@ async function getOrCreateBuyerAddieUserByPubKey(pubKey, productId) {
183
190
  if (buyers[buyerKey]) return buyers[buyerKey];
184
191
 
185
192
  const addieKeys = await sessionless.generateKeys(() => {}, () => null);
186
- sessionless.getKeys = () => addieKeys;
187
193
  const timestamp = Date.now().toString();
188
194
  const message = timestamp + addieKeys.pubKey;
189
- const signature = await sessionless.sign(message);
195
+ const signature = signMessage(message, addieKeys.privateKey);
190
196
 
191
197
  const resp = await fetch(`${getAddieUrl()}/user/create`, {
192
198
  method: 'PUT',
@@ -208,9 +214,8 @@ async function getOrCreateBuyerAddieUserByPubKey(pubKey, productId) {
208
214
  async function hasPurchasedByPubKey(tenant, pubKey, productId) {
209
215
  const orderKey = crypto.createHash('sha256').update(pubKey + productId).digest('hex');
210
216
  const sanoraUrl = getSanoraUrl();
211
- sessionless.getKeys = () => tenant.keys;
212
217
  const timestamp = Date.now().toString();
213
- const signature = await sessionless.sign(timestamp + tenant.uuid);
218
+ const signature = signMessage(timestamp + tenant.uuid, tenant.keys.privateKey);
214
219
  try {
215
220
  const resp = await fetch(
216
221
  `${sanoraUrl}/user/${tenant.uuid}/orders/${encodeURIComponent(productId)}` +
@@ -479,10 +484,9 @@ function generateEmojicode(tenants) {
479
484
 
480
485
  async function addieCreateUser() {
481
486
  const addieKeys = await sessionless.generateKeys(() => {}, () => null);
482
- sessionless.getKeys = () => addieKeys;
483
487
  const timestamp = Date.now().toString();
484
488
  const message = timestamp + addieKeys.pubKey;
485
- const signature = await sessionless.sign(message);
489
+ const signature = signMessage(message, addieKeys.privateKey);
486
490
 
487
491
  const resp = await fetch(`${getAddieUrl()}/user/create`, {
488
492
  method: 'PUT',
@@ -501,10 +505,9 @@ async function registerTenant(name) {
501
505
 
502
506
  // Create a dedicated Sanora user for this tenant
503
507
  const keys = await sessionless.generateKeys(() => {}, () => null);
504
- sessionless.getKeys = () => keys;
505
508
  const timestamp = Date.now().toString();
506
509
  const message = timestamp + keys.pubKey;
507
- const signature = await sessionless.sign(message);
510
+ const signature = signMessage(message, keys.privateKey);
508
511
 
509
512
  const resp = await fetch(`${getSanoraUrl()}/user/create`, {
510
513
  method: 'PUT',
@@ -605,8 +608,7 @@ async function sanoraEnsureUser(tenant) {
605
608
  const { keys } = tenant;
606
609
  const timestamp = Date.now().toString();
607
610
  const message = timestamp + keys.pubKey;
608
- sessionless.getKeys = () => keys;
609
- const signature = await sessionless.sign(message);
611
+ const signature = signMessage(message, keys.privateKey);
610
612
 
611
613
  const resp = await fetch(`${getSanoraUrl()}/user/create`, {
612
614
  method: 'PUT',
@@ -656,8 +658,7 @@ async function sanoraCreateProduct(tenant, title, category, description, price,
656
658
  const safePrice = price || 0;
657
659
  const message = timestamp + uuid + title + (description || '') + safePrice;
658
660
 
659
- sessionless.getKeys = () => keys;
660
- const signature = await sessionless.sign(message);
661
+ const signature = signMessage(message, keys.privateKey);
661
662
 
662
663
  const resp = await fetchWithRetry(
663
664
  `${getSanoraUrl()}/user/${uuid}/product/${encodeURIComponent(title)}`,
@@ -707,9 +708,8 @@ async function sanoraCreateProductResilient(tenant, title, category, description
707
708
  async function sanoraUploadArtifact(tenant, title, fileBuffer, filename, artifactType) {
708
709
  const { uuid, keys } = tenant;
709
710
  const timestamp = Date.now().toString();
710
- sessionless.getKeys = () => keys;
711
711
  const message = timestamp + uuid + title;
712
- const signature = await sessionless.sign(message);
712
+ const signature = signMessage(message, keys.privateKey);
713
713
 
714
714
  const form = new FormData();
715
715
  form.append('artifact', fileBuffer, { filename, contentType: getMimeType(filename) });
@@ -741,9 +741,8 @@ async function sanoraUploadArtifact(tenant, title, fileBuffer, filename, artifac
741
741
  async function sanoraUploadImage(tenant, title, imageBuffer, filename) {
742
742
  const { uuid, keys } = tenant;
743
743
  const timestamp = Date.now().toString();
744
- sessionless.getKeys = () => keys;
745
744
  const message = timestamp + uuid + title;
746
- const signature = await sessionless.sign(message);
745
+ const signature = signMessage(message, keys.privateKey);
747
746
 
748
747
  const form = new FormData();
749
748
  form.append('image', imageBuffer, { filename, contentType: getMimeType(filename) });
@@ -778,8 +777,7 @@ async function sanoraDeleteProduct(tenant, title) {
778
777
  const timestamp = Date.now().toString();
779
778
  const message = timestamp + uuid + title;
780
779
 
781
- sessionless.getKeys = () => keys;
782
- const signature = await sessionless.sign(message);
780
+ const signature = signMessage(message, keys.privateKey);
783
781
 
784
782
  await fetch(
785
783
  `${getSanoraUrl()}/user/${uuid}/product/${encodeURIComponent(title)}?timestamp=${timestamp}&signature=${encodeURIComponent(signature)}`,
@@ -792,10 +790,9 @@ async function sanoraDeleteProduct(tenant, title) {
792
790
  async function lucilleCreateUser(lucilleUrl) {
793
791
  const url = lucilleUrl || getLucilleUrl();
794
792
  const keys = await sessionless.generateKeys(() => {}, () => null);
795
- sessionless.getKeys = () => keys;
796
793
  const timestamp = Date.now().toString();
797
794
  const message = timestamp + keys.pubKey;
798
- const signature = await sessionless.sign(message);
795
+ const signature = signMessage(message, keys.privateKey);
799
796
 
800
797
  const resp = await fetch(`${url}/user/create`, {
801
798
  method: 'PUT',
@@ -824,8 +821,7 @@ async function lucilleRegisterVideo(tenant, title, description, tags, lucilleUrl
824
821
  const { lucilleKeys } = tenant;
825
822
  if (!lucilleKeys) throw new Error('Tenant has no Lucille user — re-register to enable video uploads');
826
823
  const timestamp = Date.now().toString();
827
- sessionless.getKeys = () => lucilleKeys;
828
- const signature = await sessionless.sign(timestamp + lucilleKeys.pubKey);
824
+ const signature = signMessage(timestamp + lucilleKeys.pubKey, lucilleKeys.privateKey);
829
825
 
830
826
  const resp = await fetch(
831
827
  `${url}/user/${lucilleKeys.uuid}/video/${encodeURIComponent(title)}`,
@@ -846,8 +842,7 @@ async function lucilleUploadVideo(tenant, title, fileBuffer, filename, lucilleUr
846
842
  const { lucilleKeys } = tenant;
847
843
  if (!lucilleKeys) throw new Error('Tenant has no Lucille user');
848
844
  const timestamp = Date.now().toString();
849
- sessionless.getKeys = () => lucilleKeys;
850
- const signature = await sessionless.sign(timestamp + lucilleKeys.pubKey);
845
+ const signature = signMessage(timestamp + lucilleKeys.pubKey, lucilleKeys.privateKey);
851
846
 
852
847
  const form = new FormData();
853
848
  form.append('video', fileBuffer, { filename, contentType: getMimeType(filename) });
@@ -1616,9 +1611,8 @@ async function getAppointmentSchedule(tenant, product) {
1616
1611
  async function getBookedSlots(tenant, productId) {
1617
1612
  const sanoraUrl = getSanoraUrl();
1618
1613
  const tenantKeys = tenant.keys;
1619
- sessionless.getKeys = () => tenantKeys;
1620
1614
  const timestamp = Date.now().toString();
1621
- const signature = await sessionless.sign(timestamp + tenant.uuid);
1615
+ const signature = signMessage(timestamp + tenant.uuid, tenantKeys.privateKey);
1622
1616
  const resp = await fetch(
1623
1617
  `${sanoraUrl}/user/${tenant.uuid}/orders/${encodeURIComponent(productId)}?timestamp=${timestamp}&signature=${encodeURIComponent(signature)}`
1624
1618
  );
@@ -1695,9 +1689,8 @@ async function getSubscriptionStatus(tenant, productId, recoveryKey) {
1695
1689
  const orderKey = crypto.createHash('sha256').update(recoveryKey + productId).digest('hex');
1696
1690
  const sanoraUrl = getSanoraUrl();
1697
1691
  const tenantKeys = tenant.keys;
1698
- sessionless.getKeys = () => tenantKeys;
1699
1692
  const timestamp = Date.now().toString();
1700
- const signature = await sessionless.sign(timestamp + tenant.uuid);
1693
+ const signature = signMessage(timestamp + tenant.uuid, tenantKeys.privateKey);
1701
1694
  try {
1702
1695
  const resp = await fetch(
1703
1696
  `${sanoraUrl}/user/${tenant.uuid}/orders/${encodeURIComponent(productId)}?timestamp=${timestamp}&signature=${encodeURIComponent(signature)}`
@@ -1748,12 +1741,11 @@ async function getAllOrders(tenant) {
1748
1741
  console.warn(`[shoppe] getAllOrders: Sanora unreachable — ${err.message}`);
1749
1742
  }
1750
1743
 
1751
- sessionless.getKeys = () => tenant.keys;
1752
1744
 
1753
1745
  const results = [];
1754
1746
  for (const [title, product] of Object.entries(products)) {
1755
1747
  const timestamp = Date.now().toString();
1756
- const signature = await sessionless.sign(timestamp + tenant.uuid);
1748
+ const signature = signMessage(timestamp + tenant.uuid, tenant.keys.privateKey);
1757
1749
  try {
1758
1750
  const resp = await fetch(
1759
1751
  `${sanoraUrl}/user/${tenant.uuid}/orders/${encodeURIComponent(product.productId)}` +
@@ -3426,10 +3418,9 @@ async function startServer(params) {
3426
3418
  }
3427
3419
 
3428
3420
  const addieKeys = { pubKey: tenant.addieKeys.pubKey, privateKey: tenant.addieKeys.privateKey };
3429
- sessionless.getKeys = () => addieKeys;
3430
3421
  const timestamp = Date.now().toString();
3431
3422
  const message = timestamp + tenant.addieKeys.uuid;
3432
- const signature = await sessionless.sign(message);
3423
+ const signature = signMessage(message, addieKeys.privateKey);
3433
3424
 
3434
3425
  const wikiOrigin = `${reqProto(req)}://${req.get('host')}`;
3435
3426
  const returnUrl = `${wikiOrigin}/plugin/shoppe/${tenant.uuid}/payouts/return`;
@@ -3673,9 +3664,8 @@ async function startServer(params) {
3673
3664
  : tenant.addieKeys ? [{ pubKey: tenant.addieKeys.pubKey, amount }] : [];
3674
3665
  }
3675
3666
  const buyerKeys = { pubKey: buyer.pubKey, privateKey: buyer.privateKey };
3676
- sessionless.getKeys = () => buyerKeys;
3677
3667
  const intentTimestamp = Date.now().toString();
3678
- const intentSignature = await sessionless.sign(intentTimestamp + buyer.uuid + amount + 'USD');
3668
+ const intentSignature = signMessage(intentTimestamp + buyer.uuid + amount + 'USD', buyerKeys.privateKey);
3679
3669
  const intentResp = await fetch(`${getAddieUrl()}/user/${buyer.uuid}/processor/stripe/intent`, {
3680
3670
  method: 'POST',
3681
3671
  headers: { 'Content-Type': 'application/json' },
@@ -3728,9 +3718,8 @@ async function startServer(params) {
3728
3718
  // Shoppere subscription: orderKey = sha256(pubKey + productId)
3729
3719
  const orderKey = crypto.createHash('sha256').update(pubKey + productId).digest('hex');
3730
3720
  const tenantKeys = tenant.keys;
3731
- sessionless.getKeys = () => tenantKeys;
3732
3721
  const ts = Date.now().toString();
3733
- const sig = await sessionless.sign(ts + tenant.uuid);
3722
+ const sig = signMessage(ts + tenant.uuid, tenantKeys.privateKey);
3734
3723
  const order = { orderKey, pubKey, paidAt: Date.now(), title, productId, renewalDays: renewalDays || 30, status: 'active' };
3735
3724
  await fetch(`${sanoraUrlInternal}/user/${tenant.uuid}/orders`, {
3736
3725
  method: 'PUT',
@@ -3745,9 +3734,8 @@ async function startServer(params) {
3745
3734
  // Shoppere appointment: record booking with pubKey credential
3746
3735
  const orderKey = crypto.createHash('sha256').update(pubKey + productId).digest('hex');
3747
3736
  const tenantKeys = tenant.keys;
3748
- sessionless.getKeys = () => tenantKeys;
3749
3737
  const bookingTimestamp = Date.now().toString();
3750
- const bookingSignature = await sessionless.sign(bookingTimestamp + tenant.uuid);
3738
+ const bookingSignature = signMessage(bookingTimestamp + tenant.uuid, tenantKeys.privateKey);
3751
3739
  const order = {
3752
3740
  orderKey,
3753
3741
  pubKey,
@@ -3770,9 +3758,8 @@ async function startServer(params) {
3770
3758
  // Shoppere digital product: record purchase with pubKey as credential
3771
3759
  const orderKey = crypto.createHash('sha256').update(pubKey + productId).digest('hex');
3772
3760
  const tenantKeys = tenant.keys;
3773
- sessionless.getKeys = () => tenantKeys;
3774
3761
  const ts = Date.now().toString();
3775
- const sig = await sessionless.sign(ts + tenant.uuid);
3762
+ const sig = signMessage(ts + tenant.uuid, tenantKeys.privateKey);
3776
3763
  const order = { orderKey, pubKey, paidAt: Date.now(), title, productId, status: 'purchased' };
3777
3764
  await fetch(`${sanoraUrlInternal}/user/${tenant.uuid}/orders`, {
3778
3765
  method: 'PUT',
@@ -3780,7 +3767,15 @@ async function startServer(params) {
3780
3767
  body: JSON.stringify({ timestamp: ts, signature: sig, order })
3781
3768
  });
3782
3769
  triggerTransfer();
3783
- return res.json({ success: true });
3770
+ // Return a download URL for digital goods so the Shoppere app can open the content
3771
+ const sanoraUrl = getSanoraUrl();
3772
+ const products = await fetchWithRetry(`${sanoraUrl}/products/${tenant.uuid}`).then(r => r.ok ? r.json() : {});
3773
+ const product = Object.values(products).find(p => p.uuid === productId || p.title === title);
3774
+ let downloadUrl = null;
3775
+ if (product && ['book', 'post', 'video'].includes(product.category)) {
3776
+ downloadUrl = `https://${req.get('host')}/plugin/shoppe/${tenant.uuid}/download/${encodeURIComponent(product.title)}`;
3777
+ }
3778
+ return res.json({ success: true, downloadUrl });
3784
3779
  }
3785
3780
 
3786
3781
  // ── Legacy recovery key paths ─────────────────────────────────────────
@@ -3790,9 +3785,8 @@ async function startServer(params) {
3790
3785
  // The recovery key itself is never stored; orderKey = sha256(recoveryKey + productId).
3791
3786
  const orderKey = crypto.createHash('sha256').update(recoveryKey + productId).digest('hex');
3792
3787
  const tenantKeys = tenant.keys;
3793
- sessionless.getKeys = () => tenantKeys;
3794
3788
  const ts = Date.now().toString();
3795
- const sig = await sessionless.sign(ts + tenant.uuid);
3789
+ const sig = signMessage(ts + tenant.uuid, tenantKeys.privateKey);
3796
3790
  const order = { orderKey, paidAt: Date.now(), title, productId, renewalDays: renewalDays || 30, status: 'active' };
3797
3791
  await fetch(`${sanoraUrlInternal}/user/${tenant.uuid}/orders`, {
3798
3792
  method: 'PUT',
@@ -3811,9 +3805,8 @@ async function startServer(params) {
3811
3805
 
3812
3806
  // Record the booking in Sanora (contact info flows through the server, never direct from browser)
3813
3807
  const tenantKeys = tenant.keys;
3814
- sessionless.getKeys = () => tenantKeys;
3815
3808
  const bookingTimestamp = Date.now().toString();
3816
- const bookingSignature = await sessionless.sign(bookingTimestamp + tenant.uuid);
3809
+ const bookingSignature = signMessage(bookingTimestamp + tenant.uuid, tenantKeys.privateKey);
3817
3810
  const order = {
3818
3811
  productId,
3819
3812
  title,
@@ -3843,9 +3836,8 @@ async function startServer(params) {
3843
3836
  // Physical product — record order in Sanora signed by the tenant.
3844
3837
  // The shippingAddress is collected here (post-payment) and sent once, server-side.
3845
3838
  const tenantKeys = tenant.keys;
3846
- sessionless.getKeys = () => tenantKeys;
3847
3839
  const orderTimestamp = Date.now().toString();
3848
- const orderSignature = await sessionless.sign(orderTimestamp + tenant.uuid);
3840
+ const orderSignature = signMessage(orderTimestamp + tenant.uuid, tenantKeys.privateKey);
3849
3841
  const order = {
3850
3842
  productId,
3851
3843
  title,
@@ -3964,8 +3956,7 @@ async function startServer(params) {
3964
3956
  // Record order in Sanora (fire-and-forget)
3965
3957
  if (productId && tenant.keys) {
3966
3958
  const ts = Date.now().toString();
3967
- sessionless.getKeys = () => tenant.keys;
3968
- const orderSig = await sessionless.sign(ts + tenant.uuid).catch(() => null);
3959
+ const orderSig = signMessage(ts + tenant.uuid).catch(() => null, tenant.keys.privateKey);
3969
3960
  if (orderSig) {
3970
3961
  const order = {
3971
3962
  productId,
@@ -4127,8 +4118,7 @@ async function startServer(params) {
4127
4118
  const { uuid: lucilleUuid, pubKey, privateKey } = tenant.lucilleKeys;
4128
4119
 
4129
4120
  const timestamp = Date.now().toString();
4130
- sessionless.getKeys = () => ({ pubKey, privateKey });
4131
- const signature = await sessionless.sign(timestamp + pubKey);
4121
+ const signature = signMessage(timestamp + pubKey, privateKey);
4132
4122
 
4133
4123
  const uploadUrl = `${lucilleBase}/user/${lucilleUuid}/video/${encodeURIComponent(title)}/file`;
4134
4124
  res.json({ uploadUrl, timestamp, signature });