me3-protocol 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,98 @@
1
+ # me3 Protocol (me.json)
2
+
3
+ The **me3 Protocol** (`me.json`) is a minimal standard for portable personal websites.
4
+
5
+ It treats your online identity as a **"Digital Business Card"**—a single JSON file that makes you discoverable by both humans and AI agents, without locking you into a specific platform.
6
+
7
+ ## 1. The Specification
8
+
9
+ ### File Location & Discovery
10
+ To be compliant, your `me.json` file must be hosted at one of the following locations (in order of priority):
11
+
12
+ 1. **Primary**: `https://yourdomain.com/me.json`
13
+ 2. **Fallback**: `https://yourdomain.com/.well-known/me`
14
+
15
+ ### Transport & Security
16
+ * **HTTPS Only**: The file must be served over a secure connection.
17
+ * **CORS (Cross-Origin Resource Sharing)**: You **MUST** serve the file with the following header:
18
+ ```http
19
+ Access-Control-Allow-Origin: *
20
+ ```
21
+ **Why?** This ensures that AI agents running in browsers (e.g., Chrome extensions, web-based assistants) can read your identity file even if they are running on a different domain. Without this, your digital business card is invisible to the tools that help people find you.
22
+
23
+ ### Content Type
24
+ The file should be served with `application/json` content type.
25
+
26
+ ## 2. The Schema
27
+
28
+ Your `me.json` defines who you are and where to find you. It is strictly typed to ensure compatibility across all readers.
29
+
30
+ ### Core Structure (`Me3Profile`)
31
+
32
+ | Field | Type | Required | Description |
33
+ | :--- | :--- | :--- | :--- |
34
+ | `version` | `string` | **Yes** | Protocol version (currently "0.1"). |
35
+ | `name` | `string` | **Yes** | Your display name. |
36
+ | `handle` | `string` | No | Your preferred username/handle. |
37
+ | `bio` | `string` | No | Short bio (max 500 chars). |
38
+ | `avatar` | `string` | No | URL to your profile picture. |
39
+ | `banner` | `string` | No | URL to a header/banner image. |
40
+ | `links` | `object` | No | Social links (website, github, twitter, etc.). |
41
+ | `buttons` | `array` | No | Primary actions (e.g., "Book Call", "Subscribe"). |
42
+ | `pages` | `array` | No | Custom content pages. |
43
+
44
+ ### Example
45
+
46
+ ```json
47
+ {
48
+ "version": "0.1",
49
+ "name": "Jane Doe",
50
+ "handle": "janedoe",
51
+ "bio": "Building the open web. Creative Director at Studio X.",
52
+ "avatar": "https://example.com/jane.jpg",
53
+ "links": {
54
+ "website": "https://janedoe.com",
55
+ "twitter": "janedoe",
56
+ "github": "janedoe"
57
+ },
58
+ "buttons": [
59
+ {
60
+ "text": "Hire Me",
61
+ "url": "https://cal.com/janedoe",
62
+ "style": "primary"
63
+ }
64
+ ]
65
+ }
66
+ ```
67
+
68
+ ## 3. What it is NOT
69
+
70
+ * **NOT Authentication**: `me.json` is public data. It does not handle logins, passwords, or private keys.
71
+ * **NOT a Social Network**: There is no "feed", no "likes", and no central server. You own your data.
72
+ * **NOT a Platform**: You can host this file on GitHub Pages, Vercel, WordPress, or your own server.
73
+
74
+ ## 4. Usage
75
+
76
+ ### For Developers
77
+ You can use this package to validate `me.json` files in your applications.
78
+
79
+ ```bash
80
+ npm install me3-protocol
81
+ ```
82
+
83
+ ```typescript
84
+ import { validateProfile, parseMe3Json } from 'me3-protocol'
85
+
86
+ // Validate an object
87
+ const result = validateProfile(myProfileData)
88
+
89
+ // Parse and validate a string
90
+ const result = parseMe3Json(jsonString)
91
+
92
+ if (!result.valid) {
93
+ console.error(result.errors)
94
+ }
95
+ ```
96
+
97
+ ### JSON Schema
98
+ A standard JSON Schema is available in [schema.json](./schema.json) for non-TypeScript implementations.
@@ -0,0 +1,20 @@
1
+ {
2
+ "version": "0.1",
3
+ "name": "Kieran Butler",
4
+ "handle": "kieran",
5
+ "bio": "Intuitive. Coach. Coder. Creative. Building Soulink.",
6
+ "avatar": "https://api.dicebear.com/7.x/avataaars/svg?seed=kieran",
7
+ "banner": "https://images.unsplash.com/photo-1519681393784-d120267933ba?w=1200&h=400&fit=crop",
8
+ "links": {
9
+ "website": "https://kieranbutler.com",
10
+ "github": "kieranbutler",
11
+ "soulink": "kieran"
12
+ },
13
+ "buttons": [
14
+ { "text": "Book a Discovery Call", "url": "https://cal.com/kieran", "style": "primary", "icon": "📅" },
15
+ { "text": "Buy me a coffee", "url": "https://buymeacoffee.com/kieran", "style": "secondary", "icon": "☕" }
16
+ ],
17
+ "pages": [
18
+ { "slug": "about", "title": "About", "file": "about.md", "visible": true }
19
+ ]
20
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "version": "0.1",
3
+ "name": "Jane Doe",
4
+ "handle": "janedoe",
5
+ "bio": "Minimalist example.",
6
+ "links": {
7
+ "twitter": "janedoe"
8
+ }
9
+ }
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "me3-protocol",
3
+ "version": "1.0.0",
4
+ "description": "",
5
+ "main": "index.js",
6
+ "scripts": {
7
+ "build": "tsc",
8
+ "generate-schema": "ts-json-schema-generator --path 'src/index.ts' --type 'Me3Profile' --out 'schema.json' --no-type-check",
9
+ "prepublishOnly": "npm run build && npm run generate-schema"
10
+ },
11
+ "exports": {
12
+ ".": {
13
+ "types": "./dist/index.d.ts",
14
+ "default": "./dist/index.js"
15
+ }
16
+ },
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "git+https://github.com/soulink8/me3.git"
20
+ },
21
+ "keywords": [],
22
+ "author": "",
23
+ "license": "ISC",
24
+ "bugs": {
25
+ "url": "https://github.com/soulink8/me3/issues"
26
+ },
27
+ "homepage": "https://github.com/soulink8/me3#readme",
28
+ "devDependencies": {
29
+ "ts-json-schema-generator": "^2.4.0",
30
+ "typescript": "^5.9.3"
31
+ }
32
+ }
package/schema.json ADDED
@@ -0,0 +1,161 @@
1
+ {
2
+ "$ref": "#/definitions/Me3Profile",
3
+ "$schema": "http://json-schema.org/draft-07/schema#",
4
+ "definitions": {
5
+ "Me3Button": {
6
+ "additionalProperties": false,
7
+ "properties": {
8
+ "icon": {
9
+ "description": "Optional icon (emoji or icon identifier)",
10
+ "type": "string"
11
+ },
12
+ "style": {
13
+ "description": "Button style",
14
+ "enum": [
15
+ "primary",
16
+ "secondary",
17
+ "outline"
18
+ ],
19
+ "type": "string"
20
+ },
21
+ "text": {
22
+ "description": "Button text (max 30 chars)",
23
+ "type": "string"
24
+ },
25
+ "url": {
26
+ "description": "URL to open when clicked",
27
+ "type": "string"
28
+ }
29
+ },
30
+ "required": [
31
+ "text",
32
+ "url"
33
+ ],
34
+ "type": "object"
35
+ },
36
+ "Me3Links": {
37
+ "additionalProperties": {
38
+ "anyOf": [
39
+ {
40
+ "type": "string"
41
+ },
42
+ {
43
+ "not": {}
44
+ }
45
+ ]
46
+ },
47
+ "properties": {
48
+ "email": {
49
+ "type": "string"
50
+ },
51
+ "github": {
52
+ "type": "string"
53
+ },
54
+ "instagram": {
55
+ "type": "string"
56
+ },
57
+ "linkedin": {
58
+ "type": "string"
59
+ },
60
+ "tiktok": {
61
+ "type": "string"
62
+ },
63
+ "twitter": {
64
+ "type": "string"
65
+ },
66
+ "website": {
67
+ "type": "string"
68
+ },
69
+ "youtube": {
70
+ "type": "string"
71
+ }
72
+ },
73
+ "type": "object"
74
+ },
75
+ "Me3Page": {
76
+ "additionalProperties": false,
77
+ "description": "me3 Protocol v0.1\n\nA protocol for portable personal websites. Your site lives in a single me.json file that you can take anywhere.",
78
+ "properties": {
79
+ "file": {
80
+ "description": "Path to markdown file (relative to me.json)",
81
+ "type": "string"
82
+ },
83
+ "slug": {
84
+ "description": "URL-friendly identifier",
85
+ "type": "string"
86
+ },
87
+ "title": {
88
+ "description": "Display name for navigation",
89
+ "type": "string"
90
+ },
91
+ "visible": {
92
+ "description": "Whether to show in navigation",
93
+ "type": "boolean"
94
+ }
95
+ },
96
+ "required": [
97
+ "slug",
98
+ "title",
99
+ "file",
100
+ "visible"
101
+ ],
102
+ "type": "object"
103
+ },
104
+ "Me3Profile": {
105
+ "additionalProperties": false,
106
+ "properties": {
107
+ "avatar": {
108
+ "description": "Avatar URL (absolute or relative)",
109
+ "type": "string"
110
+ },
111
+ "banner": {
112
+ "description": "Banner/header image URL",
113
+ "type": "string"
114
+ },
115
+ "bio": {
116
+ "description": "Short bio",
117
+ "type": "string"
118
+ },
119
+ "buttons": {
120
+ "description": "Call-to-action buttons (max 3)",
121
+ "items": {
122
+ "$ref": "#/definitions/Me3Button"
123
+ },
124
+ "type": "array"
125
+ },
126
+ "handle": {
127
+ "description": "Username/handle",
128
+ "type": "string"
129
+ },
130
+ "links": {
131
+ "$ref": "#/definitions/Me3Links",
132
+ "description": "Social and external links"
133
+ },
134
+ "name": {
135
+ "description": "Display name (required)",
136
+ "type": "string"
137
+ },
138
+ "pages": {
139
+ "description": "Custom pages (markdown)",
140
+ "items": {
141
+ "$ref": "#/definitions/Me3Page"
142
+ },
143
+ "type": "array"
144
+ },
145
+ "theme": {
146
+ "description": "Theme identifier (for future use)",
147
+ "type": "string"
148
+ },
149
+ "version": {
150
+ "description": "Protocol version",
151
+ "type": "string"
152
+ }
153
+ },
154
+ "required": [
155
+ "version",
156
+ "name"
157
+ ],
158
+ "type": "object"
159
+ }
160
+ }
161
+ }
package/src/index.ts ADDED
@@ -0,0 +1,246 @@
1
+ /**
2
+ * me3 Protocol v0.1
3
+ *
4
+ * A protocol for portable personal websites.
5
+ * Your site lives in a single me.json file that you can take anywhere.
6
+ */
7
+
8
+ // ============================================================================
9
+ // Types
10
+ // ============================================================================
11
+
12
+ export interface Me3Page {
13
+ /** URL-friendly identifier */
14
+ slug: string
15
+ /** Display name for navigation */
16
+ title: string
17
+ /** Path to markdown file (relative to me.json) */
18
+ file: string
19
+ /** Whether to show in navigation */
20
+ visible: boolean
21
+ }
22
+
23
+ export interface Me3Links {
24
+ website?: string
25
+ github?: string
26
+ twitter?: string
27
+ linkedin?: string
28
+ instagram?: string
29
+ youtube?: string
30
+ tiktok?: string
31
+ email?: string
32
+ [key: string]: string | undefined
33
+ }
34
+
35
+ export interface Me3Button {
36
+ /** Button text (max 30 chars) */
37
+ text: string
38
+ /** URL to open when clicked */
39
+ url: string
40
+ /** Button style */
41
+ style?: 'primary' | 'secondary' | 'outline'
42
+ /** Optional icon (emoji or icon identifier) */
43
+ icon?: string
44
+ }
45
+
46
+ export interface Me3Profile {
47
+ /** Protocol version */
48
+ version: string
49
+ /** Display name (required) */
50
+ name: string
51
+ /** Username/handle */
52
+ handle?: string
53
+ /** Short bio */
54
+ bio?: string
55
+ /** Avatar URL (absolute or relative) */
56
+ avatar?: string
57
+ /** Banner/header image URL */
58
+ banner?: string
59
+ /** Social and external links */
60
+ links?: Me3Links
61
+ /** Call-to-action buttons (max 3) */
62
+ buttons?: Me3Button[]
63
+ /** Custom pages (markdown) */
64
+ pages?: Me3Page[]
65
+ /** Theme identifier (for future use) */
66
+ theme?: string
67
+ }
68
+
69
+ // ============================================================================
70
+ // Validation
71
+ // ============================================================================
72
+
73
+ export interface ValidationError {
74
+ field: string
75
+ message: string
76
+ }
77
+
78
+ export interface ValidationResult {
79
+ valid: boolean
80
+ errors: ValidationError[]
81
+ profile?: Me3Profile
82
+ }
83
+
84
+ const CURRENT_VERSION = '0.1'
85
+ const MAX_NAME_LENGTH = 100
86
+ const MAX_BIO_LENGTH = 500
87
+ const MAX_HANDLE_LENGTH = 30
88
+ const HANDLE_REGEX = /^[a-z0-9_-]+$/i
89
+ const MAX_BUTTONS = 3
90
+ const MAX_BUTTON_TEXT_LENGTH = 30
91
+ const VALID_BUTTON_STYLES = ['primary', 'secondary', 'outline']
92
+ const URL_REGEX = /^https?:\/\/.+/i
93
+
94
+ /**
95
+ * Validate a me3 profile object
96
+ */
97
+ export function validateProfile(data: unknown): ValidationResult {
98
+ const errors: ValidationError[] = []
99
+
100
+ if (!data || typeof data !== 'object') {
101
+ return {
102
+ valid: false,
103
+ errors: [{ field: 'root', message: 'Profile must be an object' }],
104
+ }
105
+ }
106
+
107
+ const profile = data as Record<string, unknown>
108
+
109
+ // Version (required)
110
+ if (!profile.version || typeof profile.version !== 'string') {
111
+ errors.push({ field: 'version', message: 'Version is required' })
112
+ } else if (profile.version !== CURRENT_VERSION) {
113
+ errors.push({ field: 'version', message: `Unsupported version. Expected ${CURRENT_VERSION}` })
114
+ }
115
+
116
+ // Name (required)
117
+ if (!profile.name || typeof profile.name !== 'string') {
118
+ errors.push({ field: 'name', message: 'Name is required' })
119
+ } else if (profile.name.length > MAX_NAME_LENGTH) {
120
+ errors.push({ field: 'name', message: `Name must be ${MAX_NAME_LENGTH} characters or less` })
121
+ }
122
+
123
+ // Handle (optional)
124
+ if (profile.handle !== undefined) {
125
+ if (typeof profile.handle !== 'string') {
126
+ errors.push({ field: 'handle', message: 'Handle must be a string' })
127
+ } else if (profile.handle.length > MAX_HANDLE_LENGTH) {
128
+ errors.push({ field: 'handle', message: `Handle must be ${MAX_HANDLE_LENGTH} characters or less` })
129
+ } else if (!HANDLE_REGEX.test(profile.handle)) {
130
+ errors.push({ field: 'handle', message: 'Handle can only contain letters, numbers, underscores, and hyphens' })
131
+ }
132
+ }
133
+
134
+ // Bio (optional)
135
+ if (profile.bio !== undefined) {
136
+ if (typeof profile.bio !== 'string') {
137
+ errors.push({ field: 'bio', message: 'Bio must be a string' })
138
+ } else if (profile.bio.length > MAX_BIO_LENGTH) {
139
+ errors.push({ field: 'bio', message: `Bio must be ${MAX_BIO_LENGTH} characters or less` })
140
+ }
141
+ }
142
+
143
+ // Avatar (optional)
144
+ if (profile.avatar !== undefined && typeof profile.avatar !== 'string') {
145
+ errors.push({ field: 'avatar', message: 'Avatar must be a string URL' })
146
+ }
147
+
148
+ // Banner (optional)
149
+ if (profile.banner !== undefined && typeof profile.banner !== 'string') {
150
+ errors.push({ field: 'banner', message: 'Banner must be a string URL' })
151
+ }
152
+
153
+ // Links (optional)
154
+ if (profile.links !== undefined) {
155
+ if (typeof profile.links !== 'object' || profile.links === null) {
156
+ errors.push({ field: 'links', message: 'Links must be an object' })
157
+ }
158
+ }
159
+
160
+ // Buttons (optional)
161
+ if (profile.buttons !== undefined) {
162
+ if (!Array.isArray(profile.buttons)) {
163
+ errors.push({ field: 'buttons', message: 'Buttons must be an array' })
164
+ } else {
165
+ if (profile.buttons.length > MAX_BUTTONS) {
166
+ errors.push({ field: 'buttons', message: `Maximum ${MAX_BUTTONS} buttons allowed` })
167
+ }
168
+ profile.buttons.forEach((button, index) => {
169
+ if (!button || typeof button !== 'object') {
170
+ errors.push({ field: `buttons[${index}]`, message: 'Button must be an object' })
171
+ return
172
+ }
173
+ if (!button.text || typeof button.text !== 'string') {
174
+ errors.push({ field: `buttons[${index}].text`, message: 'Button text is required' })
175
+ } else if (button.text.length > MAX_BUTTON_TEXT_LENGTH) {
176
+ errors.push({ field: `buttons[${index}].text`, message: `Button text must be ${MAX_BUTTON_TEXT_LENGTH} characters or less` })
177
+ }
178
+ if (!button.url || typeof button.url !== 'string') {
179
+ errors.push({ field: `buttons[${index}].url`, message: 'Button URL is required' })
180
+ } else if (!URL_REGEX.test(button.url)) {
181
+ errors.push({ field: `buttons[${index}].url`, message: 'Button URL must be a valid URL starting with http:// or https://' })
182
+ }
183
+ if (button.style !== undefined && !VALID_BUTTON_STYLES.includes(button.style)) {
184
+ errors.push({ field: `buttons[${index}].style`, message: `Button style must be one of: ${VALID_BUTTON_STYLES.join(', ')}` })
185
+ }
186
+ if (button.icon !== undefined && typeof button.icon !== 'string') {
187
+ errors.push({ field: `buttons[${index}].icon`, message: 'Button icon must be a string' })
188
+ }
189
+ })
190
+ }
191
+ }
192
+
193
+ // Pages (optional)
194
+ if (profile.pages !== undefined) {
195
+ if (!Array.isArray(profile.pages)) {
196
+ errors.push({ field: 'pages', message: 'Pages must be an array' })
197
+ } else {
198
+ profile.pages.forEach((page, index) => {
199
+ if (!page || typeof page !== 'object') {
200
+ errors.push({ field: `pages[${index}]`, message: 'Page must be an object' })
201
+ return
202
+ }
203
+ if (!page.slug || typeof page.slug !== 'string') {
204
+ errors.push({ field: `pages[${index}].slug`, message: 'Page slug is required' })
205
+ }
206
+ if (!page.title || typeof page.title !== 'string') {
207
+ errors.push({ field: `pages[${index}].title`, message: 'Page title is required' })
208
+ }
209
+ if (!page.file || typeof page.file !== 'string') {
210
+ errors.push({ field: `pages[${index}].file`, message: 'Page file is required' })
211
+ }
212
+ if (typeof page.visible !== 'boolean') {
213
+ errors.push({ field: `pages[${index}].visible`, message: 'Page visible must be a boolean' })
214
+ }
215
+ })
216
+ }
217
+ }
218
+
219
+ if (errors.length > 0) {
220
+ return { valid: false, errors }
221
+ }
222
+
223
+ return {
224
+ valid: true,
225
+ errors: [],
226
+ profile: profile as unknown as Me3Profile,
227
+ }
228
+ }
229
+
230
+ /**
231
+ * Parse and validate a me.json string
232
+ */
233
+ export function parseMe3Json(jsonString: string): ValidationResult {
234
+ try {
235
+ const data = JSON.parse(jsonString)
236
+ return validateProfile(data)
237
+ } catch (e) {
238
+ return {
239
+ valid: false,
240
+ errors: [{ field: 'root', message: 'Invalid JSON' }],
241
+ }
242
+ }
243
+ }
244
+
245
+ export const ME3_VERSION = CURRENT_VERSION
246
+ export const ME3_FILENAME = 'me.json'
package/tsconfig.json ADDED
@@ -0,0 +1,13 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "module": "CommonJS",
5
+ "strict": true,
6
+ "esModuleInterop": true,
7
+ "skipLibCheck": true,
8
+ "forceConsistentCasingInFileNames": true,
9
+ "outDir": "./dist",
10
+ "declaration": true
11
+ },
12
+ "include": ["src/**/*"]
13
+ }