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 +1 -1
- package/package.json +1 -1
- package/server/server.js +45 -55
package/CLAUDE.md
CHANGED
package/package.json
CHANGED
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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 });
|