-
Notifications
You must be signed in to change notification settings - Fork 7
feat: NIP-05 MVP — /.well-known/nostr.json on single-user provision (#446) #447
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
5050ce2
feat: NIP-05 MVP — write /.well-known/nostr.json on single-user provi…
melvincarvalho 2cfc8c1
review: 405 guards + named-pod NIP-05 + harden multi-user test (#447)
melvincarvalho b000fa5
review: cleanup nits + honest about WAC bypass on /.well-known/* (#447)
melvincarvalho File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,173 @@ | ||
| /** | ||
| * NIP-05 MVP — /.well-known/nostr.json on single-user pods (#446). | ||
| * | ||
| * Implementation is a static file written during pod creation, not | ||
| * a server-side handler. JSS already exposes /.well-known/* as a | ||
| * public namespace (WAC bypass + dotfile allow-list); the LDP GET | ||
| * handler serves the file like any other resource. | ||
| * | ||
| * Acceptance: | ||
| * - single-user + --provision-keys → 200 { names: { _: <hex> } } | ||
| * served at /.well-known/nostr.json, publicly readable, with | ||
| * CORS open for browser-based NIP-05 verifiers. | ||
| * - single-user without --provision-keys → no file written. | ||
| * - multi-user → no file written (next slice of #445 aggregates). | ||
| */ | ||
|
|
||
| import { describe, it, before, after } from 'node:test'; | ||
| import assert from 'node:assert'; | ||
| import fs from 'fs-extra'; | ||
| import { createServer } from '../src/server.js'; | ||
|
|
||
| // Per-describe DATA_DIR suffixes so the three suites in this file | ||
| // don't collide if --test-concurrency is ever raised above 1. | ||
| // Currently 1 per the npm test script, but the latent risk was | ||
| // flagged in the #447 review. | ||
| async function startServer(dataDir, options = {}) { | ||
| await fs.remove(dataDir); | ||
| await fs.ensureDir(dataDir); | ||
| const server = createServer({ | ||
| logger: false, | ||
| forceCloseConnections: true, | ||
| root: dataDir, | ||
| ...options | ||
| }); | ||
| await server.listen({ port: 0, host: '127.0.0.1' }); | ||
| const baseUrl = `http://127.0.0.1:${server.server.address().port}`; | ||
| return { server, baseUrl }; | ||
| } | ||
|
|
||
| async function stopServer(server, dataDir) { | ||
| await server.close(); | ||
| await fs.remove(dataDir); | ||
| } | ||
|
|
||
| describe('NIP-05 MVP — single-user with provisioned key', () => { | ||
| const DATA_DIR = './test-data-nip05-with-key'; | ||
| let server, baseUrl; | ||
| let savedDataRoot; | ||
|
|
||
| before(async () => { | ||
| savedDataRoot = process.env.DATA_ROOT; | ||
| ({ server, baseUrl } = await startServer(DATA_DIR, { | ||
| singleUser: true, | ||
| provisionKeys: true | ||
| })); | ||
| }); | ||
|
|
||
| after(async () => { | ||
| await stopServer(server, DATA_DIR); | ||
| if (savedDataRoot === undefined) delete process.env.DATA_ROOT; | ||
| else process.env.DATA_ROOT = savedDataRoot; | ||
| }); | ||
|
|
||
| it('writes the NIP-05 mapping with the reserved `_` name', async () => { | ||
| const onDisk = JSON.parse( | ||
| await fs.readFile(`${DATA_DIR}/.well-known/nostr.json`, 'utf8') | ||
| ); | ||
| assert.deepStrictEqual(Object.keys(onDisk.names), ['_'], | ||
| 'MVP emits exactly one mapping (the `_` reserved name)'); | ||
| assert.match(onDisk.names._, /^[0-9a-f]{64}$/, | ||
| 'the `_` mapping must be a 32-byte hex pubkey'); | ||
| }); | ||
|
|
||
| it('serves the mapping over HTTP without auth (NIP-05 verifiers are unauth)', async () => { | ||
| const res = await fetch(`${baseUrl}/.well-known/nostr.json`); | ||
| assert.strictEqual(res.status, 200); | ||
| const body = await res.json(); | ||
| assert.match(body.names._, /^[0-9a-f]{64}$/); | ||
| }); | ||
|
|
||
| it('matches the same pubkey landed in the WebID profile VM', async () => { | ||
| // Cross-check: the NIP-05 pubkey and the profile's | ||
| // verificationMethod publicKeyMultibase should describe the | ||
| // same key. If they ever diverge, an LWS-CID verifier would | ||
| // accept the profile's VM but a NIP-05 verifier would attest | ||
| // to a different identity for the same domain. | ||
| const onDisk = JSON.parse( | ||
| await fs.readFile(`${DATA_DIR}/.well-known/nostr.json`, 'utf8') | ||
| ); | ||
| const profile = JSON.parse( | ||
| await fs.readFile(`${DATA_DIR}/profile/card.jsonld`, 'utf8') | ||
| ); | ||
| const vm = profile.verificationMethod[0]; | ||
| assert.ok(vm.publicKeyMultibase.includes(onDisk.names._), | ||
| 'NIP-05 pubkey hex must appear in the profile VM publicKeyMultibase'); | ||
| }); | ||
| }); | ||
|
|
||
| describe('NIP-05 MVP — single-user without a provisioned key', () => { | ||
| const DATA_DIR = './test-data-nip05-no-key'; | ||
| let server; | ||
| let savedDataRoot; | ||
|
|
||
| before(async () => { | ||
| savedDataRoot = process.env.DATA_ROOT; | ||
| ({ server } = await startServer(DATA_DIR, { singleUser: true })); | ||
| }); | ||
|
|
||
| after(async () => { | ||
| await stopServer(server, DATA_DIR); | ||
| if (savedDataRoot === undefined) delete process.env.DATA_ROOT; | ||
| else process.env.DATA_ROOT = savedDataRoot; | ||
| }); | ||
|
|
||
| it('does not create the file (nothing to publish)', async () => { | ||
| assert.strictEqual( | ||
| await fs.pathExists(`${DATA_DIR}/.well-known/nostr.json`), | ||
| false, | ||
| 'no key → no NIP-05 mapping → no file' | ||
| ); | ||
| }); | ||
| }); | ||
|
|
||
| describe('NIP-05 MVP — multi-user mode', () => { | ||
| const DATA_DIR = './test-data-nip05-multiuser'; | ||
| let server, baseUrl; | ||
| let savedDataRoot; | ||
|
|
||
| before(async () => { | ||
| savedDataRoot = process.env.DATA_ROOT; | ||
| // No singleUser → multi-user. The MVP only writes the NIP-05 | ||
| // file in single-user mode; aggregation across multi-user pods | ||
| // is the next slice of #445. | ||
| ({ server, baseUrl } = await startServer(DATA_DIR, {})); | ||
| }); | ||
|
|
||
| after(async () => { | ||
| await stopServer(server, DATA_DIR); | ||
| if (savedDataRoot === undefined) delete process.env.DATA_ROOT; | ||
| else process.env.DATA_ROOT = savedDataRoot; | ||
| }); | ||
|
|
||
| it('still does not write a server-level NIP-05 file when a pod is provisioned with keys', async () => { | ||
| // Actually exercise the code path that *could* have written the | ||
| // file — provision a multi-user pod with provisionKeys: true via | ||
| // POST /.pods. The pod's own /<name>/private/privkey.jsonld | ||
| // lands as expected, but the server-level /.well-known/nostr.json | ||
| // must remain absent because this is multi-user mode. | ||
| const res = await fetch(`${baseUrl}/.pods`, { | ||
| method: 'POST', | ||
| headers: { 'Content-Type': 'application/json' }, | ||
| body: JSON.stringify({ name: 'alice', provisionKeys: true }) | ||
| }); | ||
| assert.strictEqual(res.status, 201, 'pod creation must succeed'); | ||
| const body = await res.json(); | ||
| assert.ok(body.ownerKey, 'pod-level owner key must still be provisioned'); | ||
|
|
||
| // The pod's own privkey IS on disk — sanity check the multi-user | ||
| // provisioning path still ran. | ||
| assert.strictEqual( | ||
| await fs.pathExists(`${DATA_DIR}/alice/private/privkey.jsonld`), | ||
| true, | ||
| 'multi-user pod provisioning still writes the pod-internal privkey' | ||
| ); | ||
|
|
||
| // …but no server-level NIP-05 file. | ||
| assert.strictEqual( | ||
| await fs.pathExists(`${DATA_DIR}/.well-known/nostr.json`), | ||
| false, | ||
| 'multi-user mode must not write a server-level NIP-05 mapping' | ||
| ); | ||
| }); | ||
| }); | ||
|
Comment on lines
+123
to
+173
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Addressed: the multi-user test now actually creates a pod via POST /.pods with provisionKeys: true, asserts the pod-internal privkey DOES land (provisioning ran), and the server-level /.well-known/nostr.json still doesn't. Real multi-user-skip behaviour verified. |
||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Addressed: PR description updated to remove the misleading 'lock it down with .acl' claim. The file is intentionally public per NIP-05 spec and /.well-known/* is WAC-bypassed at the request-handler layer — same bypass that already exempts /.well-known/openid-configuration and /.well-known/did/nostr/*. The new description spells out that operator-written .acls don't apply here and recommends overwriting the file content if a different mapping is wanted. (No code change for this one — the bypass is correct, only the docs were wrong.)