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.
@@ -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.10",
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