javascript-solid-server 0.0.46 → 0.0.48
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/.claude/settings.local.json +62 -1
- package/README.md +47 -0
- package/package.json +2 -2
- package/src/handlers/git.js +13 -0
- package/src/server.js +8 -0
- package/src/wac/checker.js +3 -3
- package/test/interop/README.md +43 -0
- package/test/interop/css-interop.js +102 -0
- package/test/interop/nss-discovery.js +82 -0
- package/test/interop/nss-local-fixed.js +50 -0
- package/test/interop/nss-local.js +53 -0
- package/test/interop/rdflib-discovery.js +44 -0
- package/test/interop/solidcommunity-interop.js +126 -0
- package/test/interop/webid-discovery.js +58 -0
|
@@ -139,7 +139,68 @@
|
|
|
139
139
|
"Bash(gh gist view:*)",
|
|
140
140
|
"Bash(./bin/git-credential-nostr acl:*)",
|
|
141
141
|
"Bash(DATA_ROOT=/tmp/jss-git-test/data node:*)",
|
|
142
|
-
"Bash(git remote set-url:*)"
|
|
142
|
+
"Bash(git remote set-url:*)",
|
|
143
|
+
"Bash(for:*)",
|
|
144
|
+
"Bash(^/**\" | head -1 | sed ''''s/.*\\* //'''')\")",
|
|
145
|
+
"Bash(if [ ! -d \"node-solid-server\" ])",
|
|
146
|
+
"Bash(then git clone --depth 1 https://github.com/nodeSolidServer/node-solid-server.git)",
|
|
147
|
+
"Bash(node test-local-nss2.js:*)",
|
|
148
|
+
"Bash(npm test)",
|
|
149
|
+
"Bash(repos.json)",
|
|
150
|
+
"Bash(*.log)",
|
|
151
|
+
"Bash(node --check:*)",
|
|
152
|
+
"Bash(gh repo view:*)",
|
|
153
|
+
"Bash(noskey --help:*)",
|
|
154
|
+
"Bash(npx noskey --help:*)",
|
|
155
|
+
"Bash(noskey:*)",
|
|
156
|
+
"Bash(node -e:*)",
|
|
157
|
+
"Bash(node src/publish.js:*)",
|
|
158
|
+
"Bash(git remote add:*)",
|
|
159
|
+
"Bash(git fetch:*)",
|
|
160
|
+
"Bash(git rev-parse:*)",
|
|
161
|
+
"Bash(f502f06c1d7553f4b7159e8d57a1e14819dc3053b59399e080882cc8e6bb62ad )",
|
|
162
|
+
"Bash(798715377357003683b979b41c5d99c0312e6e788d789f0d5df710465483aa3e )",
|
|
163
|
+
"Bash(f810e7491da3390109ddc13a74a1fff985ba3a4735024f2b714c12d213f5ea11 )",
|
|
164
|
+
"Bash(1 )",
|
|
165
|
+
"Bash(911912000 )",
|
|
166
|
+
"Bash(4ccef8c68cf18f8f156a0bb017dfd6e0cc7ebf1672fa2d769e02e2efc700328b 1000000 )",
|
|
167
|
+
"Bash(798715377357003683b979b41c5d99c0312e6e788d789f0d5df710465483aa3e 910911000 )",
|
|
168
|
+
"Bash(~/.gitmark/faucet.txt)",
|
|
169
|
+
"Bash(blocktrails --version:*)",
|
|
170
|
+
"Bash(blocktrails --help:*)",
|
|
171
|
+
"Bash(blocktrails show:*)",
|
|
172
|
+
"Bash(git restore:*)",
|
|
173
|
+
"Bash(npm show:*)",
|
|
174
|
+
"WebFetch(domain:gitlab.com)",
|
|
175
|
+
"Bash(gh repo edit:*)",
|
|
176
|
+
"WebFetch(domain:blocktrails.github.io)",
|
|
177
|
+
"Bash(jq:*)",
|
|
178
|
+
"Bash(SOLID_SYNC=true timeout 45 node:*)",
|
|
179
|
+
"Bash(git -C /home/melvin/remote/github.com/blocktrails/gitmark-amm status)",
|
|
180
|
+
"Bash(SOLID_SYNC=true ANCHOR=true timeout 8 node:*)",
|
|
181
|
+
"Bash(SOLID_SYNC=true ANCHOR=true node:*)",
|
|
182
|
+
"Bash(git -C /home/melvin/remote/github.com/blocktrails/gitmark-amm diff src/watcher.js)",
|
|
183
|
+
"Bash(git -C /home/melvin/remote/github.com/blocktrails/gitmark-amm add src/watcher.js)",
|
|
184
|
+
"Bash(git -C /home/melvin/remote/github.com/blocktrails/gitmark-amm commit -m \"$\\(cat <<''EOF''\nAdd transfer API and HTTP 402 middleware\n\n- Add POST /transfer endpoint for user-to-user token transfers\n- Add verify402Payment middleware for token-gated APIs\n- Add GET /api/quote demo endpoint \\(costs 1 GSAT\\)\n- Add GET /balance/:did and GET /state endpoints\n- Fix anchor function to use encodeBech32m for address derivation\n- Remove OP_RETURN from anchor tx \\(state hash stored in state.json\\)\nEOF\n\\)\")",
|
|
185
|
+
"Bash(git -C /home/melvin/remote/github.com/blocktrails/gitmark-amm push)",
|
|
186
|
+
"Bash(git -C /home/melvin/remote/github.com/blocktrails/gitmark-amm add demo.html src/watcher.js debug.html paywall.html transfer.html)",
|
|
187
|
+
"Bash(git -C /home/melvin/remote/github.com/blocktrails/gitmark-amm commit -m \"$\\(cat <<''EOF''\nAdd NIP-98 paywall, transfer, withdraw, and debug pages\n\n- Implement NIP-98 \\(kind 27235\\) for HTTP 402 authentication\n- Add paywall.html demo page showing NIP-98 flow\n- Add transfer.html for user-to-user GSAT transfers\n- Add debug.html with anchors, state, verify, withdraw, and users tabs\n- Add POST /withdraw endpoint for sats → Bitcoin address\n- Add navigation to demo.html linking all pages\nEOF\n\\)\")",
|
|
188
|
+
"Bash(git -C /home/melvin/remote/github.com/blocktrails/gitmark-amm add test-amm.mjs package.json)",
|
|
189
|
+
"Bash(git -C /home/melvin/remote/github.com/blocktrails/gitmark-amm commit -m \"$\\(cat <<''EOF''\nAdd AMM tests for math, signatures, and NIP-98\n\n- AMM math tests \\(calculateGsatOut, calculateSatsOut, slippage, k invariant\\)\n- Signature verification tests \\(sell, transfer, withdraw requests\\)\n- NIP-98 event creation, verification, and encoding tests\n- Update package.json with test script\nEOF\n\\)\")",
|
|
190
|
+
"Bash(SOLID_SYNC=true node src/watcher.js:*)",
|
|
191
|
+
"Bash(git -C /home/melvin/remote/github.com/blocktrails/gitmark-amm add demo.html src/watcher.js)",
|
|
192
|
+
"Bash(git -C /home/melvin/remote/github.com/blocktrails/gitmark-amm commit -m \"$\\(cat <<''EOF''\nAdd smart polling with manual deposit check\n\n- Change poll interval from 30s to 10 minutes\n- Add POST /check endpoint for manual deposit scan\n- Add 10-second rate limit between manual checks\n- Add \"Check Deposits\" button to demo.html\nEOF\n\\)\")",
|
|
193
|
+
"Bash(git -C /home/melvin/remote/github.com/blocktrails/gitmark-amm add:*)",
|
|
194
|
+
"Bash(git -C /home/melvin/remote/github.com/blocktrails/gitmark-amm commit -m \"Use blocktrails npm package instead of local path\")",
|
|
195
|
+
"Bash(for addr in tb1pdypd4k38q4x0qz5x7hqavjhfpgt2n4tm0egggx587aafqn3wsnds8gm3yf tb1pqxmrkvuyea9v7vv323tmptjfle5tj9y6cpe5g8wqvlz6d5xmfhlqctx7py tb1p0fv2683x2j5htf9n7fkpmxsy4h7yuxmetelq2c6vp8u2zw9rhp2s5kha7v)",
|
|
196
|
+
"Bash(do echo -n \"$addr: \" curl -s \"https://mempool.space/testnet4/api/address/$addr\")",
|
|
197
|
+
"WebFetch(domain:webledgers.org)",
|
|
198
|
+
"Bash(npm pack:*)",
|
|
199
|
+
"Bash(npm info:*)",
|
|
200
|
+
"Bash(tar:*)",
|
|
201
|
+
"Bash(TEST_API=1 API_URL=https://api.solid.social node:*)",
|
|
202
|
+
"Bash(webledgers show:*)",
|
|
203
|
+
"Bash(webledgers set-balance:*)"
|
|
143
204
|
]
|
|
144
205
|
}
|
|
145
206
|
}
|
package/README.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
A minimal, fast, JSON-LD native Solid server.
|
|
4
4
|
|
|
5
|
+
**[Documentation](https://javascriptsolidserver.github.io/docs/)** | **[GitHub](https://github.com/JavaScriptSolidServer/JavaScriptSolidServer)**
|
|
6
|
+
|
|
5
7
|
## Features
|
|
6
8
|
|
|
7
9
|
### Implemented (v0.0.42)
|
|
@@ -526,6 +528,51 @@ curl -X POST https://example.com/.pods \
|
|
|
526
528
|
| [CSS](https://github.com/CommunitySolidServer/CommunitySolidServer) | 5.8 MB | 70 | Modular, configurable |
|
|
527
529
|
| [Pivot](https://github.com/solid-contrib/pivot) | ~6 MB | 70+ | Built on CSS |
|
|
528
530
|
|
|
531
|
+
## Security
|
|
532
|
+
|
|
533
|
+
### Root ACL Required
|
|
534
|
+
|
|
535
|
+
JSS uses **restrictive mode** by default: if no ACL file exists for a resource, access is denied. This prevents unauthorized writes to unprotected containers.
|
|
536
|
+
|
|
537
|
+
**You must create a root `.acl` file** in your data directory. Example (JSON-LD format):
|
|
538
|
+
|
|
539
|
+
```json
|
|
540
|
+
{
|
|
541
|
+
"@context": {
|
|
542
|
+
"acl": "http://www.w3.org/ns/auth/acl#",
|
|
543
|
+
"foaf": "http://xmlns.com/foaf/0.1/"
|
|
544
|
+
},
|
|
545
|
+
"@graph": [
|
|
546
|
+
{
|
|
547
|
+
"@id": "#owner",
|
|
548
|
+
"@type": "acl:Authorization",
|
|
549
|
+
"acl:agent": { "@id": "https://your-domain.com/profile/card#me" },
|
|
550
|
+
"acl:accessTo": { "@id": "https://your-domain.com/" },
|
|
551
|
+
"acl:default": { "@id": "https://your-domain.com/" },
|
|
552
|
+
"acl:mode": [
|
|
553
|
+
{ "@id": "acl:Read" },
|
|
554
|
+
{ "@id": "acl:Write" },
|
|
555
|
+
{ "@id": "acl:Control" }
|
|
556
|
+
]
|
|
557
|
+
},
|
|
558
|
+
{
|
|
559
|
+
"@id": "#public",
|
|
560
|
+
"@type": "acl:Authorization",
|
|
561
|
+
"acl:agentClass": { "@id": "foaf:Agent" },
|
|
562
|
+
"acl:accessTo": { "@id": "https://your-domain.com/" },
|
|
563
|
+
"acl:default": { "@id": "https://your-domain.com/" },
|
|
564
|
+
"acl:mode": [
|
|
565
|
+
{ "@id": "acl:Read" }
|
|
566
|
+
]
|
|
567
|
+
}
|
|
568
|
+
]
|
|
569
|
+
}
|
|
570
|
+
```
|
|
571
|
+
|
|
572
|
+
Save this as `data/.acl` (replacing `your-domain.com` with your actual domain).
|
|
573
|
+
|
|
574
|
+
See [Issue #32](https://github.com/JavaScriptSolidServer/JavaScriptSolidServer/issues/32) for background.
|
|
575
|
+
|
|
529
576
|
## Performance
|
|
530
577
|
|
|
531
578
|
This server is designed for speed. Benchmark results on a typical development machine:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "javascript-solid-server",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.48",
|
|
4
4
|
"description": "A minimal, fast Solid server",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"scripts": {
|
|
19
19
|
"start": "node bin/jss.js start",
|
|
20
20
|
"dev": "node --watch bin/jss.js start",
|
|
21
|
-
"test": "node --test --test-concurrency=1",
|
|
21
|
+
"test": "node --test --test-concurrency=1 'test/*.test.js'",
|
|
22
22
|
"test:cth": "node scripts/test-cth-compat.js",
|
|
23
23
|
"benchmark": "node benchmark.js"
|
|
24
24
|
},
|
package/src/handlers/git.js
CHANGED
|
@@ -70,6 +70,14 @@ function findGitDir(repoPath) {
|
|
|
70
70
|
* @param {FastifyReply} reply
|
|
71
71
|
*/
|
|
72
72
|
export async function handleGit(request, reply) {
|
|
73
|
+
// Handle CORS preflight
|
|
74
|
+
if (request.method === 'OPTIONS') {
|
|
75
|
+
reply.header('Access-Control-Allow-Origin', '*');
|
|
76
|
+
reply.header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
|
77
|
+
reply.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
|
|
78
|
+
return reply.code(200).send();
|
|
79
|
+
}
|
|
80
|
+
|
|
73
81
|
const urlPath = decodeURIComponent(request.url.split('?')[0]);
|
|
74
82
|
const queryString = request.url.split('?')[1] || '';
|
|
75
83
|
|
|
@@ -178,6 +186,11 @@ export async function handleGit(request, reply) {
|
|
|
178
186
|
}
|
|
179
187
|
}
|
|
180
188
|
|
|
189
|
+
// Add CORS headers for browser git clients
|
|
190
|
+
reply.raw.setHeader('Access-Control-Allow-Origin', '*');
|
|
191
|
+
reply.raw.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
|
192
|
+
reply.raw.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
|
|
193
|
+
|
|
181
194
|
reply.raw.writeHead(statusCode);
|
|
182
195
|
headersSent = true;
|
|
183
196
|
reply.raw.write(bodySection);
|
package/src/server.js
CHANGED
|
@@ -74,6 +74,14 @@ export function createServer(options = {}) {
|
|
|
74
74
|
done(null, body);
|
|
75
75
|
});
|
|
76
76
|
|
|
77
|
+
// Git content types need explicit handling (binary data)
|
|
78
|
+
fastify.addContentTypeParser('application/x-git-receive-pack-request', { parseAs: 'buffer' }, (req, body, done) => {
|
|
79
|
+
done(null, body);
|
|
80
|
+
});
|
|
81
|
+
fastify.addContentTypeParser('application/x-git-upload-pack-request', { parseAs: 'buffer' }, (req, body, done) => {
|
|
82
|
+
done(null, body);
|
|
83
|
+
});
|
|
84
|
+
|
|
77
85
|
// Attach server config to requests
|
|
78
86
|
fastify.decorateRequest('connegEnabled', null);
|
|
79
87
|
fastify.decorateRequest('notificationsEnabled', null);
|
package/src/wac/checker.js
CHANGED
|
@@ -28,9 +28,9 @@ export async function checkAccess({
|
|
|
28
28
|
const aclResult = await findApplicableAcl(resourceUrl, resourcePath, isContainer);
|
|
29
29
|
|
|
30
30
|
if (!aclResult) {
|
|
31
|
-
// No ACL found -
|
|
32
|
-
//
|
|
33
|
-
return { allowed:
|
|
31
|
+
// No ACL found - deny by default (restrictive mode)
|
|
32
|
+
// Security: Require explicit ACL for any access
|
|
33
|
+
return { allowed: false, wacAllow: 'user="", public=""' };
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
const { authorizations, isDefault, targetUrl: aclContainerUrl } = aclResult;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Interop Tests
|
|
2
|
+
|
|
3
|
+
Manual tests for debugging cross-server authentication issues. These tests depend on external services and are not run as part of `npm test`.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
cd test/interop
|
|
9
|
+
node <test-file>.js
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Tests
|
|
13
|
+
|
|
14
|
+
| File | Description |
|
|
15
|
+
|------|-------------|
|
|
16
|
+
| `css-interop.js` | Test OIDC config against CSS and NSS servers |
|
|
17
|
+
| `solidcommunity-interop.js` | Test DPoP auth against solidcommunity.net (CSS) |
|
|
18
|
+
| `nss-local.js` | Test DPoP auth against local NSS instance |
|
|
19
|
+
| `nss-local-fixed.js` | Test with Accept header fix applied |
|
|
20
|
+
| `nss-discovery.js` | Debug NSS OIDC discovery process |
|
|
21
|
+
| `webid-discovery.js` | Test WebID profile discovery and content negotiation |
|
|
22
|
+
| `rdflib-discovery.js` | Test rdflib parsing of WebID profiles |
|
|
23
|
+
|
|
24
|
+
## Known Issues
|
|
25
|
+
|
|
26
|
+
- **NSS WebID Discovery Bug**: NSS's `oidc-auth-manager` doesn't send Accept headers when fetching WebID profiles, causing discovery to fail when servers return HTML instead of Turtle. See: https://github.com/nodeSolidServer/oidc-auth-manager/issues/79
|
|
27
|
+
|
|
28
|
+
## Requirements
|
|
29
|
+
|
|
30
|
+
- `node-fetch`
|
|
31
|
+
- `jose`
|
|
32
|
+
- `rdflib` (for rdflib tests)
|
|
33
|
+
- Valid credentials for melvincarvalho.com IdP
|
|
34
|
+
|
|
35
|
+
## Note on WebID Formats
|
|
36
|
+
|
|
37
|
+
Two popular WebID locations exist:
|
|
38
|
+
- `https://example.com/#me` (root + fragment)
|
|
39
|
+
- `https://example.com/profile/card#me` (profile path)
|
|
40
|
+
|
|
41
|
+
JSS is currently configured for `/profile/card` but will support `/` in the future.
|
|
42
|
+
|
|
43
|
+
**Trade-offs**: When WebID is at root (`/#me`), the root container must be public for discovery. Care must be taken with child ACLs to ensure private resources remain protected.
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test against Community Solid Server (CSS) - typically more permissive
|
|
3
|
+
* Also check solidweb.org configuration
|
|
4
|
+
*/
|
|
5
|
+
const fetch = require('node-fetch');
|
|
6
|
+
const jose = require('jose');
|
|
7
|
+
const crypto = require('crypto');
|
|
8
|
+
|
|
9
|
+
const ISSUER = 'https://melvincarvalho.com/';
|
|
10
|
+
|
|
11
|
+
async function test() {
|
|
12
|
+
console.log('=== Checking OIDC Configuration Details ===\n');
|
|
13
|
+
|
|
14
|
+
// Check solidweb.org OIDC config in detail
|
|
15
|
+
console.log('1. solidweb.org OIDC configuration:');
|
|
16
|
+
const swConfig = await fetch('https://solidweb.org/.well-known/openid-configuration');
|
|
17
|
+
const swData = await swConfig.json();
|
|
18
|
+
console.log(' issuer:', swData.issuer);
|
|
19
|
+
console.log(' subject_types_supported:', swData.subject_types_supported);
|
|
20
|
+
console.log(' id_token_signing_alg_values_supported:', swData.id_token_signing_alg_values_supported);
|
|
21
|
+
console.log(' token_endpoint_auth_methods_supported:', swData.token_endpoint_auth_methods_supported);
|
|
22
|
+
console.log(' claims_supported:', swData.claims_supported);
|
|
23
|
+
|
|
24
|
+
// Check melvincarvalho.com for comparison
|
|
25
|
+
console.log('\n2. melvincarvalho.com OIDC configuration:');
|
|
26
|
+
const mcConfig = await fetch('https://melvincarvalho.com/.well-known/openid-configuration');
|
|
27
|
+
const mcData = await mcConfig.json();
|
|
28
|
+
console.log(' issuer:', mcData.issuer);
|
|
29
|
+
console.log(' id_token_signing_alg_values_supported:', mcData.id_token_signing_alg_values_supported);
|
|
30
|
+
|
|
31
|
+
// Find other known Solid servers to test
|
|
32
|
+
console.log('\n3. Testing other Solid servers...');
|
|
33
|
+
|
|
34
|
+
const otherServers = [
|
|
35
|
+
'https://solidcommunity.net/.well-known/openid-configuration',
|
|
36
|
+
'https://inrupt.net/.well-known/openid-configuration',
|
|
37
|
+
'https://login.inrupt.com/.well-known/openid-configuration',
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
for (const url of otherServers) {
|
|
41
|
+
try {
|
|
42
|
+
const resp = await fetch(url, { timeout: 5000 });
|
|
43
|
+
if (resp.ok) {
|
|
44
|
+
const data = await resp.json();
|
|
45
|
+
console.log(' ' + url.split('/')[2] + ':');
|
|
46
|
+
console.log(' issuer:', data.issuer);
|
|
47
|
+
console.log(' token_types:', data.token_types_supported);
|
|
48
|
+
}
|
|
49
|
+
} catch (err) {
|
|
50
|
+
console.log(' ' + url.split('/')[2] + ': ' + err.message);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Now test our token against solidcommunity.net (another NSS)
|
|
55
|
+
console.log('\n4. Testing DPoP token against solidcommunity.net...');
|
|
56
|
+
|
|
57
|
+
const { publicKey, privateKey } = await jose.generateKeyPair('ES256');
|
|
58
|
+
const publicJwk = await jose.exportJWK(publicKey);
|
|
59
|
+
|
|
60
|
+
const credDpopProof = await new jose.SignJWT({
|
|
61
|
+
htm: 'POST',
|
|
62
|
+
htu: ISSUER + 'idp/credentials',
|
|
63
|
+
iat: Math.floor(Date.now() / 1000),
|
|
64
|
+
jti: crypto.randomUUID(),
|
|
65
|
+
})
|
|
66
|
+
.setProtectedHeader({ alg: 'ES256', typ: 'dpop+jwt', jwk: publicJwk })
|
|
67
|
+
.sign(privateKey);
|
|
68
|
+
|
|
69
|
+
const tokenResp = await fetch(ISSUER + 'idp/credentials', {
|
|
70
|
+
method: 'POST',
|
|
71
|
+
headers: { 'Content-Type': 'application/json', 'DPoP': credDpopProof },
|
|
72
|
+
body: JSON.stringify({ email: 'melvin', password: 'melvintest123' }),
|
|
73
|
+
});
|
|
74
|
+
const { access_token } = await tokenResp.json();
|
|
75
|
+
|
|
76
|
+
// Test on solidcommunity.net
|
|
77
|
+
const testUrl = 'https://solidcommunity.net/';
|
|
78
|
+
const dpopProof = await new jose.SignJWT({
|
|
79
|
+
htm: 'GET',
|
|
80
|
+
htu: testUrl,
|
|
81
|
+
iat: Math.floor(Date.now() / 1000),
|
|
82
|
+
jti: crypto.randomUUID(),
|
|
83
|
+
})
|
|
84
|
+
.setProtectedHeader({ alg: 'ES256', typ: 'dpop+jwt', jwk: publicJwk })
|
|
85
|
+
.sign(privateKey);
|
|
86
|
+
|
|
87
|
+
const testResp = await fetch(testUrl, {
|
|
88
|
+
headers: {
|
|
89
|
+
'Authorization': 'DPoP ' + access_token,
|
|
90
|
+
'DPoP': dpopProof,
|
|
91
|
+
'Accept': 'text/turtle',
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
console.log(' Status:', testResp.status);
|
|
96
|
+
const wwwAuth = testResp.headers.get('www-authenticate');
|
|
97
|
+
if (wwwAuth) {
|
|
98
|
+
console.log(' WWW-Auth:', wwwAuth);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
test().catch(console.error);
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Simulate what NSS does to verify our token
|
|
3
|
+
*/
|
|
4
|
+
const fetch = require('node-fetch');
|
|
5
|
+
const jose = require('jose');
|
|
6
|
+
const crypto = require('crypto');
|
|
7
|
+
|
|
8
|
+
const ISSUER = 'https://melvincarvalho.com/';
|
|
9
|
+
|
|
10
|
+
async function test() {
|
|
11
|
+
console.log('=== Simulating NSS Token Verification ===\n');
|
|
12
|
+
|
|
13
|
+
// Step 1: Get a token
|
|
14
|
+
const { publicKey, privateKey } = await jose.generateKeyPair('ES256');
|
|
15
|
+
const publicJwk = await jose.exportJWK(publicKey);
|
|
16
|
+
|
|
17
|
+
const credProof = await new jose.SignJWT({
|
|
18
|
+
htm: 'POST', htu: ISSUER + 'idp/credentials',
|
|
19
|
+
iat: Math.floor(Date.now() / 1000), jti: crypto.randomUUID(),
|
|
20
|
+
}).setProtectedHeader({ alg: 'ES256', typ: 'dpop+jwt', jwk: publicJwk }).sign(privateKey);
|
|
21
|
+
|
|
22
|
+
const tokenResp = await fetch(ISSUER + 'idp/credentials', {
|
|
23
|
+
method: 'POST',
|
|
24
|
+
headers: { 'Content-Type': 'application/json', 'DPoP': credProof },
|
|
25
|
+
body: JSON.stringify({ email: 'melvin', password: 'melvintest123' }),
|
|
26
|
+
});
|
|
27
|
+
const { access_token } = await tokenResp.json();
|
|
28
|
+
|
|
29
|
+
// Decode token
|
|
30
|
+
const parts = access_token.split('.');
|
|
31
|
+
const header = JSON.parse(Buffer.from(parts[0], 'base64url').toString());
|
|
32
|
+
const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString());
|
|
33
|
+
|
|
34
|
+
console.log('Token issuer:', payload.iss);
|
|
35
|
+
console.log('Token webid:', payload.webid);
|
|
36
|
+
console.log('Token kid:', header.kid);
|
|
37
|
+
|
|
38
|
+
// Step 2: NSS would fetch WebID to discover issuer
|
|
39
|
+
console.log('\n1. Fetching WebID profile...');
|
|
40
|
+
const webidUrl = payload.webid.split('#')[0];
|
|
41
|
+
const webidResp = await fetch(webidUrl, { headers: { Accept: 'text/turtle' } });
|
|
42
|
+
console.log(' Status:', webidResp.status);
|
|
43
|
+
const webidBody = await webidResp.text();
|
|
44
|
+
const issuerMatch = webidBody.match(/oidcIssuer[^<]*<([^>]+)>/);
|
|
45
|
+
console.log(' Found oidcIssuer:', issuerMatch ? issuerMatch[1] : 'NOT FOUND');
|
|
46
|
+
|
|
47
|
+
// Step 3: NSS would fetch OIDC config
|
|
48
|
+
console.log('\n2. Fetching OIDC config...');
|
|
49
|
+
const configResp = await fetch(payload.iss + '.well-known/openid-configuration');
|
|
50
|
+
console.log(' Status:', configResp.status);
|
|
51
|
+
const config = await configResp.json();
|
|
52
|
+
console.log(' jwks_uri:', config.jwks_uri);
|
|
53
|
+
|
|
54
|
+
// Step 4: NSS would fetch JWKS
|
|
55
|
+
console.log('\n3. Fetching JWKS...');
|
|
56
|
+
const jwksResp = await fetch(config.jwks_uri);
|
|
57
|
+
console.log(' Status:', jwksResp.status);
|
|
58
|
+
const jwks = await jwksResp.json();
|
|
59
|
+
console.log(' Keys:', jwks.keys.length);
|
|
60
|
+
|
|
61
|
+
// Step 5: Find matching key
|
|
62
|
+
console.log('\n4. Finding key for kid:', header.kid);
|
|
63
|
+
const key = jwks.keys.find(k => k.kid === header.kid);
|
|
64
|
+
if (key) {
|
|
65
|
+
console.log(' Found key, alg:', key.alg);
|
|
66
|
+
|
|
67
|
+
// Try to verify
|
|
68
|
+
console.log('\n5. Verifying token signature...');
|
|
69
|
+
try {
|
|
70
|
+
const jwksSet = jose.createRemoteJWKSet(new URL(config.jwks_uri));
|
|
71
|
+
const { payload: verified } = await jose.jwtVerify(access_token, jwksSet);
|
|
72
|
+
console.log(' ✓ Signature valid!');
|
|
73
|
+
console.log(' Verified webid:', verified.webid);
|
|
74
|
+
} catch (err) {
|
|
75
|
+
console.log(' ✗ Verification failed:', err.message);
|
|
76
|
+
}
|
|
77
|
+
} else {
|
|
78
|
+
console.log(' ✗ Key not found!');
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
test().catch(console.error);
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
const fetch = require('node-fetch');
|
|
2
|
+
const jose = require('jose');
|
|
3
|
+
const crypto = require('crypto');
|
|
4
|
+
const https = require('https');
|
|
5
|
+
|
|
6
|
+
const ISSUER = 'https://melvincarvalho.com/';
|
|
7
|
+
const NSS_URL = 'https://localhost:8444/';
|
|
8
|
+
|
|
9
|
+
const agent = new https.Agent({ rejectUnauthorized: false });
|
|
10
|
+
|
|
11
|
+
async function test() {
|
|
12
|
+
console.log('=== Testing DPoP against local NSS (port 8444) ===\n');
|
|
13
|
+
|
|
14
|
+
const { publicKey, privateKey } = await jose.generateKeyPair('ES256');
|
|
15
|
+
const publicJwk = await jose.exportJWK(publicKey);
|
|
16
|
+
|
|
17
|
+
const credProof = await new jose.SignJWT({
|
|
18
|
+
htm: 'POST', htu: ISSUER + 'idp/credentials',
|
|
19
|
+
iat: Math.floor(Date.now() / 1000), jti: crypto.randomUUID(),
|
|
20
|
+
}).setProtectedHeader({ alg: 'ES256', typ: 'dpop+jwt', jwk: publicJwk }).sign(privateKey);
|
|
21
|
+
|
|
22
|
+
const tokenResp = await fetch(ISSUER + 'idp/credentials', {
|
|
23
|
+
method: 'POST',
|
|
24
|
+
headers: { 'Content-Type': 'application/json', 'DPoP': credProof },
|
|
25
|
+
body: JSON.stringify({ email: 'melvin', password: 'melvintest123' }),
|
|
26
|
+
});
|
|
27
|
+
const { access_token } = await tokenResp.json();
|
|
28
|
+
console.log('Got token from melvincarvalho.com');
|
|
29
|
+
|
|
30
|
+
const dpopProof = await new jose.SignJWT({
|
|
31
|
+
htm: 'GET', htu: NSS_URL,
|
|
32
|
+
iat: Math.floor(Date.now() / 1000), jti: crypto.randomUUID(),
|
|
33
|
+
}).setProtectedHeader({ alg: 'ES256', typ: 'dpop+jwt', jwk: publicJwk }).sign(privateKey);
|
|
34
|
+
|
|
35
|
+
console.log('Testing against local NSS:', NSS_URL);
|
|
36
|
+
const resp = await fetch(NSS_URL, {
|
|
37
|
+
agent,
|
|
38
|
+
headers: {
|
|
39
|
+
'Authorization': 'DPoP ' + access_token,
|
|
40
|
+
'DPoP': dpopProof,
|
|
41
|
+
'Accept': 'text/turtle',
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
console.log('Status:', resp.status);
|
|
46
|
+
const wwwAuth = resp.headers.get('www-authenticate');
|
|
47
|
+
if (wwwAuth) console.log('WWW-Authenticate:', wwwAuth);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
test().catch(console.error);
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
const fetch = require('node-fetch');
|
|
2
|
+
const jose = require('jose');
|
|
3
|
+
const crypto = require('crypto');
|
|
4
|
+
const https = require('https');
|
|
5
|
+
|
|
6
|
+
const ISSUER = 'https://melvincarvalho.com/';
|
|
7
|
+
const NSS_URL = 'https://localhost:8443/';
|
|
8
|
+
|
|
9
|
+
// Allow self-signed cert
|
|
10
|
+
const agent = new https.Agent({ rejectUnauthorized: false });
|
|
11
|
+
|
|
12
|
+
async function test() {
|
|
13
|
+
console.log('=== Testing DPoP against local NSS ===\n');
|
|
14
|
+
|
|
15
|
+
// Get DPoP token from our IdP
|
|
16
|
+
const { publicKey, privateKey } = await jose.generateKeyPair('ES256');
|
|
17
|
+
const publicJwk = await jose.exportJWK(publicKey);
|
|
18
|
+
|
|
19
|
+
const credProof = await new jose.SignJWT({
|
|
20
|
+
htm: 'POST', htu: ISSUER + 'idp/credentials',
|
|
21
|
+
iat: Math.floor(Date.now() / 1000), jti: crypto.randomUUID(),
|
|
22
|
+
}).setProtectedHeader({ alg: 'ES256', typ: 'dpop+jwt', jwk: publicJwk }).sign(privateKey);
|
|
23
|
+
|
|
24
|
+
const tokenResp = await fetch(ISSUER + 'idp/credentials', {
|
|
25
|
+
method: 'POST',
|
|
26
|
+
headers: { 'Content-Type': 'application/json', 'DPoP': credProof },
|
|
27
|
+
body: JSON.stringify({ email: 'melvin', password: 'melvintest123' }),
|
|
28
|
+
});
|
|
29
|
+
const { access_token } = await tokenResp.json();
|
|
30
|
+
console.log('Got token from melvincarvalho.com');
|
|
31
|
+
|
|
32
|
+
// Create DPoP proof for local NSS
|
|
33
|
+
const dpopProof = await new jose.SignJWT({
|
|
34
|
+
htm: 'GET', htu: NSS_URL,
|
|
35
|
+
iat: Math.floor(Date.now() / 1000), jti: crypto.randomUUID(),
|
|
36
|
+
}).setProtectedHeader({ alg: 'ES256', typ: 'dpop+jwt', jwk: publicJwk }).sign(privateKey);
|
|
37
|
+
|
|
38
|
+
console.log('Testing against local NSS:', NSS_URL);
|
|
39
|
+
const resp = await fetch(NSS_URL, {
|
|
40
|
+
agent,
|
|
41
|
+
headers: {
|
|
42
|
+
'Authorization': 'DPoP ' + access_token,
|
|
43
|
+
'DPoP': dpopProof,
|
|
44
|
+
'Accept': 'text/turtle',
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
console.log('Status:', resp.status);
|
|
49
|
+
const wwwAuth = resp.headers.get('www-authenticate');
|
|
50
|
+
if (wwwAuth) console.log('WWW-Authenticate:', wwwAuth);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
test().catch(console.error);
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
const rdf = require('rdflib');
|
|
2
|
+
|
|
3
|
+
const webId = 'https://melvincarvalho.com/#me';
|
|
4
|
+
|
|
5
|
+
const store = rdf.graph();
|
|
6
|
+
const fetcher = rdf.fetcher(store);
|
|
7
|
+
|
|
8
|
+
console.log('Testing rdflib fetch of WebID:', webId);
|
|
9
|
+
|
|
10
|
+
fetcher.load(webId, { force: true })
|
|
11
|
+
.then(response => {
|
|
12
|
+
console.log('\n=== Response ===');
|
|
13
|
+
console.log('Status:', response.status);
|
|
14
|
+
|
|
15
|
+
const providerTerm = rdf.namedNode('http://www.w3.org/ns/solid/terms#oidcIssuer');
|
|
16
|
+
const webIdNode = rdf.namedNode(webId);
|
|
17
|
+
|
|
18
|
+
console.log('\n=== Query ===');
|
|
19
|
+
console.log('Subject:', webIdNode.value);
|
|
20
|
+
console.log('Predicate:', providerTerm.value);
|
|
21
|
+
|
|
22
|
+
const providerUri = store.anyValue(webIdNode, providerTerm);
|
|
23
|
+
console.log('\n=== Result ===');
|
|
24
|
+
console.log('Provider URI:', providerUri);
|
|
25
|
+
console.log('Provider URI type:', typeof providerUri);
|
|
26
|
+
|
|
27
|
+
// Also check what's in the store
|
|
28
|
+
console.log('\n=== All oidcIssuer statements ===');
|
|
29
|
+
const statements = store.match(null, providerTerm, null);
|
|
30
|
+
statements.forEach(st => {
|
|
31
|
+
console.log(' Subject:', st.subject.value);
|
|
32
|
+
console.log(' Object:', st.object.value);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// Compare with token issuer
|
|
36
|
+
const tokenIssuer = 'https://melvincarvalho.com/';
|
|
37
|
+
console.log('\n=== Comparison ===');
|
|
38
|
+
console.log('Token issuer:', tokenIssuer);
|
|
39
|
+
console.log('Provider from profile:', providerUri);
|
|
40
|
+
console.log('Match:', providerUri === tokenIssuer);
|
|
41
|
+
})
|
|
42
|
+
.catch(err => {
|
|
43
|
+
console.error('Error:', err.message);
|
|
44
|
+
});
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test on solidcommunity.net to confirm DPoP auth works
|
|
3
|
+
*/
|
|
4
|
+
const fetch = require('node-fetch');
|
|
5
|
+
const jose = require('jose');
|
|
6
|
+
const crypto = require('crypto');
|
|
7
|
+
|
|
8
|
+
const ISSUER = 'https://melvincarvalho.com/';
|
|
9
|
+
|
|
10
|
+
async function test() {
|
|
11
|
+
console.log('=== Confirming DPoP works on solidcommunity.net ===\n');
|
|
12
|
+
|
|
13
|
+
const { publicKey, privateKey } = await jose.generateKeyPair('ES256');
|
|
14
|
+
const publicJwk = await jose.exportJWK(publicKey);
|
|
15
|
+
|
|
16
|
+
const credDpopProof = await new jose.SignJWT({
|
|
17
|
+
htm: 'POST',
|
|
18
|
+
htu: ISSUER + 'idp/credentials',
|
|
19
|
+
iat: Math.floor(Date.now() / 1000),
|
|
20
|
+
jti: crypto.randomUUID(),
|
|
21
|
+
})
|
|
22
|
+
.setProtectedHeader({ alg: 'ES256', typ: 'dpop+jwt', jwk: publicJwk })
|
|
23
|
+
.sign(privateKey);
|
|
24
|
+
|
|
25
|
+
const tokenResp = await fetch(ISSUER + 'idp/credentials', {
|
|
26
|
+
method: 'POST',
|
|
27
|
+
headers: { 'Content-Type': 'application/json', 'DPoP': credDpopProof },
|
|
28
|
+
body: JSON.stringify({ email: 'melvin', password: 'melvintest123' }),
|
|
29
|
+
});
|
|
30
|
+
const tokenData = await tokenResp.json();
|
|
31
|
+
const access_token = tokenData.access_token;
|
|
32
|
+
console.log('Got token, webid:', tokenData.webid);
|
|
33
|
+
|
|
34
|
+
// Parse token
|
|
35
|
+
const payload = JSON.parse(Buffer.from(access_token.split('.')[1], 'base64url').toString());
|
|
36
|
+
console.log('Token issuer:', payload.iss);
|
|
37
|
+
console.log('Token webid:', payload.webid);
|
|
38
|
+
console.log('Token cnf.jkt:', payload.cnf.jkt.substring(0, 20) + '...');
|
|
39
|
+
|
|
40
|
+
// Test different endpoints on solidcommunity.net
|
|
41
|
+
const tests = [
|
|
42
|
+
{ url: 'https://solidcommunity.net/', name: 'root (public)' },
|
|
43
|
+
{ url: 'https://melvin.solidcommunity.net/', name: 'user pod root' },
|
|
44
|
+
{ url: 'https://melvin.solidcommunity.net/profile/card', name: 'user profile' },
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
for (const t of tests) {
|
|
48
|
+
console.log('\n--- ' + t.name + ' ---');
|
|
49
|
+
console.log('URL:', t.url);
|
|
50
|
+
|
|
51
|
+
// Without auth
|
|
52
|
+
const noAuthResp = await fetch(t.url, {
|
|
53
|
+
headers: { 'Accept': 'text/turtle' },
|
|
54
|
+
});
|
|
55
|
+
console.log('Without auth:', noAuthResp.status);
|
|
56
|
+
|
|
57
|
+
// With DPoP
|
|
58
|
+
const dpopProof = await new jose.SignJWT({
|
|
59
|
+
htm: 'GET',
|
|
60
|
+
htu: t.url,
|
|
61
|
+
iat: Math.floor(Date.now() / 1000),
|
|
62
|
+
jti: crypto.randomUUID(),
|
|
63
|
+
})
|
|
64
|
+
.setProtectedHeader({ alg: 'ES256', typ: 'dpop+jwt', jwk: publicJwk })
|
|
65
|
+
.sign(privateKey);
|
|
66
|
+
|
|
67
|
+
const authResp = await fetch(t.url, {
|
|
68
|
+
headers: {
|
|
69
|
+
'Authorization': 'DPoP ' + access_token,
|
|
70
|
+
'DPoP': dpopProof,
|
|
71
|
+
'Accept': 'text/turtle',
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
console.log('With DPoP:', authResp.status);
|
|
75
|
+
|
|
76
|
+
const wwwAuth = authResp.headers.get('www-authenticate');
|
|
77
|
+
if (wwwAuth) {
|
|
78
|
+
console.log('WWW-Auth:', wwwAuth);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Now compare the same tests on solidweb.org
|
|
83
|
+
console.log('\n\n=== Comparison: Same tests on solidweb.org ===\n');
|
|
84
|
+
|
|
85
|
+
const swTests = [
|
|
86
|
+
{ url: 'https://solidweb.org/', name: 'root (public)' },
|
|
87
|
+
{ url: 'https://solid-chat.solidweb.org/', name: 'solid-chat pod root' },
|
|
88
|
+
];
|
|
89
|
+
|
|
90
|
+
for (const t of swTests) {
|
|
91
|
+
console.log('\n--- ' + t.name + ' ---');
|
|
92
|
+
console.log('URL:', t.url);
|
|
93
|
+
|
|
94
|
+
// Without auth
|
|
95
|
+
const noAuthResp = await fetch(t.url, {
|
|
96
|
+
headers: { 'Accept': 'text/turtle' },
|
|
97
|
+
});
|
|
98
|
+
console.log('Without auth:', noAuthResp.status);
|
|
99
|
+
|
|
100
|
+
// With DPoP
|
|
101
|
+
const dpopProof = await new jose.SignJWT({
|
|
102
|
+
htm: 'GET',
|
|
103
|
+
htu: t.url,
|
|
104
|
+
iat: Math.floor(Date.now() / 1000),
|
|
105
|
+
jti: crypto.randomUUID(),
|
|
106
|
+
})
|
|
107
|
+
.setProtectedHeader({ alg: 'ES256', typ: 'dpop+jwt', jwk: publicJwk })
|
|
108
|
+
.sign(privateKey);
|
|
109
|
+
|
|
110
|
+
const authResp = await fetch(t.url, {
|
|
111
|
+
headers: {
|
|
112
|
+
'Authorization': 'DPoP ' + access_token,
|
|
113
|
+
'DPoP': dpopProof,
|
|
114
|
+
'Accept': 'text/turtle',
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
console.log('With DPoP:', authResp.status);
|
|
118
|
+
|
|
119
|
+
const wwwAuth = authResp.headers.get('www-authenticate');
|
|
120
|
+
if (wwwAuth) {
|
|
121
|
+
console.log('WWW-Auth:', wwwAuth);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
test().catch(console.error);
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Simulate what NSS does: fetch WebID profile and discover oidcIssuer
|
|
3
|
+
*/
|
|
4
|
+
const fetch = require('node-fetch');
|
|
5
|
+
|
|
6
|
+
const WEBID = 'https://melvincarvalho.com/#me';
|
|
7
|
+
|
|
8
|
+
async function test() {
|
|
9
|
+
console.log('=== WebID Profile Discovery Test ===\n');
|
|
10
|
+
|
|
11
|
+
// Fetch WebID profile (what NSS does)
|
|
12
|
+
console.log('1. Fetching WebID profile:', WEBID);
|
|
13
|
+
|
|
14
|
+
const response = await fetch(WEBID.split('#')[0], {
|
|
15
|
+
headers: {
|
|
16
|
+
'Accept': 'text/turtle, application/ld+json, */*',
|
|
17
|
+
},
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
console.log(' Status:', response.status);
|
|
21
|
+
console.log(' Content-Type:', response.headers.get('content-type'));
|
|
22
|
+
|
|
23
|
+
const body = await response.text();
|
|
24
|
+
console.log(' Content length:', body.length);
|
|
25
|
+
|
|
26
|
+
// Check for solid:oidcIssuer
|
|
27
|
+
console.log('\n2. Looking for solid:oidcIssuer...');
|
|
28
|
+
|
|
29
|
+
// Look for the triple
|
|
30
|
+
const oidcIssuerMatch = body.match(/solid:oidcIssuer\s+<([^>]+)>/);
|
|
31
|
+
if (oidcIssuerMatch) {
|
|
32
|
+
console.log(' Found: solid:oidcIssuer <' + oidcIssuerMatch[1] + '>');
|
|
33
|
+
} else {
|
|
34
|
+
console.log(' Pattern 1 not found, trying other patterns...');
|
|
35
|
+
|
|
36
|
+
// Try full URI pattern
|
|
37
|
+
const fullUriMatch = body.match(/http:\/\/www\.w3\.org\/ns\/solid\/terms#oidcIssuer[^<]+<([^>]+)>/);
|
|
38
|
+
if (fullUriMatch) {
|
|
39
|
+
console.log(' Found via full URI: <' + fullUriMatch[1] + '>');
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Show relevant lines
|
|
44
|
+
console.log('\n3. Relevant lines from profile:');
|
|
45
|
+
const lines = body.split('\n');
|
|
46
|
+
for (const line of lines) {
|
|
47
|
+
if (line.includes('oidcIssuer') || line.includes('solid:') || line.includes('@prefix solid')) {
|
|
48
|
+
console.log(' ' + line.trim());
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Also check CORS headers
|
|
53
|
+
console.log('\n4. CORS headers:');
|
|
54
|
+
console.log(' Access-Control-Allow-Origin:', response.headers.get('access-control-allow-origin') || 'NOT SET');
|
|
55
|
+
console.log(' Access-Control-Allow-Credentials:', response.headers.get('access-control-allow-credentials') || 'NOT SET');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
test().catch(console.error);
|