me3-protocol 1.0.0 → 2.1.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 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
- * **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.
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,23 @@ 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 | 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. |
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
+ | `footer` | `object` | No | Custom footer config (or `false` to hide). |
52
+
53
+ ### Examples
43
54
 
44
- ### Example
55
+ Minimal:
45
56
 
46
57
  ```json
47
58
  {
@@ -49,6 +60,7 @@ Your `me.json` defines who you are and where to find you. It is strictly typed t
49
60
  "name": "Jane Doe",
50
61
  "handle": "janedoe",
51
62
  "bio": "Building the open web. Creative Director at Studio X.",
63
+ "location": "Berlin, Germany",
52
64
  "avatar": "https://example.com/jane.jpg",
53
65
  "links": {
54
66
  "website": "https://janedoe.com",
@@ -65,15 +77,25 @@ Your `me.json` defines who you are and where to find you. It is strictly typed t
65
77
  }
66
78
  ```
67
79
 
80
+ More complete examples live in [`examples/`](./examples/), including [`examples/simple.json`](./examples/simple.json) and [`examples/full.json`](./examples/full.json).
81
+
68
82
  ## 3. What it is NOT
69
83
 
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.
84
+ - **NOT Authentication**: `me.json` is public data. It does not handle logins, passwords, or private keys.
85
+ - **NOT a Social Network**: There is no "feed", no "likes", and no central server. You own your data.
86
+ - **NOT a Platform**: You can host this file on GitHub Pages, Vercel, WordPress, or your own server.
87
+ - **NOT Reputation / Ranking**: No scoring, ranking, endorsements, verification, or algorithmic ordering.
88
+
89
+ ## 4. Guardrails & versioning
90
+
91
+ - **Current version**: `0.1` (see `version` field). Validators in this repo currently require `version === "0.1"`.
92
+ - **Backwards compatibility**: `0.1` is intended to stay stable. Changes should be additive and conservative.
93
+ - **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
94
 
74
- ## 4. Usage
95
+ ## 5. Usage
75
96
 
76
97
  ### For Developers
98
+
77
99
  You can use this package to validate `me.json` files in your applications.
78
100
 
79
101
  ```bash
@@ -81,18 +103,19 @@ npm install me3-protocol
81
103
  ```
82
104
 
83
105
  ```typescript
84
- import { validateProfile, parseMe3Json } from 'me3-protocol'
106
+ import { validateProfile, parseMe3Json } from "me3-protocol";
85
107
 
86
108
  // Validate an object
87
- const result = validateProfile(myProfileData)
109
+ const result = validateProfile(myProfileData);
88
110
 
89
111
  // Parse and validate a string
90
- const result = parseMe3Json(jsonString)
112
+ const result = parseMe3Json(jsonString);
91
113
 
92
114
  if (!result.valid) {
93
- console.error(result.errors)
115
+ console.error(result.errors);
94
116
  }
95
117
  ```
96
118
 
97
119
  ### JSON Schema
120
+
98
121
  A standard JSON Schema is available in [schema.json](./schema.json) for non-TypeScript implementations.
@@ -0,0 +1,96 @@
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 Me3FooterLink {
39
+ /** Link text */
40
+ text: string;
41
+ /** URL to open when clicked */
42
+ url: string;
43
+ }
44
+ export interface Me3Footer {
45
+ /** Custom footer text (e.g. "Built by Jane") */
46
+ text?: string;
47
+ /** Optional custom footer link */
48
+ link?: Me3FooterLink;
49
+ }
50
+ export interface Me3Profile {
51
+ /** Protocol version */
52
+ version: string;
53
+ /** Display name (required) */
54
+ name: string;
55
+ /** Username/handle */
56
+ handle?: string;
57
+ /** Freeform location string (e.g. "Remote", "Berlin, Germany") */
58
+ location?: string;
59
+ /** Short bio */
60
+ bio?: string;
61
+ /** Avatar URL (absolute or relative) */
62
+ avatar?: string;
63
+ /** Banner/header image URL */
64
+ banner?: string;
65
+ /** Social and external links */
66
+ links?: Me3Links;
67
+ /** Call-to-action buttons (max 3) */
68
+ buttons?: Me3Button[];
69
+ /** Custom pages (markdown) */
70
+ pages?: Me3Page[];
71
+ /**
72
+ * Custom footer configuration.
73
+ * - `undefined`: default footer behavior (renderer-defined)
74
+ * - `false`: hide footer (renderer may restrict this to Pro tiers)
75
+ */
76
+ footer?: Me3Footer | false;
77
+ }
78
+ export interface ValidationError {
79
+ field: string;
80
+ message: string;
81
+ }
82
+ export interface ValidationResult {
83
+ valid: boolean;
84
+ errors: ValidationError[];
85
+ profile?: Me3Profile;
86
+ }
87
+ /**
88
+ * Validate a me3 profile object
89
+ */
90
+ export declare function validateProfile(data: unknown): ValidationResult;
91
+ /**
92
+ * Parse and validate a me.json string
93
+ */
94
+ export declare function parseMe3Json(jsonString: string): ValidationResult;
95
+ export declare const ME3_VERSION = "0.1";
96
+ export declare const ME3_FILENAME = "me.json";
package/dist/index.js ADDED
@@ -0,0 +1,290 @@
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
+ const MAX_FOOTER_TEXT_LENGTH = 200;
23
+ const MAX_FOOTER_LINK_TEXT_LENGTH = 60;
24
+ /**
25
+ * Validate a me3 profile object
26
+ */
27
+ function validateProfile(data) {
28
+ const errors = [];
29
+ if (!data || typeof data !== "object") {
30
+ return {
31
+ valid: false,
32
+ errors: [{ field: "root", message: "Profile must be an object" }],
33
+ };
34
+ }
35
+ const profile = data;
36
+ // Version (required)
37
+ if (!profile.version || typeof profile.version !== "string") {
38
+ errors.push({ field: "version", message: "Version is required" });
39
+ }
40
+ else if (profile.version !== CURRENT_VERSION) {
41
+ errors.push({
42
+ field: "version",
43
+ message: `Unsupported version. Expected ${CURRENT_VERSION}`,
44
+ });
45
+ }
46
+ // Name (required)
47
+ if (!profile.name || typeof profile.name !== "string") {
48
+ errors.push({ field: "name", message: "Name is required" });
49
+ }
50
+ else if (profile.name.length > MAX_NAME_LENGTH) {
51
+ errors.push({
52
+ field: "name",
53
+ message: `Name must be ${MAX_NAME_LENGTH} characters or less`,
54
+ });
55
+ }
56
+ // Handle (optional)
57
+ if (profile.handle !== undefined) {
58
+ if (typeof profile.handle !== "string") {
59
+ errors.push({ field: "handle", message: "Handle must be a string" });
60
+ }
61
+ else if (profile.handle.length > MAX_HANDLE_LENGTH) {
62
+ errors.push({
63
+ field: "handle",
64
+ message: `Handle must be ${MAX_HANDLE_LENGTH} characters or less`,
65
+ });
66
+ }
67
+ else if (!HANDLE_REGEX.test(profile.handle)) {
68
+ errors.push({
69
+ field: "handle",
70
+ message: "Handle can only contain letters, numbers, underscores, and hyphens",
71
+ });
72
+ }
73
+ }
74
+ // Location (optional)
75
+ if (profile.location !== undefined) {
76
+ if (typeof profile.location !== "string") {
77
+ errors.push({ field: "location", message: "Location must be a string" });
78
+ }
79
+ else if (profile.location.length > MAX_LOCATION_LENGTH) {
80
+ errors.push({
81
+ field: "location",
82
+ message: `Location must be ${MAX_LOCATION_LENGTH} characters or less`,
83
+ });
84
+ }
85
+ }
86
+ // Bio (optional)
87
+ if (profile.bio !== undefined) {
88
+ if (typeof profile.bio !== "string") {
89
+ errors.push({ field: "bio", message: "Bio must be a string" });
90
+ }
91
+ else if (profile.bio.length > MAX_BIO_LENGTH) {
92
+ errors.push({
93
+ field: "bio",
94
+ message: `Bio must be ${MAX_BIO_LENGTH} characters or less`,
95
+ });
96
+ }
97
+ }
98
+ // Avatar (optional)
99
+ if (profile.avatar !== undefined && typeof profile.avatar !== "string") {
100
+ errors.push({ field: "avatar", message: "Avatar must be a string URL" });
101
+ }
102
+ // Banner (optional)
103
+ if (profile.banner !== undefined && typeof profile.banner !== "string") {
104
+ errors.push({ field: "banner", message: "Banner must be a string URL" });
105
+ }
106
+ // Links (optional)
107
+ if (profile.links !== undefined) {
108
+ if (typeof profile.links !== "object" || profile.links === null) {
109
+ errors.push({ field: "links", message: "Links must be an object" });
110
+ }
111
+ }
112
+ // Buttons (optional)
113
+ if (profile.buttons !== undefined) {
114
+ if (!Array.isArray(profile.buttons)) {
115
+ errors.push({ field: "buttons", message: "Buttons must be an array" });
116
+ }
117
+ else {
118
+ if (profile.buttons.length > MAX_BUTTONS) {
119
+ errors.push({
120
+ field: "buttons",
121
+ message: `Maximum ${MAX_BUTTONS} buttons allowed`,
122
+ });
123
+ }
124
+ profile.buttons.forEach((button, index) => {
125
+ if (!button || typeof button !== "object") {
126
+ errors.push({
127
+ field: `buttons[${index}]`,
128
+ message: "Button must be an object",
129
+ });
130
+ return;
131
+ }
132
+ if (!button.text || typeof button.text !== "string") {
133
+ errors.push({
134
+ field: `buttons[${index}].text`,
135
+ message: "Button text is required",
136
+ });
137
+ }
138
+ else if (button.text.length > MAX_BUTTON_TEXT_LENGTH) {
139
+ errors.push({
140
+ field: `buttons[${index}].text`,
141
+ message: `Button text must be ${MAX_BUTTON_TEXT_LENGTH} characters or less`,
142
+ });
143
+ }
144
+ if (!button.url || typeof button.url !== "string") {
145
+ errors.push({
146
+ field: `buttons[${index}].url`,
147
+ message: "Button URL is required",
148
+ });
149
+ }
150
+ else if (!URL_REGEX.test(button.url)) {
151
+ errors.push({
152
+ field: `buttons[${index}].url`,
153
+ message: "Button URL must be a valid URL starting with http:// or https://",
154
+ });
155
+ }
156
+ if (button.style !== undefined &&
157
+ !VALID_BUTTON_STYLES.includes(button.style)) {
158
+ errors.push({
159
+ field: `buttons[${index}].style`,
160
+ message: `Button style must be one of: ${VALID_BUTTON_STYLES.join(", ")}`,
161
+ });
162
+ }
163
+ if (button.icon !== undefined && typeof button.icon !== "string") {
164
+ errors.push({
165
+ field: `buttons[${index}].icon`,
166
+ message: "Button icon must be a string",
167
+ });
168
+ }
169
+ });
170
+ }
171
+ }
172
+ // Footer (optional)
173
+ if (profile.footer !== undefined) {
174
+ if (profile.footer === false) {
175
+ // ok (renderer may enforce tier restrictions)
176
+ }
177
+ else if (typeof profile.footer !== "object" || profile.footer === null) {
178
+ errors.push({
179
+ field: "footer",
180
+ message: "Footer must be an object or false",
181
+ });
182
+ }
183
+ else {
184
+ const footer = profile.footer;
185
+ if (footer.text !== undefined) {
186
+ if (typeof footer.text !== "string") {
187
+ errors.push({ field: "footer.text", message: "Footer text must be a string" });
188
+ }
189
+ else if (footer.text.length > MAX_FOOTER_TEXT_LENGTH) {
190
+ errors.push({
191
+ field: "footer.text",
192
+ message: `Footer text must be ${MAX_FOOTER_TEXT_LENGTH} characters or less`,
193
+ });
194
+ }
195
+ }
196
+ if (footer.link !== undefined) {
197
+ if (typeof footer.link !== "object" || footer.link === null) {
198
+ errors.push({ field: "footer.link", message: "Footer link must be an object" });
199
+ }
200
+ else {
201
+ const link = footer.link;
202
+ if (!link.text || typeof link.text !== "string") {
203
+ errors.push({ field: "footer.link.text", message: "Footer link text is required" });
204
+ }
205
+ else if (link.text.length > MAX_FOOTER_LINK_TEXT_LENGTH) {
206
+ errors.push({
207
+ field: "footer.link.text",
208
+ message: `Footer link text must be ${MAX_FOOTER_LINK_TEXT_LENGTH} characters or less`,
209
+ });
210
+ }
211
+ if (!link.url || typeof link.url !== "string") {
212
+ errors.push({ field: "footer.link.url", message: "Footer link URL is required" });
213
+ }
214
+ else if (!URL_REGEX.test(link.url)) {
215
+ errors.push({
216
+ field: "footer.link.url",
217
+ message: "Footer link URL must be a valid URL starting with http:// or https://",
218
+ });
219
+ }
220
+ }
221
+ }
222
+ }
223
+ }
224
+ // Pages (optional)
225
+ if (profile.pages !== undefined) {
226
+ if (!Array.isArray(profile.pages)) {
227
+ errors.push({ field: "pages", message: "Pages must be an array" });
228
+ }
229
+ else {
230
+ profile.pages.forEach((page, index) => {
231
+ if (!page || typeof page !== "object") {
232
+ errors.push({
233
+ field: `pages[${index}]`,
234
+ message: "Page must be an object",
235
+ });
236
+ return;
237
+ }
238
+ if (!page.slug || typeof page.slug !== "string") {
239
+ errors.push({
240
+ field: `pages[${index}].slug`,
241
+ message: "Page slug is required",
242
+ });
243
+ }
244
+ if (!page.title || typeof page.title !== "string") {
245
+ errors.push({
246
+ field: `pages[${index}].title`,
247
+ message: "Page title is required",
248
+ });
249
+ }
250
+ if (!page.file || typeof page.file !== "string") {
251
+ errors.push({
252
+ field: `pages[${index}].file`,
253
+ message: "Page file is required",
254
+ });
255
+ }
256
+ if (typeof page.visible !== "boolean") {
257
+ errors.push({
258
+ field: `pages[${index}].visible`,
259
+ message: "Page visible must be a boolean",
260
+ });
261
+ }
262
+ });
263
+ }
264
+ }
265
+ if (errors.length > 0) {
266
+ return { valid: false, errors };
267
+ }
268
+ return {
269
+ valid: true,
270
+ errors: [],
271
+ profile: profile,
272
+ };
273
+ }
274
+ /**
275
+ * Parse and validate a me.json string
276
+ */
277
+ function parseMe3Json(jsonString) {
278
+ try {
279
+ const data = JSON.parse(jsonString);
280
+ return validateProfile(data);
281
+ }
282
+ catch (e) {
283
+ return {
284
+ valid: false,
285
+ errors: [{ field: "root", message: "Invalid JSON" }],
286
+ };
287
+ }
288
+ }
289
+ exports.ME3_VERSION = CURRENT_VERSION;
290
+ exports.ME3_FILENAME = "me.json";
@@ -1,18 +1,29 @@
1
1
  {
2
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",
3
+ "name": "Example Name",
4
+ "handle": "example",
5
+ "location": "City, Country",
6
+ "bio": "Short example bio describing what this person does.",
7
+ "avatar": "https://api.dicebear.com/7.x/avataaars/svg?seed=example",
7
8
  "banner": "https://images.unsplash.com/photo-1519681393784-d120267933ba?w=1200&h=400&fit=crop",
8
9
  "links": {
9
- "website": "https://kieranbutler.com",
10
- "github": "kieranbutler",
11
- "soulink": "kieran"
10
+ "website": "https://example.com",
11
+ "github": "example",
12
+ "soulink": "example"
12
13
  },
13
14
  "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": "☕" }
15
+ {
16
+ "text": "Primary Call To Action",
17
+ "url": "https://cal.com/example",
18
+ "style": "primary",
19
+ "icon": "📅"
20
+ },
21
+ {
22
+ "text": "Secondary Action",
23
+ "url": "https://buymeacoffee.com/example",
24
+ "style": "secondary",
25
+ "icon": "☕"
26
+ }
16
27
  ],
17
28
  "pages": [
18
29
  { "slug": "about", "title": "About", "file": "about.md", "visible": true }
@@ -2,6 +2,7 @@
2
2
  "version": "0.1",
3
3
  "name": "Jane Doe",
4
4
  "handle": "janedoe",
5
+ "location": "Remote",
5
6
  "bio": "Minimalist example.",
6
7
  "links": {
7
8
  "twitter": "janedoe"
package/package.json CHANGED
@@ -1,8 +1,15 @@
1
1
  {
2
2
  "name": "me3-protocol",
3
- "version": "1.0.0",
3
+ "version": "2.1.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
@@ -33,6 +33,38 @@
33
33
  ],
34
34
  "type": "object"
35
35
  },
36
+ "Me3Footer": {
37
+ "additionalProperties": false,
38
+ "properties": {
39
+ "link": {
40
+ "$ref": "#/definitions/Me3FooterLink",
41
+ "description": "Optional custom footer link"
42
+ },
43
+ "text": {
44
+ "description": "Custom footer text (e.g. \"Built by Jane\")",
45
+ "type": "string"
46
+ }
47
+ },
48
+ "type": "object"
49
+ },
50
+ "Me3FooterLink": {
51
+ "additionalProperties": false,
52
+ "properties": {
53
+ "text": {
54
+ "description": "Link text",
55
+ "type": "string"
56
+ },
57
+ "url": {
58
+ "description": "URL to open when clicked",
59
+ "type": "string"
60
+ }
61
+ },
62
+ "required": [
63
+ "text",
64
+ "url"
65
+ ],
66
+ "type": "object"
67
+ },
36
68
  "Me3Links": {
37
69
  "additionalProperties": {
38
70
  "anyOf": [
@@ -123,6 +155,18 @@
123
155
  },
124
156
  "type": "array"
125
157
  },
158
+ "footer": {
159
+ "anyOf": [
160
+ {
161
+ "$ref": "#/definitions/Me3Footer"
162
+ },
163
+ {
164
+ "const": false,
165
+ "type": "boolean"
166
+ }
167
+ ],
168
+ "description": "Custom footer configuration.\n- `undefined`: default footer behavior (renderer-defined)\n- `false`: hide footer (renderer may restrict this to Pro tiers)"
169
+ },
126
170
  "handle": {
127
171
  "description": "Username/handle",
128
172
  "type": "string"
@@ -131,6 +175,10 @@
131
175
  "$ref": "#/definitions/Me3Links",
132
176
  "description": "Social and external links"
133
177
  },
178
+ "location": {
179
+ "description": "Freeform location string (e.g. \"Remote\", \"Berlin, Germany\")",
180
+ "type": "string"
181
+ },
134
182
  "name": {
135
183
  "description": "Display name (required)",
136
184
  "type": "string"
@@ -142,10 +190,6 @@
142
190
  },
143
191
  "type": "array"
144
192
  },
145
- "theme": {
146
- "description": "Theme identifier (for future use)",
147
- "type": "string"
148
- },
149
193
  "version": {
150
194
  "description": "Protocol version",
151
195
  "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
- }