javascript-solid-server 0.0.100 → 0.0.102

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.
@@ -0,0 +1,231 @@
1
+ /**
2
+ * HTTP 402 Payment Required tests
3
+ */
4
+
5
+ import { describe, it, before, after } from 'node:test';
6
+ import assert from 'node:assert';
7
+ import crypto from 'crypto';
8
+ import { schnorr } from '@noble/curves/secp256k1';
9
+ import {
10
+ startTestServer,
11
+ stopTestServer,
12
+ getBaseUrl,
13
+ assertStatus
14
+ } from './helpers.js';
15
+ import { jcs, sha256Hex } from '../src/mrc20.js';
16
+
17
+ // Generate a test keypair for NIP-98 auth
18
+ const privkey = crypto.randomBytes(32);
19
+ const pubkey = Buffer.from(schnorr.getPublicKey(privkey)).toString('hex');
20
+
21
+ /**
22
+ * Create a NIP-98 auth header for a request
23
+ */
24
+ function createNip98Header(url, method = 'GET') {
25
+ const event = {
26
+ pubkey,
27
+ created_at: Math.floor(Date.now() / 1000),
28
+ kind: 27235,
29
+ tags: [
30
+ ['u', url],
31
+ ['method', method]
32
+ ],
33
+ content: ''
34
+ };
35
+
36
+ // Compute event id
37
+ const serialized = JSON.stringify([0, event.pubkey, event.created_at, event.kind, event.tags, event.content]);
38
+ event.id = crypto.createHash('sha256').update(serialized).digest('hex');
39
+
40
+ // Sign with schnorr
41
+ const sig = schnorr.sign(event.id, privkey);
42
+ event.sig = Buffer.from(sig).toString('hex');
43
+
44
+ const token = Buffer.from(JSON.stringify(event)).toString('base64');
45
+ return `Nostr ${token}`;
46
+ }
47
+
48
+ describe('HTTP 402 Pay Middleware', () => {
49
+ const POD_ADDRESS = 'test-pod-address';
50
+
51
+ before(async () => {
52
+ await startTestServer({ pay: true, payCost: 10, payAddress: POD_ADDRESS });
53
+ });
54
+
55
+ after(async () => {
56
+ await stopTestServer();
57
+ });
58
+
59
+ describe('GET /pay/.balance', () => {
60
+ it('should return 401 without auth', async () => {
61
+ const res = await fetch(`${getBaseUrl()}/pay/.balance`);
62
+ assertStatus(res, 401);
63
+ });
64
+
65
+ it('should return zero balance for new user', async () => {
66
+ const url = `${getBaseUrl()}/pay/.balance`;
67
+ const res = await fetch(url, {
68
+ headers: { 'Authorization': createNip98Header(url) }
69
+ });
70
+ assertStatus(res, 200);
71
+ const body = await res.json();
72
+ assert.strictEqual(body.balance, 0);
73
+ assert.strictEqual(body.cost, 10);
74
+ assert.strictEqual(body.unit, 'sat');
75
+ assert.ok(body.did.startsWith('did:nostr:'));
76
+ });
77
+ });
78
+
79
+ describe('GET /pay/* (paid access)', () => {
80
+ it('should return 401 without auth', async () => {
81
+ const res = await fetch(`${getBaseUrl()}/pay/test-resource`);
82
+ assertStatus(res, 401);
83
+ });
84
+
85
+ it('should return 402 with zero balance', async () => {
86
+ const url = `${getBaseUrl()}/pay/test-resource`;
87
+ const res = await fetch(url, {
88
+ headers: { 'Authorization': createNip98Header(url) }
89
+ });
90
+ assertStatus(res, 402);
91
+ const body = await res.json();
92
+ assert.strictEqual(body.error, 'Payment Required');
93
+ assert.strictEqual(body.balance, 0);
94
+ assert.strictEqual(body.cost, 10);
95
+ assert.strictEqual(body.deposit, '/pay/.deposit');
96
+ });
97
+ });
98
+
99
+ describe('POST /pay/.deposit', () => {
100
+ it('should return 401 without auth', async () => {
101
+ const url = `${getBaseUrl()}/pay/.deposit`;
102
+ const res = await fetch(url, { method: 'POST', body: 'test' });
103
+ assertStatus(res, 401);
104
+ });
105
+
106
+ it('should return 400 without TXO URI', async () => {
107
+ const url = `${getBaseUrl()}/pay/.deposit`;
108
+ const res = await fetch(url, {
109
+ method: 'POST',
110
+ headers: { 'Authorization': createNip98Header(url, 'POST') }
111
+ });
112
+ assertStatus(res, 400);
113
+ });
114
+
115
+ it('should return 400 for invalid TXO URI', async () => {
116
+ const url = `${getBaseUrl()}/pay/.deposit`;
117
+ const res = await fetch(url, {
118
+ method: 'POST',
119
+ headers: { 'Authorization': createNip98Header(url, 'POST') },
120
+ body: 'not-a-valid-txo'
121
+ });
122
+ assertStatus(res, 400);
123
+ const body = await res.json();
124
+ assert.ok(body.error.includes('Invalid TXO URI'));
125
+ });
126
+ });
127
+
128
+ describe('POST /pay/.deposit (MRC20)', () => {
129
+ function makeStatePair(toAddress, amt = 100) {
130
+ const prevState = {
131
+ profile: 'mono.mrc20.v0.1',
132
+ prev: '0'.repeat(64),
133
+ seq: 0,
134
+ ticker: 'TEST',
135
+ name: 'Test Token',
136
+ decimals: 0,
137
+ supply: 1000,
138
+ balances: { creator: 1000 },
139
+ ops: []
140
+ };
141
+ const state = {
142
+ profile: 'mono.mrc20.v0.1',
143
+ prev: sha256Hex(jcs(prevState)),
144
+ seq: 1,
145
+ ticker: 'TEST',
146
+ name: 'Test Token',
147
+ decimals: 0,
148
+ supply: 1000,
149
+ balances: { creator: 1000 - amt, [toAddress]: amt },
150
+ ops: [{ op: 'urn:mono:op:transfer', from: 'creator', to: toAddress, amt }]
151
+ };
152
+ return { prevState, state };
153
+ }
154
+
155
+ it('should accept valid MRC20 deposit', async () => {
156
+ const { prevState, state } = makeStatePair(POD_ADDRESS, 500);
157
+ const url = `${getBaseUrl()}/pay/.deposit`;
158
+ const res = await fetch(url, {
159
+ method: 'POST',
160
+ headers: {
161
+ 'Authorization': createNip98Header(url, 'POST'),
162
+ 'Content-Type': 'application/json'
163
+ },
164
+ body: JSON.stringify({ type: 'mrc20', state, prevState })
165
+ });
166
+ assertStatus(res, 200);
167
+ const body = await res.json();
168
+ assert.strictEqual(body.deposited, 500);
169
+ assert.strictEqual(body.ticker, 'TEST');
170
+ assert.strictEqual(body.unit, 'token');
171
+ assert.ok(body.balance >= 500);
172
+ });
173
+
174
+ it('should reject MRC20 deposit with broken chain', async () => {
175
+ const { prevState, state } = makeStatePair(POD_ADDRESS);
176
+ state.prev = 'tampered';
177
+ const url = `${getBaseUrl()}/pay/.deposit`;
178
+ const res = await fetch(url, {
179
+ method: 'POST',
180
+ headers: {
181
+ 'Authorization': createNip98Header(url, 'POST'),
182
+ 'Content-Type': 'application/json'
183
+ },
184
+ body: JSON.stringify({ type: 'mrc20', state, prevState })
185
+ });
186
+ assertStatus(res, 400);
187
+ const body = await res.json();
188
+ assert.ok(body.error.includes('State chain break'));
189
+ });
190
+
191
+ it('should reject MRC20 deposit to wrong address', async () => {
192
+ const { prevState, state } = makeStatePair('wrong-address');
193
+ const url = `${getBaseUrl()}/pay/.deposit`;
194
+ const res = await fetch(url, {
195
+ method: 'POST',
196
+ headers: {
197
+ 'Authorization': createNip98Header(url, 'POST'),
198
+ 'Content-Type': 'application/json'
199
+ },
200
+ body: JSON.stringify({ type: 'mrc20', state, prevState })
201
+ });
202
+ assertStatus(res, 400);
203
+ const body = await res.json();
204
+ assert.ok(body.error.includes('No transfers'));
205
+ });
206
+ });
207
+
208
+ describe('Pay disabled', () => {
209
+ let noPayServer;
210
+ let noPayUrl;
211
+
212
+ before(async () => {
213
+ // Start a separate server without pay enabled
214
+ const { createServer } = await import('../src/server.js');
215
+ noPayServer = createServer({ logger: false, forceCloseConnections: true, pay: false });
216
+ await noPayServer.listen({ port: 0, host: '127.0.0.1' });
217
+ const addr = noPayServer.server.address();
218
+ noPayUrl = `http://127.0.0.1:${addr.port}`;
219
+ });
220
+
221
+ after(async () => {
222
+ if (noPayServer) await noPayServer.close();
223
+ });
224
+
225
+ it('should not intercept /pay/ when disabled', async () => {
226
+ const res = await fetch(`${noPayUrl}/pay/.balance`);
227
+ // Without pay enabled, dotfile security blocks .balance with 403
228
+ assert.ok(res.status === 401 || res.status === 403 || res.status === 404);
229
+ });
230
+ });
231
+ });
@@ -0,0 +1,174 @@
1
+ /**
2
+ * Web Ledger module tests
3
+ * Verifies spec compliance with https://webledgers.org/
4
+ */
5
+
6
+ import { describe, it } from 'node:test';
7
+ import assert from 'node:assert';
8
+ import {
9
+ createLedger,
10
+ getBalance,
11
+ setBalance,
12
+ credit,
13
+ debit,
14
+ listBalances,
15
+ compact
16
+ } from '../src/webledger.js';
17
+
18
+ const CONTEXT = 'https://w3id.org/webledgers';
19
+
20
+ describe('Web Ledger', () => {
21
+ describe('createLedger', () => {
22
+ it('should create spec-compliant ledger with required fields', () => {
23
+ const ledger = createLedger();
24
+ assert.strictEqual(ledger['@context'], CONTEXT);
25
+ assert.strictEqual(ledger.type, 'WebLedger');
26
+ assert.strictEqual(ledger.defaultCurrency, 'satoshi');
27
+ assert.ok(Array.isArray(ledger.entries));
28
+ assert.strictEqual(ledger.entries.length, 0);
29
+ assert.ok(typeof ledger.created === 'number');
30
+ assert.ok(typeof ledger.updated === 'number');
31
+ });
32
+
33
+ it('should accept custom options', () => {
34
+ const ledger = createLedger({
35
+ name: 'Alice Credits',
36
+ description: 'Pod credits for alice.pod',
37
+ id: 'https://alice.example/.well-known/webledgers/webledgers.json',
38
+ defaultCurrency: 'USD'
39
+ });
40
+ assert.strictEqual(ledger.name, 'Alice Credits');
41
+ assert.strictEqual(ledger.description, 'Pod credits for alice.pod');
42
+ assert.strictEqual(ledger.id, 'https://alice.example/.well-known/webledgers/webledgers.json');
43
+ assert.strictEqual(ledger.defaultCurrency, 'USD');
44
+ });
45
+ });
46
+
47
+ describe('getBalance / setBalance', () => {
48
+ it('should return 0 for unknown URI', () => {
49
+ const ledger = createLedger();
50
+ assert.strictEqual(getBalance(ledger, 'did:nostr:abc123'), 0);
51
+ });
52
+
53
+ it('should set and get balance', () => {
54
+ const ledger = createLedger();
55
+ setBalance(ledger, 'did:nostr:abc123', 5000);
56
+ assert.strictEqual(getBalance(ledger, 'did:nostr:abc123'), 5000);
57
+ });
58
+
59
+ it('should create spec-compliant Entry', () => {
60
+ const ledger = createLedger();
61
+ setBalance(ledger, 'did:nostr:abc123', 100);
62
+ const entry = ledger.entries[0];
63
+ assert.strictEqual(entry.type, 'Entry');
64
+ assert.strictEqual(entry.url, 'did:nostr:abc123');
65
+ assert.strictEqual(entry.amount, '100');
66
+ });
67
+
68
+ it('should update existing entry', () => {
69
+ const ledger = createLedger();
70
+ setBalance(ledger, 'did:nostr:abc123', 100);
71
+ setBalance(ledger, 'did:nostr:abc123', 200);
72
+ assert.strictEqual(ledger.entries.length, 1);
73
+ assert.strictEqual(getBalance(ledger, 'did:nostr:abc123'), 200);
74
+ });
75
+
76
+ it('should handle array amount format', () => {
77
+ const ledger = createLedger();
78
+ ledger.entries.push({
79
+ type: 'Entry',
80
+ url: 'did:nostr:multi',
81
+ amount: [
82
+ { currency: 'satoshi', value: '50000' },
83
+ { currency: 'USD', value: '25.00' }
84
+ ]
85
+ });
86
+ assert.strictEqual(getBalance(ledger, 'did:nostr:multi'), 50000);
87
+ });
88
+ });
89
+
90
+ describe('credit / debit', () => {
91
+ it('should credit a new URI', () => {
92
+ const ledger = createLedger();
93
+ const bal = credit(ledger, 'did:nostr:user1', 1000);
94
+ assert.strictEqual(bal, 1000);
95
+ assert.strictEqual(getBalance(ledger, 'did:nostr:user1'), 1000);
96
+ });
97
+
98
+ it('should accumulate credits', () => {
99
+ const ledger = createLedger();
100
+ credit(ledger, 'did:nostr:user1', 1000);
101
+ const bal = credit(ledger, 'did:nostr:user1', 500);
102
+ assert.strictEqual(bal, 1500);
103
+ });
104
+
105
+ it('should debit when sufficient balance', () => {
106
+ const ledger = createLedger();
107
+ credit(ledger, 'did:nostr:user1', 1000);
108
+ const result = debit(ledger, 'did:nostr:user1', 300);
109
+ assert.strictEqual(result.success, true);
110
+ assert.strictEqual(result.balance, 700);
111
+ });
112
+
113
+ it('should fail debit when insufficient balance', () => {
114
+ const ledger = createLedger();
115
+ credit(ledger, 'did:nostr:user1', 100);
116
+ const result = debit(ledger, 'did:nostr:user1', 200);
117
+ assert.strictEqual(result.success, false);
118
+ assert.strictEqual(result.balance, 100);
119
+ // Balance unchanged
120
+ assert.strictEqual(getBalance(ledger, 'did:nostr:user1'), 100);
121
+ });
122
+
123
+ it('should fail debit on zero balance', () => {
124
+ const ledger = createLedger();
125
+ const result = debit(ledger, 'did:nostr:nobody', 1);
126
+ assert.strictEqual(result.success, false);
127
+ assert.strictEqual(result.balance, 0);
128
+ });
129
+ });
130
+
131
+ describe('listBalances / compact', () => {
132
+ it('should list non-zero balances', () => {
133
+ const ledger = createLedger();
134
+ credit(ledger, 'did:nostr:a', 100);
135
+ credit(ledger, 'did:nostr:b', 0);
136
+ credit(ledger, 'did:nostr:c', 50);
137
+ const list = listBalances(ledger);
138
+ assert.strictEqual(list.length, 2);
139
+ assert.ok(list.find(e => e.url === 'did:nostr:a' && e.amount === 100));
140
+ assert.ok(list.find(e => e.url === 'did:nostr:c' && e.amount === 50));
141
+ });
142
+
143
+ it('should compact zero-balance entries', () => {
144
+ const ledger = createLedger();
145
+ credit(ledger, 'did:nostr:a', 100);
146
+ credit(ledger, 'did:nostr:b', 50);
147
+ debit(ledger, 'did:nostr:b', 50);
148
+ assert.strictEqual(ledger.entries.length, 2);
149
+ compact(ledger);
150
+ assert.strictEqual(ledger.entries.length, 1);
151
+ assert.strictEqual(ledger.entries[0].url, 'did:nostr:a');
152
+ });
153
+ });
154
+
155
+ describe('URI format support', () => {
156
+ it('should work with did:nostr URIs', () => {
157
+ const ledger = createLedger();
158
+ credit(ledger, 'did:nostr:de7ecd1e2976a6adb2ffa5f4db81a7d812c8bb6698aa00dcf1e76adb55efd645', 100);
159
+ assert.strictEqual(getBalance(ledger, 'did:nostr:de7ecd1e2976a6adb2ffa5f4db81a7d812c8bb6698aa00dcf1e76adb55efd645'), 100);
160
+ });
161
+
162
+ it('should work with WebID URIs', () => {
163
+ const ledger = createLedger();
164
+ credit(ledger, 'https://alice.example/profile/card#me', 500);
165
+ assert.strictEqual(getBalance(ledger, 'https://alice.example/profile/card#me'), 500);
166
+ });
167
+
168
+ it('should work with mailto URIs', () => {
169
+ const ledger = createLedger();
170
+ credit(ledger, 'mailto:alice@example.com', 200);
171
+ assert.strictEqual(getBalance(ledger, 'mailto:alice@example.com'), 200);
172
+ });
173
+ });
174
+ });