~ knotless Search/
↑↓ move↵ open⇧↵ all results

chadtmiller.com/pds.js

An atproto PDS in JavaScript, for Cloudflare Workers and Node

0
0
main
902 lines · 24.4 KB Raw History
# Lexicon Resolver Rewrite Implementation Plan

> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.

**Goal:** Replace `@atproto/lexicon` and `@atproto/lex-resolver` with `@bigmoves/lexicon` and custom resolver logic for full AT Protocol spec compliance.

**Architecture:** Custom DNS-over-HTTPS resolver extracts NSID authority, looks up `_atproto.{domain}` TXT record, resolves DID to PDS endpoint, fetches lexicon. Validation uses `@bigmoves/lexicon` zero-dependency validator. Universal runtime support (Node, Cloudflare Workers, Deno).

**Tech Stack:** `@bigmoves/lexicon` (validation), DNS-over-HTTPS (Cloudflare), PLC directory (DID resolution)

---

## Task 1: Create authority.js module

**Files:**
- Create: `packages/lexicon-resolver/src/authority.js`
- Test: `packages/lexicon-resolver/test/authority.test.js`

**Step 1: Write the failing test**

Create `packages/lexicon-resolver/test/authority.test.js`:

```javascript
import { describe, expect, test } from 'vitest';
import { nsidToDomain, domainToAtprotoDns } from '../src/authority.js';

describe('authority', () => {
  describe('nsidToDomain', () => {
    test('converts app.bsky.feed.post to bsky.app', () => {
      expect(nsidToDomain('app.bsky.feed.post')).toBe('bsky.app');
    });

    test('converts com.atproto.repo.createRecord to atproto.com', () => {
      expect(nsidToDomain('com.atproto.repo.createRecord')).toBe('atproto.com');
    });

    test('converts org.example.lexicon.test to example.org', () => {
      expect(nsidToDomain('org.example.lexicon.test')).toBe('example.org');
    });
  });

  describe('domainToAtprotoDns', () => {
    test('prepends _atproto to domain', () => {
      expect(domainToAtprotoDns('bsky.app')).toBe('_atproto.bsky.app');
    });
  });
});
```

**Step 2: Run test to verify it fails**

```bash
cd /Users/chadmiller/code/pds-experiment && pnpm test packages/lexicon-resolver/test/authority.test.js
```

Expected: FAIL with "Cannot find module '../src/authority.js'"

**Step 3: Write minimal implementation**

Create `packages/lexicon-resolver/src/authority.js`:

```javascript
/**
 * Extract authority from NSID and convert to domain.
 * NSID format: tld.domain.name (e.g., app.bsky.feed.post)
 * Authority is everything except the last segment, reversed.
 *
 * @param {string} nsid - e.g., "app.bsky.feed.post"
 * @returns {string} - e.g., "bsky.app"
 */
export function nsidToDomain(nsid) {
  const parts = nsid.split('.');
  // Remove the last segment (the name), take first two for domain
  const authority = parts.slice(0, -1);
  // Reverse: [app, bsky] -> [bsky, app] -> "bsky.app"
  return authority.slice(0, 2).reverse().join('.');
}

/**
 * Convert domain to AT Protocol DNS lookup name.
 *
 * @param {string} domain - e.g., "bsky.app"
 * @returns {string} - e.g., "_atproto.bsky.app"
 */
export function domainToAtprotoDns(domain) {
  return `_atproto.${domain}`;
}
```

**Step 4: Run test to verify it passes**

```bash
cd /Users/chadmiller/code/pds-experiment && pnpm test packages/lexicon-resolver/test/authority.test.js
```

Expected: PASS (3 tests)

**Step 5: Commit**

```bash
git add packages/lexicon-resolver/src/authority.js packages/lexicon-resolver/test/authority.test.js
git commit -m "feat(lexicon-resolver): add authority module for NSID to domain conversion

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>"
```

---

## Task 2: Create dns.js module

**Files:**
- Create: `packages/lexicon-resolver/src/dns.js`
- Test: `packages/lexicon-resolver/test/dns.test.js`

**Step 1: Write the failing test**

Create `packages/lexicon-resolver/test/dns.test.js`:

```javascript
import { describe, expect, test, vi } from 'vitest';
import { lookupTxt, parseDidFromTxt } from '../src/dns.js';

describe('dns', () => {
  describe('parseDidFromTxt', () => {
    test('extracts DID from TXT record value', () => {
      expect(parseDidFromTxt('did=did:plc:xyz123')).toBe('did:plc:xyz123');
    });

    test('handles quoted TXT record', () => {
      expect(parseDidFromTxt('"did=did:plc:xyz123"')).toBe('did:plc:xyz123');
    });

    test('returns null for invalid format', () => {
      expect(parseDidFromTxt('invalid')).toBeNull();
    });

    test('returns null for empty string', () => {
      expect(parseDidFromTxt('')).toBeNull();
    });
  });

  describe('lookupTxt', () => {
    test('fetches TXT records via DoH', async () => {
      const mockFetch = vi.fn().mockResolvedValue({
        ok: true,
        json: () => Promise.resolve({
          Answer: [
            { type: 16, data: '"did=did:plc:z72i7hdynmk6r22z27h6tvur"' },
          ],
        }),
      });

      const records = await lookupTxt('_atproto.bsky.app', { fetch: mockFetch });

      expect(mockFetch).toHaveBeenCalledWith(
        'https://cloudflare-dns.com/dns-query?name=_atproto.bsky.app&type=TXT',
        expect.objectContaining({
          headers: { Accept: 'application/dns-json' },
        }),
      );
      expect(records).toEqual(['did=did:plc:z72i7hdynmk6r22z27h6tvur']);
    });

    test('returns empty array on network error', async () => {
      const mockFetch = vi.fn().mockRejectedValue(new Error('Network error'));
      const records = await lookupTxt('_atproto.example.com', { fetch: mockFetch });
      expect(records).toEqual([]);
    });

    test('returns empty array when no Answer', async () => {
      const mockFetch = vi.fn().mockResolvedValue({
        ok: true,
        json: () => Promise.resolve({}),
      });
      const records = await lookupTxt('_atproto.example.com', { fetch: mockFetch });
      expect(records).toEqual([]);
    });
  });
});
```

**Step 2: Run test to verify it fails**

```bash
cd /Users/chadmiller/code/pds-experiment && pnpm test packages/lexicon-resolver/test/dns.test.js
```

Expected: FAIL with "Cannot find module '../src/dns.js'"

**Step 3: Write minimal implementation**

Create `packages/lexicon-resolver/src/dns.js`:

```javascript
const DEFAULT_DOH_URL = 'https://cloudflare-dns.com/dns-query';

/**
 * Parse DID from AT Protocol TXT record value.
 * Format: "did=did:plc:xyz123" or did=did:plc:xyz123
 *
 * @param {string} txt - TXT record value
 * @returns {string|null} - DID or null if invalid
 */
export function parseDidFromTxt(txt) {
  if (!txt) return null;
  // Remove surrounding quotes if present
  const unquoted = txt.replace(/^"|"$/g, '');
  const match = unquoted.match(/^did=(.+)$/);
  return match ? match[1] : null;
}

/**
 * Lookup TXT records via DNS-over-HTTPS.
 *
 * @param {string} domain - Domain to lookup (e.g., "_atproto.bsky.app")
 * @param {Object} [opts]
 * @param {string} [opts.dohUrl] - DoH endpoint URL
 * @param {typeof fetch} [opts.fetch] - Fetch implementation
 * @returns {Promise<string[]>} - Array of TXT record values
 */
export async function lookupTxt(domain, opts = {}) {
  const dohUrl = opts.dohUrl ?? DEFAULT_DOH_URL;
  const fetchFn = opts.fetch ?? globalThis.fetch;

  try {
    const url = `${dohUrl}?name=${encodeURIComponent(domain)}&type=TXT`;
    const response = await fetchFn(url, {
      headers: { Accept: 'application/dns-json' },
    });

    if (!response.ok) return [];

    const data = await response.json();
    if (!data.Answer) return [];

    // Extract TXT record values (type 16 = TXT)
    return data.Answer
      .filter((record) => record.type === 16)
      .map((record) => {
        // Remove surrounding quotes from data field
        const value = record.data?.replace(/^"|"$/g, '') ?? '';
        return value;
      });
  } catch {
    return [];
  }
}
```

**Step 4: Run test to verify it passes**

```bash
cd /Users/chadmiller/code/pds-experiment && pnpm test packages/lexicon-resolver/test/dns.test.js
```

Expected: PASS (6 tests)

**Step 5: Commit**

```bash
git add packages/lexicon-resolver/src/dns.js packages/lexicon-resolver/test/dns.test.js
git commit -m "feat(lexicon-resolver): add dns module for DoH TXT lookups

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>"
```

---

## Task 3: Create did.js module

**Files:**
- Create: `packages/lexicon-resolver/src/did.js`
- Test: `packages/lexicon-resolver/test/did.test.js`

**Step 1: Write the failing test**

Create `packages/lexicon-resolver/test/did.test.js`:

```javascript
import { describe, expect, test, vi } from 'vitest';
import { resolveDid, extractPdsEndpoint } from '../src/did.js';

describe('did', () => {
  describe('extractPdsEndpoint', () => {
    test('extracts PDS endpoint from DID document', () => {
      const didDoc = {
        service: [
          {
            id: '#atproto_pds',
            type: 'AtprotoPersonalDataServer',
            serviceEndpoint: 'https://pds.example.com',
          },
        ],
      };
      expect(extractPdsEndpoint(didDoc)).toBe('https://pds.example.com');
    });

    test('finds PDS by type if id differs', () => {
      const didDoc = {
        service: [
          {
            id: '#pds',
            type: 'AtprotoPersonalDataServer',
            serviceEndpoint: 'https://pds.example.com',
          },
        ],
      };
      expect(extractPdsEndpoint(didDoc)).toBe('https://pds.example.com');
    });

    test('returns null if no PDS service', () => {
      const didDoc = { service: [] };
      expect(extractPdsEndpoint(didDoc)).toBeNull();
    });

    test('returns null if no service array', () => {
      const didDoc = {};
      expect(extractPdsEndpoint(didDoc)).toBeNull();
    });
  });

  describe('resolveDid', () => {
    test('resolves did:plc via PLC directory', async () => {
      const mockFetch = vi.fn().mockResolvedValue({
        ok: true,
        json: () => Promise.resolve({
          service: [
            {
              id: '#atproto_pds',
              type: 'AtprotoPersonalDataServer',
              serviceEndpoint: 'https://bsky.social',
            },
          ],
        }),
      });

      const endpoint = await resolveDid('did:plc:xyz123', { fetch: mockFetch });

      expect(mockFetch).toHaveBeenCalledWith('https://plc.directory/did:plc:xyz123');
      expect(endpoint).toBe('https://bsky.social');
    });

    test('resolves did:web via .well-known', async () => {
      const mockFetch = vi.fn().mockResolvedValue({
        ok: true,
        json: () => Promise.resolve({
          service: [
            {
              id: '#atproto_pds',
              type: 'AtprotoPersonalDataServer',
              serviceEndpoint: 'https://example.com',
            },
          ],
        }),
      });

      const endpoint = await resolveDid('did:web:example.com', { fetch: mockFetch });

      expect(mockFetch).toHaveBeenCalledWith('https://example.com/.well-known/did.json');
      expect(endpoint).toBe('https://example.com');
    });

    test('returns null for unknown DID method', async () => {
      const endpoint = await resolveDid('did:unknown:xyz', { fetch: vi.fn() });
      expect(endpoint).toBeNull();
    });

    test('returns null on fetch error', async () => {
      const mockFetch = vi.fn().mockRejectedValue(new Error('Network error'));
      const endpoint = await resolveDid('did:plc:xyz', { fetch: mockFetch });
      expect(endpoint).toBeNull();
    });
  });
});
```

**Step 2: Run test to verify it fails**

```bash
cd /Users/chadmiller/code/pds-experiment && pnpm test packages/lexicon-resolver/test/did.test.js
```

Expected: FAIL with "Cannot find module '../src/did.js'"

**Step 3: Write minimal implementation**

Create `packages/lexicon-resolver/src/did.js`:

```javascript
const DEFAULT_PLC_URL = 'https://plc.directory';

/**
 * Extract PDS service endpoint from DID document.
 *
 * @param {Object} didDoc - DID document
 * @returns {string|null} - PDS endpoint URL or null
 */
export function extractPdsEndpoint(didDoc) {
  if (!didDoc?.service || !Array.isArray(didDoc.service)) {
    return null;
  }

  const pdsService = didDoc.service.find(
    (s) => s.id === '#atproto_pds' || s.type === 'AtprotoPersonalDataServer',
  );

  return pdsService?.serviceEndpoint ?? null;
}

/**
 * Resolve a DID to its PDS service endpoint.
 *
 * @param {string} did - DID string (did:plc:... or did:web:...)
 * @param {Object} [opts]
 * @param {string} [opts.plcUrl] - PLC directory URL
 * @param {typeof fetch} [opts.fetch] - Fetch implementation
 * @returns {Promise<string|null>} - PDS endpoint URL or null
 */
export async function resolveDid(did, opts = {}) {
  const plcUrl = opts.plcUrl ?? DEFAULT_PLC_URL;
  const fetchFn = opts.fetch ?? globalThis.fetch;

  try {
    let didDocUrl;

    if (did.startsWith('did:plc:')) {
      didDocUrl = `${plcUrl}/${did}`;
    } else if (did.startsWith('did:web:')) {
      const domain = did.slice('did:web:'.length);
      didDocUrl = `https://${domain}/.well-known/did.json`;
    } else {
      return null;
    }

    const response = await fetchFn(didDocUrl);
    if (!response.ok) return null;

    const didDoc = await response.json();
    return extractPdsEndpoint(didDoc);
  } catch {
    return null;
  }
}
```

**Step 4: Run test to verify it passes**

```bash
cd /Users/chadmiller/code/pds-experiment && pnpm test packages/lexicon-resolver/test/did.test.js
```

Expected: PASS (8 tests)

**Step 5: Commit**

```bash
git add packages/lexicon-resolver/src/did.js packages/lexicon-resolver/test/did.test.js
git commit -m "feat(lexicon-resolver): add did module for DID resolution

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>"
```

---

## Task 4: Update package.json dependencies

**Files:**
- Modify: `packages/lexicon-resolver/package.json`

**Step 1: Update dependencies**

Replace contents of `packages/lexicon-resolver/package.json`:

```json
{
  "name": "@pdsjs/lexicon-resolver",
  "version": "0.1.0",
  "type": "module",
  "main": "src/index.js",
  "exports": {
    ".": "./src/index.js",
    "./resolver": "./src/resolver.js",
    "./dns": "./src/dns.js",
    "./did": "./src/did.js",
    "./authority": "./src/authority.js"
  },
  "dependencies": {
    "@bigmoves/lexicon": "^0.1.2"
  },
  "devDependencies": {
    "vitest": "^2.1.8"
  }
}
```

**Step 2: Install new dependencies**

```bash
cd /Users/chadmiller/code/pds-experiment && pnpm install
```

Expected: Installs `@bigmoves/lexicon`, removes `@atproto/lexicon` and `@atproto/lex-resolver`

**Step 3: Commit**

```bash
git add packages/lexicon-resolver/package.json pnpm-lock.yaml
git commit -m "chore(lexicon-resolver): switch to @bigmoves/lexicon

Remove @atproto/lexicon and @atproto/lex-resolver dependencies.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>"
```

---

## Task 5: Rewrite resolver.js

**Files:**
- Modify: `packages/lexicon-resolver/src/resolver.js`
- Modify: `packages/lexicon-resolver/test/resolver.test.js`

**Step 1: Rewrite resolver.js**

Replace contents of `packages/lexicon-resolver/src/resolver.js`:

```javascript
import { validateRecord } from '@bigmoves/lexicon';
import { lookupTxt, parseDidFromTxt } from './dns.js';
import { resolveDid } from './did.js';
import { nsidToDomain, domainToAtprotoDns } from './authority.js';

/**
 * @typedef {import('@pdsjs/core/ports').ValidationStatus} ValidationStatus
 */

/**
 * Lexicon resolver with full AT Protocol spec compliance.
 * Resolves schemas via DNS authority lookup, DID resolution, and PDS fetch.
 * Validates records using @bigmoves/lexicon.
 *
 * Implements LexiconResolverPort.
 */
export class LexiconResolver {
  /**
   * @param {Object} [opts]
   * @param {number} [opts.ttl=3600000] - Cache TTL in ms (default 1 hour)
   * @param {typeof fetch} [opts.fetch] - Fetch implementation
   * @param {string} [opts.dohUrl] - DNS-over-HTTPS endpoint
   * @param {string} [opts.plcUrl] - PLC directory URL
   * @param {Array<{id: string, defs: Object}>} [opts.schemas] - Pre-loaded static schemas
   */
  constructor(opts = {}) {
    /** @type {Map<string, {id: string, defs: Object}>} */
    this.schemas = new Map();
    /** @type {Map<string, {fetchedAt: number}>} */
    this.cache = new Map();
    this.ttl = opts.ttl ?? 3600000;
    this.fetch = opts.fetch ?? globalThis.fetch;
    this.dohUrl = opts.dohUrl;
    this.plcUrl = opts.plcUrl;

    // Pre-load static schemas
    for (const schema of opts.schemas ?? []) {
      this.schemas.set(schema.id, schema);
      this.cache.set(schema.id, { fetchedAt: Date.now() });
    }
  }

  /**
   * Validate a record against its schema.
   * @param {string} collection - The collection/NSID (e.g., 'app.bsky.feed.post')
   * @param {object} record - The record to validate
   * @returns {Promise<ValidationStatus>}
   */
  async assertValid(collection, record) {
    const hasSchema = await this.ensureSchema(collection);
    if (!hasSchema) {
      return 'unknown';
    }

    const schema = this.schemas.get(collection);
    const error = validateRecord([schema], collection, record);
    if (error) {
      const path = error.path ? `${error.path}: ` : '';
      throw new Error(`${path}${error.message}`);
    }
    return 'valid';
  }

  /**
   * Ensure a schema is loaded, fetching if necessary.
   * @param {string} nsid
   * @returns {Promise<boolean>} - true if schema is available
   */
  async ensureSchema(nsid) {
    // Check if already loaded and not stale
    const cached = this.cache.get(nsid);
    if (cached && Date.now() - cached.fetchedAt < this.ttl) {
      return this.schemas.has(nsid);
    }

    // Already have it, just not checking staleness
    if (this.schemas.has(nsid)) {
      return true;
    }

    // Try to resolve via DNS -> DID -> PDS
    try {
      const lexicon = await this.fetchLexicon(nsid);
      if (lexicon) {
        this.schemas.set(nsid, lexicon);
        this.cache.set(nsid, { fetchedAt: Date.now() });
        return true;
      }
    } catch {
      // Resolution failed
    }

    return false;
  }

  /**
   * Fetch a lexicon via the AT Protocol resolution chain.
   * @param {string} nsid
   * @returns {Promise<{id: string, defs: Object}|null>}
   */
  async fetchLexicon(nsid) {
    // 1. Convert NSID to domain
    const domain = nsidToDomain(nsid);
    const dnsName = domainToAtprotoDns(domain);

    // 2. DNS TXT lookup
    const txtRecords = await lookupTxt(dnsName, {
      dohUrl: this.dohUrl,
      fetch: this.fetch,
    });

    // 3. Parse DID from TXT record
    let did = null;
    for (const txt of txtRecords) {
      did = parseDidFromTxt(txt);
      if (did) break;
    }
    if (!did) return null;

    // 4. Resolve DID to PDS endpoint
    const pdsEndpoint = await resolveDid(did, {
      plcUrl: this.plcUrl,
      fetch: this.fetch,
    });
    if (!pdsEndpoint) return null;

    // 5. Fetch lexicon from PDS
    const url = `${pdsEndpoint}/xrpc/com.atproto.repo.getRecord?repo=${encodeURIComponent(did)}&collection=com.atproto.lexicon.schema&rkey=${encodeURIComponent(nsid)}`;

    const response = await this.fetch(url);
    if (!response.ok) return null;

    const data = await response.json();
    return data.value ?? null;
  }
}
```

**Step 2: Update test file**

Replace contents of `packages/lexicon-resolver/test/resolver.test.js`:

```javascript
import { describe, expect, test, vi } from 'vitest';
import { LexiconResolver } from '../src/resolver.js';

/** @type {import('@bigmoves/lexicon').Lexicon} */
const postSchema = {
  lexicon: 1,
  id: 'app.bsky.feed.post',
  defs: {
    main: {
      type: 'record',
      key: 'tid',
      record: {
        type: 'object',
        required: ['text', 'createdAt'],
        properties: {
          text: { type: 'string', maxLength: 3000 },
          createdAt: { type: 'string', format: 'datetime' },
        },
      },
    },
  },
};

describe('LexiconResolver', () => {
  describe('with static schemas', () => {
    test('validates record against pre-loaded schema', async () => {
      const resolver = new LexiconResolver({
        schemas: [postSchema],
      });

      const status = await resolver.assertValid('app.bsky.feed.post', {
        $type: 'app.bsky.feed.post',
        text: 'Hello world',
        createdAt: new Date().toISOString(),
      });

      expect(status).toBe('valid');
    });

    test('throws on invalid record', async () => {
      const resolver = new LexiconResolver({
        schemas: [postSchema],
      });

      await expect(
        resolver.assertValid('app.bsky.feed.post', {
          $type: 'app.bsky.feed.post',
          // missing required fields
        }),
      ).rejects.toThrow();
    });

    test('returns unknown for unloaded schema', async () => {
      const resolver = new LexiconResolver({
        schemas: [postSchema],
      });

      const status = await resolver.assertValid('com.example.unknown', {
        $type: 'com.example.unknown',
      });

      expect(status).toBe('unknown');
    });
  });

  describe('ensureSchema', () => {
    test('returns true for pre-loaded schema', async () => {
      const resolver = new LexiconResolver({
        schemas: [postSchema],
      });

      const result = await resolver.ensureSchema('app.bsky.feed.post');
      expect(result).toBe(true);
    });

    test('returns false for unknown schema when resolution fails', async () => {
      const mockFetch = vi.fn().mockRejectedValue(new Error('Network error'));
      const resolver = new LexiconResolver({ fetch: mockFetch });

      const result = await resolver.ensureSchema('com.example.unknown');
      expect(result).toBe(false);
    });
  });

  describe('fetchLexicon', () => {
    test('resolves lexicon via DNS -> DID -> PDS chain', async () => {
      const mockFetch = vi
        .fn()
        // DNS lookup
        .mockResolvedValueOnce({
          ok: true,
          json: () =>
            Promise.resolve({
              Answer: [{ type: 16, data: '"did=did:plc:testdid"' }],
            }),
        })
        // DID resolution
        .mockResolvedValueOnce({
          ok: true,
          json: () =>
            Promise.resolve({
              service: [
                {
                  id: '#atproto_pds',
                  type: 'AtprotoPersonalDataServer',
                  serviceEndpoint: 'https://pds.example.com',
                },
              ],
            }),
        })
        // Lexicon fetch
        .mockResolvedValueOnce({
          ok: true,
          json: () => Promise.resolve({ value: postSchema }),
        });

      const resolver = new LexiconResolver({ fetch: mockFetch });
      const lexicon = await resolver.fetchLexicon('app.bsky.feed.post');

      expect(lexicon).toEqual(postSchema);
      expect(mockFetch).toHaveBeenCalledTimes(3);
    });
  });
});
```

**Step 3: Run tests**

```bash
cd /Users/chadmiller/code/pds-experiment && pnpm test packages/lexicon-resolver/
```

Expected: PASS (all tests in lexicon-resolver)

**Step 4: Commit**

```bash
git add packages/lexicon-resolver/src/resolver.js packages/lexicon-resolver/test/resolver.test.js
git commit -m "feat(lexicon-resolver): rewrite resolver with custom DNS/DID resolution

- Use @bigmoves/lexicon for validation
- Custom DNS-over-HTTPS authority resolution
- Custom DID resolution (PLC + did:web)
- Full AT Protocol spec compliance

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>"
```

---

## Task 6: Update index.js exports

**Files:**
- Modify: `packages/lexicon-resolver/src/index.js`

**Step 1: Update exports**

Replace contents of `packages/lexicon-resolver/src/index.js`:

```javascript
export { LexiconResolver } from './resolver.js';
export { lookupTxt, parseDidFromTxt } from './dns.js';
export { resolveDid, extractPdsEndpoint } from './did.js';
export { nsidToDomain, domainToAtprotoDns } from './authority.js';
```

**Step 2: Commit**

```bash
git add packages/lexicon-resolver/src/index.js
git commit -m "feat(lexicon-resolver): export utility functions

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>"
```

---

## Task 7: Run full test suite and typecheck

**Step 1: Run all tests**

```bash
cd /Users/chadmiller/code/pds-experiment && pnpm test
```

Expected: All tests pass

**Step 2: Run typecheck**

```bash
cd /Users/chadmiller/code/pds-experiment && pnpm typecheck
```

Expected: No errors

**Step 3: Run lint**

```bash
cd /Users/chadmiller/code/pds-experiment && pnpm lint
```

Expected: No errors

---

## Summary

After completing all tasks:

1. **`@pdsjs/lexicon-resolver`** now uses:
   - `@bigmoves/lexicon` for validation (zero dependencies)
   - Custom `dns.js` for DNS-over-HTTPS TXT lookups
   - Custom `did.js` for DID resolution (PLC + did:web)
   - Custom `authority.js` for NSID to domain conversion

2. **Removed dependencies:**
   - `@atproto/lexicon`
   - `@atproto/lex-resolver`

3. **Universal runtime support:** Node, Cloudflare Workers, Deno

4. **Usage unchanged:**
   ```javascript
   import { LexiconResolver } from '@pdsjs/lexicon-resolver';

   const resolver = new LexiconResolver({
     schemas: [/* optional static schemas */],
   });

   // Validates via DNS -> DID -> PDS resolution chain
   const status = await resolver.assertValid('app.bsky.feed.post', record);
   ```