Lanyards is a dedicated profile for researchers, built on the AT Protocol.

Minor changes: Updated the license, Updated some read me documentation, And fixed some typos

Changed files
+233 -105
lexicons
src
app
[handle]
components
DashboardLayout
auth
types
+21
LICENSE
··· 1 + MIT License 2 + 3 + Copyright (c) 2025 Barry Prendergast 4 + 5 + Permission is hereby granted, free of charge, to any person obtaining a copy 6 + of this software and associated documentation files (the "Software"), to deal 7 + in the Software without restriction, including without limitation the rights 8 + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 + copies of the Software, and to permit persons to whom the Software is 10 + furnished to do so, subject to the following conditions: 11 + 12 + The above copyright notice and this permission notice shall be included in all 13 + copies or substantial portions of the Software. 14 + 15 + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 + SOFTWARE.
+3 -3
SETUP.md
··· 1 - # Lanyard Setup Guide 1 + # Lanyards Setup Guide 2 2 3 3 ## Quick Start with App Password (Recommended for Development) 4 4 ··· 6 6 7 7 1. Go to [https://bsky.app/settings/app-passwords](https://bsky.app/settings/app-passwords) 8 8 2. Click "Add App Password" 9 - 3. Give it a name (e.g., "Lanyard Development") 9 + 3. Give it a name (e.g., "Lanyards Development") 10 10 4. Copy the generated password 11 11 12 12 ### 2. Configure Environment Variables ··· 46 46 47 47 ## Authentication Methods 48 48 49 - Lanyard supports two authentication methods: 49 + Lanyards supports two authentication methods: 50 50 51 51 ### App Password (Development) 52 52 - **Pros**: Simple setup, no OAuth configuration needed
+98 -81
lexicons/README.md
··· 1 - # Lanyard Lexicons 1 + # Lanyards Lexicons 2 2 3 3 This directory contains the AT Protocol lexicon definitions for Lanyard. 4 + 5 + > [!NOTE] 6 + > Lanyards uses the AT Protocol Lexicon CLI [@atproto/lex-cli](https://www.npmjs.com/package/@atproto/lex-cli) to automatically generate TypeScript types from the lexicon JSON definitions. 4 7 5 8 ## Namespace Structure 6 9 ··· 8 11 9 12 ``` 10 13 at.lanyard/ 11 - ├── actor/ - Researcher identity and relationships 12 - │ ├── profile - Core researcher profile 13 - │ └── affiliation - Professional affiliations 14 - ├── document/ - Research outputs 15 - │ └── work - Scholarly contributions (DOI-based) 16 - ├── event/ - Academic activities 17 - │ └── academic - Conferences, symposiums, workshops 18 - ├── link/ - External connections 19 - │ ├── social - Social network profiles 20 - │ └── web - Custom web links 21 - ├── location/ - Geographic data (reusable) 22 - │ └── place - ISO country/region/city codes 23 - └── organization/ - Institutions (reusable) 24 - └── institution - Universities, companies (Ringgold/GRID) 14 + ├── researcher - Core researcher profile (singleton record) 15 + ├── work - Scholarly contributions (DOI-based, multiple records) 16 + ├── event - Academic events (conferences, workshops, multiple records) 17 + ├── link - External profiles and links (unified, multiple records) 18 + ├── publication - Publication venues (journals, conferences, embedded object) 19 + ├── organization - Institutions and entities (embedded object) 20 + └── location - Geographic locations (embedded object) 25 21 ``` 26 22 27 - ## Lexicon Hierarchy 23 + ## Lexicon Details 28 24 29 - ### Core Objects (Independent) 25 + ### Records (Top-level collections) 30 26 31 - **`at.lanyard.location.place`** 32 - - Geographic location using ISO codes 33 - - Used by: profile, institution, event 34 - - Fields: country (ISO 3166-1), region (ISO 3166-2), city 27 + Records are stored as collections in the user's repository with `at-uri` identifiers. 35 28 36 - **`at.lanyard.organization.institution`** 37 - - Academic/research institutions 38 - - Used by: affiliation, event (as organizer) 39 - - Identifiers: Ringgold ID, GRID ID 40 - - References: location.place 29 + > [!NOTE] 30 + > The following records are given as example. Referred to the lexicon themselves from more complete and up-to-date documentation. 41 31 42 - ### Actor (Researcher) 32 + **`at.lanyard.researcher`** 33 + - **Type**: Record (singleton, key: `literal:self`) 34 + - **Description**: The researcher's core profile and identity 35 + - **Required Fields**: `did`, `handle`, `createdAt` 36 + - **Optional Fields**: `displayName`, `avatar`, `description` (synced from Bluesky), `honorifics`, `location`, `affiliations`, `updatedAt` 37 + - **Embeds**: `location` (at.lanyard.location), `affiliations[]` (affiliation object) 38 + - **Subdefs**: `affiliation` - professional relationships with organizations 43 39 44 - **`at.lanyard.actor.profile`** 45 - - Central researcher identity 46 - - Key: `self` (singleton record) 47 - - References: location.place 48 - - Synced from: Bluesky profile (avatar, displayName, description) 49 - - Additional: honorifics (Dr, Prof) 40 + **`at.lanyard.work`** 41 + - **Type**: Record (multiple, key: `tid`) 42 + - **Description**: Scholarly contributions identified by DOI 43 + - **Required Fields**: `doi`, `type`, `createdAt` 44 + - **Optional Fields**: `title`, `authors[]`, `publicationDate`, `venue`, `publication` (ref), `event` (at-uri ref) 45 + - **Work Types**: `abstract`, `poster`, `paper`, `conference-proceeding`, `journal-article`, `book-chapter`, `book`, `preprint`, `dataset`, `other` 46 + - **Note**: Metadata auto-fetched from CrossRef/DataCite via DOI 50 47 51 - **`at.lanyard.actor.affiliation`** 52 - - Professional relationships 53 - - Key: `tid` (multiple records) 54 - - References: organization.institution 55 - - Fields: role, startDate, endDate, isPrimary 48 + **`at.lanyard.event`** 49 + - **Type**: Record (multiple, key: `tid`) 50 + - **Description**: Academic events where research is presented or discussed 51 + - **Required Fields**: `name`, `type`, `startDate`, `createdAt` 52 + - **Optional Fields**: `endDate`, `location` (ref), `organizer` (ref), `relatedWorks[]` (at-uri refs), `url` 53 + - **Event Types**: `conference`, `symposium`, `workshop`, `seminar`, `lecture`, `poster-session`, `webinar`, `other` 54 + - **Embeds**: `location` (at.lanyard.location), `organizer` (at.lanyard.organization) 56 55 57 - ### Links (External Connections) 56 + **`at.lanyard.link`** 57 + - **Type**: Record (multiple, key: `tid`) 58 + - **Description**: External profiles and custom web links (unified collection) 59 + - **Required Fields**: `url`, `type`, `createdAt` 60 + - **Optional Fields**: `platform`, `title`, `username`, `isLocked` 61 + - **Link Types**: 62 + - `social` - Twitter, LinkedIn, Bluesky 63 + - `academic` - ORCID, Google Scholar, ResearchGate, Semble 64 + - `web` - Custom web links (max 3 per profile) 65 + - **Platforms**: `bluesky`, `twitter`, `linkedin`, `researchgate`, `googlescholar`, `orcid`, `semble`, `custom` 66 + - **Constraints**: 1 per social/academic platform, max 3 custom web links 58 67 59 - **`at.lanyard.link.social`** 60 - - Social network profiles 61 - - Platforms: bluesky, twitter, linkedin, researchgate, googlescholar, semble 62 - - One per platform (Bluesky is locked/auto-synced) 68 + ### Embedded Objects (Reusable components) 63 69 64 - **`at.lanyard.link.web`** 65 - - Custom web links 66 - - Maximum 3 per profile 67 - - Fields: title, url 70 + Embedded objects are not stored as separate records but are embedded within other records. 68 71 69 - ### Research Output 72 + **`at.lanyard.location`** 73 + - **Type**: Object (embedded) 74 + - **Description**: Geographic location using ISO standard codes 75 + - **Fields**: `country` (ISO 3166-1 alpha-2), `region` (ISO 3166-2), `city`, `isVirtual` 76 + - **Used By**: researcher, organization, event 77 + - **Examples**: `{country: "US", region: "US-CA", city: "San Francisco"}`, `{isVirtual: true}` 70 78 71 - **`at.lanyard.document.work`** 72 - - Scholarly contributions 73 - - Primary identifier: DOI 74 - - Metadata auto-fetched from CrossRef/DataCite 75 - - Types: paper, poster, abstract, dataset, etc. 79 + **`at.lanyard.organization`** 80 + - **Type**: Object (embedded) 81 + - **Description**: Institutions, publishers, societies, funders, companies 82 + - **Required Fields**: `name` 83 + - **Optional Fields**: `type`, `ringgoldId`, `gridId`, `rorId`, `location` (ref), `website`, `logo` (blob) 84 + - **Organization Types**: `institution`, `publisher`, `society`, `funder`, `company`, `government`, `other` 85 + - **Used By**: researcher.affiliation, event.organizer, publication.publisher 86 + - **Identifiers**: Ringgold ID (academic institutions), GRID ID, ROR ID 76 87 77 - ### Events 78 - 79 - **`at.lanyard.event.academic`** 80 - - Conferences, symposiums, workshops 81 - - References: location.place, organization.institution (organizer) 82 - - Can link to: document.work[] (presented works) 88 + **`at.lanyard.publication`** 89 + - **Type**: Object (embedded) 90 + - **Description**: Publication venues (journals, conference proceedings, preprint servers) 91 + - **Required Fields**: `name` 92 + - **Optional Fields**: `type`, `issn`, `publisher` (ref), `website`, `subjects[]` 93 + - **Publication Types**: `journal`, `proceedings`, `preprint`, `repository`, `book-series`, `other` 94 + - **Used By**: work.publication 95 + - **Examples**: Nature, PLOS ONE, arXiv, NeurIPS Proceedings 83 96 84 97 ## Object Relationships 85 98 86 99 ``` 87 - actor.profile 88 - ├─ refs → location.place (home location) 89 - ├─ has → actor.affiliation[] (multiple) 90 - └─ has → link.social[], link.web[] 100 + at.lanyard.researcher (record) 101 + ├─ embeds → location (object) 102 + └─ embeds → affiliations[] (objects) 103 + └─ embed → organization (object) 104 + └─ embed → location (object) 91 105 92 - actor.affiliation 93 - └─ refs → organization.institution 106 + at.lanyard.work (record) 107 + ├─ embeds → publication (object) 108 + │ └─ embed → organization (object) [publisher] 109 + └─ refs → event (at-uri) [optional] 94 110 95 - organization.institution 96 - └─ refs → location.place 111 + at.lanyard.event (record) 112 + ├─ embeds → location (object) 113 + ├─ embeds → organizer (organization object) 114 + └─ refs → relatedWorks[] (at-uri) 97 115 98 - event.academic 99 - ├─ refs → organization.institution (organizer) 100 - ├─ refs → location.place (venue) 101 - └─ refs → document.work[] (presented) 116 + at.lanyard.link (record) 117 + └─ (no references) 102 118 ``` 103 119 104 120 ## Design Principles 105 121 106 - 1. **Reusability** - location and organization are shared objects 107 - 2. **Single Source of Truth** - No duplicated definitions 108 - 3. **Clear Ownership** - actor.* owns links, affiliations 109 - 4. **Modularity** - Each lexicon can evolve independently 110 - 5. **AT Protocol Conventions** - Follows app.bsky.* patterns 122 + 1. **Flat Namespace** - Simple top-level structure, no deep nesting 123 + 2. **Embedded Objects** - Reusable components (location, organization, publication) embedded, not separate records 124 + 3. **DOI-Centric** - Works primarily identified by DOI, metadata auto-fetched 125 + 4. **Unified Links** - Single collection for social, academic, and custom links 126 + 5. **AT Protocol Conventions** - Follows app.bsky.* patterns with records and embedded objects 127 + 6. **Single Source of Truth** - Bluesky profile data is locked and synced 111 128 112 129 ## Future Expansion 113 130 114 131 The structure allows for growth: 115 - - `at.lanyard.actor.education` - Academic degrees 116 - - `at.lanyard.document.grant` - Research funding 117 - - `at.lanyard.document.patent` - Patents 118 - - `at.lanyard.event.workshop` - Specific workshop type 119 - - `at.lanyard.organization.funder` - Funding bodies 132 + - `at.lanyard.education` - Academic degrees and credentials 133 + - `at.lanyard.grant` - Research funding records 134 + - `at.lanyard.patent` - Patent records 135 + - `at.lanyard.dataset` - Research datasets 136 + - `at.lanyard.teaching` - Teaching activities and courses
+10 -17
src/app/[handle]/page.tsx
··· 21 21 // Get Bluesky profile 22 22 const bskyProfile = await agent.getProfile({ actor: did }); 23 23 24 - // Get Lanyard profile 24 + // Get Lanyards profile 25 25 const repo = new ResearcherRepository(agent); 26 26 const lanyardProfile = await repo.getProfile(did); 27 27 ··· 31 31 <div className="text-center"> 32 32 <h1 className="text-2xl font-bold mb-4">Profile Not Found</h1> 33 33 <p className="text-gray-600 mb-6"> 34 - This user hasn&apos;t created a Lanyard profile yet. 34 + This user hasn&apos;t created a Lanyards profile yet. 35 35 </p> 36 - <a 37 - href="/" 38 - className="text-blue-600 hover:underline" 39 - > 36 + <a href="/" className="text-blue-600 hover:underline"> 40 37 Go to homepage 41 38 </a> 42 39 </div> ··· 45 42 } 46 43 47 44 // Get all profile data 48 - const [affiliations, webLinks, works, events] = 49 - await Promise.all([ 50 - repo.listAffiliations(did), 51 - repo.listWebLinks(did), 52 - repo.listWorks(did), 53 - repo.listEvents(did), 54 - ]); 45 + const [affiliations, webLinks, works, events] = await Promise.all([ 46 + repo.listAffiliations(did), 47 + repo.listWebLinks(did), 48 + repo.listWorks(did), 49 + repo.listEvents(did), 50 + ]); 55 51 56 52 return ( 57 53 <ProfileView ··· 76 72 <p className="text-gray-600 mb-6"> 77 73 Unable to load this profile. Please try again later. 78 74 </p> 79 - <a 80 - href="/" 81 - className="text-blue-600 hover:underline" 82 - > 75 + <a href="/" className="text-blue-600 hover:underline"> 83 76 Go to homepage 84 77 </a> 85 78 </div>
+1 -1
src/app/layout.tsx
··· 2 2 import './globals.css'; 3 3 4 4 export const metadata: Metadata = { 5 - title: 'Lanyard - Researcher Profiles on AT Protocol', 5 + title: 'Lanyards - Researcher Profiles on AT Protocol', 6 6 description: 7 7 'A dedicated profile for researchers, built on the AT Protocol. An alternative to ORCID.', 8 8 };
+1 -1
src/components/DashboardLayout/DashboardLayout.tsx
··· 24 24 {/* Header */} 25 25 <header className={styles.header}> 26 26 <div className={styles.headerContent}> 27 - <h1 className={styles.title}>Lanyard Dashboard</h1> 27 + <h1 className={styles.title}>Lanyards Dashboard</h1> 28 28 <button onClick={handleLogoutClick} className={styles.logoutButton}> 29 29 Logout 30 30 </button>
+1 -1
src/components/auth/LoginForm.tsx
··· 107 107 </button> 108 108 109 109 <p className="text-xs text-gray-500 text-center"> 110 - Sign in to access your Lanyard profile 110 + Sign in to access your Lanyards profile 111 111 </p> 112 112 </form> 113 113 );
+97
src/types/README.md
··· 1 + ## Lanyards Types Generation 2 + 3 + Lanyards uses the AT Protocol Lexicon CLI [@atproto/lex-cli](https://www.npmjs.com/package/@atproto/lex-cli) to automatically generate TypeScript types from the lexicon JSON definitions. 4 + 5 + ### Generated Files 6 + 7 + When you run the code generation, the following files are created in `src/types/generated/`: 8 + 9 + ``` 10 + src/types/generated/ 11 + ├── index.ts - Main export file with all types 12 + ├── lexicons.ts - Lexicon definitions for runtime validation 13 + ├── util.ts - Utility types and helpers 14 + └── types/at/lanyard/ - Generated type definitions 15 + ├── researcher.ts - Researcher record types 16 + ├── work.ts - Work record types 17 + ├── event.ts - Event record types 18 + ├── link.ts - Link record types 19 + ├── publication.ts - Publication object types 20 + ├── organization.ts - Organization object types 21 + └── location.ts - Location object types 22 + ``` 23 + 24 + > [!NOTE] 25 + > The types are **NOT** committed to git in most workflows, but are generated as part of the build process. 26 + 27 + ### Commands 28 + 29 + **Generate types once:** 30 + ```bash 31 + npm run lex:gen 32 + ``` 33 + This command reads all `.json` files in the `lexicons/` directory and generates TypeScript types in `src/types/generated/`. 34 + 35 + **Watch mode (development):** 36 + ```bash 37 + npm run lex:watch 38 + ``` 39 + Watches for changes to lexicon files and automatically regenerates types. 40 + 41 + **Build process:** 42 + ```bash 43 + npm run build 44 + ``` 45 + The build command automatically runs `lex:gen` before building the Next.js app to ensure types are up-to-date. 46 + 47 + ### How It Works 48 + 49 + 1. **Lexicon Definitions**: JSON files in `lexicons/` define the schema using AT Protocol lexicon syntax 50 + 2. **CLI Tool**: `@atproto/lex-cli` parses the JSON definitions 51 + 3. **Type Generation**: Creates TypeScript interfaces, types, and validation schemas 52 + 4. **Import & Use**: Generated types are imported throughout the codebase via `@/types/generated` 53 + 54 + ### Generated Type Structure 55 + 56 + Each generated file includes: 57 + - **Record types**: For top-level collection records (researcher, work, event, link) 58 + - **Object types**: For embedded objects (location, organization, publication) 59 + - **Input/Output schemas**: For API operations (create, update, delete) 60 + - **Validation functions**: Runtime validation using the lexicon definitions 61 + 62 + ### Example Usage 63 + 64 + ```typescript 65 + import { 66 + Researcher, 67 + Work, 68 + Event, 69 + Link 70 + } from '@/types/generated'; 71 + 72 + // Use generated types in your code 73 + const researcher: Researcher.Record = { 74 + did: 'did:plc:...', 75 + handle: 'researcher.bsky.social', 76 + createdAt: new Date().toISOString(), 77 + // ... other fields 78 + }; 79 + ``` 80 + 81 + ### Type Declaration Fixes 82 + 83 + Due to import path resolution in generated types, we maintain custom type declarations in `src/types/`: 84 + - **`atproto.d.ts`**: Namespace declarations for AT Protocol repo operations 85 + - **`multiformats.d.ts`**: Module declaration for CID imports 86 + 87 + These ensure the generated types work correctly with the AT Protocol SDK. 88 + 89 + ### Regenerating Types 90 + 91 + You should regenerate types whenever you: 92 + 1. **Modify lexicon files**: Add/remove fields, change types, update descriptions 93 + 2. **Add new lexicons**: Create new `.json` files in `lexicons/` 94 + 3. **Change constraints**: Update validation rules (maxLength, enum values, etc.) 95 + 4. **Pull changes**: After pulling changes that include lexicon updates 96 + 97 +
+1 -1
src/types/index.ts
··· 1 1 /** 2 - * Type exports for Lanyard application 2 + * Type exports for Lanyards application 3 3 * Re-exports generated AT Protocol lexicon types with convenient aliases 4 4 */ 5 5