javascript-solid-server 0.0.103 → 0.0.105

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/README.md CHANGED
@@ -156,6 +156,8 @@ jss --help # Show help
156
156
  | `--pay-cost <n>` | Cost per request in satoshis | 1 |
157
157
  | `--pay-mempool-url <url>` | Mempool API URL for deposit verification | (testnet4) |
158
158
  | `--pay-address <addr>` | Address for receiving deposits | - |
159
+ | `--pay-token <ticker>` | Token to sell (enables primary market + withdrawal) | - |
160
+ | `--pay-rate <n>` | Sats per token for buy/withdraw | 1 |
159
161
  | `--mongo` | Enable MongoDB-backed /db/ route | false |
160
162
  | `--mongo-url <url>` | MongoDB connection URL | mongodb://localhost:27017 |
161
163
  | `--mongo-database <name>` | MongoDB database name | solid |
@@ -187,6 +189,8 @@ export JSS_SOLIDOS_UI=true
187
189
  export JSS_PAY=true
188
190
  export JSS_PAY_COST=10
189
191
  export JSS_PAY_ADDRESS=your-address
192
+ export JSS_PAY_TOKEN=PODS
193
+ export JSS_PAY_RATE=10
190
194
  export JSS_MONGO=true
191
195
  export JSS_MONGO_URL=mongodb://localhost:27017
192
196
  export JSS_MONGO_DATABASE=solid
@@ -823,15 +827,18 @@ Supported formats: `50MB`, `1GB`, `500KB`, `1TB`
823
827
  Monetize API endpoints with per-request satoshi payments. Resources under `/pay/*` require NIP-98 authentication and a positive balance.
824
828
 
825
829
  ```bash
826
- jss start --pay --pay-cost 10 --pay-address your-address
830
+ jss start --pay --pay-cost 10 --pay-address your-address --pay-token PODS --pay-rate 10
827
831
  ```
828
832
 
829
833
  ### Routes
830
834
 
831
835
  | Method | Path | Description |
832
836
  |--------|------|-------------|
837
+ | GET | `/pay/.info` | Public: cost, token info, available routes |
833
838
  | GET | `/pay/.balance` | Check your balance (NIP-98 auth) |
834
- | POST | `/pay/.deposit` | Deposit sats via TXO URI (`txid:vout`) |
839
+ | POST | `/pay/.deposit` | Deposit sats via TXO URI or MRC20 state proof |
840
+ | POST | `/pay/.buy` | Buy tokens with sat balance (requires `--pay-token`) |
841
+ | POST | `/pay/.withdraw` | Withdraw balance as portable tokens (requires `--pay-token`) |
835
842
  | GET | `/pay/*` | Paid resource access (deducts balance) |
836
843
 
837
844
  ### How It Works
@@ -840,7 +847,8 @@ jss start --pay --pay-cost 10 --pay-address your-address
840
847
  2. Check balance at `/pay/.balance`
841
848
  3. Deposit sats by POSTing a TXO URI to `/pay/.deposit`
842
849
  4. Access paid resources — each request deducts the configured cost
843
- 5. Balance tracked in a [Web Ledger](https://webledgers.org/) at `/.well-known/webledgers/webledgers.json`
850
+ 5. Optionally buy tokens (`/pay/.buy`) or withdraw as portable tokens (`/pay/.withdraw`)
851
+ 6. Balance tracked in a [Web Ledger](https://webledgers.org/) at `/.well-known/webledgers/webledgers.json`
844
852
 
845
853
  ### Example
846
854
 
@@ -855,9 +863,21 @@ curl -X POST -H "Authorization: Nostr <base64-event>" \
855
863
 
856
864
  # Access paid resource
857
865
  curl -H "Authorization: Nostr <base64-event>" http://localhost:3000/pay/my-resource
866
+
867
+ # Buy tokens with sat balance
868
+ curl -X POST -H "Authorization: Nostr <base64-event>" \
869
+ -H "Content-Type: application/json" \
870
+ http://localhost:3000/pay/.buy \
871
+ -d '{"amount": 100}'
872
+
873
+ # Withdraw entire balance as portable tokens
874
+ curl -X POST -H "Authorization: Nostr <base64-event>" \
875
+ -H "Content-Type: application/json" \
876
+ http://localhost:3000/pay/.withdraw \
877
+ -d '{"all": true}'
858
878
  ```
859
879
 
860
- Deposit verification uses the mempool API (default: testnet4). The `X-Balance` and `X-Cost` headers are returned on successful paid requests.
880
+ Deposit verification uses the mempool API (default: testnet4). The `X-Balance` and `X-Cost` headers are returned on successful paid requests. Buy and withdraw return portable MRC20 proofs with Bitcoin anchor data for independent verification.
861
881
 
862
882
  ## Authentication
863
883
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "javascript-solid-server",
3
- "version": "0.0.103",
3
+ "version": "0.0.105",
4
4
  "description": "A minimal, fast Solid server",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -5,10 +5,13 @@
5
5
  * Authentication via NIP-98. Balance tracking via Web Ledgers spec.
6
6
  *
7
7
  * Routes:
8
- * GET /pay/.balance check your balance
9
- * POST /pay/.deposit deposit sats (TXO URI) or tokens (MRC20 state proof)
10
- * GET /pay/* paid resource access (requires balance >= cost)
11
- * PUT /pay/* upload resources (standard auth)
8
+ * GET /pay/.info public endpoint: cost, token info, available routes
9
+ * GET /pay/.balance check your balance
10
+ * POST /pay/.deposit deposit sats (TXO URI) or tokens (MRC20 state proof)
11
+ * POST /pay/.buy buy tokens with sat balance (primary market)
12
+ * POST /pay/.withdraw — withdraw balance as tokens (portable MRC20 proof)
13
+ * GET /pay/* — paid resource access (requires balance >= cost)
14
+ * PUT /pay/* — upload resources (standard auth)
12
15
  *
13
16
  * Ledger: /.well-known/webledgers/webledgers.json (webledgers.org spec)
14
17
  *
@@ -155,6 +158,30 @@ export function createPayHandler(options = {}) {
155
158
  const url = request.url.split('?')[0];
156
159
  if (!isPayRequest(request.url)) return;
157
160
 
161
+ // --- GET /pay/.info — public, no auth ---
162
+ if (url === '/pay/.info' && request.method === 'GET') {
163
+ const info = {
164
+ cost,
165
+ unit: 'sat',
166
+ deposit: '/pay/.deposit',
167
+ balance: '/pay/.balance'
168
+ };
169
+ if (payToken) {
170
+ const trail = await loadTrail(payToken);
171
+ info.token = {
172
+ ticker: payToken,
173
+ rate: payRate,
174
+ buy: '/pay/.buy',
175
+ withdraw: '/pay/.withdraw'
176
+ };
177
+ if (trail) {
178
+ info.token.supply = trail.latestState?.supply ?? null;
179
+ info.token.issuer = trail.pubkeyBase ?? null;
180
+ }
181
+ }
182
+ return reply.send(info);
183
+ }
184
+
158
185
  // --- GET /pay/.balance ---
159
186
  if (url === '/pay/.balance' && request.method === 'GET') {
160
187
  const pubkey = await getNostrPubkey(request);
@@ -363,6 +390,102 @@ export function createPayHandler(options = {}) {
363
390
  });
364
391
  }
365
392
 
393
+ // --- POST /pay/.withdraw — withdraw balance as tokens ---
394
+ if (url === '/pay/.withdraw' && request.method === 'POST') {
395
+ const pubkey = await getNostrPubkey(request);
396
+ if (!pubkey) {
397
+ return reply.code(401).send({ error: 'NIP-98 authentication required' });
398
+ }
399
+
400
+ if (!payToken) {
401
+ return reply.code(400).send({ error: 'Withdrawal not configured (no --pay-token set)' });
402
+ }
403
+
404
+ // Parse withdraw request
405
+ let body = request.body;
406
+ if (Buffer.isBuffer(body)) body = JSON.parse(body.toString('utf8'));
407
+ if (typeof body === 'string') body = JSON.parse(body);
408
+
409
+ const didUri = pubkeyToDidNostr(pubkey);
410
+ const ledger = await readLedger();
411
+ const balance = getBalance(ledger, didUri);
412
+
413
+ // Calculate withdrawal amount
414
+ let satCost, tokenAmount;
415
+ if (body?.all) {
416
+ satCost = balance;
417
+ tokenAmount = Math.floor(balance / payRate);
418
+ } else if (body?.sats) {
419
+ satCost = Math.floor(body.sats);
420
+ tokenAmount = Math.floor(satCost / payRate);
421
+ } else if (body?.tokens) {
422
+ tokenAmount = Math.floor(body.tokens);
423
+ satCost = tokenAmount * payRate;
424
+ } else {
425
+ return reply.code(400).send({
426
+ error: 'Specify tokens, sats, or all: true',
427
+ balance,
428
+ rate: payRate,
429
+ unit: 'sat/token'
430
+ });
431
+ }
432
+
433
+ if (tokenAmount <= 0) {
434
+ return reply.code(400).send({ error: 'Nothing to withdraw', balance, rate: payRate });
435
+ }
436
+
437
+ if (balance < satCost) {
438
+ return reply.code(402).send({
439
+ error: 'Insufficient balance',
440
+ balance,
441
+ cost: satCost,
442
+ rate: payRate
443
+ });
444
+ }
445
+
446
+ // Load token trail
447
+ const trail = await loadTrail(payToken);
448
+ if (!trail) {
449
+ return reply.code(500).send({ error: `Token ${payToken} not minted on this pod` });
450
+ }
451
+
452
+ // Transfer tokens to user
453
+ let result;
454
+ try {
455
+ result = await transferToken({
456
+ ticker: payToken,
457
+ to: pubkey,
458
+ amount: tokenAmount,
459
+ mempoolUrl
460
+ });
461
+ } catch (err) {
462
+ return reply.code(500).send({ error: `Transfer failed: ${err.message}` });
463
+ }
464
+
465
+ // Debit balance
466
+ debit(ledger, didUri, satCost);
467
+ await writeLedger(ledger);
468
+
469
+ return reply.send({
470
+ withdrawn: tokenAmount,
471
+ ticker: payToken,
472
+ cost: satCost,
473
+ rate: payRate,
474
+ balance: getBalance(ledger, didUri),
475
+ unit: 'sat',
476
+ txid: result.txid,
477
+ proof: {
478
+ state: result.state,
479
+ prevState: result.prevState,
480
+ anchor: {
481
+ pubkey: result.trail.pubkeyBase,
482
+ stateStrings: result.trail.stateStrings,
483
+ network: result.trail.network
484
+ }
485
+ }
486
+ });
487
+ }
488
+
366
489
  // --- GET/HEAD /pay/* — paid resource access ---
367
490
  if (request.method === 'GET' || request.method === 'HEAD') {
368
491
  const pubkey = await getNostrPubkey(request);