@adamancyzhang/claude-orchestrator 0.4.0 → 0.4.1
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 +19 -13
- package/dist/config.d.ts +17 -0
- package/dist/config.js +20 -2
- package/dist/config.js.map +1 -1
- package/dist/email-validator/__tests__/domain.test.d.ts +1 -0
- package/dist/email-validator/__tests__/domain.test.js +128 -0
- package/dist/email-validator/__tests__/domain.test.js.map +1 -0
- package/dist/email-validator/__tests__/local-part.test.d.ts +1 -0
- package/dist/email-validator/__tests__/local-part.test.js +110 -0
- package/dist/email-validator/__tests__/local-part.test.js.map +1 -0
- package/dist/email-validator/__tests__/parser.test.d.ts +1 -0
- package/dist/email-validator/__tests__/parser.test.js +161 -0
- package/dist/email-validator/__tests__/parser.test.js.map +1 -0
- package/dist/email-validator/__tests__/validator.test.d.ts +1 -0
- package/dist/email-validator/__tests__/validator.test.js +152 -0
- package/dist/email-validator/__tests__/validator.test.js.map +1 -0
- package/dist/email-validator/diagnostics.d.ts +3 -0
- package/dist/email-validator/diagnostics.js +34 -0
- package/dist/email-validator/diagnostics.js.map +1 -0
- package/dist/email-validator/domain.d.ts +2 -0
- package/dist/email-validator/domain.js +119 -0
- package/dist/email-validator/domain.js.map +1 -0
- package/dist/email-validator/index.d.ts +2 -0
- package/dist/email-validator/index.js +2 -0
- package/dist/email-validator/index.js.map +1 -0
- package/dist/email-validator/local-part.d.ts +2 -0
- package/dist/email-validator/local-part.js +67 -0
- package/dist/email-validator/local-part.js.map +1 -0
- package/dist/email-validator/parser.d.ts +9 -0
- package/dist/email-validator/parser.js +120 -0
- package/dist/email-validator/parser.js.map +1 -0
- package/dist/email-validator/types.d.ts +24 -0
- package/dist/email-validator/types.js +2 -0
- package/dist/email-validator/types.js.map +1 -0
- package/dist/email-validator/validator.d.ts +9 -0
- package/dist/email-validator/validator.js +51 -0
- package/dist/email-validator/validator.js.map +1 -0
- package/dist/executor/runner.d.ts +19 -2
- package/dist/executor/runner.js +29 -4
- package/dist/executor/runner.js.map +1 -1
- package/dist/executor/template.d.ts +3 -1
- package/dist/executor/template.js +17 -32
- package/dist/executor/template.js.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/leader/chain-router.d.ts +6 -2
- package/dist/leader/chain-router.js +81 -12
- package/dist/leader/chain-router.js.map +1 -1
- package/dist/leader/event-bus.d.ts +1 -1
- package/dist/leader/event-bus.js +1 -0
- package/dist/leader/event-bus.js.map +1 -1
- package/dist/leader/index.js +97 -9
- package/dist/leader/index.js.map +1 -1
- package/dist/leader/merge-validator.d.ts +2 -1
- package/dist/leader/merge-validator.js +13 -20
- package/dist/leader/merge-validator.js.map +1 -1
- package/dist/leader/state.d.ts +3 -0
- package/dist/leader/state.js +30 -1
- package/dist/leader/state.js.map +1 -1
- package/dist/leader/stream-tailer.d.ts +12 -0
- package/dist/leader/stream-tailer.js +58 -0
- package/dist/leader/stream-tailer.js.map +1 -0
- package/dist/leader/tui.d.ts +6 -0
- package/dist/leader/tui.js +131 -27
- package/dist/leader/tui.js.map +1 -1
- package/dist/models/schemas.d.ts +13 -6
- package/dist/models/schemas.js +3 -0
- package/dist/models/schemas.js.map +1 -1
- package/dist/modules/task-queue.js +4 -0
- package/dist/modules/task-queue.js.map +1 -1
- package/dist/orchestrator/init-checker.d.ts +33 -0
- package/dist/orchestrator/init-checker.js +297 -0
- package/dist/orchestrator/init-checker.js.map +1 -0
- package/dist/orchestrator/run.d.ts +1 -0
- package/dist/orchestrator/run.js +26 -81
- package/dist/orchestrator/run.js.map +1 -1
- package/dist/templates/agents/worker-commit-message.md +12 -0
- package/dist/templates/agents/worker-evaluate-format-hint.md +7 -0
- package/dist/templates/agents/worker-identity.md +7 -0
- package/dist/templates/agents/worker-merge-decision.md +12 -0
- package/dist/templates/agents/worker-task-doc.md +13 -0
- package/dist/templates/user-global-claude.md +65 -0
- package/dist/utils/console-capture.d.ts +2 -0
- package/dist/utils/console-capture.js +42 -0
- package/dist/utils/console-capture.js.map +1 -0
- package/dist/utils/exec.d.ts +2 -6
- package/dist/utils/exec.js +48 -29
- package/dist/utils/exec.js.map +1 -1
- package/dist/utils/json.d.ts +5 -0
- package/dist/utils/json.js +17 -0
- package/dist/utils/json.js.map +1 -0
- package/dist/worker/child-runner.js +19 -3
- package/dist/worker/child-runner.js.map +1 -1
- package/dist/worker/commit-checker.d.ts +3 -2
- package/dist/worker/commit-checker.js +13 -19
- package/dist/worker/commit-checker.js.map +1 -1
- package/dist/worker/evaluator.d.ts +3 -4
- package/dist/worker/evaluator.js +15 -50
- package/dist/worker/evaluator.js.map +1 -1
- package/dist/worker/watcher.js +9 -11
- package/dist/worker/watcher.js.map +1 -1
- package/dist/worker/worktree-initializer.js +3 -0
- package/dist/worker/worktree-initializer.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -73,7 +73,7 @@ Type a requirement in the TUI input line and press Enter. The Leader forwards it
|
|
|
73
73
|
# CLI commands (from any terminal)
|
|
74
74
|
claude-orchestrator push-task --title "Implement login endpoint" --priority 0
|
|
75
75
|
claude-orchestrator send-message --to-name Jerry --content "Starting on the auth module?"
|
|
76
|
-
claude-orchestrator
|
|
76
|
+
claude-orchestrator poll-task --status pending
|
|
77
77
|
```
|
|
78
78
|
|
|
79
79
|
---
|
|
@@ -84,7 +84,7 @@ claude-orchestrator list-tasks --status pending
|
|
|
84
84
|
|
|
85
85
|
| Component | What it does | ZK magic |
|
|
86
86
|
|-----------|-------------|----------|
|
|
87
|
-
| **Leader** | Read-only TUI, mechanical message/task routing, merge validation, orphan recovery.
|
|
87
|
+
| **Leader** | Read-only TUI, mechanical message/task routing, merge validation, orphan recovery. Self-processes decompose via claude-cli when template available; otherwise forwards to Planner. | `/leader` EPHEMERAL — exactly one Leader |
|
|
88
88
|
| **Worker** | Isolated git worktree, ZK watch loop, auto-processes messages via `claude -p`, self-evaluates output, auto-commits changes | EPHEMERAL nodes → auto-cleanup on disconnect |
|
|
89
89
|
| **Task Queue** | Push → Claim → Complete (or Block/Fail/Retry). Role-link priority sorting. | Sequential nodes for FIFO, ephemeral claims for atomic locks |
|
|
90
90
|
| **Message Router** | Point-to-point messaging via ZK watches | Persistent-sequential nodes, push notification |
|
|
@@ -99,7 +99,7 @@ Each Worker runs in its own `git worktree` under `.claude-orchestrator/worktree/
|
|
|
99
99
|
|
|
100
100
|
### CLI-Native — Zero MCP Server
|
|
101
101
|
|
|
102
|
-
Leader and Workers each connect directly to ZooKeeper. The Leader is a
|
|
102
|
+
Leader and Workers each connect directly to ZooKeeper. The Leader is primarily a router: self-processes decompose when template available (otherwise forwards to Planner), creates tasks from ChainDef JSON, mechanically executes EvalDecision JSON. All other AI intelligence runs on Workers via `claude -p`. No HTTP, no SSE, no MCP protocol.
|
|
103
103
|
|
|
104
104
|
### Responsibility Chain
|
|
105
105
|
|
|
@@ -120,7 +120,7 @@ Each link is a dedicated role. One Worker produces, the next Worker verifies —
|
|
|
120
120
|
| `push-task` | Create a task (optionally assign to someone) |
|
|
121
121
|
| `claim-task` | Grab the next task — atomic, no two instances can claim the same one |
|
|
122
122
|
| `complete-task` | Mark a task done with results |
|
|
123
|
-
| `
|
|
123
|
+
| `poll-task` | List tasks, optionally filtered by status |
|
|
124
124
|
| `task-block` | Mark a claimed task as blocked (with reason) |
|
|
125
125
|
| `task-fail` | Mark a claimed task as failed (with reason) |
|
|
126
126
|
| `task-retry` | Re-queue a failed task (retry_count + 1, max 3) |
|
|
@@ -249,7 +249,7 @@ node dist/index.js run --worker 3
|
|
|
249
249
|
### Run Tests
|
|
250
250
|
|
|
251
251
|
```bash
|
|
252
|
-
npm test #
|
|
252
|
+
npm test # Run all tests
|
|
253
253
|
npm run test:watch # Watch mode
|
|
254
254
|
npx vitest run tests/unit/worker-prompt-rendering.test.ts # Prompt rendering verification
|
|
255
255
|
```
|
|
@@ -267,14 +267,15 @@ npx vitest run tests/unit/worker-prompt-rendering.test.ts # Prompt rendering ve
|
|
|
267
267
|
│ ├── leader/ # Leader node
|
|
268
268
|
│ │ ├── index.ts # Startup / shutdown orchestration
|
|
269
269
|
│ │ ├── tui.ts # ANSI TUI with Worker Messages panel
|
|
270
|
-
│ │ ├── event-bus.ts # Typed EventEmitter (
|
|
270
|
+
│ │ ├── event-bus.ts # Typed EventEmitter (17 events)
|
|
271
271
|
│ │ ├── state.ts # Centralized LeaderState
|
|
272
272
|
│ │ ├── monitor.ts # WorkerMonitor — join/leave detection
|
|
273
273
|
│ │ ├── orchestrator.ts # TaskOrchestrator — lifecycle tracking
|
|
274
274
|
│ │ ├── recovery.ts # TaskRecovery — orphan recovery (max 3 retries)
|
|
275
275
|
│ │ ├── watcher.ts # LeaderWatcher — message processing
|
|
276
|
-
│ │ ├── chain-router.ts # ChainRouter — mechanical routing (
|
|
277
|
-
│ │
|
|
276
|
+
│ │ ├── chain-router.ts # ChainRouter — mechanical routing (self or forward decompose)
|
|
277
|
+
│ │ ├── merge-validator.ts # Cross-verify + merge worker branches
|
|
278
|
+
│ │ └── stream-tailer.ts # StreamTailer — live worker log display
|
|
278
279
|
│ ├── worker/ # Worker node
|
|
279
280
|
│ │ ├── worktree-initializer.ts# Name generation, worktree creation, role assignment
|
|
280
281
|
│ │ ├── child.ts # Child process entry point
|
|
@@ -287,16 +288,21 @@ npx vitest run tests/unit/worker-prompt-rendering.test.ts # Prompt rendering ve
|
|
|
287
288
|
│ │ └── runner.ts # ClaudeRunner — CLI execution wrapper
|
|
288
289
|
│ ├── zk/
|
|
289
290
|
│ │ ├── client.ts # ZooKeeper connection management
|
|
290
|
-
│ │
|
|
291
|
+
│ │ ├── paths.ts # ZK path constants
|
|
292
|
+
│ │ └── watcher.ts # ZK watch callback wrapper
|
|
291
293
|
│ ├── modules/
|
|
292
294
|
│ │ ├── registry.ts # Instance registry
|
|
293
295
|
│ │ ├── task-queue.ts # Task queue (push/claim/complete/block/fail/retry)
|
|
294
296
|
│ │ └── message-router.ts # Message routing + template rendering
|
|
297
|
+
│ ├── hooks/
|
|
298
|
+
│ │ └── engine.ts # HookEngine — pre/post lifecycle hooks
|
|
295
299
|
│ ├── models/
|
|
296
300
|
│ │ └── schemas.ts # Zod schemas (Instance, Task, Message, ChainDef, EvalDecision)
|
|
297
301
|
│ └── utils/
|
|
298
|
-
│ ├── exec.ts # Shell execution (execWithTee)
|
|
299
|
-
│
|
|
302
|
+
│ ├── exec.ts # Shell execution (execWithTee, execWithStreaming, execAndCapture)
|
|
303
|
+
│ ├── logger.ts # Tagged logger (+ --debug mode)
|
|
304
|
+
│ ├── output.ts # JSON output helper
|
|
305
|
+
│ └── console-capture.ts # Console redirect to file (for TUI)
|
|
300
306
|
├── templates/ # Prompt and memory templates (v0.4)
|
|
301
307
|
│ ├── agents/ # 7 Worker prompt templates
|
|
302
308
|
│ └── claude-memory/ # 6 CLAUDE.md directory memory templates
|
|
@@ -317,9 +323,9 @@ npx vitest run tests/unit/worker-prompt-rendering.test.ts # Prompt rendering ve
|
|
|
317
323
|
│ │ └── design.md # Worker initialization + directory memory design
|
|
318
324
|
│ └── v0.3/ # Archived v0.3 docs
|
|
319
325
|
├── tests/
|
|
320
|
-
│ ├── unit/ # 11 test files
|
|
326
|
+
│ ├── unit/ # 11 test files
|
|
321
327
|
│ │ └── worker-prompt-rendering.test.ts # Prompt variable substitution verification
|
|
322
|
-
│ └── integration/ #
|
|
328
|
+
│ └── integration/ # 7 integration test files
|
|
323
329
|
├── examples-workspace/ # Reference implementation of multi-agent patterns
|
|
324
330
|
├── docker-compose.yml # ZooKeeper
|
|
325
331
|
├── package.json
|
package/dist/config.d.ts
CHANGED
|
@@ -14,6 +14,20 @@ export interface HooksConfig {
|
|
|
14
14
|
worker_message_start?: string | null;
|
|
15
15
|
worker_message_end?: string | null;
|
|
16
16
|
}
|
|
17
|
+
export type StepAction = "created" | "updated" | "replaced" | "skipped" | "rejected" | "completed" | "failed";
|
|
18
|
+
export interface StepRecord {
|
|
19
|
+
action: StepAction;
|
|
20
|
+
timestamp: string;
|
|
21
|
+
reason?: string;
|
|
22
|
+
}
|
|
23
|
+
export interface InitStatus {
|
|
24
|
+
global_config?: StepRecord;
|
|
25
|
+
user_claude_md?: StepRecord;
|
|
26
|
+
team_claude_md?: StepRecord;
|
|
27
|
+
skills?: Record<string, StepRecord>;
|
|
28
|
+
worktrees?: Record<string, StepRecord>;
|
|
29
|
+
npm_install?: Record<string, StepRecord>;
|
|
30
|
+
}
|
|
17
31
|
export interface InstanceConfig {
|
|
18
32
|
instance_id?: string;
|
|
19
33
|
name?: string;
|
|
@@ -29,6 +43,7 @@ export interface InstanceConfig {
|
|
|
29
43
|
branch: string;
|
|
30
44
|
instance_id: string;
|
|
31
45
|
}>;
|
|
46
|
+
init_status?: InitStatus;
|
|
32
47
|
}
|
|
33
48
|
export interface ResolvedConfig {
|
|
34
49
|
zk: ZkConfig;
|
|
@@ -63,4 +78,6 @@ export declare function saveProjectWorktreeConfig(entries: Record<string, Worktr
|
|
|
63
78
|
branch: string;
|
|
64
79
|
instanceId: string;
|
|
65
80
|
}>): void;
|
|
81
|
+
export declare function loadInitStatus(): InitStatus;
|
|
82
|
+
export declare function saveInitStatusStep(stepId: string, record: StepRecord, subId?: string): void;
|
|
66
83
|
export declare function expandHomeDir(p: string): string;
|
package/dist/config.js
CHANGED
|
@@ -13,7 +13,7 @@ function defaultZkConfig() {
|
|
|
13
13
|
return { url: "127.0.0.1:2181", root_path: "/claude-orchestrator", auth: null };
|
|
14
14
|
}
|
|
15
15
|
function defaultCacheDir() {
|
|
16
|
-
return "
|
|
16
|
+
return ".claude-orchestrator/sessions";
|
|
17
17
|
}
|
|
18
18
|
function defaultCliCommand() {
|
|
19
19
|
return "claude --dangerously-skip-permissions --permission-mode dontAsk";
|
|
@@ -32,7 +32,7 @@ export function loadConfig(cliOpts) {
|
|
|
32
32
|
else if (process.env.ZK_HOSTS) {
|
|
33
33
|
zk.url = process.env.ZK_HOSTS;
|
|
34
34
|
}
|
|
35
|
-
const cacheDir = global.cache_dir || defaultCacheDir();
|
|
35
|
+
const cacheDir = path.resolve(process.cwd(), expandHomeDir(global.cache_dir || defaultCacheDir()));
|
|
36
36
|
// Command: project overrides global
|
|
37
37
|
const mergedCommands = project.commands || global.commands;
|
|
38
38
|
const cliCommand = mergedCommands?.["claude-cli"] || defaultCliCommand();
|
|
@@ -116,6 +116,24 @@ export function saveProjectWorktreeConfig(entries) {
|
|
|
116
116
|
}
|
|
117
117
|
saveInstanceConfig({ worktree: record }, false);
|
|
118
118
|
}
|
|
119
|
+
export function loadInitStatus() {
|
|
120
|
+
const global = loadGlobalConfig();
|
|
121
|
+
return global.init_status ?? {};
|
|
122
|
+
}
|
|
123
|
+
export function saveInitStatusStep(stepId, record, subId) {
|
|
124
|
+
const global = loadGlobalConfig();
|
|
125
|
+
const status = global.init_status ?? {};
|
|
126
|
+
if (subId) {
|
|
127
|
+
status[stepId] = {
|
|
128
|
+
...status[stepId],
|
|
129
|
+
[subId]: record,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
status[stepId] = record;
|
|
134
|
+
}
|
|
135
|
+
saveInstanceConfig({ init_status: status }, true);
|
|
136
|
+
}
|
|
119
137
|
export function expandHomeDir(p) {
|
|
120
138
|
if (p.startsWith("~")) {
|
|
121
139
|
return path.join(os.homedir(), p.slice(1));
|
package/dist/config.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,SAAS,CAAC;AAC9B,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAClC,OAAO,KAAK,EAAE,MAAM,SAAS,CAAC;AAE9B,MAAM,CAAC,MAAM,iBAAiB,GAAG,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,EAAE,sBAAsB,CAAC,CAAC;AACjF,MAAM,CAAC,MAAM,kBAAkB,GAAG,IAAI,CAAC,IAAI,CAAC,iBAAiB,EAAE,aAAa,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { validateDomain } from "../domain.js";
|
|
3
|
+
describe("validateDomain", () => {
|
|
4
|
+
describe("valid hostnames", () => {
|
|
5
|
+
it("accepts example.com", () => {
|
|
6
|
+
expect(validateDomain("example.com", false, false)).toEqual([]);
|
|
7
|
+
});
|
|
8
|
+
it("accepts subdomain", () => {
|
|
9
|
+
expect(validateDomain("mail.example.com", false, false)).toEqual([]);
|
|
10
|
+
});
|
|
11
|
+
it("accepts multi-level domain", () => {
|
|
12
|
+
expect(validateDomain("a.b.c.d.e.com", false, false)).toEqual([]);
|
|
13
|
+
});
|
|
14
|
+
it("accepts hyphen in middle of label", () => {
|
|
15
|
+
expect(validateDomain("my-domain.com", false, false)).toEqual([]);
|
|
16
|
+
});
|
|
17
|
+
it("accepts digits in labels", () => {
|
|
18
|
+
expect(validateDomain("example123.com", false, false)).toEqual([]);
|
|
19
|
+
});
|
|
20
|
+
it("accepts single-char labels", () => {
|
|
21
|
+
expect(validateDomain("a.b", false, false)).toEqual([]);
|
|
22
|
+
});
|
|
23
|
+
it("accepts 63-char label (boundary)", () => {
|
|
24
|
+
const label = "a".repeat(63);
|
|
25
|
+
expect(validateDomain(`${label}.com`, false, false)).toEqual([]);
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
describe("valid IP literals", () => {
|
|
29
|
+
it("accepts IPv4 literal", () => {
|
|
30
|
+
expect(validateDomain("192.168.1.1", true, false)).toEqual([]);
|
|
31
|
+
});
|
|
32
|
+
it("accepts IPv4 with boundary values", () => {
|
|
33
|
+
expect(validateDomain("0.0.0.0", true, false)).toEqual([]);
|
|
34
|
+
expect(validateDomain("255.255.255.255", true, false)).toEqual([]);
|
|
35
|
+
});
|
|
36
|
+
it("accepts IPv6 literal", () => {
|
|
37
|
+
expect(validateDomain("2001:db8::1", false, true)).toEqual([]);
|
|
38
|
+
});
|
|
39
|
+
it("accepts full IPv6", () => {
|
|
40
|
+
expect(validateDomain("2001:0db8:0000:0000:0000:0000:0000:0001", false, true)).toEqual([]);
|
|
41
|
+
});
|
|
42
|
+
it("accepts IPv6 loopback", () => {
|
|
43
|
+
expect(validateDomain("::1", false, true)).toEqual([]);
|
|
44
|
+
});
|
|
45
|
+
it("accepts IPv6 fe80", () => {
|
|
46
|
+
expect(validateDomain("fe80::1", false, true)).toEqual([]);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
describe("hostname label errors", () => {
|
|
50
|
+
it("rejects label starting with hyphen", () => {
|
|
51
|
+
const errors = validateDomain("example.-domain.com", false, false);
|
|
52
|
+
expect(errors.some((e) => e.code === "ERR_DOMAIN_LABEL_HYPHEN_START")).toBe(true);
|
|
53
|
+
});
|
|
54
|
+
it("rejects label ending with hyphen", () => {
|
|
55
|
+
const errors = validateDomain("example.domain-.com", false, false);
|
|
56
|
+
expect(errors.some((e) => e.code === "ERR_DOMAIN_LABEL_HYPHEN_END")).toBe(true);
|
|
57
|
+
});
|
|
58
|
+
it("rejects empty label (consecutive dots)", () => {
|
|
59
|
+
const errors = validateDomain("example..com", false, false);
|
|
60
|
+
expect(errors.some((e) => e.code === "ERR_DOMAIN_LABEL_EMPTY")).toBe(true);
|
|
61
|
+
});
|
|
62
|
+
it("rejects label exceeding 63 chars", () => {
|
|
63
|
+
const label = "a".repeat(64);
|
|
64
|
+
const errors = validateDomain(`${label}.com`, false, false);
|
|
65
|
+
expect(errors.some((e) => e.code === "ERR_DOMAIN_LABEL_LENGTH")).toBe(true);
|
|
66
|
+
});
|
|
67
|
+
it("rejects invalid char in label", () => {
|
|
68
|
+
const errors = validateDomain("example.c_m.com", false, false);
|
|
69
|
+
expect(errors.some((e) => e.code === "ERR_DOMAIN_INVALID_CHAR")).toBe(true);
|
|
70
|
+
});
|
|
71
|
+
it("rejects domain with no dot", () => {
|
|
72
|
+
const errors = validateDomain("localhost", false, false);
|
|
73
|
+
expect(errors.some((e) => e.code === "ERR_DOMAIN_NO_DOT")).toBe(true);
|
|
74
|
+
});
|
|
75
|
+
it("rejects numeric TLD", () => {
|
|
76
|
+
const errors = validateDomain("example.123", false, false);
|
|
77
|
+
expect(errors.some((e) => e.code === "ERR_DOMAIN_NUMERIC_TLD")).toBe(true);
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
describe("length errors", () => {
|
|
81
|
+
it("rejects empty domain", () => {
|
|
82
|
+
const errors = validateDomain("", false, false);
|
|
83
|
+
expect(errors.some((e) => e.code === "ERR_DOMAIN_EMPTY")).toBe(true);
|
|
84
|
+
});
|
|
85
|
+
it("rejects domain exceeding 255 chars", () => {
|
|
86
|
+
const label = "a".repeat(63);
|
|
87
|
+
const domain = `${label}.${label}.${label}.${label}.${label}`;
|
|
88
|
+
// That's ~319 chars, > 255
|
|
89
|
+
const errors = validateDomain(domain, false, false, 255);
|
|
90
|
+
expect(errors.some((e) => e.code === "ERR_DOMAIN_LENGTH")).toBe(true);
|
|
91
|
+
});
|
|
92
|
+
it("accepts with custom max domain length", () => {
|
|
93
|
+
const domain = "a".repeat(10) + ".b";
|
|
94
|
+
const errors = validateDomain(domain, false, false, 100);
|
|
95
|
+
expect(errors.filter((e) => e.code === "ERR_DOMAIN_LENGTH")).toEqual([]);
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
describe("IPv4 literal errors", () => {
|
|
99
|
+
it("rejects invalid octet value (>255)", () => {
|
|
100
|
+
const errors = validateDomain("256.1.1.1", true, false);
|
|
101
|
+
expect(errors.some((e) => e.code === "ERR_IPV4_INVALID")).toBe(true);
|
|
102
|
+
});
|
|
103
|
+
it("rejects wrong number of octets", () => {
|
|
104
|
+
const errors = validateDomain("1.2.3", true, false);
|
|
105
|
+
expect(errors.some((e) => e.code === "ERR_IPV4_INVALID")).toBe(true);
|
|
106
|
+
});
|
|
107
|
+
it("rejects non-numeric octet", () => {
|
|
108
|
+
const errors = validateDomain("abc.1.1.1", true, false);
|
|
109
|
+
expect(errors.some((e) => e.code === "ERR_IPV4_INVALID")).toBe(true);
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
describe("IPv6 literal errors", () => {
|
|
113
|
+
it("rejects empty IPv6", () => {
|
|
114
|
+
const errors = validateDomain("", false, true);
|
|
115
|
+
// Empty domain is caught by the general empty check, which takes precedence
|
|
116
|
+
expect(errors.some((e) => e.code === "ERR_DOMAIN_EMPTY")).toBe(true);
|
|
117
|
+
});
|
|
118
|
+
it("rejects invalid hex chars", () => {
|
|
119
|
+
const errors = validateDomain("gggg::1", false, true);
|
|
120
|
+
expect(errors.some((e) => e.code === "ERR_IPV6_INVALID")).toBe(true);
|
|
121
|
+
});
|
|
122
|
+
it("rejects too many segments", () => {
|
|
123
|
+
const errors = validateDomain("1:2:3:4:5:6:7:8:9", false, true);
|
|
124
|
+
expect(errors.some((e) => e.code === "ERR_IPV6_INVALID")).toBe(true);
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
//# sourceMappingURL=domain.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"domain.test.js","sourceRoot":"","sources":["../../../src/email-validator/__tests__/domain.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAC9C,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAE9C,QAAQ,CAAC,gBAAgB,EAAE,GAAG,EAAE;IAC9B,QAAQ,CAAC,iBAAiB,EAAE,GAAG,EAAE;QAC/B,EAAE,CAAC,qBAAqB,EAAE,GAAG,EAAE;YAC7B,MAAM,CAAC,cAAc,CAAC,aAAa,EAAE,KAAK,EAAE,KAAK,CAAC,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;QAClE,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,mBAAmB,EAAE,GAAG,EAAE;YAC3B,MAAM,CAAC,cAAc,CAAC,kBAAkB,EAAE,KAAK,EAAE,KAAK,CAAC,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;QACvE,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,4BAA4B,EAAE,GAAG,EAAE;YACpC,MAAM,CAAC,cAAc,CAAC,eAAe,EAAE,KAAK,EAAE,KAAK,CAAC,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;QACpE,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,mCAAmC,EAAE,GAAG,EAAE;YAC3C,MAAM,CAAC,cAAc,CAAC,eAAe,EAAE,KAAK,EAAE,KAAK,CAAC,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;QACpE,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,0BAA0B,EAAE,GAAG,EAAE;YAClC,MAAM,CAAC,cAAc,CAAC,gBAAgB,EAAE,KAAK,EAAE,KAAK,CAAC,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;QACrE,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,4BAA4B,EAAE,GAAG,EAAE;YACpC,MAAM,CAAC,cAAc,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,CAAC,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;QAC1D,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,kCAAkC,EAAE,GAAG,EAAE;YAC1C,MAAM,KAAK,GAAG,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;YAC7B,MAAM,CAAC,cAAc,CAAC,GAAG,KAAK,MAAM,EAAE,KAAK,EAAE,KAAK,CAAC,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;QACnE,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,QAAQ,CAAC,mBAAmB,EAAE,GAAG,EAAE;QACjC,EAAE,CAAC,sBAAsB,EAAE,GAAG,EAAE;YAC9B,MAAM,CAAC,cAAc,CAAC,aAAa,EAAE,IAAI,EAAE,KAAK,CAAC,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;QACjE,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,mCAAmC,EAAE,GAAG,EAAE;YAC3C,MAAM,CAAC,cAAc,CAAC,SAAS,EAAE,IAAI,EAAE,KAAK,CAAC,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;YAC3D,MAAM,CAAC,cAAc,CAAC,iBAAiB,EAAE,IAAI,EAAE,KAAK,CAAC,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;QACrE,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,sBAAsB,EAAE,GAAG,EAAE;YAC9B,MAAM,CAAC,cAAc,CAAC,aAAa,EAAE,KAAK,EAAE,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;QACjE,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,mBAAmB,EAAE,GAAG,EAAE;YAC3B,MAAM,CAAC,cAAc,CAAC,yCAAyC,EAAE,KAAK,EAAE,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;QAC7F,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,uBAAuB,EAAE,GAAG,EAAE;YAC/B,MAAM,CAAC,cAAc,CAAC,KAAK,EAAE,KAAK,EAAE,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;QACzD,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,mBAAmB,EAAE,GAAG,EAAE;YAC3B,MAAM,CAAC,cAAc,CAAC,SAAS,EAAE,KAAK,EAAE,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;QAC7D,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,QAAQ,CAAC,uBAAuB,EAAE,GAAG,EAAE;QACrC,EAAE,CAAC,oCAAoC,EAAE,GAAG,EAAE;YAC5C,MAAM,MAAM,GAAG,cAAc,CAAC,qBAAqB,EAAE,KAAK,EAAE,KAAK,CAAC,CAAC;YACnE,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,+BAA+B,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACpF,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,kCAAkC,EAAE,GAAG,EAAE;YAC1C,MAAM,MAAM,GAAG,cAAc,CAAC,qBAAqB,EAAE,KAAK,EAAE,KAAK,CAAC,CAAC;YACnE,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,6BAA6B,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAClF,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,wCAAwC,EAAE,GAAG,EAAE;YAChD,MAAM,MAAM,GAAG,cAAc,CAAC,cAAc,EAAE,KAAK,EAAE,KAAK,CAAC,CAAC;YAC5D,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,wBAAwB,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC7E,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,kCAAkC,EAAE,GAAG,EAAE;YAC1C,MAAM,KAAK,GAAG,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;YAC7B,MAAM,MAAM,GAAG,cAAc,CAAC,GAAG,KAAK,MAAM,EAAE,KAAK,EAAE,KAAK,CAAC,CAAC;YAC5D,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,yBAAyB,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC9E,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,+BAA+B,EAAE,GAAG,EAAE;YACvC,MAAM,MAAM,GAAG,cAAc,CAAC,iBAAiB,EAAE,KAAK,EAAE,KAAK,CAAC,CAAC;YAC/D,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,yBAAyB,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC9E,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,4BAA4B,EAAE,GAAG,EAAE;YACpC,MAAM,MAAM,GAAG,cAAc,CAAC,WAAW,EAAE,KAAK,EAAE,KAAK,CAAC,CAAC;YACzD,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,mBAAmB,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACxE,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,qBAAqB,EAAE,GAAG,EAAE;YAC7B,MAAM,MAAM,GAAG,cAAc,CAAC,aAAa,EAAE,KAAK,EAAE,KAAK,CAAC,CAAC;YAC3D,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,wBAAwB,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC7E,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,QAAQ,CAAC,eAAe,EAAE,GAAG,EAAE;QAC7B,EAAE,CAAC,sBAAsB,EAAE,GAAG,EAAE;YAC9B,MAAM,MAAM,GAAG,cAAc,CAAC,EAAE,EAAE,KAAK,EAAE,KAAK,CAAC,CAAC;YAChD,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,kBAAkB,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACvE,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,oCAAoC,EAAE,GAAG,EAAE;YAC5C,MAAM,KAAK,GAAG,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;YAC7B,MAAM,MAAM,GAAG,GAAG,KAAK,IAAI,KAAK,IAAI,KAAK,IAAI,KAAK,IAAI,KAAK,EAAE,CAAC;YAC9D,2BAA2B;YAC3B,MAAM,MAAM,GAAG,cAAc,CAAC,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,GAAG,CAAC,CAAC;YACzD,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,mBAAmB,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACxE,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,uCAAuC,EAAE,GAAG,EAAE;YAC/C,MAAM,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC;YACrC,MAAM,MAAM,GAAG,cAAc,CAAC,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,GAAG,CAAC,CAAC;YACzD,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,mBAAmB,CAAC,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;QAC3E,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,QAAQ,CAAC,qBAAqB,EAAE,GAAG,EAAE;QACnC,EAAE,CAAC,oCAAoC,EAAE,GAAG,EAAE;YAC5C,MAAM,MAAM,GAAG,cAAc,CAAC,WAAW,EAAE,IAAI,EAAE,KAAK,CAAC,CAAC;YACxD,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,kBAAkB,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACvE,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,gCAAgC,EAAE,GAAG,EAAE;YACxC,MAAM,MAAM,GAAG,cAAc,CAAC,OAAO,EAAE,IAAI,EAAE,KAAK,CAAC,CAAC;YACpD,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,kBAAkB,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACvE,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,2BAA2B,EAAE,GAAG,EAAE;YACnC,MAAM,MAAM,GAAG,cAAc,CAAC,WAAW,EAAE,IAAI,EAAE,KAAK,CAAC,CAAC;YACxD,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,kBAAkB,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACvE,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,QAAQ,CAAC,qBAAqB,EAAE,GAAG,EAAE;QACnC,EAAE,CAAC,oBAAoB,EAAE,GAAG,EAAE;YAC5B,MAAM,MAAM,GAAG,cAAc,CAAC,EAAE,EAAE,KAAK,EAAE,IAAI,CAAC,CAAC;YAC/C,4EAA4E;YAC5E,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,kBAAkB,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACvE,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,2BAA2B,EAAE,GAAG,EAAE;YACnC,MAAM,MAAM,GAAG,cAAc,CAAC,SAAS,EAAE,KAAK,EAAE,IAAI,CAAC,CAAC;YACtD,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,kBAAkB,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACvE,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,2BAA2B,EAAE,GAAG,EAAE;YACnC,MAAM,MAAM,GAAG,cAAc,CAAC,mBAAmB,EAAE,KAAK,EAAE,IAAI,CAAC,CAAC;YAChE,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,kBAAkB,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACvE,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { validateLocalPart } from "../local-part.js";
|
|
3
|
+
describe("validateLocalPart", () => {
|
|
4
|
+
describe("valid dot-atom", () => {
|
|
5
|
+
it("accepts simple alphanumeric", () => {
|
|
6
|
+
expect(validateLocalPart("user")).toEqual([]);
|
|
7
|
+
});
|
|
8
|
+
it("accepts single character", () => {
|
|
9
|
+
expect(validateLocalPart("a")).toEqual([]);
|
|
10
|
+
});
|
|
11
|
+
it("accepts all allowed special characters", () => {
|
|
12
|
+
expect(validateLocalPart("a!#$%&'*+-/=?^_`{|}~z")).toEqual([]);
|
|
13
|
+
});
|
|
14
|
+
it("accepts dot in middle", () => {
|
|
15
|
+
expect(validateLocalPart("first.last")).toEqual([]);
|
|
16
|
+
});
|
|
17
|
+
it("accepts multiple dots", () => {
|
|
18
|
+
expect(validateLocalPart("a.b.c")).toEqual([]);
|
|
19
|
+
});
|
|
20
|
+
it("accepts digits", () => {
|
|
21
|
+
expect(validateLocalPart("user123")).toEqual([]);
|
|
22
|
+
});
|
|
23
|
+
it("accepts plus sign (common alias)", () => {
|
|
24
|
+
expect(validateLocalPart("user+tag")).toEqual([]);
|
|
25
|
+
});
|
|
26
|
+
it("accepts 64-char local part", () => {
|
|
27
|
+
const lp = "a".repeat(64);
|
|
28
|
+
expect(validateLocalPart(lp)).toEqual([]);
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
describe("valid quoted strings", () => {
|
|
32
|
+
it("accepts quoted string with spaces", () => {
|
|
33
|
+
expect(validateLocalPart('"hello world"')).toEqual([]);
|
|
34
|
+
});
|
|
35
|
+
it("accepts quoted string with special chars", () => {
|
|
36
|
+
expect(validateLocalPart('"john@doe"')).toEqual([]);
|
|
37
|
+
});
|
|
38
|
+
it("accepts quoted string with escaped chars", () => {
|
|
39
|
+
expect(validateLocalPart('"escaped \\" quote"')).toEqual([]);
|
|
40
|
+
});
|
|
41
|
+
it("accepts empty quoted string", () => {
|
|
42
|
+
expect(validateLocalPart('""')).toEqual([]);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
describe("dot-atom errors", () => {
|
|
46
|
+
it("rejects leading dot", () => {
|
|
47
|
+
const errors = validateLocalPart(".user");
|
|
48
|
+
expect(errors.some((e) => e.code === "ERR_LOCAL_PART_DOT_START")).toBe(true);
|
|
49
|
+
});
|
|
50
|
+
it("rejects trailing dot", () => {
|
|
51
|
+
const errors = validateLocalPart("user.");
|
|
52
|
+
expect(errors.some((e) => e.code === "ERR_LOCAL_PART_DOT_END")).toBe(true);
|
|
53
|
+
});
|
|
54
|
+
it("rejects consecutive dots", () => {
|
|
55
|
+
const errors = validateLocalPart("user..name");
|
|
56
|
+
expect(errors.some((e) => e.code === "ERR_LOCAL_PART_CONSECUTIVE_DOTS")).toBe(true);
|
|
57
|
+
});
|
|
58
|
+
it("rejects invalid char (space)", () => {
|
|
59
|
+
const errors = validateLocalPart("user name");
|
|
60
|
+
expect(errors.some((e) => e.code === "ERR_LOCAL_PART_INVALID_CHAR")).toBe(true);
|
|
61
|
+
});
|
|
62
|
+
it("rejects invalid char (angle bracket)", () => {
|
|
63
|
+
const errors = validateLocalPart("<user>");
|
|
64
|
+
expect(errors.some((e) => e.code === "ERR_LOCAL_PART_INVALID_CHAR")).toBe(true);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
describe("length errors", () => {
|
|
68
|
+
it("rejects empty local part", () => {
|
|
69
|
+
const errors = validateLocalPart("");
|
|
70
|
+
expect(errors.some((e) => e.code === "ERR_LOCAL_PART_EMPTY")).toBe(true);
|
|
71
|
+
});
|
|
72
|
+
it("rejects local part exceeding default 64 chars", () => {
|
|
73
|
+
const lp = "a".repeat(65);
|
|
74
|
+
const errors = validateLocalPart(lp);
|
|
75
|
+
expect(errors.some((e) => e.code === "ERR_LOCAL_PART_LENGTH")).toBe(true);
|
|
76
|
+
});
|
|
77
|
+
it("accepts with custom max length", () => {
|
|
78
|
+
const lp = "a".repeat(10);
|
|
79
|
+
const errors = validateLocalPart(lp, 10);
|
|
80
|
+
expect(errors).toEqual([]);
|
|
81
|
+
});
|
|
82
|
+
it("rejects with custom max length exceeded", () => {
|
|
83
|
+
const lp = "a".repeat(11);
|
|
84
|
+
const errors = validateLocalPart(lp, 10);
|
|
85
|
+
expect(errors.some((e) => e.code === "ERR_LOCAL_PART_LENGTH")).toBe(true);
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
describe("quoted string errors", () => {
|
|
89
|
+
it("rejects unbalanced quotes", () => {
|
|
90
|
+
const errors = validateLocalPart('"unbalanced');
|
|
91
|
+
// Falls through to dot-atom validation since it doesn't start+end with quotes
|
|
92
|
+
expect(errors.some((e) => e.code === "ERR_LOCAL_PART_INVALID_CHAR")).toBe(true);
|
|
93
|
+
});
|
|
94
|
+
it("rejects quote in middle of quoted string", () => {
|
|
95
|
+
const errors = validateLocalPart('"hello"world"');
|
|
96
|
+
expect(errors.some((e) => e.code === "ERR_UNBALANCED_QUOTES")).toBe(true);
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
describe("edge cases", () => {
|
|
100
|
+
it("multiple errors for dot-only local part", () => {
|
|
101
|
+
const errors = validateLocalPart(".");
|
|
102
|
+
expect(errors.length).toBeGreaterThanOrEqual(1);
|
|
103
|
+
});
|
|
104
|
+
it("multiple errors for double-dot-only", () => {
|
|
105
|
+
const errors = validateLocalPart("..");
|
|
106
|
+
expect(errors.length).toBeGreaterThanOrEqual(1);
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
//# sourceMappingURL=local-part.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"local-part.test.js","sourceRoot":"","sources":["../../../src/email-validator/__tests__/local-part.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { parseEmail } from "../parser.js";
|
|
3
|
+
const defaults = { allowDisplayName: true, allowComments: false };
|
|
4
|
+
describe("parseEmail", () => {
|
|
5
|
+
describe("simple addr-spec", () => {
|
|
6
|
+
it("parses user@domain.com", () => {
|
|
7
|
+
const r = parseEmail("user@domain.com", defaults);
|
|
8
|
+
expect(r.ok).toBe(true);
|
|
9
|
+
if (r.ok) {
|
|
10
|
+
expect(r.value.localPart).toBe("user");
|
|
11
|
+
expect(r.value.domain).toBe("domain.com");
|
|
12
|
+
expect(r.value.displayName).toBeNull();
|
|
13
|
+
expect(r.value.isIpLiteral).toBe(false);
|
|
14
|
+
expect(r.value.isIPv6).toBe(false);
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
it("parses a@b.c (minimal)", () => {
|
|
18
|
+
const r = parseEmail("a@b.c", defaults);
|
|
19
|
+
expect(r.ok).toBe(true);
|
|
20
|
+
if (r.ok) {
|
|
21
|
+
expect(r.value.localPart).toBe("a");
|
|
22
|
+
expect(r.value.domain).toBe("b.c");
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
it("parses addr-spec with subdomain", () => {
|
|
26
|
+
const r = parseEmail("user@mail.example.co.uk", defaults);
|
|
27
|
+
expect(r.ok).toBe(true);
|
|
28
|
+
if (r.ok) {
|
|
29
|
+
expect(r.value.localPart).toBe("user");
|
|
30
|
+
expect(r.value.domain).toBe("mail.example.co.uk");
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
describe("display name", () => {
|
|
35
|
+
it("extracts display name from angle-bracket form", () => {
|
|
36
|
+
const r = parseEmail("John Doe <john@example.com>", defaults);
|
|
37
|
+
expect(r.ok).toBe(true);
|
|
38
|
+
if (r.ok) {
|
|
39
|
+
expect(r.value.localPart).toBe("john");
|
|
40
|
+
expect(r.value.domain).toBe("example.com");
|
|
41
|
+
expect(r.value.displayName).toBe("John Doe");
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
it("returns null display name when not present", () => {
|
|
45
|
+
const r = parseEmail("user@domain.com", defaults);
|
|
46
|
+
expect(r.ok).toBe(true);
|
|
47
|
+
if (r.ok) {
|
|
48
|
+
expect(r.value.displayName).toBeNull();
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
it("ignores display name when disabled", () => {
|
|
52
|
+
const r = parseEmail("John <john@example.com>", { allowDisplayName: false });
|
|
53
|
+
expect(r.ok).toBe(true);
|
|
54
|
+
if (r.ok) {
|
|
55
|
+
expect(r.value.localPart).toBe("John <john");
|
|
56
|
+
expect(r.value.domain).toBe("example.com>");
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
describe("comments", () => {
|
|
61
|
+
it("strips comments when enabled", () => {
|
|
62
|
+
const r = parseEmail("user(comment)@domain.com", { allowComments: true });
|
|
63
|
+
expect(r.ok).toBe(true);
|
|
64
|
+
if (r.ok) {
|
|
65
|
+
expect(r.value.localPart).toBe("user");
|
|
66
|
+
expect(r.value.domain).toBe("domain.com");
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
it("rejects comments when disabled", () => {
|
|
70
|
+
const r = parseEmail("user(comment)@domain.com", { allowComments: false });
|
|
71
|
+
expect(r.ok).toBe(false);
|
|
72
|
+
if (!r.ok) {
|
|
73
|
+
expect(r.error.code).toBe("ERR_COMMENTS_NOT_ALLOWED");
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
it("strips multiple comments", () => {
|
|
77
|
+
const r = parseEmail("(hello)user(world)@domain.com", { allowComments: true });
|
|
78
|
+
expect(r.ok).toBe(true);
|
|
79
|
+
if (r.ok) {
|
|
80
|
+
expect(r.value.localPart).toBe("user");
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
it("strips nested comments", () => {
|
|
84
|
+
const r = parseEmail("user((nested))@domain.com", { allowComments: true });
|
|
85
|
+
expect(r.ok).toBe(true);
|
|
86
|
+
if (r.ok) {
|
|
87
|
+
expect(r.value.localPart).toBe("user");
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
describe("IP literals", () => {
|
|
92
|
+
it("detects IPv4 literal", () => {
|
|
93
|
+
const r = parseEmail("user@[192.168.1.1]", defaults);
|
|
94
|
+
expect(r.ok).toBe(true);
|
|
95
|
+
if (r.ok) {
|
|
96
|
+
expect(r.value.domain).toBe("192.168.1.1");
|
|
97
|
+
expect(r.value.isIpLiteral).toBe(true);
|
|
98
|
+
expect(r.value.isIPv6).toBe(false);
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
it("detects IPv6 literal", () => {
|
|
102
|
+
const r = parseEmail("user@[IPv6:2001:db8::1]", defaults);
|
|
103
|
+
expect(r.ok).toBe(true);
|
|
104
|
+
if (r.ok) {
|
|
105
|
+
expect(r.value.domain).toBe("2001:db8::1");
|
|
106
|
+
expect(r.value.isIpLiteral).toBe(true);
|
|
107
|
+
expect(r.value.isIPv6).toBe(true);
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
it("detects IPv6 literal case-insensitively", () => {
|
|
111
|
+
const r = parseEmail("user@[ipv6:fe80::1]", defaults);
|
|
112
|
+
expect(r.ok).toBe(true);
|
|
113
|
+
if (r.ok) {
|
|
114
|
+
expect(r.value.isIPv6).toBe(true);
|
|
115
|
+
expect(r.value.domain).toBe("fe80::1");
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
describe("error cases", () => {
|
|
120
|
+
it("ERR_EMPTY_INPUT for empty string", () => {
|
|
121
|
+
const r = parseEmail("", defaults);
|
|
122
|
+
expect(r.ok).toBe(false);
|
|
123
|
+
if (!r.ok)
|
|
124
|
+
expect(r.error.code).toBe("ERR_EMPTY_INPUT");
|
|
125
|
+
});
|
|
126
|
+
it("ERR_EMPTY_INPUT for whitespace-only", () => {
|
|
127
|
+
const r = parseEmail(" ", defaults);
|
|
128
|
+
expect(r.ok).toBe(false);
|
|
129
|
+
if (!r.ok)
|
|
130
|
+
expect(r.error.code).toBe("ERR_EMPTY_INPUT");
|
|
131
|
+
});
|
|
132
|
+
it("ERR_TOTAL_LENGTH for exceeding max", () => {
|
|
133
|
+
const long = "a".repeat(255) + "@b.c";
|
|
134
|
+
const r = parseEmail(long, { maxTotalLength: 254 });
|
|
135
|
+
expect(r.ok).toBe(false);
|
|
136
|
+
if (!r.ok)
|
|
137
|
+
expect(r.error.code).toBe("ERR_TOTAL_LENGTH");
|
|
138
|
+
});
|
|
139
|
+
it("ERR_NO_AT_SIGN when missing @", () => {
|
|
140
|
+
const r = parseEmail("not-an-email", defaults);
|
|
141
|
+
expect(r.ok).toBe(false);
|
|
142
|
+
if (!r.ok)
|
|
143
|
+
expect(r.error.code).toBe("ERR_NO_AT_SIGN");
|
|
144
|
+
});
|
|
145
|
+
it("ERR_MULTIPLE_AT for double @", () => {
|
|
146
|
+
const r = parseEmail("a@b@c.com", defaults);
|
|
147
|
+
expect(r.ok).toBe(false);
|
|
148
|
+
if (!r.ok)
|
|
149
|
+
expect(r.error.code).toBe("ERR_MULTIPLE_AT");
|
|
150
|
+
});
|
|
151
|
+
it("trims whitespace around input", () => {
|
|
152
|
+
const r = parseEmail(" user@domain.com ", defaults);
|
|
153
|
+
expect(r.ok).toBe(true);
|
|
154
|
+
if (r.ok) {
|
|
155
|
+
expect(r.value.localPart).toBe("user");
|
|
156
|
+
expect(r.value.domain).toBe("domain.com");
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
//# sourceMappingURL=parser.test.js.map
|