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 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,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 | 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
+
52
+ ### Examples
43
53
 
44
- ### Example
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
- * **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.
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
- ## 4. Usage
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 'me3-protocol'
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.
@@ -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";
@@ -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
- { "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": "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 }
@@ -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.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
- }