mygensite 1.1.0 → 1.3.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.en.md +114 -1
- package/README.ko.md +115 -1
- package/README.md +125 -2
- package/bin/lt.js +169 -110
- package/lib/Tunnel.js +21 -0
- package/lib/deploy.js +69 -1
- package/lib/validate.js +123 -0
- package/localtunnel.js +3 -0
- package/package.json +1 -1
package/README.en.md
CHANGED
|
@@ -154,6 +154,70 @@ await tunnel.updateAccess({ mode: 'ip_only', allowed_ips: ['1.2.3.0/24'] });
|
|
|
154
154
|
await tunnel.extendTTL(3600);
|
|
155
155
|
```
|
|
156
156
|
|
|
157
|
+
## Constraints
|
|
158
|
+
|
|
159
|
+
### Slug (subdomain)
|
|
160
|
+
|
|
161
|
+
- 3–63 characters, lowercase letters (`a-z`), numbers (`0-9`), and hyphens (`-`) only
|
|
162
|
+
- Must start and end with a letter or number (not a hyphen)
|
|
163
|
+
- Reserved words cannot be used: `www`, `api`, `dashboard`, `admin`, `mail`, `ftp`, `static`, `docs`, `status`, `health`, `internal`, `tunnel`, `app`, `web`
|
|
164
|
+
- A slug used as a tunnel cannot be reused for static deployment (and vice versa). Delete the existing service first.
|
|
165
|
+
|
|
166
|
+
```
|
|
167
|
+
OK: my-app, demo-v2, test-123
|
|
168
|
+
BAD: My-App, -dash, ab, a_b, my--app..com
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### File Paths (static deploy)
|
|
172
|
+
|
|
173
|
+
- Allowed characters per segment: letters, numbers, hyphens (`-`), underscores (`_`), dots (`.`), spaces
|
|
174
|
+
- Forward slashes (`/`) for directory nesting
|
|
175
|
+
- Max total path length: 1024 characters. Max segment length: 255 characters.
|
|
176
|
+
- Path traversal (`..`, `.`) is rejected
|
|
177
|
+
- Hidden files (names starting with `.`) are rejected (e.g. `.env`, `.git`)
|
|
178
|
+
- No leading spaces, backslashes, or control characters
|
|
179
|
+
- Total upload size limit: **50 MB** per deployment
|
|
180
|
+
|
|
181
|
+
```
|
|
182
|
+
OK: index.html, assets/style.css, img/logo 2.png, deep/nested/file.js
|
|
183
|
+
BAD: ../secret.txt, .env, file\name.html
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### Static File Serving Behavior
|
|
187
|
+
|
|
188
|
+
- `/` serves `index.html`
|
|
189
|
+
- `/about/` serves `about/index.html`
|
|
190
|
+
- `/about` (no trailing slash) tries the literal file first, then falls back to `about/index.html`
|
|
191
|
+
- Content-Type is determined by file extension (e.g. `.css` → `text/css`, `.js` → `application/javascript`)
|
|
192
|
+
- Responses include `Cache-Control: public, max-age=60`
|
|
193
|
+
|
|
194
|
+
### TTL
|
|
195
|
+
|
|
196
|
+
- Minimum: 60 seconds (1 minute)
|
|
197
|
+
- Maximum: 86,400 seconds (24 hours)
|
|
198
|
+
- Default: 3,600 seconds (1 hour)
|
|
199
|
+
- Extending TTL resets the timer (created_at becomes now)
|
|
200
|
+
|
|
201
|
+
### Client-Side Validation
|
|
202
|
+
|
|
203
|
+
The library validates inputs before making API calls, throwing an error immediately if values are invalid:
|
|
204
|
+
|
|
205
|
+
```js
|
|
206
|
+
// Throws at construction time — no API call made
|
|
207
|
+
const tunnel = await mygensite({ port: 3000, subdomain: 'INVALID' });
|
|
208
|
+
// Error: Slug must be lowercase alphanumeric and hyphens...
|
|
209
|
+
|
|
210
|
+
// Use validators directly for custom checks
|
|
211
|
+
const { validate } = require('mygensite');
|
|
212
|
+
|
|
213
|
+
validate.validateSlug('my-app'); // { valid: true }
|
|
214
|
+
validate.validateSlug('AB'); // { valid: false, error: 'Slug must be 3-63 characters' }
|
|
215
|
+
validate.validateFilePath('assets/x.js');// { valid: true, cleaned: 'assets/x.js' }
|
|
216
|
+
validate.validateFilePath('../etc'); // { valid: false, error: 'Path traversal...' }
|
|
217
|
+
validate.validateTTL(30); // { valid: false, error: 'TTL must be...' }
|
|
218
|
+
validate.validateAccessMode('public'); // { valid: true }
|
|
219
|
+
```
|
|
220
|
+
|
|
157
221
|
## Error Codes
|
|
158
222
|
|
|
159
223
|
### Tunnel creation errors
|
|
@@ -237,11 +301,32 @@ const site = await mygensite.deploy({
|
|
|
237
301
|
access: 'public',
|
|
238
302
|
files: [
|
|
239
303
|
{ name: 'index.html', content: '<h1>Hello World</h1>' },
|
|
240
|
-
{ name: 'style.css', content: 'body { font-family: sans-serif; }' },
|
|
304
|
+
{ name: 'assets/style.css', content: 'body { font-family: sans-serif; }' },
|
|
241
305
|
],
|
|
242
306
|
});
|
|
243
307
|
```
|
|
244
308
|
|
|
309
|
+
#### Deploy with curl
|
|
310
|
+
|
|
311
|
+
Multipart `filename` strips directory paths (e.g. `assets/style.css` becomes `style.css`). Use the `filepaths` JSON field to preserve directory structure:
|
|
312
|
+
|
|
313
|
+
```bash
|
|
314
|
+
# Flat files (no subdirectories) — filepaths not needed
|
|
315
|
+
curl -X POST https://mygen.site/api/deploy \
|
|
316
|
+
-F slug=demo -F access='{"mode":"public"}' \
|
|
317
|
+
-F files=@index.html -F files=@style.css
|
|
318
|
+
|
|
319
|
+
# With subdirectories — filepaths required
|
|
320
|
+
curl -X POST https://mygen.site/api/deploy \
|
|
321
|
+
-F slug=demo -F access='{"mode":"public"}' \
|
|
322
|
+
-F 'filepaths=["index.html","assets/style.css","assets/js/app.js"]' \
|
|
323
|
+
-F files=@index.html \
|
|
324
|
+
-F files=@assets/style.css \
|
|
325
|
+
-F files=@assets/js/app.js
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
The `filepaths` field is a JSON array where each element corresponds to the `files` field in order. The server uses these paths instead of the multipart filename.
|
|
329
|
+
|
|
245
330
|
### Site instance
|
|
246
331
|
|
|
247
332
|
The returned object contains the deployment result plus convenience methods:
|
|
@@ -265,6 +350,34 @@ The returned object contains the deployment result plus convenience methods:
|
|
|
265
350
|
| `redeploy(directory)` | directory path (string) | Upload new files, replacing all existing files. Returns a Promise. |
|
|
266
351
|
| `delete(purge?)` | purge (boolean) | Delete the site. `false` = soft delete (files kept), `true` = purge S3 files. Returns a Promise. |
|
|
267
352
|
|
|
353
|
+
### mygensite.manage(options)
|
|
354
|
+
|
|
355
|
+
Create a management handle for an existing service when you already have the `slug` and `admin_token` (e.g. saved from a previous deploy). **Do not redeploy just to get a management object** — use this instead.
|
|
356
|
+
|
|
357
|
+
```js
|
|
358
|
+
const mygensite = require('mygensite');
|
|
359
|
+
|
|
360
|
+
const site = mygensite.manage({
|
|
361
|
+
slug: 'demo',
|
|
362
|
+
admin_token: 'tok_xxx', // from the original deploy/tunnel response
|
|
363
|
+
host: 'https://mygen.site', // optional
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
// Same methods as deploy result
|
|
367
|
+
await site.updateAccess({ mode: 'public' });
|
|
368
|
+
await site.extendTTL(86400);
|
|
369
|
+
await site.redeploy('./dist-v2');
|
|
370
|
+
await site.delete();
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
#### Options
|
|
374
|
+
|
|
375
|
+
| option | type | required | default | description |
|
|
376
|
+
| --- | --- | --- | --- | --- |
|
|
377
|
+
| `slug` | string | yes | — | The service slug |
|
|
378
|
+
| `admin_token` | string | yes | — | The admin token from the original deploy/tunnel response |
|
|
379
|
+
| `host` | string | | `https://mygen.site` | Server URL |
|
|
380
|
+
|
|
268
381
|
### Deploy management examples
|
|
269
382
|
|
|
270
383
|
```js
|
package/README.ko.md
CHANGED
|
@@ -154,6 +154,70 @@ await tunnel.updateAccess({ mode: 'ip_only', allowed_ips: ['1.2.3.0/24'] });
|
|
|
154
154
|
await tunnel.extendTTL(3600);
|
|
155
155
|
```
|
|
156
156
|
|
|
157
|
+
## 제약사항
|
|
158
|
+
|
|
159
|
+
### Slug (서브도메인)
|
|
160
|
+
|
|
161
|
+
- 3–63자, 소문자 영문(`a-z`), 숫자(`0-9`), 하이픈(`-`)만 허용
|
|
162
|
+
- 영문자 또는 숫자로 시작/끝나야 함 (하이픈으로 시작/끝 불가)
|
|
163
|
+
- 예약어 사용 불가: `www`, `api`, `dashboard`, `admin`, `mail`, `ftp`, `static`, `docs`, `status`, `health`, `internal`, `tunnel`, `app`, `web`
|
|
164
|
+
- 터널로 사용 중인 slug에 정적 배포 불가 (반대도 동일). 기존 서비스를 먼저 삭제해야 함.
|
|
165
|
+
|
|
166
|
+
```
|
|
167
|
+
OK: my-app, demo-v2, test-123
|
|
168
|
+
BAD: My-App, -dash, ab, a_b, my--app..com
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### 파일 경로 (정적 배포)
|
|
172
|
+
|
|
173
|
+
- 세그먼트별 허용 문자: 영문, 숫자, 하이픈(`-`), 밑줄(`_`), 점(`.`), 공백
|
|
174
|
+
- 슬래시(`/`)로 디렉토리 구조 표현
|
|
175
|
+
- 최대 경로 길이: 1024자, 세그먼트 최대: 255자
|
|
176
|
+
- 경로 탈출(`..`, `.`) 거부
|
|
177
|
+
- 숨김 파일(`.`으로 시작하는 이름) 거부 (예: `.env`, `.git`)
|
|
178
|
+
- 선행 공백, 백슬래시, 제어 문자 불가
|
|
179
|
+
- 전체 업로드 크기 제한: **50 MB**
|
|
180
|
+
|
|
181
|
+
```
|
|
182
|
+
OK: index.html, assets/style.css, img/logo 2.png, deep/nested/file.js
|
|
183
|
+
BAD: ../secret.txt, .env, file\name.html
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### 정적 파일 서빙 동작
|
|
187
|
+
|
|
188
|
+
- `/` → `index.html` 제공
|
|
189
|
+
- `/about/` → `about/index.html` 제공
|
|
190
|
+
- `/about` (슬래시 없이) → 해당 파일 먼저 시도, 없으면 `about/index.html`로 폴백
|
|
191
|
+
- Content-Type은 확장자로 결정 (`.css` → `text/css`, `.js` → `application/javascript`)
|
|
192
|
+
- 응답에 `Cache-Control: public, max-age=60` 포함
|
|
193
|
+
|
|
194
|
+
### TTL
|
|
195
|
+
|
|
196
|
+
- 최소: 60초 (1분)
|
|
197
|
+
- 최대: 86,400초 (24시간)
|
|
198
|
+
- 기본: 3,600초 (1시간)
|
|
199
|
+
- TTL 연장 시 타이머 리셋 (created_at이 현재 시각으로 변경)
|
|
200
|
+
|
|
201
|
+
### 클라이언트 사전 검증
|
|
202
|
+
|
|
203
|
+
라이브러리가 API 호출 전에 입력값을 검증하여 잘못된 값은 즉시 에러를 발생시킵니다:
|
|
204
|
+
|
|
205
|
+
```js
|
|
206
|
+
// 생성 시점에 즉시 에러 — API 호출 없음
|
|
207
|
+
const tunnel = await mygensite({ port: 3000, subdomain: 'INVALID' });
|
|
208
|
+
// Error: Slug must be lowercase alphanumeric and hyphens...
|
|
209
|
+
|
|
210
|
+
// 검증 함수 직접 사용
|
|
211
|
+
const { validate } = require('mygensite');
|
|
212
|
+
|
|
213
|
+
validate.validateSlug('my-app'); // { valid: true }
|
|
214
|
+
validate.validateSlug('AB'); // { valid: false, error: 'Slug must be 3-63 characters' }
|
|
215
|
+
validate.validateFilePath('assets/x.js');// { valid: true, cleaned: 'assets/x.js' }
|
|
216
|
+
validate.validateFilePath('../etc'); // { valid: false, error: '경로 탈출 불가...' }
|
|
217
|
+
validate.validateTTL(30); // { valid: false, error: 'TTL 범위 초과...' }
|
|
218
|
+
validate.validateAccessMode('public'); // { valid: true }
|
|
219
|
+
```
|
|
220
|
+
|
|
157
221
|
## 에러 코드
|
|
158
222
|
|
|
159
223
|
### 터널 생성 에러
|
|
@@ -237,11 +301,32 @@ const site = await mygensite.deploy({
|
|
|
237
301
|
access: 'public',
|
|
238
302
|
files: [
|
|
239
303
|
{ name: 'index.html', content: '<h1>Hello World</h1>' },
|
|
240
|
-
{ name: 'style.css', content: 'body { font-family: sans-serif; }' },
|
|
304
|
+
{ name: 'assets/style.css', content: 'body { font-family: sans-serif; }' },
|
|
241
305
|
],
|
|
242
306
|
});
|
|
243
307
|
```
|
|
244
308
|
|
|
309
|
+
#### curl로 배포
|
|
310
|
+
|
|
311
|
+
Multipart의 `filename`은 디렉토리 경로를 제거합니다 (예: `assets/style.css` → `style.css`). 서브디렉토리가 있으면 `filepaths` JSON 필드를 사용하세요:
|
|
312
|
+
|
|
313
|
+
```bash
|
|
314
|
+
# 플랫 파일 (서브디렉토리 없음) - filepaths 불필요
|
|
315
|
+
curl -X POST https://mygen.site/api/deploy \
|
|
316
|
+
-F slug=demo -F access='{"mode":"public"}' \
|
|
317
|
+
-F files=@index.html -F files=@style.css
|
|
318
|
+
|
|
319
|
+
# 서브디렉토리 있음 - filepaths 필수
|
|
320
|
+
curl -X POST https://mygen.site/api/deploy \
|
|
321
|
+
-F slug=demo -F access='{"mode":"public"}' \
|
|
322
|
+
-F 'filepaths=["index.html","assets/style.css","assets/js/app.js"]' \
|
|
323
|
+
-F files=@index.html \
|
|
324
|
+
-F files=@assets/style.css \
|
|
325
|
+
-F files=@assets/js/app.js
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
`filepaths`는 JSON 배열로, 각 원소가 `files` 필드와 순서대로 대응됩니다. 서버는 multipart filename 대신 이 경로를 사용합니다.
|
|
329
|
+
|
|
245
330
|
### Site 인스턴스
|
|
246
331
|
|
|
247
332
|
반환 객체에 배포 결과와 편의 메서드가 포함됩니다:
|
|
@@ -265,6 +350,35 @@ const site = await mygensite.deploy({
|
|
|
265
350
|
| `redeploy(directory)` | 디렉토리 경로 (string) | 새 파일로 교체 업로드. Promise 반환. |
|
|
266
351
|
| `delete(purge?)` | purge (boolean) | 사이트 삭제. `false` = 소프트 삭제 (파일 유지), `true` = S3 파일까지 삭제. Promise 반환. |
|
|
267
352
|
|
|
353
|
+
### mygensite.manage(options)
|
|
354
|
+
|
|
355
|
+
기존 서비스의 `slug`과 `admin_token`이 있을 때 관리 핸들을 생성합니다 (예: 이전 배포에서 저장해둔 값).
|
|
356
|
+
**관리 객체를 얻기 위해 재배포하지 마세요** — 이 함수를 사용하세요.
|
|
357
|
+
|
|
358
|
+
```js
|
|
359
|
+
const mygensite = require('mygensite');
|
|
360
|
+
|
|
361
|
+
const site = mygensite.manage({
|
|
362
|
+
slug: 'demo',
|
|
363
|
+
admin_token: 'tok_xxx', // 원래 배포/터널 생성 시 반환된 값
|
|
364
|
+
host: 'https://mygen.site', // 선택
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
// deploy 결과와 동일한 메서드 사용 가능
|
|
368
|
+
await site.updateAccess({ mode: 'public' });
|
|
369
|
+
await site.extendTTL(86400);
|
|
370
|
+
await site.redeploy('./dist-v2');
|
|
371
|
+
await site.delete();
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
#### 옵션
|
|
375
|
+
|
|
376
|
+
| 옵션 | 타입 | 필수 | 기본값 | 설명 |
|
|
377
|
+
| --- | --- | --- | --- | --- |
|
|
378
|
+
| `slug` | string | 예 | — | 서비스 slug |
|
|
379
|
+
| `admin_token` | string | 예 | — | 원래 배포/터널 생성 시 반환된 admin token |
|
|
380
|
+
| `host` | string | | `https://mygen.site` | 서버 URL |
|
|
381
|
+
|
|
268
382
|
### 배포 관리 예제
|
|
269
383
|
|
|
270
384
|
```js
|
package/README.md
CHANGED
|
@@ -66,6 +66,27 @@ await site.delete(); // soft delete
|
|
|
66
66
|
await site.delete(true); // purge (delete S3 files too)
|
|
67
67
|
```
|
|
68
68
|
|
|
69
|
+
## Manage (existing service)
|
|
70
|
+
|
|
71
|
+
Use `manage()` when you already have the `slug` and `admin_token` from a previous deploy or tunnel.
|
|
72
|
+
**Do not redeploy just to get a management object** — use this instead.
|
|
73
|
+
|
|
74
|
+
```js
|
|
75
|
+
const mygensite = require('mygensite');
|
|
76
|
+
|
|
77
|
+
const site = mygensite.manage({
|
|
78
|
+
slug: 'demo',
|
|
79
|
+
admin_token: 'tok_xxx', // from the original deploy response
|
|
80
|
+
host: 'https://mygen.site', // optional
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// Same methods as deploy result
|
|
84
|
+
await site.updateAccess({ mode: 'public' });
|
|
85
|
+
await site.extendTTL(86400);
|
|
86
|
+
await site.redeploy('./dist-v2');
|
|
87
|
+
await site.delete();
|
|
88
|
+
```
|
|
89
|
+
|
|
69
90
|
## Access Modes
|
|
70
91
|
|
|
71
92
|
| mode | behavior |
|
|
@@ -75,6 +96,48 @@ await site.delete(true); // purge (delete S3 files too)
|
|
|
75
96
|
| `ip_only` | allowed_ips only |
|
|
76
97
|
| `both` | allowed_ips + password (default) |
|
|
77
98
|
|
|
99
|
+
## Constraints
|
|
100
|
+
|
|
101
|
+
### Slug (subdomain)
|
|
102
|
+
|
|
103
|
+
- 3–63 characters, lowercase letters, numbers, and hyphens only
|
|
104
|
+
- Must start and end with a letter or number (not a hyphen)
|
|
105
|
+
- Reserved: `www`, `api`, `dashboard`, `admin`, `docs`, `status`, `health`, etc.
|
|
106
|
+
- A slug used as a tunnel cannot be reused for static deploy (and vice versa)
|
|
107
|
+
|
|
108
|
+
### File Paths (static deploy)
|
|
109
|
+
|
|
110
|
+
- Allowed: letters, numbers, `-`, `_`, `.`, spaces, `/` for directories
|
|
111
|
+
- Max path: 1024 chars, max segment: 255 chars
|
|
112
|
+
- No `..`, no hidden files (`.env`), no backslashes
|
|
113
|
+
- Total upload limit: **50 MB**
|
|
114
|
+
|
|
115
|
+
### Static File Serving
|
|
116
|
+
|
|
117
|
+
- `/` → `index.html`
|
|
118
|
+
- `/about/` → `about/index.html`
|
|
119
|
+
- `/about` (no slash) → falls back to `about/index.html`
|
|
120
|
+
- Content-Type set by extension (`.css` → `text/css`, `.js` → `application/javascript`)
|
|
121
|
+
|
|
122
|
+
### Client-Side Validation
|
|
123
|
+
|
|
124
|
+
The library validates slug, file paths, TTL, and access mode before making API calls:
|
|
125
|
+
|
|
126
|
+
```js
|
|
127
|
+
const mygensite = require('mygensite');
|
|
128
|
+
|
|
129
|
+
// Throws immediately if slug is invalid
|
|
130
|
+
const tunnel = await mygensite({ port: 3000, subdomain: 'INVALID' });
|
|
131
|
+
// Error: Slug must be lowercase alphanumeric and hyphens...
|
|
132
|
+
|
|
133
|
+
// Use validators directly
|
|
134
|
+
const { validate } = mygensite;
|
|
135
|
+
validate.validateSlug('my-app'); // { valid: true }
|
|
136
|
+
validate.validateSlug('AB'); // { valid: false, error: '...' }
|
|
137
|
+
validate.validateFilePath('../etc'); // { valid: false, error: '...' }
|
|
138
|
+
validate.validateTTL(30); // { valid: false, error: '...' }
|
|
139
|
+
```
|
|
140
|
+
|
|
78
141
|
## Error Codes
|
|
79
142
|
|
|
80
143
|
| status | error | when | fix |
|
|
@@ -93,9 +156,69 @@ await site.delete(true); // purge (delete S3 files too)
|
|
|
93
156
|
|
|
94
157
|
```bash
|
|
95
158
|
# Tunnel
|
|
96
|
-
mygensite --port 3000 --subdomain my-app --access password --password secret --ttl 7200
|
|
159
|
+
mygensite --port 3000 --subdomain my-app --access password --password 'secret' --ttl 7200
|
|
97
160
|
|
|
98
|
-
# Deploy
|
|
161
|
+
# Deploy
|
|
162
|
+
mygensite deploy --directory ./dist --subdomain demo --access public --ttl 86400
|
|
163
|
+
|
|
164
|
+
# Deploy with password
|
|
165
|
+
mygensite deploy -d ./dist -s private-demo --access password --password 'mypass'
|
|
166
|
+
|
|
167
|
+
# Redeploy (reuse admin_token)
|
|
168
|
+
mygensite deploy -d ./dist-v2 -s demo --admin-token tok_xxx
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
## curl Deploy
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
# Simple (flat files)
|
|
175
|
+
curl -X POST https://mygen.site/api/deploy \
|
|
176
|
+
-F slug=demo -F access='{"mode":"public"}' \
|
|
177
|
+
-F files=@index.html -F files=@style.css
|
|
178
|
+
|
|
179
|
+
# With subdirectories — use filepaths to preserve directory structure
|
|
180
|
+
curl -X POST https://mygen.site/api/deploy \
|
|
181
|
+
-F slug=demo -F access='{"mode":"public"}' \
|
|
182
|
+
-F 'filepaths=["index.html","assets/style.css","assets/js/app.js"]' \
|
|
183
|
+
-F files=@index.html \
|
|
184
|
+
-F files=@assets/style.css \
|
|
185
|
+
-F files=@assets/js/app.js
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
> **Note:** Multipart `filename` strips directory paths. The `filepaths` JSON field tells the server the correct path for each file, in order.
|
|
189
|
+
|
|
190
|
+
## Passwords with Special Characters
|
|
191
|
+
|
|
192
|
+
When using the CLI or curl, passwords with special characters (like `!`, `$`, `&`, `\`) may be modified by your shell before reaching the program.
|
|
193
|
+
|
|
194
|
+
**Use single quotes** to prevent shell interpretation:
|
|
195
|
+
|
|
196
|
+
```bash
|
|
197
|
+
# CORRECT — single quotes preserve special characters
|
|
198
|
+
mygensite --port 3000 --password 'my!p@ss$word'
|
|
199
|
+
|
|
200
|
+
# WRONG — double quotes: bash expands ! and $
|
|
201
|
+
mygensite --port 3000 --password "my!p@ss$word"
|
|
202
|
+
|
|
203
|
+
# WRONG — unquoted: shell interprets special chars
|
|
204
|
+
mygensite --port 3000 --password my!p@ss$word
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
When using curl:
|
|
208
|
+
|
|
209
|
+
```bash
|
|
210
|
+
# CORRECT
|
|
211
|
+
curl 'http://mygen.site/api/tunnels/my-app?password=my!pass'
|
|
212
|
+
|
|
213
|
+
# WRONG — double quotes let bash expand !
|
|
214
|
+
curl "http://mygen.site/api/tunnels/my-app?password=my!pass"
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
When using the **Node.js API**, no escaping is needed — JavaScript string literals preserve all characters:
|
|
218
|
+
|
|
219
|
+
```js
|
|
220
|
+
const tunnel = await mygensite({ port: 3000, password: 'my!p@ss$word' });
|
|
221
|
+
// password is stored and returned exactly as provided
|
|
99
222
|
```
|
|
100
223
|
|
|
101
224
|
## Documentation
|
package/bin/lt.js
CHANGED
|
@@ -5,125 +5,184 @@ const openurl = require('openurl');
|
|
|
5
5
|
const yargs = require('yargs');
|
|
6
6
|
|
|
7
7
|
const localtunnel = require('../localtunnel');
|
|
8
|
+
const deploy = require('../lib/deploy');
|
|
8
9
|
const { version } = require('../package');
|
|
9
10
|
|
|
10
|
-
|
|
11
|
-
.usage('Usage: mygensite
|
|
11
|
+
yargs
|
|
12
|
+
.usage('Usage: mygensite <command> [options]')
|
|
12
13
|
.env(true)
|
|
13
|
-
.
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
.option('owner-email', {
|
|
59
|
-
describe: 'Owner email for dashboard management',
|
|
60
|
-
})
|
|
61
|
-
.option('ttl', {
|
|
62
|
-
describe: 'Tunnel TTL in seconds (60-86400)',
|
|
63
|
-
type: 'number',
|
|
64
|
-
})
|
|
65
|
-
.require('port')
|
|
66
|
-
.boolean('local-https')
|
|
67
|
-
.boolean('allow-invalid-cert')
|
|
68
|
-
.boolean('print-requests')
|
|
69
|
-
.help('help', 'Show this help and exit')
|
|
70
|
-
.version(version);
|
|
14
|
+
.command('deploy', 'Deploy a static site', (y) => {
|
|
15
|
+
return y
|
|
16
|
+
.option('directory', {
|
|
17
|
+
alias: 'd',
|
|
18
|
+
describe: 'Directory to deploy',
|
|
19
|
+
type: 'string',
|
|
20
|
+
demandOption: true,
|
|
21
|
+
})
|
|
22
|
+
.option('host', {
|
|
23
|
+
alias: 'h',
|
|
24
|
+
describe: 'Server host',
|
|
25
|
+
default: 'https://mygen.site',
|
|
26
|
+
})
|
|
27
|
+
.option('subdomain', {
|
|
28
|
+
alias: 's',
|
|
29
|
+
describe: 'Subdomain (slug)',
|
|
30
|
+
})
|
|
31
|
+
.option('access', {
|
|
32
|
+
describe: 'Access mode: public, password, ip_only, both',
|
|
33
|
+
})
|
|
34
|
+
.option('password', {
|
|
35
|
+
describe: 'Password for access control',
|
|
36
|
+
})
|
|
37
|
+
.option('owner-email', {
|
|
38
|
+
describe: 'Owner email for dashboard',
|
|
39
|
+
})
|
|
40
|
+
.option('ttl', {
|
|
41
|
+
describe: 'TTL in seconds (60-86400)',
|
|
42
|
+
type: 'number',
|
|
43
|
+
})
|
|
44
|
+
.option('admin-token', {
|
|
45
|
+
describe: 'Admin token for redeployment',
|
|
46
|
+
});
|
|
47
|
+
}, async (argv) => {
|
|
48
|
+
try {
|
|
49
|
+
const result = await deploy({
|
|
50
|
+
host: argv.host,
|
|
51
|
+
subdomain: argv.subdomain,
|
|
52
|
+
directory: argv.directory,
|
|
53
|
+
access: argv.access,
|
|
54
|
+
password: argv.password,
|
|
55
|
+
owner_email: argv.ownerEmail,
|
|
56
|
+
ttl: argv.ttl,
|
|
57
|
+
admin_token: argv.adminToken,
|
|
58
|
+
});
|
|
71
59
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
}
|
|
60
|
+
console.log('your url is: %s', result.url);
|
|
61
|
+
console.log('your slug is: %s', result.slug);
|
|
62
|
+
if (result.admin_token) {
|
|
63
|
+
console.log('your admin_token is: %s', result.admin_token);
|
|
64
|
+
}
|
|
65
|
+
if (result.password) {
|
|
66
|
+
console.log('your password is: %s', result.password);
|
|
67
|
+
}
|
|
68
|
+
if (result.expires_at) {
|
|
69
|
+
console.log('expires at: %s', result.expires_at);
|
|
70
|
+
}
|
|
71
|
+
} catch (err) {
|
|
72
|
+
console.error('deploy failed: %s', err.message);
|
|
73
|
+
process.exit(1);
|
|
74
|
+
}
|
|
75
|
+
})
|
|
76
|
+
.command('$0', 'Create a tunnel (default)', (y) => {
|
|
77
|
+
return y
|
|
78
|
+
.option('p', {
|
|
79
|
+
alias: 'port',
|
|
80
|
+
describe: 'Internal HTTP server port',
|
|
81
|
+
})
|
|
82
|
+
.option('h', {
|
|
83
|
+
alias: 'host',
|
|
84
|
+
describe: 'Upstream server providing forwarding',
|
|
85
|
+
default: 'https://mygen.site',
|
|
86
|
+
})
|
|
87
|
+
.option('s', {
|
|
88
|
+
alias: 'subdomain',
|
|
89
|
+
describe: 'Request this subdomain',
|
|
90
|
+
})
|
|
91
|
+
.option('l', {
|
|
92
|
+
alias: 'local-host',
|
|
93
|
+
describe: 'Tunnel traffic to this host instead of localhost, override Host header to this host',
|
|
94
|
+
})
|
|
95
|
+
.option('local-https', {
|
|
96
|
+
describe: 'Tunnel traffic to a local HTTPS server',
|
|
97
|
+
})
|
|
98
|
+
.option('local-cert', {
|
|
99
|
+
describe: 'Path to certificate PEM file for local HTTPS server',
|
|
100
|
+
})
|
|
101
|
+
.option('local-key', {
|
|
102
|
+
describe: 'Path to certificate key file for local HTTPS server',
|
|
103
|
+
})
|
|
104
|
+
.option('local-ca', {
|
|
105
|
+
describe: 'Path to certificate authority file for self-signed certificates',
|
|
106
|
+
})
|
|
107
|
+
.option('allow-invalid-cert', {
|
|
108
|
+
describe: 'Disable certificate checks for your local HTTPS server (ignore cert/key/ca options)',
|
|
109
|
+
})
|
|
110
|
+
.options('o', {
|
|
111
|
+
alias: 'open',
|
|
112
|
+
describe: 'Opens the tunnel URL in your browser',
|
|
113
|
+
})
|
|
114
|
+
.option('print-requests', {
|
|
115
|
+
describe: 'Print basic request info',
|
|
116
|
+
})
|
|
117
|
+
.option('access', {
|
|
118
|
+
describe: 'Access control mode: public, password, ip_only, both',
|
|
119
|
+
})
|
|
120
|
+
.option('password', {
|
|
121
|
+
describe: 'Password for access control',
|
|
122
|
+
})
|
|
123
|
+
.option('owner-email', {
|
|
124
|
+
describe: 'Owner email for dashboard management',
|
|
125
|
+
})
|
|
126
|
+
.option('ttl', {
|
|
127
|
+
describe: 'Tunnel TTL in seconds (60-86400)',
|
|
128
|
+
type: 'number',
|
|
129
|
+
})
|
|
130
|
+
.boolean('local-https')
|
|
131
|
+
.boolean('allow-invalid-cert')
|
|
132
|
+
.boolean('print-requests');
|
|
133
|
+
}, async (argv) => {
|
|
134
|
+
if (typeof argv.port !== 'number') {
|
|
135
|
+
yargs.showHelp();
|
|
136
|
+
console.error('\nInvalid argument: `port` must be a number');
|
|
137
|
+
process.exit(1);
|
|
138
|
+
}
|
|
77
139
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
});
|
|
140
|
+
const tunnel = await localtunnel({
|
|
141
|
+
port: argv.port,
|
|
142
|
+
host: argv.host,
|
|
143
|
+
subdomain: argv.subdomain,
|
|
144
|
+
local_host: argv.localHost,
|
|
145
|
+
local_https: argv.localHttps,
|
|
146
|
+
local_cert: argv.localCert,
|
|
147
|
+
local_key: argv.localKey,
|
|
148
|
+
local_ca: argv.localCa,
|
|
149
|
+
allow_invalid_cert: argv.allowInvalidCert,
|
|
150
|
+
access: argv.access,
|
|
151
|
+
password: argv.password,
|
|
152
|
+
owner_email: argv.ownerEmail,
|
|
153
|
+
ttl: argv.ttl,
|
|
154
|
+
}).catch(err => {
|
|
155
|
+
throw err;
|
|
156
|
+
});
|
|
96
157
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
158
|
+
tunnel.on('error', err => {
|
|
159
|
+
throw err;
|
|
160
|
+
});
|
|
100
161
|
|
|
101
|
-
|
|
162
|
+
console.log('your url is: %s', tunnel.url);
|
|
102
163
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
164
|
+
if (tunnel.password) {
|
|
165
|
+
console.log('your password is: %s', tunnel.password);
|
|
166
|
+
}
|
|
106
167
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
168
|
+
if (tunnel.admin_token) {
|
|
169
|
+
console.log('your admin_token is: %s', tunnel.admin_token);
|
|
170
|
+
}
|
|
110
171
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
* @see https://github.com/localtunnel/localtunnel/pull/319#discussion_r319846289
|
|
115
|
-
*/
|
|
116
|
-
if (tunnel.cachedUrl) {
|
|
117
|
-
console.log('your cachedUrl is: %s', tunnel.cachedUrl);
|
|
118
|
-
}
|
|
172
|
+
if (tunnel.cachedUrl) {
|
|
173
|
+
console.log('your cachedUrl is: %s', tunnel.cachedUrl);
|
|
174
|
+
}
|
|
119
175
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
176
|
+
if (argv.open) {
|
|
177
|
+
openurl.open(tunnel.url);
|
|
178
|
+
}
|
|
123
179
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
})
|
|
180
|
+
if (argv['print-requests']) {
|
|
181
|
+
tunnel.on('request', info => {
|
|
182
|
+
console.log(new Date().toString(), info.method, info.path);
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
})
|
|
186
|
+
.help('help', 'Show this help and exit')
|
|
187
|
+
.version(version)
|
|
188
|
+
.parse();
|
package/lib/Tunnel.js
CHANGED
|
@@ -6,6 +6,7 @@ const axios = require('axios');
|
|
|
6
6
|
const debug = require('debug')('localtunnel:client');
|
|
7
7
|
|
|
8
8
|
const TunnelCluster = require('./TunnelCluster');
|
|
9
|
+
const { validateSlug, validateTTL, validateAccessMode } = require('./validate');
|
|
9
10
|
|
|
10
11
|
module.exports = class Tunnel extends EventEmitter {
|
|
11
12
|
constructor(opts = {}) {
|
|
@@ -15,6 +16,26 @@ module.exports = class Tunnel extends EventEmitter {
|
|
|
15
16
|
if (!this.opts.host) {
|
|
16
17
|
this.opts.host = 'https://mygen.site';
|
|
17
18
|
}
|
|
19
|
+
|
|
20
|
+
// Client-side validation — fail fast before API call
|
|
21
|
+
if (opts.subdomain) {
|
|
22
|
+
const slugCheck = validateSlug(opts.subdomain);
|
|
23
|
+
if (!slugCheck.valid) {
|
|
24
|
+
throw new Error(slugCheck.error);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
if (opts.ttl != null) {
|
|
28
|
+
const ttlCheck = validateTTL(Number(opts.ttl));
|
|
29
|
+
if (!ttlCheck.valid) {
|
|
30
|
+
throw new Error(ttlCheck.error);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
if (opts.access) {
|
|
34
|
+
const accessCheck = validateAccessMode(opts.access);
|
|
35
|
+
if (!accessCheck.valid) {
|
|
36
|
+
throw new Error(accessCheck.error);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
18
39
|
}
|
|
19
40
|
|
|
20
41
|
_getInfo(body) {
|
package/lib/deploy.js
CHANGED
|
@@ -3,6 +3,7 @@ const path = require('path');
|
|
|
3
3
|
const axios = require('axios');
|
|
4
4
|
const FormData = require('form-data');
|
|
5
5
|
const debug = require('debug')('mygensite:deploy');
|
|
6
|
+
const { validateSlug, validateFilePath, validateTTL, validateAccessMode } = require('./validate');
|
|
6
7
|
|
|
7
8
|
const DEFAULT_HOST = 'https://mygen.site';
|
|
8
9
|
|
|
@@ -94,6 +95,29 @@ async function deploy(options) {
|
|
|
94
95
|
admin_token,
|
|
95
96
|
} = options;
|
|
96
97
|
|
|
98
|
+
// Client-side validation — fail fast before API call
|
|
99
|
+
if (subdomain) {
|
|
100
|
+
const slugCheck = validateSlug(subdomain);
|
|
101
|
+
if (!slugCheck.valid) {
|
|
102
|
+
throw new Error(slugCheck.error);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
if (ttl != null) {
|
|
106
|
+
const ttlCheck = validateTTL(Number(ttl));
|
|
107
|
+
if (!ttlCheck.valid) {
|
|
108
|
+
throw new Error(ttlCheck.error);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
if (access) {
|
|
112
|
+
const mode = typeof access === 'string' ? access : access.mode;
|
|
113
|
+
if (mode) {
|
|
114
|
+
const accessCheck = validateAccessMode(mode);
|
|
115
|
+
if (!accessCheck.valid) {
|
|
116
|
+
throw new Error(accessCheck.error);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
97
121
|
// Build file list
|
|
98
122
|
let fileList;
|
|
99
123
|
if (directory) {
|
|
@@ -116,6 +140,15 @@ async function deploy(options) {
|
|
|
116
140
|
throw new Error('No files found to deploy');
|
|
117
141
|
}
|
|
118
142
|
|
|
143
|
+
// Validate all file paths before uploading
|
|
144
|
+
for (const file of fileList) {
|
|
145
|
+
const pathCheck = validateFilePath(file.name);
|
|
146
|
+
if (!pathCheck.valid) {
|
|
147
|
+
throw new Error(`Invalid file path "${file.name}": ${pathCheck.error}`);
|
|
148
|
+
}
|
|
149
|
+
file.name = pathCheck.cleaned;
|
|
150
|
+
}
|
|
151
|
+
|
|
119
152
|
debug('deploying %d files to %s', fileList.length, host);
|
|
120
153
|
|
|
121
154
|
// Build multipart form
|
|
@@ -137,9 +170,12 @@ async function deploy(options) {
|
|
|
137
170
|
}
|
|
138
171
|
form.append('access', JSON.stringify(accessObj));
|
|
139
172
|
|
|
173
|
+
// Send file paths as separate JSON field (busboy strips directories from filename)
|
|
174
|
+
form.append('filepaths', JSON.stringify(fileList.map(f => f.name)));
|
|
175
|
+
|
|
140
176
|
for (const file of fileList) {
|
|
141
177
|
form.append('files', file.content, {
|
|
142
|
-
|
|
178
|
+
filepath: file.name,
|
|
143
179
|
contentType: file.contentType,
|
|
144
180
|
});
|
|
145
181
|
}
|
|
@@ -169,4 +205,36 @@ async function deploy(options) {
|
|
|
169
205
|
return data;
|
|
170
206
|
}
|
|
171
207
|
|
|
208
|
+
/**
|
|
209
|
+
* Create a management handle for an existing service.
|
|
210
|
+
* Use this when you already have the slug and admin_token (e.g. from a previous deploy).
|
|
211
|
+
*
|
|
212
|
+
* @param {object} options
|
|
213
|
+
* @param {string} options.slug - The service slug
|
|
214
|
+
* @param {string} options.admin_token - The admin token from the original deploy
|
|
215
|
+
* @param {string} [options.host] - API host (default: https://mygen.site)
|
|
216
|
+
* @returns {object} Management object with updateAccess, extendTTL, redeploy, delete methods
|
|
217
|
+
*/
|
|
218
|
+
function manage(options) {
|
|
219
|
+
const {
|
|
220
|
+
slug,
|
|
221
|
+
admin_token,
|
|
222
|
+
host = DEFAULT_HOST,
|
|
223
|
+
} = options;
|
|
224
|
+
|
|
225
|
+
if (!slug) throw new Error('slug is required');
|
|
226
|
+
if (!admin_token) throw new Error('admin_token is required');
|
|
227
|
+
|
|
228
|
+
return {
|
|
229
|
+
slug,
|
|
230
|
+
admin_token,
|
|
231
|
+
url: `https://${slug}.${host.replace(/^https?:\/\//, '')}`,
|
|
232
|
+
updateAccess: (access) => patchService(host, slug, admin_token, { access }),
|
|
233
|
+
extendTTL: (ttl) => patchService(host, slug, admin_token, { ttl }),
|
|
234
|
+
redeploy: (dir) => deploy({ host, subdomain: slug, directory: dir, admin_token }),
|
|
235
|
+
delete: (purge) => deleteService(host, slug, admin_token, purge),
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
172
239
|
module.exports = deploy;
|
|
240
|
+
module.exports.manage = manage;
|
package/lib/validate.js
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Validation helpers for mygensite.
|
|
5
|
+
*
|
|
6
|
+
* These mirror the server-side rules so you can catch errors locally
|
|
7
|
+
* before making an API call.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
// Must match: server/src/api/routes/tunnels.ts SLUG_REGEX
|
|
11
|
+
const SLUG_REGEX = /^[a-z0-9][a-z0-9-]{1,61}[a-z0-9]$/;
|
|
12
|
+
|
|
13
|
+
const RESERVED_SLUGS = new Set([
|
|
14
|
+
'www', 'api', 'dashboard', 'admin', 'mail',
|
|
15
|
+
'ftp', 'static', 'docs', 'status', 'health',
|
|
16
|
+
'internal', 'tunnel', 'app', 'web',
|
|
17
|
+
]);
|
|
18
|
+
|
|
19
|
+
// Must match: server/src/api/routes/deploy.ts SAFE_PATH_SEGMENT
|
|
20
|
+
const SAFE_PATH_SEGMENT = /^[a-zA-Z0-9_\-. ]+$/;
|
|
21
|
+
|
|
22
|
+
const VALID_ACCESS_MODES = ['public', 'password', 'ip_only', 'both'];
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Validate a slug (subdomain) name.
|
|
26
|
+
* @param {string} slug
|
|
27
|
+
* @returns {{ valid: boolean, error?: string }}
|
|
28
|
+
*/
|
|
29
|
+
function validateSlug(slug) {
|
|
30
|
+
if (!slug || typeof slug !== 'string') {
|
|
31
|
+
return { valid: false, error: 'Slug is required' };
|
|
32
|
+
}
|
|
33
|
+
if (slug.length < 3 || slug.length > 63) {
|
|
34
|
+
return { valid: false, error: 'Slug must be 3-63 characters' };
|
|
35
|
+
}
|
|
36
|
+
if (!SLUG_REGEX.test(slug)) {
|
|
37
|
+
return { valid: false, error: 'Slug must be lowercase alphanumeric and hyphens, starting and ending with alphanumeric' };
|
|
38
|
+
}
|
|
39
|
+
if (RESERVED_SLUGS.has(slug)) {
|
|
40
|
+
return { valid: false, error: `"${slug}" is a reserved slug` };
|
|
41
|
+
}
|
|
42
|
+
return { valid: true };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Validate a file path for static deployment.
|
|
47
|
+
* @param {string} name - File path (e.g. "index.html", "assets/style.css")
|
|
48
|
+
* @returns {{ valid: boolean, cleaned?: string, error?: string }}
|
|
49
|
+
*/
|
|
50
|
+
function validateFilePath(name) {
|
|
51
|
+
if (!name || typeof name !== 'string') {
|
|
52
|
+
return { valid: false, error: 'File path is required' };
|
|
53
|
+
}
|
|
54
|
+
if (name.length > 1024) {
|
|
55
|
+
return { valid: false, error: 'File path must not exceed 1024 characters' };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const cleaned = name.replace(/^\/+/, '');
|
|
59
|
+
if (!cleaned) {
|
|
60
|
+
return { valid: false, error: 'File path is empty after cleaning' };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const segments = cleaned.split('/');
|
|
64
|
+
for (const seg of segments) {
|
|
65
|
+
if (!seg) {
|
|
66
|
+
return { valid: false, error: 'File path contains empty segments (double slash)' };
|
|
67
|
+
}
|
|
68
|
+
if (seg === '.' || seg === '..') {
|
|
69
|
+
return { valid: false, error: 'Path traversal ("." or "..") is not allowed' };
|
|
70
|
+
}
|
|
71
|
+
if (seg.length > 255) {
|
|
72
|
+
return { valid: false, error: `Segment "${seg.slice(0, 20)}..." exceeds 255 characters` };
|
|
73
|
+
}
|
|
74
|
+
if (seg.startsWith('.')) {
|
|
75
|
+
return { valid: false, error: `Hidden files (starting with ".") are not allowed: "${seg}"` };
|
|
76
|
+
}
|
|
77
|
+
if (seg.startsWith(' ')) {
|
|
78
|
+
return { valid: false, error: 'File names must not start with a space' };
|
|
79
|
+
}
|
|
80
|
+
if (!SAFE_PATH_SEGMENT.test(seg)) {
|
|
81
|
+
return { valid: false, error: `Invalid characters in "${seg}". Allowed: letters, numbers, hyphens, underscores, dots, spaces` };
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return { valid: true, cleaned: segments.join('/') };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Validate TTL value.
|
|
90
|
+
* @param {number} ttl - TTL in seconds
|
|
91
|
+
* @returns {{ valid: boolean, error?: string }}
|
|
92
|
+
*/
|
|
93
|
+
function validateTTL(ttl) {
|
|
94
|
+
if (typeof ttl !== 'number' || !Number.isFinite(ttl)) {
|
|
95
|
+
return { valid: false, error: 'TTL must be a number' };
|
|
96
|
+
}
|
|
97
|
+
if (ttl < 60 || ttl > 86400) {
|
|
98
|
+
return { valid: false, error: 'TTL must be between 60 and 86400 seconds (1 min to 24 hours)' };
|
|
99
|
+
}
|
|
100
|
+
return { valid: true };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Validate access mode.
|
|
105
|
+
* @param {string} mode
|
|
106
|
+
* @returns {{ valid: boolean, error?: string }}
|
|
107
|
+
*/
|
|
108
|
+
function validateAccessMode(mode) {
|
|
109
|
+
if (!VALID_ACCESS_MODES.includes(mode)) {
|
|
110
|
+
return { valid: false, error: `Access mode must be one of: ${VALID_ACCESS_MODES.join(', ')}` };
|
|
111
|
+
}
|
|
112
|
+
return { valid: true };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
module.exports = {
|
|
116
|
+
validateSlug,
|
|
117
|
+
validateFilePath,
|
|
118
|
+
validateTTL,
|
|
119
|
+
validateAccessMode,
|
|
120
|
+
SLUG_REGEX,
|
|
121
|
+
RESERVED_SLUGS,
|
|
122
|
+
VALID_ACCESS_MODES,
|
|
123
|
+
};
|
package/localtunnel.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const Tunnel = require('./lib/Tunnel');
|
|
2
2
|
const deploy = require('./lib/deploy');
|
|
3
|
+
const validate = require('./lib/validate');
|
|
3
4
|
|
|
4
5
|
function localtunnel(arg1, arg2, arg3) {
|
|
5
6
|
const options = typeof arg1 === 'object' ? arg1 : { ...arg2, port: arg1 };
|
|
@@ -15,5 +16,7 @@ function localtunnel(arg1, arg2, arg3) {
|
|
|
15
16
|
}
|
|
16
17
|
|
|
17
18
|
localtunnel.deploy = deploy;
|
|
19
|
+
localtunnel.manage = deploy.manage;
|
|
20
|
+
localtunnel.validate = validate;
|
|
18
21
|
|
|
19
22
|
module.exports = localtunnel;
|