wiki-plugin-shoppe 0.0.30 → 0.0.32

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/server/server.js +75 -12
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wiki-plugin-shoppe",
3
- "version": "0.0.30",
3
+ "version": "0.0.32",
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
@@ -477,6 +477,44 @@ function getMimeType(filename) {
477
477
  })[ext] || 'application/octet-stream';
478
478
  }
479
479
 
480
+ // Ensure the tenant's Sanora user exists (Redis may have been wiped).
481
+ // If the user is found by pubKey but has a different UUID (new registration),
482
+ // updates tenants.json so all subsequent product calls use the correct UUID.
483
+ async function sanoraEnsureUser(tenant) {
484
+ const { keys } = tenant;
485
+ const timestamp = Date.now().toString();
486
+ const message = timestamp + keys.pubKey;
487
+ sessionless.getKeys = () => keys;
488
+ const signature = await sessionless.sign(message);
489
+
490
+ const resp = await fetch(`${getSanoraUrl()}/user/create`, {
491
+ method: 'PUT',
492
+ headers: { 'Content-Type': 'application/json' },
493
+ body: JSON.stringify({ timestamp, pubKey: keys.pubKey, signature }),
494
+ timeout: 15000
495
+ });
496
+
497
+ if (!resp.ok) {
498
+ const text = await resp.text().catch(() => '');
499
+ throw new Error(`Sanora user ensure failed (${resp.status}): ${text.slice(0, 200)}`);
500
+ }
501
+
502
+ const sanoraUser = await resp.json();
503
+ if (sanoraUser.error) throw new Error(`Sanora user ensure: ${sanoraUser.error}`);
504
+
505
+ if (sanoraUser.uuid !== tenant.uuid) {
506
+ console.log(`[shoppe] Sanora UUID changed ${tenant.uuid} → ${sanoraUser.uuid} (Redis was reset). Updating tenants.json.`);
507
+ const tenants = loadTenants();
508
+ const oldUuid = tenant.uuid;
509
+ delete tenants[oldUuid];
510
+ tenant.uuid = sanoraUser.uuid;
511
+ tenants[sanoraUser.uuid] = tenant;
512
+ saveTenants(tenants);
513
+ }
514
+
515
+ return tenant; // tenant.uuid is now correct
516
+ }
517
+
480
518
  async function sanoraCreateProduct(tenant, title, category, description, price, shipping, tags) {
481
519
  const { uuid, keys } = tenant;
482
520
  const timestamp = Date.now().toString();
@@ -505,11 +543,32 @@ async function sanoraCreateProduct(tenant, title, category, description, price,
505
543
  }
506
544
  );
507
545
 
546
+ if (!resp.ok) {
547
+ const text = await resp.text().catch(() => '');
548
+ throw new Error(`Create product failed (${resp.status}): ${text.slice(0, 200)}`);
549
+ }
508
550
  const product = await resp.json();
509
551
  if (product.error) throw new Error(`Create product failed: ${product.error}`);
510
552
  return product;
511
553
  }
512
554
 
555
+ // Wrapper used by processArchive. On "not found" (Sanora Redis cleared mid-upload),
556
+ // re-registers the tenant and retries once. tenant.uuid may be updated in place.
557
+ async function sanoraCreateProductResilient(tenant, title, category, description, price, shipping, tags) {
558
+ try {
559
+ return await sanoraCreateProduct(tenant, title, category, description, price, shipping, tags);
560
+ } catch (err) {
561
+ if (err.message.includes('not found') || err.message.includes('404')) {
562
+ console.warn(`[shoppe] Sanora user lost mid-upload, re-registering and retrying: ${title}`);
563
+ const updated = await sanoraEnsureUser(tenant);
564
+ // Mutate tenant in place so all subsequent calls use the new UUID
565
+ tenant.uuid = updated.uuid;
566
+ return await sanoraCreateProduct(tenant, title, category, description, price, shipping, tags);
567
+ }
568
+ throw err;
569
+ }
570
+ }
571
+
513
572
  async function sanoraUploadArtifact(tenant, title, fileBuffer, filename, artifactType) {
514
573
  const { uuid, keys } = tenant;
515
574
  const timestamp = Date.now().toString();
@@ -717,7 +776,7 @@ async function processArchive(zipPath) {
717
776
  throw new Error('manifest.json must contain uuid and emojicode');
718
777
  }
719
778
 
720
- const tenant = getTenantByIdentifier(manifest.uuid);
779
+ let tenant = getTenantByIdentifier(manifest.uuid);
721
780
  if (!tenant) throw new Error(`Unknown UUID: ${manifest.uuid}`);
722
781
  if (tenant.emojicode !== manifest.emojicode) {
723
782
  throw new Error('emojicode does not match registered tenant');
@@ -741,6 +800,10 @@ async function processArchive(zipPath) {
741
800
  Object.assign(tenant, tenantUpdates);
742
801
  }
743
802
 
803
+ // Ensure the Sanora user exists before uploading any products.
804
+ // If Redis was wiped, this re-creates the user and updates tenant.uuid.
805
+ tenant = await sanoraEnsureUser(tenant);
806
+
744
807
  const results = { books: [], music: [], posts: [], albums: [], products: [], videos: [], appointments: [], subscriptions: [], warnings: [] };
745
808
 
746
809
  function readInfo(entryPath) {
@@ -769,7 +832,7 @@ async function processArchive(zipPath) {
769
832
  const description = info.description || '';
770
833
  const price = info.price || 0;
771
834
 
772
- await sanoraCreateProduct(tenant, title, 'book', description, price, 0, buildTags('book', info.keywords));
835
+ await sanoraCreateProductResilient(tenant, title, 'book', description, price, 0, buildTags('book', info.keywords));
773
836
 
774
837
  // Cover image — use info.cover to pin a specific file, else first image found
775
838
  const covers = fs.readdirSync(entryPath).filter(f => IMAGE_EXTS.has(path.extname(f).toLowerCase()));
@@ -811,7 +874,7 @@ async function processArchive(zipPath) {
811
874
  try {
812
875
  const description = info.description || `Album: ${albumTitle}`;
813
876
  const price = info.price || 0;
814
- await sanoraCreateProduct(tenant, albumTitle, 'music', description, price, 0, buildTags('music,album', info.keywords));
877
+ await sanoraCreateProductResilient(tenant, albumTitle, 'music', description, price, 0, buildTags('music,album', info.keywords));
815
878
  const coverFile = info.cover ? (covers.find(f => f === info.cover) || covers[0]) : covers[0];
816
879
  if (coverFile) {
817
880
  const coverBuf = fs.readFileSync(path.join(entryPath, coverFile));
@@ -840,7 +903,7 @@ async function processArchive(zipPath) {
840
903
  const buf = fs.readFileSync(entryPath);
841
904
  const description = trackInfo.description || `Track: ${title}`;
842
905
  const price = trackInfo.price || 0;
843
- await sanoraCreateProduct(tenant, title, 'music', description, price, 0, buildTags('music,track', trackInfo.keywords));
906
+ await sanoraCreateProductResilient(tenant, title, 'music', description, price, 0, buildTags('music,track', trackInfo.keywords));
844
907
  await sanoraUploadArtifact(tenant, title, buf, entry, 'audio');
845
908
  results.music.push({ title, type: 'track' });
846
909
  console.log(`[shoppe] 🎵 track: ${title}`);
@@ -880,7 +943,7 @@ async function processArchive(zipPath) {
880
943
  // Register the series itself as a parent product
881
944
  try {
882
945
  const description = info.description || `A ${subDirs.length}-part series`;
883
- await sanoraCreateProduct(tenant, seriesTitle, 'post-series', description, 0, 0, buildTags(`post,series,order:${order}`, info.keywords));
946
+ await sanoraCreateProductResilient(tenant, seriesTitle, 'post-series', description, 0, 0, buildTags(`post,series,order:${order}`, info.keywords));
884
947
 
885
948
  const covers = fs.readdirSync(entryPath).filter(f => IMAGE_EXTS.has(path.extname(f).toLowerCase()));
886
949
  if (covers.length > 0) {
@@ -920,7 +983,7 @@ async function processArchive(zipPath) {
920
983
  const productTitle = `${seriesTitle}: ${resolvedTitle}`;
921
984
  const description = partInfo.description || partFm.body.split('\n\n')[0].replace(/^#+\s*/, '').trim() || resolvedTitle;
922
985
 
923
- await sanoraCreateProduct(tenant, productTitle, 'post', description, 0, 0,
986
+ await sanoraCreateProductResilient(tenant, productTitle, 'post', description, 0, 0,
924
987
  buildTags(`post,blog,series:${seriesTitle},part:${partIndex + 1},order:${order}`, partInfo.keywords));
925
988
 
926
989
  await sanoraUploadArtifact(tenant, productTitle, mdBuf, partMdFiles[0], 'text');
@@ -962,7 +1025,7 @@ async function processArchive(zipPath) {
962
1025
  const firstLine = fm.body.split('\n').find(l => l.trim()).replace(/^#+\s*/, '');
963
1026
  const description = info.description || fm.body.split('\n\n')[0].replace(/^#+\s*/, '').trim() || firstLine || title;
964
1027
 
965
- await sanoraCreateProduct(tenant, title, 'post', description, 0, 0, buildTags(`post,blog,order:${order}`, info.keywords));
1028
+ await sanoraCreateProductResilient(tenant, title, 'post', description, 0, 0, buildTags(`post,blog,order:${order}`, info.keywords));
966
1029
  await sanoraUploadArtifact(tenant, title, mdBuf, mdFiles[0], 'text');
967
1030
 
968
1031
  const covers = fs.readdirSync(entryPath).filter(f => IMAGE_EXTS.has(path.extname(f).toLowerCase()));
@@ -999,7 +1062,7 @@ async function processArchive(zipPath) {
999
1062
  if (!fs.statSync(entryPath).isDirectory()) continue;
1000
1063
  const images = fs.readdirSync(entryPath).filter(f => IMAGE_EXTS.has(path.extname(f).toLowerCase()));
1001
1064
  try {
1002
- await sanoraCreateProduct(tenant, entry, 'album', `Photo album: ${entry}`, 0, 0, 'album,photos');
1065
+ await sanoraCreateProductResilient(tenant, entry, 'album', `Photo album: ${entry}`, 0, 0, 'album,photos');
1003
1066
  if (images.length > 0) {
1004
1067
  const coverBuf = fs.readFileSync(path.join(entryPath, images[0]));
1005
1068
  await sanoraUploadImage(tenant, entry, coverBuf, images[0]);
@@ -1036,7 +1099,7 @@ async function processArchive(zipPath) {
1036
1099
  const price = info.price || 0;
1037
1100
  const shipping = info.shipping || 0;
1038
1101
 
1039
- await sanoraCreateProduct(tenant, title, 'product', description, price, shipping, buildTags(`product,physical,order:${order}`, info.keywords));
1102
+ await sanoraCreateProductResilient(tenant, title, 'product', description, price, shipping, buildTags(`product,physical,order:${order}`, info.keywords));
1040
1103
 
1041
1104
  // Hero image: prefer hero.jpg / hero.png, fall back to first image
1042
1105
  const images = fs.readdirSync(entryPath).filter(f => IMAGE_EXTS.has(path.extname(f).toLowerCase()));
@@ -1077,7 +1140,7 @@ async function processArchive(zipPath) {
1077
1140
  renewalDays: info.renewalDays || 30
1078
1141
  };
1079
1142
 
1080
- await sanoraCreateProduct(tenant, title, 'subscription', description, price, 0, buildTags('subscription', info.keywords));
1143
+ await sanoraCreateProductResilient(tenant, title, 'subscription', description, price, 0, buildTags('subscription', info.keywords));
1081
1144
 
1082
1145
  // Upload tier metadata (benefits list, renewal period) as a JSON artifact
1083
1146
  const tierBuf = Buffer.from(JSON.stringify(tierMeta));
@@ -1152,7 +1215,7 @@ async function processArchive(zipPath) {
1152
1215
  (lucilleVideoId ? `,lucille-id:${lucilleVideoId},lucille-url:${lucilleBase}` : '');
1153
1216
 
1154
1217
  // Sanora catalog entry (for discovery / storefront)
1155
- await sanoraCreateProduct(tenant, title, 'video', description, price, 0, videoTags);
1218
+ await sanoraCreateProductResilient(tenant, title, 'video', description, price, 0, videoTags);
1156
1219
 
1157
1220
  // Cover / poster image (optional)
1158
1221
  const images = fs.readdirSync(entryPath).filter(f => IMAGE_EXTS.has(path.extname(f).toLowerCase()));
@@ -1198,7 +1261,7 @@ async function processArchive(zipPath) {
1198
1261
  advanceDays: info.advanceDays || 30
1199
1262
  };
1200
1263
 
1201
- await sanoraCreateProduct(tenant, title, 'appointment', description, price, 0, buildTags('appointment', info.keywords));
1264
+ await sanoraCreateProductResilient(tenant, title, 'appointment', description, price, 0, buildTags('appointment', info.keywords));
1202
1265
 
1203
1266
  // Upload schedule as a JSON artifact so the booking page can retrieve it
1204
1267
  const scheduleBuf = Buffer.from(JSON.stringify(schedule));