+21
LICENSE
+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
+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
+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
+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't created a Lanyard profile yet.
34
+
This user hasn'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
+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
+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
+1
-1
src/components/auth/LoginForm.tsx
+97
src/types/README.md
+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
+