me3-protocol 1.0.0 → 2.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 +48 -26
- package/dist/index.d.ts +78 -0
- package/dist/index.js +236 -0
- package/examples/full.json +13 -2
- package/examples/simple.json +1 -0
- package/package.json +11 -2
- package/schema.json +4 -4
- package/src/index.ts +0 -246
- package/tsconfig.json +0 -13
package/README.md
CHANGED
|
@@ -4,23 +4,30 @@ The **me3 Protocol** (`me.json`) is a minimal standard for portable personal web
|
|
|
4
4
|
|
|
5
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
6
|
|
|
7
|
+
## Why it exists (brief)
|
|
8
|
+
|
|
9
|
+
People increasingly ask AI to *find* a person (to hire, collaborate with, or learn from). Scraping arbitrary personal sites is a failure mode for agents: it’s brittle, slow, and ambiguous. `me.json` is a small, predictable identity endpoint that makes discovery and summarization reliable.
|
|
10
|
+
|
|
7
11
|
## 1. The Specification
|
|
8
12
|
|
|
9
13
|
### File Location & Discovery
|
|
14
|
+
|
|
10
15
|
To be compliant, your `me.json` file must be hosted at one of the following locations (in order of priority):
|
|
11
16
|
|
|
12
17
|
1. **Primary**: `https://yourdomain.com/me.json`
|
|
13
18
|
2. **Fallback**: `https://yourdomain.com/.well-known/me`
|
|
14
19
|
|
|
15
20
|
### Transport & Security
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
21
|
+
|
|
22
|
+
- **HTTPS Only**: The file must be served over a secure connection.
|
|
23
|
+
- **CORS (Cross-Origin Resource Sharing)**: You **MUST** serve the file with the following header:
|
|
24
|
+
```http
|
|
25
|
+
Access-Control-Allow-Origin: *
|
|
26
|
+
```
|
|
27
|
+
**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
28
|
|
|
23
29
|
### Content Type
|
|
30
|
+
|
|
24
31
|
The file should be served with `application/json` content type.
|
|
25
32
|
|
|
26
33
|
## 2. The Schema
|
|
@@ -29,19 +36,22 @@ Your `me.json` defines who you are and where to find you. It is strictly typed t
|
|
|
29
36
|
|
|
30
37
|
### Core Structure (`Me3Profile`)
|
|
31
38
|
|
|
32
|
-
| Field
|
|
33
|
-
|
|
|
34
|
-
| `version` | `string` | **Yes**
|
|
35
|
-
| `name`
|
|
36
|
-
| `handle`
|
|
37
|
-
| `bio`
|
|
38
|
-
| `avatar`
|
|
39
|
-
| `banner`
|
|
40
|
-
| `
|
|
41
|
-
| `
|
|
42
|
-
| `
|
|
39
|
+
| Field | Type | Required | Description |
|
|
40
|
+
| :-------- | :------- | :------- | :------------------------------------------------ |
|
|
41
|
+
| `version` | `string` | **Yes** | Protocol version (currently "0.1"). |
|
|
42
|
+
| `name` | `string` | **Yes** | Your display name. |
|
|
43
|
+
| `handle` | `string` | No | Your preferred username/handle. |
|
|
44
|
+
| `bio` | `string` | No | Short bio (max 500 chars). |
|
|
45
|
+
| `avatar` | `string` | No | URL to your profile picture. |
|
|
46
|
+
| `banner` | `string` | No | URL to a header/banner image. |
|
|
47
|
+
| `location`| `string` | No | Freeform location string (e.g. "Remote"). |
|
|
48
|
+
| `links` | `object` | No | Social links (website, github, twitter, etc.). |
|
|
49
|
+
| `buttons` | `array` | No | Primary actions (e.g., "Book Call", "Subscribe"). |
|
|
50
|
+
| `pages` | `array` | No | Custom content pages. |
|
|
51
|
+
|
|
52
|
+
### Examples
|
|
43
53
|
|
|
44
|
-
|
|
54
|
+
Minimal:
|
|
45
55
|
|
|
46
56
|
```json
|
|
47
57
|
{
|
|
@@ -49,6 +59,7 @@ Your `me.json` defines who you are and where to find you. It is strictly typed t
|
|
|
49
59
|
"name": "Jane Doe",
|
|
50
60
|
"handle": "janedoe",
|
|
51
61
|
"bio": "Building the open web. Creative Director at Studio X.",
|
|
62
|
+
"location": "Berlin, Germany",
|
|
52
63
|
"avatar": "https://example.com/jane.jpg",
|
|
53
64
|
"links": {
|
|
54
65
|
"website": "https://janedoe.com",
|
|
@@ -65,15 +76,25 @@ Your `me.json` defines who you are and where to find you. It is strictly typed t
|
|
|
65
76
|
}
|
|
66
77
|
```
|
|
67
78
|
|
|
79
|
+
More complete examples live in [`examples/`](./examples/), including [`examples/simple.json`](./examples/simple.json) and [`examples/full.json`](./examples/full.json).
|
|
80
|
+
|
|
68
81
|
## 3. What it is NOT
|
|
69
82
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
83
|
+
- **NOT Authentication**: `me.json` is public data. It does not handle logins, passwords, or private keys.
|
|
84
|
+
- **NOT a Social Network**: There is no "feed", no "likes", and no central server. You own your data.
|
|
85
|
+
- **NOT a Platform**: You can host this file on GitHub Pages, Vercel, WordPress, or your own server.
|
|
86
|
+
- **NOT Reputation / Ranking**: No scoring, ranking, endorsements, verification, or algorithmic ordering.
|
|
87
|
+
|
|
88
|
+
## 4. Guardrails & versioning
|
|
89
|
+
|
|
90
|
+
- **Current version**: `0.1` (see `version` field). Validators in this repo currently require `version === "0.1"`.
|
|
91
|
+
- **Backwards compatibility**: `0.1` is intended to stay stable. Changes should be additive and conservative.
|
|
92
|
+
- **Extensions**: top-level fields are intentionally strict. If you need custom keys, prefer placing them under `links` (e.g. `"links": { "mastodon": "...", "custom": "..." }`) until the protocol defines a first-class place for extensions.
|
|
73
93
|
|
|
74
|
-
##
|
|
94
|
+
## 5. Usage
|
|
75
95
|
|
|
76
96
|
### For Developers
|
|
97
|
+
|
|
77
98
|
You can use this package to validate `me.json` files in your applications.
|
|
78
99
|
|
|
79
100
|
```bash
|
|
@@ -81,18 +102,19 @@ npm install me3-protocol
|
|
|
81
102
|
```
|
|
82
103
|
|
|
83
104
|
```typescript
|
|
84
|
-
import { validateProfile, parseMe3Json } from
|
|
105
|
+
import { validateProfile, parseMe3Json } from "me3-protocol";
|
|
85
106
|
|
|
86
107
|
// Validate an object
|
|
87
|
-
const result = validateProfile(myProfileData)
|
|
108
|
+
const result = validateProfile(myProfileData);
|
|
88
109
|
|
|
89
110
|
// Parse and validate a string
|
|
90
|
-
const result = parseMe3Json(jsonString)
|
|
111
|
+
const result = parseMe3Json(jsonString);
|
|
91
112
|
|
|
92
113
|
if (!result.valid) {
|
|
93
|
-
console.error(result.errors)
|
|
114
|
+
console.error(result.errors);
|
|
94
115
|
}
|
|
95
116
|
```
|
|
96
117
|
|
|
97
118
|
### JSON Schema
|
|
119
|
+
|
|
98
120
|
A standard JSON Schema is available in [schema.json](./schema.json) for non-TypeScript implementations.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
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
|
+
export interface Me3Page {
|
|
8
|
+
/** URL-friendly identifier */
|
|
9
|
+
slug: string;
|
|
10
|
+
/** Display name for navigation */
|
|
11
|
+
title: string;
|
|
12
|
+
/** Path to markdown file (relative to me.json) */
|
|
13
|
+
file: string;
|
|
14
|
+
/** Whether to show in navigation */
|
|
15
|
+
visible: boolean;
|
|
16
|
+
}
|
|
17
|
+
export interface Me3Links {
|
|
18
|
+
website?: string;
|
|
19
|
+
github?: string;
|
|
20
|
+
twitter?: string;
|
|
21
|
+
linkedin?: string;
|
|
22
|
+
instagram?: string;
|
|
23
|
+
youtube?: string;
|
|
24
|
+
tiktok?: string;
|
|
25
|
+
email?: string;
|
|
26
|
+
[key: string]: string | undefined;
|
|
27
|
+
}
|
|
28
|
+
export interface Me3Button {
|
|
29
|
+
/** Button text (max 30 chars) */
|
|
30
|
+
text: string;
|
|
31
|
+
/** URL to open when clicked */
|
|
32
|
+
url: string;
|
|
33
|
+
/** Button style */
|
|
34
|
+
style?: "primary" | "secondary" | "outline";
|
|
35
|
+
/** Optional icon (emoji or icon identifier) */
|
|
36
|
+
icon?: string;
|
|
37
|
+
}
|
|
38
|
+
export interface Me3Profile {
|
|
39
|
+
/** Protocol version */
|
|
40
|
+
version: string;
|
|
41
|
+
/** Display name (required) */
|
|
42
|
+
name: string;
|
|
43
|
+
/** Username/handle */
|
|
44
|
+
handle?: string;
|
|
45
|
+
/** Freeform location string (e.g. "Remote", "Berlin, Germany") */
|
|
46
|
+
location?: string;
|
|
47
|
+
/** Short bio */
|
|
48
|
+
bio?: string;
|
|
49
|
+
/** Avatar URL (absolute or relative) */
|
|
50
|
+
avatar?: string;
|
|
51
|
+
/** Banner/header image URL */
|
|
52
|
+
banner?: string;
|
|
53
|
+
/** Social and external links */
|
|
54
|
+
links?: Me3Links;
|
|
55
|
+
/** Call-to-action buttons (max 3) */
|
|
56
|
+
buttons?: Me3Button[];
|
|
57
|
+
/** Custom pages (markdown) */
|
|
58
|
+
pages?: Me3Page[];
|
|
59
|
+
}
|
|
60
|
+
export interface ValidationError {
|
|
61
|
+
field: string;
|
|
62
|
+
message: string;
|
|
63
|
+
}
|
|
64
|
+
export interface ValidationResult {
|
|
65
|
+
valid: boolean;
|
|
66
|
+
errors: ValidationError[];
|
|
67
|
+
profile?: Me3Profile;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Validate a me3 profile object
|
|
71
|
+
*/
|
|
72
|
+
export declare function validateProfile(data: unknown): ValidationResult;
|
|
73
|
+
/**
|
|
74
|
+
* Parse and validate a me.json string
|
|
75
|
+
*/
|
|
76
|
+
export declare function parseMe3Json(jsonString: string): ValidationResult;
|
|
77
|
+
export declare const ME3_VERSION = "0.1";
|
|
78
|
+
export declare const ME3_FILENAME = "me.json";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* me3 Protocol v0.1
|
|
4
|
+
*
|
|
5
|
+
* A protocol for portable personal websites.
|
|
6
|
+
* Your site lives in a single me.json file that you can take anywhere.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.ME3_FILENAME = exports.ME3_VERSION = void 0;
|
|
10
|
+
exports.validateProfile = validateProfile;
|
|
11
|
+
exports.parseMe3Json = parseMe3Json;
|
|
12
|
+
const CURRENT_VERSION = "0.1";
|
|
13
|
+
const MAX_NAME_LENGTH = 100;
|
|
14
|
+
const MAX_BIO_LENGTH = 500;
|
|
15
|
+
const MAX_HANDLE_LENGTH = 30;
|
|
16
|
+
const HANDLE_REGEX = /^[a-z0-9_-]+$/i;
|
|
17
|
+
const MAX_LOCATION_LENGTH = 100;
|
|
18
|
+
const MAX_BUTTONS = 3;
|
|
19
|
+
const MAX_BUTTON_TEXT_LENGTH = 30;
|
|
20
|
+
const VALID_BUTTON_STYLES = ["primary", "secondary", "outline"];
|
|
21
|
+
const URL_REGEX = /^https?:\/\/.+/i;
|
|
22
|
+
/**
|
|
23
|
+
* Validate a me3 profile object
|
|
24
|
+
*/
|
|
25
|
+
function validateProfile(data) {
|
|
26
|
+
const errors = [];
|
|
27
|
+
if (!data || typeof data !== "object") {
|
|
28
|
+
return {
|
|
29
|
+
valid: false,
|
|
30
|
+
errors: [{ field: "root", message: "Profile must be an object" }],
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
const profile = data;
|
|
34
|
+
// Version (required)
|
|
35
|
+
if (!profile.version || typeof profile.version !== "string") {
|
|
36
|
+
errors.push({ field: "version", message: "Version is required" });
|
|
37
|
+
}
|
|
38
|
+
else if (profile.version !== CURRENT_VERSION) {
|
|
39
|
+
errors.push({
|
|
40
|
+
field: "version",
|
|
41
|
+
message: `Unsupported version. Expected ${CURRENT_VERSION}`,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
// Name (required)
|
|
45
|
+
if (!profile.name || typeof profile.name !== "string") {
|
|
46
|
+
errors.push({ field: "name", message: "Name is required" });
|
|
47
|
+
}
|
|
48
|
+
else if (profile.name.length > MAX_NAME_LENGTH) {
|
|
49
|
+
errors.push({
|
|
50
|
+
field: "name",
|
|
51
|
+
message: `Name must be ${MAX_NAME_LENGTH} characters or less`,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
// Handle (optional)
|
|
55
|
+
if (profile.handle !== undefined) {
|
|
56
|
+
if (typeof profile.handle !== "string") {
|
|
57
|
+
errors.push({ field: "handle", message: "Handle must be a string" });
|
|
58
|
+
}
|
|
59
|
+
else if (profile.handle.length > MAX_HANDLE_LENGTH) {
|
|
60
|
+
errors.push({
|
|
61
|
+
field: "handle",
|
|
62
|
+
message: `Handle must be ${MAX_HANDLE_LENGTH} characters or less`,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
else if (!HANDLE_REGEX.test(profile.handle)) {
|
|
66
|
+
errors.push({
|
|
67
|
+
field: "handle",
|
|
68
|
+
message: "Handle can only contain letters, numbers, underscores, and hyphens",
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
// Location (optional)
|
|
73
|
+
if (profile.location !== undefined) {
|
|
74
|
+
if (typeof profile.location !== "string") {
|
|
75
|
+
errors.push({ field: "location", message: "Location must be a string" });
|
|
76
|
+
}
|
|
77
|
+
else if (profile.location.length > MAX_LOCATION_LENGTH) {
|
|
78
|
+
errors.push({
|
|
79
|
+
field: "location",
|
|
80
|
+
message: `Location must be ${MAX_LOCATION_LENGTH} characters or less`,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
// Bio (optional)
|
|
85
|
+
if (profile.bio !== undefined) {
|
|
86
|
+
if (typeof profile.bio !== "string") {
|
|
87
|
+
errors.push({ field: "bio", message: "Bio must be a string" });
|
|
88
|
+
}
|
|
89
|
+
else if (profile.bio.length > MAX_BIO_LENGTH) {
|
|
90
|
+
errors.push({
|
|
91
|
+
field: "bio",
|
|
92
|
+
message: `Bio must be ${MAX_BIO_LENGTH} characters or less`,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
// Avatar (optional)
|
|
97
|
+
if (profile.avatar !== undefined && typeof profile.avatar !== "string") {
|
|
98
|
+
errors.push({ field: "avatar", message: "Avatar must be a string URL" });
|
|
99
|
+
}
|
|
100
|
+
// Banner (optional)
|
|
101
|
+
if (profile.banner !== undefined && typeof profile.banner !== "string") {
|
|
102
|
+
errors.push({ field: "banner", message: "Banner must be a string URL" });
|
|
103
|
+
}
|
|
104
|
+
// Links (optional)
|
|
105
|
+
if (profile.links !== undefined) {
|
|
106
|
+
if (typeof profile.links !== "object" || profile.links === null) {
|
|
107
|
+
errors.push({ field: "links", message: "Links must be an object" });
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
// Buttons (optional)
|
|
111
|
+
if (profile.buttons !== undefined) {
|
|
112
|
+
if (!Array.isArray(profile.buttons)) {
|
|
113
|
+
errors.push({ field: "buttons", message: "Buttons must be an array" });
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
if (profile.buttons.length > MAX_BUTTONS) {
|
|
117
|
+
errors.push({
|
|
118
|
+
field: "buttons",
|
|
119
|
+
message: `Maximum ${MAX_BUTTONS} buttons allowed`,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
profile.buttons.forEach((button, index) => {
|
|
123
|
+
if (!button || typeof button !== "object") {
|
|
124
|
+
errors.push({
|
|
125
|
+
field: `buttons[${index}]`,
|
|
126
|
+
message: "Button must be an object",
|
|
127
|
+
});
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
if (!button.text || typeof button.text !== "string") {
|
|
131
|
+
errors.push({
|
|
132
|
+
field: `buttons[${index}].text`,
|
|
133
|
+
message: "Button text is required",
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
else if (button.text.length > MAX_BUTTON_TEXT_LENGTH) {
|
|
137
|
+
errors.push({
|
|
138
|
+
field: `buttons[${index}].text`,
|
|
139
|
+
message: `Button text must be ${MAX_BUTTON_TEXT_LENGTH} characters or less`,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
if (!button.url || typeof button.url !== "string") {
|
|
143
|
+
errors.push({
|
|
144
|
+
field: `buttons[${index}].url`,
|
|
145
|
+
message: "Button URL is required",
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
else if (!URL_REGEX.test(button.url)) {
|
|
149
|
+
errors.push({
|
|
150
|
+
field: `buttons[${index}].url`,
|
|
151
|
+
message: "Button URL must be a valid URL starting with http:// or https://",
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
if (button.style !== undefined &&
|
|
155
|
+
!VALID_BUTTON_STYLES.includes(button.style)) {
|
|
156
|
+
errors.push({
|
|
157
|
+
field: `buttons[${index}].style`,
|
|
158
|
+
message: `Button style must be one of: ${VALID_BUTTON_STYLES.join(", ")}`,
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
if (button.icon !== undefined && typeof button.icon !== "string") {
|
|
162
|
+
errors.push({
|
|
163
|
+
field: `buttons[${index}].icon`,
|
|
164
|
+
message: "Button icon must be a string",
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
// Pages (optional)
|
|
171
|
+
if (profile.pages !== undefined) {
|
|
172
|
+
if (!Array.isArray(profile.pages)) {
|
|
173
|
+
errors.push({ field: "pages", message: "Pages must be an array" });
|
|
174
|
+
}
|
|
175
|
+
else {
|
|
176
|
+
profile.pages.forEach((page, index) => {
|
|
177
|
+
if (!page || typeof page !== "object") {
|
|
178
|
+
errors.push({
|
|
179
|
+
field: `pages[${index}]`,
|
|
180
|
+
message: "Page must be an object",
|
|
181
|
+
});
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
if (!page.slug || typeof page.slug !== "string") {
|
|
185
|
+
errors.push({
|
|
186
|
+
field: `pages[${index}].slug`,
|
|
187
|
+
message: "Page slug is required",
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
if (!page.title || typeof page.title !== "string") {
|
|
191
|
+
errors.push({
|
|
192
|
+
field: `pages[${index}].title`,
|
|
193
|
+
message: "Page title is required",
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
if (!page.file || typeof page.file !== "string") {
|
|
197
|
+
errors.push({
|
|
198
|
+
field: `pages[${index}].file`,
|
|
199
|
+
message: "Page file is required",
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
if (typeof page.visible !== "boolean") {
|
|
203
|
+
errors.push({
|
|
204
|
+
field: `pages[${index}].visible`,
|
|
205
|
+
message: "Page visible must be a boolean",
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
if (errors.length > 0) {
|
|
212
|
+
return { valid: false, errors };
|
|
213
|
+
}
|
|
214
|
+
return {
|
|
215
|
+
valid: true,
|
|
216
|
+
errors: [],
|
|
217
|
+
profile: profile,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Parse and validate a me.json string
|
|
222
|
+
*/
|
|
223
|
+
function parseMe3Json(jsonString) {
|
|
224
|
+
try {
|
|
225
|
+
const data = JSON.parse(jsonString);
|
|
226
|
+
return validateProfile(data);
|
|
227
|
+
}
|
|
228
|
+
catch (e) {
|
|
229
|
+
return {
|
|
230
|
+
valid: false,
|
|
231
|
+
errors: [{ field: "root", message: "Invalid JSON" }],
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
exports.ME3_VERSION = CURRENT_VERSION;
|
|
236
|
+
exports.ME3_FILENAME = "me.json";
|
package/examples/full.json
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
"version": "0.1",
|
|
3
3
|
"name": "Kieran Butler",
|
|
4
4
|
"handle": "kieran",
|
|
5
|
+
"location": "Austin, TX",
|
|
5
6
|
"bio": "Intuitive. Coach. Coder. Creative. Building Soulink.",
|
|
6
7
|
"avatar": "https://api.dicebear.com/7.x/avataaars/svg?seed=kieran",
|
|
7
8
|
"banner": "https://images.unsplash.com/photo-1519681393784-d120267933ba?w=1200&h=400&fit=crop",
|
|
@@ -11,8 +12,18 @@
|
|
|
11
12
|
"soulink": "kieran"
|
|
12
13
|
},
|
|
13
14
|
"buttons": [
|
|
14
|
-
{
|
|
15
|
-
|
|
15
|
+
{
|
|
16
|
+
"text": "Book a Discovery Call",
|
|
17
|
+
"url": "https://cal.com/kieran",
|
|
18
|
+
"style": "primary",
|
|
19
|
+
"icon": "📅"
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"text": "Buy me a coffee",
|
|
23
|
+
"url": "https://buymeacoffee.com/kieran",
|
|
24
|
+
"style": "secondary",
|
|
25
|
+
"icon": "☕"
|
|
26
|
+
}
|
|
16
27
|
],
|
|
17
28
|
"pages": [
|
|
18
29
|
{ "slug": "about", "title": "About", "file": "about.md", "visible": true }
|
package/examples/simple.json
CHANGED
package/package.json
CHANGED
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "me3-protocol",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "",
|
|
5
|
-
"main": "index.js",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"types": "./dist/index.d.ts",
|
|
7
|
+
"files": [
|
|
8
|
+
"dist",
|
|
9
|
+
"schema.json",
|
|
10
|
+
"README.md",
|
|
11
|
+
"examples"
|
|
12
|
+
],
|
|
6
13
|
"scripts": {
|
|
7
14
|
"build": "tsc",
|
|
8
15
|
"generate-schema": "ts-json-schema-generator --path 'src/index.ts' --type 'Me3Profile' --out 'schema.json' --no-type-check",
|
|
@@ -11,6 +18,8 @@
|
|
|
11
18
|
"exports": {
|
|
12
19
|
".": {
|
|
13
20
|
"types": "./dist/index.d.ts",
|
|
21
|
+
"import": "./dist/index.js",
|
|
22
|
+
"require": "./dist/index.js",
|
|
14
23
|
"default": "./dist/index.js"
|
|
15
24
|
}
|
|
16
25
|
},
|
package/schema.json
CHANGED
|
@@ -131,6 +131,10 @@
|
|
|
131
131
|
"$ref": "#/definitions/Me3Links",
|
|
132
132
|
"description": "Social and external links"
|
|
133
133
|
},
|
|
134
|
+
"location": {
|
|
135
|
+
"description": "Freeform location string (e.g. \"Remote\", \"Berlin, Germany\")",
|
|
136
|
+
"type": "string"
|
|
137
|
+
},
|
|
134
138
|
"name": {
|
|
135
139
|
"description": "Display name (required)",
|
|
136
140
|
"type": "string"
|
|
@@ -142,10 +146,6 @@
|
|
|
142
146
|
},
|
|
143
147
|
"type": "array"
|
|
144
148
|
},
|
|
145
|
-
"theme": {
|
|
146
|
-
"description": "Theme identifier (for future use)",
|
|
147
|
-
"type": "string"
|
|
148
|
-
},
|
|
149
149
|
"version": {
|
|
150
150
|
"description": "Protocol version",
|
|
151
151
|
"type": "string"
|
package/src/index.ts
DELETED
|
@@ -1,246 +0,0 @@
|
|
|
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
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
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
|
-
}
|