Skip to content

Commit 48ae891

Browse files
feat: zero-cost PaymentCondition as membership gate
PaymentCondition with amount=0 grants access if the user has a ledger entry (has deposited at some point). No sats deducted. Checks for entry existence, not balance >= 0, since getBalance returns 0 for both "no entry" and "entry with zero balance". Fixes JavaScriptSolidServer#244
1 parent 10158fd commit 48ae891

2 files changed

Lines changed: 35 additions & 4 deletions

File tree

src/wac/checker.js

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -163,15 +163,22 @@ async function checkAuthorizations(authorizations, targetUrl, agentWebId, requir
163163
c.type === 'PaymentCondition' || c.type === 'https://webacl.org/ns#PaymentCondition'
164164
);
165165
if (paymentCondition) {
166-
// Check if agent has sufficient balance
167166
const cost = parseInt(paymentCondition.amount, 10) || 0;
168167
const currency = paymentCondition.currency || 'sat';
169-
if (agentWebId && cost > 0) {
168+
169+
if (agentWebId) {
170170
try {
171171
const ledger = await readLedger();
172172
const balance = getBalance(ledger, agentWebId);
173-
if (balance >= cost) {
174-
// Deduct and grant access
173+
174+
// Zero-cost gate: verify they have a ledger entry (have deposited at some point)
175+
if (cost === 0) {
176+
const hasEntry = ledger.entries?.some(e => e.url === agentWebId);
177+
if (hasEntry) return { allowed: true };
178+
}
179+
180+
// Paid access: check balance and deduct
181+
if (cost > 0 && balance >= cost) {
175182
debit(ledger, agentWebId, cost, currency === 'sats' ? 'sat' : currency);
176183
const { writeLedger } = await import('../webledger.js');
177184
await writeLedger(ledger);

test/wac.test.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -428,6 +428,30 @@ describe('WAC Conditions', () => {
428428
assert.strictEqual(auths[0].conditions[0].type, 'UnknownFutureCondition');
429429
});
430430

431+
it('should parse zero-cost PaymentCondition', async () => {
432+
const acl = {
433+
'@context': { 'acl': 'http://www.w3.org/ns/auth/acl#' },
434+
'@graph': [{
435+
'@id': '#gate',
436+
'@type': 'acl:Authorization',
437+
'acl:agentClass': { '@id': 'acl:AuthenticatedAgent' },
438+
'acl:accessTo': { '@id': 'https://alice.example/members/' },
439+
'acl:mode': [{ '@id': 'acl:Read' }],
440+
'acl:condition': {
441+
'@type': 'PaymentCondition',
442+
'amount': '0',
443+
'currency': 'sats'
444+
}
445+
}]
446+
};
447+
448+
const auths = await parseAcl(JSON.stringify(acl), 'https://alice.example/members/.acl');
449+
const condition = auths[0].conditions[0];
450+
451+
assert.strictEqual(condition.type, 'PaymentCondition');
452+
assert.strictEqual(condition.amount, '0');
453+
});
454+
431455
it('should parse PaymentCondition with all fields', async () => {
432456
const acl = {
433457
'@context': { 'acl': 'http://www.w3.org/ns/auth/acl#' },

0 commit comments

Comments
 (0)