wiki-plugin-allyabase 0.0.10 → 0.0.12
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/client/allyabase.js +55 -0
- package/package.json +3 -2
- package/server/get-paid.js +216 -0
- package/server/server.js +55 -0
package/client/allyabase.js
CHANGED
|
@@ -405,8 +405,21 @@ function emit($item, item) {
|
|
|
405
405
|
launchButton.style.padding = '10px 20px';
|
|
406
406
|
launchButton.style.marginBottom = '15px';
|
|
407
407
|
launchButton.style.cursor = 'pointer';
|
|
408
|
+
launchButton.style.marginRight = '10px';
|
|
408
409
|
container.appendChild(launchButton);
|
|
409
410
|
|
|
411
|
+
// Add update button
|
|
412
|
+
const updateButton = document.createElement('button');
|
|
413
|
+
updateButton.textContent = 'Check for Updates';
|
|
414
|
+
updateButton.style.padding = '10px 20px';
|
|
415
|
+
updateButton.style.marginBottom = '15px';
|
|
416
|
+
updateButton.style.cursor = 'pointer';
|
|
417
|
+
updateButton.style.backgroundColor = '#0066cc';
|
|
418
|
+
updateButton.style.color = 'white';
|
|
419
|
+
updateButton.style.border = 'none';
|
|
420
|
+
updateButton.style.borderRadius = '4px';
|
|
421
|
+
container.appendChild(updateButton);
|
|
422
|
+
|
|
410
423
|
// Add status container
|
|
411
424
|
const statusContainer = document.createElement('div');
|
|
412
425
|
statusContainer.id = 'allyabase-status';
|
|
@@ -652,6 +665,48 @@ function emit($item, item) {
|
|
|
652
665
|
}
|
|
653
666
|
});
|
|
654
667
|
|
|
668
|
+
// Update button click handler
|
|
669
|
+
updateButton.addEventListener('click', async () => {
|
|
670
|
+
updateButton.disabled = true;
|
|
671
|
+
updateButton.textContent = 'Updating...';
|
|
672
|
+
|
|
673
|
+
try {
|
|
674
|
+
const response = await post('/plugin/allyabase/update');
|
|
675
|
+
const result = await response.json();
|
|
676
|
+
|
|
677
|
+
const msg = document.createElement('div');
|
|
678
|
+
msg.style.marginTop = '10px';
|
|
679
|
+
msg.style.fontSize = '12px';
|
|
680
|
+
msg.style.fontFamily = 'monospace';
|
|
681
|
+
|
|
682
|
+
if (result.success) {
|
|
683
|
+
const failed = Object.entries(result.results).filter(([, v]) => v.startsWith('error'));
|
|
684
|
+
if (failed.length === 0) {
|
|
685
|
+
msg.style.color = 'green';
|
|
686
|
+
msg.textContent = '✓ All services updated and restarted.';
|
|
687
|
+
} else {
|
|
688
|
+
msg.style.color = 'orange';
|
|
689
|
+
msg.innerHTML = `⚠️ Updated with errors:<br>` +
|
|
690
|
+
failed.map(([svc, err]) => `${svc}: ${err}`).join('<br>');
|
|
691
|
+
}
|
|
692
|
+
} else {
|
|
693
|
+
msg.style.color = 'red';
|
|
694
|
+
msg.textContent = `✗ Update failed: ${result.error}`;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
container.insertBefore(msg, statusContainer);
|
|
698
|
+
setTimeout(() => {
|
|
699
|
+
msg.remove();
|
|
700
|
+
updateStatus();
|
|
701
|
+
}, 5000);
|
|
702
|
+
} catch (err) {
|
|
703
|
+
console.error('[allyabase] update error:', err);
|
|
704
|
+
} finally {
|
|
705
|
+
updateButton.disabled = false;
|
|
706
|
+
updateButton.textContent = 'Check for Updates';
|
|
707
|
+
}
|
|
708
|
+
});
|
|
709
|
+
|
|
655
710
|
// Initial status check
|
|
656
711
|
updateStatus();
|
|
657
712
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wiki-plugin-allyabase",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.12",
|
|
4
4
|
"description": "Allyabase management plugin for the federated wiki",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"wiki",
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
27
|
"bdo-js": "^0.0.7",
|
|
28
|
-
"http-proxy": "^1.18.1"
|
|
28
|
+
"http-proxy": "^1.18.1",
|
|
29
|
+
"sessionless-node": "latest"
|
|
29
30
|
}
|
|
30
31
|
}
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* get-paid.js
|
|
3
|
+
*
|
|
4
|
+
* Manages the allyabase server's own Addie user so it can receive a
|
|
5
|
+
* commission on purchases that flow through plugins hosted on this wiki.
|
|
6
|
+
*
|
|
7
|
+
* Any plugin that knows the allyabase URL calls:
|
|
8
|
+
* GET /plugin/allyabase/get-paid
|
|
9
|
+
* and receives a payee descriptor { pubKey, addieURL, percent, signature }
|
|
10
|
+
* that it adds to the `payees` array when creating a payment intent.
|
|
11
|
+
* Addie's /verify-payee validates the signature before transferring.
|
|
12
|
+
*
|
|
13
|
+
* Stripe Connect setup (owner only):
|
|
14
|
+
* GET /plugin/allyabase/setup/stripe?token=DEPLOYMENT_TOKEN
|
|
15
|
+
* GET /plugin/allyabase/setup/stripe/done ← Stripe return URL
|
|
16
|
+
*
|
|
17
|
+
* Config stored at ~/.allyabase/addie-keys.json:
|
|
18
|
+
* { uuid, pubKey, privateKey, stripeOnboarded }
|
|
19
|
+
*
|
|
20
|
+
* Wiki startup args:
|
|
21
|
+
* --allyabase_commission N commission percent, 1–9 (default 1)
|
|
22
|
+
* --owner_email EMAIL required for Stripe Connect
|
|
23
|
+
* --deployment_token TOKEN protects the setup/stripe endpoint
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
const sessionless = require('sessionless-node');
|
|
27
|
+
const fs = require('fs');
|
|
28
|
+
const path = require('path');
|
|
29
|
+
const os = require('os');
|
|
30
|
+
|
|
31
|
+
const KEYS_PATH = path.join(os.homedir(), '.allyabase', 'addie-keys.json');
|
|
32
|
+
const LOCAL_ADDIE_URL = 'http://localhost:3005';
|
|
33
|
+
const DEFAULT_COMMISSION = 1;
|
|
34
|
+
|
|
35
|
+
let cachedKeys = null;
|
|
36
|
+
|
|
37
|
+
function loadKeys() {
|
|
38
|
+
try {
|
|
39
|
+
return JSON.parse(fs.readFileSync(KEYS_PATH, 'utf8'));
|
|
40
|
+
} catch {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function saveKeysToDisk(keys) {
|
|
46
|
+
const dir = path.dirname(KEYS_PATH);
|
|
47
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
48
|
+
fs.writeFileSync(KEYS_PATH, JSON.stringify(keys, null, 2));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Sign with a specific private key without disturbing the shared sessionless singleton.
|
|
52
|
+
// Safe in Node.js single-threaded model as long as no await occurs between set and sign.
|
|
53
|
+
function signWith(message, privateKey) {
|
|
54
|
+
const saved = sessionless.getKeys;
|
|
55
|
+
sessionless.getKeys = () => ({ privateKey });
|
|
56
|
+
const sig = sessionless.sign(message);
|
|
57
|
+
sessionless.getKeys = saved;
|
|
58
|
+
return sig;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function wikiOrigin(req) {
|
|
62
|
+
const proto = req.headers['x-forwarded-proto'] || req.protocol;
|
|
63
|
+
return `${proto}://${req.get('host')}`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function ensureAddieUser() {
|
|
67
|
+
cachedKeys = loadKeys();
|
|
68
|
+
if (cachedKeys && cachedKeys.uuid) return cachedKeys;
|
|
69
|
+
|
|
70
|
+
let generated = {};
|
|
71
|
+
await sessionless.generateKeys(
|
|
72
|
+
(k) => { generated = k; },
|
|
73
|
+
() => generated
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
const timestamp = Date.now().toString();
|
|
77
|
+
const signature = signWith(timestamp + generated.pubKey, generated.privateKey);
|
|
78
|
+
|
|
79
|
+
const resp = await fetch(`${LOCAL_ADDIE_URL}/user/create`, {
|
|
80
|
+
method: 'PUT',
|
|
81
|
+
headers: { 'Content-Type': 'application/json' },
|
|
82
|
+
body: JSON.stringify({ timestamp, pubKey: generated.pubKey, signature })
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
if (!resp.ok) throw new Error(`Addie user create failed: ${resp.status}`);
|
|
86
|
+
|
|
87
|
+
const addieUser = await resp.json();
|
|
88
|
+
if (addieUser.error) throw new Error(`Addie: ${addieUser.error}`);
|
|
89
|
+
|
|
90
|
+
const keys = {
|
|
91
|
+
uuid: addieUser.uuid,
|
|
92
|
+
pubKey: generated.pubKey,
|
|
93
|
+
privateKey: generated.privateKey,
|
|
94
|
+
stripeOnboarded: false
|
|
95
|
+
};
|
|
96
|
+
saveKeysToDisk(keys);
|
|
97
|
+
cachedKeys = keys;
|
|
98
|
+
return keys;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function addRoutes(params) {
|
|
102
|
+
const app = params.app;
|
|
103
|
+
const argv = params.argv;
|
|
104
|
+
|
|
105
|
+
const commission = Math.min(9, Math.max(1, parseInt(argv.allyabase_commission || DEFAULT_COMMISSION, 10)));
|
|
106
|
+
const DEPLOYMENT_TOKEN = argv.deployment_token || process.env.DEPLOYMENT_TOKEN;
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
await ensureAddieUser();
|
|
110
|
+
console.log(`[wiki-plugin-allyabase] 💰 Get-paid user ready (${commission}% commission, Stripe ${cachedKeys.stripeOnboarded ? 'connected' : 'not yet connected'})`);
|
|
111
|
+
} catch (err) {
|
|
112
|
+
console.error('[wiki-plugin-allyabase] ❌ Failed to create get-paid Addie user:', err.message);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ── GET /plugin/allyabase/get-paid ──────────────────────────────────────────
|
|
116
|
+
// Returns a payee descriptor for this allyabase host.
|
|
117
|
+
// Only returns a descriptor when Stripe Connect is complete — otherwise the
|
|
118
|
+
// commission would be taken but never land anywhere.
|
|
119
|
+
app.get('/plugin/allyabase/get-paid', function(req, res) {
|
|
120
|
+
if (!cachedKeys || !cachedKeys.uuid) {
|
|
121
|
+
return res.status(503).json({ error: 'Get-paid not ready' });
|
|
122
|
+
}
|
|
123
|
+
if (!cachedKeys.stripeOnboarded) {
|
|
124
|
+
// 204 = no content: caller should skip adding this payee
|
|
125
|
+
return res.status(204).end();
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const addieURL = `${wikiOrigin(req)}/plugin/allyabase/addie`;
|
|
129
|
+
const signature = signWith(cachedKeys.pubKey + addieURL + commission, cachedKeys.privateKey);
|
|
130
|
+
|
|
131
|
+
res.json({ pubKey: cachedKeys.pubKey, addieURL, percent: commission, signature });
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// ── GET /plugin/allyabase/setup/stripe ──────────────────────────────────────
|
|
135
|
+
// Initiates Stripe Connect Express onboarding for the allyabase host.
|
|
136
|
+
// Protected by deployment token — open in a browser with ?token=YOUR_TOKEN.
|
|
137
|
+
app.get('/plugin/allyabase/setup/stripe', async function(req, res) {
|
|
138
|
+
const token = req.query.token;
|
|
139
|
+
if (!DEPLOYMENT_TOKEN || token !== DEPLOYMENT_TOKEN) {
|
|
140
|
+
return res.status(403).send('<h1>Forbidden</h1><p>Provide your deployment token as ?token=TOKEN</p>');
|
|
141
|
+
}
|
|
142
|
+
if (!cachedKeys || !cachedKeys.uuid) {
|
|
143
|
+
return res.status(503).send('<h1>Not ready</h1><p>Addie user not initialized yet — wait a moment and retry.</p>');
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const email = argv.owner_email || process.env.OWNER_EMAIL;
|
|
147
|
+
if (!email) {
|
|
148
|
+
return res.status(400).send(
|
|
149
|
+
'<h1>Owner email required</h1>' +
|
|
150
|
+
'<p>Start the wiki with <code>--owner_email YOUR@EMAIL.COM</code> to enable Stripe Connect.</p>'
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
try {
|
|
155
|
+
const origin = wikiOrigin(req);
|
|
156
|
+
const refreshUrl = `${origin}/plugin/allyabase/setup/stripe?token=${encodeURIComponent(token)}`;
|
|
157
|
+
const returnUrl = `${origin}/plugin/allyabase/setup/stripe/done?token=${encodeURIComponent(token)}`;
|
|
158
|
+
|
|
159
|
+
const timestamp = Date.now().toString();
|
|
160
|
+
const signature = signWith(timestamp + cachedKeys.uuid + email, cachedKeys.privateKey);
|
|
161
|
+
|
|
162
|
+
const resp = await fetch(`${LOCAL_ADDIE_URL}/user/${cachedKeys.uuid}/processor/stripe/express`, {
|
|
163
|
+
method: 'PUT',
|
|
164
|
+
headers: { 'Content-Type': 'application/json' },
|
|
165
|
+
body: JSON.stringify({ timestamp, country: 'US', email, refreshUrl, returnUrl, signature })
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
const result = await resp.json();
|
|
169
|
+
if (result.error) {
|
|
170
|
+
return res.status(500).send(`<h1>Stripe setup error</h1><p>${result.error}</p>`);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (result.alreadyConnected) {
|
|
174
|
+
cachedKeys.stripeOnboarded = true;
|
|
175
|
+
saveKeysToDisk(cachedKeys);
|
|
176
|
+
return res.send(`<!doctype html><html><head><title>Stripe Already Connected</title>
|
|
177
|
+
<style>body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;max-width:560px;margin:80px auto;text-align:center;color:#1d1d1f;}</style></head>
|
|
178
|
+
<body><h1>✅ Stripe already connected</h1>
|
|
179
|
+
<p>Your allyabase Stripe account is already set up.</p>
|
|
180
|
+
<a href="javascript:window.close()">Close this tab</a></body></html>`);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const onboardingUrl = result.stripeOnboardingUrl;
|
|
184
|
+
if (!onboardingUrl) return res.status(500).send('<h1>No onboarding URL returned from Addie</h1>');
|
|
185
|
+
|
|
186
|
+
res.redirect(onboardingUrl);
|
|
187
|
+
} catch (err) {
|
|
188
|
+
console.error('[wiki-plugin-allyabase] Stripe setup error:', err);
|
|
189
|
+
res.status(500).send(`<h1>Setup failed</h1><p>${err.message}</p>`);
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
// ── GET /plugin/allyabase/setup/stripe/done ─────────────────────────────────
|
|
194
|
+
// Stripe redirects here after onboarding completes.
|
|
195
|
+
app.get('/plugin/allyabase/setup/stripe/done', function(req, res) {
|
|
196
|
+
const token = req.query.token;
|
|
197
|
+
if (!DEPLOYMENT_TOKEN || token !== DEPLOYMENT_TOKEN) {
|
|
198
|
+
return res.status(403).send('<h1>Forbidden</h1>');
|
|
199
|
+
}
|
|
200
|
+
if (cachedKeys) {
|
|
201
|
+
cachedKeys.stripeOnboarded = true;
|
|
202
|
+
saveKeysToDisk(cachedKeys);
|
|
203
|
+
}
|
|
204
|
+
console.log('[wiki-plugin-allyabase] ✅ Stripe Connect onboarding complete');
|
|
205
|
+
res.send(`<!doctype html><html><head><title>Stripe Setup Complete</title>
|
|
206
|
+
<style>body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;max-width:560px;margin:80px auto;text-align:center;color:#1d1d1f;}
|
|
207
|
+
h1{font-size:28px;margin-bottom:12px;} p{font-size:15px;color:#555;line-height:1.6;}
|
|
208
|
+
code{display:block;background:#f4f4f4;padding:10px 16px;border-radius:8px;font-size:13px;margin:16px auto;}</style></head>
|
|
209
|
+
<body><h1>✅ Allyabase payouts enabled</h1>
|
|
210
|
+
<p>Your allyabase server is now connected to Stripe. You'll receive a ${commission}% commission on all purchases flowing through this base.</p>
|
|
211
|
+
<p>Any plugin that calls <code>${LOCAL_ADDIE_URL.replace('localhost', 'your-wiki-host')}/plugin/allyabase/get-paid</code> will automatically include you as a payee.</p>
|
|
212
|
+
<a href="javascript:window.close()">Close this tab</a></body></html>`);
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
module.exports = { addRoutes };
|
package/server/server.js
CHANGED
|
@@ -6,6 +6,7 @@ const httpProxy = require('http-proxy');
|
|
|
6
6
|
const fs = require('fs');
|
|
7
7
|
const federationResolver = require('./federation-resolver');
|
|
8
8
|
const deployment = require('./deployment');
|
|
9
|
+
const getPaid = require('./get-paid');
|
|
9
10
|
const bdoModule = require('bdo-js');
|
|
10
11
|
const bdo = bdoModule.default || bdoModule;
|
|
11
12
|
|
|
@@ -768,6 +769,57 @@ async function startServer(params) {
|
|
|
768
769
|
}
|
|
769
770
|
});
|
|
770
771
|
|
|
772
|
+
// Endpoint to update all allyabase services (git pull + npm install + pm2 restart)
|
|
773
|
+
app.post('/plugin/allyabase/update', owner, async function(req, res) {
|
|
774
|
+
const services = [
|
|
775
|
+
'addie', 'aretha', 'bdo', 'continuebee', 'covenant',
|
|
776
|
+
'dolores', 'fount', 'joan', 'julia', 'minnie', 'pref', 'prof', 'sanora'
|
|
777
|
+
];
|
|
778
|
+
const buildDir = '/var/lib/allyabase';
|
|
779
|
+
const results = {};
|
|
780
|
+
|
|
781
|
+
for (const service of services) {
|
|
782
|
+
const serviceDir = `${buildDir}/${service}`;
|
|
783
|
+
try {
|
|
784
|
+
await new Promise((resolve, reject) => {
|
|
785
|
+
exec(
|
|
786
|
+
`git -C "${serviceDir}" pull --ff-only && npm install --prefix "${serviceDir}/src/server/node" --silent`,
|
|
787
|
+
{ timeout: 60000 },
|
|
788
|
+
(err, stdout, stderr) => {
|
|
789
|
+
if (err) return reject(err);
|
|
790
|
+
resolve(stdout);
|
|
791
|
+
}
|
|
792
|
+
);
|
|
793
|
+
});
|
|
794
|
+
results[service] = 'updated';
|
|
795
|
+
} catch (err) {
|
|
796
|
+
results[service] = `error: ${err.message}`;
|
|
797
|
+
console.error(`[allyabase] update ${service}:`, err.message);
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
// Restart all services via pm2
|
|
802
|
+
let restartOutput = '';
|
|
803
|
+
try {
|
|
804
|
+
await new Promise((resolve, reject) => {
|
|
805
|
+
exec(
|
|
806
|
+
`cd "${buildDir}" && ./node_modules/.bin/pm2 restart ecosystem.config.js`,
|
|
807
|
+
{ timeout: 30000 },
|
|
808
|
+
(err, stdout) => {
|
|
809
|
+
if (err) return reject(err);
|
|
810
|
+
restartOutput = stdout;
|
|
811
|
+
resolve();
|
|
812
|
+
}
|
|
813
|
+
);
|
|
814
|
+
});
|
|
815
|
+
} catch (err) {
|
|
816
|
+
console.error('[allyabase] pm2 restart error:', err.message);
|
|
817
|
+
restartOutput = `restart error: ${err.message}`;
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
res.send({ success: true, results, restart: restartOutput.trim() });
|
|
821
|
+
});
|
|
822
|
+
|
|
771
823
|
// Endpoint to get healthcheck
|
|
772
824
|
app.get('/plugin/allyabase/healthcheck', async function(req, res) {
|
|
773
825
|
try {
|
|
@@ -1147,6 +1199,9 @@ async function startServer(params) {
|
|
|
1147
1199
|
// Add deployment routes
|
|
1148
1200
|
deployment.addRoutes(params);
|
|
1149
1201
|
|
|
1202
|
+
// Add get-paid routes (Stripe Connect commission for agora purchases)
|
|
1203
|
+
await getPaid.addRoutes(params);
|
|
1204
|
+
|
|
1150
1205
|
console.log('✅ wiki-plugin-allyabase ready!');
|
|
1151
1206
|
|
|
1152
1207
|
// Set up shutdown hooks to clean up allyabase/PM2 processes
|