Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions scripts/demo-402.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
#!/bin/bash
# Demo: HTTP 402 Payment-Gated Article
# Requires: JSS running on localhost:3000 with --pay --pay-cost 10

BASE="http://localhost:3000"

echo "=== Setting up payment-gated article demo ==="

# 1. Create the premium article
echo "Creating premium article..."
curl -s -X PUT "$BASE/premium/article.jsonld" \
-H "Content-Type: application/ld+json" \
-d '{
"@context": "https://schema.org",
"@type": "Article",
"headline": "The Future of Web Payments",
"author": "Melvin Carvalho",
"datePublished": "2026-03-26",
"articleBody": "This premium article explains how HTTP 402 enables native web payments. The decentralised web finally has a business model. No Stripe. No PayPal. No app store taking 30%. Just HTTP status codes and Lightning invoices."
}'

# 2. Create the ACL with PaymentCondition
echo "Creating payment-gated ACL..."
curl -s -X PUT "$BASE/premium/article.jsonld.acl" \
-H "Content-Type: application/ld+json" \
-d '{
"@context": {
"acl": "http://www.w3.org/ns/auth/acl#",
"foaf": "http://xmlns.com/foaf/0.1/"
},
"@graph": [{
"@id": "#paid",
"@type": "acl:Authorization",
"acl:agentClass": { "@id": "acl:AuthenticatedAgent" },
"acl:accessTo": { "@id": "'$BASE'/premium/article.jsonld" },
"acl:mode": [{ "@id": "acl:Read" }],
"acl:condition": {
"@type": "PaymentCondition",
"amount": "10",
"currency": "sats",
"chain": "tbtc4"
}
}]
}'

echo ""
echo "=== Demo ready ==="
echo ""
echo "1. Try accessing the article (should get 402):"
echo " curl $BASE/premium/article.jsonld"
echo ""
echo "2. Deposit testnet4 sats:"
echo " curl -X POST -H 'Authorization: Nostr <nip98-token>' $BASE/pay/.deposit -d 'txo:tbtc4:txid:vout'"
echo ""
echo "3. Try again (should get 200 + article):"
echo " curl -H 'Authorization: Nostr <nip98-token>' $BASE/premium/article.jsonld"
23 changes: 21 additions & 2 deletions src/wac/checker.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import * as storage from '../storage/filesystem.js';
import { parseAcl, AccessMode, AgentClass } from './parser.js';
import { getAclUrl } from '../ldp/headers.js';
import { readLedger, getBalance, debit } from '../webledger.js';

/**
* Check if agent has required access mode for resource
Expand Down Expand Up @@ -37,7 +38,7 @@ export async function checkAccess({

// Check authorizations
// Note: For default ACLs, we check if the ACL's default rules apply to the actual resource URL
const result = checkAuthorizations(
const result = await checkAuthorizations(
authorizations,
resourceUrl, // Use actual resource URL, not the ACL container URL
agentWebId,
Expand Down Expand Up @@ -128,7 +129,7 @@ function getParentPath(path) {
// Supported condition types
const SUPPORTED_CONDITIONS = ['PaymentCondition', 'https://webacl.org/ns#PaymentCondition'];

function checkAuthorizations(authorizations, targetUrl, agentWebId, requiredMode, isDefault) {
async function checkAuthorizations(authorizations, targetUrl, agentWebId, requiredMode, isDefault) {
for (const auth of authorizations) {
// For default ACLs, check if auth has default rules and matches target
// For direct ACLs, check if accessTo matches target
Expand Down Expand Up @@ -162,6 +163,24 @@ function checkAuthorizations(authorizations, targetUrl, agentWebId, requiredMode
c.type === 'PaymentCondition' || c.type === 'https://webacl.org/ns#PaymentCondition'
);
if (paymentCondition) {
// Check if agent has sufficient balance
const cost = parseInt(paymentCondition.amount, 10) || 0;
const currency = paymentCondition.currency || 'sat';
if (agentWebId && cost > 0) {
try {
const ledger = await readLedger();
const balance = getBalance(ledger, agentWebId, currency === 'sats' ? 'sat' : currency);
if (balance >= cost) {
// Deduct and grant access
debit(ledger, agentWebId, cost, currency === 'sats' ? 'sat' : currency);
const { writeLedger } = await import('../webledger.js');
await writeLedger(ledger);
return { allowed: true, paid: cost };
}
} catch (e) {
// Ledger read failed — fall through to payment required
}
}
return { allowed: false, paymentRequired: paymentCondition };
}
}
Expand Down