Compare commits
11 Commits
7beac62b08
...
2055b62afd
| Author | SHA1 | Date |
|---|---|---|
|
|
2055b62afd | |
|
|
6037bf2bd6 | |
|
|
d6f222c2e0 | |
|
|
0028091f34 | |
|
|
0bcba03393 | |
|
|
6234c27138 | |
|
|
9f4ee690db | |
|
|
a272c62b97 | |
|
|
18a46c9af5 | |
|
|
0ac78360a1 | |
|
|
1710562c54 |
|
|
@ -0,0 +1,18 @@
|
|||
# ===== EternalAI 环境变量配置 =====
|
||||
# 复制此文件为 .env 并填写真实值
|
||||
# cp .env.example .env
|
||||
|
||||
# 数据库连接字符串
|
||||
# 格式: postgresql://用户名:密码@主机:端口/数据库名
|
||||
DATABASE_URL="postgresql://eternalai:YOUR_PASSWORD@localhost:5432/eternalai"
|
||||
|
||||
# JWT 密钥 — 生产环境必须修改为随机长字符串(至少 32 字节)
|
||||
# 生成方法: openssl rand -base64 48
|
||||
JWT_SECRET="PLEASE_CHANGE_THIS_TO_A_RANDOM_SECRET"
|
||||
|
||||
# 服务端口
|
||||
PORT=3001
|
||||
|
||||
# CORS 允许的来源(逗号分隔,生产环境应限制为实际域名)
|
||||
# 留空则允许所有来源(不推荐)
|
||||
ALLOWED_ORIGINS=""
|
||||
|
|
@ -1,26 +1,72 @@
|
|||
name: Deploy EternalAI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- master
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: self-hosted
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '18'
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm install
|
||||
- name: Install dependencies
|
||||
run: npm install
|
||||
|
||||
- name: Build project
|
||||
run: npm run build
|
||||
- name: Generate Prisma Client
|
||||
run: npx prisma generate
|
||||
|
||||
- name: Deploy
|
||||
run: ./scripts/deploy.sh
|
||||
- name: Push database schema
|
||||
run: npx prisma db push
|
||||
env:
|
||||
DATABASE_URL: ${{ secrets.DATABASE_URL }}
|
||||
|
||||
- name: Create logs directory
|
||||
run: mkdir -p logs
|
||||
|
||||
- name: Restart PM2 process
|
||||
run: |
|
||||
if pm2 describe eternalai &>/dev/null; then
|
||||
pm2 reload ecosystem.config.js --update-env
|
||||
echo "PM2 process reloaded"
|
||||
else
|
||||
pm2 start ecosystem.config.js
|
||||
echo "PM2 process started"
|
||||
fi
|
||||
pm2 save
|
||||
|
||||
- name: Health check
|
||||
run: |
|
||||
sleep 3
|
||||
for i in $(seq 1 15); do
|
||||
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:3001 || echo "000")
|
||||
if [ "$HTTP_CODE" = "200" ]; then
|
||||
echo "Health check passed (HTTP $HTTP_CODE)"
|
||||
exit 0
|
||||
fi
|
||||
echo "Waiting for server... (attempt $i, HTTP $HTTP_CODE)"
|
||||
sleep 2
|
||||
done
|
||||
echo "Health check failed"
|
||||
pm2 logs eternalai --lines 30 --nostream
|
||||
exit 1
|
||||
|
||||
- name: Reload Nginx
|
||||
run: sudo nginx -t && sudo nginx -s reload || echo "Nginx reload skipped"
|
||||
|
||||
- name: Deploy summary
|
||||
run: |
|
||||
echo "============================================"
|
||||
echo " Deploy completed: $(git rev-parse --short HEAD)"
|
||||
echo " $(git log -1 --format='%s')"
|
||||
echo "============================================"
|
||||
|
|
|
|||
|
|
@ -1 +1,7 @@
|
|||
node_modules/
|
||||
.DS_Store
|
||||
*.log
|
||||
.env
|
||||
test-results/
|
||||
playwright-report/
|
||||
logs/
|
||||
|
|
|
|||
311
README.md
311
README.md
|
|
@ -1,15 +1,306 @@
|
|||
# EternalAI
|
||||
# Eternal AI
|
||||
|
||||
一个永恒的 AI 系统,旨在长期运行和自我进化。
|
||||
AI 陪伴平台 — 人设创作者设定并发布角色,自动生成 Hermes Agent 可用的配置文件(SOUL.md + config.yaml),支持跨机器 CLI 拉取部署。
|
||||
|
||||
## 项目结构
|
||||
## 架构
|
||||
|
||||
- `src/` - 源代码
|
||||
- `docs/` - 文档
|
||||
- `tests/` - 测试文件
|
||||
- `scripts/` - 脚本
|
||||
- `configs/` - 配置文件
|
||||
```
|
||||
┌──────────────────────────────────────────────────────────┐
|
||||
│ 浏览器 (SPA) │
|
||||
│ index.html + app.js + styles.css │
|
||||
│ ┌──────────┐ ┌──────────┐ ┌───────────┐ ┌───────────┐ │
|
||||
│ │ 认证视图 │ │ 角色库 │ │ 创作者中心 │ │ 角色编辑器 │ │
|
||||
│ └──────────┘ └──────────┘ └───────────┘ └───────────┘ │
|
||||
└──────────────────────┬───────────────────────────────────┘
|
||||
│ HTTP / JSON
|
||||
┌──────────────────────▼───────────────────────────────────┐
|
||||
│ Express.js 服务端 (server.js) │
|
||||
│ ┌─────────┐ ┌─────────┐ ┌──────────┐ ┌───────────────┐ │
|
||||
│ │ /api/auth│ │/api/roles│ │/api/apikeys│ │ /api/hermes │ │
|
||||
│ └────┬────┘ └────┬────┘ └─────┬────┘ └──────┬────────┘ │
|
||||
│ │ │ │ │ │
|
||||
│ ┌────▼───────────▼────────────▼──────────────▼────────┐ │
|
||||
│ │ 认证中间件 (src/lib/auth.js) │ │
|
||||
│ │ JWT 认证 (authMiddleware) + API Key 认证 │ │
|
||||
│ │ (apiKeyMiddleware, eak_ 前缀, SHA-256 哈希) │ │
|
||||
│ └─────────────────────┬───────────────────────────────┘ │
|
||||
└────────────────────────┼─────────────────────────────────┘
|
||||
│ Prisma ORM
|
||||
┌────────────────────────▼─────────────────────────────────┐
|
||||
│ PostgreSQL 数据库 │
|
||||
│ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ │
|
||||
│ │ User │ │ Role │ │ ApiKey │ │ Order │ │
|
||||
│ └────────┘ └────────┘ └────────┘ └────────┘ │
|
||||
└──────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## 部署
|
||||
### 技术栈
|
||||
|
||||
该项目将通过 Gitea Actions 自动部署。
|
||||
| 层 | 技术 |
|
||||
|----|------|
|
||||
| 前端 | 原生 HTML5 SPA(IIFE 模式,无框架)、玻璃拟态 UI |
|
||||
| 后端 | Node.js + Express 5.x |
|
||||
| 数据库 | PostgreSQL 15 + Prisma ORM 5.x |
|
||||
| 认证 | JWT(7 天有效期)+ API Key(`eak_` 前缀,SHA-256 哈希存储) |
|
||||
| 密码 | bcryptjs(10 轮盐) |
|
||||
| 测试 | Playwright E2E(35 个测试用例) |
|
||||
| 部署 | PM2 + Nginx 反向代理 |
|
||||
|
||||
### 项目结构
|
||||
|
||||
```
|
||||
EternalAI/
|
||||
├── server.js # Express 入口,注册路由 + 静态文件
|
||||
├── app.js # 前端 SPA 逻辑(路由、状态、表单、API 调用)
|
||||
├── index.html # 单页应用入口(9 个视图)
|
||||
├── styles.css # 玻璃拟态样式
|
||||
├── prisma/
|
||||
│ └── schema.prisma # 数据模型定义(User / Role / ApiKey / Order)
|
||||
├── src/
|
||||
│ ├── lib/
|
||||
│ │ ├── auth.js # JWT + API Key 认证中间件
|
||||
│ │ └── prisma.js # Prisma Client 单例
|
||||
│ └── routes/
|
||||
│ ├── auth.js # 注册 / 登录 / 用户信息 / 设置
|
||||
│ ├── roles.js # 角色 CRUD + 角色库
|
||||
│ ├── apikeys.js # API Key 生成 / 列表 / 删除
|
||||
│ └── hermes.js # Hermes 配置拉取端点(SOUL.md / config.yaml)
|
||||
├── deploy/
|
||||
│ ├── setup-server.sh # 服务器一键初始化脚本
|
||||
│ ├── deploy.sh # CI/CD 部署脚本
|
||||
│ └── nginx.conf # Nginx 反向代理配置
|
||||
├── e2e/ # Playwright E2E 测试
|
||||
├── docs/ # 需求文档与规划
|
||||
└── package.json
|
||||
```
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 本地开发
|
||||
|
||||
```bash
|
||||
# 1. 安装依赖
|
||||
npm install
|
||||
|
||||
# 2. 配置环境变量
|
||||
cp .env.example .env
|
||||
# 编辑 .env,设置 DATABASE_URL 和 JWT_SECRET
|
||||
# JWT_SECRET 可用 openssl rand -hex 32 生成
|
||||
|
||||
# 3. 初始化数据库
|
||||
npx prisma db push
|
||||
npx prisma generate
|
||||
|
||||
# 4. 启动开发服务器
|
||||
npm run dev
|
||||
# → http://localhost:3001
|
||||
```
|
||||
|
||||
### 服务器部署
|
||||
|
||||
```bash
|
||||
# 在服务器上执行一键初始化脚本
|
||||
bash deploy/setup-server.sh
|
||||
# 自动安装 Node.js / PostgreSQL / Nginx / PM2,创建 .env,推送 Schema
|
||||
```
|
||||
|
||||
## 操作流程
|
||||
|
||||
### 1. 用户注册与登录
|
||||
|
||||
```
|
||||
用户 → 首页 → 登录/注册视图
|
||||
├─ 注册:填写账号 + 密码(≥6位)→ 自动登录,返回 JWT
|
||||
└─ 登录:账号 + 密码 → 验证成功,返回 JWT
|
||||
└─ JWT 存入 localStorage,后续请求携带 Authorization: Bearer <token>
|
||||
```
|
||||
|
||||
### 2. 创作者入驻与角色设定
|
||||
|
||||
```
|
||||
登录用户 → 创作者入驻页 → 开通创作者身份
|
||||
→ 创作者管理中心(三个 Tab)
|
||||
├─ 角色 Tab:新建 / 编辑角色
|
||||
├─ 收入 Tab:余额 + 付费流水
|
||||
└─ 设置 Tab:昵称 / 角色库名 / API Key 管理
|
||||
|
||||
新建角色(4 步表单):
|
||||
Step 1 → 基础信息(名称、性别、年龄、关系)
|
||||
Step 2 → 性格设定(性格、背景、说话风格、喜好)
|
||||
Step 3 → 模型配置(模型、温度、最大 Token、记忆/工具开关)
|
||||
Step 4 → 发布设置(问候语、价格、描述)
|
||||
→ 提交后自动生成 SOUL.md + config.yaml 存入数据库
|
||||
→ 角色上架到角色库
|
||||
```
|
||||
|
||||
### 3. 角色库浏览与购买
|
||||
|
||||
```
|
||||
任意用户 → 角色库 → 浏览已上架角色
|
||||
→ 点击角色卡片 → 查看详情
|
||||
→ 付款 → 创建订单 → 解锁角色
|
||||
```
|
||||
|
||||
## 与 Hermes Agent 交互流程
|
||||
|
||||
EternalAI 生成的角色配置(SOUL.md + config.yaml)可通过 CLI 拉取命令部署到任意 Hermes Agent 机器上,支持跨机器部署。
|
||||
|
||||
### 架构图
|
||||
|
||||
```
|
||||
服务器 A (EternalAI) 服务器 B (Hermes Agent)
|
||||
┌───────────────────────┐ ┌───────────────────────┐
|
||||
│ 1. 设置页生成 API Key │ │ │
|
||||
│ (eak_xxxxxxxxxx) │ │ 5. hermes profile │
|
||||
│ │ │ create role-mio │
|
||||
│ 2. 创建角色 → 发布 │ │ │
|
||||
│ 生成 SOUL.md + │ │ 6. curl 拉取文件 │
|
||||
│ config.yaml │ │ SOUL.md │
|
||||
│ │ │ config.yaml │
|
||||
│ 3. 角色管理页显示 │ 4. 用户复制 │ │
|
||||
│ curl 命令模板 │──── curl ────▶│ 7. 配置 .env │
|
||||
│ (含 API Key) │ 命令执行 │ (API 密钥) │
|
||||
│ │ │ │
|
||||
│ │ │ 8. role-mio chat │
|
||||
└───────────────────────┘ └───────────────────────┘
|
||||
```
|
||||
|
||||
### 步骤详解
|
||||
|
||||
#### Step 1-3:在 EternalAI Web UI 操作
|
||||
|
||||
1. **生成 API Key**:创作者管理中心 → 设置 Tab → 点击「生成 API Key」→ 明文 Key 仅显示一次,请妥善保存
|
||||
2. **创建角色**:创作者管理中心 → 角色 Tab → 新建角色 → 填写 4 步表单 → 发布
|
||||
3. **获取部署命令**:角色卡片点击「Hermes 部署」→ 弹窗显示完整的 curl 命令模板(含服务器地址、角色 ID、API Key)→ 点击复制
|
||||
|
||||
#### Step 4-8:在 Hermes Agent 机器上操作
|
||||
|
||||
```bash
|
||||
# 4. 复制 curl 命令到 Hermes 机器执行
|
||||
|
||||
# 5. 创建 Hermes profile
|
||||
hermes profile create role-mio
|
||||
|
||||
# 6. 拉取 SOUL.md
|
||||
curl -H "Authorization: Bearer eak_xxx" \
|
||||
https://your-eternalai-domain.com/api/hermes/roles/<role-id>/SOUL.md \
|
||||
-o ~/.hermes/profiles/role-mio/SOUL.md
|
||||
|
||||
# 7. 拉取 config.yaml
|
||||
curl -H "Authorization: Bearer eak_xxx" \
|
||||
https://your-eternalai-domain.com/api/hermes/roles/<role-id>/config.yaml \
|
||||
-o ~/.hermes/profiles/role-mio/config.yaml
|
||||
|
||||
# 8. 配置 API 密钥并启动
|
||||
echo "OPENROUTER_API_KEY=sk-or-xxx" > ~/.hermes/profiles/role-mio/.env
|
||||
role-mio chat
|
||||
```
|
||||
|
||||
### config.yaml 格式适配
|
||||
|
||||
EternalAI 角色数据自动转换为 Hermes config.yaml 格式:
|
||||
|
||||
| EternalAI 字段 | Hermes config.yaml 字段 |
|
||||
|---|---|
|
||||
| `role.model` | `model:` |
|
||||
| `role.temperature` | `temperature:` |
|
||||
| `role.maxTokens` | `max_tokens:` |
|
||||
| `role.enableMemory` | `memory.enabled:` |
|
||||
| `role.enableTools` | `tools.enabled:` |
|
||||
|
||||
生成的 config.yaml 示例:
|
||||
|
||||
```yaml
|
||||
# Hermes Agent Config — generated by EternalAI
|
||||
# Character: Mio
|
||||
# Agent ID: default
|
||||
# Generated at: 2026-06-21T12:00:00.000Z
|
||||
|
||||
model: "gpt-4o"
|
||||
temperature: 0.85
|
||||
max_tokens: 2048
|
||||
|
||||
memory:
|
||||
enabled: true
|
||||
storage: local
|
||||
|
||||
tools:
|
||||
enabled: true
|
||||
toolsets:
|
||||
- memory
|
||||
|
||||
terminal:
|
||||
backend: local
|
||||
```
|
||||
|
||||
### API Key 安全机制
|
||||
|
||||
- **格式**:`eak_` + 32 位随机 hex(128 位熵)
|
||||
- **存储**:SHA-256 哈希存储,明文仅在生成时返回一次
|
||||
- **脱敏显示**:列表中仅显示前 12 位(如 `eak_a1b2c3d4`)
|
||||
- **认证**:通过 `Authorization: Bearer eak_xxx` 头传递
|
||||
- **权限**:API Key 只能拉取所属用户创建的角色配置
|
||||
- **可吊销**:随时在设置页删除,删除后立即失效
|
||||
|
||||
## API 参考
|
||||
|
||||
### 认证 API
|
||||
|
||||
| 方法 | 路径 | 认证 | 说明 |
|
||||
|------|------|------|------|
|
||||
| POST | `/api/auth/register` | 无 | 注册(账号 + 密码) |
|
||||
| POST | `/api/auth/login` | 无 | 登录 |
|
||||
| GET | `/api/auth/me` | JWT | 获取当前用户信息 |
|
||||
| PUT | `/api/auth/settings` | JWT | 更新用户设置 |
|
||||
|
||||
### 角色 API
|
||||
|
||||
| 方法 | 路径 | 认证 | 说明 |
|
||||
|------|------|------|------|
|
||||
| GET | `/api/roles` | 无 | 获取角色库(已上架) |
|
||||
| GET | `/api/roles/:id` | 无 | 获取角色详情 |
|
||||
| GET | `/api/roles/my/roles` | JWT | 获取我创建的角色 |
|
||||
| POST | `/api/roles` | JWT | 发布新角色 |
|
||||
| PUT | `/api/roles/:id` | JWT | 编辑角色 |
|
||||
| GET | `/api/roles/:id/full` | JWT | 获取角色完整信息(含 SOUL.md) |
|
||||
|
||||
### API Key 管理
|
||||
|
||||
| 方法 | 路径 | 认证 | 说明 |
|
||||
|------|------|------|------|
|
||||
| POST | `/api/apikeys` | JWT | 生成 API Key(明文仅返回一次) |
|
||||
| GET | `/api/apikeys` | JWT | 列出所有 API Key(脱敏) |
|
||||
| DELETE | `/api/apikeys/:id` | JWT | 删除 API Key |
|
||||
|
||||
### Hermes 配置拉取
|
||||
|
||||
| 方法 | 路径 | 认证 | 说明 |
|
||||
|------|------|------|------|
|
||||
| GET | `/api/hermes/roles/:id/SOUL.md` | API Key / JWT | 拉取 SOUL.md(text/plain) |
|
||||
| GET | `/api/hermes/roles/:id/config.yaml` | API Key / JWT | 拉取 config.yaml(text/plain) |
|
||||
|
||||
## 测试
|
||||
|
||||
```bash
|
||||
# 运行全部 E2E 测试(35 个用例)
|
||||
npm test
|
||||
|
||||
# 查看测试报告
|
||||
npx playwright show-report
|
||||
```
|
||||
|
||||
测试覆盖:认证流程、角色发布/编辑、角色库浏览、导航与可访问性。
|
||||
|
||||
## 安全
|
||||
|
||||
- **JWT 密钥**:生产环境必须设置 `JWT_SECRET` 环境变量,未设置时 fail-fast 抛错
|
||||
- **密码存储**:bcrypt 哈希(10 轮盐),不存明文
|
||||
- **API Key 存储**:SHA-256 哈希存储,明文仅显示一次
|
||||
- **XSS 防护**:所有用户可控数据通过 `escapeHtml()` 转义后插入 DOM
|
||||
- **YAML 注入防护**:config.yaml 生成时对用户可控字段做双引号转义和换行符过滤
|
||||
- **所有权校验**:所有角色操作验证 `creatorId === req.userId`
|
||||
- **.env 文件权限**:部署脚本自动设置 `chmod 600`
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
|
|
|
|||
535
app.js
535
app.js
|
|
@ -1,6 +1,46 @@
|
|||
(() => {
|
||||
'use strict';
|
||||
|
||||
// --- API helper ---
|
||||
const API_BASE = '/api';
|
||||
const TOKEN_KEY = 'eternal_ai_token';
|
||||
|
||||
// 安全:转义 HTML,防止存储型 XSS(用于 innerHTML 插入的用户可控数据)
|
||||
function escapeHtml(str) {
|
||||
return String(str).replace(/[&<>"']/g, (ch) => {
|
||||
const map = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' };
|
||||
return map[ch];
|
||||
});
|
||||
}
|
||||
|
||||
function getToken() {
|
||||
return localStorage.getItem(TOKEN_KEY) || '';
|
||||
}
|
||||
|
||||
function setToken(token) {
|
||||
if (token) localStorage.setItem(TOKEN_KEY, token);
|
||||
else localStorage.removeItem(TOKEN_KEY);
|
||||
}
|
||||
|
||||
async function api(path, options = {}) {
|
||||
const token = getToken();
|
||||
const headers = { 'Content-Type': 'application/json', ...(options.headers || {}) };
|
||||
if (token) headers['Authorization'] = `Bearer ${token}`;
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}${path}`, { ...options, headers });
|
||||
const data = await res.json();
|
||||
if (!res.ok) {
|
||||
throw new Error(data.error || `请求失败 (${res.status})`);
|
||||
}
|
||||
return data;
|
||||
} catch (err) {
|
||||
if (err.message === 'Failed to fetch') {
|
||||
throw new Error('无法连接服务器,请检查网络');
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
// --- U9: Unified state management ---
|
||||
const STORAGE_KEY = 'eternal_ai_state';
|
||||
|
||||
|
|
@ -8,6 +48,7 @@
|
|||
isLoggedIn: false,
|
||||
isCreator: false,
|
||||
account: null,
|
||||
userId: null,
|
||||
boundCreator: null,
|
||||
libraryName: '我的 [XXX]',
|
||||
creatorName: '',
|
||||
|
|
@ -93,6 +134,7 @@
|
|||
let activeAuthTab = 'login';
|
||||
let activeCenterTab = 'roles';
|
||||
let currentRole = null;
|
||||
let editingRoleId = null;
|
||||
let viewHistory = ['landing'];
|
||||
|
||||
// --- U9: Unified showView with history and tab-bar sync ---
|
||||
|
|
@ -231,10 +273,13 @@
|
|||
return true;
|
||||
}
|
||||
|
||||
function login(account) {
|
||||
function applyUserData(user) {
|
||||
state.isLoggedIn = true;
|
||||
state.account = account;
|
||||
state.boundCreator = state.boundCreator || { name: '云朵', roles: mockRoles };
|
||||
state.userId = user.id;
|
||||
state.account = user.account;
|
||||
state.isCreator = user.isCreator || false;
|
||||
state.creatorName = user.creatorName || '';
|
||||
state.libraryName = user.libraryName || '我的角色库';
|
||||
saveState();
|
||||
updateLandingCard();
|
||||
}
|
||||
|
|
@ -243,55 +288,74 @@
|
|||
state.isLoggedIn = false;
|
||||
state.isCreator = false;
|
||||
state.account = null;
|
||||
state.userId = null;
|
||||
state.boundCreator = null;
|
||||
state.roles = [];
|
||||
state.income = { balance: 0, records: [] };
|
||||
setToken('');
|
||||
saveState();
|
||||
updateLandingCard();
|
||||
showView('landing');
|
||||
}
|
||||
|
||||
// --- U2: Role Library ---
|
||||
function renderRoleLibrary() {
|
||||
async function renderRoleLibrary() {
|
||||
const listEl = document.getElementById('role-list');
|
||||
const emptyEl = document.getElementById('library-empty');
|
||||
const titleEl = document.getElementById('library-title');
|
||||
|
||||
titleEl.textContent = state.libraryName || '我的角色库';
|
||||
|
||||
if (!state.boundCreator) {
|
||||
listEl.innerHTML = '';
|
||||
emptyEl.hidden = false;
|
||||
return;
|
||||
}
|
||||
|
||||
listEl.innerHTML = '<p style="text-align:center;color:var(--text-muted)">加载中…</p>';
|
||||
emptyEl.hidden = true;
|
||||
listEl.innerHTML = mockRoles
|
||||
.map(
|
||||
(role) => `
|
||||
<article class="role-card" data-role-id="${role.id}" role="button" tabindex="0" aria-label="${role.name},${role.desc},每月${role.price}元">
|
||||
<img class="role-card__avatar" src="${role.avatar}" alt="${role.name}" />
|
||||
|
||||
try {
|
||||
const { roles } = await api('/roles');
|
||||
if (roles.length === 0) {
|
||||
listEl.innerHTML = '';
|
||||
emptyEl.hidden = false;
|
||||
return;
|
||||
}
|
||||
listEl.innerHTML = roles
|
||||
.map((role) => {
|
||||
const id = escapeHtml(role.id);
|
||||
const name = escapeHtml(role.displayName);
|
||||
const desc = escapeHtml(role.desc || '');
|
||||
const avatar = escapeHtml(role.avatar || 'https://trae-api-cn.mchost.guru/api/ide/v1/text_to_image?prompt=anime%20character%20portrait&image_size=square');
|
||||
const price = escapeHtml(role.price);
|
||||
return `
|
||||
<article class="role-card" data-role-id="${id}" role="button" tabindex="0" aria-label="${name},${desc},每月${price}元">
|
||||
<img class="role-card__avatar" src="${avatar}" alt="${name}" />
|
||||
<div class="role-card__info">
|
||||
<h3 class="role-card__name">${role.name}</h3>
|
||||
<p class="role-card__desc">${role.desc}</p>
|
||||
<span class="role-card__price">¥${role.price}</span>
|
||||
<h3 class="role-card__name">${name}</h3>
|
||||
<p class="role-card__desc">${desc}</p>
|
||||
<span class="role-card__price">¥${price}</span>
|
||||
</div>
|
||||
</article>`
|
||||
)
|
||||
.join('');
|
||||
</article>`;
|
||||
})
|
||||
.join('');
|
||||
} catch (err) {
|
||||
listEl.innerHTML = `<p style="text-align:center;color:var(--text-muted)">加载失败:${escapeHtml(err.message)}</p>`;
|
||||
}
|
||||
}
|
||||
|
||||
// --- U3: Role Detail ---
|
||||
function renderRoleDetail(roleId) {
|
||||
const role = mockRoles.find((r) => r.id === roleId);
|
||||
if (!role) return;
|
||||
currentRole = role;
|
||||
async function renderRoleDetail(roleId) {
|
||||
try {
|
||||
const { role } = await api(`/roles/${roleId}`);
|
||||
currentRole = role;
|
||||
|
||||
document.getElementById('detail-name').textContent = role.name;
|
||||
document.getElementById('detail-hero').style.backgroundImage = `url(${role.avatar})`;
|
||||
document.getElementById('detail-role-name').textContent = role.name;
|
||||
document.getElementById('detail-role-desc').textContent = role.desc;
|
||||
document.getElementById('detail-price').innerHTML = `<span class="detail-price__value">¥${role.price}</span><span class="detail-price__unit">/ 月</span>`;
|
||||
document.getElementById('detail-name').textContent = role.displayName;
|
||||
document.getElementById('detail-hero').style.backgroundImage = `url(${role.avatar || ''})`;
|
||||
document.getElementById('detail-role-name').textContent = role.displayName;
|
||||
document.getElementById('detail-role-desc').textContent = role.desc || role.personality || '';
|
||||
document.getElementById('detail-price').innerHTML = `<span class="detail-price__value">¥${escapeHtml(role.price)}</span><span class="detail-price__unit">/ 月</span>`;
|
||||
|
||||
document.getElementById('detail-actions-pre').hidden = false;
|
||||
document.getElementById('detail-paid').hidden = true;
|
||||
document.getElementById('detail-actions-pre').hidden = false;
|
||||
document.getElementById('detail-paid').hidden = true;
|
||||
} catch (err) {
|
||||
alert('加载角色详情失败:' + err.message);
|
||||
goBack();
|
||||
}
|
||||
}
|
||||
|
||||
function payRole() {
|
||||
|
|
@ -348,22 +412,41 @@
|
|||
renderSettings();
|
||||
}
|
||||
|
||||
function renderCreatorRoles() {
|
||||
async function renderCreatorRoles() {
|
||||
const listEl = document.getElementById('creator-role-list');
|
||||
const roles = state.roles.length > 0 ? state.roles : mockRoles;
|
||||
listEl.innerHTML = roles
|
||||
.map(
|
||||
(role) => `
|
||||
<article class="role-card" data-role-id="${role.id}" role="button" tabindex="0" aria-label="${role.name},${role.status === 'running' ? '运行中' : '已停止'}">
|
||||
<img class="role-card__avatar" src="${role.avatar}" alt="${role.name}" />
|
||||
listEl.innerHTML = '<p style="text-align:center;color:var(--text-muted)">加载中…</p>';
|
||||
try {
|
||||
const { roles } = await api('/roles/my/roles');
|
||||
state.roles = roles;
|
||||
saveState();
|
||||
if (roles.length === 0) {
|
||||
listEl.innerHTML = '<p style="text-align:center;color:var(--text-muted)">还没有创建角色,点击「新建角色」开始</p>';
|
||||
return;
|
||||
}
|
||||
listEl.innerHTML = roles
|
||||
.map((role) => {
|
||||
const id = escapeHtml(role.id);
|
||||
const name = escapeHtml(role.displayName);
|
||||
const avatar = escapeHtml(role.avatar || 'https://trae-api-cn.mchost.guru/api/ide/v1/text_to_image?prompt=anime%20character%20portrait&image_size=square');
|
||||
const statusText = role.status === 'running' ? '运行中' : '已停止';
|
||||
const statusClass = escapeHtml(role.status);
|
||||
return `
|
||||
<article class="role-card" data-role-id="${id}" role="button" tabindex="0" aria-label="${name},${statusText}">
|
||||
<img class="role-card__avatar" src="${avatar}" alt="${name}" />
|
||||
<div class="role-card__info">
|
||||
<h3 class="role-card__name">${role.name}</h3>
|
||||
<span class="role-card__status role-card__status--${role.status}">${role.status === 'running' ? '运行中' : '已停止'}</span>
|
||||
<h3 class="role-card__name">${name}</h3>
|
||||
<span class="role-card__status role-card__status--${statusClass}">${statusText}</span>
|
||||
</div>
|
||||
<button class="btn btn--small btn--outline" type="button" data-action="edit-role" data-role-id="${role.id}">编辑</button>
|
||||
</article>`
|
||||
)
|
||||
.join('');
|
||||
<div style="display:flex;gap:0.5rem;">
|
||||
<button class="btn btn--small btn--outline" type="button" data-action="edit-role" data-role-id="${id}">编辑</button>
|
||||
<button class="btn btn--small btn--outline" type="button" data-action="hermes-deploy" data-role-id="${id}" data-role-name="${name}">Hermes 部署</button>
|
||||
</div>
|
||||
</article>`;
|
||||
})
|
||||
.join('');
|
||||
} catch (err) {
|
||||
listEl.innerHTML = `<p style="text-align:center;color:var(--text-muted)">加载失败:${escapeHtml(err.message)}</p>`;
|
||||
}
|
||||
}
|
||||
|
||||
function renderIncome() {
|
||||
|
|
@ -375,22 +458,162 @@
|
|||
return;
|
||||
}
|
||||
listEl.innerHTML = income.records
|
||||
.map(
|
||||
(r) => `
|
||||
.map((r) => {
|
||||
const role = escapeHtml(r.role);
|
||||
const time = escapeHtml(r.time);
|
||||
const amount = r.amount.toFixed(2);
|
||||
return `
|
||||
<div class="income-record">
|
||||
<div class="income-record__info">
|
||||
<span class="income-record__role">${r.role}</span>
|
||||
<span class="income-record__time">${r.time}</span>
|
||||
<span class="income-record__role">${role}</span>
|
||||
<span class="income-record__time">${time}</span>
|
||||
</div>
|
||||
<span class="income-record__amount">+¥${r.amount.toFixed(2)}</span>
|
||||
</div>`
|
||||
)
|
||||
<span class="income-record__amount">+¥${amount}</span>
|
||||
</div>`;
|
||||
})
|
||||
.join('');
|
||||
}
|
||||
|
||||
function renderSettings() {
|
||||
document.getElementById('settings-name').value = state.creatorName || '';
|
||||
document.getElementById('settings-library').value = state.libraryName === '我的 [XXX]' ? '' : state.libraryName;
|
||||
renderApiKeys();
|
||||
}
|
||||
|
||||
// --- API Key 管理 ---
|
||||
async function renderApiKeys() {
|
||||
const listEl = document.getElementById('apikey-list');
|
||||
if (!listEl) return;
|
||||
listEl.innerHTML = '<p style="color:var(--text-muted);font-size:0.875rem;">加载中…</p>';
|
||||
try {
|
||||
const { apiKeys } = await api('/apikeys');
|
||||
if (apiKeys.length === 0) {
|
||||
listEl.innerHTML = '<p style="color:var(--text-muted);font-size:0.875rem;">还没有 API Key</p>';
|
||||
return;
|
||||
}
|
||||
listEl.innerHTML = apiKeys
|
||||
.map((k) => {
|
||||
const id = escapeHtml(k.id);
|
||||
const name = escapeHtml(k.name);
|
||||
const prefix = escapeHtml(k.keyPrefix);
|
||||
const created = new Date(k.createdAt).toLocaleDateString('zh-CN');
|
||||
const lastUsed = k.lastUsedAt ? new Date(k.lastUsedAt).toLocaleString('zh-CN') : '从未使用';
|
||||
return `
|
||||
<div class="apikey-item" style="padding:0.75rem 0;border-bottom:1px solid var(--border);">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;">
|
||||
<div>
|
||||
<strong>${name}</strong>
|
||||
<code style="margin-left:0.5rem;color:var(--text-muted);">${prefix}…</code>
|
||||
</div>
|
||||
<button class="btn btn--small btn--ghost" type="button" data-action="delete-apikey" data-apikey-id="${id}">删除</button>
|
||||
</div>
|
||||
<div style="font-size:0.75rem;color:var(--text-muted);margin-top:0.25rem;">
|
||||
创建于 ${created} · 最后使用:${lastUsed}
|
||||
</div>
|
||||
</div>`;
|
||||
})
|
||||
.join('');
|
||||
} catch (err) {
|
||||
listEl.innerHTML = `<p style="color:var(--text-muted);font-size:0.875rem;">加载失败:${escapeHtml(err.message)}</p>`;
|
||||
}
|
||||
}
|
||||
|
||||
async function generateApiKey() {
|
||||
try {
|
||||
const { apiKey } = await api('/apikeys', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ name: 'hermes-deploy' }),
|
||||
});
|
||||
// 明文 Key 只显示一次
|
||||
alert(`API Key 已生成(仅显示一次,请妥善保存):\n\n${apiKey.key}\n\n复制此 Key,在 Hermes 机器上使用 curl 命令时需要带上。`);
|
||||
renderApiKeys();
|
||||
} catch (err) {
|
||||
alert(`生成失败:${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteApiKey(id) {
|
||||
if (!confirm('确定删除此 API Key?删除后无法恢复。')) return;
|
||||
try {
|
||||
await api(`/apikeys/${id}`, { method: 'DELETE' });
|
||||
renderApiKeys();
|
||||
} catch (err) {
|
||||
alert(`删除失败:${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// --- Hermes 部署指南 ---
|
||||
function showHermesDeployGuide(roleId, roleName) {
|
||||
// 获取当前服务器地址
|
||||
const baseUrl = window.location.origin;
|
||||
const profileName = `role-${roleName.toLowerCase().replace(/[^a-z0-9]/g, '-').slice(0, 20)}`;
|
||||
const soulUrl = `${baseUrl}/api/hermes/roles/${roleId}/SOUL.md`;
|
||||
const configUrl = `${baseUrl}/api/hermes/roles/${roleId}/config.yaml`;
|
||||
|
||||
const soulCmd = `curl -H "Authorization: Bearer YOUR_API_KEY" \\\n ${soulUrl} \\\n -o ~/.hermes/profiles/${profileName}/SOUL.md`;
|
||||
const configCmd = `curl -H "Authorization: Bearer YOUR_API_KEY" \\\n ${configUrl} \\\n -o ~/.hermes/profiles/${profileName}/config.yaml`;
|
||||
|
||||
// 移除已有弹窗
|
||||
const existing = document.getElementById('hermes-modal');
|
||||
if (existing) existing.remove();
|
||||
|
||||
const modal = document.createElement('div');
|
||||
modal.id = 'hermes-modal';
|
||||
modal.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.5);display:flex;align-items:center;justify-content:center;z-index:9999;padding:1rem;';
|
||||
modal.innerHTML = `
|
||||
<div style="background:var(--bg-card,#fff);border-radius:12px;max-width:640px;width:100%;max-height:85vh;overflow-y:auto;padding:1.5rem;box-shadow:0 8px 32px rgba(0,0,0,0.2);">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:1rem;">
|
||||
<h3 style="margin:0;">Hermes Agent 部署指南</h3>
|
||||
<button class="btn btn--small btn--ghost" type="button" data-action="close-hermes-modal">✕</button>
|
||||
</div>
|
||||
|
||||
<p style="color:var(--text-muted);font-size:0.875rem;margin-bottom:1rem;">
|
||||
角色「${escapeHtml(roleName)}」的配置文件可通过以下命令拉取到 Hermes Agent 所在的机器。
|
||||
</p>
|
||||
|
||||
<div style="background:var(--bg-muted,#f5f5f5);border-radius:8px;padding:0.75rem;margin-bottom:1rem;">
|
||||
<strong style="font-size:0.875rem;">前提条件</strong>
|
||||
<ul style="margin:0.5rem 0 0 1.25rem;padding:0;font-size:0.8125rem;color:var(--text-muted);">
|
||||
<li>已安装 Hermes Agent(<code>curl -fsSL https://hermes-agent.nousresearch.com/install.sh | bash</code>)</li>
|
||||
<li>已在 EternalAI 设置页生成 API Key</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<h4 style="font-size:0.9375rem;margin-bottom:0.5rem;">1. 创建 Hermes Profile</h4>
|
||||
<pre style="background:var(--bg-muted,#1e1e1e);color:var(--text-light,#eee);border-radius:8px;padding:0.75rem;overflow-x:auto;font-size:0.8125rem;margin-bottom:1rem;"><code>hermes profile create ${escapeHtml(profileName)}</code></pre>
|
||||
|
||||
<h4 style="font-size:0.9375rem;margin-bottom:0.5rem;">2. 拉取 SOUL.md</h4>
|
||||
<div style="position:relative;margin-bottom:1rem;">
|
||||
<pre style="background:var(--bg-muted,#1e1e1e);color:var(--text-light,#eee);border-radius:8px;padding:0.75rem;overflow-x:auto;font-size:0.8125rem;margin:0;"><code>${escapeHtml(soulCmd)}</code></pre>
|
||||
<button class="btn btn--small btn--outline" type="button" data-action="copy-hermes-cmd" data-cmd="${escapeHtml(soulCmd)}" style="position:absolute;top:0.5rem;right:0.5rem;">复制</button>
|
||||
</div>
|
||||
|
||||
<h4 style="font-size:0.9375rem;margin-bottom:0.5rem;">3. 拉取 config.yaml</h4>
|
||||
<div style="position:relative;margin-bottom:1rem;">
|
||||
<pre style="background:var(--bg-muted,#1e1e1e);color:var(--text-light,#eee);border-radius:8px;padding:0.75rem;overflow-x:auto;font-size:0.8125rem;margin:0;"><code>${escapeHtml(configCmd)}</code></pre>
|
||||
<button class="btn btn--small btn--outline" type="button" data-action="copy-hermes-cmd" data-cmd="${escapeHtml(configCmd)}" style="position:absolute;top:0.5rem;right:0.5rem;">复制</button>
|
||||
</div>
|
||||
|
||||
<h4 style="font-size:0.9375rem;margin-bottom:0.5rem;">4. 配置 API 密钥</h4>
|
||||
<pre style="background:var(--bg-muted,#1e1e1e);color:var(--text-light,#eee);border-radius:8px;padding:0.75rem;overflow-x:auto;font-size:0.8125rem;margin-bottom:1rem;"><code>echo "OPENROUTER_API_KEY=sk-or-your-key" > ~/.hermes/profiles/${escapeHtml(profileName)}/.env</code></pre>
|
||||
|
||||
<h4 style="font-size:0.9375rem;margin-bottom:0.5rem;">5. 启动对话</h4>
|
||||
<pre style="background:var(--bg-muted,#1e1e1e);color:var(--text-light,#eee);border-radius:8px;padding:0.75rem;overflow-x:auto;font-size:0.8125rem;margin-bottom:1rem;"><code>${escapeHtml(profileName)} chat</code></pre>
|
||||
|
||||
<div style="border-top:1px solid var(--border);padding-top:1rem;margin-top:0.5rem;">
|
||||
<p style="font-size:0.8125rem;color:var(--text-muted);margin:0;">
|
||||
提示:将命令中的 <code>YOUR_API_KEY</code> 替换为你在设置页生成的 API Key(以 <code>eak_</code> 开头)。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// 点击遮罩关闭
|
||||
modal.addEventListener('click', (e) => {
|
||||
if (e.target === modal) modal.remove();
|
||||
});
|
||||
|
||||
document.body.appendChild(modal);
|
||||
}
|
||||
|
||||
function switchCenterTab(tab) {
|
||||
|
|
@ -588,7 +811,7 @@ ${data.secrets || '无'}
|
|||
}
|
||||
}
|
||||
|
||||
function publish() {
|
||||
async function publish() {
|
||||
if (!validateStep(currentStep)) return;
|
||||
|
||||
const data = getFormData();
|
||||
|
|
@ -599,9 +822,37 @@ ${data.secrets || '无'}
|
|||
generatedSoul = generateSoulMd(data);
|
||||
generatedConfig = generateConfigYaml(data);
|
||||
|
||||
form.hidden = true;
|
||||
resultPanel.hidden = false;
|
||||
renderPreview();
|
||||
// 持久化到数据库
|
||||
try {
|
||||
const payload = {
|
||||
...data,
|
||||
soulMd: generatedSoul,
|
||||
configYaml: generatedConfig,
|
||||
desc: data.personality.split(/[,,]/).slice(0, 2).join(','),
|
||||
price: parseFloat(data.price) || 29.9,
|
||||
avatar: data.avatar || `https://trae-api-cn.mchost.guru/api/ide/v1/text_to_image?prompt=anime%20${encodeURIComponent(data.displayName)}%20portrait&image_size=square`,
|
||||
temperature: parseFloat(data.temperature) || 0.8,
|
||||
maxTokens: parseInt(data.maxTokens) || 2048,
|
||||
};
|
||||
|
||||
if (editingRoleId) {
|
||||
await api(`/roles/${editingRoleId}`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
} else {
|
||||
await api('/roles', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
}
|
||||
|
||||
form.hidden = true;
|
||||
resultPanel.hidden = false;
|
||||
renderPreview();
|
||||
} catch (err) {
|
||||
alert('保存失败:' + err.message);
|
||||
}
|
||||
}
|
||||
|
||||
function renderPreview() {
|
||||
|
|
@ -627,8 +878,53 @@ ${data.secrets || '无'}
|
|||
generatedSoul = '';
|
||||
generatedConfig = '';
|
||||
activePreview = 'soul';
|
||||
editingRoleId = null;
|
||||
updateStep(0);
|
||||
updateTabs();
|
||||
updateSystemPromptPreview();
|
||||
}
|
||||
|
||||
// 加载已有角色数据到表单(编辑模式)
|
||||
async function loadRoleForEdit(roleId) {
|
||||
try {
|
||||
const { role } = await api(`/roles/${roleId}/full`);
|
||||
editingRoleId = roleId;
|
||||
form.hidden = false;
|
||||
resultPanel.hidden = true;
|
||||
|
||||
// 填充表单字段
|
||||
const fields = {
|
||||
displayName: role.displayName,
|
||||
gender: role.gender,
|
||||
age: role.age || '',
|
||||
relationship: role.relationship || '',
|
||||
personality: role.personality,
|
||||
background: role.background,
|
||||
speechStyle: role.speechStyle,
|
||||
likes: role.likes || '',
|
||||
dislikes: role.dislikes || '',
|
||||
memories: role.memories || '',
|
||||
secrets: role.secrets || '',
|
||||
greeting: role.greeting,
|
||||
systemPrompt: role.systemPrompt || '',
|
||||
model: role.model,
|
||||
temperature: String(role.temperature),
|
||||
maxTokens: String(role.maxTokens),
|
||||
price: String(role.price),
|
||||
};
|
||||
Object.entries(fields).forEach(([name, value]) => {
|
||||
const el = form.elements[name];
|
||||
if (el) el.value = value;
|
||||
});
|
||||
if (form.elements.enableMemory) form.elements.enableMemory.checked = role.enableMemory;
|
||||
if (form.elements.enableTools) form.elements.enableTools.checked = role.enableTools;
|
||||
|
||||
updateStep(0);
|
||||
updateTabs();
|
||||
updateSystemPromptPreview();
|
||||
} catch (err) {
|
||||
alert('加载角色数据失败:' + err.message);
|
||||
}
|
||||
}
|
||||
|
||||
function updateTabs() {
|
||||
|
|
@ -652,7 +948,7 @@ ${data.secrets || '无'}
|
|||
}
|
||||
});
|
||||
|
||||
document.addEventListener('click', (e) => {
|
||||
document.addEventListener('click', async (e) => {
|
||||
// FAQ toggle (U4)
|
||||
const faqBtn = e.target.closest('.faq-q');
|
||||
if (faqBtn) {
|
||||
|
|
@ -772,11 +1068,53 @@ ${data.secrets || '无'}
|
|||
|
||||
if (action === 'edit-role') {
|
||||
e.preventDefault();
|
||||
resetCreator();
|
||||
const roleId = target.dataset.roleId;
|
||||
await loadRoleForEdit(roleId);
|
||||
showView('creator');
|
||||
return;
|
||||
}
|
||||
|
||||
if (action === 'hermes-deploy') {
|
||||
e.preventDefault();
|
||||
const roleId = target.dataset.roleId;
|
||||
const roleName = target.dataset.roleName || 'role';
|
||||
showHermesDeployGuide(roleId, roleName);
|
||||
return;
|
||||
}
|
||||
|
||||
if (action === 'generate-apikey') {
|
||||
e.preventDefault();
|
||||
await generateApiKey();
|
||||
return;
|
||||
}
|
||||
|
||||
if (action === 'delete-apikey') {
|
||||
e.preventDefault();
|
||||
const keyId = target.dataset.apikeyId;
|
||||
await deleteApiKey(keyId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (action === 'close-hermes-modal') {
|
||||
e.preventDefault();
|
||||
const modal = document.getElementById('hermes-modal');
|
||||
if (modal) modal.remove();
|
||||
return;
|
||||
}
|
||||
|
||||
if (action === 'copy-hermes-cmd') {
|
||||
e.preventDefault();
|
||||
const cmd = target.dataset.cmd || '';
|
||||
try {
|
||||
await navigator.clipboard.writeText(cmd);
|
||||
target.textContent = '已复制';
|
||||
setTimeout(() => { target.textContent = '复制'; }, 2000);
|
||||
} catch {
|
||||
alert('复制失败,请手动选择文本复制');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (action === 'logout') {
|
||||
e.preventDefault();
|
||||
logout();
|
||||
|
|
@ -848,34 +1186,70 @@ ${data.secrets || '无'}
|
|||
|
||||
// --- Auth form submissions ---
|
||||
document.querySelectorAll('.auth-form').forEach((authForm) => {
|
||||
authForm.addEventListener('submit', (e) => {
|
||||
authForm.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
if (!validatePasswordMatch(authForm)) return;
|
||||
const formData = new FormData(authForm);
|
||||
const data = Object.fromEntries(formData.entries());
|
||||
login(data.account);
|
||||
const isRegister = authForm.dataset.form === 'register';
|
||||
|
||||
// After login, go to role library or creator center
|
||||
if (state.isCreator) {
|
||||
showView('creator-center');
|
||||
renderCreatorCenter();
|
||||
} else {
|
||||
renderRoleLibrary();
|
||||
showView('role-library');
|
||||
try {
|
||||
const endpoint = isRegister ? '/auth/register' : '/auth/login';
|
||||
const result = await api(endpoint, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ account: data.account, password: data.password }),
|
||||
});
|
||||
|
||||
setToken(result.token);
|
||||
applyUserData(result.user);
|
||||
|
||||
// 注册成功后自动成为创作者
|
||||
if (isRegister) {
|
||||
await api('/auth/settings', {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ isCreator: true, creatorName: data.account }),
|
||||
});
|
||||
state.isCreator = true;
|
||||
state.creatorName = data.account;
|
||||
saveState();
|
||||
updateLandingCard();
|
||||
}
|
||||
|
||||
// 登录后跳转
|
||||
if (state.isCreator) {
|
||||
showView('creator-center');
|
||||
renderCreatorCenter();
|
||||
} else {
|
||||
renderRoleLibrary();
|
||||
showView('role-library');
|
||||
}
|
||||
} catch (err) {
|
||||
alert(err.message);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// --- Settings form (U7) ---
|
||||
document.getElementById('settings-form').addEventListener('submit', (e) => {
|
||||
document.getElementById('settings-form').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const formData = new FormData(e.target);
|
||||
const data = Object.fromEntries(formData.entries());
|
||||
state.creatorName = data.creatorName || '';
|
||||
state.libraryName = data.libraryName || '我的 [XXX]';
|
||||
saveState();
|
||||
updateLandingCard();
|
||||
alert('设置已保存');
|
||||
try {
|
||||
const { user } = await api('/auth/settings', {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({
|
||||
creatorName: data.creatorName,
|
||||
libraryName: data.libraryName,
|
||||
}),
|
||||
});
|
||||
state.creatorName = user.creatorName || '';
|
||||
state.libraryName = user.libraryName || '我的 [XXX]';
|
||||
saveState();
|
||||
updateLandingCard();
|
||||
alert('设置已保存');
|
||||
} catch (err) {
|
||||
alert('保存失败:' + err.message);
|
||||
}
|
||||
});
|
||||
|
||||
// --- Withdraw form (U7) ---
|
||||
|
|
@ -904,4 +1278,17 @@ ${data.secrets || '无'}
|
|||
updateTabBar('landing');
|
||||
initFaqA11y();
|
||||
initViewA11y();
|
||||
|
||||
// 页面加载时验证 token,恢复登录态
|
||||
(async () => {
|
||||
const token = getToken();
|
||||
if (!token) return;
|
||||
try {
|
||||
const { user } = await api('/auth/me');
|
||||
applyUserData(user);
|
||||
} catch {
|
||||
// token 过期,清除
|
||||
setToken('');
|
||||
}
|
||||
})();
|
||||
})();
|
||||
|
|
|
|||
|
|
@ -0,0 +1,99 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# ===== EternalAI 部署脚本 =====
|
||||
# 每次部署时运行,拉取最新代码、安装依赖、迁移数据库、重启服务
|
||||
# 用法: bash deploy/deploy.sh
|
||||
|
||||
# ---- 颜色输出 ----
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m'
|
||||
|
||||
info() { echo -e "${GREEN}[INFO]${NC} $1"; }
|
||||
warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
|
||||
error() { echo -e "${RED}[ERROR]${NC} $1"; }
|
||||
|
||||
# ---- 配置 ----
|
||||
APP_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
HEALTH_URL="http://localhost:3001"
|
||||
HEALTH_TIMEOUT=30
|
||||
|
||||
cd "$APP_DIR"
|
||||
info "应用目录: $APP_DIR"
|
||||
|
||||
# ---- 前置检查 ----
|
||||
if [[ ! -f .env ]]; then
|
||||
error ".env 文件不存在,请先运行 deploy/setup-server.sh"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v pm2 &>/dev/null; then
|
||||
error "PM2 未安装,请先运行 deploy/setup-server.sh"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ---- 拉取最新代码 ----
|
||||
info "拉取最新代码..."
|
||||
git fetch --all
|
||||
git reset --hard origin/master
|
||||
info "当前版本: $(git rev-parse --short HEAD)"
|
||||
|
||||
# ---- 安装依赖 ----
|
||||
info "安装 npm 依赖..."
|
||||
npm install --production=false
|
||||
|
||||
# ---- 生成 Prisma Client ----
|
||||
info "生成 Prisma Client..."
|
||||
npx prisma generate
|
||||
|
||||
# ---- 数据库迁移 ----
|
||||
info "推送数据库 Schema..."
|
||||
npx prisma db push
|
||||
|
||||
# ---- 创建日志目录 ----
|
||||
mkdir -p logs
|
||||
|
||||
# ---- 重启 PM2 进程 ----
|
||||
info "重启 PM2 进程..."
|
||||
if pm2 describe eternalai &>/dev/null; then
|
||||
pm2 reload ecosystem.config.js --update-env
|
||||
info "PM2 进程已 reload"
|
||||
else
|
||||
pm2 start ecosystem.config.js
|
||||
info "PM2 进程已启动"
|
||||
fi
|
||||
pm2 save
|
||||
|
||||
# ---- 健康检查 ----
|
||||
info "健康检查 (${HEALTH_TIMEOUT}s 超时)..."
|
||||
ELAPSED=0
|
||||
while [[ $ELAPSED -lt $HEALTH_TIMEOUT ]]; do
|
||||
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" "$HEALTH_URL" 2>/dev/null || echo "000")
|
||||
if [[ "$HTTP_CODE" == "200" ]]; then
|
||||
info "健康检查通过 (HTTP $HTTP_CODE)"
|
||||
break
|
||||
fi
|
||||
sleep 2
|
||||
ELAPSED=$((ELAPSED + 2))
|
||||
echo -n "."
|
||||
done
|
||||
echo ""
|
||||
|
||||
if [[ "$HTTP_CODE" != "200" ]]; then
|
||||
error "健康检查失败 (HTTP $HTTP_CODE),请检查日志: pm2 logs eternalai"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ---- 完成 ----
|
||||
echo ""
|
||||
info "============================================"
|
||||
info " 部署完成!"
|
||||
info "============================================"
|
||||
echo ""
|
||||
echo "版本: $(git rev-parse --short HEAD)"
|
||||
echo "分支: $(git branch --show-current)"
|
||||
echo "状态: pm2 status"
|
||||
echo "日志: pm2 logs eternalai"
|
||||
echo ""
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
# ===== EternalAI Nginx 反向代理配置 =====
|
||||
#
|
||||
# 使用方法:
|
||||
# 1. 复制到 Nginx 配置目录:
|
||||
# sudo cp deploy/nginx.conf /etc/nginx/sites-available/eternalai
|
||||
# sudo ln -s /etc/nginx/sites-available/eternalai /etc/nginx/sites-enabled/
|
||||
# 2. 替换 YOUR_DOMAIN 为实际域名
|
||||
# 3. 测试配置: sudo nginx -t
|
||||
# 4. 重载: sudo nginx -s reload
|
||||
#
|
||||
# HTTPS 配置(推荐):
|
||||
# sudo certbot --nginx -d YOUR_DOMAIN
|
||||
|
||||
# 替换 YOUR_DOMAIN 为你的实际域名
|
||||
upstream eternalai_backend {
|
||||
server 127.0.0.1:3001;
|
||||
keepalive 32;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name YOUR_DOMAIN;
|
||||
|
||||
# 安全头
|
||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header X-XSS-Protection "1; mode=block" always;
|
||||
|
||||
# 请求体大小限制
|
||||
client_max_body_size 10m;
|
||||
|
||||
# API 请求代理到 Node.js
|
||||
location /api/ {
|
||||
proxy_pass http://eternalai_backend;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection 'upgrade';
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_cache_bypass $http_upgrade;
|
||||
proxy_read_timeout 60s;
|
||||
proxy_send_timeout 60s;
|
||||
}
|
||||
|
||||
# 静态文件缓存
|
||||
location ~* \.(css|js|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
|
||||
proxy_pass http://eternalai_backend;
|
||||
proxy_cache_bypass $http_upgrade;
|
||||
expires 1d;
|
||||
add_header Cache-Control "public, immutable";
|
||||
}
|
||||
|
||||
# 主页和其他路由
|
||||
location / {
|
||||
proxy_pass http://eternalai_backend;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
# 禁止访问敏感文件
|
||||
location ~ /\. {
|
||||
deny all;
|
||||
}
|
||||
location ~ /\.(env|git) {
|
||||
deny all;
|
||||
}
|
||||
location ~ /node_modules/ {
|
||||
deny all;
|
||||
}
|
||||
location ~ /prisma/ {
|
||||
deny all;
|
||||
}
|
||||
location ~ /e2e/ {
|
||||
deny all;
|
||||
}
|
||||
location ~ /deploy/ {
|
||||
deny all;
|
||||
}
|
||||
|
||||
access_log /var/log/nginx/eternalai_access.log;
|
||||
error_log /var/log/nginx/eternalai_error.log;
|
||||
}
|
||||
|
|
@ -0,0 +1,198 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# ===== EternalAI 服务器初始化脚本 =====
|
||||
# 首次部署时在服务器上运行,自动检测并安装所有依赖
|
||||
# 用法: bash deploy/setup-server.sh
|
||||
#
|
||||
# 可通过环境变量自定义:
|
||||
# DB_NAME=eternalai 数据库名
|
||||
# DB_USER=eternalai 数据库用户
|
||||
# DB_PASS=xxx 数据库密码(必填,否则自动生成)
|
||||
|
||||
# ---- 颜色输出 ----
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m'
|
||||
|
||||
info() { echo -e "${GREEN}[INFO]${NC} $1"; }
|
||||
warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
|
||||
error() { echo -e "${RED}[ERROR]${NC} $1"; }
|
||||
|
||||
# ---- 配置 ----
|
||||
DB_NAME="${DB_NAME:-eternalai}"
|
||||
DB_USER="${DB_USER:-eternalai}"
|
||||
DB_PASS="${DB_PASS:-$(openssl rand -base64 24 | tr -d '/+=' | head -c 24)}"
|
||||
APP_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
NODE_VERSION="20"
|
||||
|
||||
info "应用目录: $APP_DIR"
|
||||
info "数据库: $DB_NAME (用户: $DB_USER)"
|
||||
|
||||
# ---- 检测 OS ----
|
||||
if [[ -f /etc/os-release ]]; then
|
||||
. /etc/os-release
|
||||
OS_ID="$ID"
|
||||
info "检测到操作系统: $PRETTY_NAME"
|
||||
else
|
||||
error "无法检测操作系统,仅支持 Ubuntu/Debian/CentOS"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ---- 安装 Node.js ----
|
||||
if ! command -v node &>/dev/null; then
|
||||
info "安装 Node.js $NODE_VERSION LTS..."
|
||||
if [[ "$OS_ID" == "ubuntu" || "$OS_ID" == "debian" ]]; then
|
||||
curl -fsSL "https://deb.nodesource.com/setup_${NODE_VERSION}.x" | sudo -E bash -
|
||||
sudo apt-get install -y nodejs
|
||||
elif [[ "$OS_ID" == "centos" || "$OS_ID" == "rhel" || "$OS_ID" == "rocky" ]]; then
|
||||
curl -fsSL "https://rpm.nodesource.com/setup_${NODE_VERSION}.x" | sudo -E bash -
|
||||
sudo yum install -y nodejs
|
||||
else
|
||||
error "不支持的操作系统: $OS_ID,请手动安装 Node.js $NODE_VERSION"
|
||||
exit 1
|
||||
fi
|
||||
info "Node.js 安装完成: $(node -v)"
|
||||
else
|
||||
info "Node.js 已安装: $(node -v)"
|
||||
NODE_MAJOR=$(node -v | sed 's/v\([0-9]*\).*/\1/')
|
||||
if [[ "$NODE_MAJOR" -lt 18 ]]; then
|
||||
warn "Node.js 版本过低 (当前 $(node -v)),建议升级到 $NODE_VERSION LTS"
|
||||
fi
|
||||
fi
|
||||
|
||||
# ---- 安装 PM2 ----
|
||||
if ! command -v pm2 &>/dev/null; then
|
||||
info "安装 PM2..."
|
||||
sudo npm install -g pm2
|
||||
info "PM2 安装完成: $(pm2 -v)"
|
||||
else
|
||||
info "PM2 已安装: $(pm2 -v)"
|
||||
fi
|
||||
|
||||
# ---- 安装 PostgreSQL ----
|
||||
if ! command -v psql &>/dev/null; then
|
||||
info "安装 PostgreSQL 15..."
|
||||
if [[ "$OS_ID" == "ubuntu" || "$OS_ID" == "debian" ]]; then
|
||||
sudo apt-get install -y ca-certificates curl gnupg lsb-release
|
||||
sudo install -m 0755 -d /etc/apt/keyrings
|
||||
curl -fsSL https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo gpg --dearmor -o /etc/apt/keyrings/postgresql.gpg
|
||||
echo "deb [signed-by=/etc/apt/keyrings/postgresql.gpg] http://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" | sudo tee /etc/apt/sources.list.d/pgdg.list
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y postgresql-15
|
||||
elif [[ "$OS_ID" == "centos" || "$OS_ID" == "rhel" || "$OS_ID" == "rocky" ]]; then
|
||||
sudo yum install -y "https://download.postgresql.org/pub/repos/yum/reporpms/EL-$(rpm -E %{rhel})-x86_64/pgdg-redhat-repo-latest.noarch.rpm"
|
||||
sudo yum install -y postgresql15-server postgresql15
|
||||
sudo /usr/pgsql-15/bin/postgresql-15-setup initdb
|
||||
else
|
||||
error "不支持的操作系统: $OS_ID,请手动安装 PostgreSQL 15"
|
||||
exit 1
|
||||
fi
|
||||
info "PostgreSQL 安装完成"
|
||||
else
|
||||
info "PostgreSQL 已安装: $(psql --version)"
|
||||
fi
|
||||
|
||||
# ---- 启动 PostgreSQL ----
|
||||
if ! sudo systemctl is-active --quiet postgresql; then
|
||||
info "启动 PostgreSQL 服务..."
|
||||
sudo systemctl enable postgresql
|
||||
sudo systemctl start postgresql
|
||||
fi
|
||||
|
||||
# ---- 创建数据库和用户 ----
|
||||
info "创建数据库 $DB_NAME 和用户 $DB_USER..."
|
||||
sudo -u postgres psql -tc "SELECT 1 FROM pg_roles WHERE rolname='$DB_USER'" | grep -q 1 || {
|
||||
sudo -u postgres psql -c "CREATE USER $DB_USER WITH PASSWORD '$DB_PASS';"
|
||||
info "数据库用户 $DB_USER 已创建"
|
||||
}
|
||||
sudo -u postgres psql -tc "SELECT 1 FROM pg_database WHERE datname='$DB_NAME'" | grep -q 1 || {
|
||||
sudo -u postgres psql -c "CREATE DATABASE $DB_NAME OWNER $DB_USER;"
|
||||
info "数据库 $DB_NAME 已创建"
|
||||
}
|
||||
sudo -u postgres psql -c "GRANT ALL PRIVILEGES ON DATABASE $DB_NAME TO $DB_USER;" >/dev/null
|
||||
|
||||
# 确保 PostgreSQL 对 pg_hba.conf 允许密码认证
|
||||
PG_HBA=$(sudo -u postgres psql -t -c "SHOW hba_file" | xargs)
|
||||
if [[ -f "$PG_HBA" ]]; then
|
||||
if ! sudo grep -q "$DB_USER" "$PG_HBA" 2>/dev/null; then
|
||||
warn "如果连接数据库失败,请检查 $PG_HBA 是否允许 md5/scram-sha-256 认证"
|
||||
warn "添加: host $DB_NAME $DB_USER 127.0.0.1/32 scram-sha-256"
|
||||
fi
|
||||
fi
|
||||
|
||||
# ---- 安装 Nginx ----
|
||||
if ! command -v nginx &>/dev/null; then
|
||||
info "安装 Nginx..."
|
||||
if [[ "$OS_ID" == "ubuntu" || "$OS_ID" == "debian" ]]; then
|
||||
sudo apt-get install -y nginx
|
||||
elif [[ "$OS_ID" == "centos" || "$OS_ID" == "rhel" || "$OS_ID" == "rocky" ]]; then
|
||||
sudo yum install -y nginx
|
||||
fi
|
||||
info "Nginx 安装完成: $(nginx -v 2>&1)"
|
||||
else
|
||||
info "Nginx 已安装: $(nginx -v 2>&1)"
|
||||
fi
|
||||
|
||||
if ! sudo systemctl is-active --quiet nginx; then
|
||||
sudo systemctl enable nginx
|
||||
sudo systemctl start nginx
|
||||
fi
|
||||
|
||||
# ---- 创建 .env 文件 ----
|
||||
ENV_FILE="$APP_DIR/.env"
|
||||
if [[ ! -f "$ENV_FILE" ]]; then
|
||||
info "创建 .env 文件..."
|
||||
JWT_SECRET=$(openssl rand -base64 48)
|
||||
cat > "$ENV_FILE" <<EOF
|
||||
DATABASE_URL="postgresql://$DB_USER:$DB_PASS@localhost:5432/$DB_NAME"
|
||||
JWT_SECRET="$JWT_SECRET"
|
||||
PORT=3001
|
||||
EOF
|
||||
chmod 600 "$ENV_FILE"
|
||||
info ".env 文件已创建(权限 600,请妥善保管数据库密码和 JWT 密钥)"
|
||||
echo ""
|
||||
echo "=============================="
|
||||
echo " JWT 密钥已自动生成"
|
||||
echo " 数据库密码已写入 .env 文件"
|
||||
echo "=============================="
|
||||
echo ""
|
||||
else
|
||||
info ".env 文件已存在,跳过创建"
|
||||
fi
|
||||
|
||||
# ---- 创建日志目录 ----
|
||||
mkdir -p "$APP_DIR/logs"
|
||||
|
||||
# ---- 安装项目依赖 ----
|
||||
info "安装项目依赖..."
|
||||
cd "$APP_DIR"
|
||||
npm install
|
||||
|
||||
# ---- 生成 Prisma Client ----
|
||||
info "生成 Prisma Client..."
|
||||
npx prisma generate
|
||||
|
||||
# ---- 推送数据库 Schema ----
|
||||
info "推送数据库 Schema..."
|
||||
npx prisma db push
|
||||
|
||||
# ---- 配置 PM2 开机自启 ----
|
||||
info "配置 PM2 开机自启..."
|
||||
pm2 start ecosystem.config.js || true
|
||||
pm2 save
|
||||
pm2 startup systemd -u "$(whoami)" --hp "$HOME" 2>/dev/null | grep "sudo" | bash || true
|
||||
|
||||
# ---- 完成 ----
|
||||
echo ""
|
||||
info "============================================"
|
||||
info " 服务器初始化完成!"
|
||||
info "============================================"
|
||||
echo ""
|
||||
echo "下一步:"
|
||||
echo " 1. 配置 Nginx 反向代理(参考 deploy/nginx.conf)"
|
||||
echo " 2. 检查应用状态: pm2 status"
|
||||
echo " 3. 查看日志: pm2 logs eternalai"
|
||||
echo " 4. 访问应用: http://localhost:3001"
|
||||
echo ""
|
||||
|
|
@ -0,0 +1,100 @@
|
|||
# Hermes Agent 跨机器部署 — 需求文档
|
||||
|
||||
**日期**: 2026-06-20
|
||||
**状态**: 已确认,待实现
|
||||
|
||||
## 概述
|
||||
|
||||
EternalAI 生成的角色配置(Soul.md + config.yaml)需要能直接复制到远程 Hermes Agent 中使用。EternalAI 服务器与 Hermes Agent 可能不在同一台机器上,用户通过 CLI 拉取命令(curl)跨机器获取配置文件。
|
||||
|
||||
## 用户场景
|
||||
|
||||
用户在 EternalAI Web UI 创建角色后,需要在另一台安装了 Hermes Agent 的机器上部署该角色。用户不希望 EternalAI 直接写入 Hermes 目录,而是生成可直接使用的文件,通过 curl 命令拉取后手动放置到 Hermes profile 目录。
|
||||
|
||||
## 工作流程
|
||||
|
||||
```
|
||||
服务器 A (EternalAI) 服务器 B (Hermes Agent)
|
||||
┌─────────────────────┐ ┌─────────────────────┐
|
||||
│ 1. 设置页生成 API Key│ │ │
|
||||
│ (eak_xxxxx) │ │ 5. hermes profile │
|
||||
│ │ │ create role-mio │
|
||||
│ 2. 创建角色 → 发布 │ │ │
|
||||
│ 生成 Soul.md + │ │ 6. curl 拉取文件 │
|
||||
│ config.yaml │ │ SOUL.md │
|
||||
│ │ │ config.yaml │
|
||||
│ 3. 角色管理页显示 │ 4. 用户复制 │ │
|
||||
│ curl 命令模板 │──── curl ────▶│ 7. 配置 .env │
|
||||
│ (含 API Key) │ 命令执行 │ (API 密钥) │
|
||||
│ │ │ │
|
||||
│ │ │ 8. role-mio chat │
|
||||
└─────────────────────┘ └─────────────────────┘
|
||||
```
|
||||
|
||||
### 用户在 Hermes 机器上的操作
|
||||
|
||||
```bash
|
||||
# 1. 创建 profile
|
||||
hermes profile create role-mio
|
||||
|
||||
# 2. 拉取 SOUL.md
|
||||
curl -H "Authorization: Bearer eak_xxx" \
|
||||
https://eternalai.example.com/api/hermes/roles/<id>/SOUL.md \
|
||||
-o ~/.hermes/profiles/role-mio/SOUL.md
|
||||
|
||||
# 3. 拉取 config.yaml
|
||||
curl -H "Authorization: Bearer eak_xxx" \
|
||||
https://eternalai.example.com/api/hermes/roles/<id>/config.yaml \
|
||||
-o ~/.hermes/profiles/role-mio/config.yaml
|
||||
|
||||
# 4. 配置 API 密钥
|
||||
echo "OPENROUTER_API_KEY=sk-or-xxx" > ~/.hermes/profiles/role-mio/.env
|
||||
|
||||
# 5. 启动
|
||||
role-mio chat
|
||||
```
|
||||
|
||||
## 功能需求
|
||||
|
||||
### 1. API Key 管理
|
||||
|
||||
- 用户在设置页生成长期 API Key(格式:`eak_` + 32 位随机 hex)
|
||||
- API Key 列表显示:名称、创建时间、最后使用时间、Key(脱敏显示)
|
||||
- 支持删除 API Key
|
||||
- API Key 存储在数据库中,使用 bcrypt 哈希存储(只明文显示一次)
|
||||
|
||||
### 2. Hermes 配置 API
|
||||
|
||||
两个端点,均需 API Key 认证:
|
||||
|
||||
- `GET /api/hermes/roles/:id/SOUL.md` — 返回角色的 SOUL.md 内容(text/plain)
|
||||
- `GET /api/hermes/roles/:id/config.yaml` — 返回适配后的 Hermes config.yaml(text/plain)
|
||||
|
||||
认证方式:`Authorization: Bearer eak_xxxxx`
|
||||
|
||||
权限:API Key 所属用户只能拉取自己创建的角色。
|
||||
|
||||
### 3. config.yaml 格式适配
|
||||
|
||||
EternalAI 数据库中的角色数据 → Hermes config.yaml 格式:
|
||||
|
||||
| EternalAI 字段 | Hermes config.yaml 字段 |
|
||||
|---|---|
|
||||
| `role.model` | `model:` |
|
||||
| `role.temperature` | `temperature:` |
|
||||
| `role.maxTokens` | `max_tokens:` |
|
||||
| `role.enableMemory` | `memory.enabled:` |
|
||||
| `role.enableTools` | `tools.enabled:` |
|
||||
|
||||
### 4. Web UI 部署指南
|
||||
|
||||
角色管理页每个角色卡片增加"Hermes 部署"区域:
|
||||
- 显示完整的 curl 命令模板(自动填入服务器地址、角色 ID、API Key)
|
||||
- 操作步骤说明
|
||||
- 复制按钮
|
||||
|
||||
## 非功能需求
|
||||
|
||||
- API Key 使用 bcrypt 哈希存储,明文只在生成时显示一次
|
||||
- Hermes API 端点返回 `text/plain` 格式,适合 curl 直接输出到文件
|
||||
- config.yaml 适配在后端完成,用户拉取到的文件可直接使用
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,332 @@
|
|||
# EternalAI 部署指南
|
||||
|
||||
## 架构概览
|
||||
|
||||
```
|
||||
用户浏览器 → Nginx (80/443) → Node.js Express (3001) → PostgreSQL (5432)
|
||||
↓
|
||||
PM2 进程管理
|
||||
```
|
||||
|
||||
## 文件清单
|
||||
|
||||
| 文件 | 用途 |
|
||||
|------|------|
|
||||
| `ecosystem.config.js` | PM2 进程配置 |
|
||||
| `.env.example` | 环境变量模板 |
|
||||
| `deploy/setup-server.sh` | 首次服务器初始化脚本 |
|
||||
| `deploy/deploy.sh` | 每次部署脚本 |
|
||||
| `deploy/nginx.conf` | Nginx 反向代理配置模板 |
|
||||
| `.gitea/workflows/deploy.yml` | Gitea Actions CI/CD 工作流 |
|
||||
|
||||
---
|
||||
|
||||
## 一、首次部署(全量部署)
|
||||
|
||||
### 1.1 准备服务器
|
||||
|
||||
在目标服务器上执行以下操作。
|
||||
|
||||
```bash
|
||||
# 克隆代码仓库
|
||||
cd /opt # 或你选择的部署目录
|
||||
git clone http://gitea.fischerai.cn/chigulong/eternalai.git
|
||||
cd eternalai
|
||||
```
|
||||
|
||||
### 1.2 运行服务器初始化脚本
|
||||
|
||||
```bash
|
||||
bash deploy/setup-server.sh
|
||||
```
|
||||
|
||||
此脚本会自动完成:
|
||||
- 检测并安装 Node.js 20 LTS(如缺失)
|
||||
- 检测并安装 PostgreSQL 15(如缺失)
|
||||
- 检测并安装 PM2(如缺失)
|
||||
- 检测并安装 Nginx(如缺失)
|
||||
- 创建数据库 `eternalai` 和用户
|
||||
- 自动生成 `.env` 文件(含随机 JWT 密钥和数据库密码)
|
||||
- 安装 npm 依赖
|
||||
- 生成 Prisma Client
|
||||
- 推送数据库 Schema
|
||||
- 启动 PM2 进程并配置开机自启
|
||||
|
||||
**自定义数据库配置**(可选):
|
||||
```bash
|
||||
DB_NAME=mydb DB_USER=myuser DB_PASS=mypassword bash deploy/setup-server.sh
|
||||
```
|
||||
|
||||
脚本完成后会输出数据库密码,请妥善保存。
|
||||
|
||||
### 1.3 配置 Nginx 反向代理
|
||||
|
||||
```bash
|
||||
# 复制 Nginx 配置
|
||||
sudo cp deploy/nginx.conf /etc/nginx/sites-available/eternalai
|
||||
sudo ln -s /etc/nginx/sites-available/eternalai /etc/nginx/sites-enabled/
|
||||
|
||||
# 编辑配置,替换 YOUR_DOMAIN 为实际域名
|
||||
sudo nano /etc/nginx/sites-enabled/eternalai
|
||||
# 将所有 YOUR_DOMAIN 替换为你的域名,如 eternalai.example.com
|
||||
|
||||
# 测试配置
|
||||
sudo nginx -t
|
||||
|
||||
# 重载 Nginx
|
||||
sudo nginx -s reload
|
||||
```
|
||||
|
||||
### 1.4 配置 HTTPS(推荐)
|
||||
|
||||
```bash
|
||||
# 安装 Certbot
|
||||
sudo apt-get install -y certbot python3-certbot-nginx # Ubuntu/Debian
|
||||
# 或
|
||||
sudo yum install -y certbot python3-certbot-nginx # CentOS/RHEL
|
||||
|
||||
# 自动获取并配置 SSL 证书
|
||||
sudo certbot --nginx -d YOUR_DOMAIN
|
||||
|
||||
# 证书自动续期(Certbot 会自动配置 cron)
|
||||
sudo certbot renew --dry-run
|
||||
```
|
||||
|
||||
### 1.5 验证部署
|
||||
|
||||
```bash
|
||||
# 检查 PM2 进程状态
|
||||
pm2 status
|
||||
|
||||
# 查看应用日志
|
||||
pm2 logs eternalai
|
||||
|
||||
# 测试本地访问
|
||||
curl http://localhost:3001
|
||||
|
||||
# 测试 Nginx 代理访问
|
||||
curl http://YOUR_DOMAIN
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 二、CI/CD 自动部署(推送即部署)
|
||||
|
||||
### 2.1 配置 Gitea Secrets
|
||||
|
||||
在 Gitea 仓库设置中添加 Secret:
|
||||
|
||||
1. 进入仓库 → Settings → Actions → Secrets
|
||||
2. 添加 `DATABASE_URL`,值为 `.env` 文件中的 `DATABASE_URL`
|
||||
|
||||
### 2.2 配置 Gitea Runner
|
||||
|
||||
确保已注册 self-hosted runner:
|
||||
|
||||
```bash
|
||||
# 在服务器上注册 runner(如尚未注册)
|
||||
# 参考 Gitea 官方文档: https://docs.gitea.com/usage/actions/quickstart
|
||||
```
|
||||
|
||||
### 2.3 自动部署流程
|
||||
|
||||
每次推送代码到 `master` 分支时,Gitea Actions 会自动:
|
||||
|
||||
1. 拉取最新代码
|
||||
2. 安装 npm 依赖
|
||||
3. 生成 Prisma Client
|
||||
4. 推送数据库 Schema(`prisma db push`)
|
||||
5. 重启 PM2 进程(`pm2 reload`)
|
||||
6. 健康检查(等待 HTTP 200)
|
||||
7. 重载 Nginx
|
||||
|
||||
```bash
|
||||
# 推送代码触发自动部署
|
||||
git push origin master
|
||||
```
|
||||
|
||||
### 2.4 手动触发部署
|
||||
|
||||
在 Gitea 仓库 → Actions 页面,可手动触发 `Deploy EternalAI` 工作流。
|
||||
|
||||
---
|
||||
|
||||
## 三、手动部署(不使用 CI/CD)
|
||||
|
||||
```bash
|
||||
# SSH 登录服务器
|
||||
ssh user@your-server
|
||||
|
||||
# 进入应用目录
|
||||
cd /opt/eternalai
|
||||
|
||||
# 运行部署脚本
|
||||
bash deploy/deploy.sh
|
||||
```
|
||||
|
||||
部署脚本会自动完成:拉取代码 → 安装依赖 → 数据库迁移 → 重启 PM2 → 健康检查。
|
||||
|
||||
---
|
||||
|
||||
## 四、常用运维命令
|
||||
|
||||
### PM2 进程管理
|
||||
|
||||
```bash
|
||||
pm2 status # 查看进程状态
|
||||
pm2 logs eternalai # 实时查看日志
|
||||
pm2 logs eternalai --lines 100 # 查看最近 100 行日志
|
||||
pm2 restart eternalai # 重启进程
|
||||
pm2 reload eternalai # 零停机重载
|
||||
pm2 stop eternalai # 停止进程
|
||||
pm2 delete eternalai # 删除进程
|
||||
pm2 monit # 监控面板
|
||||
```
|
||||
|
||||
### 数据库操作
|
||||
|
||||
```bash
|
||||
# 推送 Schema 变更
|
||||
npx prisma db push
|
||||
|
||||
# 打开 Prisma Studio(数据库可视化管理)
|
||||
npx prisma studio
|
||||
|
||||
# 连接 PostgreSQL
|
||||
psql -U eternalai -d eternalai -h localhost
|
||||
```
|
||||
|
||||
### Nginx 操作
|
||||
|
||||
```bash
|
||||
sudo nginx -t # 测试配置
|
||||
sudo nginx -s reload # 重载配置
|
||||
sudo systemctl status nginx # 查看状态
|
||||
sudo tail -f /var/log/nginx/eternalai_error.log # 查看错误日志
|
||||
```
|
||||
|
||||
### 日志查看
|
||||
|
||||
```bash
|
||||
# 应用日志
|
||||
tail -f logs/out.log # 标准输出
|
||||
tail -f logs/err.log # 错误输出
|
||||
|
||||
# Nginx 日志
|
||||
sudo tail -f /var/log/nginx/eternalai_access.log
|
||||
sudo tail -f /var/log/nginx/eternalai_error.log
|
||||
|
||||
# PostgreSQL 日志
|
||||
sudo tail -f /var/log/postgresql/postgresql-15-main.log
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 五、更新部署
|
||||
|
||||
### 5.1 日常更新(代码变更)
|
||||
|
||||
```bash
|
||||
# 方法一:推送代码触发 CI/CD(推荐)
|
||||
git push origin master
|
||||
|
||||
# 方法二:手动部署
|
||||
bash deploy/deploy.sh
|
||||
```
|
||||
|
||||
### 5.2 数据库 Schema 变更
|
||||
|
||||
修改 `prisma/schema.prisma` 后:
|
||||
|
||||
```bash
|
||||
# 推送到 master,CI/CD 会自动执行 prisma db push
|
||||
git add prisma/schema.prisma
|
||||
git commit -m "feat: update schema"
|
||||
git push origin master
|
||||
|
||||
# 或手动执行
|
||||
npx prisma db push
|
||||
pm2 reload eternalai
|
||||
```
|
||||
|
||||
### 5.3 环境变量变更
|
||||
|
||||
```bash
|
||||
# 编辑 .env 文件
|
||||
nano .env
|
||||
|
||||
# 重启应用使配置生效
|
||||
pm2 reload eternalai --update-env
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 六、回滚
|
||||
|
||||
```bash
|
||||
# 查看部署历史
|
||||
git log --oneline -10
|
||||
|
||||
# 回滚到指定版本
|
||||
git checkout <commit-hash>
|
||||
npm install
|
||||
npx prisma db push
|
||||
pm2 reload eternalai
|
||||
|
||||
# 或回滚到上一个版本
|
||||
git checkout HEAD~1
|
||||
npm install
|
||||
npx prisma db push
|
||||
pm2 reload eternalai
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 七、故障排查
|
||||
|
||||
### 应用无法启动
|
||||
|
||||
```bash
|
||||
# 查看错误日志
|
||||
pm2 logs eternalai --err --lines 50
|
||||
|
||||
# 常见原因:
|
||||
# 1. .env 文件缺失 → 运行 deploy/setup-server.sh
|
||||
# 2. 数据库连接失败 → 检查 DATABASE_URL 和 PostgreSQL 服务
|
||||
# 3. 端口被占用 → 检查 PORT 环境变量
|
||||
```
|
||||
|
||||
### 数据库连接失败
|
||||
|
||||
```bash
|
||||
# 检查 PostgreSQL 服务
|
||||
sudo systemctl status postgresql
|
||||
|
||||
# 测试连接
|
||||
psql -U eternalai -d eternalai -h localhost -W
|
||||
|
||||
# 检查 pg_hba.conf 认证配置
|
||||
sudo cat /etc/postgresql/15/main/pg_hba.conf | grep -v '^#'
|
||||
```
|
||||
|
||||
### Nginx 502 Bad Gateway
|
||||
|
||||
```bash
|
||||
# 检查 Node.js 进程是否运行
|
||||
pm2 status
|
||||
|
||||
# 检查端口
|
||||
curl http://localhost:3001
|
||||
|
||||
# 检查 Nginx 错误日志
|
||||
sudo tail -f /var/log/nginx/eternalai_error.log
|
||||
```
|
||||
|
||||
### PM2 进程未开机自启
|
||||
|
||||
```bash
|
||||
# 重新配置开机自启
|
||||
pm2 startup systemd
|
||||
# 执行输出的 sudo 命令
|
||||
pm2 save
|
||||
```
|
||||
|
|
@ -0,0 +1,160 @@
|
|||
const { test, expect } = require('@playwright/test');
|
||||
const testData = require('./fixtures/test-data');
|
||||
const { cleanDatabase, seedExistingUser, disconnect } = require('./fixtures/database');
|
||||
|
||||
test.describe('认证流程', () => {
|
||||
test.beforeAll(async () => {
|
||||
await cleanDatabase();
|
||||
await seedExistingUser();
|
||||
});
|
||||
|
||||
test.afterAll(async () => {
|
||||
await cleanDatabase();
|
||||
await disconnect();
|
||||
});
|
||||
|
||||
test('注册新用户成功', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
// 点击「我的 XXX」卡片进入 auth 视图(未登录时)
|
||||
await page.locator('[data-action="open-characters"]').first().click();
|
||||
await expect(page.locator('#auth')).toBeVisible();
|
||||
|
||||
// 切换到注册 tab
|
||||
await page.locator('.auth-tabs [data-tab="register"]').click();
|
||||
// 等待注册表单的 fadeIn 动画完成
|
||||
await expect(page.locator('[data-form="register"]')).toBeVisible();
|
||||
await page.waitForTimeout(400);
|
||||
|
||||
// 填写注册表单
|
||||
await page.fill('[data-form="register"] [name="account"]', testData.users.newUser.account);
|
||||
await page.fill('[data-form="register"] [name="password"]', testData.users.newUser.password);
|
||||
await page.fill('[data-form="register"] [name="confirmPassword"]', testData.users.newUser.password);
|
||||
|
||||
// 提交
|
||||
await page.locator('[data-form="register"] button[type="submit"]').click();
|
||||
|
||||
// 注册成功后自动成为创作者,应跳转到创作者中心
|
||||
await expect(page.locator('#creator-center')).toBeVisible({ timeout: 5000 });
|
||||
});
|
||||
|
||||
test('重复注册同一账号失败', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await page.locator('[data-action="open-characters"]').first().click();
|
||||
await expect(page.locator('#auth')).toBeVisible();
|
||||
await page.locator('.auth-tabs [data-tab="register"]').click();
|
||||
// 等待注册表单的 fadeIn 动画完成
|
||||
await expect(page.locator('[data-form="register"]')).toBeVisible();
|
||||
await page.waitForTimeout(400);
|
||||
|
||||
await page.fill('[data-form="register"] [name="account"]', testData.users.existing.account);
|
||||
await page.fill('[data-form="register"] [name="password"]', testData.users.existing.password);
|
||||
await page.fill('[data-form="register"] [name="confirmPassword"]', testData.users.existing.password);
|
||||
|
||||
// 监听 dialog(错误信息通过 alert 弹出)
|
||||
const dialogPromise = page.waitForEvent('dialog');
|
||||
await page.locator('[data-form="register"] button[type="submit"]').click();
|
||||
const dialog = await dialogPromise;
|
||||
expect(dialog.message()).toContain('已注册');
|
||||
await dialog.accept();
|
||||
|
||||
// 应仍停留在 auth 视图
|
||||
await expect(page.locator('#auth')).toBeVisible();
|
||||
});
|
||||
|
||||
test('登录已有用户成功', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await page.locator('[data-action="open-characters"]').first().click();
|
||||
await expect(page.locator('#auth')).toBeVisible();
|
||||
|
||||
// 默认在登录 tab
|
||||
await page.fill('[data-form="login"] [name="account"]', testData.users.existing.account);
|
||||
await page.fill('[data-form="login"] [name="password"]', testData.users.existing.password);
|
||||
await page.locator('[data-form="login"] button[type="submit"]').click();
|
||||
|
||||
// 登录成功后(existing 是创作者)应跳转到创作者中心
|
||||
await expect(page.locator('#creator-center')).toBeVisible({ timeout: 5000 });
|
||||
});
|
||||
|
||||
test('错误密码登录失败', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await page.locator('[data-action="open-characters"]').first().click();
|
||||
await expect(page.locator('#auth')).toBeVisible();
|
||||
|
||||
await page.fill('[data-form="login"] [name="account"]', testData.users.existing.account);
|
||||
await page.fill('[data-form="login"] [name="password"]', 'wrongpassword');
|
||||
|
||||
const dialogPromise = page.waitForEvent('dialog');
|
||||
await page.locator('[data-form="login"] button[type="submit"]').click();
|
||||
const dialog = await dialogPromise;
|
||||
expect(dialog.message()).toContain('账号或密码错误');
|
||||
await dialog.accept();
|
||||
|
||||
// 应仍停留在 auth 视图
|
||||
await expect(page.locator('#auth')).toBeVisible();
|
||||
});
|
||||
|
||||
test('注册时密码不一致提示', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await page.locator('[data-action="open-characters"]').first().click();
|
||||
await expect(page.locator('#auth')).toBeVisible();
|
||||
await page.locator('.auth-tabs [data-tab="register"]').click();
|
||||
// 等待注册表单的 fadeIn 动画完成
|
||||
await expect(page.locator('[data-form="register"]')).toBeVisible();
|
||||
await page.waitForTimeout(400);
|
||||
|
||||
await page.fill('[data-form="register"] [name="account"]', 'mismatch_user');
|
||||
await page.fill('[data-form="register"] [name="password"]', 'password1');
|
||||
await page.fill('[data-form="register"] [name="confirmPassword"]', 'password2');
|
||||
|
||||
// 密码不一致通过原生 setCustomValidity + reportValidity 提示(非 alert)
|
||||
await page.locator('[data-form="register"] button[type="submit"]').click();
|
||||
|
||||
// 验证 confirmPassword 字段的 validationMessage 包含「不一致」
|
||||
const validationMessage = await page
|
||||
.locator('[data-form="register"] [name="confirmPassword"]')
|
||||
.evaluate((el) => el.validationMessage);
|
||||
expect(validationMessage).toContain('不一致');
|
||||
|
||||
// 应仍停留在 auth 视图
|
||||
await expect(page.locator('#auth')).toBeVisible();
|
||||
});
|
||||
|
||||
test('登出后回到首页', async ({ page }) => {
|
||||
// 先登录
|
||||
await page.goto('/');
|
||||
await page.locator('[data-action="open-characters"]').first().click();
|
||||
await expect(page.locator('#auth')).toBeVisible();
|
||||
await page.fill('[data-form="login"] [name="account"]', testData.users.existing.account);
|
||||
await page.fill('[data-form="login"] [name="password"]', testData.users.existing.password);
|
||||
await page.locator('[data-form="login"] button[type="submit"]').click();
|
||||
await expect(page.locator('#creator-center')).toBeVisible({ timeout: 5000 });
|
||||
|
||||
// 切换到设置 tab(登出按钮在设置面板)
|
||||
await page.locator('[data-center-tab="settings"]').click();
|
||||
await expect(page.locator('#center-settings')).toBeVisible();
|
||||
|
||||
// 点击登出
|
||||
await page.locator('[data-action="logout"]').click();
|
||||
|
||||
// 应回到首页
|
||||
await expect(page.locator('#landing')).toBeVisible();
|
||||
});
|
||||
|
||||
test('登录态持久化(刷新页面后仍登录)', async ({ page }) => {
|
||||
// 先登录
|
||||
await page.goto('/');
|
||||
await page.locator('[data-action="open-characters"]').first().click();
|
||||
await expect(page.locator('#auth')).toBeVisible();
|
||||
await page.fill('[data-form="login"] [name="account"]', testData.users.existing.account);
|
||||
await page.fill('[data-form="login"] [name="password"]', testData.users.existing.password);
|
||||
await page.locator('[data-form="login"] button[type="submit"]').click();
|
||||
await expect(page.locator('#creator-center')).toBeVisible({ timeout: 5000 });
|
||||
|
||||
// 刷新页面
|
||||
await page.reload();
|
||||
|
||||
// 首页应可见,且卡片按钮文案应变化(不再是「登录 / 注册」)
|
||||
await expect(page.locator('#landing')).toBeVisible();
|
||||
await expect(page.locator('#characters-btn')).not.toHaveText(/登录/);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,240 @@
|
|||
const { test, expect } = require('@playwright/test');
|
||||
const testData = require('./fixtures/test-data');
|
||||
const { cleanDatabase, seedExistingUser, disconnect, prisma } = require('./fixtures/database');
|
||||
|
||||
test.describe('创作者中心与角色发布/编辑', () => {
|
||||
let existingUser;
|
||||
let existingRole;
|
||||
|
||||
test.beforeAll(async () => {
|
||||
await cleanDatabase();
|
||||
await seedExistingUser();
|
||||
existingUser = await prisma.user.findUnique({
|
||||
where: { account: testData.users.existing.account },
|
||||
});
|
||||
|
||||
existingRole = await prisma.role.create({
|
||||
data: {
|
||||
creatorId: existingUser.id,
|
||||
displayName: '已有角色',
|
||||
gender: 'female',
|
||||
personality: '温柔体贴',
|
||||
background: '来自南方',
|
||||
speechStyle: '轻声细语',
|
||||
greeting: '你好~',
|
||||
desc: '温柔体贴的角色',
|
||||
price: 19.9,
|
||||
status: 'running',
|
||||
temperature: 0.8,
|
||||
maxTokens: 2048,
|
||||
enableMemory: true,
|
||||
enableTools: false,
|
||||
model: 'gpt-4o',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test.afterAll(async () => {
|
||||
await cleanDatabase();
|
||||
await disconnect();
|
||||
});
|
||||
|
||||
async function loginAsExisting(page) {
|
||||
await page.goto('/');
|
||||
await page.locator('[data-action="open-characters"]').first().click();
|
||||
await expect(page.locator('#auth')).toBeVisible();
|
||||
await page.fill('[data-form="login"] [name="account"]', testData.users.existing.account);
|
||||
await page.fill('[data-form="login"] [name="password"]', testData.users.existing.password);
|
||||
await page.locator('[data-form="login"] button[type="submit"]').click();
|
||||
await expect(page.locator('#creator-center')).toBeVisible({ timeout: 5000 });
|
||||
}
|
||||
|
||||
test('创作者中心显示三个 tab', async ({ page }) => {
|
||||
await loginAsExisting(page);
|
||||
await expect(page.locator('[data-center-tab="roles"]')).toBeVisible();
|
||||
await expect(page.locator('[data-center-tab="income"]')).toBeVisible();
|
||||
await expect(page.locator('[data-center-tab="settings"]')).toBeVisible();
|
||||
});
|
||||
|
||||
test('角色 tab 显示已有角色', async ({ page }) => {
|
||||
await loginAsExisting(page);
|
||||
// 默认在 roles tab
|
||||
await expect(page.locator('#creator-role-list .role-card')).toHaveCount(1, { timeout: 5000 });
|
||||
await expect(page.locator('#creator-role-list .role-card__name')).toHaveText('已有角色');
|
||||
});
|
||||
|
||||
test('收入 tab 显示余额和流水', async ({ page }) => {
|
||||
await loginAsExisting(page);
|
||||
await page.locator('[data-center-tab="income"]').click();
|
||||
// income 面板可见(#center-income 带 active class)
|
||||
await expect(page.locator('#center-income')).toBeVisible();
|
||||
// 余额元素可见
|
||||
await expect(page.locator('#income-balance')).toBeVisible({ timeout: 5000 });
|
||||
// 流水列表容器存在
|
||||
await expect(page.locator('#income-list')).toBeVisible();
|
||||
});
|
||||
|
||||
test('设置 tab 可保存昵称和库名', async ({ page }) => {
|
||||
await loginAsExisting(page);
|
||||
await page.locator('[data-center-tab="settings"]').click();
|
||||
await expect(page.locator('#center-settings')).toBeVisible();
|
||||
|
||||
// 修改设置
|
||||
await page.fill('#settings-form [name="creatorName"]', '新昵称');
|
||||
await page.fill('#settings-form [name="libraryName"]', '新库名');
|
||||
|
||||
// 保存成功会弹 alert「设置已保存」
|
||||
const dialogPromise = page.waitForEvent('dialog');
|
||||
await page.locator('#settings-form button[type="submit"]').click();
|
||||
const dialog = await dialogPromise;
|
||||
expect(dialog.message()).toContain('保存');
|
||||
await dialog.accept();
|
||||
});
|
||||
|
||||
test('新建角色完整流程(4步表单)', async ({ page }) => {
|
||||
await loginAsExisting(page);
|
||||
|
||||
// 点击新建角色
|
||||
await page.locator('[data-action="new-role"]').click();
|
||||
await expect(page.locator('#creator')).toBeVisible();
|
||||
|
||||
// Step 1: 基础身份(agentId 必填,pattern [a-zA-Z0-9_]+;displayName 必填)
|
||||
await page.fill('#character-form [name="agentId"]', 'e2e_test_role');
|
||||
await page.fill('#character-form [name="displayName"]', testData.role.displayName);
|
||||
await page.selectOption('#character-form [name="gender"]', testData.role.gender);
|
||||
await page.fill('#character-form [name="age"]', testData.role.age);
|
||||
await page.locator('.form-step.active [data-action="next"]').click();
|
||||
|
||||
// Step 2: 灵魂设定(background / personality / speechStyle 必填)
|
||||
await page.fill('#character-form [name="background"]', testData.role.background);
|
||||
await page.fill('#character-form [name="personality"]', testData.role.personality);
|
||||
await page.fill('#character-form [name="speechStyle"]', testData.role.speechStyle);
|
||||
await page.fill('#character-form [name="likes"]', testData.role.likes);
|
||||
await page.fill('#character-form [name="dislikes"]', testData.role.dislikes);
|
||||
await page.locator('.form-step.active [data-action="next"]').click();
|
||||
|
||||
// Step 3: 关系与记忆(greeting 必填)
|
||||
await page.fill('#character-form [name="relationship"]', testData.role.relationship);
|
||||
await page.fill('#character-form [name="memories"]', testData.role.memories);
|
||||
await page.fill('#character-form [name="secrets"]', testData.role.secrets);
|
||||
await page.fill('#character-form [name="greeting"]', testData.role.greeting);
|
||||
await page.locator('.form-step.active [data-action="next"]').click();
|
||||
|
||||
// Step 4: 运行配置(model/temperature/maxTokens 有默认值;无 price 字段)
|
||||
// 捕获可能的错误弹窗
|
||||
let dialogMessage = '';
|
||||
page.on('dialog', async (dialog) => {
|
||||
dialogMessage = dialog.message();
|
||||
await dialog.accept();
|
||||
});
|
||||
await page.locator('.form-step.active [data-action="publish"]').click();
|
||||
|
||||
// 应显示生成结果面板
|
||||
await expect(page.locator('#result-panel')).toBeVisible({ timeout: 15000 });
|
||||
|
||||
// 验证 Soul.md 内容(单个 #preview-code 元素,默认显示 soul)
|
||||
await expect(page.locator('#preview-code')).toBeVisible();
|
||||
const soulContent = await page.locator('#preview-code').textContent();
|
||||
expect(soulContent).toContain(testData.role.displayName);
|
||||
// generateSoulMd 会把 personality 按 [,,] 拆分成 " | " 连接的标签
|
||||
const personalityTag = testData.role.personality.split(/[,,]/)[0].trim();
|
||||
expect(soulContent).toContain(personalityTag);
|
||||
|
||||
// 切换到 config.yaml 预览(preview-tabs 内的 [data-tab="config"])
|
||||
await page.locator('.preview-tabs [data-tab="config"]').click();
|
||||
const configContent = await page.locator('#preview-code').textContent();
|
||||
expect(configContent).toContain('model:');
|
||||
expect(configContent).toContain('temperature:');
|
||||
});
|
||||
|
||||
test('新建角色后数据库有记录', async ({ page }) => {
|
||||
await loginAsExisting(page);
|
||||
await page.locator('[data-action="new-role"]').click();
|
||||
await expect(page.locator('#creator')).toBeVisible();
|
||||
|
||||
// Step 1: 填写最小必填字段
|
||||
await page.fill('#character-form [name="agentId"]', 'e2e_db_role');
|
||||
await page.fill('#character-form [name="displayName"]', 'DB验证角色');
|
||||
await page.selectOption('#character-form [name="gender"]', 'female');
|
||||
await page.locator('.form-step.active [data-action="next"]').click();
|
||||
|
||||
// Step 2
|
||||
await page.fill('#character-form [name="personality"]', '测试性格');
|
||||
await page.fill('#character-form [name="background"]', '测试背景');
|
||||
await page.fill('#character-form [name="speechStyle"]', '测试风格');
|
||||
await page.locator('.form-step.active [data-action="next"]').click();
|
||||
|
||||
// Step 3
|
||||
await page.fill('#character-form [name="greeting"]', '测试问候');
|
||||
await page.locator('.form-step.active [data-action="next"]').click();
|
||||
|
||||
// Step 4: 直接发布(无 price 字段,使用默认值)
|
||||
let dialogMessage = '';
|
||||
page.on('dialog', async (dialog) => {
|
||||
dialogMessage = dialog.message();
|
||||
await dialog.accept();
|
||||
});
|
||||
await page.locator('.form-step.active [data-action="publish"]').click();
|
||||
await expect(page.locator('#result-panel')).toBeVisible({ timeout: 15000 });
|
||||
|
||||
// 验证数据库
|
||||
const dbRole = await prisma.role.findFirst({
|
||||
where: { displayName: 'DB验证角色' },
|
||||
});
|
||||
expect(dbRole).not.toBeNull();
|
||||
expect(dbRole.personality).toBe('测试性格');
|
||||
expect(dbRole.greeting).toBe('测试问候');
|
||||
expect(dbRole.soulMd).not.toBeNull();
|
||||
expect(dbRole.configYaml).not.toBeNull();
|
||||
});
|
||||
|
||||
test('编辑角色加载已有数据', async ({ page }) => {
|
||||
await loginAsExisting(page);
|
||||
|
||||
// 等待角色列表加载,找到已 seeded 的角色(其他测试可能已创建额外角色)
|
||||
await expect(page.locator('#creator-role-list .role-card')).not.toHaveCount(0, { timeout: 5000 });
|
||||
// 定位到"已有角色"那张卡片的编辑按钮
|
||||
const seededCard = page.locator('#creator-role-list .role-card', { hasText: '已有角色' }).first();
|
||||
await expect(seededCard).toBeVisible({ timeout: 5000 });
|
||||
await seededCard.locator('[data-action="edit-role"]').click();
|
||||
await expect(page.locator('#creator')).toBeVisible();
|
||||
|
||||
// 验证表单已预填(loadRoleForEdit 会拉取 /roles/:id/full 填充表单)
|
||||
await expect(page.locator('#character-form [name="displayName"]')).toHaveValue('已有角色', { timeout: 5000 });
|
||||
await expect(page.locator('#character-form [name="personality"]')).toHaveValue('温柔体贴');
|
||||
});
|
||||
|
||||
test('表单验证 - 必填字段为空时阻止提交', async ({ page }) => {
|
||||
await loginAsExisting(page);
|
||||
await page.locator('[data-action="new-role"]').click();
|
||||
await expect(page.locator('#creator')).toBeVisible();
|
||||
|
||||
// 不填任何字段直接点下一步
|
||||
await page.locator('.form-step.active [data-action="next"]').click();
|
||||
|
||||
// 应仍停留在 step 0(agentId / displayName 必填,验证失败)
|
||||
// step 0 仍 active => agentId 字段仍可见
|
||||
await expect(page.locator('#character-form [name="agentId"]')).toBeVisible();
|
||||
await expect(page.locator('#character-form [name="displayName"]')).toBeVisible();
|
||||
});
|
||||
|
||||
test('步骤导航 - 上一步按钮', async ({ page }) => {
|
||||
await loginAsExisting(page);
|
||||
await page.locator('[data-action="new-role"]').click();
|
||||
await expect(page.locator('#creator')).toBeVisible();
|
||||
|
||||
// 填写 step 1 并前进
|
||||
await page.fill('#character-form [name="agentId"]', 'e2e_nav_test');
|
||||
await page.fill('#character-form [name="displayName"]', '导航测试');
|
||||
await page.selectOption('#character-form [name="gender"]', 'female');
|
||||
await page.locator('.form-step.active [data-action="next"]').click();
|
||||
|
||||
// 进入 step 2 后点上一步(data-action="prev",非 prev-step)
|
||||
await expect(page.locator('#character-form [name="background"]')).toBeVisible();
|
||||
await page.locator('.form-step.active [data-action="prev"]').click();
|
||||
|
||||
// 应回到 step 1,且数据保留
|
||||
await expect(page.locator('#character-form [name="displayName"]')).toBeVisible();
|
||||
await expect(page.locator('#character-form [name="displayName"]')).toHaveValue('导航测试');
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
// 数据库清理 helper
|
||||
const { PrismaClient } = require('@prisma/client');
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
async function cleanDatabase() {
|
||||
await prisma.order.deleteMany();
|
||||
await prisma.role.deleteMany();
|
||||
await prisma.user.deleteMany();
|
||||
}
|
||||
|
||||
async function seedExistingUser() {
|
||||
const bcrypt = require('bcryptjs');
|
||||
const testData = require('./test-data');
|
||||
await prisma.user.create({
|
||||
data: {
|
||||
account: testData.users.existing.account,
|
||||
password: bcrypt.hashSync(testData.users.existing.password, 10),
|
||||
isCreator: true,
|
||||
creatorName: testData.users.existing.account,
|
||||
libraryName: 'E2E测试库',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function disconnect() {
|
||||
await prisma.$disconnect();
|
||||
}
|
||||
|
||||
module.exports = { cleanDatabase, seedExistingUser, disconnect, prisma };
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
// 测试数据 fixtures
|
||||
const testData = {
|
||||
users: {
|
||||
newUser: {
|
||||
account: `e2e_${Date.now()}`,
|
||||
password: 'Test123456',
|
||||
},
|
||||
existing: {
|
||||
account: 'e2e_existing',
|
||||
password: 'Test123456',
|
||||
},
|
||||
},
|
||||
role: {
|
||||
displayName: 'E2E测试角色',
|
||||
gender: 'female',
|
||||
age: '25',
|
||||
relationship: '女友',
|
||||
personality: '温柔,体贴,善解人意',
|
||||
background: '来自江南水乡的女孩,喜欢读书和绘画',
|
||||
speechStyle: '轻声细语,偶尔带点小俏皮',
|
||||
likes: '读书,绘画,猫咪',
|
||||
dislikes: '吵闹,谎言',
|
||||
memories: '一起看过的樱花,第一次约会',
|
||||
secrets: '其实很怕打雷',
|
||||
greeting: '你回来啦~今天过得怎么样?',
|
||||
model: 'gpt-4o',
|
||||
temperature: '0.8',
|
||||
maxTokens: '2048',
|
||||
price: '29.9',
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = testData;
|
||||
|
|
@ -0,0 +1,214 @@
|
|||
const { test, expect } = require('@playwright/test');
|
||||
const bcrypt = require('bcryptjs');
|
||||
const { cleanDatabase, disconnect, prisma } = require('./fixtures/database');
|
||||
|
||||
test.describe('导航与可访问性', () => {
|
||||
test.beforeAll(async () => {
|
||||
await cleanDatabase();
|
||||
});
|
||||
|
||||
test.afterAll(async () => {
|
||||
await cleanDatabase();
|
||||
await disconnect();
|
||||
});
|
||||
|
||||
test('首页显示两张卡片', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await expect(page.locator('#landing')).toBeVisible();
|
||||
// 两张卡片:open-characters 和 open-distill
|
||||
const cards = page.locator('[data-action="open-characters"], [data-action="open-distill"]');
|
||||
await expect(cards).toHaveCount(2, { timeout: 5000 });
|
||||
});
|
||||
|
||||
test('底部 tabBar 三个 tab 可见', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await expect(page.locator('.tab-bar')).toBeVisible();
|
||||
await expect(page.locator('[data-tab-action="tab-home"]')).toBeVisible();
|
||||
await expect(page.locator('[data-tab-action="tab-distill"]')).toBeVisible();
|
||||
await expect(page.locator('[data-tab-action="tab-mine"]')).toBeVisible();
|
||||
});
|
||||
|
||||
test('tabBar 切换视图', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
|
||||
// 点击蒸馏前任 tab
|
||||
await page.locator('[data-tab-action="tab-distill"]').click();
|
||||
await expect(page.locator('#distill')).toBeVisible();
|
||||
|
||||
// 点击首页 tab
|
||||
await page.locator('[data-tab-action="tab-home"]').click();
|
||||
await expect(page.locator('#landing')).toBeVisible();
|
||||
|
||||
// 点击我的 tab(未登录应跳转到 auth)
|
||||
await page.locator('[data-tab-action="tab-mine"]').click();
|
||||
await expect(page.locator('#auth')).toBeVisible();
|
||||
});
|
||||
|
||||
test('首页卡片点击进入对应视图', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
|
||||
// 点击蒸馏前任卡片
|
||||
await page.locator('[data-action="open-distill"]').first().click();
|
||||
await expect(page.locator('#distill')).toBeVisible();
|
||||
|
||||
// 返回首页
|
||||
await page.locator('[data-tab-action="tab-home"]').click();
|
||||
await expect(page.locator('#landing')).toBeVisible();
|
||||
|
||||
// 点击「我的 XXX」卡片(未登录进入 auth)
|
||||
await page.locator('[data-action="open-characters"]').first().click();
|
||||
await expect(page.locator('#auth')).toBeVisible();
|
||||
});
|
||||
|
||||
test('关于页面可访问', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await page.locator('[data-action="open-about"]').first().click();
|
||||
await expect(page.locator('#about')).toBeVisible();
|
||||
});
|
||||
|
||||
test('创作者入驻页面可访问', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await page.locator('[data-action="open-onboarding"]').first().click();
|
||||
await expect(page.locator('#onboarding')).toBeVisible();
|
||||
});
|
||||
|
||||
test('FAQ 折叠展开', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await page.locator('[data-action="open-about"]').first().click();
|
||||
await expect(page.locator('#about')).toBeVisible();
|
||||
|
||||
// 点击第一个 FAQ 按钮(.faq-q,非 .faq-item__question)
|
||||
const faqButton = page.locator('.faq-q').first();
|
||||
await faqButton.click();
|
||||
|
||||
// 验证 aria-expanded 变为 true
|
||||
await expect(faqButton).toHaveAttribute('aria-expanded', 'true');
|
||||
|
||||
// 再次点击折叠
|
||||
await faqButton.click();
|
||||
await expect(faqButton).toHaveAttribute('aria-expanded', 'false');
|
||||
});
|
||||
|
||||
test('返回按钮基于历史记录导航', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
|
||||
// 导航路径: landing → about → 返回
|
||||
await page.locator('[data-action="open-about"]').first().click();
|
||||
await expect(page.locator('#about')).toBeVisible();
|
||||
|
||||
// about 视图使用 data-action="back"
|
||||
await page.locator('#about [data-action="back"]').click();
|
||||
await expect(page.locator('#landing')).toBeVisible();
|
||||
});
|
||||
|
||||
test('跳过链接(a11y)存在且可用', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
const skipLink = page.locator('.skip-link');
|
||||
// 跳过链接应存在
|
||||
await expect(skipLink).toHaveCount(1);
|
||||
});
|
||||
|
||||
test('tabBar 有正确的 ARIA 角色', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
const tabbar = page.locator('.tab-bar');
|
||||
await expect(tabbar).toHaveAttribute('role', 'tablist');
|
||||
|
||||
// tab-bar 内的按钮应有 role="tab"
|
||||
const tabs = page.locator('.tab-bar [data-tab-action]');
|
||||
const count = await tabs.count();
|
||||
expect(count).toBe(3);
|
||||
for (let i = 0; i < count; i++) {
|
||||
await expect(tabs.nth(i)).toHaveAttribute('role', 'tab');
|
||||
}
|
||||
});
|
||||
|
||||
test('视图切换时 aria-live 播报', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
// #sr-announce 是 aria-live 区域,role="status" aria-live="polite"
|
||||
const liveRegion = page.locator('#sr-announce');
|
||||
await expect(liveRegion).toHaveAttribute('aria-live', 'polite');
|
||||
await expect(liveRegion).toHaveAttribute('role', 'status');
|
||||
|
||||
// 切换视图后 live region 应有内容
|
||||
await page.locator('[data-action="open-about"]').first().click();
|
||||
await expect(liveRegion).not.toHaveText('');
|
||||
const liveText = await liveRegion.textContent();
|
||||
expect(liveText.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('角色卡片支持键盘操作', async ({ page }) => {
|
||||
// 创建非创作者用户 + 角色
|
||||
const bcrypt = require('bcryptjs');
|
||||
const user = await prisma.user.create({
|
||||
data: {
|
||||
account: 'e2e_keyboard_user',
|
||||
password: bcrypt.hashSync('Test123456', 10),
|
||||
isCreator: false,
|
||||
libraryName: '键盘测试库',
|
||||
},
|
||||
});
|
||||
await prisma.role.create({
|
||||
data: {
|
||||
creatorId: user.id,
|
||||
displayName: '键盘测试角色',
|
||||
gender: 'female',
|
||||
personality: '测试',
|
||||
background: '测试',
|
||||
speechStyle: '测试',
|
||||
greeting: '测试',
|
||||
desc: '键盘测试',
|
||||
price: 9.9,
|
||||
status: 'running',
|
||||
},
|
||||
});
|
||||
|
||||
// 登录非创作者(登录后自动进入角色库)
|
||||
await page.goto('/');
|
||||
await page.locator('[data-action="open-characters"]').first().click();
|
||||
await expect(page.locator('#auth')).toBeVisible();
|
||||
await page.fill('[data-form="login"] [name="account"]', 'e2e_keyboard_user');
|
||||
await page.fill('[data-form="login"] [name="password"]', 'Test123456');
|
||||
await page.locator('[data-form="login"] button[type="submit"]').click();
|
||||
await expect(page.locator('#role-library')).toBeVisible({ timeout: 5000 });
|
||||
|
||||
// 聚焦角色卡片(role-card 有 tabindex="0")
|
||||
const roleCard = page.locator('#role-list .role-card').first();
|
||||
await roleCard.focus();
|
||||
|
||||
// 按 Enter 应进入详情
|
||||
await page.keyboard.press('Enter');
|
||||
await expect(page.locator('#role-detail')).toBeVisible({ timeout: 5000 });
|
||||
|
||||
// 清理
|
||||
await prisma.role.deleteMany();
|
||||
await prisma.user.deleteMany();
|
||||
});
|
||||
|
||||
test('表单 label 与 input 关联', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await page.locator('[data-action="open-characters"]').first().click();
|
||||
await expect(page.locator('#auth')).toBeVisible();
|
||||
|
||||
// 检查登录表单的 input 存在
|
||||
const accountInput = page.locator('[data-form="login"] [name="account"]');
|
||||
await expect(accountInput).toBeVisible();
|
||||
|
||||
// 检查是否有对应的 label(包裹式 label 包含账号相关文案)
|
||||
const labelText = await page.locator('label').filter({ hasText: /账号|用户名|Account/i }).count();
|
||||
expect(labelText).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('focus-visible 样式存在', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
// Tab 到第一个可聚焦元素
|
||||
await page.keyboard.press('Tab');
|
||||
// 聚焦元素应有可见的焦点样式(:focus-visible 使用 outline)
|
||||
const focused = await page.evaluate(() => {
|
||||
const el = document.activeElement;
|
||||
if (!el) return false;
|
||||
const styles = window.getComputedStyle(el);
|
||||
return styles.outlineStyle !== 'none' || styles.boxShadow !== 'none';
|
||||
});
|
||||
expect(focused).toBe(true);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,148 @@
|
|||
const { test, expect } = require('@playwright/test');
|
||||
const bcrypt = require('bcryptjs');
|
||||
const testData = require('./fixtures/test-data');
|
||||
const { cleanDatabase, seedExistingUser, disconnect, prisma } = require('./fixtures/database');
|
||||
|
||||
// 非创作者用户(用于角色库测试)
|
||||
const NON_CREATOR = {
|
||||
account: 'e2e_noncreator',
|
||||
password: 'Test123456',
|
||||
};
|
||||
|
||||
test.describe('角色库与角色详情', () => {
|
||||
let existingUser;
|
||||
let testRole;
|
||||
|
||||
test.beforeAll(async () => {
|
||||
await cleanDatabase();
|
||||
await seedExistingUser();
|
||||
existingUser = await prisma.user.findUnique({
|
||||
where: { account: testData.users.existing.account },
|
||||
});
|
||||
|
||||
// 创建非创作者用户(open-characters 对非创作者进入 role-library)
|
||||
await prisma.user.create({
|
||||
data: {
|
||||
account: NON_CREATOR.account,
|
||||
password: bcrypt.hashSync(NON_CREATOR.password, 10),
|
||||
isCreator: false,
|
||||
libraryName: 'E2E测试库',
|
||||
},
|
||||
});
|
||||
|
||||
// 创建一个测试角色(status=running 才会在角色库展示)
|
||||
testRole = await prisma.role.create({
|
||||
data: {
|
||||
creatorId: existingUser.id,
|
||||
displayName: '测试角色A',
|
||||
gender: 'female',
|
||||
age: '24',
|
||||
relationship: '女友',
|
||||
personality: '温柔可爱,善解人意',
|
||||
background: '来自海边的小镇女孩',
|
||||
speechStyle: '轻柔细腻',
|
||||
greeting: '你好呀~',
|
||||
desc: '温柔可爱的女友角色',
|
||||
price: 29.9,
|
||||
status: 'running',
|
||||
avatar: 'https://example.com/avatar.png',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test.afterAll(async () => {
|
||||
await cleanDatabase();
|
||||
await disconnect();
|
||||
});
|
||||
|
||||
// 以非创作者身份登录,登录后自动进入角色库
|
||||
async function loginAsNonCreator(page) {
|
||||
await page.goto('/');
|
||||
await page.locator('[data-action="open-characters"]').first().click();
|
||||
await expect(page.locator('#auth')).toBeVisible();
|
||||
// 确保登录 tab 激活
|
||||
await page.locator('.auth-tabs [data-tab="login"]').click();
|
||||
await page.locator('[data-form="login"]').waitFor();
|
||||
await page.fill('[data-form="login"] [name="account"]', NON_CREATOR.account);
|
||||
await page.fill('[data-form="login"] [name="password"]', NON_CREATOR.password);
|
||||
await page.locator('[data-form="login"] button[type="submit"]').click();
|
||||
await expect(page.locator('#role-library')).toBeVisible({ timeout: 5000 });
|
||||
}
|
||||
|
||||
test('角色库显示已上架角色', async ({ page }) => {
|
||||
await loginAsNonCreator(page);
|
||||
|
||||
// 应显示测试角色
|
||||
await expect(page.locator('#role-list .role-card')).toHaveCount(1, { timeout: 5000 });
|
||||
await expect(page.locator('#role-list .role-card__name')).toHaveText('测试角色A');
|
||||
});
|
||||
|
||||
test('空角色库显示空状态', async ({ page }) => {
|
||||
// 清空角色
|
||||
await prisma.role.deleteMany();
|
||||
|
||||
await loginAsNonCreator(page);
|
||||
|
||||
// 应显示空状态
|
||||
await expect(page.locator('#library-empty')).toBeVisible({ timeout: 5000 });
|
||||
});
|
||||
|
||||
test('点击角色卡片进入详情页', async ({ page }) => {
|
||||
// 重新创建角色
|
||||
testRole = await prisma.role.create({
|
||||
data: {
|
||||
creatorId: existingUser.id,
|
||||
displayName: '详情测试角色',
|
||||
gender: 'male',
|
||||
personality: '阳光开朗',
|
||||
background: '运动少年',
|
||||
speechStyle: '热情直接',
|
||||
greeting: '嘿!你好!',
|
||||
desc: '阳光开朗的男孩',
|
||||
price: 19.9,
|
||||
status: 'running',
|
||||
},
|
||||
});
|
||||
|
||||
await loginAsNonCreator(page);
|
||||
|
||||
// 点击角色卡片
|
||||
await page.locator('#role-list .role-card').first().click();
|
||||
await expect(page.locator('#role-detail')).toBeVisible();
|
||||
|
||||
// 验证详情内容
|
||||
await expect(page.locator('#detail-name')).toHaveText('详情测试角色');
|
||||
await expect(page.locator('#detail-role-name')).toHaveText('详情测试角色');
|
||||
await expect(page.locator('#detail-price')).toContainText('19.9');
|
||||
});
|
||||
|
||||
test('角色详情页返回按钮', async ({ page }) => {
|
||||
await loginAsNonCreator(page);
|
||||
|
||||
await page.locator('#role-list .role-card').first().click();
|
||||
await expect(page.locator('#role-detail')).toBeVisible();
|
||||
|
||||
// 点击返回(role-detail 使用 back-to-library)
|
||||
await page.locator('[data-action="back-to-library"]').click();
|
||||
await expect(page.locator('#role-library')).toBeVisible();
|
||||
});
|
||||
|
||||
test('角色详情付款流程', async ({ page }) => {
|
||||
await loginAsNonCreator(page);
|
||||
|
||||
await page.locator('#role-list .role-card').first().click();
|
||||
await expect(page.locator('#role-detail')).toBeVisible();
|
||||
|
||||
// 付款前:pre-pay 按钮区可见,paid 区隐藏
|
||||
await expect(page.locator('#detail-actions-pre')).toBeVisible();
|
||||
await expect(page.locator('#detail-paid')).toBeHidden();
|
||||
|
||||
// 点击付款按钮(data-action="pay",非 pay-role)
|
||||
await page.locator('[data-action="pay"]').click();
|
||||
|
||||
// 应显示已付款区域(含二维码 / 头像下载)
|
||||
await expect(page.locator('#detail-paid')).toBeVisible({ timeout: 5000 });
|
||||
await expect(page.locator('#detail-actions-pre')).toBeHidden();
|
||||
await expect(page.locator('[data-action="download-avatar"]')).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
module.exports = {
|
||||
apps: [
|
||||
{
|
||||
name: 'eternalai',
|
||||
script: 'server.js',
|
||||
cwd: __dirname,
|
||||
instances: 1,
|
||||
exec_mode: 'fork',
|
||||
autorestart: true,
|
||||
max_restarts: 10,
|
||||
restart_delay: 3000,
|
||||
max_memory_restart: '512M',
|
||||
env: {
|
||||
NODE_ENV: 'production',
|
||||
},
|
||||
error_file: './logs/err.log',
|
||||
out_file: './logs/out.log',
|
||||
log_date_format: 'YYYY-MM-DD HH:mm:ss Z',
|
||||
merge_logs: true,
|
||||
time: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
15
index.html
15
index.html
|
|
@ -387,6 +387,19 @@
|
|||
<button class="btn btn--primary btn--block" type="submit">保存设置</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- API Key 管理 -->
|
||||
<div class="api-key-section" style="margin-top: 2rem;">
|
||||
<h3 class="settings-section-title">API Key 管理</h3>
|
||||
<p class="settings-section-desc" style="color: var(--text-muted); font-size: 0.875rem; margin-bottom: 1rem;">
|
||||
用于在 Hermes 机器上通过 curl 拉取角色配置文件
|
||||
</p>
|
||||
<div class="form-actions" style="margin-bottom: 1rem;">
|
||||
<button class="btn btn--outline btn--block" type="button" data-action="generate-apikey">生成新 API Key</button>
|
||||
</div>
|
||||
<div id="apikey-list"></div>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<button class="btn btn--ghost btn--block" type="button" data-action="logout">退出登录</button>
|
||||
</div>
|
||||
|
|
@ -509,7 +522,7 @@
|
|||
</label>
|
||||
<label class="field field--half">
|
||||
<span class="field__label">Temperature</span>
|
||||
<input class="field__input" name="temperature" type="number" step="0.1" min="0" max="2" value="0.85" />
|
||||
<input class="field__input" name="temperature" type="number" step="0.05" min="0" max="2" value="0.85" />
|
||||
</label>
|
||||
<label class="field field--half">
|
||||
<span class="field__label">Max Tokens</span>
|
||||
|
|
|
|||
|
|
@ -1,849 +0,0 @@
|
|||
{
|
||||
"name": "eternalai",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"node_modules/accepts": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/accepts/-/accepts-2.0.0.tgz",
|
||||
"integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mime-types": "^3.0.0",
|
||||
"negotiator": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/body-parser": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmmirror.com/body-parser/-/body-parser-2.3.0.tgz",
|
||||
"integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bytes": "^3.1.2",
|
||||
"content-type": "^2.0.0",
|
||||
"debug": "^4.4.3",
|
||||
"http-errors": "^2.0.1",
|
||||
"iconv-lite": "^0.7.2",
|
||||
"on-finished": "^2.4.1",
|
||||
"qs": "^6.15.2",
|
||||
"raw-body": "^3.0.2",
|
||||
"type-is": "^2.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/body-parser/node_modules/content-type": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/content-type/-/content-type-2.0.0.tgz",
|
||||
"integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/bytes": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmmirror.com/bytes/-/bytes-3.1.2.tgz",
|
||||
"integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/call-bind-apply-helpers": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
||||
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"function-bind": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/call-bound": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmmirror.com/call-bound/-/call-bound-1.0.4.tgz",
|
||||
"integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.2",
|
||||
"get-intrinsic": "^1.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/content-disposition": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/content-disposition/-/content-disposition-1.1.0.tgz",
|
||||
"integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/content-type": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmmirror.com/content-type/-/content-type-1.0.5.tgz",
|
||||
"integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/cookie": {
|
||||
"version": "0.7.2",
|
||||
"resolved": "https://registry.npmmirror.com/cookie/-/cookie-0.7.2.tgz",
|
||||
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/cookie-signature": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmmirror.com/cookie-signature/-/cookie-signature-1.2.2.tgz",
|
||||
"integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.4.3",
|
||||
"resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz",
|
||||
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"supports-color": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/depd": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/depd/-/depd-2.0.0.tgz",
|
||||
"integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/dunder-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.1",
|
||||
"es-errors": "^1.3.0",
|
||||
"gopd": "^1.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/ee-first": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmmirror.com/ee-first/-/ee-first-1.1.1.tgz",
|
||||
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/encodeurl": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/encodeurl/-/encodeurl-2.0.0.tgz",
|
||||
"integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/es-define-property": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-errors": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz",
|
||||
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-object-atoms": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.2.tgz",
|
||||
"integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/escape-html": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmmirror.com/escape-html/-/escape-html-1.0.3.tgz",
|
||||
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/etag": {
|
||||
"version": "1.8.1",
|
||||
"resolved": "https://registry.npmmirror.com/etag/-/etag-1.8.1.tgz",
|
||||
"integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/express": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmmirror.com/express/-/express-5.2.1.tgz",
|
||||
"integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"accepts": "^2.0.0",
|
||||
"body-parser": "^2.2.1",
|
||||
"content-disposition": "^1.0.0",
|
||||
"content-type": "^1.0.5",
|
||||
"cookie": "^0.7.1",
|
||||
"cookie-signature": "^1.2.1",
|
||||
"debug": "^4.4.0",
|
||||
"depd": "^2.0.0",
|
||||
"encodeurl": "^2.0.0",
|
||||
"escape-html": "^1.0.3",
|
||||
"etag": "^1.8.1",
|
||||
"finalhandler": "^2.1.0",
|
||||
"fresh": "^2.0.0",
|
||||
"http-errors": "^2.0.0",
|
||||
"merge-descriptors": "^2.0.0",
|
||||
"mime-types": "^3.0.0",
|
||||
"on-finished": "^2.4.1",
|
||||
"once": "^1.4.0",
|
||||
"parseurl": "^1.3.3",
|
||||
"proxy-addr": "^2.0.7",
|
||||
"qs": "^6.14.0",
|
||||
"range-parser": "^1.2.1",
|
||||
"router": "^2.2.0",
|
||||
"send": "^1.1.0",
|
||||
"serve-static": "^2.2.0",
|
||||
"statuses": "^2.0.1",
|
||||
"type-is": "^2.0.1",
|
||||
"vary": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/finalhandler": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmmirror.com/finalhandler/-/finalhandler-2.1.1.tgz",
|
||||
"integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "^4.4.0",
|
||||
"encodeurl": "^2.0.0",
|
||||
"escape-html": "^1.0.3",
|
||||
"on-finished": "^2.4.1",
|
||||
"parseurl": "^1.3.3",
|
||||
"statuses": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/forwarded": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmmirror.com/forwarded/-/forwarded-0.2.0.tgz",
|
||||
"integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/fresh": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/fresh/-/fresh-2.0.0.tgz",
|
||||
"integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/function-bind": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz",
|
||||
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/get-intrinsic": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
||||
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.2",
|
||||
"es-define-property": "^1.0.1",
|
||||
"es-errors": "^1.3.0",
|
||||
"es-object-atoms": "^1.1.1",
|
||||
"function-bind": "^1.1.2",
|
||||
"get-proto": "^1.0.1",
|
||||
"gopd": "^1.2.0",
|
||||
"has-symbols": "^1.1.0",
|
||||
"hasown": "^2.0.2",
|
||||
"math-intrinsics": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/get-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/get-proto/-/get-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"dunder-proto": "^1.0.1",
|
||||
"es-object-atoms": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/gopd": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz",
|
||||
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/has-symbols": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz",
|
||||
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/hasown": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.4.tgz",
|
||||
"integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"function-bind": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/http-errors": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/http-errors/-/http-errors-2.0.1.tgz",
|
||||
"integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"depd": "~2.0.0",
|
||||
"inherits": "~2.0.4",
|
||||
"setprototypeof": "~1.2.0",
|
||||
"statuses": "~2.0.2",
|
||||
"toidentifier": "~1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/iconv-lite": {
|
||||
"version": "0.7.2",
|
||||
"resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.7.2.tgz",
|
||||
"integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/inherits": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz",
|
||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/ipaddr.js": {
|
||||
"version": "1.9.1",
|
||||
"resolved": "https://registry.npmmirror.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
|
||||
"integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/is-promise": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/is-promise/-/is-promise-4.0.0.tgz",
|
||||
"integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/math-intrinsics": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/media-typer": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/media-typer/-/media-typer-1.1.0.tgz",
|
||||
"integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/merge-descriptors": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/merge-descriptors/-/merge-descriptors-2.0.0.tgz",
|
||||
"integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-db": {
|
||||
"version": "1.54.0",
|
||||
"resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.54.0.tgz",
|
||||
"integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-types": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-3.0.2.tgz",
|
||||
"integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mime-db": "^1.54.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/negotiator": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/negotiator/-/negotiator-1.0.0.tgz",
|
||||
"integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/object-inspect": {
|
||||
"version": "1.13.4",
|
||||
"resolved": "https://registry.npmmirror.com/object-inspect/-/object-inspect-1.13.4.tgz",
|
||||
"integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/on-finished": {
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmmirror.com/on-finished/-/on-finished-2.4.1.tgz",
|
||||
"integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ee-first": "1.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/once": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmmirror.com/once/-/once-1.4.0.tgz",
|
||||
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"wrappy": "1"
|
||||
}
|
||||
},
|
||||
"node_modules/parseurl": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmmirror.com/parseurl/-/parseurl-1.3.3.tgz",
|
||||
"integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/path-to-regexp": {
|
||||
"version": "8.4.2",
|
||||
"resolved": "https://registry.npmmirror.com/path-to-regexp/-/path-to-regexp-8.4.2.tgz",
|
||||
"integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/proxy-addr": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmmirror.com/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
||||
"integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"forwarded": "0.2.0",
|
||||
"ipaddr.js": "1.9.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/qs": {
|
||||
"version": "6.15.2",
|
||||
"resolved": "https://registry.npmmirror.com/qs/-/qs-6.15.2.tgz",
|
||||
"integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"side-channel": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/range-parser": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmmirror.com/range-parser/-/range-parser-1.2.1.tgz",
|
||||
"integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/raw-body": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/raw-body/-/raw-body-3.0.2.tgz",
|
||||
"integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bytes": "~3.1.2",
|
||||
"http-errors": "~2.0.1",
|
||||
"iconv-lite": "~0.7.0",
|
||||
"unpipe": "~1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/router": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmmirror.com/router/-/router-2.2.0.tgz",
|
||||
"integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "^4.4.0",
|
||||
"depd": "^2.0.0",
|
||||
"is-promise": "^4.0.0",
|
||||
"parseurl": "^1.3.3",
|
||||
"path-to-regexp": "^8.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"node_modules/safer-buffer": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmmirror.com/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/send": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmmirror.com/send/-/send-1.2.1.tgz",
|
||||
"integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "^4.4.3",
|
||||
"encodeurl": "^2.0.0",
|
||||
"escape-html": "^1.0.3",
|
||||
"etag": "^1.8.1",
|
||||
"fresh": "^2.0.0",
|
||||
"http-errors": "^2.0.1",
|
||||
"mime-types": "^3.0.2",
|
||||
"ms": "^2.1.3",
|
||||
"on-finished": "^2.4.1",
|
||||
"range-parser": "^1.2.1",
|
||||
"statuses": "^2.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/serve-static": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmmirror.com/serve-static/-/serve-static-2.2.1.tgz",
|
||||
"integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"encodeurl": "^2.0.0",
|
||||
"escape-html": "^1.0.3",
|
||||
"parseurl": "^1.3.3",
|
||||
"send": "^1.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/setprototypeof": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmmirror.com/setprototypeof/-/setprototypeof-1.2.0.tgz",
|
||||
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/side-channel": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/side-channel/-/side-channel-1.1.0.tgz",
|
||||
"integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"object-inspect": "^1.13.3",
|
||||
"side-channel-list": "^1.0.0",
|
||||
"side-channel-map": "^1.0.1",
|
||||
"side-channel-weakmap": "^1.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/side-channel-list": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/side-channel-list/-/side-channel-list-1.0.1.tgz",
|
||||
"integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"object-inspect": "^1.13.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/side-channel-map": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/side-channel-map/-/side-channel-map-1.0.1.tgz",
|
||||
"integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bound": "^1.0.2",
|
||||
"es-errors": "^1.3.0",
|
||||
"get-intrinsic": "^1.2.5",
|
||||
"object-inspect": "^1.13.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/side-channel-weakmap": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
|
||||
"integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bound": "^1.0.2",
|
||||
"es-errors": "^1.3.0",
|
||||
"get-intrinsic": "^1.2.5",
|
||||
"object-inspect": "^1.13.3",
|
||||
"side-channel-map": "^1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/statuses": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/statuses/-/statuses-2.0.2.tgz",
|
||||
"integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/toidentifier": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/toidentifier/-/toidentifier-1.0.1.tgz",
|
||||
"integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/type-is": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/type-is/-/type-is-2.1.0.tgz",
|
||||
"integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"content-type": "^2.0.0",
|
||||
"media-typer": "^1.1.0",
|
||||
"mime-types": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/type-is/node_modules/content-type": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/content-type/-/content-type-2.0.0.tgz",
|
||||
"integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/unpipe": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/unpipe/-/unpipe-1.0.0.tgz",
|
||||
"integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/vary": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmmirror.com/vary/-/vary-1.1.2.tgz",
|
||||
"integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/wrappy": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/wrappy/-/wrappy-1.0.2.tgz",
|
||||
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
|
||||
"license": "ISC"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,250 +0,0 @@
|
|||
2.0.0 / 2024-08-31
|
||||
==================
|
||||
|
||||
* Drop node <18 support
|
||||
* deps: mime-types@^3.0.0
|
||||
* deps: negotiator@^1.0.0
|
||||
|
||||
1.3.8 / 2022-02-02
|
||||
==================
|
||||
|
||||
* deps: mime-types@~2.1.34
|
||||
- deps: mime-db@~1.51.0
|
||||
* deps: negotiator@0.6.3
|
||||
|
||||
1.3.7 / 2019-04-29
|
||||
==================
|
||||
|
||||
* deps: negotiator@0.6.2
|
||||
- Fix sorting charset, encoding, and language with extra parameters
|
||||
|
||||
1.3.6 / 2019-04-28
|
||||
==================
|
||||
|
||||
* deps: mime-types@~2.1.24
|
||||
- deps: mime-db@~1.40.0
|
||||
|
||||
1.3.5 / 2018-02-28
|
||||
==================
|
||||
|
||||
* deps: mime-types@~2.1.18
|
||||
- deps: mime-db@~1.33.0
|
||||
|
||||
1.3.4 / 2017-08-22
|
||||
==================
|
||||
|
||||
* deps: mime-types@~2.1.16
|
||||
- deps: mime-db@~1.29.0
|
||||
|
||||
1.3.3 / 2016-05-02
|
||||
==================
|
||||
|
||||
* deps: mime-types@~2.1.11
|
||||
- deps: mime-db@~1.23.0
|
||||
* deps: negotiator@0.6.1
|
||||
- perf: improve `Accept` parsing speed
|
||||
- perf: improve `Accept-Charset` parsing speed
|
||||
- perf: improve `Accept-Encoding` parsing speed
|
||||
- perf: improve `Accept-Language` parsing speed
|
||||
|
||||
1.3.2 / 2016-03-08
|
||||
==================
|
||||
|
||||
* deps: mime-types@~2.1.10
|
||||
- Fix extension of `application/dash+xml`
|
||||
- Update primary extension for `audio/mp4`
|
||||
- deps: mime-db@~1.22.0
|
||||
|
||||
1.3.1 / 2016-01-19
|
||||
==================
|
||||
|
||||
* deps: mime-types@~2.1.9
|
||||
- deps: mime-db@~1.21.0
|
||||
|
||||
1.3.0 / 2015-09-29
|
||||
==================
|
||||
|
||||
* deps: mime-types@~2.1.7
|
||||
- deps: mime-db@~1.19.0
|
||||
* deps: negotiator@0.6.0
|
||||
- Fix including type extensions in parameters in `Accept` parsing
|
||||
- Fix parsing `Accept` parameters with quoted equals
|
||||
- Fix parsing `Accept` parameters with quoted semicolons
|
||||
- Lazy-load modules from main entry point
|
||||
- perf: delay type concatenation until needed
|
||||
- perf: enable strict mode
|
||||
- perf: hoist regular expressions
|
||||
- perf: remove closures getting spec properties
|
||||
- perf: remove a closure from media type parsing
|
||||
- perf: remove property delete from media type parsing
|
||||
|
||||
1.2.13 / 2015-09-06
|
||||
===================
|
||||
|
||||
* deps: mime-types@~2.1.6
|
||||
- deps: mime-db@~1.18.0
|
||||
|
||||
1.2.12 / 2015-07-30
|
||||
===================
|
||||
|
||||
* deps: mime-types@~2.1.4
|
||||
- deps: mime-db@~1.16.0
|
||||
|
||||
1.2.11 / 2015-07-16
|
||||
===================
|
||||
|
||||
* deps: mime-types@~2.1.3
|
||||
- deps: mime-db@~1.15.0
|
||||
|
||||
1.2.10 / 2015-07-01
|
||||
===================
|
||||
|
||||
* deps: mime-types@~2.1.2
|
||||
- deps: mime-db@~1.14.0
|
||||
|
||||
1.2.9 / 2015-06-08
|
||||
==================
|
||||
|
||||
* deps: mime-types@~2.1.1
|
||||
- perf: fix deopt during mapping
|
||||
|
||||
1.2.8 / 2015-06-07
|
||||
==================
|
||||
|
||||
* deps: mime-types@~2.1.0
|
||||
- deps: mime-db@~1.13.0
|
||||
* perf: avoid argument reassignment & argument slice
|
||||
* perf: avoid negotiator recursive construction
|
||||
* perf: enable strict mode
|
||||
* perf: remove unnecessary bitwise operator
|
||||
|
||||
1.2.7 / 2015-05-10
|
||||
==================
|
||||
|
||||
* deps: negotiator@0.5.3
|
||||
- Fix media type parameter matching to be case-insensitive
|
||||
|
||||
1.2.6 / 2015-05-07
|
||||
==================
|
||||
|
||||
* deps: mime-types@~2.0.11
|
||||
- deps: mime-db@~1.9.1
|
||||
* deps: negotiator@0.5.2
|
||||
- Fix comparing media types with quoted values
|
||||
- Fix splitting media types with quoted commas
|
||||
|
||||
1.2.5 / 2015-03-13
|
||||
==================
|
||||
|
||||
* deps: mime-types@~2.0.10
|
||||
- deps: mime-db@~1.8.0
|
||||
|
||||
1.2.4 / 2015-02-14
|
||||
==================
|
||||
|
||||
* Support Node.js 0.6
|
||||
* deps: mime-types@~2.0.9
|
||||
- deps: mime-db@~1.7.0
|
||||
* deps: negotiator@0.5.1
|
||||
- Fix preference sorting to be stable for long acceptable lists
|
||||
|
||||
1.2.3 / 2015-01-31
|
||||
==================
|
||||
|
||||
* deps: mime-types@~2.0.8
|
||||
- deps: mime-db@~1.6.0
|
||||
|
||||
1.2.2 / 2014-12-30
|
||||
==================
|
||||
|
||||
* deps: mime-types@~2.0.7
|
||||
- deps: mime-db@~1.5.0
|
||||
|
||||
1.2.1 / 2014-12-30
|
||||
==================
|
||||
|
||||
* deps: mime-types@~2.0.5
|
||||
- deps: mime-db@~1.3.1
|
||||
|
||||
1.2.0 / 2014-12-19
|
||||
==================
|
||||
|
||||
* deps: negotiator@0.5.0
|
||||
- Fix list return order when large accepted list
|
||||
- Fix missing identity encoding when q=0 exists
|
||||
- Remove dynamic building of Negotiator class
|
||||
|
||||
1.1.4 / 2014-12-10
|
||||
==================
|
||||
|
||||
* deps: mime-types@~2.0.4
|
||||
- deps: mime-db@~1.3.0
|
||||
|
||||
1.1.3 / 2014-11-09
|
||||
==================
|
||||
|
||||
* deps: mime-types@~2.0.3
|
||||
- deps: mime-db@~1.2.0
|
||||
|
||||
1.1.2 / 2014-10-14
|
||||
==================
|
||||
|
||||
* deps: negotiator@0.4.9
|
||||
- Fix error when media type has invalid parameter
|
||||
|
||||
1.1.1 / 2014-09-28
|
||||
==================
|
||||
|
||||
* deps: mime-types@~2.0.2
|
||||
- deps: mime-db@~1.1.0
|
||||
* deps: negotiator@0.4.8
|
||||
- Fix all negotiations to be case-insensitive
|
||||
- Stable sort preferences of same quality according to client order
|
||||
|
||||
1.1.0 / 2014-09-02
|
||||
==================
|
||||
|
||||
* update `mime-types`
|
||||
|
||||
1.0.7 / 2014-07-04
|
||||
==================
|
||||
|
||||
* Fix wrong type returned from `type` when match after unknown extension
|
||||
|
||||
1.0.6 / 2014-06-24
|
||||
==================
|
||||
|
||||
* deps: negotiator@0.4.7
|
||||
|
||||
1.0.5 / 2014-06-20
|
||||
==================
|
||||
|
||||
* fix crash when unknown extension given
|
||||
|
||||
1.0.4 / 2014-06-19
|
||||
==================
|
||||
|
||||
* use `mime-types`
|
||||
|
||||
1.0.3 / 2014-06-11
|
||||
==================
|
||||
|
||||
* deps: negotiator@0.4.6
|
||||
- Order by specificity when quality is the same
|
||||
|
||||
1.0.2 / 2014-05-29
|
||||
==================
|
||||
|
||||
* Fix interpretation when header not in request
|
||||
* deps: pin negotiator@0.4.5
|
||||
|
||||
1.0.1 / 2014-01-18
|
||||
==================
|
||||
|
||||
* Identity encoding isn't always acceptable
|
||||
* deps: negotiator@~0.4.0
|
||||
|
||||
1.0.0 / 2013-12-27
|
||||
==================
|
||||
|
||||
* Genesis
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
(The MIT License)
|
||||
|
||||
Copyright (c) 2014 Jonathan Ong <me@jongleberry.com>
|
||||
Copyright (c) 2015 Douglas Christopher Wilson <doug@somethingdoug.com>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
'Software'), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
|
@ -1,140 +0,0 @@
|
|||
# accepts
|
||||
|
||||
[![NPM Version][npm-version-image]][npm-url]
|
||||
[![NPM Downloads][npm-downloads-image]][npm-url]
|
||||
[![Node.js Version][node-version-image]][node-version-url]
|
||||
[![Build Status][github-actions-ci-image]][github-actions-ci-url]
|
||||
[![Test Coverage][coveralls-image]][coveralls-url]
|
||||
|
||||
Higher level content negotiation based on [negotiator](https://www.npmjs.com/package/negotiator).
|
||||
Extracted from [koa](https://www.npmjs.com/package/koa) for general use.
|
||||
|
||||
In addition to negotiator, it allows:
|
||||
|
||||
- Allows types as an array or arguments list, ie `(['text/html', 'application/json'])`
|
||||
as well as `('text/html', 'application/json')`.
|
||||
- Allows type shorthands such as `json`.
|
||||
- Returns `false` when no types match
|
||||
- Treats non-existent headers as `*`
|
||||
|
||||
## Installation
|
||||
|
||||
This is a [Node.js](https://nodejs.org/en/) module available through the
|
||||
[npm registry](https://www.npmjs.com/). Installation is done using the
|
||||
[`npm install` command](https://docs.npmjs.com/getting-started/installing-npm-packages-locally):
|
||||
|
||||
```sh
|
||||
$ npm install accepts
|
||||
```
|
||||
|
||||
## API
|
||||
|
||||
```js
|
||||
var accepts = require('accepts')
|
||||
```
|
||||
|
||||
### accepts(req)
|
||||
|
||||
Create a new `Accepts` object for the given `req`.
|
||||
|
||||
#### .charset(charsets)
|
||||
|
||||
Return the first accepted charset. If nothing in `charsets` is accepted,
|
||||
then `false` is returned.
|
||||
|
||||
#### .charsets()
|
||||
|
||||
Return the charsets that the request accepts, in the order of the client's
|
||||
preference (most preferred first).
|
||||
|
||||
#### .encoding(encodings)
|
||||
|
||||
Return the first accepted encoding. If nothing in `encodings` is accepted,
|
||||
then `false` is returned.
|
||||
|
||||
#### .encodings()
|
||||
|
||||
Return the encodings that the request accepts, in the order of the client's
|
||||
preference (most preferred first).
|
||||
|
||||
#### .language(languages)
|
||||
|
||||
Return the first accepted language. If nothing in `languages` is accepted,
|
||||
then `false` is returned.
|
||||
|
||||
#### .languages()
|
||||
|
||||
Return the languages that the request accepts, in the order of the client's
|
||||
preference (most preferred first).
|
||||
|
||||
#### .type(types)
|
||||
|
||||
Return the first accepted type (and it is returned as the same text as what
|
||||
appears in the `types` array). If nothing in `types` is accepted, then `false`
|
||||
is returned.
|
||||
|
||||
The `types` array can contain full MIME types or file extensions. Any value
|
||||
that is not a full MIME type is passed to `require('mime-types').lookup`.
|
||||
|
||||
#### .types()
|
||||
|
||||
Return the types that the request accepts, in the order of the client's
|
||||
preference (most preferred first).
|
||||
|
||||
## Examples
|
||||
|
||||
### Simple type negotiation
|
||||
|
||||
This simple example shows how to use `accepts` to return a different typed
|
||||
respond body based on what the client wants to accept. The server lists it's
|
||||
preferences in order and will get back the best match between the client and
|
||||
server.
|
||||
|
||||
```js
|
||||
var accepts = require('accepts')
|
||||
var http = require('http')
|
||||
|
||||
function app (req, res) {
|
||||
var accept = accepts(req)
|
||||
|
||||
// the order of this list is significant; should be server preferred order
|
||||
switch (accept.type(['json', 'html'])) {
|
||||
case 'json':
|
||||
res.setHeader('Content-Type', 'application/json')
|
||||
res.write('{"hello":"world!"}')
|
||||
break
|
||||
case 'html':
|
||||
res.setHeader('Content-Type', 'text/html')
|
||||
res.write('<b>hello, world!</b>')
|
||||
break
|
||||
default:
|
||||
// the fallback is text/plain, so no need to specify it above
|
||||
res.setHeader('Content-Type', 'text/plain')
|
||||
res.write('hello, world!')
|
||||
break
|
||||
}
|
||||
|
||||
res.end()
|
||||
}
|
||||
|
||||
http.createServer(app).listen(3000)
|
||||
```
|
||||
|
||||
You can test this out with the cURL program:
|
||||
```sh
|
||||
curl -I -H'Accept: text/html' http://localhost:3000/
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
[MIT](LICENSE)
|
||||
|
||||
[coveralls-image]: https://badgen.net/coveralls/c/github/jshttp/accepts/master
|
||||
[coveralls-url]: https://coveralls.io/r/jshttp/accepts?branch=master
|
||||
[github-actions-ci-image]: https://badgen.net/github/checks/jshttp/accepts/master?label=ci
|
||||
[github-actions-ci-url]: https://github.com/jshttp/accepts/actions/workflows/ci.yml
|
||||
[node-version-image]: https://badgen.net/npm/node/accepts
|
||||
[node-version-url]: https://nodejs.org/en/download
|
||||
[npm-downloads-image]: https://badgen.net/npm/dm/accepts
|
||||
[npm-url]: https://npmjs.org/package/accepts
|
||||
[npm-version-image]: https://badgen.net/npm/v/accepts
|
||||
|
|
@ -1,238 +0,0 @@
|
|||
/*!
|
||||
* accepts
|
||||
* Copyright(c) 2014 Jonathan Ong
|
||||
* Copyright(c) 2015 Douglas Christopher Wilson
|
||||
* MIT Licensed
|
||||
*/
|
||||
|
||||
'use strict'
|
||||
|
||||
/**
|
||||
* Module dependencies.
|
||||
* @private
|
||||
*/
|
||||
|
||||
var Negotiator = require('negotiator')
|
||||
var mime = require('mime-types')
|
||||
|
||||
/**
|
||||
* Module exports.
|
||||
* @public
|
||||
*/
|
||||
|
||||
module.exports = Accepts
|
||||
|
||||
/**
|
||||
* Create a new Accepts object for the given req.
|
||||
*
|
||||
* @param {object} req
|
||||
* @public
|
||||
*/
|
||||
|
||||
function Accepts (req) {
|
||||
if (!(this instanceof Accepts)) {
|
||||
return new Accepts(req)
|
||||
}
|
||||
|
||||
this.headers = req.headers
|
||||
this.negotiator = new Negotiator(req)
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the given `type(s)` is acceptable, returning
|
||||
* the best match when true, otherwise `undefined`, in which
|
||||
* case you should respond with 406 "Not Acceptable".
|
||||
*
|
||||
* The `type` value may be a single mime type string
|
||||
* such as "application/json", the extension name
|
||||
* such as "json" or an array `["json", "html", "text/plain"]`. When a list
|
||||
* or array is given the _best_ match, if any is returned.
|
||||
*
|
||||
* Examples:
|
||||
*
|
||||
* // Accept: text/html
|
||||
* this.types('html');
|
||||
* // => "html"
|
||||
*
|
||||
* // Accept: text/*, application/json
|
||||
* this.types('html');
|
||||
* // => "html"
|
||||
* this.types('text/html');
|
||||
* // => "text/html"
|
||||
* this.types('json', 'text');
|
||||
* // => "json"
|
||||
* this.types('application/json');
|
||||
* // => "application/json"
|
||||
*
|
||||
* // Accept: text/*, application/json
|
||||
* this.types('image/png');
|
||||
* this.types('png');
|
||||
* // => undefined
|
||||
*
|
||||
* // Accept: text/*;q=.5, application/json
|
||||
* this.types(['html', 'json']);
|
||||
* this.types('html', 'json');
|
||||
* // => "json"
|
||||
*
|
||||
* @param {String|Array} types...
|
||||
* @return {String|Array|Boolean}
|
||||
* @public
|
||||
*/
|
||||
|
||||
Accepts.prototype.type =
|
||||
Accepts.prototype.types = function (types_) {
|
||||
var types = types_
|
||||
|
||||
// support flattened arguments
|
||||
if (types && !Array.isArray(types)) {
|
||||
types = new Array(arguments.length)
|
||||
for (var i = 0; i < types.length; i++) {
|
||||
types[i] = arguments[i]
|
||||
}
|
||||
}
|
||||
|
||||
// no types, return all requested types
|
||||
if (!types || types.length === 0) {
|
||||
return this.negotiator.mediaTypes()
|
||||
}
|
||||
|
||||
// no accept header, return first given type
|
||||
if (!this.headers.accept) {
|
||||
return types[0]
|
||||
}
|
||||
|
||||
var mimes = types.map(extToMime)
|
||||
var accepts = this.negotiator.mediaTypes(mimes.filter(validMime))
|
||||
var first = accepts[0]
|
||||
|
||||
return first
|
||||
? types[mimes.indexOf(first)]
|
||||
: false
|
||||
}
|
||||
|
||||
/**
|
||||
* Return accepted encodings or best fit based on `encodings`.
|
||||
*
|
||||
* Given `Accept-Encoding: gzip, deflate`
|
||||
* an array sorted by quality is returned:
|
||||
*
|
||||
* ['gzip', 'deflate']
|
||||
*
|
||||
* @param {String|Array} encodings...
|
||||
* @return {String|Array}
|
||||
* @public
|
||||
*/
|
||||
|
||||
Accepts.prototype.encoding =
|
||||
Accepts.prototype.encodings = function (encodings_) {
|
||||
var encodings = encodings_
|
||||
|
||||
// support flattened arguments
|
||||
if (encodings && !Array.isArray(encodings)) {
|
||||
encodings = new Array(arguments.length)
|
||||
for (var i = 0; i < encodings.length; i++) {
|
||||
encodings[i] = arguments[i]
|
||||
}
|
||||
}
|
||||
|
||||
// no encodings, return all requested encodings
|
||||
if (!encodings || encodings.length === 0) {
|
||||
return this.negotiator.encodings()
|
||||
}
|
||||
|
||||
return this.negotiator.encodings(encodings)[0] || false
|
||||
}
|
||||
|
||||
/**
|
||||
* Return accepted charsets or best fit based on `charsets`.
|
||||
*
|
||||
* Given `Accept-Charset: utf-8, iso-8859-1;q=0.2, utf-7;q=0.5`
|
||||
* an array sorted by quality is returned:
|
||||
*
|
||||
* ['utf-8', 'utf-7', 'iso-8859-1']
|
||||
*
|
||||
* @param {String|Array} charsets...
|
||||
* @return {String|Array}
|
||||
* @public
|
||||
*/
|
||||
|
||||
Accepts.prototype.charset =
|
||||
Accepts.prototype.charsets = function (charsets_) {
|
||||
var charsets = charsets_
|
||||
|
||||
// support flattened arguments
|
||||
if (charsets && !Array.isArray(charsets)) {
|
||||
charsets = new Array(arguments.length)
|
||||
for (var i = 0; i < charsets.length; i++) {
|
||||
charsets[i] = arguments[i]
|
||||
}
|
||||
}
|
||||
|
||||
// no charsets, return all requested charsets
|
||||
if (!charsets || charsets.length === 0) {
|
||||
return this.negotiator.charsets()
|
||||
}
|
||||
|
||||
return this.negotiator.charsets(charsets)[0] || false
|
||||
}
|
||||
|
||||
/**
|
||||
* Return accepted languages or best fit based on `langs`.
|
||||
*
|
||||
* Given `Accept-Language: en;q=0.8, es, pt`
|
||||
* an array sorted by quality is returned:
|
||||
*
|
||||
* ['es', 'pt', 'en']
|
||||
*
|
||||
* @param {String|Array} langs...
|
||||
* @return {Array|String}
|
||||
* @public
|
||||
*/
|
||||
|
||||
Accepts.prototype.lang =
|
||||
Accepts.prototype.langs =
|
||||
Accepts.prototype.language =
|
||||
Accepts.prototype.languages = function (languages_) {
|
||||
var languages = languages_
|
||||
|
||||
// support flattened arguments
|
||||
if (languages && !Array.isArray(languages)) {
|
||||
languages = new Array(arguments.length)
|
||||
for (var i = 0; i < languages.length; i++) {
|
||||
languages[i] = arguments[i]
|
||||
}
|
||||
}
|
||||
|
||||
// no languages, return all requested languages
|
||||
if (!languages || languages.length === 0) {
|
||||
return this.negotiator.languages()
|
||||
}
|
||||
|
||||
return this.negotiator.languages(languages)[0] || false
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert extnames to mime.
|
||||
*
|
||||
* @param {String} type
|
||||
* @return {String}
|
||||
* @private
|
||||
*/
|
||||
|
||||
function extToMime (type) {
|
||||
return type.indexOf('/') === -1
|
||||
? mime.lookup(type)
|
||||
: type
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if mime is valid.
|
||||
*
|
||||
* @param {String} type
|
||||
* @return {Boolean}
|
||||
* @private
|
||||
*/
|
||||
|
||||
function validMime (type) {
|
||||
return typeof type === 'string'
|
||||
}
|
||||
|
|
@ -1,47 +0,0 @@
|
|||
{
|
||||
"name": "accepts",
|
||||
"description": "Higher-level content negotiation",
|
||||
"version": "2.0.0",
|
||||
"contributors": [
|
||||
"Douglas Christopher Wilson <doug@somethingdoug.com>",
|
||||
"Jonathan Ong <me@jongleberry.com> (http://jongleberry.com)"
|
||||
],
|
||||
"license": "MIT",
|
||||
"repository": "jshttp/accepts",
|
||||
"dependencies": {
|
||||
"mime-types": "^3.0.0",
|
||||
"negotiator": "^1.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"deep-equal": "1.0.1",
|
||||
"eslint": "7.32.0",
|
||||
"eslint-config-standard": "14.1.1",
|
||||
"eslint-plugin-import": "2.25.4",
|
||||
"eslint-plugin-markdown": "2.2.1",
|
||||
"eslint-plugin-node": "11.1.0",
|
||||
"eslint-plugin-promise": "4.3.1",
|
||||
"eslint-plugin-standard": "4.1.0",
|
||||
"mocha": "9.2.0",
|
||||
"nyc": "15.1.0"
|
||||
},
|
||||
"files": [
|
||||
"LICENSE",
|
||||
"HISTORY.md",
|
||||
"index.js"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
},
|
||||
"scripts": {
|
||||
"lint": "eslint .",
|
||||
"test": "mocha --reporter spec --check-leaks --bail test/",
|
||||
"test-ci": "nyc --reporter=lcov --reporter=text npm test",
|
||||
"test-cov": "nyc --reporter=html --reporter=text npm test"
|
||||
},
|
||||
"keywords": [
|
||||
"content",
|
||||
"negotiation",
|
||||
"accept",
|
||||
"accepts"
|
||||
]
|
||||
}
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
(The MIT License)
|
||||
|
||||
Copyright (c) 2014 Jonathan Ong <me@jongleberry.com>
|
||||
Copyright (c) 2014-2015 Douglas Christopher Wilson <doug@somethingdoug.com>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
'Software'), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
|
@ -1,509 +0,0 @@
|
|||
# body-parser
|
||||
|
||||
[![NPM Version][npm-version-image]][npm-url]
|
||||
[![NPM Downloads][npm-downloads-image]][npm-url]
|
||||
[![Build Status][ci-image]][ci-url]
|
||||
[![Test Coverage][coveralls-image]][coveralls-url]
|
||||
[![OpenSSF Scorecard Badge][ossf-scorecard-badge]][ossf-scorecard-visualizer]
|
||||
|
||||
Node.js body parsing middleware.
|
||||
|
||||
Parse incoming request bodies in a middleware before your handlers, available
|
||||
under the `req.body` property.
|
||||
|
||||
**Note** As `req.body`'s shape is based on user-controlled input, all
|
||||
properties and values in this object are untrusted and should be validated
|
||||
before trusting. For example, `req.body.foo.toString()` may fail in multiple
|
||||
ways, for example the `foo` property may not be there or may not be a string,
|
||||
and `toString` may not be a function and instead a string or other user input.
|
||||
|
||||
[Learn about the anatomy of an HTTP transaction in Node.js](https://nodejs.org/en/learn/http/anatomy-of-an-http-transaction).
|
||||
|
||||
_This does not handle multipart bodies_, due to their complex and typically
|
||||
large nature. For multipart bodies, you may be interested in the following
|
||||
modules:
|
||||
|
||||
* [busboy](https://www.npmjs.com/package/busboy#readme) and
|
||||
[connect-busboy](https://www.npmjs.com/package/connect-busboy#readme)
|
||||
* [multiparty](https://www.npmjs.com/package/multiparty#readme) and
|
||||
[connect-multiparty](https://www.npmjs.com/package/connect-multiparty#readme)
|
||||
* [formidable](https://www.npmjs.com/package/formidable#readme)
|
||||
* [multer](https://www.npmjs.com/package/multer#readme)
|
||||
|
||||
This module provides the following parsers:
|
||||
|
||||
* [JSON body parser](#bodyparserjsonoptions)
|
||||
* [Raw body parser](#bodyparserrawoptions)
|
||||
* [Text body parser](#bodyparsertextoptions)
|
||||
* [URL-encoded form body parser](#bodyparserurlencodedoptions)
|
||||
|
||||
Other body parsers you might be interested in:
|
||||
|
||||
- [body](https://www.npmjs.com/package/body#readme)
|
||||
- [co-body](https://www.npmjs.com/package/co-body#readme)
|
||||
|
||||
## Installation
|
||||
|
||||
```sh
|
||||
$ npm install body-parser
|
||||
```
|
||||
|
||||
## API
|
||||
|
||||
```js
|
||||
// Import all parsers
|
||||
const bodyParser = require('body-parser')
|
||||
|
||||
// Or import individual parsers directly
|
||||
const json = require('body-parser/json')
|
||||
const urlencoded = require('body-parser/urlencoded')
|
||||
const raw = require('body-parser/raw')
|
||||
const text = require('body-parser/text')
|
||||
```
|
||||
|
||||
The `bodyParser` object exposes various factories to create middlewares. All
|
||||
middlewares will populate the `req.body` property with the parsed body when
|
||||
the `Content-Type` request header matches the `type` option.
|
||||
|
||||
The various errors returned by this module are described in the
|
||||
[errors section](#errors).
|
||||
|
||||
### bodyParser.json([options])
|
||||
|
||||
Returns middleware that only parses `json` and only looks at requests where
|
||||
the `Content-Type` header matches the `type` option. This parser accepts any
|
||||
Unicode encoding of the body and supports automatic inflation of `gzip`,
|
||||
`br` (brotli) and `deflate` encodings.
|
||||
|
||||
A new `body` object containing the parsed data is populated on the `request`
|
||||
object after the middleware (i.e. `req.body`).
|
||||
|
||||
#### Options
|
||||
|
||||
The `json` function takes an optional `options` object that may contain any of
|
||||
the following keys:
|
||||
|
||||
##### defaultCharset
|
||||
|
||||
Specify the default character set for the json content if the charset is not
|
||||
specified in the `Content-Type` header of the request. Defaults to `utf-8`.
|
||||
|
||||
##### inflate
|
||||
|
||||
When set to `true`, then deflated (compressed) bodies will be inflated; when
|
||||
`false`, deflated bodies are rejected. Defaults to `true`.
|
||||
|
||||
##### limit
|
||||
|
||||
Controls the maximum request body size. If this is a number, then the value
|
||||
specifies the number of bytes; if it is a string, the value is passed to the
|
||||
[bytes](https://www.npmjs.com/package/bytes) library for parsing. Defaults
|
||||
to `'100kb'`.
|
||||
|
||||
> It’s recommended not to configure a very high limit and to use the default value whenever possible. Allowing larger payloads increases memory usage because of the resources required for decoding and transformations, and it can also lead to longer response times as more data is processed. By ‘very high’, we mean values above the default, for example payloads of 5 MB or more can already start to introduce these risks. With the default limits, these issues do not occur.
|
||||
|
||||
##### reviver
|
||||
|
||||
The `reviver` option is passed directly to `JSON.parse` as the second
|
||||
argument. You can find more information on this argument
|
||||
[in the MDN documentation about JSON.parse](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse#the_reviver_parameter).
|
||||
|
||||
##### strict
|
||||
|
||||
When set to `true`, will only accept arrays and objects; when `false` will
|
||||
accept anything `JSON.parse` accepts. Defaults to `true`.
|
||||
|
||||
##### type
|
||||
|
||||
The `type` option is used to determine what media type the middleware will
|
||||
parse. This option can be a string, array of strings, or a function. If not a
|
||||
function, `type` option is passed directly to the
|
||||
[type-is](https://www.npmjs.com/package/type-is#readme) library and this can
|
||||
be an extension name (like `json`), a mime type (like `application/json`), or
|
||||
a mime type with a wildcard (like `*/*` or `*/json`). If a function, the `type`
|
||||
option is called as `fn(req)` and the request is parsed if it returns a truthy
|
||||
value. Defaults to `application/json`.
|
||||
|
||||
##### verify
|
||||
|
||||
The `verify` option, if supplied, is called as `verify(req, res, buf, encoding)`,
|
||||
where `buf` is a `Buffer` of the raw request body and `encoding` is the
|
||||
encoding of the request. The parsing can be aborted by throwing an error.
|
||||
|
||||
### bodyParser.raw([options])
|
||||
|
||||
Returns middleware that parses all bodies as a `Buffer` and only looks at
|
||||
requests where the `Content-Type` header matches the `type` option. This
|
||||
parser supports automatic inflation of `gzip`, `br` (brotli) and `deflate`
|
||||
encodings.
|
||||
|
||||
A new `body` object containing the parsed data is populated on the `request`
|
||||
object after the middleware (i.e. `req.body`). This will be a `Buffer` object
|
||||
of the body.
|
||||
|
||||
#### Options
|
||||
|
||||
The `raw` function takes an optional `options` object that may contain any of
|
||||
the following keys:
|
||||
|
||||
##### inflate
|
||||
|
||||
When set to `true`, then deflated (compressed) bodies will be inflated; when
|
||||
`false`, deflated bodies are rejected. Defaults to `true`.
|
||||
|
||||
##### limit
|
||||
|
||||
Controls the maximum request body size. If this is a number, then the value
|
||||
specifies the number of bytes; if it is a string, the value is passed to the
|
||||
[bytes](https://www.npmjs.com/package/bytes) library for parsing. Defaults
|
||||
to `'100kb'`.
|
||||
|
||||
> It’s recommended not to configure a very high limit and to use the default value whenever possible. Allowing larger payloads increases memory usage because of the resources required for decoding and transformations, and it can also lead to longer response times as more data is processed. By ‘very high’, we mean values above the default, for example payloads of 5 MB or more can already start to introduce these risks. With the default limits, these issues do not occur.
|
||||
|
||||
##### type
|
||||
|
||||
The `type` option is used to determine what media type the middleware will
|
||||
parse. This option can be a string, array of strings, or a function.
|
||||
If not a function, `type` option is passed directly to the
|
||||
[type-is](https://www.npmjs.com/package/type-is#readme) library and this
|
||||
can be an extension name (like `bin`), a mime type (like
|
||||
`application/octet-stream`), or a mime type with a wildcard (like `*/*` or
|
||||
`application/*`). If a function, the `type` option is called as `fn(req)`
|
||||
and the request is parsed if it returns a truthy value. Defaults to
|
||||
`application/octet-stream`.
|
||||
|
||||
##### verify
|
||||
|
||||
The `verify` option, if supplied, is called as `verify(req, res, buf, encoding)`,
|
||||
where `buf` is a `Buffer` of the raw request body and `encoding` is the
|
||||
encoding of the request. The parsing can be aborted by throwing an error.
|
||||
|
||||
### bodyParser.text([options])
|
||||
|
||||
Returns middleware that parses all bodies as a string and only looks at
|
||||
requests where the `Content-Type` header matches the `type` option. This
|
||||
parser supports automatic inflation of `gzip`, `br` (brotli) and `deflate`
|
||||
encodings.
|
||||
|
||||
A new `body` string containing the parsed data is populated on the `request`
|
||||
object after the middleware (i.e. `req.body`). This will be a string of the
|
||||
body.
|
||||
|
||||
#### Options
|
||||
|
||||
The `text` function takes an optional `options` object that may contain any of
|
||||
the following keys:
|
||||
|
||||
##### defaultCharset
|
||||
|
||||
Specify the default character set for the text content if the charset is not
|
||||
specified in the `Content-Type` header of the request. Defaults to `utf-8`.
|
||||
|
||||
##### inflate
|
||||
|
||||
When set to `true`, then deflated (compressed) bodies will be inflated; when
|
||||
`false`, deflated bodies are rejected. Defaults to `true`.
|
||||
|
||||
##### limit
|
||||
|
||||
Controls the maximum request body size. If this is a number, then the value
|
||||
specifies the number of bytes; if it is a string, the value is passed to the
|
||||
[bytes](https://www.npmjs.com/package/bytes) library for parsing. Defaults
|
||||
to `'100kb'`.
|
||||
|
||||
> It’s recommended not to configure a very high limit and to use the default value whenever possible. Allowing larger payloads increases memory usage because of the resources required for decoding and transformations, and it can also lead to longer response times as more data is processed. By ‘very high’, we mean values above the default, for example payloads of 5 MB or more can already start to introduce these risks. With the default limits, these issues do not occur.
|
||||
|
||||
##### type
|
||||
|
||||
The `type` option is used to determine what media type the middleware will
|
||||
parse. This option can be a string, array of strings, or a function. If not
|
||||
a function, `type` option is passed directly to the
|
||||
[type-is](https://www.npmjs.com/package/type-is#readme) library and this can
|
||||
be an extension name (like `txt`), a mime type (like `text/plain`), or a mime
|
||||
type with a wildcard (like `*/*` or `text/*`). If a function, the `type`
|
||||
option is called as `fn(req)` and the request is parsed if it returns a
|
||||
truthy value. Defaults to `text/plain`.
|
||||
|
||||
##### verify
|
||||
|
||||
The `verify` option, if supplied, is called as `verify(req, res, buf, encoding)`,
|
||||
where `buf` is a `Buffer` of the raw request body and `encoding` is the
|
||||
encoding of the request. The parsing can be aborted by throwing an error.
|
||||
|
||||
### bodyParser.urlencoded([options])
|
||||
|
||||
Returns middleware that only parses `urlencoded` bodies and only looks at
|
||||
requests where the `Content-Type` header matches the `type` option. This
|
||||
parser accepts only UTF-8 and ISO-8859-1 encodings of the body and supports
|
||||
automatic inflation of `gzip`, `br` (brotli) and `deflate` encodings.
|
||||
|
||||
A new `body` object containing the parsed data is populated on the `request`
|
||||
object after the middleware (i.e. `req.body`). This object will contain
|
||||
key-value pairs, where the value can be a string or array (when `extended` is
|
||||
`false`), or any type (when `extended` is `true`).
|
||||
|
||||
#### Options
|
||||
|
||||
The `urlencoded` function takes an optional `options` object that may contain
|
||||
any of the following keys:
|
||||
|
||||
##### extended
|
||||
|
||||
The "extended" syntax allows for rich objects and arrays to be encoded into the
|
||||
URL-encoded format, allowing for a JSON-like experience with URL-encoded. For
|
||||
more information, please [see the qs
|
||||
library](https://www.npmjs.com/package/qs#readme).
|
||||
|
||||
Defaults to `false`.
|
||||
|
||||
##### inflate
|
||||
|
||||
When set to `true`, then deflated (compressed) bodies will be inflated; when
|
||||
`false`, deflated bodies are rejected. Defaults to `true`.
|
||||
|
||||
##### limit
|
||||
|
||||
Controls the maximum request body size. If this is a number, then the value
|
||||
specifies the number of bytes; if it is a string, the value is passed to the
|
||||
[bytes](https://www.npmjs.com/package/bytes) library for parsing. Defaults
|
||||
to `'100kb'`.
|
||||
|
||||
> It’s recommended not to configure a very high limit and to use the default value whenever possible. Allowing larger payloads increases memory usage because of the resources required for decoding and transformations, and it can also lead to longer response times as more data is processed. By ‘very high’, we mean values above the default, for example payloads of 5 MB or more can already start to introduce these risks. With the default limits, these issues do not occur.
|
||||
|
||||
##### parameterLimit
|
||||
|
||||
The `parameterLimit` option controls the maximum number of parameters that
|
||||
are allowed in the URL-encoded data. If a request contains more parameters
|
||||
than this value, a 413 will be returned to the client. Defaults to `1000`.
|
||||
|
||||
##### type
|
||||
|
||||
The `type` option is used to determine what media type the middleware will
|
||||
parse. This option can be a string, array of strings, or a function. If not
|
||||
a function, `type` option is passed directly to the
|
||||
[type-is](https://www.npmjs.com/package/type-is#readme) library and this can
|
||||
be an extension name (like `urlencoded`), a mime type (like
|
||||
`application/x-www-form-urlencoded`), or a mime type with a wildcard (like
|
||||
`*/x-www-form-urlencoded`). If a function, the `type` option is called as
|
||||
`fn(req)` and the request is parsed if it returns a truthy value. Defaults
|
||||
to `application/x-www-form-urlencoded`.
|
||||
|
||||
##### verify
|
||||
|
||||
The `verify` option, if supplied, is called as `verify(req, res, buf, encoding)`,
|
||||
where `buf` is a `Buffer` of the raw request body and `encoding` is the
|
||||
encoding of the request. The parsing can be aborted by throwing an error.
|
||||
|
||||
##### defaultCharset
|
||||
|
||||
The default charset to parse as, if not specified in content-type. Must be
|
||||
either `utf-8` or `iso-8859-1`. Defaults to `utf-8`.
|
||||
|
||||
##### charsetSentinel
|
||||
|
||||
Whether to let the value of the `utf8` parameter take precedence as the charset
|
||||
selector. It requires the form to contain a parameter named `utf8` with a value
|
||||
of `✓`. Defaults to `false`.
|
||||
|
||||
##### interpretNumericEntities
|
||||
|
||||
Whether to decode numeric entities such as `☺` when parsing an iso-8859-1
|
||||
form. Defaults to `false`.
|
||||
|
||||
|
||||
##### depth
|
||||
|
||||
The `depth` option is used to configure the maximum depth of the `qs` library when `extended` is `true`. This allows you to limit the amount of keys that are parsed and can be useful to prevent certain types of abuse. Defaults to `32`. It is recommended to keep this value as low as possible.
|
||||
|
||||
## Errors
|
||||
|
||||
The middlewares provided by this module create errors using the
|
||||
[`http-errors` module](https://www.npmjs.com/package/http-errors). The errors
|
||||
will typically have a `status`/`statusCode` property that contains the suggested
|
||||
HTTP response code, an `expose` property to determine if the `message` property
|
||||
should be displayed to the client, a `type` property to determine the type of
|
||||
error without matching against the `message`, and a `body` property containing
|
||||
the read body, if available.
|
||||
|
||||
The following are the common errors created, though any error can come through
|
||||
for various reasons.
|
||||
|
||||
### content encoding unsupported
|
||||
|
||||
This error will occur when the request had a `Content-Encoding` header that
|
||||
contained an encoding but the "inflation" option was set to `false`. The
|
||||
`status` property is set to `415`, the `type` property is set to
|
||||
`'encoding.unsupported'`, and the `charset` property will be set to the
|
||||
encoding that is unsupported.
|
||||
|
||||
### entity parse failed
|
||||
|
||||
This error will occur when the request contained an entity that could not be
|
||||
parsed by the middleware. The `status` property is set to `400`, the `type`
|
||||
property is set to `'entity.parse.failed'`, and the `body` property is set to
|
||||
the entity value that failed parsing.
|
||||
|
||||
### entity verify failed
|
||||
|
||||
This error will occur when the request contained an entity that could not be
|
||||
failed verification by the defined `verify` option. The `status` property is
|
||||
set to `403`, the `type` property is set to `'entity.verify.failed'`, and the
|
||||
`body` property is set to the entity value that failed verification.
|
||||
|
||||
### request aborted
|
||||
|
||||
This error will occur when the request is aborted by the client before reading
|
||||
the body has finished. The `received` property will be set to the number of
|
||||
bytes received before the request was aborted and the `expected` property is
|
||||
set to the number of expected bytes. The `status` property is set to `400`
|
||||
and `type` property is set to `'request.aborted'`.
|
||||
|
||||
### request entity too large
|
||||
|
||||
This error will occur when the request body's size is larger than the "limit"
|
||||
option. The `limit` property will be set to the byte limit and the `length`
|
||||
property will be set to the request body's length. The `status` property is
|
||||
set to `413` and the `type` property is set to `'entity.too.large'`.
|
||||
|
||||
### request size did not match content length
|
||||
|
||||
This error will occur when the request's length did not match the length from
|
||||
the `Content-Length` header. This typically occurs when the request is malformed,
|
||||
typically when the `Content-Length` header was calculated based on characters
|
||||
instead of bytes. The `status` property is set to `400` and the `type` property
|
||||
is set to `'request.size.invalid'`.
|
||||
|
||||
### stream encoding should not be set
|
||||
|
||||
This error will occur when something called the `req.setEncoding` method prior
|
||||
to this middleware. This module operates directly on bytes only and you cannot
|
||||
call `req.setEncoding` when using this module. The `status` property is set to
|
||||
`500` and the `type` property is set to `'stream.encoding.set'`.
|
||||
|
||||
### stream is not readable
|
||||
|
||||
This error will occur when the request is no longer readable when this middleware
|
||||
attempts to read it. This typically means something other than a middleware from
|
||||
this module read the request body already and the middleware was also configured to
|
||||
read the same request. The `status` property is set to `500` and the `type`
|
||||
property is set to `'stream.not.readable'`.
|
||||
|
||||
### too many parameters
|
||||
|
||||
This error will occur when the content of the request exceeds the configured
|
||||
`parameterLimit` for the `urlencoded` parser. The `status` property is set to
|
||||
`413` and the `type` property is set to `'parameters.too.many'`.
|
||||
|
||||
### unsupported charset "BOGUS"
|
||||
|
||||
This error will occur when the request had a charset parameter in the
|
||||
`Content-Type` header, but the `iconv-lite` module does not support it OR the
|
||||
parser does not support it. The charset is contained in the message as well
|
||||
as in the `charset` property. The `status` property is set to `415`, the
|
||||
`type` property is set to `'charset.unsupported'`, and the `charset` property
|
||||
is set to the charset that is unsupported.
|
||||
|
||||
### unsupported content encoding "bogus"
|
||||
|
||||
This error will occur when the request had a `Content-Encoding` header that
|
||||
contained an unsupported encoding. The encoding is contained in the message
|
||||
as well as in the `encoding` property. The `status` property is set to `415`,
|
||||
the `type` property is set to `'encoding.unsupported'`, and the `encoding`
|
||||
property is set to the encoding that is unsupported.
|
||||
|
||||
### The input exceeded the depth
|
||||
|
||||
This error occurs when using `bodyParser.urlencoded` with the `extended` property set to `true` and the input exceeds the configured `depth` option. The `status` property is set to `400`. It is recommended to review the `depth` option and evaluate if it requires a higher value. When the `depth` option is set to `32` (default value), the error will not be thrown.
|
||||
|
||||
## Examples
|
||||
|
||||
### Express/Connect top-level generic
|
||||
|
||||
This example demonstrates adding a generic JSON and URL-encoded parser as a
|
||||
top-level middleware, which will parse the bodies of all incoming requests.
|
||||
This is the simplest setup.
|
||||
|
||||
```js
|
||||
const express = require('express')
|
||||
const bodyParser = require('body-parser')
|
||||
|
||||
const app = express()
|
||||
|
||||
// parse application/x-www-form-urlencoded
|
||||
app.use(bodyParser.urlencoded())
|
||||
|
||||
// parse application/json
|
||||
app.use(bodyParser.json())
|
||||
|
||||
app.use(function (req, res) {
|
||||
res.setHeader('Content-Type', 'text/plain')
|
||||
res.write('you posted:\n')
|
||||
res.end(String(JSON.stringify(req.body, null, 2)))
|
||||
})
|
||||
```
|
||||
|
||||
### Express route-specific
|
||||
|
||||
This example demonstrates adding body parsers specifically to the routes that
|
||||
need them. In general, this is the most recommended way to use body-parser with
|
||||
Express.
|
||||
|
||||
```js
|
||||
const express = require('express')
|
||||
const bodyParser = require('body-parser')
|
||||
|
||||
const app = express()
|
||||
|
||||
// create application/json parser
|
||||
const jsonParser = bodyParser.json()
|
||||
|
||||
// create application/x-www-form-urlencoded parser
|
||||
const urlencodedParser = bodyParser.urlencoded()
|
||||
|
||||
// POST /login gets urlencoded bodies
|
||||
app.post('/login', urlencodedParser, function (req, res) {
|
||||
if (!req.body || !req.body.username) res.sendStatus(400)
|
||||
res.send('welcome, ' + req.body.username)
|
||||
})
|
||||
|
||||
// POST /api/users gets JSON bodies
|
||||
app.post('/api/users', jsonParser, function (req, res) {
|
||||
if (!req.body) res.sendStatus(400)
|
||||
// create user in req.body
|
||||
})
|
||||
```
|
||||
|
||||
### Change accepted type for parsers
|
||||
|
||||
All the parsers accept a `type` option which allows you to change the
|
||||
`Content-Type` that the middleware will parse.
|
||||
|
||||
```js
|
||||
const express = require('express')
|
||||
const bodyParser = require('body-parser')
|
||||
|
||||
const app = express()
|
||||
|
||||
// parse various different custom JSON types as JSON
|
||||
app.use(bodyParser.json({ type: 'application/*+json' }))
|
||||
|
||||
// parse some custom thing into a Buffer
|
||||
app.use(bodyParser.raw({ type: 'application/vnd.custom-type' }))
|
||||
|
||||
// parse an HTML body into a string
|
||||
app.use(bodyParser.text({ type: 'text/html' }))
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
[MIT](LICENSE)
|
||||
|
||||
[ci-image]: https://img.shields.io/github/actions/workflow/status/expressjs/body-parser/ci.yml?branch=master&label=ci
|
||||
[ci-url]: https://github.com/expressjs/body-parser/actions/workflows/ci.yml
|
||||
[coveralls-image]: https://img.shields.io/coverallsCoverage/github/expressjs/body-parser?branch=master
|
||||
[coveralls-url]: https://coveralls.io/r/expressjs/body-parser?branch=master
|
||||
[npm-downloads-image]: https://img.shields.io/npm/dm/body-parser
|
||||
[npm-url]: https://npmjs.com/package/body-parser
|
||||
[npm-version-image]: https://img.shields.io/npm/v/body-parser
|
||||
[ossf-scorecard-badge]: https://api.scorecard.dev/projects/github.com/expressjs/body-parser/badge
|
||||
[ossf-scorecard-visualizer]: https://ossf.github.io/scorecard-visualizer/#/projects/github.com/expressjs/body-parser
|
||||
|
|
@ -1,55 +0,0 @@
|
|||
/*!
|
||||
* body-parser
|
||||
* Copyright(c) 2014-2015 Douglas Christopher Wilson
|
||||
* MIT Licensed
|
||||
*/
|
||||
|
||||
'use strict'
|
||||
|
||||
/**
|
||||
* @typedef {Object} Parsers
|
||||
* @property {Function} json JSON parser
|
||||
* @property {Function} raw Raw parser
|
||||
* @property {Function} text Text parser
|
||||
* @property {Function} urlencoded URL-encoded parser
|
||||
*/
|
||||
|
||||
/**
|
||||
* Module exports.
|
||||
* @type {Function & Parsers}
|
||||
*/
|
||||
exports = module.exports = bodyParser
|
||||
|
||||
/**
|
||||
* JSON parser.
|
||||
* @public
|
||||
*/
|
||||
exports.json = require('./lib/types/json')
|
||||
|
||||
/**
|
||||
* Raw parser.
|
||||
* @public
|
||||
*/
|
||||
exports.raw = require('./lib/types/raw')
|
||||
|
||||
/**
|
||||
* Text parser.
|
||||
* @public
|
||||
*/
|
||||
exports.text = require('./lib/types/text')
|
||||
|
||||
/**
|
||||
* URL-encoded parser.
|
||||
* @public
|
||||
*/
|
||||
exports.urlencoded = require('./lib/types/urlencoded')
|
||||
|
||||
/**
|
||||
* Create a middleware to parse json and urlencoded bodies.
|
||||
*
|
||||
* @deprecated
|
||||
* @public
|
||||
*/
|
||||
function bodyParser () {
|
||||
throw new Error('The bodyParser() generic has been split into individual middleware to use instead.')
|
||||
}
|
||||
|
|
@ -1,247 +0,0 @@
|
|||
/*!
|
||||
* body-parser
|
||||
* Copyright(c) 2014-2015 Douglas Christopher Wilson
|
||||
* MIT Licensed
|
||||
*/
|
||||
|
||||
'use strict'
|
||||
|
||||
/**
|
||||
* Module dependencies.
|
||||
* @private
|
||||
*/
|
||||
|
||||
const createError = require('http-errors')
|
||||
const getBody = require('raw-body')
|
||||
const iconv = require('iconv-lite')
|
||||
const onFinished = require('on-finished')
|
||||
const zlib = require('node:zlib')
|
||||
const hasBody = require('type-is').hasBody
|
||||
const { getCharset } = require('./utils')
|
||||
|
||||
/**
|
||||
* Module exports.
|
||||
*/
|
||||
|
||||
module.exports = read
|
||||
|
||||
/**
|
||||
* Read a request into a buffer and parse.
|
||||
*
|
||||
* @param {Object} req
|
||||
* @param {Object} res
|
||||
* @param {Function} next
|
||||
* @param {Function} parse
|
||||
* @param {Function} debug
|
||||
* @param {Object} options
|
||||
* @private
|
||||
*/
|
||||
function read (req, res, next, parse, debug, options) {
|
||||
if (onFinished.isFinished(req)) {
|
||||
debug('body already parsed')
|
||||
next()
|
||||
return
|
||||
}
|
||||
|
||||
if (!('body' in req)) {
|
||||
req.body = undefined
|
||||
}
|
||||
|
||||
// skip requests without bodies
|
||||
if (!hasBody(req)) {
|
||||
debug('skip empty body')
|
||||
next()
|
||||
return
|
||||
}
|
||||
|
||||
debug('content-type %j', req.headers['content-type'])
|
||||
|
||||
// determine if request should be parsed
|
||||
if (!options.shouldParse(req)) {
|
||||
debug('skip parsing')
|
||||
next()
|
||||
return
|
||||
}
|
||||
|
||||
let encoding = null
|
||||
if (options?.skipCharset !== true) {
|
||||
encoding = getCharset(req) || options.defaultCharset
|
||||
|
||||
// validate charset
|
||||
if (!!options?.isValidCharset && !options.isValidCharset(encoding)) {
|
||||
debug('invalid charset')
|
||||
next(createError(415, 'unsupported charset "' + encoding.toUpperCase() + '"', {
|
||||
charset: encoding,
|
||||
type: 'charset.unsupported'
|
||||
}))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
let length
|
||||
const opts = options
|
||||
let stream
|
||||
|
||||
// read options
|
||||
const verify = opts.verify
|
||||
|
||||
try {
|
||||
// get the content stream
|
||||
stream = contentstream(req, debug, opts.inflate)
|
||||
length = stream.length
|
||||
stream.length = undefined
|
||||
} catch (err) {
|
||||
return next(err)
|
||||
}
|
||||
|
||||
// set raw-body options
|
||||
opts.length = length
|
||||
opts.encoding = verify
|
||||
? null
|
||||
: encoding
|
||||
|
||||
// assert charset is supported
|
||||
if (opts.encoding === null && encoding !== null && !iconv.encodingExists(encoding)) {
|
||||
return next(createError(415, 'unsupported charset "' + encoding.toUpperCase() + '"', {
|
||||
charset: encoding.toLowerCase(),
|
||||
type: 'charset.unsupported'
|
||||
}))
|
||||
}
|
||||
|
||||
// read body
|
||||
debug('read body')
|
||||
getBody(stream, opts, function (error, body) {
|
||||
if (error) {
|
||||
let _error
|
||||
|
||||
if (error.type === 'encoding.unsupported') {
|
||||
// echo back charset
|
||||
_error = createError(415, 'unsupported charset "' + encoding.toUpperCase() + '"', {
|
||||
charset: encoding.toLowerCase(),
|
||||
type: 'charset.unsupported'
|
||||
})
|
||||
} else {
|
||||
// set status code on error
|
||||
_error = createError(400, error)
|
||||
}
|
||||
|
||||
// unpipe from stream and destroy
|
||||
if (stream !== req) {
|
||||
req.unpipe()
|
||||
stream.destroy()
|
||||
}
|
||||
|
||||
// read off entire request
|
||||
dump(req, function onfinished () {
|
||||
next(createError(400, _error))
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// verify
|
||||
if (verify) {
|
||||
try {
|
||||
debug('verify body')
|
||||
verify(req, res, body, encoding)
|
||||
} catch (err) {
|
||||
next(createError(403, err, {
|
||||
body: body,
|
||||
type: err.type || 'entity.verify.failed'
|
||||
}))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// parse
|
||||
let str = body
|
||||
try {
|
||||
debug('parse body')
|
||||
str = typeof body !== 'string' && encoding !== null
|
||||
? iconv.decode(body, encoding)
|
||||
: body
|
||||
req.body = parse(str, encoding)
|
||||
} catch (err) {
|
||||
next(createError(400, err, {
|
||||
body: str,
|
||||
type: err.type || 'entity.parse.failed'
|
||||
}))
|
||||
return
|
||||
}
|
||||
|
||||
next()
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the content stream of the request.
|
||||
*
|
||||
* @param {Object} req
|
||||
* @param {Function} debug
|
||||
* @param {boolean} inflate
|
||||
* @returns {Object}
|
||||
* @private
|
||||
*/
|
||||
function contentstream (req, debug, inflate) {
|
||||
const encoding = (req.headers['content-encoding'] || 'identity').toLowerCase()
|
||||
const length = req.headers['content-length']
|
||||
|
||||
debug('content-encoding "%s"', encoding)
|
||||
|
||||
if (inflate === false && encoding !== 'identity') {
|
||||
throw createError(415, 'content encoding unsupported', {
|
||||
encoding: encoding,
|
||||
type: 'encoding.unsupported'
|
||||
})
|
||||
}
|
||||
|
||||
if (encoding === 'identity') {
|
||||
req.length = length
|
||||
return req
|
||||
}
|
||||
|
||||
const stream = createDecompressionStream(encoding, debug)
|
||||
req.pipe(stream)
|
||||
return stream
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a decompression stream for the given encoding.
|
||||
* @param {string} encoding
|
||||
* @param {Function} debug
|
||||
* @returns {Object}
|
||||
* @private
|
||||
*/
|
||||
function createDecompressionStream (encoding, debug) {
|
||||
switch (encoding) {
|
||||
case 'deflate':
|
||||
debug('inflate body')
|
||||
return zlib.createInflate()
|
||||
case 'gzip':
|
||||
debug('gunzip body')
|
||||
return zlib.createGunzip()
|
||||
case 'br':
|
||||
debug('brotli decompress body')
|
||||
return zlib.createBrotliDecompress()
|
||||
default:
|
||||
throw createError(415, 'unsupported content encoding "' + encoding + '"', {
|
||||
encoding: encoding,
|
||||
type: 'encoding.unsupported'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dump the contents of a request.
|
||||
*
|
||||
* @param {Object} req
|
||||
* @param {Function} callback
|
||||
* @private
|
||||
*/
|
||||
function dump (req, callback) {
|
||||
if (onFinished.isFinished(req)) {
|
||||
callback(null)
|
||||
} else {
|
||||
onFinished(req, callback)
|
||||
req.resume()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,186 +0,0 @@
|
|||
/*!
|
||||
* body-parser
|
||||
* Copyright(c) 2014 Jonathan Ong
|
||||
* Copyright(c) 2014-2015 Douglas Christopher Wilson
|
||||
* MIT Licensed
|
||||
*/
|
||||
|
||||
'use strict'
|
||||
|
||||
/**
|
||||
* Module dependencies.
|
||||
* @private
|
||||
*/
|
||||
|
||||
const debug = require('debug')('body-parser:json')
|
||||
const read = require('../read')
|
||||
const { normalizeOptions } = require('../utils')
|
||||
|
||||
/**
|
||||
* Module exports.
|
||||
*/
|
||||
|
||||
module.exports = json
|
||||
|
||||
/**
|
||||
* RegExp to match the first non-space in a string.
|
||||
*
|
||||
* Allowed whitespace is defined in RFC 7159:
|
||||
*
|
||||
* ws = *(
|
||||
* %x20 / ; Space
|
||||
* %x09 / ; Horizontal tab
|
||||
* %x0A / ; Line feed or New line
|
||||
* %x0D ) ; Carriage return
|
||||
*/
|
||||
const FIRST_CHAR_REGEXP = /^[\x20\x09\x0a\x0d]*([^\x20\x09\x0a\x0d])/ // eslint-disable-line no-control-regex
|
||||
|
||||
const JSON_SYNTAX_CHAR = '#'
|
||||
const JSON_SYNTAX_REGEXP = /#+/g
|
||||
|
||||
/**
|
||||
* Create a middleware to parse JSON bodies.
|
||||
*
|
||||
* @param {Object} [options]
|
||||
* @returns {Function}
|
||||
* @public
|
||||
*/
|
||||
function json (options) {
|
||||
const normalizedOptions = normalizeOptions(options, 'application/json')
|
||||
|
||||
const parse = createJsonParser(options)
|
||||
|
||||
const readOptions = {
|
||||
...normalizedOptions,
|
||||
// assert charset per RFC 7159 sec 8.1
|
||||
isValidCharset: (charset) => charset.slice(0, 4) === 'utf-'
|
||||
}
|
||||
|
||||
return function jsonParser (req, res, next) {
|
||||
read(req, res, next, parse, debug, readOptions)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a JSON parse function
|
||||
*
|
||||
* @param {object} [options]
|
||||
* @return {function}
|
||||
* @private
|
||||
*/
|
||||
function createJsonParser (options) {
|
||||
const reviver = options?.reviver
|
||||
const strict = options?.strict !== false
|
||||
|
||||
if (strict) {
|
||||
return function parse (body) {
|
||||
if (body.length === 0) {
|
||||
// special-case empty json body, as it's a common client-side mistake
|
||||
// TODO: maybe make this configurable or part of "strict" option
|
||||
return {}
|
||||
}
|
||||
|
||||
const first = firstchar(body)
|
||||
if (first !== '{' && first !== '[') {
|
||||
debug('strict violation')
|
||||
throw createStrictSyntaxError(body, first)
|
||||
}
|
||||
|
||||
try {
|
||||
debug('parse json')
|
||||
return JSON.parse(body, reviver)
|
||||
} catch (e) {
|
||||
throw normalizeJsonSyntaxError(e, {
|
||||
message: e.message,
|
||||
stack: e.stack
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return function parse (body) {
|
||||
if (body.length === 0) {
|
||||
// special-case empty json body, as it's a common client-side mistake
|
||||
// TODO: maybe make this configurable or part of "strict" option
|
||||
return {}
|
||||
}
|
||||
|
||||
try {
|
||||
debug('parse json')
|
||||
return JSON.parse(body, reviver)
|
||||
} catch (e) {
|
||||
throw normalizeJsonSyntaxError(e, {
|
||||
message: e.message,
|
||||
stack: e.stack
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create strict violation syntax error matching native error.
|
||||
*
|
||||
* @param {string} str
|
||||
* @param {string} char
|
||||
* @returns {Error}
|
||||
* @private
|
||||
*/
|
||||
function createStrictSyntaxError (str, char) {
|
||||
const index = str.indexOf(char)
|
||||
let partial = ''
|
||||
|
||||
if (index !== -1) {
|
||||
partial = str.substring(0, index) + JSON_SYNTAX_CHAR.repeat(str.length - index)
|
||||
}
|
||||
|
||||
try {
|
||||
JSON.parse(partial); /* istanbul ignore next */ throw new SyntaxError('strict violation')
|
||||
} catch (e) {
|
||||
return normalizeJsonSyntaxError(e, {
|
||||
message: e.message.replace(JSON_SYNTAX_REGEXP, function (placeholder) {
|
||||
return str.substring(index, index + placeholder.length)
|
||||
}),
|
||||
stack: e.stack
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the first non-whitespace character in a string.
|
||||
*
|
||||
* @param {string} str
|
||||
* @returns {string|undefined}
|
||||
* @private
|
||||
*/
|
||||
function firstchar (str) {
|
||||
const match = FIRST_CHAR_REGEXP.exec(str)
|
||||
|
||||
return match
|
||||
? match[1]
|
||||
: undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize a SyntaxError for JSON.parse.
|
||||
*
|
||||
* @param {SyntaxError} error
|
||||
* @param {Object} obj
|
||||
* @returns {SyntaxError}
|
||||
* @private
|
||||
*/
|
||||
function normalizeJsonSyntaxError (error, obj) {
|
||||
const keys = Object.getOwnPropertyNames(error)
|
||||
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
const key = keys[i]
|
||||
if (key !== 'stack' && key !== 'message') {
|
||||
delete error[key]
|
||||
}
|
||||
}
|
||||
|
||||
// replace stack before message for Node.js 0.10 and below
|
||||
error.stack = obj.stack.replace(error.message, obj.message)
|
||||
error.message = obj.message
|
||||
|
||||
return error
|
||||
}
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
/*!
|
||||
* body-parser
|
||||
* Copyright(c) 2014-2015 Douglas Christopher Wilson
|
||||
* MIT Licensed
|
||||
*/
|
||||
|
||||
'use strict'
|
||||
|
||||
/**
|
||||
* Module dependencies.
|
||||
*/
|
||||
|
||||
const debug = require('debug')('body-parser:raw')
|
||||
const read = require('../read')
|
||||
const { normalizeOptions, passthrough } = require('../utils')
|
||||
|
||||
/**
|
||||
* Module exports.
|
||||
*/
|
||||
|
||||
module.exports = raw
|
||||
|
||||
/**
|
||||
* Create a middleware to parse raw bodies.
|
||||
*
|
||||
* @param {Object} [options]
|
||||
* @returns {Function}
|
||||
* @public
|
||||
*/
|
||||
function raw (options) {
|
||||
const normalizedOptions = normalizeOptions(options, 'application/octet-stream')
|
||||
|
||||
const readOptions = {
|
||||
...normalizedOptions,
|
||||
// Skip charset validation and parse the body as is
|
||||
skipCharset: true
|
||||
}
|
||||
|
||||
return function rawParser (req, res, next) {
|
||||
read(req, res, next, passthrough, debug, readOptions)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
/*!
|
||||
* body-parser
|
||||
* Copyright(c) 2014-2015 Douglas Christopher Wilson
|
||||
* MIT Licensed
|
||||
*/
|
||||
|
||||
'use strict'
|
||||
|
||||
/**
|
||||
* Module dependencies.
|
||||
*/
|
||||
|
||||
const debug = require('debug')('body-parser:text')
|
||||
const read = require('../read')
|
||||
const { normalizeOptions, passthrough } = require('../utils')
|
||||
|
||||
/**
|
||||
* Module exports.
|
||||
*/
|
||||
|
||||
module.exports = text
|
||||
|
||||
/**
|
||||
* Create a middleware to parse text bodies.
|
||||
*
|
||||
* @param {Object} [options]
|
||||
* @returns {Function}
|
||||
* @public
|
||||
*/
|
||||
function text (options) {
|
||||
const normalizedOptions = normalizeOptions(options, 'text/plain')
|
||||
|
||||
return function textParser (req, res, next) {
|
||||
read(req, res, next, passthrough, debug, normalizedOptions)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,138 +0,0 @@
|
|||
/*!
|
||||
* body-parser
|
||||
* Copyright(c) 2014 Jonathan Ong
|
||||
* Copyright(c) 2014-2015 Douglas Christopher Wilson
|
||||
* MIT Licensed
|
||||
*/
|
||||
|
||||
'use strict'
|
||||
|
||||
/**
|
||||
* Module dependencies.
|
||||
* @private
|
||||
*/
|
||||
|
||||
const createError = require('http-errors')
|
||||
const debug = require('debug')('body-parser:urlencoded')
|
||||
const read = require('../read')
|
||||
const qs = require('qs')
|
||||
const { normalizeOptions } = require('../utils')
|
||||
|
||||
/**
|
||||
* Module exports.
|
||||
*/
|
||||
|
||||
module.exports = urlencoded
|
||||
|
||||
/**
|
||||
* Create a middleware to parse urlencoded bodies.
|
||||
*
|
||||
* @param {Object} [options]
|
||||
* @returns {Function}
|
||||
* @public
|
||||
*/
|
||||
function urlencoded (options) {
|
||||
const normalizedOptions = normalizeOptions(options, 'application/x-www-form-urlencoded')
|
||||
|
||||
if (normalizedOptions.defaultCharset !== 'utf-8' && normalizedOptions.defaultCharset !== 'iso-8859-1') {
|
||||
throw new TypeError('option defaultCharset must be either utf-8 or iso-8859-1')
|
||||
}
|
||||
|
||||
// create the appropriate query parser
|
||||
const parse = createQueryParser(options)
|
||||
|
||||
const readOptions = {
|
||||
...normalizedOptions,
|
||||
// assert charset
|
||||
isValidCharset: (charset) => charset === 'utf-8' || charset === 'iso-8859-1'
|
||||
}
|
||||
|
||||
return function urlencodedParser (req, res, next) {
|
||||
read(req, res, next, parse, debug, readOptions)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the extended query parser.
|
||||
*
|
||||
* @param {Object} options
|
||||
* @returns {Function}
|
||||
* @private
|
||||
*/
|
||||
function createQueryParser (options) {
|
||||
const extended = Boolean(options?.extended)
|
||||
let parameterLimit = options?.parameterLimit !== undefined
|
||||
? options?.parameterLimit
|
||||
: 1000
|
||||
const charsetSentinel = options?.charsetSentinel
|
||||
const interpretNumericEntities = options?.interpretNumericEntities
|
||||
const depth = extended ? (options?.depth !== undefined ? options?.depth : 32) : 0
|
||||
|
||||
if (isNaN(parameterLimit) || parameterLimit < 1) {
|
||||
throw new TypeError('option parameterLimit must be a positive number')
|
||||
}
|
||||
|
||||
if (isNaN(depth) || depth < 0) {
|
||||
throw new TypeError('option depth must be a zero or a positive number')
|
||||
}
|
||||
|
||||
if (isFinite(parameterLimit)) {
|
||||
parameterLimit = parameterLimit | 0
|
||||
}
|
||||
|
||||
return function parse (body, encoding) {
|
||||
if (!body.length) return {}
|
||||
|
||||
const paramCount = parameterCount(body, parameterLimit)
|
||||
|
||||
if (paramCount === undefined) {
|
||||
debug('too many parameters')
|
||||
throw createError(413, 'too many parameters', {
|
||||
type: 'parameters.too.many'
|
||||
})
|
||||
}
|
||||
|
||||
const arrayLimit = extended ? Math.max(100, paramCount) : paramCount
|
||||
|
||||
debug('parse ' + (extended ? 'extended ' : '') + 'urlencoding')
|
||||
try {
|
||||
return qs.parse(body, {
|
||||
allowPrototypes: true,
|
||||
arrayLimit: arrayLimit,
|
||||
depth: depth,
|
||||
charsetSentinel: charsetSentinel,
|
||||
interpretNumericEntities: interpretNumericEntities,
|
||||
charset: encoding,
|
||||
parameterLimit: parameterLimit,
|
||||
strictDepth: true
|
||||
})
|
||||
} catch (err) {
|
||||
if (err instanceof RangeError) {
|
||||
throw createError(400, 'The input exceeded the depth', {
|
||||
type: 'querystring.parse.rangeError'
|
||||
})
|
||||
} else {
|
||||
throw err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Count the number of parameters, stopping once limit reached
|
||||
*
|
||||
* @param {string} body
|
||||
* @param {number} limit
|
||||
* @returns {number|undefined} Returns undefined if limit exceeded
|
||||
* @private
|
||||
*/
|
||||
function parameterCount (body, limit) {
|
||||
let count = 0
|
||||
let index = -1
|
||||
do {
|
||||
count++
|
||||
if (count > limit) return undefined // Early exit if limit exceeded
|
||||
index = body.indexOf('&', index + 1)
|
||||
} while (index !== -1)
|
||||
return count
|
||||
}
|
||||
|
|
@ -1,100 +0,0 @@
|
|||
'use strict'
|
||||
|
||||
/**
|
||||
* Module dependencies.
|
||||
*/
|
||||
|
||||
const bytes = require('bytes')
|
||||
const contentType = require('content-type')
|
||||
const typeis = require('type-is')
|
||||
|
||||
/**
|
||||
* Module exports.
|
||||
*/
|
||||
module.exports = {
|
||||
getCharset,
|
||||
normalizeOptions,
|
||||
passthrough
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the charset of a request.
|
||||
*
|
||||
* @param {Object} req
|
||||
* @returns {string | undefined}
|
||||
* @private
|
||||
*/
|
||||
function getCharset (req) {
|
||||
const header = req.headers['content-type']
|
||||
if (!header) return undefined
|
||||
return contentType.parse(header).parameters.charset?.toLowerCase()
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the simple type checker.
|
||||
*
|
||||
* @param {string | string[]} type
|
||||
* @returns {Function}
|
||||
* @private
|
||||
*/
|
||||
function typeChecker (type) {
|
||||
return function checkType (req) {
|
||||
return Boolean(typeis(req, type))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes the common options for all parsers.
|
||||
*
|
||||
* @param {Object} options options to normalize
|
||||
* @param {string | string[] | Function} defaultType default content type(s) or a function to determine it
|
||||
* @returns {Object}
|
||||
* @private
|
||||
*/
|
||||
function normalizeOptions (options, defaultType) {
|
||||
if (!defaultType) {
|
||||
// Parsers must define a default content type
|
||||
throw new TypeError('defaultType must be provided')
|
||||
}
|
||||
|
||||
const inflate = options?.inflate !== false
|
||||
const limit = typeof options?.limit === 'undefined' || options?.limit === null
|
||||
? 102400 // 100kb default
|
||||
: bytes.parse(options.limit)
|
||||
const type = options?.type || defaultType
|
||||
const verify = options?.verify || false
|
||||
const defaultCharset = options?.defaultCharset || 'utf-8'
|
||||
|
||||
if (limit === null) {
|
||||
throw new TypeError(`option limit "${String(options.limit)}" is invalid`)
|
||||
}
|
||||
|
||||
if (verify !== false && typeof verify !== 'function') {
|
||||
throw new TypeError('option verify must be function')
|
||||
}
|
||||
|
||||
// create the appropriate type checking function
|
||||
const shouldParse = typeof type !== 'function'
|
||||
? typeChecker(type)
|
||||
: type
|
||||
|
||||
return {
|
||||
inflate,
|
||||
limit,
|
||||
verify,
|
||||
defaultCharset,
|
||||
shouldParse
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Passthrough function that returns input unchanged.
|
||||
* Used by parsers that don't need to transform the data.
|
||||
*
|
||||
* @param {*} value
|
||||
* @returns {*}
|
||||
* @private
|
||||
*/
|
||||
function passthrough (value) {
|
||||
return value
|
||||
}
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
(The MIT License)
|
||||
|
||||
Copyright (c) 2015 Douglas Christopher Wilson
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
'Software'), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
|
@ -1,69 +0,0 @@
|
|||
# content-type
|
||||
|
||||
[![NPM version][npm-image]][npm-url]
|
||||
[![NPM downloads][downloads-image]][downloads-url]
|
||||
[![Build status][build-image]][build-url]
|
||||
[![Build coverage][coverage-image]][coverage-url]
|
||||
[![License][license-image]][license-url]
|
||||
|
||||
Create and parse HTTP `Content-Type` header.
|
||||
|
||||
## Installation
|
||||
|
||||
```sh
|
||||
npm install content-type
|
||||
```
|
||||
|
||||
## API
|
||||
|
||||
```js
|
||||
const contentType = require("content-type");
|
||||
```
|
||||
|
||||
### contentType.parse(string, options?)
|
||||
|
||||
```js
|
||||
const obj = contentType.parse("image/svg+xml; charset=utf-8");
|
||||
```
|
||||
|
||||
Parse a `Content-Type` header. This will return an object with the following properties (examples are shown for the string `'image/svg+xml; charset=utf-8'`):
|
||||
|
||||
- `type`: The media type. Example: `'image/svg+xml'`.
|
||||
- `parameters`: An object of the parameters in the media type (parameter name is always lower case). Example: `{charset: 'utf-8'}`.
|
||||
|
||||
The parser is lenient and does not error. You should validate `type` and `parameters` before trusting them.
|
||||
|
||||
#### Options
|
||||
|
||||
- `parameters` (default: `true`): Set to `false` to skip parameters.
|
||||
|
||||
### contentType.format(obj)
|
||||
|
||||
```js
|
||||
const str = contentType.format({
|
||||
type: "image/svg+xml",
|
||||
parameters: { charset: "utf-8" },
|
||||
});
|
||||
```
|
||||
|
||||
Format an object into a `Content-Type` header. This will return a string of the content type for the given object with the following properties (examples are shown that produce the string `'image/svg+xml; charset=utf-8'`):
|
||||
|
||||
- `type`: The media type. Example: `'image/svg+xml'`.
|
||||
- `parameters`: An optional object of the parameters in the media type. Example: `{charset: 'utf-8'}`.
|
||||
|
||||
Throws a `TypeError` if the object contains an invalid type or parameter names.
|
||||
|
||||
## License
|
||||
|
||||
[MIT](LICENSE)
|
||||
|
||||
[npm-image]: https://img.shields.io/npm/v/content-type
|
||||
[npm-url]: https://npmjs.org/package/content-type
|
||||
[downloads-image]: https://img.shields.io/npm/dm/content-type
|
||||
[downloads-url]: https://npmjs.org/package/content-type
|
||||
[build-image]: https://img.shields.io/github/actions/workflow/status/jshttp/content-type/ci.yml?branch=master
|
||||
[build-url]: https://github.com/jshttp/content-type/actions/workflows/ci.yml?query=branch%3Amaster
|
||||
[coverage-image]: https://img.shields.io/codecov/c/gh/jshttp/content-type
|
||||
[coverage-url]: https://codecov.io/gh/jshttp/content-type
|
||||
[license-image]: http://img.shields.io/npm/l/content-type.svg?style=flat
|
||||
[license-url]: LICENSE
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
/*!
|
||||
* content-type
|
||||
* Copyright(c) 2015 Douglas Christopher Wilson
|
||||
* MIT Licensed
|
||||
*/
|
||||
/**
|
||||
* The content type object contains a type string and optional parameters.
|
||||
*/
|
||||
export interface ContentType {
|
||||
type: string;
|
||||
parameters: Record<string, string>;
|
||||
}
|
||||
/**
|
||||
* Format an object into a `Content-Type` header.
|
||||
*/
|
||||
export declare function format(obj: Partial<ContentType>): string;
|
||||
/**
|
||||
* Options for parsing a `Content-Type` header.
|
||||
*/
|
||||
export interface ParseOptions {
|
||||
parameters?: boolean;
|
||||
}
|
||||
/**
|
||||
* Parse a `Content-Type` header.
|
||||
*/
|
||||
export declare function parse(header: string, options?: ParseOptions): ContentType;
|
||||
|
|
@ -1,170 +0,0 @@
|
|||
"use strict";
|
||||
/*!
|
||||
* content-type
|
||||
* Copyright(c) 2015 Douglas Christopher Wilson
|
||||
* MIT Licensed
|
||||
*/
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.format = format;
|
||||
exports.parse = parse;
|
||||
const TEXT_REGEXP = /^[\u0009\u0020-\u007e\u0080-\u00ff]*$/;
|
||||
const TOKEN_REGEXP = /^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/;
|
||||
/**
|
||||
* RegExp to match chars that must be quoted-pair in RFC 9110 sec 5.6.4
|
||||
*/
|
||||
const QUOTE_REGEXP = /[\\"]/g;
|
||||
/**
|
||||
* RegExp to match type in RFC 9110 sec 8.3.1
|
||||
*
|
||||
* media-type = type "/" subtype
|
||||
* type = token
|
||||
* subtype = token
|
||||
*/
|
||||
const TYPE_REGEXP = /^[!#$%&'*+.^_`|~0-9A-Za-z-]+\/[!#$%&'*+.^_`|~0-9A-Za-z-]+$/;
|
||||
/**
|
||||
* Null object perf optimization. Faster than `Object.create(null)` and `{ __proto__: null }`.
|
||||
*/
|
||||
const NullObject = /* @__PURE__ */ (() => {
|
||||
const C = function () { };
|
||||
C.prototype = Object.create(null);
|
||||
return C;
|
||||
})();
|
||||
/**
|
||||
* Format an object into a `Content-Type` header.
|
||||
*/
|
||||
function format(obj) {
|
||||
const { type, parameters } = obj;
|
||||
if (!type || !TYPE_REGEXP.test(type)) {
|
||||
throw new TypeError(`Invalid type: ${type}`);
|
||||
}
|
||||
let result = type;
|
||||
if (parameters) {
|
||||
for (const param of Object.keys(parameters)) {
|
||||
if (!TOKEN_REGEXP.test(param)) {
|
||||
throw new TypeError(`Invalid parameter name: ${param}`);
|
||||
}
|
||||
result += `; ${param}=${qstring(parameters[param])}`;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
/**
|
||||
* Parse a `Content-Type` header.
|
||||
*/
|
||||
function parse(header, options) {
|
||||
const len = header.length;
|
||||
let index = skipOWS(header, 0, len);
|
||||
const valueStart = index;
|
||||
index = skipValue(header, index, len);
|
||||
const valueEnd = trailingOWS(header, valueStart, index);
|
||||
const type = header.slice(valueStart, valueEnd).toLowerCase();
|
||||
const parameters = options?.parameters === false
|
||||
? new NullObject()
|
||||
: parseParameters(header, index, len);
|
||||
return { type, parameters };
|
||||
}
|
||||
const SP = 32; // " "
|
||||
const HTAB = 9; // "\t"
|
||||
const SEMI = 59; // ";"
|
||||
const EQ = 61; // "="
|
||||
const DQUOTE = 34; // '"'
|
||||
const BSLASH = 92; // "\\"
|
||||
/**
|
||||
* Parses the parameters of a `Content-Type` header starting at the given index.
|
||||
*/
|
||||
function parseParameters(header, index, len) {
|
||||
const parameters = new NullObject();
|
||||
parameter: while (index < len) {
|
||||
index = skipOWS(header, index + 1 /* Skip over ; */, len);
|
||||
const keyStart = index;
|
||||
while (index < len) {
|
||||
const code = header.charCodeAt(index);
|
||||
if (code === SEMI)
|
||||
continue parameter;
|
||||
if (code === EQ) {
|
||||
const keyEnd = trailingOWS(header, keyStart, index);
|
||||
const key = header.slice(keyStart, keyEnd).toLowerCase();
|
||||
index = skipOWS(header, index + 1, len);
|
||||
if (index < len && header.charCodeAt(index) === DQUOTE) {
|
||||
index++;
|
||||
let value = "";
|
||||
while (index < len) {
|
||||
const code = header.charCodeAt(index++);
|
||||
if (code === DQUOTE) {
|
||||
index = skipValue(header, index, len);
|
||||
if (parameters[key] === undefined)
|
||||
parameters[key] = value;
|
||||
break;
|
||||
}
|
||||
if (code === BSLASH && index < len) {
|
||||
value += header[index++];
|
||||
continue;
|
||||
}
|
||||
value += String.fromCharCode(code);
|
||||
}
|
||||
continue parameter;
|
||||
}
|
||||
const valueStart = index;
|
||||
index = skipValue(header, index, len);
|
||||
if (parameters[key] === undefined) {
|
||||
const valueEnd = trailingOWS(header, valueStart, index);
|
||||
parameters[key] = header.slice(valueStart, valueEnd);
|
||||
}
|
||||
continue parameter;
|
||||
}
|
||||
index++;
|
||||
}
|
||||
}
|
||||
return parameters;
|
||||
}
|
||||
/**
|
||||
* Skip over characters until a semicolon.
|
||||
*/
|
||||
function skipValue(str, index, len) {
|
||||
while (index < len) {
|
||||
const char = str.charCodeAt(index);
|
||||
if (char === SEMI)
|
||||
break;
|
||||
index++;
|
||||
}
|
||||
return index;
|
||||
}
|
||||
/**
|
||||
* Skip optional whitespace (OWS) in an HTTP header value.
|
||||
*
|
||||
* OWS is defined in RFC 9110 sec 5.6.3 as SP (" ") or HTAB ("\t").
|
||||
*/
|
||||
function skipOWS(header, index, len) {
|
||||
while (index < len) {
|
||||
const char = header.charCodeAt(index);
|
||||
if (char !== SP && char !== HTAB)
|
||||
break;
|
||||
index++;
|
||||
}
|
||||
return index;
|
||||
}
|
||||
/**
|
||||
* Trim optional whitespace (OWS) from the end of a substring.
|
||||
*
|
||||
* OWS is defined in RFC 9110 sec 5.6.3 as SP (" ") or HTAB ("\t").
|
||||
*/
|
||||
function trailingOWS(header, start, end) {
|
||||
while (end > start) {
|
||||
const char = header.charCodeAt(end - 1);
|
||||
if (char !== SP && char !== HTAB)
|
||||
break;
|
||||
end--;
|
||||
}
|
||||
return end;
|
||||
}
|
||||
/**
|
||||
* Serialize a parameter value.
|
||||
*/
|
||||
function qstring(str) {
|
||||
if (TOKEN_REGEXP.test(str))
|
||||
return str;
|
||||
if (TEXT_REGEXP.test(str))
|
||||
return `"${str.replace(QUOTE_REGEXP, "\\$&")}"`;
|
||||
throw new TypeError(`Invalid parameter value: ${str}`);
|
||||
}
|
||||
//# sourceMappingURL=index.js.map
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -1,52 +0,0 @@
|
|||
{
|
||||
"name": "content-type",
|
||||
"version": "2.0.0",
|
||||
"description": "Create and parse HTTP Content-Type header",
|
||||
"keywords": [
|
||||
"content-type",
|
||||
"http",
|
||||
"req",
|
||||
"res",
|
||||
"rfc7231",
|
||||
"rfc9110"
|
||||
],
|
||||
"repository": "jshttp/content-type",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
},
|
||||
"license": "MIT",
|
||||
"author": "Douglas Christopher Wilson <doug@somethingdoug.com>",
|
||||
"type": "commonjs",
|
||||
"exports": "./dist/index.js",
|
||||
"main": "./dist/index.js",
|
||||
"typings": "./dist/index.d.ts",
|
||||
"files": [
|
||||
"dist/"
|
||||
],
|
||||
"scripts": {
|
||||
"bench": "vitest bench",
|
||||
"build": "ts-scripts build",
|
||||
"format": "ts-scripts format",
|
||||
"prepare": "ts-scripts install && npm run build",
|
||||
"specs": "ts-scripts specs",
|
||||
"test": "ts-scripts test"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@borderless/ts-scripts": "^0.15.0",
|
||||
"@vitest/coverage-v8": "^3.0.5",
|
||||
"typescript": "^5.7.3",
|
||||
"vitest": "^3.2.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"ts-scripts": {
|
||||
"dist": [
|
||||
"dist"
|
||||
],
|
||||
"project": [
|
||||
"tsconfig.build.json"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -1,65 +0,0 @@
|
|||
{
|
||||
"name": "body-parser",
|
||||
"description": "Node.js body parsing middleware",
|
||||
"version": "2.3.0",
|
||||
"contributors": [
|
||||
"Douglas Christopher Wilson <doug@somethingdoug.com>",
|
||||
"Jonathan Ong <me@jongleberry.com> (http://jongleberry.com)"
|
||||
],
|
||||
"license": "MIT",
|
||||
"repository": "expressjs/body-parser",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
},
|
||||
"type": "commonjs",
|
||||
"exports": {
|
||||
".": "./index.js",
|
||||
"./package.json": "./package.json",
|
||||
"./json": "./lib/types/json.js",
|
||||
"./raw": "./lib/types/raw.js",
|
||||
"./text": "./lib/types/text.js",
|
||||
"./urlencoded": "./lib/types/urlencoded.js",
|
||||
"./lib/*": "./lib/*.js",
|
||||
"./lib/*.js": "./lib/*.js",
|
||||
"./lib/types/*": "./lib/types/*.js",
|
||||
"./lib/types/*.js": "./lib/types/*.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"bytes": "^3.1.2",
|
||||
"content-type": "^2.0.0",
|
||||
"debug": "^4.4.3",
|
||||
"http-errors": "^2.0.1",
|
||||
"iconv-lite": "^0.7.2",
|
||||
"on-finished": "^2.4.1",
|
||||
"qs": "^6.15.2",
|
||||
"raw-body": "^3.0.2",
|
||||
"type-is": "^2.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint": "^8.57.1",
|
||||
"eslint-config-standard": "^14.1.1",
|
||||
"eslint-plugin-import": "^2.32.0",
|
||||
"eslint-plugin-markdown": "^3.0.1",
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"eslint-plugin-promise": "^6.6.0",
|
||||
"eslint-plugin-standard": "^4.1.0",
|
||||
"mocha": "^11.7.6",
|
||||
"nyc": "^17.1.0",
|
||||
"supertest": "^7.2.2"
|
||||
},
|
||||
"files": [
|
||||
"lib/",
|
||||
"LICENSE",
|
||||
"index.js"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"scripts": {
|
||||
"lint": "eslint .",
|
||||
"test": "mocha --reporter spec --check-leaks test/",
|
||||
"test-ci": "nyc --reporter=lcovonly --reporter=text npm test",
|
||||
"test-cov": "nyc --reporter=html --reporter=text npm test"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,97 +0,0 @@
|
|||
3.1.2 / 2022-01-27
|
||||
==================
|
||||
|
||||
* Fix return value for un-parsable strings
|
||||
|
||||
3.1.1 / 2021-11-15
|
||||
==================
|
||||
|
||||
* Fix "thousandsSeparator" incorrecting formatting fractional part
|
||||
|
||||
3.1.0 / 2019-01-22
|
||||
==================
|
||||
|
||||
* Add petabyte (`pb`) support
|
||||
|
||||
3.0.0 / 2017-08-31
|
||||
==================
|
||||
|
||||
* Change "kB" to "KB" in format output
|
||||
* Remove support for Node.js 0.6
|
||||
* Remove support for ComponentJS
|
||||
|
||||
2.5.0 / 2017-03-24
|
||||
==================
|
||||
|
||||
* Add option "unit"
|
||||
|
||||
2.4.0 / 2016-06-01
|
||||
==================
|
||||
|
||||
* Add option "unitSeparator"
|
||||
|
||||
2.3.0 / 2016-02-15
|
||||
==================
|
||||
|
||||
* Drop partial bytes on all parsed units
|
||||
* Fix non-finite numbers to `.format` to return `null`
|
||||
* Fix parsing byte string that looks like hex
|
||||
* perf: hoist regular expressions
|
||||
|
||||
2.2.0 / 2015-11-13
|
||||
==================
|
||||
|
||||
* add option "decimalPlaces"
|
||||
* add option "fixedDecimals"
|
||||
|
||||
2.1.0 / 2015-05-21
|
||||
==================
|
||||
|
||||
* add `.format` export
|
||||
* add `.parse` export
|
||||
|
||||
2.0.2 / 2015-05-20
|
||||
==================
|
||||
|
||||
* remove map recreation
|
||||
* remove unnecessary object construction
|
||||
|
||||
2.0.1 / 2015-05-07
|
||||
==================
|
||||
|
||||
* fix browserify require
|
||||
* remove node.extend dependency
|
||||
|
||||
2.0.0 / 2015-04-12
|
||||
==================
|
||||
|
||||
* add option "case"
|
||||
* add option "thousandsSeparator"
|
||||
* return "null" on invalid parse input
|
||||
* support proper round-trip: bytes(bytes(num)) === num
|
||||
* units no longer case sensitive when parsing
|
||||
|
||||
1.0.0 / 2014-05-05
|
||||
==================
|
||||
|
||||
* add negative support. fixes #6
|
||||
|
||||
0.3.0 / 2014-03-19
|
||||
==================
|
||||
|
||||
* added terabyte support
|
||||
|
||||
0.2.1 / 2013-04-01
|
||||
==================
|
||||
|
||||
* add .component
|
||||
|
||||
0.2.0 / 2012-10-28
|
||||
==================
|
||||
|
||||
* bytes(200).should.eql('200b')
|
||||
|
||||
0.1.0 / 2012-07-04
|
||||
==================
|
||||
|
||||
* add bytes to string conversion [yields]
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
(The MIT License)
|
||||
|
||||
Copyright (c) 2012-2014 TJ Holowaychuk <tj@vision-media.ca>
|
||||
Copyright (c) 2015 Jed Watson <jed.watson@me.com>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
'Software'), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
|
@ -1,152 +0,0 @@
|
|||
# Bytes utility
|
||||
|
||||
[![NPM Version][npm-image]][npm-url]
|
||||
[![NPM Downloads][downloads-image]][downloads-url]
|
||||
[![Build Status][ci-image]][ci-url]
|
||||
[![Test Coverage][coveralls-image]][coveralls-url]
|
||||
|
||||
Utility to parse a string bytes (ex: `1TB`) to bytes (`1099511627776`) and vice-versa.
|
||||
|
||||
## Installation
|
||||
|
||||
This is a [Node.js](https://nodejs.org/en/) module available through the
|
||||
[npm registry](https://www.npmjs.com/). Installation is done using the
|
||||
[`npm install` command](https://docs.npmjs.com/getting-started/installing-npm-packages-locally):
|
||||
|
||||
```bash
|
||||
$ npm install bytes
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```js
|
||||
var bytes = require('bytes');
|
||||
```
|
||||
|
||||
#### bytes(number|string value, [options]): number|string|null
|
||||
|
||||
Default export function. Delegates to either `bytes.format` or `bytes.parse` based on the type of `value`.
|
||||
|
||||
**Arguments**
|
||||
|
||||
| Name | Type | Description |
|
||||
|---------|----------|--------------------|
|
||||
| value | `number`|`string` | Number value to format or string value to parse |
|
||||
| options | `Object` | Conversion options for `format` |
|
||||
|
||||
**Returns**
|
||||
|
||||
| Name | Type | Description |
|
||||
|---------|------------------|-------------------------------------------------|
|
||||
| results | `string`|`number`|`null` | Return null upon error. Numeric value in bytes, or string value otherwise. |
|
||||
|
||||
**Example**
|
||||
|
||||
```js
|
||||
bytes(1024);
|
||||
// output: '1KB'
|
||||
|
||||
bytes('1KB');
|
||||
// output: 1024
|
||||
```
|
||||
|
||||
#### bytes.format(number value, [options]): string|null
|
||||
|
||||
Format the given value in bytes into a string. If the value is negative, it is kept as such. If it is a float, it is
|
||||
rounded.
|
||||
|
||||
**Arguments**
|
||||
|
||||
| Name | Type | Description |
|
||||
|---------|----------|--------------------|
|
||||
| value | `number` | Value in bytes |
|
||||
| options | `Object` | Conversion options |
|
||||
|
||||
**Options**
|
||||
|
||||
| Property | Type | Description |
|
||||
|-------------------|--------|-----------------------------------------------------------------------------------------|
|
||||
| decimalPlaces | `number`|`null` | Maximum number of decimal places to include in output. Default value to `2`. |
|
||||
| fixedDecimals | `boolean`|`null` | Whether to always display the maximum number of decimal places. Default value to `false` |
|
||||
| thousandsSeparator | `string`|`null` | Example of values: `' '`, `','` and `'.'`... Default value to `''`. |
|
||||
| unit | `string`|`null` | The unit in which the result will be returned (B/KB/MB/GB/TB). Default value to `''` (which means auto detect). |
|
||||
| unitSeparator | `string`|`null` | Separator to use between number and unit. Default value to `''`. |
|
||||
|
||||
**Returns**
|
||||
|
||||
| Name | Type | Description |
|
||||
|---------|------------------|-------------------------------------------------|
|
||||
| results | `string`|`null` | Return null upon error. String value otherwise. |
|
||||
|
||||
**Example**
|
||||
|
||||
```js
|
||||
bytes.format(1024);
|
||||
// output: '1KB'
|
||||
|
||||
bytes.format(1000);
|
||||
// output: '1000B'
|
||||
|
||||
bytes.format(1000, {thousandsSeparator: ' '});
|
||||
// output: '1 000B'
|
||||
|
||||
bytes.format(1024 * 1.7, {decimalPlaces: 0});
|
||||
// output: '2KB'
|
||||
|
||||
bytes.format(1024, {unitSeparator: ' '});
|
||||
// output: '1 KB'
|
||||
```
|
||||
|
||||
#### bytes.parse(string|number value): number|null
|
||||
|
||||
Parse the string value into an integer in bytes. If no unit is given, or `value`
|
||||
is a number, it is assumed the value is in bytes.
|
||||
|
||||
Supported units and abbreviations are as follows and are case-insensitive:
|
||||
|
||||
* `b` for bytes
|
||||
* `kb` for kilobytes
|
||||
* `mb` for megabytes
|
||||
* `gb` for gigabytes
|
||||
* `tb` for terabytes
|
||||
* `pb` for petabytes
|
||||
|
||||
The units are in powers of two, not ten. This means 1kb = 1024b according to this parser.
|
||||
|
||||
**Arguments**
|
||||
|
||||
| Name | Type | Description |
|
||||
|---------------|--------|--------------------|
|
||||
| value | `string`|`number` | String to parse, or number in bytes. |
|
||||
|
||||
**Returns**
|
||||
|
||||
| Name | Type | Description |
|
||||
|---------|-------------|-------------------------|
|
||||
| results | `number`|`null` | Return null upon error. Value in bytes otherwise. |
|
||||
|
||||
**Example**
|
||||
|
||||
```js
|
||||
bytes.parse('1KB');
|
||||
// output: 1024
|
||||
|
||||
bytes.parse('1024');
|
||||
// output: 1024
|
||||
|
||||
bytes.parse(1024);
|
||||
// output: 1024
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
[MIT](LICENSE)
|
||||
|
||||
[ci-image]: https://badgen.net/github/checks/visionmedia/bytes.js/master?label=ci
|
||||
[ci-url]: https://github.com/visionmedia/bytes.js/actions?query=workflow%3Aci
|
||||
[coveralls-image]: https://badgen.net/coveralls/c/github/visionmedia/bytes.js/master
|
||||
[coveralls-url]: https://coveralls.io/r/visionmedia/bytes.js?branch=master
|
||||
[downloads-image]: https://badgen.net/npm/dm/bytes
|
||||
[downloads-url]: https://npmjs.org/package/bytes
|
||||
[npm-image]: https://badgen.net/npm/v/bytes
|
||||
[npm-url]: https://npmjs.org/package/bytes
|
||||
|
|
@ -1,170 +0,0 @@
|
|||
/*!
|
||||
* bytes
|
||||
* Copyright(c) 2012-2014 TJ Holowaychuk
|
||||
* Copyright(c) 2015 Jed Watson
|
||||
* MIT Licensed
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Module exports.
|
||||
* @public
|
||||
*/
|
||||
|
||||
module.exports = bytes;
|
||||
module.exports.format = format;
|
||||
module.exports.parse = parse;
|
||||
|
||||
/**
|
||||
* Module variables.
|
||||
* @private
|
||||
*/
|
||||
|
||||
var formatThousandsRegExp = /\B(?=(\d{3})+(?!\d))/g;
|
||||
|
||||
var formatDecimalsRegExp = /(?:\.0*|(\.[^0]+)0+)$/;
|
||||
|
||||
var map = {
|
||||
b: 1,
|
||||
kb: 1 << 10,
|
||||
mb: 1 << 20,
|
||||
gb: 1 << 30,
|
||||
tb: Math.pow(1024, 4),
|
||||
pb: Math.pow(1024, 5),
|
||||
};
|
||||
|
||||
var parseRegExp = /^((-|\+)?(\d+(?:\.\d+)?)) *(kb|mb|gb|tb|pb)$/i;
|
||||
|
||||
/**
|
||||
* Convert the given value in bytes into a string or parse to string to an integer in bytes.
|
||||
*
|
||||
* @param {string|number} value
|
||||
* @param {{
|
||||
* case: [string],
|
||||
* decimalPlaces: [number]
|
||||
* fixedDecimals: [boolean]
|
||||
* thousandsSeparator: [string]
|
||||
* unitSeparator: [string]
|
||||
* }} [options] bytes options.
|
||||
*
|
||||
* @returns {string|number|null}
|
||||
*/
|
||||
|
||||
function bytes(value, options) {
|
||||
if (typeof value === 'string') {
|
||||
return parse(value);
|
||||
}
|
||||
|
||||
if (typeof value === 'number') {
|
||||
return format(value, options);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format the given value in bytes into a string.
|
||||
*
|
||||
* If the value is negative, it is kept as such. If it is a float,
|
||||
* it is rounded.
|
||||
*
|
||||
* @param {number} value
|
||||
* @param {object} [options]
|
||||
* @param {number} [options.decimalPlaces=2]
|
||||
* @param {number} [options.fixedDecimals=false]
|
||||
* @param {string} [options.thousandsSeparator=]
|
||||
* @param {string} [options.unit=]
|
||||
* @param {string} [options.unitSeparator=]
|
||||
*
|
||||
* @returns {string|null}
|
||||
* @public
|
||||
*/
|
||||
|
||||
function format(value, options) {
|
||||
if (!Number.isFinite(value)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var mag = Math.abs(value);
|
||||
var thousandsSeparator = (options && options.thousandsSeparator) || '';
|
||||
var unitSeparator = (options && options.unitSeparator) || '';
|
||||
var decimalPlaces = (options && options.decimalPlaces !== undefined) ? options.decimalPlaces : 2;
|
||||
var fixedDecimals = Boolean(options && options.fixedDecimals);
|
||||
var unit = (options && options.unit) || '';
|
||||
|
||||
if (!unit || !map[unit.toLowerCase()]) {
|
||||
if (mag >= map.pb) {
|
||||
unit = 'PB';
|
||||
} else if (mag >= map.tb) {
|
||||
unit = 'TB';
|
||||
} else if (mag >= map.gb) {
|
||||
unit = 'GB';
|
||||
} else if (mag >= map.mb) {
|
||||
unit = 'MB';
|
||||
} else if (mag >= map.kb) {
|
||||
unit = 'KB';
|
||||
} else {
|
||||
unit = 'B';
|
||||
}
|
||||
}
|
||||
|
||||
var val = value / map[unit.toLowerCase()];
|
||||
var str = val.toFixed(decimalPlaces);
|
||||
|
||||
if (!fixedDecimals) {
|
||||
str = str.replace(formatDecimalsRegExp, '$1');
|
||||
}
|
||||
|
||||
if (thousandsSeparator) {
|
||||
str = str.split('.').map(function (s, i) {
|
||||
return i === 0
|
||||
? s.replace(formatThousandsRegExp, thousandsSeparator)
|
||||
: s
|
||||
}).join('.');
|
||||
}
|
||||
|
||||
return str + unitSeparator + unit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the string value into an integer in bytes.
|
||||
*
|
||||
* If no unit is given, it is assumed the value is in bytes.
|
||||
*
|
||||
* @param {number|string} val
|
||||
*
|
||||
* @returns {number|null}
|
||||
* @public
|
||||
*/
|
||||
|
||||
function parse(val) {
|
||||
if (typeof val === 'number' && !isNaN(val)) {
|
||||
return val;
|
||||
}
|
||||
|
||||
if (typeof val !== 'string') {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Test if the string passed is valid
|
||||
var results = parseRegExp.exec(val);
|
||||
var floatValue;
|
||||
var unit = 'b';
|
||||
|
||||
if (!results) {
|
||||
// Nothing could be extracted from the given string
|
||||
floatValue = parseInt(val, 10);
|
||||
unit = 'b'
|
||||
} else {
|
||||
// Retrieve the value and the unit
|
||||
floatValue = parseFloat(results[1]);
|
||||
unit = results[4].toLowerCase();
|
||||
}
|
||||
|
||||
if (isNaN(floatValue)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Math.floor(map[unit] * floatValue);
|
||||
}
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
{
|
||||
"name": "bytes",
|
||||
"description": "Utility to parse a string bytes to bytes and vice-versa",
|
||||
"version": "3.1.2",
|
||||
"author": "TJ Holowaychuk <tj@vision-media.ca> (http://tjholowaychuk.com)",
|
||||
"contributors": [
|
||||
"Jed Watson <jed.watson@me.com>",
|
||||
"Théo FIDRY <theo.fidry@gmail.com>"
|
||||
],
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
"byte",
|
||||
"bytes",
|
||||
"utility",
|
||||
"parse",
|
||||
"parser",
|
||||
"convert",
|
||||
"converter"
|
||||
],
|
||||
"repository": "visionmedia/bytes.js",
|
||||
"devDependencies": {
|
||||
"eslint": "7.32.0",
|
||||
"eslint-plugin-markdown": "2.2.1",
|
||||
"mocha": "9.2.0",
|
||||
"nyc": "15.1.0"
|
||||
},
|
||||
"files": [
|
||||
"History.md",
|
||||
"LICENSE",
|
||||
"Readme.md",
|
||||
"index.js"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
},
|
||||
"scripts": {
|
||||
"lint": "eslint .",
|
||||
"test": "mocha --check-leaks --reporter spec",
|
||||
"test-ci": "nyc --reporter=lcov --reporter=text npm test",
|
||||
"test-cov": "nyc --reporter=html --reporter=text npm test"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
{
|
||||
"root": true,
|
||||
|
||||
"extends": "@ljharb",
|
||||
|
||||
"rules": {
|
||||
"func-name-matching": 0,
|
||||
"id-length": 0,
|
||||
"new-cap": [2, {
|
||||
"capIsNewExceptions": [
|
||||
"GetIntrinsic",
|
||||
],
|
||||
}],
|
||||
"no-extra-parens": 0,
|
||||
"no-magic-numbers": 0,
|
||||
},
|
||||
}
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
# These are supported funding model platforms
|
||||
|
||||
github: [ljharb]
|
||||
patreon: # Replace with a single Patreon username
|
||||
open_collective: # Replace with a single Open Collective username
|
||||
ko_fi: # Replace with a single Ko-fi username
|
||||
tidelift: npm/call-bind-apply-helpers
|
||||
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||
liberapay: # Replace with a single Liberapay username
|
||||
issuehunt: # Replace with a single IssueHunt username
|
||||
otechie: # Replace with a single Otechie username
|
||||
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
{
|
||||
"all": true,
|
||||
"check-coverage": false,
|
||||
"reporter": ["text-summary", "text", "html", "json"],
|
||||
"exclude": [
|
||||
"coverage",
|
||||
"test"
|
||||
]
|
||||
}
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [v1.0.2](https://github.com/ljharb/call-bind-apply-helpers/compare/v1.0.1...v1.0.2) - 2025-02-12
|
||||
|
||||
### Commits
|
||||
|
||||
- [types] improve inferred types [`e6f9586`](https://github.com/ljharb/call-bind-apply-helpers/commit/e6f95860a3c72879cb861a858cdfb8138fbedec1)
|
||||
- [Dev Deps] update `@arethetypeswrong/cli`, `@ljharb/tsconfig`, `@types/tape`, `es-value-fixtures`, `for-each`, `has-strict-mode`, `object-inspect` [`e43d540`](https://github.com/ljharb/call-bind-apply-helpers/commit/e43d5409f97543bfbb11f345d47d8ce4e066d8c1)
|
||||
|
||||
## [v1.0.1](https://github.com/ljharb/call-bind-apply-helpers/compare/v1.0.0...v1.0.1) - 2024-12-08
|
||||
|
||||
### Commits
|
||||
|
||||
- [types] `reflectApply`: fix types [`4efc396`](https://github.com/ljharb/call-bind-apply-helpers/commit/4efc3965351a4f02cc55e836fa391d3d11ef2ef8)
|
||||
- [Fix] `reflectApply`: oops, Reflect is not a function [`83cc739`](https://github.com/ljharb/call-bind-apply-helpers/commit/83cc7395de6b79b7730bdf092f1436f0b1263c75)
|
||||
- [Dev Deps] update `@arethetypeswrong/cli` [`80bd5d3`](https://github.com/ljharb/call-bind-apply-helpers/commit/80bd5d3ae58b4f6b6995ce439dd5a1bcb178a940)
|
||||
|
||||
## v1.0.0 - 2024-12-05
|
||||
|
||||
### Commits
|
||||
|
||||
- Initial implementation, tests, readme [`7879629`](https://github.com/ljharb/call-bind-apply-helpers/commit/78796290f9b7430c9934d6f33d94ae9bc89fce04)
|
||||
- Initial commit [`3f1dc16`](https://github.com/ljharb/call-bind-apply-helpers/commit/3f1dc164afc43285631b114a5f9dd9137b2b952f)
|
||||
- npm init [`081df04`](https://github.com/ljharb/call-bind-apply-helpers/commit/081df048c312fcee400922026f6e97281200a603)
|
||||
- Only apps should have lockfiles [`5b9ca0f`](https://github.com/ljharb/call-bind-apply-helpers/commit/5b9ca0fe8101ebfaf309c549caac4e0a017ed930)
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2024 Jordan Harband
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
|
@ -1,62 +0,0 @@
|
|||
# call-bind-apply-helpers <sup>[![Version Badge][npm-version-svg]][package-url]</sup>
|
||||
|
||||
[![github actions][actions-image]][actions-url]
|
||||
[![coverage][codecov-image]][codecov-url]
|
||||
[![dependency status][deps-svg]][deps-url]
|
||||
[![dev dependency status][dev-deps-svg]][dev-deps-url]
|
||||
[![License][license-image]][license-url]
|
||||
[![Downloads][downloads-image]][downloads-url]
|
||||
|
||||
[![npm badge][npm-badge-png]][package-url]
|
||||
|
||||
Helper functions around Function call/apply/bind, for use in `call-bind`.
|
||||
|
||||
The only packages that should likely ever use this package directly are `call-bind` and `get-intrinsic`.
|
||||
Please use `call-bind` unless you have a very good reason not to.
|
||||
|
||||
## Getting started
|
||||
|
||||
```sh
|
||||
npm install --save call-bind-apply-helpers
|
||||
```
|
||||
|
||||
## Usage/Examples
|
||||
|
||||
```js
|
||||
const assert = require('assert');
|
||||
const callBindBasic = require('call-bind-apply-helpers');
|
||||
|
||||
function f(a, b) {
|
||||
assert.equal(this, 1);
|
||||
assert.equal(a, 2);
|
||||
assert.equal(b, 3);
|
||||
assert.equal(arguments.length, 2);
|
||||
}
|
||||
|
||||
const fBound = callBindBasic([f, 1]);
|
||||
|
||||
delete Function.prototype.call;
|
||||
delete Function.prototype.bind;
|
||||
|
||||
fBound(2, 3);
|
||||
```
|
||||
|
||||
## Tests
|
||||
|
||||
Clone the repo, `npm install`, and run `npm test`
|
||||
|
||||
[package-url]: https://npmjs.org/package/call-bind-apply-helpers
|
||||
[npm-version-svg]: https://versionbadg.es/ljharb/call-bind-apply-helpers.svg
|
||||
[deps-svg]: https://david-dm.org/ljharb/call-bind-apply-helpers.svg
|
||||
[deps-url]: https://david-dm.org/ljharb/call-bind-apply-helpers
|
||||
[dev-deps-svg]: https://david-dm.org/ljharb/call-bind-apply-helpers/dev-status.svg
|
||||
[dev-deps-url]: https://david-dm.org/ljharb/call-bind-apply-helpers#info=devDependencies
|
||||
[npm-badge-png]: https://nodei.co/npm/call-bind-apply-helpers.png?downloads=true&stars=true
|
||||
[license-image]: https://img.shields.io/npm/l/call-bind-apply-helpers.svg
|
||||
[license-url]: LICENSE
|
||||
[downloads-image]: https://img.shields.io/npm/dm/call-bind-apply-helpers.svg
|
||||
[downloads-url]: https://npm-stat.com/charts.html?package=call-bind-apply-helpers
|
||||
[codecov-image]: https://codecov.io/gh/ljharb/call-bind-apply-helpers/branch/main/graphs/badge.svg
|
||||
[codecov-url]: https://app.codecov.io/gh/ljharb/call-bind-apply-helpers/
|
||||
[actions-image]: https://img.shields.io/endpoint?url=https://github-actions-badge-u3jn4tfpocch.runkit.sh/ljharb/call-bind-apply-helpers
|
||||
[actions-url]: https://github.com/ljharb/call-bind-apply-helpers/actions
|
||||
|
|
@ -1 +0,0 @@
|
|||
export = Reflect.apply;
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
var bind = require('function-bind');
|
||||
|
||||
var $apply = require('./functionApply');
|
||||
var $call = require('./functionCall');
|
||||
var $reflectApply = require('./reflectApply');
|
||||
|
||||
/** @type {import('./actualApply')} */
|
||||
module.exports = $reflectApply || bind.call($call, $apply);
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
import actualApply from './actualApply';
|
||||
|
||||
type TupleSplitHead<T extends any[], N extends number> = T['length'] extends N
|
||||
? T
|
||||
: T extends [...infer R, any]
|
||||
? TupleSplitHead<R, N>
|
||||
: never
|
||||
|
||||
type TupleSplitTail<T, N extends number, O extends any[] = []> = O['length'] extends N
|
||||
? T
|
||||
: T extends [infer F, ...infer R]
|
||||
? TupleSplitTail<[...R], N, [...O, F]>
|
||||
: never
|
||||
|
||||
type TupleSplit<T extends any[], N extends number> = [TupleSplitHead<T, N>, TupleSplitTail<T, N>]
|
||||
|
||||
declare function applyBind(...args: TupleSplit<Parameters<typeof actualApply>, 2>[1]): ReturnType<typeof actualApply>;
|
||||
|
||||
export = applyBind;
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
var bind = require('function-bind');
|
||||
var $apply = require('./functionApply');
|
||||
var actualApply = require('./actualApply');
|
||||
|
||||
/** @type {import('./applyBind')} */
|
||||
module.exports = function applyBind() {
|
||||
return actualApply(bind, $apply, arguments);
|
||||
};
|
||||
|
|
@ -1 +0,0 @@
|
|||
export = Function.prototype.apply;
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
/** @type {import('./functionApply')} */
|
||||
module.exports = Function.prototype.apply;
|
||||
|
|
@ -1 +0,0 @@
|
|||
export = Function.prototype.call;
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
/** @type {import('./functionCall')} */
|
||||
module.exports = Function.prototype.call;
|
||||
|
|
@ -1,64 +0,0 @@
|
|||
type RemoveFromTuple<
|
||||
Tuple extends readonly unknown[],
|
||||
RemoveCount extends number,
|
||||
Index extends 1[] = []
|
||||
> = Index["length"] extends RemoveCount
|
||||
? Tuple
|
||||
: Tuple extends [infer First, ...infer Rest]
|
||||
? RemoveFromTuple<Rest, RemoveCount, [...Index, 1]>
|
||||
: Tuple;
|
||||
|
||||
type ConcatTuples<
|
||||
Prefix extends readonly unknown[],
|
||||
Suffix extends readonly unknown[]
|
||||
> = [...Prefix, ...Suffix];
|
||||
|
||||
type ExtractFunctionParams<T> = T extends (this: infer TThis, ...args: infer P extends readonly unknown[]) => infer R
|
||||
? { thisArg: TThis; params: P; returnType: R }
|
||||
: never;
|
||||
|
||||
type BindFunction<
|
||||
T extends (this: any, ...args: any[]) => any,
|
||||
TThis,
|
||||
TBoundArgs extends readonly unknown[],
|
||||
ReceiverBound extends boolean
|
||||
> = ExtractFunctionParams<T> extends {
|
||||
thisArg: infer OrigThis;
|
||||
params: infer P extends readonly unknown[];
|
||||
returnType: infer R;
|
||||
}
|
||||
? ReceiverBound extends true
|
||||
? (...args: RemoveFromTuple<P, Extract<TBoundArgs["length"], number>>) => R extends [OrigThis, ...infer Rest]
|
||||
? [TThis, ...Rest] // Replace `this` with `thisArg`
|
||||
: R
|
||||
: <U, RemainingArgs extends RemoveFromTuple<P, Extract<TBoundArgs["length"], number>>>(
|
||||
thisArg: U,
|
||||
...args: RemainingArgs
|
||||
) => R extends [OrigThis, ...infer Rest]
|
||||
? [U, ...ConcatTuples<TBoundArgs, Rest>] // Preserve bound args in return type
|
||||
: R
|
||||
: never;
|
||||
|
||||
declare function callBind<
|
||||
const T extends (this: any, ...args: any[]) => any,
|
||||
Extracted extends ExtractFunctionParams<T>,
|
||||
const TBoundArgs extends Partial<Extracted["params"]> & readonly unknown[],
|
||||
const TThis extends Extracted["thisArg"]
|
||||
>(
|
||||
args: [fn: T, thisArg: TThis, ...boundArgs: TBoundArgs]
|
||||
): BindFunction<T, TThis, TBoundArgs, true>;
|
||||
|
||||
declare function callBind<
|
||||
const T extends (this: any, ...args: any[]) => any,
|
||||
Extracted extends ExtractFunctionParams<T>,
|
||||
const TBoundArgs extends Partial<Extracted["params"]> & readonly unknown[]
|
||||
>(
|
||||
args: [fn: T, ...boundArgs: TBoundArgs]
|
||||
): BindFunction<T, Extracted["thisArg"], TBoundArgs, false>;
|
||||
|
||||
declare function callBind<const TArgs extends readonly unknown[]>(
|
||||
args: [fn: Exclude<TArgs[0], Function>, ...rest: TArgs]
|
||||
): never;
|
||||
|
||||
// export as namespace callBind;
|
||||
export = callBind;
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
var bind = require('function-bind');
|
||||
var $TypeError = require('es-errors/type');
|
||||
|
||||
var $call = require('./functionCall');
|
||||
var $actualApply = require('./actualApply');
|
||||
|
||||
/** @type {(args: [Function, thisArg?: unknown, ...args: unknown[]]) => Function} TODO FIXME, find a way to use import('.') */
|
||||
module.exports = function callBindBasic(args) {
|
||||
if (args.length < 1 || typeof args[0] !== 'function') {
|
||||
throw new $TypeError('a function is required');
|
||||
}
|
||||
return $actualApply(bind, $call, args);
|
||||
};
|
||||
|
|
@ -1,85 +0,0 @@
|
|||
{
|
||||
"name": "call-bind-apply-helpers",
|
||||
"version": "1.0.2",
|
||||
"description": "Helper functions around Function call/apply/bind, for use in `call-bind`",
|
||||
"main": "index.js",
|
||||
"exports": {
|
||||
".": "./index.js",
|
||||
"./actualApply": "./actualApply.js",
|
||||
"./applyBind": "./applyBind.js",
|
||||
"./functionApply": "./functionApply.js",
|
||||
"./functionCall": "./functionCall.js",
|
||||
"./reflectApply": "./reflectApply.js",
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"scripts": {
|
||||
"prepack": "npmignore --auto --commentLines=auto",
|
||||
"prepublish": "not-in-publish || npm run prepublishOnly",
|
||||
"prepublishOnly": "safe-publish-latest",
|
||||
"prelint": "evalmd README.md",
|
||||
"lint": "eslint --ext=.js,.mjs .",
|
||||
"postlint": "tsc -p . && attw -P",
|
||||
"pretest": "npm run lint",
|
||||
"tests-only": "nyc tape 'test/**/*.js'",
|
||||
"test": "npm run tests-only",
|
||||
"posttest": "npx npm@'>=10.2' audit --production",
|
||||
"version": "auto-changelog && git add CHANGELOG.md",
|
||||
"postversion": "auto-changelog && git add CHANGELOG.md && git commit --no-edit --amend && git tag -f \"v$(node -e \"console.log(require('./package.json').version)\")\""
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/ljharb/call-bind-apply-helpers.git"
|
||||
},
|
||||
"author": "Jordan Harband <ljharb@gmail.com>",
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/ljharb/call-bind-apply-helpers/issues"
|
||||
},
|
||||
"homepage": "https://github.com/ljharb/call-bind-apply-helpers#readme",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"function-bind": "^1.1.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@arethetypeswrong/cli": "^0.17.3",
|
||||
"@ljharb/eslint-config": "^21.1.1",
|
||||
"@ljharb/tsconfig": "^0.2.3",
|
||||
"@types/for-each": "^0.3.3",
|
||||
"@types/function-bind": "^1.1.10",
|
||||
"@types/object-inspect": "^1.13.0",
|
||||
"@types/tape": "^5.8.1",
|
||||
"auto-changelog": "^2.5.0",
|
||||
"encoding": "^0.1.13",
|
||||
"es-value-fixtures": "^1.7.1",
|
||||
"eslint": "=8.8.0",
|
||||
"evalmd": "^0.0.19",
|
||||
"for-each": "^0.3.5",
|
||||
"has-strict-mode": "^1.1.0",
|
||||
"in-publish": "^2.0.1",
|
||||
"npmignore": "^0.3.1",
|
||||
"nyc": "^10.3.2",
|
||||
"object-inspect": "^1.13.4",
|
||||
"safe-publish-latest": "^2.0.0",
|
||||
"tape": "^5.9.0",
|
||||
"typescript": "next"
|
||||
},
|
||||
"testling": {
|
||||
"files": "test/index.js"
|
||||
},
|
||||
"auto-changelog": {
|
||||
"output": "CHANGELOG.md",
|
||||
"template": "keepachangelog",
|
||||
"unreleased": false,
|
||||
"commitLimit": false,
|
||||
"backfillLimit": false,
|
||||
"hideCredit": true
|
||||
},
|
||||
"publishConfig": {
|
||||
"ignore": [
|
||||
".github/workflows"
|
||||
]
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
declare const reflectApply: false | typeof Reflect.apply;
|
||||
|
||||
export = reflectApply;
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
/** @type {import('./reflectApply')} */
|
||||
module.exports = typeof Reflect !== 'undefined' && Reflect && Reflect.apply;
|
||||
|
|
@ -1,63 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
var callBind = require('../');
|
||||
var hasStrictMode = require('has-strict-mode')();
|
||||
var forEach = require('for-each');
|
||||
var inspect = require('object-inspect');
|
||||
var v = require('es-value-fixtures');
|
||||
|
||||
var test = require('tape');
|
||||
|
||||
test('callBindBasic', function (t) {
|
||||
forEach(v.nonFunctions, function (nonFunction) {
|
||||
t['throws'](
|
||||
// @ts-expect-error
|
||||
function () { callBind([nonFunction]); },
|
||||
TypeError,
|
||||
inspect(nonFunction) + ' is not a function'
|
||||
);
|
||||
});
|
||||
|
||||
var sentinel = { sentinel: true };
|
||||
/** @type {<T, A extends number, B extends number>(this: T, a: A, b: B) => [T | undefined, A, B]} */
|
||||
var func = function (a, b) {
|
||||
// eslint-disable-next-line no-invalid-this
|
||||
return [!hasStrictMode && this === global ? undefined : this, a, b];
|
||||
};
|
||||
t.equal(func.length, 2, 'original function length is 2');
|
||||
|
||||
/** type {(thisArg: unknown, a: number, b: number) => [unknown, number, number]} */
|
||||
var bound = callBind([func]);
|
||||
/** type {((a: number, b: number) => [typeof sentinel, typeof a, typeof b])} */
|
||||
var boundR = callBind([func, sentinel]);
|
||||
/** type {((b: number) => [typeof sentinel, number, typeof b])} */
|
||||
var boundArg = callBind([func, sentinel, /** @type {const} */ (1)]);
|
||||
|
||||
// @ts-expect-error
|
||||
t.deepEqual(bound(), [undefined, undefined, undefined], 'bound func with no args');
|
||||
|
||||
// @ts-expect-error
|
||||
t.deepEqual(func(), [undefined, undefined, undefined], 'unbound func with too few args');
|
||||
// @ts-expect-error
|
||||
t.deepEqual(bound(1, 2), [hasStrictMode ? 1 : Object(1), 2, undefined], 'bound func too few args');
|
||||
// @ts-expect-error
|
||||
t.deepEqual(boundR(), [sentinel, undefined, undefined], 'bound func with receiver, with too few args');
|
||||
// @ts-expect-error
|
||||
t.deepEqual(boundArg(), [sentinel, 1, undefined], 'bound func with receiver and arg, with too few args');
|
||||
|
||||
t.deepEqual(func(1, 2), [undefined, 1, 2], 'unbound func with right args');
|
||||
t.deepEqual(bound(1, 2, 3), [hasStrictMode ? 1 : Object(1), 2, 3], 'bound func with right args');
|
||||
t.deepEqual(boundR(1, 2), [sentinel, 1, 2], 'bound func with receiver, with right args');
|
||||
t.deepEqual(boundArg(2), [sentinel, 1, 2], 'bound func with receiver and arg, with right arg');
|
||||
|
||||
// @ts-expect-error
|
||||
t.deepEqual(func(1, 2, 3), [undefined, 1, 2], 'unbound func with too many args');
|
||||
// @ts-expect-error
|
||||
t.deepEqual(bound(1, 2, 3, 4), [hasStrictMode ? 1 : Object(1), 2, 3], 'bound func with too many args');
|
||||
// @ts-expect-error
|
||||
t.deepEqual(boundR(1, 2, 3), [sentinel, 1, 2], 'bound func with receiver, with too many args');
|
||||
// @ts-expect-error
|
||||
t.deepEqual(boundArg(2, 3), [sentinel, 1, 2], 'bound func with receiver and arg, with too many args');
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
{
|
||||
"extends": "@ljharb/tsconfig",
|
||||
"compilerOptions": {
|
||||
"target": "es2021",
|
||||
},
|
||||
"exclude": [
|
||||
"coverage",
|
||||
],
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
{
|
||||
"root": true,
|
||||
|
||||
"extends": "@ljharb",
|
||||
|
||||
"rules": {
|
||||
"new-cap": [2, {
|
||||
"capIsNewExceptions": [
|
||||
"GetIntrinsic",
|
||||
],
|
||||
}],
|
||||
},
|
||||
}
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
# These are supported funding model platforms
|
||||
|
||||
github: [ljharb]
|
||||
patreon: # Replace with a single Patreon username
|
||||
open_collective: # Replace with a single Open Collective username
|
||||
ko_fi: # Replace with a single Ko-fi username
|
||||
tidelift: npm/call-bound
|
||||
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||
liberapay: # Replace with a single Liberapay username
|
||||
issuehunt: # Replace with a single IssueHunt username
|
||||
otechie: # Replace with a single Otechie username
|
||||
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
{
|
||||
"all": true,
|
||||
"check-coverage": false,
|
||||
"reporter": ["text-summary", "text", "html", "json"],
|
||||
"exclude": [
|
||||
"coverage",
|
||||
"test"
|
||||
]
|
||||
}
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [v1.0.4](https://github.com/ljharb/call-bound/compare/v1.0.3...v1.0.4) - 2025-03-03
|
||||
|
||||
### Commits
|
||||
|
||||
- [types] improve types [`e648922`](https://github.com/ljharb/call-bound/commit/e6489222a9e54f350fbf952ceabe51fd8b6027ff)
|
||||
- [Dev Deps] update `@arethetypeswrong/cli`, `@ljharb/tsconfig`, `@types/tape`, `es-value-fixtures`, `for-each`, `has-strict-mode`, `object-inspect` [`a42a5eb`](https://github.com/ljharb/call-bound/commit/a42a5ebe6c1b54fcdc7997c7dc64fdca9e936719)
|
||||
- [Deps] update `call-bind-apply-helpers`, `get-intrinsic` [`f529eac`](https://github.com/ljharb/call-bound/commit/f529eac132404c17156bbc23ab2297a25d0f20b8)
|
||||
|
||||
## [v1.0.3](https://github.com/ljharb/call-bound/compare/v1.0.2...v1.0.3) - 2024-12-15
|
||||
|
||||
### Commits
|
||||
|
||||
- [Refactor] use `call-bind-apply-helpers` instead of `call-bind` [`5e0b134`](https://github.com/ljharb/call-bound/commit/5e0b13496df14fb7d05dae9412f088da8d3f75be)
|
||||
- [Deps] update `get-intrinsic` [`41fc967`](https://github.com/ljharb/call-bound/commit/41fc96732a22c7b7e8f381f93ccc54bb6293be2e)
|
||||
- [readme] fix example [`79a0137`](https://github.com/ljharb/call-bound/commit/79a0137723f7c6d09c9c05452bbf8d5efb5d6e49)
|
||||
- [meta] add `sideEffects` flag [`08b07be`](https://github.com/ljharb/call-bound/commit/08b07be7f1c03f67dc6f3cdaf0906259771859f7)
|
||||
|
||||
## [v1.0.2](https://github.com/ljharb/call-bound/compare/v1.0.1...v1.0.2) - 2024-12-10
|
||||
|
||||
### Commits
|
||||
|
||||
- [Dev Deps] update `@arethetypeswrong/cli`, `@ljharb/tsconfig`, `gopd` [`e6a5ffe`](https://github.com/ljharb/call-bound/commit/e6a5ffe849368fe4f74dfd6cdeca1b9baa39e8d5)
|
||||
- [Deps] update `call-bind`, `get-intrinsic` [`2aeb5b5`](https://github.com/ljharb/call-bound/commit/2aeb5b521dc2b2683d1345c753ea1161de2d1c14)
|
||||
- [types] improve return type [`1a0c9fe`](https://github.com/ljharb/call-bound/commit/1a0c9fe3114471e7ca1f57d104e2efe713bb4871)
|
||||
|
||||
## v1.0.1 - 2024-12-05
|
||||
|
||||
### Commits
|
||||
|
||||
- Initial implementation, tests, readme, types [`6d94121`](https://github.com/ljharb/call-bound/commit/6d94121a9243602e506334069f7a03189fe3363d)
|
||||
- Initial commit [`0eae867`](https://github.com/ljharb/call-bound/commit/0eae867334ea025c33e6e91cdecfc9df96680cf9)
|
||||
- npm init [`71b2479`](https://github.com/ljharb/call-bound/commit/71b2479c6723e0b7d91a6b663613067e98b7b275)
|
||||
- Only apps should have lockfiles [`c3754a9`](https://github.com/ljharb/call-bound/commit/c3754a949b7f9132b47e2d18c1729889736741eb)
|
||||
- [actions] skip `npm ls` in node < 10 [`74275a5`](https://github.com/ljharb/call-bound/commit/74275a5186b8caf6309b6b97472bdcb0df4683a8)
|
||||
- [Dev Deps] add missing peer dep [`1354de8`](https://github.com/ljharb/call-bound/commit/1354de8679413e4ae9c523d85f76fa7a5e032d97)
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2024 Jordan Harband
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
|
@ -1,53 +0,0 @@
|
|||
# call-bound <sup>[![Version Badge][npm-version-svg]][package-url]</sup>
|
||||
|
||||
[![github actions][actions-image]][actions-url]
|
||||
[![coverage][codecov-image]][codecov-url]
|
||||
[![dependency status][deps-svg]][deps-url]
|
||||
[![dev dependency status][dev-deps-svg]][dev-deps-url]
|
||||
[![License][license-image]][license-url]
|
||||
[![Downloads][downloads-image]][downloads-url]
|
||||
|
||||
[![npm badge][npm-badge-png]][package-url]
|
||||
|
||||
Robust call-bound JavaScript intrinsics, using `call-bind` and `get-intrinsic`.
|
||||
|
||||
## Getting started
|
||||
|
||||
```sh
|
||||
npm install --save call-bound
|
||||
```
|
||||
|
||||
## Usage/Examples
|
||||
|
||||
```js
|
||||
const assert = require('assert');
|
||||
const callBound = require('call-bound');
|
||||
|
||||
const slice = callBound('Array.prototype.slice');
|
||||
|
||||
delete Function.prototype.call;
|
||||
delete Function.prototype.bind;
|
||||
delete Array.prototype.slice;
|
||||
|
||||
assert.deepEqual(slice([1, 2, 3, 4], 1, -1), [2, 3]);
|
||||
```
|
||||
|
||||
## Tests
|
||||
|
||||
Clone the repo, `npm install`, and run `npm test`
|
||||
|
||||
[package-url]: https://npmjs.org/package/call-bound
|
||||
[npm-version-svg]: https://versionbadg.es/ljharb/call-bound.svg
|
||||
[deps-svg]: https://david-dm.org/ljharb/call-bound.svg
|
||||
[deps-url]: https://david-dm.org/ljharb/call-bound
|
||||
[dev-deps-svg]: https://david-dm.org/ljharb/call-bound/dev-status.svg
|
||||
[dev-deps-url]: https://david-dm.org/ljharb/call-bound#info=devDependencies
|
||||
[npm-badge-png]: https://nodei.co/npm/call-bound.png?downloads=true&stars=true
|
||||
[license-image]: https://img.shields.io/npm/l/call-bound.svg
|
||||
[license-url]: LICENSE
|
||||
[downloads-image]: https://img.shields.io/npm/dm/call-bound.svg
|
||||
[downloads-url]: https://npm-stat.com/charts.html?package=call-bound
|
||||
[codecov-image]: https://codecov.io/gh/ljharb/call-bound/branch/main/graphs/badge.svg
|
||||
[codecov-url]: https://app.codecov.io/gh/ljharb/call-bound/
|
||||
[actions-image]: https://img.shields.io/endpoint?url=https://github-actions-badge-u3jn4tfpocch.runkit.sh/ljharb/call-bound
|
||||
[actions-url]: https://github.com/ljharb/call-bound/actions
|
||||
|
|
@ -1,94 +0,0 @@
|
|||
type Intrinsic = typeof globalThis;
|
||||
|
||||
type IntrinsicName = keyof Intrinsic | `%${keyof Intrinsic}%`;
|
||||
|
||||
type IntrinsicPath = IntrinsicName | `${StripPercents<IntrinsicName>}.${string}` | `%${StripPercents<IntrinsicName>}.${string}%`;
|
||||
|
||||
type AllowMissing = boolean;
|
||||
|
||||
type StripPercents<T extends string> = T extends `%${infer U}%` ? U : T;
|
||||
|
||||
type BindMethodPrecise<F> =
|
||||
F extends (this: infer This, ...args: infer Args) => infer R
|
||||
? (obj: This, ...args: Args) => R
|
||||
: F extends {
|
||||
(this: infer This1, ...args: infer Args1): infer R1;
|
||||
(this: infer This2, ...args: infer Args2): infer R2
|
||||
}
|
||||
? {
|
||||
(obj: This1, ...args: Args1): R1;
|
||||
(obj: This2, ...args: Args2): R2
|
||||
}
|
||||
: never
|
||||
|
||||
// Extract method type from a prototype
|
||||
type GetPrototypeMethod<T extends keyof typeof globalThis, M extends string> =
|
||||
(typeof globalThis)[T] extends { prototype: any }
|
||||
? M extends keyof (typeof globalThis)[T]['prototype']
|
||||
? (typeof globalThis)[T]['prototype'][M]
|
||||
: never
|
||||
: never
|
||||
|
||||
// Get static property/method
|
||||
type GetStaticMember<T extends keyof typeof globalThis, P extends string> =
|
||||
P extends keyof (typeof globalThis)[T] ? (typeof globalThis)[T][P] : never
|
||||
|
||||
// Type that maps string path to actual bound function or value with better precision
|
||||
type BoundIntrinsic<S extends string> =
|
||||
S extends `${infer Obj}.prototype.${infer Method}`
|
||||
? Obj extends keyof typeof globalThis
|
||||
? BindMethodPrecise<GetPrototypeMethod<Obj, Method & string>>
|
||||
: unknown
|
||||
: S extends `${infer Obj}.${infer Prop}`
|
||||
? Obj extends keyof typeof globalThis
|
||||
? GetStaticMember<Obj, Prop & string>
|
||||
: unknown
|
||||
: unknown
|
||||
|
||||
declare function arraySlice<T>(array: readonly T[], start?: number, end?: number): T[];
|
||||
declare function arraySlice<T>(array: ArrayLike<T>, start?: number, end?: number): T[];
|
||||
declare function arraySlice<T>(array: IArguments, start?: number, end?: number): T[];
|
||||
|
||||
// Special cases for methods that need explicit typing
|
||||
interface SpecialCases {
|
||||
'%Object.prototype.isPrototypeOf%': (thisArg: {}, obj: unknown) => boolean;
|
||||
'%String.prototype.replace%': {
|
||||
(str: string, searchValue: string | RegExp, replaceValue: string): string;
|
||||
(str: string, searchValue: string | RegExp, replacer: (substring: string, ...args: any[]) => string): string
|
||||
};
|
||||
'%Object.prototype.toString%': (obj: {}) => string;
|
||||
'%Object.prototype.hasOwnProperty%': (obj: {}, v: PropertyKey) => boolean;
|
||||
'%Array.prototype.slice%': typeof arraySlice;
|
||||
'%Array.prototype.map%': <T, U>(array: readonly T[], callbackfn: (value: T, index: number, array: readonly T[]) => U, thisArg?: any) => U[];
|
||||
'%Array.prototype.filter%': <T>(array: readonly T[], predicate: (value: T, index: number, array: readonly T[]) => unknown, thisArg?: any) => T[];
|
||||
'%Array.prototype.indexOf%': <T>(array: readonly T[], searchElement: T, fromIndex?: number) => number;
|
||||
'%Function.prototype.apply%': <T, A extends any[], R>(fn: (...args: A) => R, thisArg: any, args: A) => R;
|
||||
'%Function.prototype.call%': <T, A extends any[], R>(fn: (...args: A) => R, thisArg: any, ...args: A) => R;
|
||||
'%Function.prototype.bind%': <T, A extends any[], R>(fn: (...args: A) => R, thisArg: any, ...args: A) => (...remainingArgs: A) => R;
|
||||
'%Promise.prototype.then%': {
|
||||
<T, R>(promise: Promise<T>, onfulfilled: (value: T) => R | PromiseLike<R>): Promise<R>;
|
||||
<T, R>(promise: Promise<T>, onfulfilled: ((value: T) => R | PromiseLike<R>) | undefined | null, onrejected: (reason: any) => R | PromiseLike<R>): Promise<R>;
|
||||
};
|
||||
'%RegExp.prototype.test%': (regexp: RegExp, str: string) => boolean;
|
||||
'%RegExp.prototype.exec%': (regexp: RegExp, str: string) => RegExpExecArray | null;
|
||||
'%Error.prototype.toString%': (error: Error) => string;
|
||||
'%TypeError.prototype.toString%': (error: TypeError) => string;
|
||||
'%String.prototype.split%': (
|
||||
obj: unknown,
|
||||
splitter: string | RegExp | {
|
||||
[Symbol.split](string: string, limit?: number): string[];
|
||||
},
|
||||
limit?: number | undefined
|
||||
) => string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a bound function for a prototype method, or a value for a static property.
|
||||
*
|
||||
* @param name - The name of the intrinsic (e.g. 'Array.prototype.slice')
|
||||
* @param {AllowMissing} [allowMissing] - Whether to allow missing intrinsics (default: false)
|
||||
*/
|
||||
declare function callBound<K extends keyof SpecialCases | StripPercents<keyof SpecialCases>, S extends IntrinsicPath>(name: K, allowMissing?: AllowMissing): SpecialCases[`%${StripPercents<K>}%`];
|
||||
declare function callBound<K extends keyof SpecialCases | StripPercents<keyof SpecialCases>, S extends IntrinsicPath>(name: S, allowMissing?: AllowMissing): BoundIntrinsic<S>;
|
||||
|
||||
export = callBound;
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
var GetIntrinsic = require('get-intrinsic');
|
||||
|
||||
var callBindBasic = require('call-bind-apply-helpers');
|
||||
|
||||
/** @type {(thisArg: string, searchString: string, position?: number) => number} */
|
||||
var $indexOf = callBindBasic([GetIntrinsic('%String.prototype.indexOf%')]);
|
||||
|
||||
/** @type {import('.')} */
|
||||
module.exports = function callBoundIntrinsic(name, allowMissing) {
|
||||
/* eslint no-extra-parens: 0 */
|
||||
|
||||
var intrinsic = /** @type {(this: unknown, ...args: unknown[]) => unknown} */ (GetIntrinsic(name, !!allowMissing));
|
||||
if (typeof intrinsic === 'function' && $indexOf(name, '.prototype.') > -1) {
|
||||
return callBindBasic(/** @type {const} */ ([intrinsic]));
|
||||
}
|
||||
return intrinsic;
|
||||
};
|
||||
|
|
@ -1,99 +0,0 @@
|
|||
{
|
||||
"name": "call-bound",
|
||||
"version": "1.0.4",
|
||||
"description": "Robust call-bound JavaScript intrinsics, using `call-bind` and `get-intrinsic`.",
|
||||
"main": "index.js",
|
||||
"exports": {
|
||||
".": "./index.js",
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"sideEffects": false,
|
||||
"scripts": {
|
||||
"prepack": "npmignore --auto --commentLines=auto",
|
||||
"prepublish": "not-in-publish || npm run prepublishOnly",
|
||||
"prepublishOnly": "safe-publish-latest",
|
||||
"prelint": "evalmd README.md",
|
||||
"lint": "eslint --ext=.js,.mjs .",
|
||||
"postlint": "tsc -p . && attw -P",
|
||||
"pretest": "npm run lint",
|
||||
"tests-only": "nyc tape 'test/**/*.js'",
|
||||
"test": "npm run tests-only",
|
||||
"posttest": "npx npm@'>=10.2' audit --production",
|
||||
"version": "auto-changelog && git add CHANGELOG.md",
|
||||
"postversion": "auto-changelog && git add CHANGELOG.md && git commit --no-edit --amend && git tag -f \"v$(node -e \"console.log(require('./package.json').version)\")\""
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/ljharb/call-bound.git"
|
||||
},
|
||||
"keywords": [
|
||||
"javascript",
|
||||
"ecmascript",
|
||||
"es",
|
||||
"js",
|
||||
"callbind",
|
||||
"callbound",
|
||||
"call",
|
||||
"bind",
|
||||
"bound",
|
||||
"call-bind",
|
||||
"call-bound",
|
||||
"function",
|
||||
"es-abstract"
|
||||
],
|
||||
"author": "Jordan Harband <ljharb@gmail.com>",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
},
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/ljharb/call-bound/issues"
|
||||
},
|
||||
"homepage": "https://github.com/ljharb/call-bound#readme",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.2",
|
||||
"get-intrinsic": "^1.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@arethetypeswrong/cli": "^0.17.4",
|
||||
"@ljharb/eslint-config": "^21.1.1",
|
||||
"@ljharb/tsconfig": "^0.3.0",
|
||||
"@types/call-bind": "^1.0.5",
|
||||
"@types/get-intrinsic": "^1.2.3",
|
||||
"@types/tape": "^5.8.1",
|
||||
"auto-changelog": "^2.5.0",
|
||||
"encoding": "^0.1.13",
|
||||
"es-value-fixtures": "^1.7.1",
|
||||
"eslint": "=8.8.0",
|
||||
"evalmd": "^0.0.19",
|
||||
"for-each": "^0.3.5",
|
||||
"gopd": "^1.2.0",
|
||||
"has-strict-mode": "^1.1.0",
|
||||
"in-publish": "^2.0.1",
|
||||
"npmignore": "^0.3.1",
|
||||
"nyc": "^10.3.2",
|
||||
"object-inspect": "^1.13.4",
|
||||
"safe-publish-latest": "^2.0.0",
|
||||
"tape": "^5.9.0",
|
||||
"typescript": "next"
|
||||
},
|
||||
"testling": {
|
||||
"files": "test/index.js"
|
||||
},
|
||||
"auto-changelog": {
|
||||
"output": "CHANGELOG.md",
|
||||
"template": "keepachangelog",
|
||||
"unreleased": false,
|
||||
"commitLimit": false,
|
||||
"backfillLimit": false,
|
||||
"hideCredit": true
|
||||
},
|
||||
"publishConfig": {
|
||||
"ignore": [
|
||||
".github/workflows"
|
||||
]
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,61 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
var test = require('tape');
|
||||
|
||||
var callBound = require('../');
|
||||
|
||||
/** @template {true} T @template U @typedef {T extends U ? T : never} AssertType */
|
||||
|
||||
test('callBound', function (t) {
|
||||
// static primitive
|
||||
t.equal(callBound('Array.length'), Array.length, 'Array.length yields itself');
|
||||
t.equal(callBound('%Array.length%'), Array.length, '%Array.length% yields itself');
|
||||
|
||||
// static non-function object
|
||||
t.equal(callBound('Array.prototype'), Array.prototype, 'Array.prototype yields itself');
|
||||
t.equal(callBound('%Array.prototype%'), Array.prototype, '%Array.prototype% yields itself');
|
||||
t.equal(callBound('Array.constructor'), Array.constructor, 'Array.constructor yields itself');
|
||||
t.equal(callBound('%Array.constructor%'), Array.constructor, '%Array.constructor% yields itself');
|
||||
|
||||
// static function
|
||||
t.equal(callBound('Date.parse'), Date.parse, 'Date.parse yields itself');
|
||||
t.equal(callBound('%Date.parse%'), Date.parse, '%Date.parse% yields itself');
|
||||
|
||||
// prototype primitive
|
||||
t.equal(callBound('Error.prototype.message'), Error.prototype.message, 'Error.prototype.message yields itself');
|
||||
t.equal(callBound('%Error.prototype.message%'), Error.prototype.message, '%Error.prototype.message% yields itself');
|
||||
|
||||
var x = callBound('Object.prototype.toString');
|
||||
var y = callBound('%Object.prototype.toString%');
|
||||
|
||||
// prototype function
|
||||
t.notEqual(x, Object.prototype.toString, 'Object.prototype.toString does not yield itself');
|
||||
t.notEqual(y, Object.prototype.toString, '%Object.prototype.toString% does not yield itself');
|
||||
t.equal(x(true), Object.prototype.toString.call(true), 'call-bound Object.prototype.toString calls into the original');
|
||||
t.equal(y(true), Object.prototype.toString.call(true), 'call-bound %Object.prototype.toString% calls into the original');
|
||||
|
||||
t['throws'](
|
||||
// @ts-expect-error
|
||||
function () { callBound('does not exist'); },
|
||||
SyntaxError,
|
||||
'nonexistent intrinsic throws'
|
||||
);
|
||||
t['throws'](
|
||||
// @ts-expect-error
|
||||
function () { callBound('does not exist', true); },
|
||||
SyntaxError,
|
||||
'allowMissing arg still throws for unknown intrinsic'
|
||||
);
|
||||
|
||||
t.test('real but absent intrinsic', { skip: typeof WeakRef !== 'undefined' }, function (st) {
|
||||
st['throws'](
|
||||
function () { callBound('WeakRef'); },
|
||||
TypeError,
|
||||
'real but absent intrinsic throws'
|
||||
);
|
||||
st.equal(callBound('WeakRef', true), undefined, 'allowMissing arg avoids exception');
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
{
|
||||
"extends": "@ljharb/tsconfig",
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"lib": ["es2024"],
|
||||
},
|
||||
"exclude": [
|
||||
"coverage",
|
||||
],
|
||||
}
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
(The MIT License)
|
||||
|
||||
Copyright (c) 2014-2017 Douglas Christopher Wilson
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
'Software'), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
|
@ -1,141 +0,0 @@
|
|||
# content-disposition
|
||||
|
||||
[![NPM Version][npm-image]][npm-url]
|
||||
[![NPM Downloads][downloads-image]][downloads-url]
|
||||
[![Node.js Version][node-version-image]][node-version-url]
|
||||
[![Build Status][github-actions-ci-image]][github-actions-ci-url]
|
||||
[![Test Coverage][coveralls-image]][coveralls-url]
|
||||
|
||||
Create and parse HTTP `Content-Disposition` header
|
||||
|
||||
## Installation
|
||||
|
||||
```sh
|
||||
$ npm install content-disposition
|
||||
```
|
||||
|
||||
## API
|
||||
|
||||
```js
|
||||
const contentDisposition = require('content-disposition')
|
||||
```
|
||||
|
||||
### contentDisposition(filename, options)
|
||||
|
||||
Create an attachment `Content-Disposition` header value using the given file name,
|
||||
if supplied. The `filename` is optional and if no file name is desired, but you
|
||||
want to specify `options`, set `filename` to `undefined`.
|
||||
|
||||
```js
|
||||
res.setHeader('Content-Disposition', contentDisposition('∫ maths.pdf'))
|
||||
```
|
||||
|
||||
**note** HTTP headers are of the ISO-8859-1 character set. If you are writing this
|
||||
header through a means different from `setHeader` in Node.js, you'll want to specify
|
||||
the `'binary'` encoding in Node.js.
|
||||
|
||||
#### Options
|
||||
|
||||
`contentDisposition` accepts these properties in the options object.
|
||||
|
||||
##### fallback
|
||||
|
||||
If the `filename` option is outside ISO-8859-1, then the file name is actually
|
||||
stored in a supplemental field for clients that support Unicode file names and
|
||||
a ISO-8859-1 version of the file name is automatically generated.
|
||||
|
||||
This specifies the ISO-8859-1 file name to override the automatic generation or
|
||||
disables the generation all together, defaults to `true`.
|
||||
|
||||
- A string will specify the ISO-8859-1 file name to use in place of automatic
|
||||
generation.
|
||||
- `false` will disable including a ISO-8859-1 file name and only include the
|
||||
Unicode version (unless the file name is already ISO-8859-1).
|
||||
- `true` will enable automatic generation if the file name is outside ISO-8859-1.
|
||||
|
||||
If the `filename` option is ISO-8859-1 and this option is specified and has a
|
||||
different value, then the `filename` option is encoded in the extended field
|
||||
and this set as the fallback field, even though they are both ISO-8859-1.
|
||||
|
||||
##### type
|
||||
|
||||
Specifies the disposition type, defaults to `"attachment"`. This can also be
|
||||
`"inline"`, or any other value (all values except inline are treated like
|
||||
`attachment`, but can convey additional information if both parties agree to
|
||||
it). The type is normalized to lower-case.
|
||||
|
||||
### contentDisposition.parse(string)
|
||||
|
||||
```js
|
||||
const disposition = contentDisposition.parse('attachment; filename="EURO rates.txt"; filename*=UTF-8\'\'%e2%82%ac%20rates.txt')
|
||||
```
|
||||
|
||||
Parse a `Content-Disposition` header string. This automatically handles extended
|
||||
("Unicode") parameters by decoding them and providing them under the standard
|
||||
parameter name. This will return an object with the following properties (examples
|
||||
are shown for the string `'attachment; filename="EURO rates.txt"; filename*=UTF-8\'\'%e2%82%ac%20rates.txt'`):
|
||||
|
||||
- `type`: The disposition type (always lower case). Example: `'attachment'`
|
||||
|
||||
- `parameters`: An object of the parameters in the disposition (name of parameter
|
||||
always lower case and extended versions replace non-extended versions). Example:
|
||||
`{filename: "€ rates.txt"}`
|
||||
|
||||
## Examples
|
||||
|
||||
### Send a file for download
|
||||
|
||||
```js
|
||||
const contentDisposition = require('content-disposition')
|
||||
const fs = require('fs')
|
||||
const http = require('http')
|
||||
const onFinished = require('on-finished')
|
||||
|
||||
const filePath = '/path/to/public/plans.pdf'
|
||||
|
||||
http.createServer(function onRequest (req, res) {
|
||||
// set headers
|
||||
res.setHeader('Content-Type', 'application/pdf')
|
||||
res.setHeader('Content-Disposition', contentDisposition(filePath))
|
||||
|
||||
// send file
|
||||
const stream = fs.createReadStream(filePath)
|
||||
stream.pipe(res)
|
||||
onFinished(res, function () {
|
||||
stream.destroy()
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
```sh
|
||||
$ npm test
|
||||
```
|
||||
|
||||
## References
|
||||
|
||||
- [RFC 2616: Hypertext Transfer Protocol -- HTTP/1.1][rfc-2616]
|
||||
- [RFC 5987: Character Set and Language Encoding for Hypertext Transfer Protocol (HTTP) Header Field Parameters][rfc-5987]
|
||||
- [RFC 6266: Use of the Content-Disposition Header Field in the Hypertext Transfer Protocol (HTTP)][rfc-6266]
|
||||
- [Test Cases for HTTP Content-Disposition header field (RFC 6266) and the Encodings defined in RFCs 2047, 2231 and 5987][tc-2231]
|
||||
|
||||
[rfc-2616]: https://datatracker.ietf.org/doc/html/rfc2616
|
||||
[rfc-5987]: https://datatracker.ietf.org/doc/html/rfc5987
|
||||
[rfc-6266]: https://datatracker.ietf.org/doc/html/rfc6266
|
||||
[tc-2231]: http://greenbytes.de/tech/tc2231/
|
||||
|
||||
## License
|
||||
|
||||
[MIT](LICENSE)
|
||||
|
||||
[npm-image]: https://img.shields.io/npm/v/content-disposition
|
||||
[npm-url]: https://www.npmjs.com/package/content-disposition
|
||||
[node-version-image]: https://img.shields.io/node/v/content-disposition
|
||||
[node-version-url]: https://nodejs.org/en/download
|
||||
[coveralls-image]: https://img.shields.io/coverallsCoverage/github/jshttp/content-disposition
|
||||
[coveralls-url]: https://coveralls.io/github/jshttp/content-disposition?branch=master
|
||||
[downloads-image]: https://img.shields.io/npm/dm/content-disposition
|
||||
[downloads-url]: https://www.npmjs.com/package/content-disposition
|
||||
[github-actions-ci-image]: https://img.shields.io/github/actions/workflow/status/jshttp/content-disposition/ci.yml
|
||||
[github-actions-ci-url]: https://github.com/jshttp/content-disposition/actions/workflows/ci.yml
|
||||
|
|
@ -1,536 +0,0 @@
|
|||
/*!
|
||||
* content-disposition
|
||||
* Copyright(c) 2014-2017 Douglas Christopher Wilson
|
||||
* MIT Licensed
|
||||
*/
|
||||
|
||||
'use strict'
|
||||
|
||||
/**
|
||||
* Module exports.
|
||||
* @public
|
||||
*/
|
||||
|
||||
module.exports = contentDisposition
|
||||
module.exports.parse = parse
|
||||
|
||||
/**
|
||||
* TextDecoder instance for UTF-8 decoding when decodeURIComponent fails due to invalid byte sequences.
|
||||
* @type {TextDecoder}
|
||||
* @private
|
||||
*/
|
||||
const utf8Decoder = new TextDecoder('utf-8')
|
||||
|
||||
/**
|
||||
* RegExp to match non attr-char, *after* encodeURIComponent (i.e. not including "%")
|
||||
* @private
|
||||
*/
|
||||
|
||||
var ENCODE_URL_ATTR_CHAR_REGEXP = /[\x00-\x20"'()*,/:;<=>?@[\\\]{}\x7f]/g // eslint-disable-line no-control-regex
|
||||
|
||||
/**
|
||||
* RegExp to match non-latin1 characters.
|
||||
* @private
|
||||
*/
|
||||
|
||||
var NON_LATIN1_REGEXP = /[^\x20-\x7e\xa0-\xff]/g
|
||||
|
||||
/**
|
||||
* RegExp to match quoted-pair in RFC 2616
|
||||
*
|
||||
* quoted-pair = "\" CHAR
|
||||
* CHAR = <any US-ASCII character (octets 0 - 127)>
|
||||
* @private
|
||||
*/
|
||||
|
||||
var QESC_REGEXP = /\\([\u0000-\u007f])/g // eslint-disable-line no-control-regex
|
||||
|
||||
/**
|
||||
* RegExp to match chars that must be quoted-pair in RFC 2616
|
||||
* @private
|
||||
*/
|
||||
|
||||
var QUOTE_REGEXP = /([\\"])/g
|
||||
|
||||
/**
|
||||
* RegExp for various RFC 2616 grammar
|
||||
*
|
||||
* parameter = token "=" ( token | quoted-string )
|
||||
* token = 1*<any CHAR except CTLs or separators>
|
||||
* separators = "(" | ")" | "<" | ">" | "@"
|
||||
* | "," | ";" | ":" | "\" | <">
|
||||
* | "/" | "[" | "]" | "?" | "="
|
||||
* | "{" | "}" | SP | HT
|
||||
* quoted-string = ( <"> *(qdtext | quoted-pair ) <"> )
|
||||
* qdtext = <any TEXT except <">>
|
||||
* quoted-pair = "\" CHAR
|
||||
* CHAR = <any US-ASCII character (octets 0 - 127)>
|
||||
* TEXT = <any OCTET except CTLs, but including LWS>
|
||||
* LWS = [CRLF] 1*( SP | HT )
|
||||
* CRLF = CR LF
|
||||
* CR = <US-ASCII CR, carriage return (13)>
|
||||
* LF = <US-ASCII LF, linefeed (10)>
|
||||
* SP = <US-ASCII SP, space (32)>
|
||||
* HT = <US-ASCII HT, horizontal-tab (9)>
|
||||
* CTL = <any US-ASCII control character (octets 0 - 31) and DEL (127)>
|
||||
* OCTET = <any 8-bit sequence of data>
|
||||
* @private
|
||||
*/
|
||||
|
||||
var PARAM_REGEXP = /;[\x09\x20]*([!#$%&'*+.0-9A-Z^_`a-z|~-]+)[\x09\x20]*=[\x09\x20]*("(?:[\x20!\x23-\x5b\x5d-\x7e\x80-\xff]|\\[\x20-\x7e])*"|[!#$%&'*+.0-9A-Z^_`a-z|~-]+)[\x09\x20]*/g // eslint-disable-line no-control-regex
|
||||
var TEXT_REGEXP = /^[\x20-\x7e\x80-\xff]+$/
|
||||
var TOKEN_REGEXP = /^[!#$%&'*+.0-9A-Z^_`a-z|~-]+$/
|
||||
|
||||
/**
|
||||
* RegExp for various RFC 5987 grammar
|
||||
*
|
||||
* ext-value = charset "'" [ language ] "'" value-chars
|
||||
* charset = "UTF-8" / "ISO-8859-1" / mime-charset
|
||||
* mime-charset = 1*mime-charsetc
|
||||
* mime-charsetc = ALPHA / DIGIT
|
||||
* / "!" / "#" / "$" / "%" / "&"
|
||||
* / "+" / "-" / "^" / "_" / "`"
|
||||
* / "{" / "}" / "~"
|
||||
* language = ( 2*3ALPHA [ extlang ] )
|
||||
* / 4ALPHA
|
||||
* / 5*8ALPHA
|
||||
* extlang = *3( "-" 3ALPHA )
|
||||
* value-chars = *( pct-encoded / attr-char )
|
||||
* pct-encoded = "%" HEXDIG HEXDIG
|
||||
* attr-char = ALPHA / DIGIT
|
||||
* / "!" / "#" / "$" / "&" / "+" / "-" / "."
|
||||
* / "^" / "_" / "`" / "|" / "~"
|
||||
* @private
|
||||
*/
|
||||
|
||||
var EXT_VALUE_REGEXP = /^([A-Za-z0-9!#$%&+\-^_`{}~]+)'(?:[A-Za-z]{2,3}(?:-[A-Za-z]{3}){0,3}|[A-Za-z]{4,8}|)'((?:%[0-9A-Fa-f]{2}|[A-Za-z0-9!#$&+.^_`|~-])+)$/
|
||||
|
||||
/**
|
||||
* RegExp for various RFC 6266 grammar
|
||||
*
|
||||
* disposition-type = "inline" | "attachment" | disp-ext-type
|
||||
* disp-ext-type = token
|
||||
* disposition-parm = filename-parm | disp-ext-parm
|
||||
* filename-parm = "filename" "=" value
|
||||
* | "filename*" "=" ext-value
|
||||
* disp-ext-parm = token "=" value
|
||||
* | ext-token "=" ext-value
|
||||
* ext-token = <the characters in token, followed by "*">
|
||||
* @private
|
||||
*/
|
||||
|
||||
var DISPOSITION_TYPE_REGEXP = /^([!#$%&'*+.0-9A-Z^_`a-z|~-]+)[\x09\x20]*(?:$|;)/ // eslint-disable-line no-control-regex
|
||||
|
||||
/**
|
||||
* Create an attachment Content-Disposition header.
|
||||
*
|
||||
* @param {string} [filename]
|
||||
* @param {object} [options]
|
||||
* @param {string} [options.type=attachment]
|
||||
* @param {string|boolean} [options.fallback=true]
|
||||
* @return {string}
|
||||
* @public
|
||||
*/
|
||||
|
||||
function contentDisposition (filename, options) {
|
||||
var opts = options || {}
|
||||
|
||||
// get type
|
||||
var type = opts.type || 'attachment'
|
||||
|
||||
// get parameters
|
||||
var params = createparams(filename, opts.fallback)
|
||||
|
||||
// format into string
|
||||
return format(new ContentDisposition(type, params))
|
||||
}
|
||||
|
||||
/**
|
||||
* Create parameters object from filename and fallback.
|
||||
*
|
||||
* @param {string} [filename]
|
||||
* @param {string|boolean} [fallback=true]
|
||||
* @return {object}
|
||||
* @private
|
||||
*/
|
||||
|
||||
function createparams (filename, fallback) {
|
||||
if (filename === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
var params = {}
|
||||
|
||||
if (typeof filename !== 'string') {
|
||||
throw new TypeError('filename must be a string')
|
||||
}
|
||||
|
||||
// fallback defaults to true
|
||||
if (fallback === undefined) {
|
||||
fallback = true
|
||||
}
|
||||
|
||||
if (typeof fallback !== 'string' && typeof fallback !== 'boolean') {
|
||||
throw new TypeError('fallback must be a string or boolean')
|
||||
}
|
||||
|
||||
if (typeof fallback === 'string' && NON_LATIN1_REGEXP.test(fallback)) {
|
||||
throw new TypeError('fallback must be ISO-8859-1 string')
|
||||
}
|
||||
|
||||
// restrict to file base name
|
||||
var name = basename(filename)
|
||||
|
||||
// determine if name is suitable for quoted string
|
||||
var isQuotedString = TEXT_REGEXP.test(name)
|
||||
|
||||
// generate fallback name
|
||||
var fallbackName = typeof fallback !== 'string'
|
||||
? fallback && getlatin1(name)
|
||||
: basename(fallback)
|
||||
var hasFallback = typeof fallbackName === 'string' && fallbackName !== name
|
||||
|
||||
// set extended filename parameter
|
||||
if (hasFallback || !isQuotedString || hasHexEscape(name)) {
|
||||
params['filename*'] = name
|
||||
}
|
||||
|
||||
// set filename parameter
|
||||
if (isQuotedString || hasFallback) {
|
||||
params.filename = hasFallback
|
||||
? fallbackName
|
||||
: name
|
||||
}
|
||||
|
||||
return params
|
||||
}
|
||||
|
||||
/**
|
||||
* Format object to Content-Disposition header.
|
||||
*
|
||||
* @param {object} obj
|
||||
* @param {string} obj.type
|
||||
* @param {object} [obj.parameters]
|
||||
* @return {string}
|
||||
* @private
|
||||
*/
|
||||
|
||||
function format (obj) {
|
||||
var parameters = obj.parameters
|
||||
var type = obj.type
|
||||
|
||||
if (!type || typeof type !== 'string' || !TOKEN_REGEXP.test(type)) {
|
||||
throw new TypeError('invalid type')
|
||||
}
|
||||
|
||||
// start with normalized type
|
||||
var string = String(type).toLowerCase()
|
||||
|
||||
// append parameters
|
||||
if (parameters && typeof parameters === 'object') {
|
||||
var param
|
||||
var params = Object.keys(parameters).sort()
|
||||
|
||||
for (var i = 0; i < params.length; i++) {
|
||||
param = params[i]
|
||||
|
||||
var val = param.slice(-1) === '*'
|
||||
? ustring(parameters[param])
|
||||
: qstring(parameters[param])
|
||||
|
||||
string += '; ' + param + '=' + val
|
||||
}
|
||||
}
|
||||
|
||||
return string
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode a RFC 5987 field value (gracefully).
|
||||
*
|
||||
* @param {string} str
|
||||
* @return {string}
|
||||
* @private
|
||||
*/
|
||||
|
||||
function decodefield (str) {
|
||||
const match = EXT_VALUE_REGEXP.exec(str)
|
||||
|
||||
if (!match) {
|
||||
throw new TypeError('invalid extended field value')
|
||||
}
|
||||
|
||||
const charset = match[1].toLowerCase()
|
||||
const encoded = match[2]
|
||||
|
||||
switch (charset) {
|
||||
case 'iso-8859-1':
|
||||
{
|
||||
const binary = decodeHexEscapes(encoded)
|
||||
return getlatin1(binary)
|
||||
}
|
||||
case 'utf-8':
|
||||
case 'utf8':
|
||||
{
|
||||
try {
|
||||
return decodeURIComponent(encoded)
|
||||
} catch {
|
||||
// Failed to decode with decodeURIComponent, fallback to lenient decoding which replaces invalid UTF-8 byte sequences with the Unicode replacement character
|
||||
// TODO: Consider removing in the next major version to be more strict about invalid percent-encodings
|
||||
const binary = decodeHexEscapes(encoded)
|
||||
|
||||
const bytes = new Uint8Array(binary.length)
|
||||
for (let idx = 0; idx < binary.length; idx++) {
|
||||
bytes[idx] = binary.charCodeAt(idx)
|
||||
}
|
||||
|
||||
return utf8Decoder.decode(bytes)
|
||||
}
|
||||
}
|
||||
}
|
||||
throw new TypeError('unsupported charset in extended field')
|
||||
}
|
||||
|
||||
/**
|
||||
* Get ISO-8859-1 version of string.
|
||||
*
|
||||
* @param {string} val
|
||||
* @return {string}
|
||||
* @private
|
||||
*/
|
||||
|
||||
function getlatin1 (val) {
|
||||
// simple Unicode -> ISO-8859-1 transformation
|
||||
return String(val).replace(NON_LATIN1_REGEXP, '?')
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse Content-Disposition header string.
|
||||
*
|
||||
* @param {string} string
|
||||
* @return {object}
|
||||
* @public
|
||||
*/
|
||||
|
||||
function parse (string) {
|
||||
if (!string || typeof string !== 'string') {
|
||||
throw new TypeError('argument string is required')
|
||||
}
|
||||
|
||||
var match = DISPOSITION_TYPE_REGEXP.exec(string)
|
||||
|
||||
if (!match) {
|
||||
throw new TypeError('invalid type format')
|
||||
}
|
||||
|
||||
// normalize type
|
||||
var index = match[0].length
|
||||
var type = match[1].toLowerCase()
|
||||
|
||||
var key
|
||||
var names = []
|
||||
var params = {}
|
||||
var value
|
||||
|
||||
// calculate index to start at
|
||||
index = PARAM_REGEXP.lastIndex = match[0].slice(-1) === ';'
|
||||
? index - 1
|
||||
: index
|
||||
|
||||
// match parameters
|
||||
while ((match = PARAM_REGEXP.exec(string))) {
|
||||
if (match.index !== index) {
|
||||
throw new TypeError('invalid parameter format')
|
||||
}
|
||||
|
||||
index += match[0].length
|
||||
key = match[1].toLowerCase()
|
||||
value = match[2]
|
||||
|
||||
if (names.indexOf(key) !== -1) {
|
||||
throw new TypeError('invalid duplicate parameter')
|
||||
}
|
||||
|
||||
names.push(key)
|
||||
|
||||
if (key.indexOf('*') + 1 === key.length) {
|
||||
// decode extended value
|
||||
key = key.slice(0, -1)
|
||||
value = decodefield(value)
|
||||
|
||||
// overwrite existing value
|
||||
params[key] = value
|
||||
continue
|
||||
}
|
||||
|
||||
if (typeof params[key] === 'string') {
|
||||
continue
|
||||
}
|
||||
|
||||
if (value[0] === '"') {
|
||||
// remove quotes and escapes
|
||||
value = value
|
||||
.slice(1, -1)
|
||||
.replace(QESC_REGEXP, '$1')
|
||||
}
|
||||
|
||||
params[key] = value
|
||||
}
|
||||
|
||||
if (index !== -1 && index !== string.length) {
|
||||
throw new TypeError('invalid parameter format')
|
||||
}
|
||||
|
||||
return new ContentDisposition(type, params)
|
||||
}
|
||||
|
||||
/**
|
||||
* Percent encode a single character.
|
||||
*
|
||||
* @param {string} char
|
||||
* @return {string}
|
||||
* @private
|
||||
*/
|
||||
|
||||
function pencode (char) {
|
||||
return '%' + String(char)
|
||||
.charCodeAt(0)
|
||||
.toString(16)
|
||||
.toUpperCase()
|
||||
}
|
||||
|
||||
/**
|
||||
* Quote a string for HTTP.
|
||||
*
|
||||
* @param {string} val
|
||||
* @return {string}
|
||||
* @private
|
||||
*/
|
||||
|
||||
function qstring (val) {
|
||||
var str = String(val)
|
||||
|
||||
return '"' + str.replace(QUOTE_REGEXP, '\\$1') + '"'
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode a Unicode string for HTTP (RFC 5987).
|
||||
*
|
||||
* @param {string} val
|
||||
* @return {string}
|
||||
* @private
|
||||
*/
|
||||
|
||||
function ustring (val) {
|
||||
var str = String(val)
|
||||
|
||||
// percent encode as UTF-8
|
||||
var encoded = encodeURIComponent(str)
|
||||
.replace(ENCODE_URL_ATTR_CHAR_REGEXP, pencode)
|
||||
|
||||
return 'UTF-8\'\'' + encoded
|
||||
}
|
||||
|
||||
/**
|
||||
* Class for parsed Content-Disposition header for v8 optimization
|
||||
*
|
||||
* @public
|
||||
* @param {string} type
|
||||
* @param {object} parameters
|
||||
* @constructor
|
||||
*/
|
||||
|
||||
function ContentDisposition (type, parameters) {
|
||||
this.type = type
|
||||
this.parameters = parameters
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the last portion of a path
|
||||
*
|
||||
* @param {string} path
|
||||
* @returns {string}
|
||||
*/
|
||||
function basename (path) {
|
||||
const normalized = path.replaceAll('\\', '/')
|
||||
|
||||
let end = normalized.length
|
||||
while (end > 0 && normalized[end - 1] === '/') {
|
||||
end--
|
||||
}
|
||||
|
||||
if (end === 0) {
|
||||
return ''
|
||||
}
|
||||
|
||||
let start = end - 1
|
||||
while (start >= 0 && normalized[start] !== '/') {
|
||||
start--
|
||||
}
|
||||
|
||||
return normalized.slice(start + 1, end)
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a character is a hex digit [0-9A-Fa-f]
|
||||
*
|
||||
* @param {string} char
|
||||
* @return {boolean}
|
||||
* @private
|
||||
*/
|
||||
function isHexDigit (char) {
|
||||
const code = char.charCodeAt(0)
|
||||
return (
|
||||
(code >= 48 && code <= 57) || // 0-9
|
||||
(code >= 65 && code <= 70) || // A-F
|
||||
(code >= 97 && code <= 102) // a-f
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a string contains percent encoding escapes.
|
||||
*
|
||||
* @param {string} str
|
||||
* @return {boolean}
|
||||
* @private
|
||||
*/
|
||||
function hasHexEscape (str) {
|
||||
const maxIndex = str.length - 3
|
||||
let lastIndex = -1
|
||||
|
||||
while ((lastIndex = str.indexOf('%', lastIndex + 1)) !== -1 && lastIndex <= maxIndex) {
|
||||
if (isHexDigit(str[lastIndex + 1]) && isHexDigit(str[lastIndex + 2])) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode hex escapes in a string (e.g., %20 -> space)
|
||||
*
|
||||
* @param {string} str
|
||||
* @return {string}
|
||||
* @private
|
||||
*/
|
||||
function decodeHexEscapes (str) {
|
||||
const firstEscape = str.indexOf('%')
|
||||
if (firstEscape === -1) return str
|
||||
|
||||
let result = str.slice(0, firstEscape)
|
||||
for (let idx = firstEscape; idx < str.length; idx++) {
|
||||
if (
|
||||
str[idx] === '%' &&
|
||||
idx + 2 < str.length &&
|
||||
isHexDigit(str[idx + 1]) &&
|
||||
isHexDigit(str[idx + 2])
|
||||
) {
|
||||
result += String.fromCharCode(Number.parseInt(str[idx + 1] + str[idx + 2], 16))
|
||||
idx += 2
|
||||
} else {
|
||||
result += str[idx]
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
{
|
||||
"name": "content-disposition",
|
||||
"description": "Create and parse Content-Disposition header",
|
||||
"version": "1.1.0",
|
||||
"author": "Douglas Christopher Wilson <doug@somethingdoug.com>",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
"content-disposition",
|
||||
"http",
|
||||
"rfc6266",
|
||||
"res"
|
||||
],
|
||||
"repository": "jshttp/content-disposition",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
},
|
||||
"devDependencies": {
|
||||
"c8": "^10.1.2",
|
||||
"eslint": "^8.57.1",
|
||||
"eslint-config-standard": "^14.1.1",
|
||||
"eslint-plugin-import": "^2.32.0",
|
||||
"eslint-plugin-markdown": "^3.0.1",
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"eslint-plugin-promise": "^6.6.0",
|
||||
"eslint-plugin-standard": "^4.1.0"
|
||||
},
|
||||
"files": [
|
||||
"index.js"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"scripts": {
|
||||
"lint": "eslint .",
|
||||
"test": "node --test --test-reporter spec",
|
||||
"test-ci": "c8 --reporter=lcovonly --reporter=text npm test",
|
||||
"test-cov": "c8 --reporter=html --reporter=text npm test"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
1.0.5 / 2023-01-29
|
||||
==================
|
||||
|
||||
* perf: skip value escaping when unnecessary
|
||||
|
||||
1.0.4 / 2017-09-11
|
||||
==================
|
||||
|
||||
* perf: skip parameter parsing when no parameters
|
||||
|
||||
1.0.3 / 2017-09-10
|
||||
==================
|
||||
|
||||
* perf: remove argument reassignment
|
||||
|
||||
1.0.2 / 2016-05-09
|
||||
==================
|
||||
|
||||
* perf: enable strict mode
|
||||
|
||||
1.0.1 / 2015-02-13
|
||||
==================
|
||||
|
||||
* Improve missing `Content-Type` header error message
|
||||
|
||||
1.0.0 / 2015-02-01
|
||||
==================
|
||||
|
||||
* Initial implementation, derived from `media-typer@0.3.0`
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
(The MIT License)
|
||||
|
||||
Copyright (c) 2015 Douglas Christopher Wilson
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
'Software'), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
|
@ -1,94 +0,0 @@
|
|||
# content-type
|
||||
|
||||
[![NPM Version][npm-version-image]][npm-url]
|
||||
[![NPM Downloads][npm-downloads-image]][npm-url]
|
||||
[![Node.js Version][node-image]][node-url]
|
||||
[![Build Status][ci-image]][ci-url]
|
||||
[![Coverage Status][coveralls-image]][coveralls-url]
|
||||
|
||||
Create and parse HTTP Content-Type header according to RFC 7231
|
||||
|
||||
## Installation
|
||||
|
||||
```sh
|
||||
$ npm install content-type
|
||||
```
|
||||
|
||||
## API
|
||||
|
||||
```js
|
||||
var contentType = require('content-type')
|
||||
```
|
||||
|
||||
### contentType.parse(string)
|
||||
|
||||
```js
|
||||
var obj = contentType.parse('image/svg+xml; charset=utf-8')
|
||||
```
|
||||
|
||||
Parse a `Content-Type` header. This will return an object with the following
|
||||
properties (examples are shown for the string `'image/svg+xml; charset=utf-8'`):
|
||||
|
||||
- `type`: The media type (the type and subtype, always lower case).
|
||||
Example: `'image/svg+xml'`
|
||||
|
||||
- `parameters`: An object of the parameters in the media type (name of parameter
|
||||
always lower case). Example: `{charset: 'utf-8'}`
|
||||
|
||||
Throws a `TypeError` if the string is missing or invalid.
|
||||
|
||||
### contentType.parse(req)
|
||||
|
||||
```js
|
||||
var obj = contentType.parse(req)
|
||||
```
|
||||
|
||||
Parse the `Content-Type` header from the given `req`. Short-cut for
|
||||
`contentType.parse(req.headers['content-type'])`.
|
||||
|
||||
Throws a `TypeError` if the `Content-Type` header is missing or invalid.
|
||||
|
||||
### contentType.parse(res)
|
||||
|
||||
```js
|
||||
var obj = contentType.parse(res)
|
||||
```
|
||||
|
||||
Parse the `Content-Type` header set on the given `res`. Short-cut for
|
||||
`contentType.parse(res.getHeader('content-type'))`.
|
||||
|
||||
Throws a `TypeError` if the `Content-Type` header is missing or invalid.
|
||||
|
||||
### contentType.format(obj)
|
||||
|
||||
```js
|
||||
var str = contentType.format({
|
||||
type: 'image/svg+xml',
|
||||
parameters: { charset: 'utf-8' }
|
||||
})
|
||||
```
|
||||
|
||||
Format an object into a `Content-Type` header. This will return a string of the
|
||||
content type for the given object with the following properties (examples are
|
||||
shown that produce the string `'image/svg+xml; charset=utf-8'`):
|
||||
|
||||
- `type`: The media type (will be lower-cased). Example: `'image/svg+xml'`
|
||||
|
||||
- `parameters`: An object of the parameters in the media type (name of the
|
||||
parameter will be lower-cased). Example: `{charset: 'utf-8'}`
|
||||
|
||||
Throws a `TypeError` if the object contains an invalid type or parameter names.
|
||||
|
||||
## License
|
||||
|
||||
[MIT](LICENSE)
|
||||
|
||||
[ci-image]: https://badgen.net/github/checks/jshttp/content-type/master?label=ci
|
||||
[ci-url]: https://github.com/jshttp/content-type/actions/workflows/ci.yml
|
||||
[coveralls-image]: https://badgen.net/coveralls/c/github/jshttp/content-type/master
|
||||
[coveralls-url]: https://coveralls.io/r/jshttp/content-type?branch=master
|
||||
[node-image]: https://badgen.net/npm/node/content-type
|
||||
[node-url]: https://nodejs.org/en/download
|
||||
[npm-downloads-image]: https://badgen.net/npm/dm/content-type
|
||||
[npm-url]: https://npmjs.org/package/content-type
|
||||
[npm-version-image]: https://badgen.net/npm/v/content-type
|
||||
|
|
@ -1,225 +0,0 @@
|
|||
/*!
|
||||
* content-type
|
||||
* Copyright(c) 2015 Douglas Christopher Wilson
|
||||
* MIT Licensed
|
||||
*/
|
||||
|
||||
'use strict'
|
||||
|
||||
/**
|
||||
* RegExp to match *( ";" parameter ) in RFC 7231 sec 3.1.1.1
|
||||
*
|
||||
* parameter = token "=" ( token / quoted-string )
|
||||
* token = 1*tchar
|
||||
* tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*"
|
||||
* / "+" / "-" / "." / "^" / "_" / "`" / "|" / "~"
|
||||
* / DIGIT / ALPHA
|
||||
* ; any VCHAR, except delimiters
|
||||
* quoted-string = DQUOTE *( qdtext / quoted-pair ) DQUOTE
|
||||
* qdtext = HTAB / SP / %x21 / %x23-5B / %x5D-7E / obs-text
|
||||
* obs-text = %x80-FF
|
||||
* quoted-pair = "\" ( HTAB / SP / VCHAR / obs-text )
|
||||
*/
|
||||
var PARAM_REGEXP = /; *([!#$%&'*+.^_`|~0-9A-Za-z-]+) *= *("(?:[\u000b\u0020\u0021\u0023-\u005b\u005d-\u007e\u0080-\u00ff]|\\[\u000b\u0020-\u00ff])*"|[!#$%&'*+.^_`|~0-9A-Za-z-]+) */g // eslint-disable-line no-control-regex
|
||||
var TEXT_REGEXP = /^[\u000b\u0020-\u007e\u0080-\u00ff]+$/ // eslint-disable-line no-control-regex
|
||||
var TOKEN_REGEXP = /^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/
|
||||
|
||||
/**
|
||||
* RegExp to match quoted-pair in RFC 7230 sec 3.2.6
|
||||
*
|
||||
* quoted-pair = "\" ( HTAB / SP / VCHAR / obs-text )
|
||||
* obs-text = %x80-FF
|
||||
*/
|
||||
var QESC_REGEXP = /\\([\u000b\u0020-\u00ff])/g // eslint-disable-line no-control-regex
|
||||
|
||||
/**
|
||||
* RegExp to match chars that must be quoted-pair in RFC 7230 sec 3.2.6
|
||||
*/
|
||||
var QUOTE_REGEXP = /([\\"])/g
|
||||
|
||||
/**
|
||||
* RegExp to match type in RFC 7231 sec 3.1.1.1
|
||||
*
|
||||
* media-type = type "/" subtype
|
||||
* type = token
|
||||
* subtype = token
|
||||
*/
|
||||
var TYPE_REGEXP = /^[!#$%&'*+.^_`|~0-9A-Za-z-]+\/[!#$%&'*+.^_`|~0-9A-Za-z-]+$/
|
||||
|
||||
/**
|
||||
* Module exports.
|
||||
* @public
|
||||
*/
|
||||
|
||||
exports.format = format
|
||||
exports.parse = parse
|
||||
|
||||
/**
|
||||
* Format object to media type.
|
||||
*
|
||||
* @param {object} obj
|
||||
* @return {string}
|
||||
* @public
|
||||
*/
|
||||
|
||||
function format (obj) {
|
||||
if (!obj || typeof obj !== 'object') {
|
||||
throw new TypeError('argument obj is required')
|
||||
}
|
||||
|
||||
var parameters = obj.parameters
|
||||
var type = obj.type
|
||||
|
||||
if (!type || !TYPE_REGEXP.test(type)) {
|
||||
throw new TypeError('invalid type')
|
||||
}
|
||||
|
||||
var string = type
|
||||
|
||||
// append parameters
|
||||
if (parameters && typeof parameters === 'object') {
|
||||
var param
|
||||
var params = Object.keys(parameters).sort()
|
||||
|
||||
for (var i = 0; i < params.length; i++) {
|
||||
param = params[i]
|
||||
|
||||
if (!TOKEN_REGEXP.test(param)) {
|
||||
throw new TypeError('invalid parameter name')
|
||||
}
|
||||
|
||||
string += '; ' + param + '=' + qstring(parameters[param])
|
||||
}
|
||||
}
|
||||
|
||||
return string
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse media type to object.
|
||||
*
|
||||
* @param {string|object} string
|
||||
* @return {Object}
|
||||
* @public
|
||||
*/
|
||||
|
||||
function parse (string) {
|
||||
if (!string) {
|
||||
throw new TypeError('argument string is required')
|
||||
}
|
||||
|
||||
// support req/res-like objects as argument
|
||||
var header = typeof string === 'object'
|
||||
? getcontenttype(string)
|
||||
: string
|
||||
|
||||
if (typeof header !== 'string') {
|
||||
throw new TypeError('argument string is required to be a string')
|
||||
}
|
||||
|
||||
var index = header.indexOf(';')
|
||||
var type = index !== -1
|
||||
? header.slice(0, index).trim()
|
||||
: header.trim()
|
||||
|
||||
if (!TYPE_REGEXP.test(type)) {
|
||||
throw new TypeError('invalid media type')
|
||||
}
|
||||
|
||||
var obj = new ContentType(type.toLowerCase())
|
||||
|
||||
// parse parameters
|
||||
if (index !== -1) {
|
||||
var key
|
||||
var match
|
||||
var value
|
||||
|
||||
PARAM_REGEXP.lastIndex = index
|
||||
|
||||
while ((match = PARAM_REGEXP.exec(header))) {
|
||||
if (match.index !== index) {
|
||||
throw new TypeError('invalid parameter format')
|
||||
}
|
||||
|
||||
index += match[0].length
|
||||
key = match[1].toLowerCase()
|
||||
value = match[2]
|
||||
|
||||
if (value.charCodeAt(0) === 0x22 /* " */) {
|
||||
// remove quotes
|
||||
value = value.slice(1, -1)
|
||||
|
||||
// remove escapes
|
||||
if (value.indexOf('\\') !== -1) {
|
||||
value = value.replace(QESC_REGEXP, '$1')
|
||||
}
|
||||
}
|
||||
|
||||
obj.parameters[key] = value
|
||||
}
|
||||
|
||||
if (index !== header.length) {
|
||||
throw new TypeError('invalid parameter format')
|
||||
}
|
||||
}
|
||||
|
||||
return obj
|
||||
}
|
||||
|
||||
/**
|
||||
* Get content-type from req/res objects.
|
||||
*
|
||||
* @param {object}
|
||||
* @return {Object}
|
||||
* @private
|
||||
*/
|
||||
|
||||
function getcontenttype (obj) {
|
||||
var header
|
||||
|
||||
if (typeof obj.getHeader === 'function') {
|
||||
// res-like
|
||||
header = obj.getHeader('content-type')
|
||||
} else if (typeof obj.headers === 'object') {
|
||||
// req-like
|
||||
header = obj.headers && obj.headers['content-type']
|
||||
}
|
||||
|
||||
if (typeof header !== 'string') {
|
||||
throw new TypeError('content-type header is missing from object')
|
||||
}
|
||||
|
||||
return header
|
||||
}
|
||||
|
||||
/**
|
||||
* Quote a string if necessary.
|
||||
*
|
||||
* @param {string} val
|
||||
* @return {string}
|
||||
* @private
|
||||
*/
|
||||
|
||||
function qstring (val) {
|
||||
var str = String(val)
|
||||
|
||||
// no need to quote tokens
|
||||
if (TOKEN_REGEXP.test(str)) {
|
||||
return str
|
||||
}
|
||||
|
||||
if (str.length > 0 && !TEXT_REGEXP.test(str)) {
|
||||
throw new TypeError('invalid parameter value')
|
||||
}
|
||||
|
||||
return '"' + str.replace(QUOTE_REGEXP, '\\$1') + '"'
|
||||
}
|
||||
|
||||
/**
|
||||
* Class to represent a content type.
|
||||
* @private
|
||||
*/
|
||||
function ContentType (type) {
|
||||
this.parameters = Object.create(null)
|
||||
this.type = type
|
||||
}
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
{
|
||||
"name": "content-type",
|
||||
"description": "Create and parse HTTP Content-Type header",
|
||||
"version": "1.0.5",
|
||||
"author": "Douglas Christopher Wilson <doug@somethingdoug.com>",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
"content-type",
|
||||
"http",
|
||||
"req",
|
||||
"res",
|
||||
"rfc7231"
|
||||
],
|
||||
"repository": "jshttp/content-type",
|
||||
"devDependencies": {
|
||||
"deep-equal": "1.0.1",
|
||||
"eslint": "8.32.0",
|
||||
"eslint-config-standard": "15.0.1",
|
||||
"eslint-plugin-import": "2.27.5",
|
||||
"eslint-plugin-node": "11.1.0",
|
||||
"eslint-plugin-promise": "6.1.1",
|
||||
"eslint-plugin-standard": "4.1.0",
|
||||
"mocha": "10.2.0",
|
||||
"nyc": "15.1.0"
|
||||
},
|
||||
"files": [
|
||||
"LICENSE",
|
||||
"HISTORY.md",
|
||||
"README.md",
|
||||
"index.js"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
},
|
||||
"scripts": {
|
||||
"lint": "eslint .",
|
||||
"test": "mocha --reporter spec --check-leaks --bail test/",
|
||||
"test-ci": "nyc --reporter=lcovonly --reporter=text npm test",
|
||||
"test-cov": "nyc --reporter=html --reporter=text npm test",
|
||||
"version": "node scripts/version-history.js && git add HISTORY.md"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,70 +0,0 @@
|
|||
1.2.2 / 2024-10-29
|
||||
==================
|
||||
|
||||
* various metadata/documentation tweaks (incl. #51)
|
||||
|
||||
|
||||
1.2.1 / 2023-02-27
|
||||
==================
|
||||
|
||||
* update annotations for allowed secret key types (#44, thanks @jyasskin!)
|
||||
|
||||
|
||||
1.2.0 / 2022-02-17
|
||||
==================
|
||||
|
||||
* allow buffer and other node-supported types as key (#33)
|
||||
* be pickier about extra content after signed portion (#40)
|
||||
* some internal code clarity/cleanup improvements (#26)
|
||||
|
||||
|
||||
1.1.0 / 2018-01-18
|
||||
==================
|
||||
|
||||
* switch to built-in `crypto.timingSafeEqual` for validation instead of previous double-hash method (thank you @jodevsa!)
|
||||
|
||||
|
||||
1.0.7 / 2023-04-12
|
||||
==================
|
||||
|
||||
Later release for older node.js versions. See the [v1.0.x branch notes](https://github.com/tj/node-cookie-signature/blob/v1.0.x/History.md#107--2023-04-12).
|
||||
|
||||
|
||||
1.0.6 / 2015-02-03
|
||||
==================
|
||||
|
||||
* use `npm test` instead of `make test` to run tests
|
||||
* clearer assertion messages when checking input
|
||||
|
||||
|
||||
1.0.5 / 2014-09-05
|
||||
==================
|
||||
|
||||
* add license to package.json
|
||||
|
||||
1.0.4 / 2014-06-25
|
||||
==================
|
||||
|
||||
* corrected avoidance of timing attacks (thanks @tenbits!)
|
||||
|
||||
1.0.3 / 2014-01-28
|
||||
==================
|
||||
|
||||
* [incorrect] fix for timing attacks
|
||||
|
||||
1.0.2 / 2014-01-28
|
||||
==================
|
||||
|
||||
* fix missing repository warning
|
||||
* fix typo in test
|
||||
|
||||
1.0.1 / 2013-04-15
|
||||
==================
|
||||
|
||||
* Revert "Changed underlying HMAC algo. to sha512."
|
||||
* Revert "Fix for timing attacks on MAC verification."
|
||||
|
||||
0.0.1 / 2010-01-03
|
||||
==================
|
||||
|
||||
* Initial release
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
(The MIT License)
|
||||
|
||||
Copyright (c) 2012–2024 LearnBoost <tj@learnboost.com> and other contributors;
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
'Software'), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
|
||||
# cookie-signature
|
||||
|
||||
Sign and unsign cookies.
|
||||
|
||||
## Example
|
||||
|
||||
```js
|
||||
var cookie = require('cookie-signature');
|
||||
|
||||
var val = cookie.sign('hello', 'tobiiscool');
|
||||
val.should.equal('hello.DGDUkGlIkCzPz+C0B064FNgHdEjox7ch8tOBGslZ5QI');
|
||||
|
||||
var val = cookie.sign('hello', 'tobiiscool');
|
||||
cookie.unsign(val, 'tobiiscool').should.equal('hello');
|
||||
cookie.unsign(val, 'luna').should.be.false;
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
MIT.
|
||||
|
||||
See LICENSE file for details.
|
||||
|
|
@ -1,47 +0,0 @@
|
|||
/**
|
||||
* Module dependencies.
|
||||
*/
|
||||
|
||||
var crypto = require('crypto');
|
||||
|
||||
/**
|
||||
* Sign the given `val` with `secret`.
|
||||
*
|
||||
* @param {String} val
|
||||
* @param {String|NodeJS.ArrayBufferView|crypto.KeyObject} secret
|
||||
* @return {String}
|
||||
* @api private
|
||||
*/
|
||||
|
||||
exports.sign = function(val, secret){
|
||||
if ('string' != typeof val) throw new TypeError("Cookie value must be provided as a string.");
|
||||
if (null == secret) throw new TypeError("Secret key must be provided.");
|
||||
return val + '.' + crypto
|
||||
.createHmac('sha256', secret)
|
||||
.update(val)
|
||||
.digest('base64')
|
||||
.replace(/\=+$/, '');
|
||||
};
|
||||
|
||||
/**
|
||||
* Unsign and decode the given `input` with `secret`,
|
||||
* returning `false` if the signature is invalid.
|
||||
*
|
||||
* @param {String} input
|
||||
* @param {String|NodeJS.ArrayBufferView|crypto.KeyObject} secret
|
||||
* @return {String|Boolean}
|
||||
* @api private
|
||||
*/
|
||||
|
||||
exports.unsign = function(input, secret){
|
||||
if ('string' != typeof input) throw new TypeError("Signed cookie string must be provided.");
|
||||
if (null == secret) throw new TypeError("Secret key must be provided.");
|
||||
var tentativeValue = input.slice(0, input.lastIndexOf('.')),
|
||||
expectedInput = exports.sign(tentativeValue, secret),
|
||||
expectedBuffer = Buffer.from(expectedInput),
|
||||
inputBuffer = Buffer.from(input);
|
||||
return (
|
||||
expectedBuffer.length === inputBuffer.length &&
|
||||
crypto.timingSafeEqual(expectedBuffer, inputBuffer)
|
||||
) ? tentativeValue : false;
|
||||
};
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
{
|
||||
"name": "cookie-signature",
|
||||
"version": "1.2.2",
|
||||
"main": "index.js",
|
||||
"description": "Sign and unsign cookies",
|
||||
"keywords": ["cookie", "sign", "unsign"],
|
||||
"author": "TJ Holowaychuk <tj@learnboost.com>",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/visionmedia/node-cookie-signature.git"
|
||||
},
|
||||
"dependencies": {},
|
||||
"engines": {
|
||||
"node": ">=6.6.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"mocha": "*",
|
||||
"should": "*"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "mocha --require should --reporter spec"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
(The MIT License)
|
||||
|
||||
Copyright (c) 2012-2014 Roman Shtylman <shtylman@gmail.com>
|
||||
Copyright (c) 2015 Douglas Christopher Wilson <doug@somethingdoug.com>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
'Software'), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
|
|
@ -1,317 +0,0 @@
|
|||
# cookie
|
||||
|
||||
[![NPM Version][npm-version-image]][npm-url]
|
||||
[![NPM Downloads][npm-downloads-image]][npm-url]
|
||||
[![Node.js Version][node-image]][node-url]
|
||||
[![Build Status][ci-image]][ci-url]
|
||||
[![Coverage Status][coveralls-image]][coveralls-url]
|
||||
|
||||
Basic HTTP cookie parser and serializer for HTTP servers.
|
||||
|
||||
## Installation
|
||||
|
||||
This is a [Node.js](https://nodejs.org/en/) module available through the
|
||||
[npm registry](https://www.npmjs.com/). Installation is done using the
|
||||
[`npm install` command](https://docs.npmjs.com/getting-started/installing-npm-packages-locally):
|
||||
|
||||
```sh
|
||||
$ npm install cookie
|
||||
```
|
||||
|
||||
## API
|
||||
|
||||
```js
|
||||
var cookie = require('cookie');
|
||||
```
|
||||
|
||||
### cookie.parse(str, options)
|
||||
|
||||
Parse an HTTP `Cookie` header string and returning an object of all cookie name-value pairs.
|
||||
The `str` argument is the string representing a `Cookie` header value and `options` is an
|
||||
optional object containing additional parsing options.
|
||||
|
||||
```js
|
||||
var cookies = cookie.parse('foo=bar; equation=E%3Dmc%5E2');
|
||||
// { foo: 'bar', equation: 'E=mc^2' }
|
||||
```
|
||||
|
||||
#### Options
|
||||
|
||||
`cookie.parse` accepts these properties in the options object.
|
||||
|
||||
##### decode
|
||||
|
||||
Specifies a function that will be used to decode a cookie's value. Since the value of a cookie
|
||||
has a limited character set (and must be a simple string), this function can be used to decode
|
||||
a previously-encoded cookie value into a JavaScript string or other object.
|
||||
|
||||
The default function is the global `decodeURIComponent`, which will decode any URL-encoded
|
||||
sequences into their byte representations.
|
||||
|
||||
**note** if an error is thrown from this function, the original, non-decoded cookie value will
|
||||
be returned as the cookie's value.
|
||||
|
||||
### cookie.serialize(name, value, options)
|
||||
|
||||
Serialize a cookie name-value pair into a `Set-Cookie` header string. The `name` argument is the
|
||||
name for the cookie, the `value` argument is the value to set the cookie to, and the `options`
|
||||
argument is an optional object containing additional serialization options.
|
||||
|
||||
```js
|
||||
var setCookie = cookie.serialize('foo', 'bar');
|
||||
// foo=bar
|
||||
```
|
||||
|
||||
#### Options
|
||||
|
||||
`cookie.serialize` accepts these properties in the options object.
|
||||
|
||||
##### domain
|
||||
|
||||
Specifies the value for the [`Domain` `Set-Cookie` attribute][rfc-6265-5.2.3]. By default, no
|
||||
domain is set, and most clients will consider the cookie to apply to only the current domain.
|
||||
|
||||
##### encode
|
||||
|
||||
Specifies a function that will be used to encode a cookie's value. Since value of a cookie
|
||||
has a limited character set (and must be a simple string), this function can be used to encode
|
||||
a value into a string suited for a cookie's value.
|
||||
|
||||
The default function is the global `encodeURIComponent`, which will encode a JavaScript string
|
||||
into UTF-8 byte sequences and then URL-encode any that fall outside of the cookie range.
|
||||
|
||||
##### expires
|
||||
|
||||
Specifies the `Date` object to be the value for the [`Expires` `Set-Cookie` attribute][rfc-6265-5.2.1].
|
||||
By default, no expiration is set, and most clients will consider this a "non-persistent cookie" and
|
||||
will delete it on a condition like exiting a web browser application.
|
||||
|
||||
**note** the [cookie storage model specification][rfc-6265-5.3] states that if both `expires` and
|
||||
`maxAge` are set, then `maxAge` takes precedence, but it is possible not all clients by obey this,
|
||||
so if both are set, they should point to the same date and time.
|
||||
|
||||
##### httpOnly
|
||||
|
||||
Specifies the `boolean` value for the [`HttpOnly` `Set-Cookie` attribute][rfc-6265-5.2.6]. When truthy,
|
||||
the `HttpOnly` attribute is set, otherwise it is not. By default, the `HttpOnly` attribute is not set.
|
||||
|
||||
**note** be careful when setting this to `true`, as compliant clients will not allow client-side
|
||||
JavaScript to see the cookie in `document.cookie`.
|
||||
|
||||
##### maxAge
|
||||
|
||||
Specifies the `number` (in seconds) to be the value for the [`Max-Age` `Set-Cookie` attribute][rfc-6265-5.2.2].
|
||||
The given number will be converted to an integer by rounding down. By default, no maximum age is set.
|
||||
|
||||
**note** the [cookie storage model specification][rfc-6265-5.3] states that if both `expires` and
|
||||
`maxAge` are set, then `maxAge` takes precedence, but it is possible not all clients by obey this,
|
||||
so if both are set, they should point to the same date and time.
|
||||
|
||||
##### partitioned
|
||||
|
||||
Specifies the `boolean` value for the [`Partitioned` `Set-Cookie`](rfc-cutler-httpbis-partitioned-cookies)
|
||||
attribute. When truthy, the `Partitioned` attribute is set, otherwise it is not. By default, the
|
||||
`Partitioned` attribute is not set.
|
||||
|
||||
**note** This is an attribute that has not yet been fully standardized, and may change in the future.
|
||||
This also means many clients may ignore this attribute until they understand it.
|
||||
|
||||
More information about can be found in [the proposal](https://github.com/privacycg/CHIPS).
|
||||
|
||||
##### path
|
||||
|
||||
Specifies the value for the [`Path` `Set-Cookie` attribute][rfc-6265-5.2.4]. By default, the path
|
||||
is considered the ["default path"][rfc-6265-5.1.4].
|
||||
|
||||
##### priority
|
||||
|
||||
Specifies the `string` to be the value for the [`Priority` `Set-Cookie` attribute][rfc-west-cookie-priority-00-4.1].
|
||||
|
||||
- `'low'` will set the `Priority` attribute to `Low`.
|
||||
- `'medium'` will set the `Priority` attribute to `Medium`, the default priority when not set.
|
||||
- `'high'` will set the `Priority` attribute to `High`.
|
||||
|
||||
More information about the different priority levels can be found in
|
||||
[the specification][rfc-west-cookie-priority-00-4.1].
|
||||
|
||||
**note** This is an attribute that has not yet been fully standardized, and may change in the future.
|
||||
This also means many clients may ignore this attribute until they understand it.
|
||||
|
||||
##### sameSite
|
||||
|
||||
Specifies the `boolean` or `string` to be the value for the [`SameSite` `Set-Cookie` attribute][rfc-6265bis-09-5.4.7].
|
||||
|
||||
- `true` will set the `SameSite` attribute to `Strict` for strict same site enforcement.
|
||||
- `false` will not set the `SameSite` attribute.
|
||||
- `'lax'` will set the `SameSite` attribute to `Lax` for lax same site enforcement.
|
||||
- `'none'` will set the `SameSite` attribute to `None` for an explicit cross-site cookie.
|
||||
- `'strict'` will set the `SameSite` attribute to `Strict` for strict same site enforcement.
|
||||
|
||||
More information about the different enforcement levels can be found in
|
||||
[the specification][rfc-6265bis-09-5.4.7].
|
||||
|
||||
**note** This is an attribute that has not yet been fully standardized, and may change in the future.
|
||||
This also means many clients may ignore this attribute until they understand it.
|
||||
|
||||
##### secure
|
||||
|
||||
Specifies the `boolean` value for the [`Secure` `Set-Cookie` attribute][rfc-6265-5.2.5]. When truthy,
|
||||
the `Secure` attribute is set, otherwise it is not. By default, the `Secure` attribute is not set.
|
||||
|
||||
**note** be careful when setting this to `true`, as compliant clients will not send the cookie back to
|
||||
the server in the future if the browser does not have an HTTPS connection.
|
||||
|
||||
## Example
|
||||
|
||||
The following example uses this module in conjunction with the Node.js core HTTP server
|
||||
to prompt a user for their name and display it back on future visits.
|
||||
|
||||
```js
|
||||
var cookie = require('cookie');
|
||||
var escapeHtml = require('escape-html');
|
||||
var http = require('http');
|
||||
var url = require('url');
|
||||
|
||||
function onRequest(req, res) {
|
||||
// Parse the query string
|
||||
var query = url.parse(req.url, true, true).query;
|
||||
|
||||
if (query && query.name) {
|
||||
// Set a new cookie with the name
|
||||
res.setHeader('Set-Cookie', cookie.serialize('name', String(query.name), {
|
||||
httpOnly: true,
|
||||
maxAge: 60 * 60 * 24 * 7 // 1 week
|
||||
}));
|
||||
|
||||
// Redirect back after setting cookie
|
||||
res.statusCode = 302;
|
||||
res.setHeader('Location', req.headers.referer || '/');
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse the cookies on the request
|
||||
var cookies = cookie.parse(req.headers.cookie || '');
|
||||
|
||||
// Get the visitor name set in the cookie
|
||||
var name = cookies.name;
|
||||
|
||||
res.setHeader('Content-Type', 'text/html; charset=UTF-8');
|
||||
|
||||
if (name) {
|
||||
res.write('<p>Welcome back, <b>' + escapeHtml(name) + '</b>!</p>');
|
||||
} else {
|
||||
res.write('<p>Hello, new visitor!</p>');
|
||||
}
|
||||
|
||||
res.write('<form method="GET">');
|
||||
res.write('<input placeholder="enter your name" name="name"> <input type="submit" value="Set Name">');
|
||||
res.end('</form>');
|
||||
}
|
||||
|
||||
http.createServer(onRequest).listen(3000);
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
```sh
|
||||
$ npm test
|
||||
```
|
||||
|
||||
## Benchmark
|
||||
|
||||
```
|
||||
$ npm run bench
|
||||
|
||||
> cookie@0.5.0 bench
|
||||
> node benchmark/index.js
|
||||
|
||||
node@18.18.2
|
||||
acorn@8.10.0
|
||||
ada@2.6.0
|
||||
ares@1.19.1
|
||||
brotli@1.0.9
|
||||
cldr@43.1
|
||||
icu@73.2
|
||||
llhttp@6.0.11
|
||||
modules@108
|
||||
napi@9
|
||||
nghttp2@1.57.0
|
||||
nghttp3@0.7.0
|
||||
ngtcp2@0.8.1
|
||||
openssl@3.0.10+quic
|
||||
simdutf@3.2.14
|
||||
tz@2023c
|
||||
undici@5.26.3
|
||||
unicode@15.0
|
||||
uv@1.44.2
|
||||
uvwasi@0.0.18
|
||||
v8@10.2.154.26-node.26
|
||||
zlib@1.2.13.1-motley
|
||||
|
||||
> node benchmark/parse-top.js
|
||||
|
||||
cookie.parse - top sites
|
||||
|
||||
14 tests completed.
|
||||
|
||||
parse accounts.google.com x 2,588,913 ops/sec ±0.74% (186 runs sampled)
|
||||
parse apple.com x 2,370,002 ops/sec ±0.69% (186 runs sampled)
|
||||
parse cloudflare.com x 2,213,102 ops/sec ±0.88% (188 runs sampled)
|
||||
parse docs.google.com x 2,194,157 ops/sec ±1.03% (184 runs sampled)
|
||||
parse drive.google.com x 2,265,084 ops/sec ±0.79% (187 runs sampled)
|
||||
parse en.wikipedia.org x 457,099 ops/sec ±0.81% (186 runs sampled)
|
||||
parse linkedin.com x 504,407 ops/sec ±0.89% (186 runs sampled)
|
||||
parse maps.google.com x 1,230,959 ops/sec ±0.98% (186 runs sampled)
|
||||
parse microsoft.com x 926,294 ops/sec ±0.88% (184 runs sampled)
|
||||
parse play.google.com x 2,311,338 ops/sec ±0.83% (185 runs sampled)
|
||||
parse support.google.com x 1,508,850 ops/sec ±0.86% (186 runs sampled)
|
||||
parse www.google.com x 1,022,582 ops/sec ±1.32% (182 runs sampled)
|
||||
parse youtu.be x 332,136 ops/sec ±1.02% (185 runs sampled)
|
||||
parse youtube.com x 323,833 ops/sec ±0.77% (183 runs sampled)
|
||||
|
||||
> node benchmark/parse.js
|
||||
|
||||
cookie.parse - generic
|
||||
|
||||
6 tests completed.
|
||||
|
||||
simple x 3,214,032 ops/sec ±1.61% (183 runs sampled)
|
||||
decode x 587,237 ops/sec ±1.16% (187 runs sampled)
|
||||
unquote x 2,954,618 ops/sec ±1.35% (183 runs sampled)
|
||||
duplicates x 857,008 ops/sec ±0.89% (187 runs sampled)
|
||||
10 cookies x 292,133 ops/sec ±0.89% (187 runs sampled)
|
||||
100 cookies x 22,610 ops/sec ±0.68% (187 runs sampled)
|
||||
```
|
||||
|
||||
## References
|
||||
|
||||
- [RFC 6265: HTTP State Management Mechanism][rfc-6265]
|
||||
- [Same-site Cookies][rfc-6265bis-09-5.4.7]
|
||||
|
||||
[rfc-cutler-httpbis-partitioned-cookies]: https://tools.ietf.org/html/draft-cutler-httpbis-partitioned-cookies/
|
||||
[rfc-west-cookie-priority-00-4.1]: https://tools.ietf.org/html/draft-west-cookie-priority-00#section-4.1
|
||||
[rfc-6265bis-09-5.4.7]: https://tools.ietf.org/html/draft-ietf-httpbis-rfc6265bis-09#section-5.4.7
|
||||
[rfc-6265]: https://tools.ietf.org/html/rfc6265
|
||||
[rfc-6265-5.1.4]: https://tools.ietf.org/html/rfc6265#section-5.1.4
|
||||
[rfc-6265-5.2.1]: https://tools.ietf.org/html/rfc6265#section-5.2.1
|
||||
[rfc-6265-5.2.2]: https://tools.ietf.org/html/rfc6265#section-5.2.2
|
||||
[rfc-6265-5.2.3]: https://tools.ietf.org/html/rfc6265#section-5.2.3
|
||||
[rfc-6265-5.2.4]: https://tools.ietf.org/html/rfc6265#section-5.2.4
|
||||
[rfc-6265-5.2.5]: https://tools.ietf.org/html/rfc6265#section-5.2.5
|
||||
[rfc-6265-5.2.6]: https://tools.ietf.org/html/rfc6265#section-5.2.6
|
||||
[rfc-6265-5.3]: https://tools.ietf.org/html/rfc6265#section-5.3
|
||||
|
||||
## License
|
||||
|
||||
[MIT](LICENSE)
|
||||
|
||||
[ci-image]: https://badgen.net/github/checks/jshttp/cookie/master?label=ci
|
||||
[ci-url]: https://github.com/jshttp/cookie/actions/workflows/ci.yml
|
||||
[coveralls-image]: https://badgen.net/coveralls/c/github/jshttp/cookie/master
|
||||
[coveralls-url]: https://coveralls.io/r/jshttp/cookie?branch=master
|
||||
[node-image]: https://badgen.net/npm/node/cookie
|
||||
[node-url]: https://nodejs.org/en/download
|
||||
[npm-downloads-image]: https://badgen.net/npm/dm/cookie
|
||||
[npm-url]: https://npmjs.org/package/cookie
|
||||
[npm-version-image]: https://badgen.net/npm/v/cookie
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
# Security Policies and Procedures
|
||||
|
||||
## Reporting a Bug
|
||||
|
||||
The `cookie` team and community take all security bugs seriously. Thank
|
||||
you for improving the security of the project. We appreciate your efforts and
|
||||
responsible disclosure and will make every effort to acknowledge your
|
||||
contributions.
|
||||
|
||||
Report security bugs by emailing the current owner(s) of `cookie`. This
|
||||
information can be found in the npm registry using the command
|
||||
`npm owner ls cookie`.
|
||||
If unsure or unable to get the information from the above, open an issue
|
||||
in the [project issue tracker](https://github.com/jshttp/cookie/issues)
|
||||
asking for the current contact information.
|
||||
|
||||
To ensure the timely response to your report, please ensure that the entirety
|
||||
of the report is contained within the email body and not solely behind a web
|
||||
link or an attachment.
|
||||
|
||||
At least one owner will acknowledge your email within 48 hours, and will send a
|
||||
more detailed response within 48 hours indicating the next steps in handling
|
||||
your report. After the initial reply to your report, the owners will
|
||||
endeavor to keep you informed of the progress towards a fix and full
|
||||
announcement, and may ask for additional information or guidance.
|
||||
|
|
@ -1,335 +0,0 @@
|
|||
/*!
|
||||
* cookie
|
||||
* Copyright(c) 2012-2014 Roman Shtylman
|
||||
* Copyright(c) 2015 Douglas Christopher Wilson
|
||||
* MIT Licensed
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Module exports.
|
||||
* @public
|
||||
*/
|
||||
|
||||
exports.parse = parse;
|
||||
exports.serialize = serialize;
|
||||
|
||||
/**
|
||||
* Module variables.
|
||||
* @private
|
||||
*/
|
||||
|
||||
var __toString = Object.prototype.toString
|
||||
var __hasOwnProperty = Object.prototype.hasOwnProperty
|
||||
|
||||
/**
|
||||
* RegExp to match cookie-name in RFC 6265 sec 4.1.1
|
||||
* This refers out to the obsoleted definition of token in RFC 2616 sec 2.2
|
||||
* which has been replaced by the token definition in RFC 7230 appendix B.
|
||||
*
|
||||
* cookie-name = token
|
||||
* token = 1*tchar
|
||||
* tchar = "!" / "#" / "$" / "%" / "&" / "'" /
|
||||
* "*" / "+" / "-" / "." / "^" / "_" /
|
||||
* "`" / "|" / "~" / DIGIT / ALPHA
|
||||
*/
|
||||
|
||||
var cookieNameRegExp = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
|
||||
|
||||
/**
|
||||
* RegExp to match cookie-value in RFC 6265 sec 4.1.1
|
||||
*
|
||||
* cookie-value = *cookie-octet / ( DQUOTE *cookie-octet DQUOTE )
|
||||
* cookie-octet = %x21 / %x23-2B / %x2D-3A / %x3C-5B / %x5D-7E
|
||||
* ; US-ASCII characters excluding CTLs,
|
||||
* ; whitespace DQUOTE, comma, semicolon,
|
||||
* ; and backslash
|
||||
*/
|
||||
|
||||
var cookieValueRegExp = /^("?)[\u0021\u0023-\u002B\u002D-\u003A\u003C-\u005B\u005D-\u007E]*\1$/;
|
||||
|
||||
/**
|
||||
* RegExp to match domain-value in RFC 6265 sec 4.1.1
|
||||
*
|
||||
* domain-value = <subdomain>
|
||||
* ; defined in [RFC1034], Section 3.5, as
|
||||
* ; enhanced by [RFC1123], Section 2.1
|
||||
* <subdomain> = <label> | <subdomain> "." <label>
|
||||
* <label> = <let-dig> [ [ <ldh-str> ] <let-dig> ]
|
||||
* Labels must be 63 characters or less.
|
||||
* 'let-dig' not 'letter' in the first char, per RFC1123
|
||||
* <ldh-str> = <let-dig-hyp> | <let-dig-hyp> <ldh-str>
|
||||
* <let-dig-hyp> = <let-dig> | "-"
|
||||
* <let-dig> = <letter> | <digit>
|
||||
* <letter> = any one of the 52 alphabetic characters A through Z in
|
||||
* upper case and a through z in lower case
|
||||
* <digit> = any one of the ten digits 0 through 9
|
||||
*
|
||||
* Keep support for leading dot: https://github.com/jshttp/cookie/issues/173
|
||||
*
|
||||
* > (Note that a leading %x2E ("."), if present, is ignored even though that
|
||||
* character is not permitted, but a trailing %x2E ("."), if present, will
|
||||
* cause the user agent to ignore the attribute.)
|
||||
*/
|
||||
|
||||
var domainValueRegExp = /^([.]?[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)([.][a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)*$/i;
|
||||
|
||||
/**
|
||||
* RegExp to match path-value in RFC 6265 sec 4.1.1
|
||||
*
|
||||
* path-value = <any CHAR except CTLs or ";">
|
||||
* CHAR = %x01-7F
|
||||
* ; defined in RFC 5234 appendix B.1
|
||||
*/
|
||||
|
||||
var pathValueRegExp = /^[\u0020-\u003A\u003D-\u007E]*$/;
|
||||
|
||||
/**
|
||||
* Parse a cookie header.
|
||||
*
|
||||
* Parse the given cookie header string into an object
|
||||
* The object has the various cookies as keys(names) => values
|
||||
*
|
||||
* @param {string} str
|
||||
* @param {object} [opt]
|
||||
* @return {object}
|
||||
* @public
|
||||
*/
|
||||
|
||||
function parse(str, opt) {
|
||||
if (typeof str !== 'string') {
|
||||
throw new TypeError('argument str must be a string');
|
||||
}
|
||||
|
||||
var obj = {};
|
||||
var len = str.length;
|
||||
// RFC 6265 sec 4.1.1, RFC 2616 2.2 defines a cookie name consists of one char minimum, plus '='.
|
||||
if (len < 2) return obj;
|
||||
|
||||
var dec = (opt && opt.decode) || decode;
|
||||
var index = 0;
|
||||
var eqIdx = 0;
|
||||
var endIdx = 0;
|
||||
|
||||
do {
|
||||
eqIdx = str.indexOf('=', index);
|
||||
if (eqIdx === -1) break; // No more cookie pairs.
|
||||
|
||||
endIdx = str.indexOf(';', index);
|
||||
|
||||
if (endIdx === -1) {
|
||||
endIdx = len;
|
||||
} else if (eqIdx > endIdx) {
|
||||
// backtrack on prior semicolon
|
||||
index = str.lastIndexOf(';', eqIdx - 1) + 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
var keyStartIdx = startIndex(str, index, eqIdx);
|
||||
var keyEndIdx = endIndex(str, eqIdx, keyStartIdx);
|
||||
var key = str.slice(keyStartIdx, keyEndIdx);
|
||||
|
||||
// only assign once
|
||||
if (!__hasOwnProperty.call(obj, key)) {
|
||||
var valStartIdx = startIndex(str, eqIdx + 1, endIdx);
|
||||
var valEndIdx = endIndex(str, endIdx, valStartIdx);
|
||||
|
||||
if (str.charCodeAt(valStartIdx) === 0x22 /* " */ && str.charCodeAt(valEndIdx - 1) === 0x22 /* " */) {
|
||||
valStartIdx++;
|
||||
valEndIdx--;
|
||||
}
|
||||
|
||||
var val = str.slice(valStartIdx, valEndIdx);
|
||||
obj[key] = tryDecode(val, dec);
|
||||
}
|
||||
|
||||
index = endIdx + 1
|
||||
} while (index < len);
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
function startIndex(str, index, max) {
|
||||
do {
|
||||
var code = str.charCodeAt(index);
|
||||
if (code !== 0x20 /* */ && code !== 0x09 /* \t */) return index;
|
||||
} while (++index < max);
|
||||
return max;
|
||||
}
|
||||
|
||||
function endIndex(str, index, min) {
|
||||
while (index > min) {
|
||||
var code = str.charCodeAt(--index);
|
||||
if (code !== 0x20 /* */ && code !== 0x09 /* \t */) return index + 1;
|
||||
}
|
||||
return min;
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize data into a cookie header.
|
||||
*
|
||||
* Serialize a name value pair into a cookie string suitable for
|
||||
* http headers. An optional options object specifies cookie parameters.
|
||||
*
|
||||
* serialize('foo', 'bar', { httpOnly: true })
|
||||
* => "foo=bar; httpOnly"
|
||||
*
|
||||
* @param {string} name
|
||||
* @param {string} val
|
||||
* @param {object} [opt]
|
||||
* @return {string}
|
||||
* @public
|
||||
*/
|
||||
|
||||
function serialize(name, val, opt) {
|
||||
var enc = (opt && opt.encode) || encodeURIComponent;
|
||||
|
||||
if (typeof enc !== 'function') {
|
||||
throw new TypeError('option encode is invalid');
|
||||
}
|
||||
|
||||
if (!cookieNameRegExp.test(name)) {
|
||||
throw new TypeError('argument name is invalid');
|
||||
}
|
||||
|
||||
var value = enc(val);
|
||||
|
||||
if (!cookieValueRegExp.test(value)) {
|
||||
throw new TypeError('argument val is invalid');
|
||||
}
|
||||
|
||||
var str = name + '=' + value;
|
||||
if (!opt) return str;
|
||||
|
||||
if (null != opt.maxAge) {
|
||||
var maxAge = Math.floor(opt.maxAge);
|
||||
|
||||
if (!isFinite(maxAge)) {
|
||||
throw new TypeError('option maxAge is invalid')
|
||||
}
|
||||
|
||||
str += '; Max-Age=' + maxAge;
|
||||
}
|
||||
|
||||
if (opt.domain) {
|
||||
if (!domainValueRegExp.test(opt.domain)) {
|
||||
throw new TypeError('option domain is invalid');
|
||||
}
|
||||
|
||||
str += '; Domain=' + opt.domain;
|
||||
}
|
||||
|
||||
if (opt.path) {
|
||||
if (!pathValueRegExp.test(opt.path)) {
|
||||
throw new TypeError('option path is invalid');
|
||||
}
|
||||
|
||||
str += '; Path=' + opt.path;
|
||||
}
|
||||
|
||||
if (opt.expires) {
|
||||
var expires = opt.expires
|
||||
|
||||
if (!isDate(expires) || isNaN(expires.valueOf())) {
|
||||
throw new TypeError('option expires is invalid');
|
||||
}
|
||||
|
||||
str += '; Expires=' + expires.toUTCString()
|
||||
}
|
||||
|
||||
if (opt.httpOnly) {
|
||||
str += '; HttpOnly';
|
||||
}
|
||||
|
||||
if (opt.secure) {
|
||||
str += '; Secure';
|
||||
}
|
||||
|
||||
if (opt.partitioned) {
|
||||
str += '; Partitioned'
|
||||
}
|
||||
|
||||
if (opt.priority) {
|
||||
var priority = typeof opt.priority === 'string'
|
||||
? opt.priority.toLowerCase() : opt.priority;
|
||||
|
||||
switch (priority) {
|
||||
case 'low':
|
||||
str += '; Priority=Low'
|
||||
break
|
||||
case 'medium':
|
||||
str += '; Priority=Medium'
|
||||
break
|
||||
case 'high':
|
||||
str += '; Priority=High'
|
||||
break
|
||||
default:
|
||||
throw new TypeError('option priority is invalid')
|
||||
}
|
||||
}
|
||||
|
||||
if (opt.sameSite) {
|
||||
var sameSite = typeof opt.sameSite === 'string'
|
||||
? opt.sameSite.toLowerCase() : opt.sameSite;
|
||||
|
||||
switch (sameSite) {
|
||||
case true:
|
||||
str += '; SameSite=Strict';
|
||||
break;
|
||||
case 'lax':
|
||||
str += '; SameSite=Lax';
|
||||
break;
|
||||
case 'strict':
|
||||
str += '; SameSite=Strict';
|
||||
break;
|
||||
case 'none':
|
||||
str += '; SameSite=None';
|
||||
break;
|
||||
default:
|
||||
throw new TypeError('option sameSite is invalid');
|
||||
}
|
||||
}
|
||||
|
||||
return str;
|
||||
}
|
||||
|
||||
/**
|
||||
* URL-decode string value. Optimized to skip native call when no %.
|
||||
*
|
||||
* @param {string} str
|
||||
* @returns {string}
|
||||
*/
|
||||
|
||||
function decode (str) {
|
||||
return str.indexOf('%') !== -1
|
||||
? decodeURIComponent(str)
|
||||
: str
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if value is a Date.
|
||||
*
|
||||
* @param {*} val
|
||||
* @private
|
||||
*/
|
||||
|
||||
function isDate (val) {
|
||||
return __toString.call(val) === '[object Date]';
|
||||
}
|
||||
|
||||
/**
|
||||
* Try decoding a string using a decoding function.
|
||||
*
|
||||
* @param {string} str
|
||||
* @param {function} decode
|
||||
* @private
|
||||
*/
|
||||
|
||||
function tryDecode(str, decode) {
|
||||
try {
|
||||
return decode(str);
|
||||
} catch (e) {
|
||||
return str;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,44 +0,0 @@
|
|||
{
|
||||
"name": "cookie",
|
||||
"description": "HTTP server cookie parsing and serialization",
|
||||
"version": "0.7.2",
|
||||
"author": "Roman Shtylman <shtylman@gmail.com>",
|
||||
"contributors": [
|
||||
"Douglas Christopher Wilson <doug@somethingdoug.com>"
|
||||
],
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
"cookie",
|
||||
"cookies"
|
||||
],
|
||||
"repository": "jshttp/cookie",
|
||||
"devDependencies": {
|
||||
"beautify-benchmark": "0.2.4",
|
||||
"benchmark": "2.1.4",
|
||||
"eslint": "8.53.0",
|
||||
"eslint-plugin-markdown": "3.0.1",
|
||||
"mocha": "10.2.0",
|
||||
"nyc": "15.1.0",
|
||||
"safe-buffer": "5.2.1",
|
||||
"top-sites": "1.1.194"
|
||||
},
|
||||
"files": [
|
||||
"HISTORY.md",
|
||||
"LICENSE",
|
||||
"README.md",
|
||||
"SECURITY.md",
|
||||
"index.js"
|
||||
],
|
||||
"main": "index.js",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
},
|
||||
"scripts": {
|
||||
"bench": "node benchmark/index.js",
|
||||
"lint": "eslint .",
|
||||
"test": "mocha --reporter spec --bail --check-leaks test/",
|
||||
"test-ci": "nyc --reporter=lcov --reporter=text npm test",
|
||||
"test-cov": "nyc --reporter=html --reporter=text npm test",
|
||||
"update-bench": "node scripts/update-benchmark.js"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
(The MIT License)
|
||||
|
||||
Copyright (c) 2014-2017 TJ Holowaychuk <tj@vision-media.ca>
|
||||
Copyright (c) 2018-2021 Josh Junon
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||
and associated documentation files (the 'Software'), to deal in the Software without restriction,
|
||||
including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial
|
||||
portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
|
||||
LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
|
|
@ -1,481 +0,0 @@
|
|||
# debug
|
||||
[](#backers)
|
||||
[](#sponsors)
|
||||
|
||||
<img width="647" src="https://user-images.githubusercontent.com/71256/29091486-fa38524c-7c37-11e7-895f-e7ec8e1039b6.png">
|
||||
|
||||
A tiny JavaScript debugging utility modelled after Node.js core's debugging
|
||||
technique. Works in Node.js and web browsers.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
$ npm install debug
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
`debug` exposes a function; simply pass this function the name of your module, and it will return a decorated version of `console.error` for you to pass debug statements to. This will allow you to toggle the debug output for different parts of your module as well as the module as a whole.
|
||||
|
||||
Example [_app.js_](./examples/node/app.js):
|
||||
|
||||
```js
|
||||
var debug = require('debug')('http')
|
||||
, http = require('http')
|
||||
, name = 'My App';
|
||||
|
||||
// fake app
|
||||
|
||||
debug('booting %o', name);
|
||||
|
||||
http.createServer(function(req, res){
|
||||
debug(req.method + ' ' + req.url);
|
||||
res.end('hello\n');
|
||||
}).listen(3000, function(){
|
||||
debug('listening');
|
||||
});
|
||||
|
||||
// fake worker of some kind
|
||||
|
||||
require('./worker');
|
||||
```
|
||||
|
||||
Example [_worker.js_](./examples/node/worker.js):
|
||||
|
||||
```js
|
||||
var a = require('debug')('worker:a')
|
||||
, b = require('debug')('worker:b');
|
||||
|
||||
function work() {
|
||||
a('doing lots of uninteresting work');
|
||||
setTimeout(work, Math.random() * 1000);
|
||||
}
|
||||
|
||||
work();
|
||||
|
||||
function workb() {
|
||||
b('doing some work');
|
||||
setTimeout(workb, Math.random() * 2000);
|
||||
}
|
||||
|
||||
workb();
|
||||
```
|
||||
|
||||
The `DEBUG` environment variable is then used to enable these based on space or
|
||||
comma-delimited names.
|
||||
|
||||
Here are some examples:
|
||||
|
||||
<img width="647" alt="screen shot 2017-08-08 at 12 53 04 pm" src="https://user-images.githubusercontent.com/71256/29091703-a6302cdc-7c38-11e7-8304-7c0b3bc600cd.png">
|
||||
<img width="647" alt="screen shot 2017-08-08 at 12 53 38 pm" src="https://user-images.githubusercontent.com/71256/29091700-a62a6888-7c38-11e7-800b-db911291ca2b.png">
|
||||
<img width="647" alt="screen shot 2017-08-08 at 12 53 25 pm" src="https://user-images.githubusercontent.com/71256/29091701-a62ea114-7c38-11e7-826a-2692bedca740.png">
|
||||
|
||||
#### Windows command prompt notes
|
||||
|
||||
##### CMD
|
||||
|
||||
On Windows the environment variable is set using the `set` command.
|
||||
|
||||
```cmd
|
||||
set DEBUG=*,-not_this
|
||||
```
|
||||
|
||||
Example:
|
||||
|
||||
```cmd
|
||||
set DEBUG=* & node app.js
|
||||
```
|
||||
|
||||
##### PowerShell (VS Code default)
|
||||
|
||||
PowerShell uses different syntax to set environment variables.
|
||||
|
||||
```cmd
|
||||
$env:DEBUG = "*,-not_this"
|
||||
```
|
||||
|
||||
Example:
|
||||
|
||||
```cmd
|
||||
$env:DEBUG='app';node app.js
|
||||
```
|
||||
|
||||
Then, run the program to be debugged as usual.
|
||||
|
||||
npm script example:
|
||||
```js
|
||||
"windowsDebug": "@powershell -Command $env:DEBUG='*';node app.js",
|
||||
```
|
||||
|
||||
## Namespace Colors
|
||||
|
||||
Every debug instance has a color generated for it based on its namespace name.
|
||||
This helps when visually parsing the debug output to identify which debug instance
|
||||
a debug line belongs to.
|
||||
|
||||
#### Node.js
|
||||
|
||||
In Node.js, colors are enabled when stderr is a TTY. You also _should_ install
|
||||
the [`supports-color`](https://npmjs.org/supports-color) module alongside debug,
|
||||
otherwise debug will only use a small handful of basic colors.
|
||||
|
||||
<img width="521" src="https://user-images.githubusercontent.com/71256/29092181-47f6a9e6-7c3a-11e7-9a14-1928d8a711cd.png">
|
||||
|
||||
#### Web Browser
|
||||
|
||||
Colors are also enabled on "Web Inspectors" that understand the `%c` formatting
|
||||
option. These are WebKit web inspectors, Firefox ([since version
|
||||
31](https://hacks.mozilla.org/2014/05/editable-box-model-multiple-selection-sublime-text-keys-much-more-firefox-developer-tools-episode-31/))
|
||||
and the Firebug plugin for Firefox (any version).
|
||||
|
||||
<img width="524" src="https://user-images.githubusercontent.com/71256/29092033-b65f9f2e-7c39-11e7-8e32-f6f0d8e865c1.png">
|
||||
|
||||
|
||||
## Millisecond diff
|
||||
|
||||
When actively developing an application it can be useful to see when the time spent between one `debug()` call and the next. Suppose for example you invoke `debug()` before requesting a resource, and after as well, the "+NNNms" will show you how much time was spent between calls.
|
||||
|
||||
<img width="647" src="https://user-images.githubusercontent.com/71256/29091486-fa38524c-7c37-11e7-895f-e7ec8e1039b6.png">
|
||||
|
||||
When stdout is not a TTY, `Date#toISOString()` is used, making it more useful for logging the debug information as shown below:
|
||||
|
||||
<img width="647" src="https://user-images.githubusercontent.com/71256/29091956-6bd78372-7c39-11e7-8c55-c948396d6edd.png">
|
||||
|
||||
|
||||
## Conventions
|
||||
|
||||
If you're using this in one or more of your libraries, you _should_ use the name of your library so that developers may toggle debugging as desired without guessing names. If you have more than one debuggers you _should_ prefix them with your library name and use ":" to separate features. For example "bodyParser" from Connect would then be "connect:bodyParser". If you append a "*" to the end of your name, it will always be enabled regardless of the setting of the DEBUG environment variable. You can then use it for normal output as well as debug output.
|
||||
|
||||
## Wildcards
|
||||
|
||||
The `*` character may be used as a wildcard. Suppose for example your library has
|
||||
debuggers named "connect:bodyParser", "connect:compress", "connect:session",
|
||||
instead of listing all three with
|
||||
`DEBUG=connect:bodyParser,connect:compress,connect:session`, you may simply do
|
||||
`DEBUG=connect:*`, or to run everything using this module simply use `DEBUG=*`.
|
||||
|
||||
You can also exclude specific debuggers by prefixing them with a "-" character.
|
||||
For example, `DEBUG=*,-connect:*` would include all debuggers except those
|
||||
starting with "connect:".
|
||||
|
||||
## Environment Variables
|
||||
|
||||
When running through Node.js, you can set a few environment variables that will
|
||||
change the behavior of the debug logging:
|
||||
|
||||
| Name | Purpose |
|
||||
|-----------|-------------------------------------------------|
|
||||
| `DEBUG` | Enables/disables specific debugging namespaces. |
|
||||
| `DEBUG_HIDE_DATE` | Hide date from debug output (non-TTY). |
|
||||
| `DEBUG_COLORS`| Whether or not to use colors in the debug output. |
|
||||
| `DEBUG_DEPTH` | Object inspection depth. |
|
||||
| `DEBUG_SHOW_HIDDEN` | Shows hidden properties on inspected objects. |
|
||||
|
||||
|
||||
__Note:__ The environment variables beginning with `DEBUG_` end up being
|
||||
converted into an Options object that gets used with `%o`/`%O` formatters.
|
||||
See the Node.js documentation for
|
||||
[`util.inspect()`](https://nodejs.org/api/util.html#util_util_inspect_object_options)
|
||||
for the complete list.
|
||||
|
||||
## Formatters
|
||||
|
||||
Debug uses [printf-style](https://wikipedia.org/wiki/Printf_format_string) formatting.
|
||||
Below are the officially supported formatters:
|
||||
|
||||
| Formatter | Representation |
|
||||
|-----------|----------------|
|
||||
| `%O` | Pretty-print an Object on multiple lines. |
|
||||
| `%o` | Pretty-print an Object all on a single line. |
|
||||
| `%s` | String. |
|
||||
| `%d` | Number (both integer and float). |
|
||||
| `%j` | JSON. Replaced with the string '[Circular]' if the argument contains circular references. |
|
||||
| `%%` | Single percent sign ('%'). This does not consume an argument. |
|
||||
|
||||
|
||||
### Custom formatters
|
||||
|
||||
You can add custom formatters by extending the `debug.formatters` object.
|
||||
For example, if you wanted to add support for rendering a Buffer as hex with
|
||||
`%h`, you could do something like:
|
||||
|
||||
```js
|
||||
const createDebug = require('debug')
|
||||
createDebug.formatters.h = (v) => {
|
||||
return v.toString('hex')
|
||||
}
|
||||
|
||||
// …elsewhere
|
||||
const debug = createDebug('foo')
|
||||
debug('this is hex: %h', new Buffer('hello world'))
|
||||
// foo this is hex: 68656c6c6f20776f726c6421 +0ms
|
||||
```
|
||||
|
||||
|
||||
## Browser Support
|
||||
|
||||
You can build a browser-ready script using [browserify](https://github.com/substack/node-browserify),
|
||||
or just use the [browserify-as-a-service](https://wzrd.in/) [build](https://wzrd.in/standalone/debug@latest),
|
||||
if you don't want to build it yourself.
|
||||
|
||||
Debug's enable state is currently persisted by `localStorage`.
|
||||
Consider the situation shown below where you have `worker:a` and `worker:b`,
|
||||
and wish to debug both. You can enable this using `localStorage.debug`:
|
||||
|
||||
```js
|
||||
localStorage.debug = 'worker:*'
|
||||
```
|
||||
|
||||
And then refresh the page.
|
||||
|
||||
```js
|
||||
a = debug('worker:a');
|
||||
b = debug('worker:b');
|
||||
|
||||
setInterval(function(){
|
||||
a('doing some work');
|
||||
}, 1000);
|
||||
|
||||
setInterval(function(){
|
||||
b('doing some work');
|
||||
}, 1200);
|
||||
```
|
||||
|
||||
In Chromium-based web browsers (e.g. Brave, Chrome, and Electron), the JavaScript console will—by default—only show messages logged by `debug` if the "Verbose" log level is _enabled_.
|
||||
|
||||
<img width="647" src="https://user-images.githubusercontent.com/7143133/152083257-29034707-c42c-4959-8add-3cee850e6fcf.png">
|
||||
|
||||
## Output streams
|
||||
|
||||
By default `debug` will log to stderr, however this can be configured per-namespace by overriding the `log` method:
|
||||
|
||||
Example [_stdout.js_](./examples/node/stdout.js):
|
||||
|
||||
```js
|
||||
var debug = require('debug');
|
||||
var error = debug('app:error');
|
||||
|
||||
// by default stderr is used
|
||||
error('goes to stderr!');
|
||||
|
||||
var log = debug('app:log');
|
||||
// set this namespace to log via console.log
|
||||
log.log = console.log.bind(console); // don't forget to bind to console!
|
||||
log('goes to stdout');
|
||||
error('still goes to stderr!');
|
||||
|
||||
// set all output to go via console.info
|
||||
// overrides all per-namespace log settings
|
||||
debug.log = console.info.bind(console);
|
||||
error('now goes to stdout via console.info');
|
||||
log('still goes to stdout, but via console.info now');
|
||||
```
|
||||
|
||||
## Extend
|
||||
You can simply extend debugger
|
||||
```js
|
||||
const log = require('debug')('auth');
|
||||
|
||||
//creates new debug instance with extended namespace
|
||||
const logSign = log.extend('sign');
|
||||
const logLogin = log.extend('login');
|
||||
|
||||
log('hello'); // auth hello
|
||||
logSign('hello'); //auth:sign hello
|
||||
logLogin('hello'); //auth:login hello
|
||||
```
|
||||
|
||||
## Set dynamically
|
||||
|
||||
You can also enable debug dynamically by calling the `enable()` method :
|
||||
|
||||
```js
|
||||
let debug = require('debug');
|
||||
|
||||
console.log(1, debug.enabled('test'));
|
||||
|
||||
debug.enable('test');
|
||||
console.log(2, debug.enabled('test'));
|
||||
|
||||
debug.disable();
|
||||
console.log(3, debug.enabled('test'));
|
||||
|
||||
```
|
||||
|
||||
print :
|
||||
```
|
||||
1 false
|
||||
2 true
|
||||
3 false
|
||||
```
|
||||
|
||||
Usage :
|
||||
`enable(namespaces)`
|
||||
`namespaces` can include modes separated by a colon and wildcards.
|
||||
|
||||
Note that calling `enable()` completely overrides previously set DEBUG variable :
|
||||
|
||||
```
|
||||
$ DEBUG=foo node -e 'var dbg = require("debug"); dbg.enable("bar"); console.log(dbg.enabled("foo"))'
|
||||
=> false
|
||||
```
|
||||
|
||||
`disable()`
|
||||
|
||||
Will disable all namespaces. The functions returns the namespaces currently
|
||||
enabled (and skipped). This can be useful if you want to disable debugging
|
||||
temporarily without knowing what was enabled to begin with.
|
||||
|
||||
For example:
|
||||
|
||||
```js
|
||||
let debug = require('debug');
|
||||
debug.enable('foo:*,-foo:bar');
|
||||
let namespaces = debug.disable();
|
||||
debug.enable(namespaces);
|
||||
```
|
||||
|
||||
Note: There is no guarantee that the string will be identical to the initial
|
||||
enable string, but semantically they will be identical.
|
||||
|
||||
## Checking whether a debug target is enabled
|
||||
|
||||
After you've created a debug instance, you can determine whether or not it is
|
||||
enabled by checking the `enabled` property:
|
||||
|
||||
```javascript
|
||||
const debug = require('debug')('http');
|
||||
|
||||
if (debug.enabled) {
|
||||
// do stuff...
|
||||
}
|
||||
```
|
||||
|
||||
You can also manually toggle this property to force the debug instance to be
|
||||
enabled or disabled.
|
||||
|
||||
## Usage in child processes
|
||||
|
||||
Due to the way `debug` detects if the output is a TTY or not, colors are not shown in child processes when `stderr` is piped. A solution is to pass the `DEBUG_COLORS=1` environment variable to the child process.
|
||||
For example:
|
||||
|
||||
```javascript
|
||||
worker = fork(WORKER_WRAP_PATH, [workerPath], {
|
||||
stdio: [
|
||||
/* stdin: */ 0,
|
||||
/* stdout: */ 'pipe',
|
||||
/* stderr: */ 'pipe',
|
||||
'ipc',
|
||||
],
|
||||
env: Object.assign({}, process.env, {
|
||||
DEBUG_COLORS: 1 // without this settings, colors won't be shown
|
||||
}),
|
||||
});
|
||||
|
||||
worker.stderr.pipe(process.stderr, { end: false });
|
||||
```
|
||||
|
||||
|
||||
## Authors
|
||||
|
||||
- TJ Holowaychuk
|
||||
- Nathan Rajlich
|
||||
- Andrew Rhyne
|
||||
- Josh Junon
|
||||
|
||||
## Backers
|
||||
|
||||
Support us with a monthly donation and help us continue our activities. [[Become a backer](https://opencollective.com/debug#backer)]
|
||||
|
||||
<a href="https://opencollective.com/debug/backer/0/website" target="_blank"><img src="https://opencollective.com/debug/backer/0/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/debug/backer/1/website" target="_blank"><img src="https://opencollective.com/debug/backer/1/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/debug/backer/2/website" target="_blank"><img src="https://opencollective.com/debug/backer/2/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/debug/backer/3/website" target="_blank"><img src="https://opencollective.com/debug/backer/3/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/debug/backer/4/website" target="_blank"><img src="https://opencollective.com/debug/backer/4/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/debug/backer/5/website" target="_blank"><img src="https://opencollective.com/debug/backer/5/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/debug/backer/6/website" target="_blank"><img src="https://opencollective.com/debug/backer/6/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/debug/backer/7/website" target="_blank"><img src="https://opencollective.com/debug/backer/7/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/debug/backer/8/website" target="_blank"><img src="https://opencollective.com/debug/backer/8/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/debug/backer/9/website" target="_blank"><img src="https://opencollective.com/debug/backer/9/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/debug/backer/10/website" target="_blank"><img src="https://opencollective.com/debug/backer/10/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/debug/backer/11/website" target="_blank"><img src="https://opencollective.com/debug/backer/11/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/debug/backer/12/website" target="_blank"><img src="https://opencollective.com/debug/backer/12/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/debug/backer/13/website" target="_blank"><img src="https://opencollective.com/debug/backer/13/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/debug/backer/14/website" target="_blank"><img src="https://opencollective.com/debug/backer/14/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/debug/backer/15/website" target="_blank"><img src="https://opencollective.com/debug/backer/15/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/debug/backer/16/website" target="_blank"><img src="https://opencollective.com/debug/backer/16/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/debug/backer/17/website" target="_blank"><img src="https://opencollective.com/debug/backer/17/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/debug/backer/18/website" target="_blank"><img src="https://opencollective.com/debug/backer/18/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/debug/backer/19/website" target="_blank"><img src="https://opencollective.com/debug/backer/19/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/debug/backer/20/website" target="_blank"><img src="https://opencollective.com/debug/backer/20/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/debug/backer/21/website" target="_blank"><img src="https://opencollective.com/debug/backer/21/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/debug/backer/22/website" target="_blank"><img src="https://opencollective.com/debug/backer/22/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/debug/backer/23/website" target="_blank"><img src="https://opencollective.com/debug/backer/23/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/debug/backer/24/website" target="_blank"><img src="https://opencollective.com/debug/backer/24/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/debug/backer/25/website" target="_blank"><img src="https://opencollective.com/debug/backer/25/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/debug/backer/26/website" target="_blank"><img src="https://opencollective.com/debug/backer/26/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/debug/backer/27/website" target="_blank"><img src="https://opencollective.com/debug/backer/27/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/debug/backer/28/website" target="_blank"><img src="https://opencollective.com/debug/backer/28/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/debug/backer/29/website" target="_blank"><img src="https://opencollective.com/debug/backer/29/avatar.svg"></a>
|
||||
|
||||
|
||||
## Sponsors
|
||||
|
||||
Become a sponsor and get your logo on our README on Github with a link to your site. [[Become a sponsor](https://opencollective.com/debug#sponsor)]
|
||||
|
||||
<a href="https://opencollective.com/debug/sponsor/0/website" target="_blank"><img src="https://opencollective.com/debug/sponsor/0/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/debug/sponsor/1/website" target="_blank"><img src="https://opencollective.com/debug/sponsor/1/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/debug/sponsor/2/website" target="_blank"><img src="https://opencollective.com/debug/sponsor/2/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/debug/sponsor/3/website" target="_blank"><img src="https://opencollective.com/debug/sponsor/3/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/debug/sponsor/4/website" target="_blank"><img src="https://opencollective.com/debug/sponsor/4/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/debug/sponsor/5/website" target="_blank"><img src="https://opencollective.com/debug/sponsor/5/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/debug/sponsor/6/website" target="_blank"><img src="https://opencollective.com/debug/sponsor/6/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/debug/sponsor/7/website" target="_blank"><img src="https://opencollective.com/debug/sponsor/7/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/debug/sponsor/8/website" target="_blank"><img src="https://opencollective.com/debug/sponsor/8/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/debug/sponsor/9/website" target="_blank"><img src="https://opencollective.com/debug/sponsor/9/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/debug/sponsor/10/website" target="_blank"><img src="https://opencollective.com/debug/sponsor/10/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/debug/sponsor/11/website" target="_blank"><img src="https://opencollective.com/debug/sponsor/11/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/debug/sponsor/12/website" target="_blank"><img src="https://opencollective.com/debug/sponsor/12/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/debug/sponsor/13/website" target="_blank"><img src="https://opencollective.com/debug/sponsor/13/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/debug/sponsor/14/website" target="_blank"><img src="https://opencollective.com/debug/sponsor/14/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/debug/sponsor/15/website" target="_blank"><img src="https://opencollective.com/debug/sponsor/15/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/debug/sponsor/16/website" target="_blank"><img src="https://opencollective.com/debug/sponsor/16/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/debug/sponsor/17/website" target="_blank"><img src="https://opencollective.com/debug/sponsor/17/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/debug/sponsor/18/website" target="_blank"><img src="https://opencollective.com/debug/sponsor/18/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/debug/sponsor/19/website" target="_blank"><img src="https://opencollective.com/debug/sponsor/19/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/debug/sponsor/20/website" target="_blank"><img src="https://opencollective.com/debug/sponsor/20/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/debug/sponsor/21/website" target="_blank"><img src="https://opencollective.com/debug/sponsor/21/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/debug/sponsor/22/website" target="_blank"><img src="https://opencollective.com/debug/sponsor/22/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/debug/sponsor/23/website" target="_blank"><img src="https://opencollective.com/debug/sponsor/23/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/debug/sponsor/24/website" target="_blank"><img src="https://opencollective.com/debug/sponsor/24/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/debug/sponsor/25/website" target="_blank"><img src="https://opencollective.com/debug/sponsor/25/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/debug/sponsor/26/website" target="_blank"><img src="https://opencollective.com/debug/sponsor/26/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/debug/sponsor/27/website" target="_blank"><img src="https://opencollective.com/debug/sponsor/27/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/debug/sponsor/28/website" target="_blank"><img src="https://opencollective.com/debug/sponsor/28/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/debug/sponsor/29/website" target="_blank"><img src="https://opencollective.com/debug/sponsor/29/avatar.svg"></a>
|
||||
|
||||
## License
|
||||
|
||||
(The MIT License)
|
||||
|
||||
Copyright (c) 2014-2017 TJ Holowaychuk <tj@vision-media.ca>
|
||||
Copyright (c) 2018-2021 Josh Junon
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
'Software'), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
|
@ -1,64 +0,0 @@
|
|||
{
|
||||
"name": "debug",
|
||||
"version": "4.4.3",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git://github.com/debug-js/debug.git"
|
||||
},
|
||||
"description": "Lightweight debugging utility for Node.js and the browser",
|
||||
"keywords": [
|
||||
"debug",
|
||||
"log",
|
||||
"debugger"
|
||||
],
|
||||
"files": [
|
||||
"src",
|
||||
"LICENSE",
|
||||
"README.md"
|
||||
],
|
||||
"author": "Josh Junon (https://github.com/qix-)",
|
||||
"contributors": [
|
||||
"TJ Holowaychuk <tj@vision-media.ca>",
|
||||
"Nathan Rajlich <nathan@tootallnate.net> (http://n8.io)",
|
||||
"Andrew Rhyne <rhyneandrew@gmail.com>"
|
||||
],
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"lint": "xo",
|
||||
"test": "npm run test:node && npm run test:browser && npm run lint",
|
||||
"test:node": "mocha test.js test.node.js",
|
||||
"test:browser": "karma start --single-run",
|
||||
"test:coverage": "cat ./coverage/lcov.info | coveralls"
|
||||
},
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"brfs": "^2.0.1",
|
||||
"browserify": "^16.2.3",
|
||||
"coveralls": "^3.0.2",
|
||||
"karma": "^3.1.4",
|
||||
"karma-browserify": "^6.0.0",
|
||||
"karma-chrome-launcher": "^2.2.0",
|
||||
"karma-mocha": "^1.3.0",
|
||||
"mocha": "^5.2.0",
|
||||
"mocha-lcov-reporter": "^1.2.0",
|
||||
"sinon": "^14.0.0",
|
||||
"xo": "^0.23.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"supports-color": {
|
||||
"optional": true
|
||||
}
|
||||
},
|
||||
"main": "./src/index.js",
|
||||
"browser": "./src/browser.js",
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
},
|
||||
"xo": {
|
||||
"rules": {
|
||||
"import/extensions": "off"
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue