chore: prepare relay-auth-manager for npm publishing

- Point exports/main/types to dist/ for correct npm resolution
- Add tsconfig.build.json for emitting ESM JS + declarations
- Add build/clean/prepublishOnly scripts
- Add LICENSE, description, keywords, author, files field
- Add Vite resolve alias + tsconfig paths for workspace dev
  (resolves source directly, no pre-build needed for dev)
- Fix TypeScript strict errors in test file
- Clean npm pack output: dist/, README.md, LICENSE only

https://claude.ai/code/session_01XqrjeQVtJKw9uC1XAw6rqd
This commit is contained in:
Claude
2026-02-19 22:40:35 +00:00
parent a8b90d9204
commit 245a7a2c85
7 changed files with 112 additions and 42 deletions

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 Alejandro Gómez
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -12,8 +12,8 @@ This is a workspace package. It has a single peer dependency on `rxjs >= 7`.
import { RelayAuthManager } from "relay-auth-manager";
const manager = new RelayAuthManager({
pool, // relay pool (applesauce-relay compatible)
signer$, // Observable<AuthSigner | null>
pool, // relay pool (applesauce-relay compatible)
signer$, // Observable<AuthSigner | null>
storage: localStorage, // optional persistence
});
@@ -40,15 +40,15 @@ manager.setPreference("wss://relay.example.com", "always");
new RelayAuthManager(options: RelayAuthManagerOptions)
```
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `pool` | `AuthRelayPool` | *required* | Relay pool to monitor. Relays are auto-monitored on `add$` and cleaned up on `remove$`. |
| `signer$` | `Observable<AuthSigner \| null>` | *required* | Current signer. Emit `null` when logged out or read-only. |
| `storage` | `AuthPreferenceStorage` | `undefined` | Persistent storage for preferences. Anything with `getItem`/`setItem` works. |
| `storageKey` | `string` | `"relay-auth-preferences"` | Key used in storage. |
| `challengeTTL` | `number` | `300000` (5 min) | How long a challenge stays pending before being filtered out. |
| `initialRelays` | `Iterable<AuthRelay>` | `[]` | Relays already in the pool at creation time. |
| `normalizeUrl` | `(url: string) => string` | adds `wss://`, strips trailing `/` | Custom URL normalizer. Applied to all URLs used as map keys (preferences, state lookups). Provide this if your app uses a different normalization (e.g., lowercase hostname, trailing slash). |
| Option | Type | Default | Description |
| --------------- | -------------------------------- | ---------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `pool` | `AuthRelayPool` | _required_ | Relay pool to monitor. Relays are auto-monitored on `add$` and cleaned up on `remove$`. |
| `signer$` | `Observable<AuthSigner \| null>` | _required_ | Current signer. Emit `null` when logged out or read-only. |
| `storage` | `AuthPreferenceStorage` | `undefined` | Persistent storage for preferences. Anything with `getItem`/`setItem` works. |
| `storageKey` | `string` | `"relay-auth-preferences"` | Key used in storage. |
| `challengeTTL` | `number` | `300000` (5 min) | How long a challenge stays pending before being filtered out. |
| `initialRelays` | `Iterable<AuthRelay>` | `[]` | Relays already in the pool at creation time. |
| `normalizeUrl` | `(url: string) => string` | adds `wss://`, strips trailing `/` | Custom URL normalizer. Applied to all URLs used as map keys (preferences, state lookups). Provide this if your app uses a different normalization (e.g., lowercase hostname, trailing slash). |
## Observables
@@ -67,6 +67,7 @@ manager.states$.subscribe((states) => {
### `pendingChallenges$: BehaviorSubject<PendingAuthChallenge[]>`
Challenges that need user interaction. Already filtered — only includes relays where:
- Status is `"challenge_received"`
- A signer is available
- Challenge hasn't expired
@@ -77,40 +78,40 @@ Challenges that need user interaction. Already filtered — only includes relays
### Authentication
| Method | Description |
|--------|-------------|
| `authenticate(relayUrl)` | Accept a pending challenge. Signs and sends AUTH. Returns a Promise that resolves when `authenticated$` confirms. Rejects if relay disconnects, auth fails, or preconditions aren't met (no challenge, no signer, relay not monitored). |
| `retry(relayUrl)` | Retry authentication for a relay in `"failed"` state. Re-reads the challenge from the relay. Same promise semantics as `authenticate()`. |
| `reject(relayUrl, rememberForSession?)` | Reject a challenge. If `rememberForSession` is `true` (default), suppresses future prompts for this relay until page reload. |
| Method | Description |
| --------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `authenticate(relayUrl)` | Accept a pending challenge. Signs and sends AUTH. Returns a Promise that resolves when `authenticated$` confirms. Rejects if relay disconnects, auth fails, or preconditions aren't met (no challenge, no signer, relay not monitored). |
| `retry(relayUrl)` | Retry authentication for a relay in `"failed"` state. Re-reads the challenge from the relay. Same promise semantics as `authenticate()`. |
| `reject(relayUrl, rememberForSession?)` | Reject a challenge. If `rememberForSession` is `true` (default), suppresses future prompts for this relay until page reload. |
### Preferences
| Method | Description |
|--------|-------------|
| Method | Description |
| -------------------------- | ------------------------------------------------------------------------------------------------------------------- |
| `setPreference(url, pref)` | Set `"always"`, `"never"`, or `"ask"` for a relay. Persisted to storage. URL is normalized for consistent matching. |
| `getPreference(url)` | Get preference for a relay, or `undefined`. |
| `removePreference(url)` | Remove a preference. Returns `true` if one existed. Persisted to storage. |
| `getAllPreferences()` | `ReadonlyMap<string, AuthPreference>` of all preferences. |
| `getPreference(url)` | Get preference for a relay, or `undefined`. |
| `removePreference(url)` | Remove a preference. Returns `true` if one existed. Persisted to storage. |
| `getAllPreferences()` | `ReadonlyMap<string, AuthPreference>` of all preferences. |
### Relay Monitoring
| Method | Description |
|--------|-------------|
| Method | Description |
| --------------------- | ------------------------------------------------------------------------------------------ |
| `monitorRelay(relay)` | Start monitoring a relay for challenges. Idempotent. Called automatically for pool relays. |
| `unmonitorRelay(url)` | Stop monitoring. Called automatically on pool `remove$`. |
| `unmonitorRelay(url)` | Stop monitoring. Called automatically on pool `remove$`. |
### State Queries
| Method | Description |
|--------|-------------|
| `getRelayState(url)` | Get `RelayAuthState` snapshot for a single relay. Returns a copy, not a live reference. |
| `getAllStates()` | Snapshot of all states. Same as `states$.value`. |
| `hasSignerAvailable()` | Whether a signer is currently available. |
| Method | Description |
| ---------------------- | --------------------------------------------------------------------------------------- |
| `getRelayState(url)` | Get `RelayAuthState` snapshot for a single relay. Returns a copy, not a live reference. |
| `getAllStates()` | Snapshot of all states. Same as `states$.value`. |
| `hasSignerAvailable()` | Whether a signer is currently available. |
### Lifecycle
| Method | Description |
|--------|-------------|
| Method | Description |
| ----------- | -------------------------------------------------------------------------- |
| `destroy()` | Unsubscribe everything, complete observables. Safe to call multiple times. |
## Auth Lifecycle
@@ -131,11 +132,11 @@ Disconnect from any state resets to `none`. Failed relays can be retried via `re
Preferences control what happens when a challenge arrives:
| Preference | Behavior |
|------------|----------|
| `"always"` | Auto-authenticate (no user prompt). Waits for signer if unavailable. |
| `"never"` | Auto-reject (no user prompt). |
| `"ask"` | Show in `pendingChallenges$` for user to decide. This is the default. |
| Preference | Behavior |
| ---------- | --------------------------------------------------------------------- |
| `"always"` | Auto-authenticate (no user prompt). Waits for signer if unavailable. |
| `"never"` | Auto-reject (no user prompt). |
| `"ask"` | Show in `pendingChallenges$` for user to decide. This is the default. |
## Storage

View File

@@ -1,11 +1,34 @@
{
"name": "relay-auth-manager",
"version": "0.1.0",
"description": "Generic NIP-42 relay authentication manager for Nostr clients. Framework and storage agnostic.",
"type": "module",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "MIT",
"author": "Alejandro Gómez",
"keywords": [
"nostr",
"nip-42",
"relay",
"authentication",
"auth"
],
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": "./src/index.ts"
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
}
},
"files": [
"dist",
"README.md",
"LICENSE"
],
"scripts": {
"build": "tsc --project tsconfig.build.json",
"clean": "rm -rf dist",
"prepublishOnly": "npm run clean && npm run build"
},
"peerDependencies": {
"rxjs": "^7.0.0"

View File

@@ -486,7 +486,8 @@ describe("RelayAuthManager", () => {
// Set challenge directly on relay without triggering observable state transition
// (In production, relay.challenge is a getter synced with challenge$)
(relay as Record<string, unknown>).challenge = "retry-challenge";
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(relay as any).challenge = "retry-challenge";
// Retry succeeds (default mock behavior restores after mockRejectedValueOnce)
await manager.retry("wss://relay.example.com");
@@ -545,7 +546,8 @@ describe("RelayAuthManager", () => {
// Remove signer and set challenge directly (without triggering state transition)
signer$.next(null);
(relay as Record<string, unknown>).challenge = "retry-challenge";
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(relay as any).challenge = "retry-challenge";
await expect(manager.retry("wss://relay.example.com")).rejects.toThrow(
"No signer available",

View File

@@ -0,0 +1,17 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "Node16",
"moduleResolution": "Node16",
"strict": true,
"isolatedModules": true,
"skipLibCheck": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"outDir": "dist",
"rootDir": "src"
},
"include": ["src"],
"exclude": ["src/__tests__"]
}

View File

@@ -24,7 +24,8 @@
/* Path aliases */
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
"@/*": ["./src/*"],
"relay-auth-manager": ["./packages/relay-auth-manager/src/index.ts"]
}
},
"include": ["src"]

View File

@@ -12,6 +12,11 @@ export default defineConfig({
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),
// Resolve workspace package source directly (bypasses dist/)
"relay-auth-manager": path.resolve(
__dirname,
"./packages/relay-auth-manager/src/index.ts",
),
},
},
server: {