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