verify-phone-sms 0.9.4
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/.env.example +20 -0
- package/CHANGELOG.md +71 -0
- package/DEPLOYMENT.md +151 -0
- package/README.md +475 -0
- package/bun.lock +1110 -0
- package/docs/.source/index.ts +11 -0
- package/docs/.source/source.config.mjs +9 -0
- package/docs/app/(home)/layout.tsx +7 -0
- package/docs/app/(home)/page.tsx +38 -0
- package/docs/app/docs/[[...slug]]/page.tsx +59 -0
- package/docs/app/docs/layout.tsx +12 -0
- package/docs/app/docs-og/[...slug]/route.ts +24 -0
- package/docs/app/globals.css +587 -0
- package/docs/app/layout.config.tsx +13 -0
- package/docs/app/layout.tsx +27 -0
- package/docs/app/logo.tsx +35 -0
- package/docs/bun.lock +923 -0
- package/docs/content/docs/API_AUTHENTICATION.md +91 -0
- package/docs/content/docs/DEPLOYMENT.md +181 -0
- package/docs/content/docs/api/post.mdx +35 -0
- package/docs/content/docs/api/verify.mdx +34 -0
- package/docs/content/docs/meta.json +8 -0
- package/docs/content/docs/verify-legal-name.md +339 -0
- package/docs/lib/source.ts +14 -0
- package/docs/mdx-components.tsx +12 -0
- package/docs/next.config.mjs +51 -0
- package/docs/openapi.json +329 -0
- package/docs/package.json +37 -0
- package/docs/postcss.config.mjs +5 -0
- package/docs/scripts/generate-docs.mjs +23 -0
- package/docs/source.config.ts +5 -0
- package/docs/tsconfig.json +29 -0
- package/docs/worker.js +35 -0
- package/docs/wrangler.toml +26 -0
- package/examples/client.ts +105 -0
- package/examples/demo.html +325 -0
- package/examples/libphonenumber-example.ts +120 -0
- package/openapi.json +329 -0
- package/package.json +75 -0
- package/scripts/deploy.sh +63 -0
- package/src/identity-verification-server.ts +677 -0
- package/src/index.ts +8 -0
- package/src/sns.ts +265 -0
- package/src/verify-phone-server.ts +503 -0
- package/src/verify-phone.ts +577 -0
- package/test/api.test.ts +205 -0
- package/test/integration.test.ts +152 -0
- package/test/metadata-test.ts +73 -0
- package/test/server.test.ts +143 -0
- package/test/setup.ts +32 -0
- package/test/utils.test.ts +186 -0
- package/test/verify.test.ts +20 -0
- package/test/voip.test.ts +113 -0
- package/tsconfig.json +24 -0
- package/vitest.config.ts +10 -0
- package/wrangler.toml +24 -0
package/README.md
ADDED
|
@@ -0,0 +1,475 @@
|
|
|
1
|
+
# SMS Verification API Server
|
|
2
|
+
|
|
3
|
+
A complete Hono-based server for SMS verification using AWS SNS. Built for Cloudflare Workers with comprehensive API documentation and security features.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- ✅ **SMS Verification**: Send verification codes via AWS SNS
|
|
8
|
+
- ✅ **VoIP Blocking**: Optional blocking of VoIP numbers
|
|
9
|
+
- ✅ **API Authentication**: Secure API key-based authentication
|
|
10
|
+
- ✅ **Rate Limiting**: Built-in rate limiting protection
|
|
11
|
+
- ✅ **OpenAPI Documentation**: Auto-generated API documentation
|
|
12
|
+
- ✅ **CORS Support**: Cross-origin resource sharing enabled
|
|
13
|
+
- ✅ **Security Headers**: Secure headers middleware
|
|
14
|
+
- ✅ **Error Handling**: Comprehensive error handling
|
|
15
|
+
- ✅ **Health Checks**: Built-in health monitoring
|
|
16
|
+
- ✅ **General SMS**: Send custom SMS messages
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
## Other Verifications
|
|
21
|
+
|
|
22
|
+
- add with persona api inergration $250/month (includes 166 verification )
|
|
23
|
+
|
|
24
|
+
- auto-sign in api with phone of registered users
|
|
25
|
+
- sell corps ability to verify their customers are real
|
|
26
|
+
- past addresses are better than legal id which can be ai-gen or reused
|
|
27
|
+
- demographic info for ads
|
|
28
|
+
|
|
29
|
+
- [Leaderboard](https://pages.nist.gov/frvt/html/frvt11.html)
|
|
30
|
+
- [Liveliness Check](https://github.com/Faceplugin-ltd/FaceRecognition-Android)
|
|
31
|
+
- [Face Check](https://github.com/DoubangoTelecom/FaceLivenessDetection-SDK)
|
|
32
|
+
- [FaceLivenessDetection-SDK](https://github.com/DoubangoTelecom/FaceLivenessDetection-SDK)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
## Quick Start
|
|
36
|
+
|
|
37
|
+
### 1. Install Dependencies
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
npm install
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### 2. Set Environment Variables
|
|
44
|
+
|
|
45
|
+
Create a `.env` file or set environment variables:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
# AWS Credentials
|
|
49
|
+
AWS_ACCESS_KEY_ID=your_aws_access_key
|
|
50
|
+
AWS_SECRET_ACCESS_KEY=your_aws_secret_key
|
|
51
|
+
AWS_REGION=us-east-1
|
|
52
|
+
|
|
53
|
+
# API Configuration
|
|
54
|
+
API_KEY=sms_1234567890abcdef1234567890abcdef
|
|
55
|
+
SMS_SENDER_ID=Verify
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### 3. Run Development Server
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
npm run dev
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
The server will be available at `http://localhost:8787`
|
|
65
|
+
|
|
66
|
+
### 4. Deploy to Cloudflare Workers
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
# Deploy API only
|
|
70
|
+
npm run deploy
|
|
71
|
+
|
|
72
|
+
# Deploy docs only
|
|
73
|
+
npm run build:docs
|
|
74
|
+
npm run deploy:docs
|
|
75
|
+
|
|
76
|
+
# Deploy both API and docs
|
|
77
|
+
npm run deploy:all
|
|
78
|
+
|
|
79
|
+
# Deploy to specific environments
|
|
80
|
+
npm run deploy:staging
|
|
81
|
+
npm run deploy:production
|
|
82
|
+
npm run deploy:all:staging
|
|
83
|
+
npm run deploy:all:production
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### 5. Quick Deployment Script
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
# Deploy everything to default environment
|
|
90
|
+
./scripts/deploy.sh
|
|
91
|
+
|
|
92
|
+
# Deploy to staging
|
|
93
|
+
./scripts/deploy.sh staging
|
|
94
|
+
|
|
95
|
+
# Deploy to production
|
|
96
|
+
./scripts/deploy.sh production
|
|
97
|
+
|
|
98
|
+
# Deploy only API
|
|
99
|
+
./scripts/deploy.sh default api
|
|
100
|
+
|
|
101
|
+
# Deploy only docs
|
|
102
|
+
./scripts/deploy.sh default docs
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Deployment
|
|
106
|
+
|
|
107
|
+
### Prerequisites
|
|
108
|
+
|
|
109
|
+
1. **Install Wrangler CLI**:
|
|
110
|
+
```bash
|
|
111
|
+
npm install -g wrangler
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
2. **Login to Cloudflare**:
|
|
115
|
+
```bash
|
|
116
|
+
wrangler login
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
3. **Set Secrets**:
|
|
120
|
+
```bash
|
|
121
|
+
wrangler secret put AWS_ACCESS_KEY_ID
|
|
122
|
+
wrangler secret put AWS_SECRET_ACCESS_KEY
|
|
123
|
+
wrangler secret put API_KEY
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### Environment Configuration
|
|
127
|
+
|
|
128
|
+
The project supports multiple deployment environments:
|
|
129
|
+
|
|
130
|
+
- **Default**: Development/testing environment
|
|
131
|
+
- **Staging**: Pre-production testing
|
|
132
|
+
- **Production**: Live production environment
|
|
133
|
+
|
|
134
|
+
Each environment can have its own configuration and secrets.
|
|
135
|
+
|
|
136
|
+
### Automated Deployment
|
|
137
|
+
|
|
138
|
+
GitHub Actions workflows are included for automated deployment:
|
|
139
|
+
|
|
140
|
+
- **Push to `main`**: Deploys to production
|
|
141
|
+
- **Push to `staging`**: Deploys to staging
|
|
142
|
+
- **Pull Requests**: Runs tests and builds
|
|
143
|
+
|
|
144
|
+
Required GitHub Secrets:
|
|
145
|
+
- `CLOUDFLARE_API_TOKEN`
|
|
146
|
+
- `CLOUDFLARE_ACCOUNT_ID`
|
|
147
|
+
|
|
148
|
+
For detailed deployment instructions, see [DEPLOYMENT.md](./DEPLOYMENT.md).
|
|
149
|
+
|
|
150
|
+
## API Endpoints
|
|
151
|
+
|
|
152
|
+
### Health Check
|
|
153
|
+
|
|
154
|
+
```http
|
|
155
|
+
GET /
|
|
156
|
+
GET /health
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### Send Verification Code
|
|
160
|
+
|
|
161
|
+
```http
|
|
162
|
+
POST /api/send
|
|
163
|
+
Content-Type: application/json
|
|
164
|
+
X-API-Key: your_api_key
|
|
165
|
+
|
|
166
|
+
{
|
|
167
|
+
"phoneNumber": "+1234567890",
|
|
168
|
+
"code": "123456", // optional, auto-generated if not provided
|
|
169
|
+
"blockVoip": true, // optional, default: false
|
|
170
|
+
"senderId": "MyApp", // optional, default: "Verify"
|
|
171
|
+
"messageTemplate": "Your code is: {code}", // optional
|
|
172
|
+
"smsType": "Transactional" // optional, "Transactional" or "Promotional"
|
|
173
|
+
}
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
**Response:**
|
|
177
|
+
```json
|
|
178
|
+
{
|
|
179
|
+
"success": true,
|
|
180
|
+
"message": "Verification code sent successfully",
|
|
181
|
+
"messageId": "abc123def456",
|
|
182
|
+
"code": "123456",
|
|
183
|
+
"phoneNumber": "+1234567890",
|
|
184
|
+
"expiresIn": 600
|
|
185
|
+
}
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### Verify Code
|
|
189
|
+
|
|
190
|
+
```http
|
|
191
|
+
POST /api/verify
|
|
192
|
+
Content-Type: application/json
|
|
193
|
+
X-API-Key: your_api_key
|
|
194
|
+
|
|
195
|
+
{
|
|
196
|
+
"phoneNumber": "+1234567890",
|
|
197
|
+
"code": "123456"
|
|
198
|
+
}
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
**Response:**
|
|
202
|
+
```json
|
|
203
|
+
{
|
|
204
|
+
"success": true,
|
|
205
|
+
"message": "Code verified successfully",
|
|
206
|
+
"verified": true
|
|
207
|
+
}
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
### Send General SMS
|
|
211
|
+
|
|
212
|
+
```http
|
|
213
|
+
POST /api/sms
|
|
214
|
+
Content-Type: application/json
|
|
215
|
+
X-API-Key: your_api_key
|
|
216
|
+
|
|
217
|
+
{
|
|
218
|
+
"phoneNumber": "+1234567890",
|
|
219
|
+
"message": "Hello from your app!",
|
|
220
|
+
"senderId": "MyApp",
|
|
221
|
+
"smsType": "Transactional"
|
|
222
|
+
}
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
**Response:**
|
|
226
|
+
```json
|
|
227
|
+
{
|
|
228
|
+
"success": true,
|
|
229
|
+
"message": "SMS sent successfully",
|
|
230
|
+
"messageId": "abc123def456",
|
|
231
|
+
"phoneNumber": "+1234567890"
|
|
232
|
+
}
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
## API Documentation
|
|
236
|
+
|
|
237
|
+
Visit `/docs` to see the interactive OpenAPI documentation.
|
|
238
|
+
|
|
239
|
+
## Authentication
|
|
240
|
+
|
|
241
|
+
All API endpoints require authentication using an API key. Include the key in the request header:
|
|
242
|
+
|
|
243
|
+
```http
|
|
244
|
+
X-API-Key: your_api_key
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
Or as a Bearer token:
|
|
248
|
+
|
|
249
|
+
```http
|
|
250
|
+
Authorization: Bearer your_api_key
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
## Configuration
|
|
254
|
+
|
|
255
|
+
### Environment Variables
|
|
256
|
+
|
|
257
|
+
| Variable | Description | Default |
|
|
258
|
+
|----------|-------------|---------|
|
|
259
|
+
| `AWS_ACCESS_KEY_ID` | AWS Access Key ID | Required |
|
|
260
|
+
| `AWS_SECRET_ACCESS_KEY` | AWS Secret Access Key | Required |
|
|
261
|
+
| `AWS_REGION` | AWS Region | `us-east-1` |
|
|
262
|
+
| `API_KEY` | API Key for authentication | Required |
|
|
263
|
+
| `SMS_SENDER_ID` | Default SMS sender ID | `Verify` |
|
|
264
|
+
|
|
265
|
+
### Rate Limiting
|
|
266
|
+
|
|
267
|
+
- **Window**: 15 minutes
|
|
268
|
+
- **Max Requests**: 100 per IP
|
|
269
|
+
- **Headers**: Standard rate limit headers included
|
|
270
|
+
|
|
271
|
+
### Phone Number Validation Options
|
|
272
|
+
|
|
273
|
+
The API supports two methods for phone number validation and VoIP detection:
|
|
274
|
+
|
|
275
|
+
#### 1. External API Method (Default)
|
|
276
|
+
- Uses external phone lookup service for VoIP detection
|
|
277
|
+
- Basic phone number formatting and validation
|
|
278
|
+
- Requires internet access for VoIP checks
|
|
279
|
+
- More accurate VoIP detection
|
|
280
|
+
|
|
281
|
+
#### 2. libphonenumber-js Method
|
|
282
|
+
- Uses Google's libphonenumber library for local analysis
|
|
283
|
+
- Advanced phone number formatting and validation
|
|
284
|
+
- No external API calls required
|
|
285
|
+
- Heuristic-based VoIP detection
|
|
286
|
+
- Smaller bundle size (145 kB vs 550 kB for full libphonenumber)
|
|
287
|
+
- Better international number support
|
|
288
|
+
|
|
289
|
+
#### Usage Examples
|
|
290
|
+
|
|
291
|
+
```javascript
|
|
292
|
+
// Using libphonenumber-js for VoIP detection with full metadata
|
|
293
|
+
const result = await verifyPhone({
|
|
294
|
+
phoneNumber: '+1-800-555-0123',
|
|
295
|
+
code: '123456',
|
|
296
|
+
blockVoip: true,
|
|
297
|
+
voipDetectionMethod: 'libphonenumber', // Use local analysis
|
|
298
|
+
useLibPhoneNumber: true, // Use libphonenumber-js for formatting/validation
|
|
299
|
+
metadataType: 'full' // Use full metadata (140KB) for better phone type detection
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
// Using libphonenumber-js for formatting only
|
|
303
|
+
const result = await verifyPhone({
|
|
304
|
+
phoneNumber: '555-123-4567', // US number without country code
|
|
305
|
+
code: '789012',
|
|
306
|
+
useLibPhoneNumber: true, // Use libphonenumber-js for formatting/validation
|
|
307
|
+
blockVoip: false // Don't block VoIP numbers
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
// Traditional approach (external API)
|
|
311
|
+
const result = await verifyPhone({
|
|
312
|
+
phoneNumber: '+44 20 7946 0958', // UK number
|
|
313
|
+
code: 'ABCDEF',
|
|
314
|
+
blockVoip: true,
|
|
315
|
+
voipDetectionMethod: 'api', // Use external API (default)
|
|
316
|
+
useLibPhoneNumber: false // Use basic formatting/validation
|
|
317
|
+
});
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
#### VoIP Detection Methods
|
|
321
|
+
|
|
322
|
+
**External API (`voipDetectionMethod: 'api'`):**
|
|
323
|
+
- Checks carrier information from phone lookup service
|
|
324
|
+
- Identifies Bandwidth, VoIP, and mobile line types
|
|
325
|
+
- More accurate but requires external API calls
|
|
326
|
+
|
|
327
|
+
**libphonenumber-js (`voipDetectionMethod: 'libphonenumber'`):**
|
|
328
|
+
- Analyzes phone number patterns and structure
|
|
329
|
+
- Identifies common VoIP area codes (800, 888, 877, etc.)
|
|
330
|
+
- Detects non-geographic numbers
|
|
331
|
+
- Recognizes patterns like repeated digits, sequential numbers
|
|
332
|
+
- Heuristic-based approach for common VoIP characteristics
|
|
333
|
+
|
|
334
|
+
#### Metadata Options
|
|
335
|
+
|
|
336
|
+
**Minimal Metadata (`metadataType: 'minimal'` - Default, 75KB):**
|
|
337
|
+
- Uses pattern-based heuristics for VoIP detection
|
|
338
|
+
- Smaller bundle size
|
|
339
|
+
- Works with all countries
|
|
340
|
+
- Less accurate but faster
|
|
341
|
+
|
|
342
|
+
**Full Metadata (`metadataType: 'full' - 140KB):**
|
|
343
|
+
- Uses phone number type detection (MOBILE, FIXED_LINE, VOIP, etc.)
|
|
344
|
+
- More accurate VoIP detection
|
|
345
|
+
- Larger bundle size (65KB additional)
|
|
346
|
+
- Matches Google's libphonenumber behavior
|
|
347
|
+
- Phone number types: MOBILE, FIXED_LINE, VOIP, PREMIUM_RATE, TOLL_FREE, SHARED_COST
|
|
348
|
+
|
|
349
|
+
## Development
|
|
350
|
+
|
|
351
|
+
### Running Tests
|
|
352
|
+
|
|
353
|
+
```bash
|
|
354
|
+
npm test
|
|
355
|
+
npm run test:run
|
|
356
|
+
npm run test:ui
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
### Local Development
|
|
360
|
+
|
|
361
|
+
```bash
|
|
362
|
+
npm run dev
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
### Deployment
|
|
366
|
+
|
|
367
|
+
```bash
|
|
368
|
+
# Deploy to production
|
|
369
|
+
npm run deploy
|
|
370
|
+
|
|
371
|
+
# Deploy to staging
|
|
372
|
+
npm run deploy:staging
|
|
373
|
+
|
|
374
|
+
# Deploy to specific environment
|
|
375
|
+
npm run deploy:production
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
## Example Usage
|
|
379
|
+
|
|
380
|
+
### JavaScript/Node.js
|
|
381
|
+
|
|
382
|
+
```javascript
|
|
383
|
+
// Send verification code
|
|
384
|
+
const response = await fetch('https://your-api.workers.dev/api/send', {
|
|
385
|
+
method: 'POST',
|
|
386
|
+
headers: {
|
|
387
|
+
'Content-Type': 'application/json',
|
|
388
|
+
'X-API-Key': 'your_api_key'
|
|
389
|
+
},
|
|
390
|
+
body: JSON.stringify({
|
|
391
|
+
phoneNumber: '+1234567890',
|
|
392
|
+
blockVoip: true,
|
|
393
|
+
senderId: 'MyApp'
|
|
394
|
+
})
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
const result = await response.json();
|
|
398
|
+
console.log(result);
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
### cURL
|
|
402
|
+
|
|
403
|
+
```bash
|
|
404
|
+
# Send verification code
|
|
405
|
+
curl -X POST https://your-api.workers.dev/api/send \
|
|
406
|
+
-H "Content-Type: application/json" \
|
|
407
|
+
-H "X-API-Key: your_api_key" \
|
|
408
|
+
-d '{
|
|
409
|
+
"phoneNumber": "+1234567890",
|
|
410
|
+
"blockVoip": true,
|
|
411
|
+
"senderId": "MyApp"
|
|
412
|
+
}'
|
|
413
|
+
|
|
414
|
+
# Verify code
|
|
415
|
+
curl -X POST https://your-api.workers.dev/api/verify \
|
|
416
|
+
-H "Content-Type: application/json" \
|
|
417
|
+
-H "X-API-Key: your_api_key" \
|
|
418
|
+
-d '{
|
|
419
|
+
"phoneNumber": "+1234567890",
|
|
420
|
+
"code": "123456"
|
|
421
|
+
}'
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
## Error Handling
|
|
425
|
+
|
|
426
|
+
The API returns consistent error responses:
|
|
427
|
+
|
|
428
|
+
```json
|
|
429
|
+
{
|
|
430
|
+
"success": false,
|
|
431
|
+
"error": "Error message",
|
|
432
|
+
"details": "Additional error details"
|
|
433
|
+
}
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
Common HTTP status codes:
|
|
437
|
+
- `200`: Success
|
|
438
|
+
- `400`: Bad request (invalid input)
|
|
439
|
+
- `401`: Unauthorized (invalid API key)
|
|
440
|
+
- `429`: Too many requests (rate limited)
|
|
441
|
+
- `500`: Internal server error
|
|
442
|
+
|
|
443
|
+
## Security Features
|
|
444
|
+
|
|
445
|
+
- ✅ API key authentication
|
|
446
|
+
- ✅ Rate limiting
|
|
447
|
+
- ✅ CORS protection
|
|
448
|
+
- ✅ Secure headers
|
|
449
|
+
- ✅ Input validation
|
|
450
|
+
- ✅ Error sanitization
|
|
451
|
+
- ✅ VoIP number blocking (optional)
|
|
452
|
+
|
|
453
|
+
## Architecture
|
|
454
|
+
|
|
455
|
+
```
|
|
456
|
+
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
|
|
457
|
+
│ Client App │───▶│ Hono Server │───▶│ AWS SNS │
|
|
458
|
+
│ │ │ │ │ │
|
|
459
|
+
│ - Web App │ │ - Rate Limiting │ │ - SMS Delivery │
|
|
460
|
+
│ - Mobile App │ │ - Auth │ │ - Message ID │
|
|
461
|
+
│ - API Client │ │ - Validation │ │ - Error Handling│
|
|
462
|
+
└─────────────────┘ └─────────────────┘ └─────────────────┘
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
## Contributing
|
|
466
|
+
|
|
467
|
+
1. Fork the repository
|
|
468
|
+
2. Create a feature branch
|
|
469
|
+
3. Make your changes
|
|
470
|
+
4. Add tests
|
|
471
|
+
5. Submit a pull request
|
|
472
|
+
|
|
473
|
+
## License
|
|
474
|
+
|
|
475
|
+
MIT License - see LICENSE file for details.
|