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.
- package/package.json +1 -1
- package/server/server.js +75 -12
package/package.json
CHANGED
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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));
|