mirror of
https://github.com/shuaiplus/nodewarden.git
synced 2026-08-05 14:50:11 +00:00
Compare commits
25
Commits
82f968e51f
...
v1.7.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ce3674669e | ||
|
|
aa7b87e041 | ||
|
|
e4215b4025 | ||
|
|
8d292ca7b8 | ||
|
|
5dd9dff045 | ||
|
|
709a8c1768 | ||
|
|
e2c3516ce9 | ||
|
|
55b5c57f9e | ||
|
|
b6fb62603b | ||
|
|
35071c2719 | ||
|
|
5bd7dab277 | ||
|
|
99f2d7f444 | ||
|
|
fb9a2aeda1 | ||
|
|
c87e6ac984 | ||
|
|
a6f1c6dea2 | ||
|
|
49c872a8ec | ||
|
|
78af1f9bdd | ||
|
|
32b3d2ade1 | ||
|
|
64f26e76f6 | ||
|
|
68c42a0330 | ||
|
|
0d1bb196e2 | ||
|
|
e31f82c0d6 | ||
|
|
f82dcc3c17 | ||
|
|
4378e1b430 | ||
|
|
5eeaf4e32e |
@@ -1,7 +1,7 @@
|
|||||||
blank_issues_enabled: false
|
blank_issues_enabled: false
|
||||||
contact_links:
|
contact_links:
|
||||||
- name: Project Wiki/ 项目文档
|
- name: Project Wiki/ 项目文档
|
||||||
url: https://github.com/shuaiplus/nodewarden/wiki
|
url: https://nodewarden.app
|
||||||
about: |
|
about: |
|
||||||
Please check the documentation for common questions and troubleshooting steps.
|
Please check the documentation for common questions and troubleshooting steps.
|
||||||
请先查看文档,常见问题和排查步骤可能已经覆盖了你的问题。
|
请先查看文档,常见问题和排查步骤可能已经覆盖了你的问题。
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
version: 2
|
||||||
|
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: "npm"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: "weekly"
|
||||||
|
day: "monday"
|
||||||
|
time: "05:00"
|
||||||
|
timezone: "Asia/Shanghai"
|
||||||
|
open-pull-requests-limit: 5
|
||||||
|
groups:
|
||||||
|
npm-minor-and-patch:
|
||||||
|
update-types:
|
||||||
|
- "minor"
|
||||||
|
- "patch"
|
||||||
|
ignore:
|
||||||
|
- dependency-name: "tailwindcss"
|
||||||
|
update-types:
|
||||||
|
- "version-update:semver-major"
|
||||||
|
|
||||||
|
- package-ecosystem: "github-actions"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: "weekly"
|
||||||
|
day: "monday"
|
||||||
|
time: "05:10"
|
||||||
|
timezone: "Asia/Shanghai"
|
||||||
|
open-pull-requests-limit: 0
|
||||||
|
groups:
|
||||||
|
github-actions:
|
||||||
|
patterns:
|
||||||
|
- "*"
|
||||||
@@ -1,467 +0,0 @@
|
|||||||
const fs = require('fs');
|
|
||||||
const path = require('path');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Security Report Generator (Node.js)
|
|
||||||
* Better, faster, and more maintainable than Bash.
|
|
||||||
*/
|
|
||||||
|
|
||||||
class SecurityReport {
|
|
||||||
constructor() {
|
|
||||||
this.results = {
|
|
||||||
codeql: { status: 'PASS', findings: [], alertCount: 0, rulesCount: 0 },
|
|
||||||
snyk: { status: 'PASS', findings: [], vulnCount: 0 },
|
|
||||||
gitleaks: { status: 'PASS', findings: [], leaksCount: 0 },
|
|
||||||
trivy: { status: 'PASS', findings: [], misconfigCount: 0 },
|
|
||||||
coverage: { actions: 0, js: 0, ts: 0 },
|
|
||||||
artifactUris: []
|
|
||||||
};
|
|
||||||
this.auditTime = new Date().toISOString().replace('T', ' ').substring(0, 19) + ' UTC';
|
|
||||||
this.runId = process.env.GITHUB_RUN_ID || '0';
|
|
||||||
this.repository = process.env.GITHUB_REPOSITORY || 'unknown/repo';
|
|
||||||
this.runUrl = `https://github.com/${this.repository}/actions/runs/${this.runId}`;
|
|
||||||
|
|
||||||
this.locales = {
|
|
||||||
zh: {
|
|
||||||
filename: 'security-report-cn.md',
|
|
||||||
switcher: '[English](security-report.md) | 中文',
|
|
||||||
title: '🛡️ 安全审计与透明度报告',
|
|
||||||
grade: '安全评级',
|
|
||||||
important: '> [!IMPORTANT]\n> 本报告由 **GitHub Actions** 自动生成。为确保数据主权的绝对透明度,所有核心模块的安全扫描结果均实时公开。',
|
|
||||||
auditTime: '📅 审计时间',
|
|
||||||
runId: '📝 运行 ID',
|
|
||||||
env: '🛠️ 环境',
|
|
||||||
dashboard: '📉 实时安全仪表盘',
|
|
||||||
tool: '工具',
|
|
||||||
status: '状态',
|
|
||||||
findings: '发现项',
|
|
||||||
leaks: '泄露',
|
|
||||||
vulns: '漏洞',
|
|
||||||
alerts: '告警',
|
|
||||||
coverageTitle: '🔍 扫描覆盖范围',
|
|
||||||
module: '模块',
|
|
||||||
auditedFiles: '已审计文件',
|
|
||||||
coverage: '覆盖率',
|
|
||||||
detailedFindings: '🔍 详细发现项',
|
|
||||||
gitleaksTitle: '🔑 凭据泄露检查 (Gitleaks)',
|
|
||||||
gitleaksDesc: '`检测代码历史记录中硬编码的 API 密钥、密码或其他敏感令牌。`',
|
|
||||||
gitleaksSafe: '✅ **安全**:未发现硬编码的敏感凭据。',
|
|
||||||
gitleaksScope: '`扫描范围:所有代码更改和 Git 历史记录 (Gitleaks 全量扫描)`',
|
|
||||||
snykTitle: '📦 第三方依赖',
|
|
||||||
snykSafe: '✅ **安全**:在依赖项中未发现已知漏洞。',
|
|
||||||
package: '软件包',
|
|
||||||
severity: '严重程度',
|
|
||||||
description: '描述',
|
|
||||||
fixPlan: '修复方案',
|
|
||||||
codeqlTitle: '💻 代码质量与安全 (CodeQL)',
|
|
||||||
codeqlSummary: '#### 摘要',
|
|
||||||
rulesChecked: '已检查规则',
|
|
||||||
totalAlerts: '告警总数',
|
|
||||||
codeqlSafe: '✅ **安全**:CodeQL 扫描清洁,未检测到问题。',
|
|
||||||
ruleId: '规则 ID',
|
|
||||||
level: '级别',
|
|
||||||
location: '位置',
|
|
||||||
auditedList: '📂 已审计文件列表',
|
|
||||||
guideTitle: '⚠️ 操作指南',
|
|
||||||
guideDesc: '如果您看到 **FAIL** 状态或严重的代码问题:',
|
|
||||||
guideStep1: '1. **开发人员**:使用上方表格中的 **位置** 列找到确切的文件和行号。',
|
|
||||||
guideStep2: '2. **纠正**:遵循为每个规则提供的文档链接以提交修复。',
|
|
||||||
guideStep3: '3. **可追溯性**:完整的原始 `.sarif` 数据已附加到此分支。下载并将其导入您的 IDE(例如 VS Code SARIF 查看器)进行本地分析。',
|
|
||||||
footer: '💡 *由 NodeWarden 安全工作流生成。透明度是我们的承诺。*',
|
|
||||||
auditedIcon: '✅ **已审计**',
|
|
||||||
noFiles: '未检索到文件。',
|
|
||||||
trivyTitle: '🛡️ 容器配置安全 (Trivy)',
|
|
||||||
trivyDesc: '`检测 Dockerfile 和容器配置中的安全风险与最佳实践。`',
|
|
||||||
trivySafe: '✅ **安全**:未发现容器配置缺陷。'
|
|
||||||
},
|
|
||||||
en: {
|
|
||||||
filename: 'security-report.md',
|
|
||||||
switcher: 'English | [中文](security-report-cn.md)',
|
|
||||||
title: '🛡️ Security Audit & Transparency Report',
|
|
||||||
grade: 'Security Grade',
|
|
||||||
important: '> [!IMPORTANT]\n> This report is automatically generated by **GitHub Actions**. To ensure absolute transparency of data sovereignty, all core module security scan results are made public in real-time.',
|
|
||||||
auditTime: '📅 Audit Time',
|
|
||||||
runId: '📝 Run ID',
|
|
||||||
env: '🛠️ Environment',
|
|
||||||
dashboard: '📉 Real-time Security Dashboard',
|
|
||||||
tool: 'Tool',
|
|
||||||
status: 'Status',
|
|
||||||
findings: 'Findings',
|
|
||||||
leaks: 'Leaks',
|
|
||||||
vulns: 'Vulns',
|
|
||||||
alerts: 'Alerts',
|
|
||||||
coverageTitle: '🔍 Scan Coverage',
|
|
||||||
module: 'Module',
|
|
||||||
auditedFiles: 'Audited Files',
|
|
||||||
coverage: 'Coverage',
|
|
||||||
detailedFindings: '🔍 Detailed Findings',
|
|
||||||
gitleaksTitle: '🔑 Credential Leak Check (Gitleaks)',
|
|
||||||
gitleaksDesc: '`This section detects hardcoded API Keys, passwords, or other sensitive tokens in the code history.`',
|
|
||||||
gitleaksSafe: '✅ **SAFE**: No hardcoded sensitive credentials found.',
|
|
||||||
gitleaksScope: '`Scan Scope: All code changes and Git history (Gitleaks Full Scan)`',
|
|
||||||
snykTitle: '📦 Third-party Dependencies',
|
|
||||||
snykSafe: '✅ **SAFE**: No known vulnerabilities found in dependencies.',
|
|
||||||
package: 'Package',
|
|
||||||
severity: 'Severity',
|
|
||||||
description: 'Description',
|
|
||||||
fixPlan: 'Fix Plan',
|
|
||||||
codeqlTitle: '💻 Code Quality & Safety (CodeQL)',
|
|
||||||
codeqlSummary: '#### Summary',
|
|
||||||
rulesChecked: 'Rules Checked',
|
|
||||||
totalAlerts: 'Total Alerts',
|
|
||||||
codeqlSafe: '✅ **SAFE**: CodeQL clean. No issues detected.',
|
|
||||||
ruleId: 'Rule ID',
|
|
||||||
level: 'Level',
|
|
||||||
location: 'Location',
|
|
||||||
auditedList: '📂 Audited File List',
|
|
||||||
guideTitle: '⚠️ Action Guide',
|
|
||||||
guideDesc: 'If you see a **FAIL** status or serious code issues:',
|
|
||||||
guideStep1: '1. **Developers**: Use the **Location** column in the tables above to find the exact file and line number.',
|
|
||||||
guideStep2: '2. **Remediate**: Follow the documentation links provided for each rule to submit a fix.',
|
|
||||||
guideStep3: '3. **Traceability**: Full raw `.sarif` data is attached to this branch. Download and import it into your IDE (e.g., VS Code SARIF Viewer) for local analysis.',
|
|
||||||
footer: '💡 *Generated by the NodeWarden security workflow. Transparency is our commitment.*',
|
|
||||||
auditedIcon: '✅ **Audited**',
|
|
||||||
noFiles: 'No files found.',
|
|
||||||
trivyTitle: '🛡️ Container Config Security (Trivy)',
|
|
||||||
trivyDesc: '`This section detects security risks and best practices in Dockerfile and container configurations.`',
|
|
||||||
trivySafe: '✅ **SAFE**: No container configuration defects found.'
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Data Parsers ---
|
|
||||||
|
|
||||||
async parseCodeQL() {
|
|
||||||
const sarifPath = 'sarif-results';
|
|
||||||
if (!fs.existsSync(sarifPath)) return;
|
|
||||||
|
|
||||||
const files = this.globFiles(sarifPath, '.sarif');
|
|
||||||
let totalAlerts = 0;
|
|
||||||
let rulesSet = new Set();
|
|
||||||
let findings = [];
|
|
||||||
let artifactUris = new Set();
|
|
||||||
|
|
||||||
for (const file of files) {
|
|
||||||
const data = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
||||||
for (const run of data.runs || []) {
|
|
||||||
// Collect Rules
|
|
||||||
(run.tool.driver.rules || []).forEach(r => rulesSet.add(r.id));
|
|
||||||
(run.tool.extensions || []).forEach(ext => {
|
|
||||||
(ext.rules || []).forEach(r => rulesSet.add(r.id));
|
|
||||||
});
|
|
||||||
|
|
||||||
// Collect Results
|
|
||||||
for (const res of run.results || []) {
|
|
||||||
totalAlerts++;
|
|
||||||
const loc = (res.locations && res.locations[0]?.physicalLocation) || {};
|
|
||||||
findings.push({
|
|
||||||
id: res.ruleId,
|
|
||||||
level: res.level || 'warning',
|
|
||||||
path: loc.artifactLocation?.uri || 'Global',
|
|
||||||
line: loc.region?.startLine || '-',
|
|
||||||
message: res.message?.text || 'No description'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Track Coverage (Deduplicated)
|
|
||||||
(run.artifacts || []).forEach(art => {
|
|
||||||
const uri = art.location?.uri || '';
|
|
||||||
if (uri) artifactUris.add(uri);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.results.artifactUris = Array.from(artifactUris).sort();
|
|
||||||
this.results.coverage.actions = this.results.artifactUris.filter(u => u.startsWith('.github/workflows/')).length;
|
|
||||||
this.results.coverage.js = this.results.artifactUris.filter(u => u.endsWith('.js')).length;
|
|
||||||
this.results.coverage.ts = this.results.artifactUris.filter(u => u.endsWith('.ts')).length;
|
|
||||||
|
|
||||||
this.results.codeql.alertCount = totalAlerts;
|
|
||||||
this.results.codeql.rulesCount = rulesSet.size;
|
|
||||||
this.results.codeql.findings = findings;
|
|
||||||
if (totalAlerts > 0) this.results.codeql.status = 'INFO';
|
|
||||||
}
|
|
||||||
|
|
||||||
async parseSnyk() {
|
|
||||||
const jsonPath = 'snyk_result.json';
|
|
||||||
if (!fs.existsSync(jsonPath)) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const data = JSON.parse(fs.readFileSync(jsonPath, 'utf8'));
|
|
||||||
const projects = Array.isArray(data) ? data : [data];
|
|
||||||
let vulnTotal = 0;
|
|
||||||
let findings = [];
|
|
||||||
|
|
||||||
for (const proj of projects) {
|
|
||||||
const vulns = proj.vulnerabilities || [];
|
|
||||||
vulnTotal += vulns.length;
|
|
||||||
vulns.forEach(v => {
|
|
||||||
findings.push({
|
|
||||||
pkg: `${v.packageName}@${v.version}`,
|
|
||||||
severity: v.severity,
|
|
||||||
title: v.title,
|
|
||||||
url: v.url,
|
|
||||||
fixedIn: Array.isArray(v.fixedIn) ? v.fixedIn.join(', ') : (v.fixedIn || 'N/A')
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
this.results.snyk.vulnCount = vulnTotal;
|
|
||||||
this.results.snyk.findings = findings;
|
|
||||||
if (vulnTotal > 0) this.results.snyk.status = 'WARN';
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Error parsing Snyk JSON:', e.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async parseGitleaks() {
|
|
||||||
const files = this.globFiles('.', 'results.sarif');
|
|
||||||
if (files.length === 0) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const data = JSON.parse(fs.readFileSync(files[0], 'utf8'));
|
|
||||||
let leaks = 0;
|
|
||||||
let findings = [];
|
|
||||||
for (const run of data.runs || []) {
|
|
||||||
for (const res of run.results || []) {
|
|
||||||
leaks++;
|
|
||||||
findings.push({
|
|
||||||
id: res.ruleId,
|
|
||||||
message: res.message.text,
|
|
||||||
path: res.locations[0]?.physicalLocation?.artifactLocation?.uri || 'Unknown'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.results.gitleaks.leaksCount = leaks;
|
|
||||||
this.results.gitleaks.findings = findings;
|
|
||||||
if (leaks > 0) this.results.gitleaks.status = 'FAIL';
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Error parsing Gitleaks SARIF:', e.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async parseTrivy() {
|
|
||||||
const jsonPath = 'trivy_result.json';
|
|
||||||
if (!fs.existsSync(jsonPath)) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const data = JSON.parse(fs.readFileSync(jsonPath, 'utf8'));
|
|
||||||
let misconfigs = 0;
|
|
||||||
let findings = [];
|
|
||||||
|
|
||||||
(data.Results || []).forEach(res => {
|
|
||||||
(res.Misconfigurations || []).forEach(m => {
|
|
||||||
misconfigs++;
|
|
||||||
findings.push({
|
|
||||||
id: m.ID,
|
|
||||||
severity: m.Severity,
|
|
||||||
title: m.Title,
|
|
||||||
message: m.Message,
|
|
||||||
status: m.Status,
|
|
||||||
target: res.Target
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
this.results.trivy.misconfigCount = misconfigs;
|
|
||||||
this.results.trivy.findings = findings;
|
|
||||||
if (misconfigs > 0) this.results.trivy.status = 'WARN';
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Error parsing Trivy JSON:', e.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
generateTable(type, t) {
|
|
||||||
let files = [];
|
|
||||||
if (type === 'actions') files = this.results.artifactUris.filter(u => u.startsWith('.github/workflows/'));
|
|
||||||
else if (type === 'js') files = this.results.artifactUris.filter(u => u.endsWith('.js'));
|
|
||||||
else if (type === 'ts') files = this.results.artifactUris.filter(u => u.endsWith('.ts'));
|
|
||||||
|
|
||||||
if (files.length === 0) return `> ${t.noFiles}\n`;
|
|
||||||
|
|
||||||
let table = `| ${t.module} | ${t.location} | ${t.status} |\n| :--- | :--- | :--- |\n`;
|
|
||||||
files.forEach(f => {
|
|
||||||
const filename = path.basename(f);
|
|
||||||
table += `| \`${filename}\` | \`${f}\` | ${t.auditedIcon} |\n`;
|
|
||||||
});
|
|
||||||
return table;
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Renderers ---
|
|
||||||
|
|
||||||
generateMarkdown(localeKey) {
|
|
||||||
const { codeql, snyk, gitleaks, coverage } = this.results;
|
|
||||||
const t = this.locales[localeKey];
|
|
||||||
|
|
||||||
// Calculate Grade
|
|
||||||
let grade = 'A+';
|
|
||||||
let gradeColor = 'success';
|
|
||||||
if (gitleaks.status === 'FAIL') { grade = 'D'; gradeColor = 'red'; }
|
|
||||||
else if (snyk.vulnCount > 10 || this.results.trivy.misconfigCount > 5) { grade = 'C'; gradeColor = 'orange'; }
|
|
||||||
else if (snyk.vulnCount > 0 || codeql.alertCount > 0 || this.results.trivy.misconfigCount > 0) { grade = 'B'; gradeColor = 'blue'; }
|
|
||||||
|
|
||||||
const badge = (label, value, color) => `}-${value}-${color}?style=for-the-badge)`;
|
|
||||||
|
|
||||||
let md = `# ${t.title}\n\n`;
|
|
||||||
md += `${t.switcher}\n\n`;
|
|
||||||
md += `${badge(t.grade.replace(/ /g, '_'), grade, gradeColor)}\n\n`;
|
|
||||||
md += `${t.important}\n\n`;
|
|
||||||
|
|
||||||
md += `| ${t.auditTime} | ${t.runId} | ${t.env} |\n`;
|
|
||||||
md += `| :--- | :--- | :--- |\n`;
|
|
||||||
md += `| \`${this.auditTime}\` | [#${this.runId}](${this.runUrl}) | \`GitHub CI/CD\` |\n\n`;
|
|
||||||
|
|
||||||
md += `---\n\n## ${t.dashboard}\n\n`;
|
|
||||||
md += `| ${t.tool} | ${t.status} | ${t.findings} |\n`;
|
|
||||||
md += `| :--- | :--- | :--- |\n`;
|
|
||||||
md += `| **Credential Leak (Gitleaks)** | ${this.getBadge(gitleaks.status)} | \`${gitleaks.leaksCount}\` ${t.leaks} |\n`;
|
|
||||||
md += `| **Dependency Scan (Snyk)** | ${this.getBadge(snyk.status)} | \`${snyk.vulnCount}\` ${t.vulns} |\n`;
|
|
||||||
md += `| **Static Analysis (CodeQL)** | ${this.getBadge(codeql.status)} | \`${codeql.alertCount}\` ${t.alerts} |\n`;
|
|
||||||
md += `| **Container Scan (Trivy)** | ${this.getBadge(this.results.trivy.status)} | \`${this.results.trivy.misconfigCount}\` ${t.findings} |\n\n`;
|
|
||||||
|
|
||||||
md += `---\n\n## ${t.coverageTitle}\n\n`;
|
|
||||||
md += `| ${t.module} | ${t.auditedFiles} | ${t.coverage} |\n`;
|
|
||||||
md += `| :--- | :---: | :---: |\n`;
|
|
||||||
md += `| **GitHub Actions** | \`${coverage.actions}\` | ✨ **100%** |\n`;
|
|
||||||
md += `| **JavaScript (Frontend)** | \`${coverage.js}\` | ✨ **100%** |\n`;
|
|
||||||
md += `| **TypeScript (Backend)** | \`${coverage.ts}\` | ✨ **100%** |\n\n`;
|
|
||||||
|
|
||||||
md += `---\n\n## ${t.detailedFindings}\n\n`;
|
|
||||||
|
|
||||||
// Gitleaks Section
|
|
||||||
md += `### ${t.gitleaksTitle}\n`;
|
|
||||||
md += `${t.gitleaksDesc} ${t.gitleaksScope}\n\n`;
|
|
||||||
if (gitleaks.findings.length > 0) {
|
|
||||||
md += `| ${t.ruleId} | ${t.location} | ${t.description} |\n`;
|
|
||||||
md += `| :--- | :--- | :--- |\n`;
|
|
||||||
gitleaks.findings.forEach(f => {
|
|
||||||
md += `| \`${f.id}\` | \`${f.path}\` | ${f.message} |\n`;
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
md += `${t.gitleaksSafe}\n`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Trivy Section
|
|
||||||
md += `\n### ${t.trivyTitle}\n`;
|
|
||||||
md += `${t.trivyDesc}\n\n`;
|
|
||||||
if (this.results.trivy.findings.length > 0) {
|
|
||||||
md += `| ${t.ruleId} | ${t.severity} | ${t.location} | ${t.description} |\n`;
|
|
||||||
md += `| :--- | :---: | :--- | :--- |\n`;
|
|
||||||
this.results.trivy.findings.forEach(f => {
|
|
||||||
const icon = f.severity === 'CRITICAL' ? '🔴' : (f.severity === 'HIGH' ? '🟠' : '🟡');
|
|
||||||
md += `| \`${f.id}\` | ${icon} ${f.severity} | \`${f.target}\` | ${f.title}: ${f.message} |\n`;
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
md += `${t.trivySafe}\n`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Snyk Section
|
|
||||||
md += `\n### ${t.snykTitle}\n`;
|
|
||||||
if (snyk.findings.length > 0) {
|
|
||||||
md += `| ${t.package} | ${t.severity} | ${t.description} | ${t.fixPlan} |\n`;
|
|
||||||
md += `| :--- | :---: | :--- | :--- |\n`;
|
|
||||||
snyk.findings.forEach(f => {
|
|
||||||
const icon = f.severity === 'critical' ? '🔴' : (f.severity === 'high' ? '🟠' : '🟡');
|
|
||||||
md += `| \`${f.pkg}\` | ${icon} ${f.severity} | [${f.title}](${f.url}) | ${f.fixedIn === 'N/A' ? 'No fix' : `Upgrade to \`${f.fixedIn}\``} |\n`;
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
md += `${t.snykSafe}\n`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// CodeQL Section
|
|
||||||
md += `\n### ${t.codeqlTitle}\n`;
|
|
||||||
if (codeql.findings.length > 0) {
|
|
||||||
md += `${t.codeqlSummary}\n- **${t.rulesChecked}**: \`${codeql.rulesCount}\`\n- **${t.totalAlerts}**: \`${codeql.alertCount}\`\n\n`;
|
|
||||||
md += `| ${t.ruleId} | ${t.level} | ${t.location} | ${t.description} |\n`;
|
|
||||||
md += `| :--- | :---: | :--- | :--- |\n`;
|
|
||||||
codeql.findings.forEach(f => {
|
|
||||||
const icon = f.level === 'error' ? '🔴' : (f.level === 'warning' ? '🟠' : '🔵');
|
|
||||||
const prefix = f.id.split('/')[0];
|
|
||||||
const langMap = {
|
|
||||||
'js': 'javascript',
|
|
||||||
'actions': 'github-actions',
|
|
||||||
'cpp': 'cpp',
|
|
||||||
'cs': 'csharp',
|
|
||||||
'go': 'go',
|
|
||||||
'java': 'java',
|
|
||||||
'py': 'python',
|
|
||||||
'rb': 'ruby',
|
|
||||||
'swift': 'swift'
|
|
||||||
};
|
|
||||||
const langPath = langMap[prefix] || 'javascript';
|
|
||||||
md += `| [${f.id}](https://codeql.github.com/codeql-query-help/${langPath}/${f.id.replace(/\//g, '-')}/) | ${icon} ${f.level} | \`${f.path}:${f.line}\` | ${f.message} |\n`;
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
md += `${t.codeqlSafe}\n`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Audited Files List
|
|
||||||
md += `\n### ${t.auditedList}\n`;
|
|
||||||
md += `<details>\n<summary><b>GitHub Actions (${this.results.coverage.actions})</b></summary>\n\n`;
|
|
||||||
md += this.generateTable('actions', t);
|
|
||||||
md += `\n</details>\n\n`;
|
|
||||||
|
|
||||||
md += `<details>\n<summary><b>JavaScript (${this.results.coverage.js})</b></summary>\n\n`;
|
|
||||||
md += this.generateTable('js', t);
|
|
||||||
md += `\n</details>\n\n`;
|
|
||||||
|
|
||||||
md += `<details>\n<summary><b>TypeScript (${this.results.coverage.ts})</b></summary>\n\n`;
|
|
||||||
md += this.generateTable('ts', t);
|
|
||||||
md += `\n</details>\n\n`;
|
|
||||||
|
|
||||||
// Action Guide
|
|
||||||
md += `--- \n\n## ${t.guideTitle}\n\n`;
|
|
||||||
md += `${t.guideDesc}\n`;
|
|
||||||
md += `${t.guideStep1}\n`;
|
|
||||||
md += `${t.guideStep2}\n`;
|
|
||||||
md += `${t.guideStep3}\n\n`;
|
|
||||||
|
|
||||||
md += `--- \n\n${t.footer}`;
|
|
||||||
|
|
||||||
return md;
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Helpers ---
|
|
||||||
|
|
||||||
getBadge(status) {
|
|
||||||
if (status === 'PASS') return '';
|
|
||||||
if (status === 'WARN' || status === 'INFO') return '';
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
globFiles(dir, ext) {
|
|
||||||
let results = [];
|
|
||||||
const list = fs.readdirSync(dir);
|
|
||||||
for (const file of list) {
|
|
||||||
const fullPath = path.join(dir, file);
|
|
||||||
const stat = fs.statSync(fullPath);
|
|
||||||
if (stat && stat.isDirectory()) {
|
|
||||||
results = results.concat(this.globFiles(fullPath, ext));
|
|
||||||
} else if (file.endsWith(ext)) {
|
|
||||||
results.push(fullPath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return results;
|
|
||||||
}
|
|
||||||
|
|
||||||
async run() {
|
|
||||||
console.log('--- Security Report Generation Started ---');
|
|
||||||
await this.parseCodeQL();
|
|
||||||
await this.parseSnyk();
|
|
||||||
await this.parseGitleaks();
|
|
||||||
await this.parseTrivy();
|
|
||||||
|
|
||||||
for (const localeKey of Object.keys(this.locales)) {
|
|
||||||
const locale = this.locales[localeKey];
|
|
||||||
const markdown = this.generateMarkdown(localeKey);
|
|
||||||
fs.writeFileSync(locale.filename, markdown);
|
|
||||||
console.log(`Report generated successfully at ${locale.filename}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
new SecurityReport().run().catch(err => {
|
|
||||||
console.error('Report generation failed:', err);
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
name: "CodeQL Advanced"
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- "**"
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
actions: read
|
||||||
|
security-events: write
|
||||||
|
packages: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
analyze:
|
||||||
|
name: CodeQL Analyze (${{ matrix.language }})
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- language: actions
|
||||||
|
build-mode: none
|
||||||
|
- language: javascript-typescript
|
||||||
|
build-mode: none
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
|
- name: Initialize CodeQL
|
||||||
|
uses: github/codeql-action/init@411bbbe57033eedfc1a82d68c01345aa96c737d7
|
||||||
|
with:
|
||||||
|
languages: ${{ matrix.language }}
|
||||||
|
build-mode: ${{ matrix.build-mode }}
|
||||||
|
queries: security-extended,security-and-quality
|
||||||
|
|
||||||
|
- name: Perform CodeQL Analysis
|
||||||
|
uses: github/codeql-action/analyze@411bbbe57033eedfc1a82d68c01345aa96c737d7
|
||||||
|
with:
|
||||||
|
category: "/language:${{ matrix.language }}"
|
||||||
@@ -0,0 +1,200 @@
|
|||||||
|
name: "Extra Security Scan"
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- "**"
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
gitleaks:
|
||||||
|
name: Gitleaks Secret Scan
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout full history
|
||||||
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
|
- name: Run Gitleaks
|
||||||
|
uses: gitleaks/gitleaks-action@e0c47f4f8be36e29cdc102c57e68cb5cbf0e8d1e
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
GITLEAKS_ENABLE_SUMMARY: "true"
|
||||||
|
GITLEAKS_ENABLE_UPLOAD_ARTIFACT: "true"
|
||||||
|
# 如果仓库属于 GitHub Organization,需要在 Settings -> Secrets 里加 GITLEAKS_LICENSE
|
||||||
|
# GITLEAKS_LICENSE: ${{ secrets.GITLEAKS_LICENSE }}
|
||||||
|
|
||||||
|
osv:
|
||||||
|
name: OSV Dependency Scan
|
||||||
|
uses: google/osv-scanner-action/.github/workflows/osv-scanner-reusable.yml@9a498708959aeaef5ef730655706c5a1df1edbc2
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
actions: read
|
||||||
|
security-events: write
|
||||||
|
|
||||||
|
with:
|
||||||
|
scan-args: |-
|
||||||
|
--recursive
|
||||||
|
./
|
||||||
|
upload-sarif: true
|
||||||
|
fail-on-vuln: true
|
||||||
|
|
||||||
|
pnpm-audit:
|
||||||
|
name: pnpm audit
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e
|
||||||
|
with:
|
||||||
|
node-version: 22
|
||||||
|
|
||||||
|
- name: Run pnpm audit
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
if [ ! -f pnpm-lock.yaml ]; then
|
||||||
|
echo "pnpm-lock.yaml not found, skip pnpm audit."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
corepack enable
|
||||||
|
corepack prepare pnpm@10 --activate
|
||||||
|
pnpm audit --audit-level=high
|
||||||
|
|
||||||
|
semgrep:
|
||||||
|
name: Semgrep CE Scan
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
security-events: write
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
|
- name: Run Semgrep CE
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
docker run --rm \
|
||||||
|
-v "${PWD}:/src" \
|
||||||
|
-w /src \
|
||||||
|
semgrep/semgrep:latest \
|
||||||
|
semgrep scan --config p/default --sarif --output semgrep.sarif . || true
|
||||||
|
|
||||||
|
if [ ! -f semgrep.sarif ]; then
|
||||||
|
cat > semgrep.sarif <<'EOF'
|
||||||
|
{
|
||||||
|
"version": "2.1.0",
|
||||||
|
"$schema": "https://json.schemastore.org/sarif-2.1.0.json",
|
||||||
|
"runs": [
|
||||||
|
{
|
||||||
|
"tool": {
|
||||||
|
"driver": {
|
||||||
|
"name": "Semgrep",
|
||||||
|
"informationUri": "https://semgrep.dev",
|
||||||
|
"rules": []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"results": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Upload Semgrep SARIF
|
||||||
|
uses: github/codeql-action/upload-sarif@411bbbe57033eedfc1a82d68c01345aa96c737d7
|
||||||
|
with:
|
||||||
|
sarif_file: semgrep.sarif
|
||||||
|
category: semgrep
|
||||||
|
|
||||||
|
actionlint:
|
||||||
|
name: GitHub Actions Syntax Scan
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
|
- name: Run actionlint
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
docker run --rm \
|
||||||
|
-v "${PWD}:/repo" \
|
||||||
|
-w /repo \
|
||||||
|
rhysd/actionlint:latest
|
||||||
|
|
||||||
|
zizmor:
|
||||||
|
name: GitHub Actions Security Scan
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
actions: read
|
||||||
|
security-events: write
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
|
- name: Run zizmor
|
||||||
|
uses: zizmorcore/zizmor-action@192e21d79ab29983730a13d1382995c2307fbcaa
|
||||||
|
with:
|
||||||
|
persona: auditor
|
||||||
|
min-severity: medium
|
||||||
|
min-confidence: medium
|
||||||
|
|
||||||
|
scorecard:
|
||||||
|
name: OpenSSF Scorecard
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: github.ref == 'refs/heads/main'
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
security-events: write
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
|
- name: Run OpenSSF Scorecard
|
||||||
|
uses: ossf/scorecard-action@99c09fe975337306107572b4fdf4db224cf8e2f2
|
||||||
|
with:
|
||||||
|
results_file: scorecard.sarif
|
||||||
|
results_format: sarif
|
||||||
|
publish_results: false
|
||||||
|
|
||||||
|
- name: Upload Scorecard SARIF
|
||||||
|
uses: github/codeql-action/upload-sarif@411bbbe57033eedfc1a82d68c01345aa96c737d7
|
||||||
|
with:
|
||||||
|
sarif_file: scorecard.sarif
|
||||||
|
category: openssf-scorecard
|
||||||
@@ -1,142 +0,0 @@
|
|||||||
name: Security Scan
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
pull_request:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
scan:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
security-events: write
|
|
||||||
actions: read
|
|
||||||
env:
|
|
||||||
SECURITY_SNYK_TOKEN: ${{ secrets.SECURITY_SNYK_TOKEN }}
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v5
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Initialize CodeQL
|
|
||||||
if: env.ACT != 'true'
|
|
||||||
continue-on-error: true
|
|
||||||
uses: github/codeql-action/init@v4
|
|
||||||
with:
|
|
||||||
languages: javascript-typescript, actions
|
|
||||||
build-mode: none
|
|
||||||
queries: security-extended,security-and-quality
|
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
|
||||||
if: env.ACT != 'true'
|
|
||||||
continue-on-error: true
|
|
||||||
uses: github/codeql-action/analyze@v4
|
|
||||||
with:
|
|
||||||
upload: true
|
|
||||||
output: sarif-results
|
|
||||||
|
|
||||||
- name: Install Gitleaks
|
|
||||||
if: env.ACT != 'true'
|
|
||||||
continue-on-error: true
|
|
||||||
run: |
|
|
||||||
GITLEAKS_VERSION="8.28.0"
|
|
||||||
curl -sSL -o gitleaks.tar.gz "https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz"
|
|
||||||
tar -xzf gitleaks.tar.gz gitleaks
|
|
||||||
chmod +x gitleaks
|
|
||||||
sudo mv gitleaks /usr/local/bin/gitleaks
|
|
||||||
|
|
||||||
- name: Secret Detection
|
|
||||||
if: env.ACT != 'true'
|
|
||||||
continue-on-error: true
|
|
||||||
run: |
|
|
||||||
gitleaks git . --report-format sarif --report-path results.sarif --no-banner || true
|
|
||||||
|
|
||||||
- name: Install Project Dependencies
|
|
||||||
if: env.SECURITY_SNYK_TOKEN != ''
|
|
||||||
env:
|
|
||||||
SECURITY_PACKAGE: ${{ vars.SECURITY_PACKAGE || '' }}
|
|
||||||
run: |
|
|
||||||
echo "Preparing dependency lock files for security scanning..."
|
|
||||||
if [ -z "$SECURITY_PACKAGE" ]; then
|
|
||||||
echo "SECURITY_PACKAGE is empty, installing in root..."
|
|
||||||
npm install --package-lock-only
|
|
||||||
else
|
|
||||||
echo "SECURITY_PACKAGE is set to: $SECURITY_PACKAGE"
|
|
||||||
# Split by comma and install
|
|
||||||
IFS=',' read -ra PACKAGES <<< "$SECURITY_PACKAGE"
|
|
||||||
for pkg in "${PACKAGES[@]}"; do
|
|
||||||
if [ -d "$pkg" ]; then
|
|
||||||
echo "Installing in "$pkg"..."
|
|
||||||
npm install --prefix "$pkg" --package-lock-only
|
|
||||||
else
|
|
||||||
echo "Warning: Directory $pkg not found, skipping."
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Dependency Scan
|
|
||||||
id: snyk
|
|
||||||
if: env.SECURITY_SNYK_TOKEN != ''
|
|
||||||
continue-on-error: true
|
|
||||||
run: |
|
|
||||||
npm install -g snyk
|
|
||||||
snyk auth ${{ secrets.SECURITY_SNYK_TOKEN }}
|
|
||||||
snyk test --all-projects --json-file-output=snyk_result.json > snyk_result.txt || true
|
|
||||||
env:
|
|
||||||
SECURITY_SNYK_TOKEN: ${{ secrets.SECURITY_SNYK_TOKEN }}
|
|
||||||
|
|
||||||
- name: Check for Dockerfile
|
|
||||||
id: check_docker
|
|
||||||
run: |
|
|
||||||
if [ -f "Dockerfile" ]; then
|
|
||||||
echo "exists=true" >> $GITHUB_OUTPUT
|
|
||||||
else
|
|
||||||
echo "exists=false" >> $GITHUB_OUTPUT
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Container Security Scan (Trivy)
|
|
||||||
if: steps.check_docker.outputs.exists == 'true'
|
|
||||||
continue-on-error: true
|
|
||||||
run: |
|
|
||||||
VERSION="0.56.1"
|
|
||||||
echo "Installing Trivy $VERSION..."
|
|
||||||
curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local/bin "v$VERSION"
|
|
||||||
trivy config . --format json --output trivy_result.json --severity CRITICAL,HIGH || true
|
|
||||||
|
|
||||||
- name: Generate Security Report
|
|
||||||
run: |
|
|
||||||
# Gitleaks typically produces results.sarif if configured or by default in some versions
|
|
||||||
# We'll ensure it exists for our reporter
|
|
||||||
node .github/scripts/security.cjs
|
|
||||||
|
|
||||||
# Also append to step summary for immediate visibility in GHA UI
|
|
||||||
cat security-report.md >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo -e "\n---\n" >> $GITHUB_STEP_SUMMARY
|
|
||||||
cat security-report-cn.md >> $GITHUB_STEP_SUMMARY
|
|
||||||
|
|
||||||
- name: Upload Gitleaks Results to GitHub Security
|
|
||||||
uses: github/codeql-action/upload-sarif@v4
|
|
||||||
if: always()
|
|
||||||
with:
|
|
||||||
sarif_file: results.sarif
|
|
||||||
category: gitleaks
|
|
||||||
|
|
||||||
- name: Upload Security Report Artifacts
|
|
||||||
if: always()
|
|
||||||
uses: actions/upload-artifact@v6
|
|
||||||
with:
|
|
||||||
name: security-report
|
|
||||||
if-no-files-found: ignore
|
|
||||||
path: |
|
|
||||||
security-report.md
|
|
||||||
security-report-cn.md
|
|
||||||
snyk_result.txt
|
|
||||||
snyk_result.json
|
|
||||||
trivy_result.json
|
|
||||||
results.sarif
|
|
||||||
sarif-results/*.sarif
|
|
||||||
@@ -19,9 +19,9 @@ jobs:
|
|||||||
sync-global-domains:
|
sync-global-domains:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
|
||||||
|
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e
|
||||||
with:
|
with:
|
||||||
node-version: 22
|
node-version: 22
|
||||||
|
|
||||||
@@ -32,7 +32,7 @@ jobs:
|
|||||||
run: git diff --exit-code -- src/static/global_domains.custom.json
|
run: git diff --exit-code -- src/static/global_domains.custom.json
|
||||||
|
|
||||||
- name: Create pull request
|
- name: Create pull request
|
||||||
uses: peter-evans/create-pull-request@v6
|
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1
|
||||||
with:
|
with:
|
||||||
branch: chore/sync-bitwarden-global-domains
|
branch: chore/sync-bitwarden-global-domains
|
||||||
delete-branch: true
|
delete-branch: true
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ jobs:
|
|||||||
sync:
|
sync:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
@@ -49,9 +49,11 @@ jobs:
|
|||||||
echo "Tag '$LATEST_TAG' not found after fetch."
|
echo "Tag '$LATEST_TAG' not found after fetch."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
echo "mode=auto" >> $GITHUB_OUTPUT
|
{
|
||||||
echo "latest_tag=$LATEST_TAG" >> $GITHUB_OUTPUT
|
echo "mode=auto"
|
||||||
echo "target_sha=$TARGET_SHA" >> $GITHUB_OUTPUT
|
echo "latest_tag=$LATEST_TAG"
|
||||||
|
echo "target_sha=$TARGET_SHA"
|
||||||
|
} >> "$GITHUB_OUTPUT"
|
||||||
echo "Auto mode — latest release: $LATEST_TAG ($TARGET_SHA)"
|
echo "Auto mode — latest release: $LATEST_TAG ($TARGET_SHA)"
|
||||||
|
|
||||||
elif [ -n "$MANUAL_INPUT" ]; then
|
elif [ -n "$MANUAL_INPUT" ]; then
|
||||||
@@ -61,15 +63,19 @@ jobs:
|
|||||||
echo "Cannot resolve '$MANUAL_INPUT' to a commit."
|
echo "Cannot resolve '$MANUAL_INPUT' to a commit."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
echo "mode=manual" >> $GITHUB_OUTPUT
|
{
|
||||||
echo "target_sha=$TARGET_SHA" >> $GITHUB_OUTPUT
|
echo "mode=manual"
|
||||||
|
echo "target_sha=$TARGET_SHA"
|
||||||
|
} >> "$GITHUB_OUTPUT"
|
||||||
echo "Manual mode — target: $MANUAL_INPUT ($TARGET_SHA)"
|
echo "Manual mode — target: $MANUAL_INPUT ($TARGET_SHA)"
|
||||||
|
|
||||||
else
|
else
|
||||||
# Manual mode, blank input: use latest commit on upstream/main
|
# Manual mode, blank input: use latest commit on upstream/main
|
||||||
TARGET_SHA=$(git rev-parse upstream/main)
|
TARGET_SHA=$(git rev-parse upstream/main)
|
||||||
echo "mode=manual" >> $GITHUB_OUTPUT
|
{
|
||||||
echo "target_sha=$TARGET_SHA" >> $GITHUB_OUTPUT
|
echo "mode=manual"
|
||||||
|
echo "target_sha=$TARGET_SHA"
|
||||||
|
} >> "$GITHUB_OUTPUT"
|
||||||
echo "Manual mode — latest commit: $TARGET_SHA"
|
echo "Manual mode — latest commit: $TARGET_SHA"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -84,19 +90,19 @@ jobs:
|
|||||||
CURRENT_SHA=$(git rev-parse HEAD)
|
CURRENT_SHA=$(git rev-parse HEAD)
|
||||||
if [ "$CURRENT_SHA" = "$TARGET_SHA" ]; then
|
if [ "$CURRENT_SHA" = "$TARGET_SHA" ]; then
|
||||||
echo "Already at $TARGET_SHA — skipping."
|
echo "Already at $TARGET_SHA — skipping."
|
||||||
echo "needs_update=false" >> $GITHUB_OUTPUT
|
echo "needs_update=false" >> "$GITHUB_OUTPUT"
|
||||||
else
|
else
|
||||||
echo "Switching to $TARGET_SHA"
|
echo "Switching to $TARGET_SHA"
|
||||||
echo "needs_update=true" >> $GITHUB_OUTPUT
|
echo "needs_update=true" >> "$GITHUB_OUTPUT"
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
# Auto: skip if target is already in ancestry
|
# Auto: skip if target is already in ancestry
|
||||||
if git merge-base --is-ancestor "$TARGET_SHA" HEAD 2>/dev/null; then
|
if git merge-base --is-ancestor "$TARGET_SHA" HEAD 2>/dev/null; then
|
||||||
echo "Already up to date with $TARGET_SHA — skipping."
|
echo "Already up to date with $TARGET_SHA — skipping."
|
||||||
echo "needs_update=false" >> $GITHUB_OUTPUT
|
echo "needs_update=false" >> "$GITHUB_OUTPUT"
|
||||||
else
|
else
|
||||||
echo "Update needed — target: $TARGET_SHA"
|
echo "Update needed — target: $TARGET_SHA"
|
||||||
echo "needs_update=true" >> $GITHUB_OUTPUT
|
echo "needs_update=true" >> "$GITHUB_OUTPUT"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -117,7 +123,7 @@ jobs:
|
|||||||
if: steps.check.outputs.needs_update == 'true'
|
if: steps.check.outputs.needs_update == 'true'
|
||||||
run: |
|
run: |
|
||||||
# Always keep our own workflow file, never let upstream overwrite it
|
# Always keep our own workflow file, never let upstream overwrite it
|
||||||
git checkout HEAD@{1} -- .github/workflows/sync-upstream.yml 2>/dev/null || true
|
git checkout 'HEAD@{1}' -- .github/workflows/sync-upstream.yml 2>/dev/null || true
|
||||||
if ! git diff --cached --quiet; then
|
if ! git diff --cached --quiet; then
|
||||||
git commit -m "chore: restore sync-upstream workflow after sync"
|
git commit -m "chore: restore sync-upstream workflow after sync"
|
||||||
fi
|
fi
|
||||||
@@ -134,10 +140,12 @@ jobs:
|
|||||||
- name: Summary
|
- name: Summary
|
||||||
run: |
|
run: |
|
||||||
if [ "${{ steps.check.outputs.needs_update }}" = "true" ]; then
|
if [ "${{ steps.check.outputs.needs_update }}" = "true" ]; then
|
||||||
echo "### Synced successfully" >> $GITHUB_STEP_SUMMARY
|
{
|
||||||
echo "- **Mode:** ${{ steps.resolve.outputs.mode }}" >> $GITHUB_STEP_SUMMARY
|
echo "### Synced successfully"
|
||||||
echo "- **Tag:** ${{ steps.resolve.outputs.latest_tag || 'N/A (manual)' }}" >> $GITHUB_STEP_SUMMARY
|
echo "- **Mode:** ${{ steps.resolve.outputs.mode }}"
|
||||||
echo "- **Commit:** \`${{ steps.resolve.outputs.target_sha }}\`" >> $GITHUB_STEP_SUMMARY
|
echo "- **Tag:** ${{ steps.resolve.outputs.latest_tag || 'N/A (manual)' }}"
|
||||||
|
echo "- **Commit:** \`${{ steps.resolve.outputs.target_sha }}\`"
|
||||||
|
} >> "$GITHUB_STEP_SUMMARY"
|
||||||
else
|
else
|
||||||
echo "### Nothing to update" >> $GITHUB_STEP_SUMMARY
|
echo "### Nothing to update" >> "$GITHUB_STEP_SUMMARY"
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ NodeWarden-compat/
|
|||||||
.codex-upstream/bitwarden-browser/
|
.codex-upstream/bitwarden-browser/
|
||||||
|
|
||||||
.reasonix/
|
.reasonix/
|
||||||
|
.upstream/
|
||||||
|
|
||||||
# Compatibility analysis documents
|
# Compatibility analysis documents
|
||||||
BITWARDEN_COMPATIBILITY_ANALYSIS.md
|
BITWARDEN_COMPATIBILITY_ANALYSIS.md
|
||||||
|
|||||||
Generated
+197
-185
@@ -1,36 +1,36 @@
|
|||||||
{
|
{
|
||||||
"name": "nodewarden",
|
"name": "nodewarden",
|
||||||
"version": "1.7.1",
|
"version": "1.7.2",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "nodewarden",
|
"name": "nodewarden",
|
||||||
"version": "1.7.1",
|
"version": "1.7.2",
|
||||||
"license": "LGPL-3.0",
|
"license": "LGPL-3.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@noble/hashes": "^2.0.1",
|
"@noble/hashes": "^2.2.0",
|
||||||
"@simplewebauthn/server": "^13.3.1",
|
"@simplewebauthn/server": "^13.3.2",
|
||||||
"@tanstack/react-query": "^5.90.21",
|
"@tanstack/react-query": "^5.101.2",
|
||||||
"@zip.js/zip.js": "^2.8.22",
|
"@zip.js/zip.js": "^2.8.26",
|
||||||
"fflate": "^0.8.2",
|
"fflate": "^0.8.3",
|
||||||
"lucide-preact": "^0.575.0",
|
"lucide-preact": "^1.22.0",
|
||||||
"preact": "^10.28.4",
|
"preact": "^10.29.3",
|
||||||
"qrcode-generator": "^2.0.4",
|
"qrcode-generator": "^2.0.4",
|
||||||
"wouter": "^3.9.0"
|
"wouter": "^3.10.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@cloudflare/workers-types": "^4.20260131.0",
|
"@cloudflare/workers-types": "^4.20260630.1",
|
||||||
"@preact/preset-vite": "^2.10.3",
|
"@preact/preset-vite": "^2.10.5",
|
||||||
"@types/node": "^25.2.3",
|
"@types/node": "^26.0.1",
|
||||||
"autoprefixer": "^10.4.21",
|
"autoprefixer": "^10.5.2",
|
||||||
"opencc-js": "^1.0.5",
|
"opencc-js": "^1.3.2",
|
||||||
"postcss": "^8.5.6",
|
"postcss": "^8.5.16",
|
||||||
"tailwindcss": "^3.4.17",
|
"tailwindcss": "^3.4.19",
|
||||||
"tsx": "^4.21.0",
|
"tsx": "^4.22.4",
|
||||||
"typescript": "^5.9.3",
|
"typescript": "^6.0.3",
|
||||||
"vite": "^7.3.1",
|
"vite": "^7.3.1",
|
||||||
"wrangler": "^4.71.0"
|
"wrangler": "^4.105.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@alloc/quick-lru": {
|
"node_modules/@alloc/quick-lru": {
|
||||||
@@ -612,9 +612,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@cloudflare/workerd-darwin-64": {
|
"node_modules/@cloudflare/workerd-darwin-64": {
|
||||||
"version": "1.20260603.1",
|
"version": "1.20260625.1",
|
||||||
"resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20260603.1.tgz",
|
"resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20260625.1.tgz",
|
||||||
"integrity": "sha512-cEXDWu6V3ZrpmwWkM4OJE9AeXjdAgOY5rh8EHhcBVCuP5rxnzUbPzLtrVOHx0UUUAcCrFq0Xsa6mZKL1VUZsKQ==",
|
"integrity": "sha512-naCfBv0WnnTQIQPTniqMoUlklOIFjrAcSn1X+IAOhY8aFLF/xGYtFjs1eEE8sFib3ZuChGGpU23FFORVczqr0A==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -629,9 +629,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@cloudflare/workerd-darwin-arm64": {
|
"node_modules/@cloudflare/workerd-darwin-arm64": {
|
||||||
"version": "1.20260603.1",
|
"version": "1.20260625.1",
|
||||||
"resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20260603.1.tgz",
|
"resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20260625.1.tgz",
|
||||||
"integrity": "sha512-uBPK4LaWJNbbCYwPnUAehlHbbVulhVZPZsdcAhBPfZhHb3QAuAEPAQepO/P67R3V6Cni4YGx1fLbL8A5wwoaNA==",
|
"integrity": "sha512-jmH6zjp6Wrux46+qtFwDwrj+vd7s5bdwEqeGvdnwE0a4IEeAhKs0L42HQOyID+g5lkrHq9m55+AbhtmRAm63Pw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -646,9 +646,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@cloudflare/workerd-linux-64": {
|
"node_modules/@cloudflare/workerd-linux-64": {
|
||||||
"version": "1.20260603.1",
|
"version": "1.20260625.1",
|
||||||
"resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20260603.1.tgz",
|
"resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20260625.1.tgz",
|
||||||
"integrity": "sha512-ht9l6/8Tk7Rp6kA4S9oFZ4X8u0VjnnFdmU/6B3fnABYKREYTKh2RdOqXqXxcp5eNJseireKnWik/hQOPK1CutQ==",
|
"integrity": "sha512-MiQkpA/dX8d83Zp64pzHUKfd6ca4cvwxnNobSP6CnXvfESvnNI9pfa+nfwnParla36sPmnYntNkjR7NjRuDeKQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -663,9 +663,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@cloudflare/workerd-linux-arm64": {
|
"node_modules/@cloudflare/workerd-linux-arm64": {
|
||||||
"version": "1.20260603.1",
|
"version": "1.20260625.1",
|
||||||
"resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20260603.1.tgz",
|
"resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20260625.1.tgz",
|
||||||
"integrity": "sha512-LJZ6x00rAjSrobV4m0ZW0TpH5ilBbKcWBzlH+y+KOUsIE/CpTuhAzKV43TbSnFLRX5+jrWKiz2v0hO91lPXy6A==",
|
"integrity": "sha512-LxxW7Qv60Xvv37+w6gUSDpYZziyqMy+cZWd9IvSA5ehVgKAxmzEaYPMiSZlxk32nbIWL9u/tfjXYCOKJ4Lo+XQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -680,9 +680,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@cloudflare/workerd-windows-64": {
|
"node_modules/@cloudflare/workerd-windows-64": {
|
||||||
"version": "1.20260603.1",
|
"version": "1.20260625.1",
|
||||||
"resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20260603.1.tgz",
|
"resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20260625.1.tgz",
|
||||||
"integrity": "sha512-DvwqkXMAJRPoDN4PxapAwhlz/6ouD+6R1ttbAEK3cWD/QBvFF5STx7Ds/9Irf+rBly3np3uHWkeX+wZnNFEuzA==",
|
"integrity": "sha512-LH6iIX1HHaTwVKV5VokDxxUErXJzQoNZFRwVm7Vx/3fB/ApcTcRCUaMqcxI4as94jEUqg+pmX5czOndiveohow==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -697,9 +697,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@cloudflare/workers-types": {
|
"node_modules/@cloudflare/workers-types": {
|
||||||
"version": "4.20260609.1",
|
"version": "4.20260630.1",
|
||||||
"resolved": "https://registry.npmjs.org/@cloudflare/workers-types/-/workers-types-4.20260609.1.tgz",
|
"resolved": "https://registry.npmjs.org/@cloudflare/workers-types/-/workers-types-4.20260630.1.tgz",
|
||||||
"integrity": "sha512-krGHtwSApCFBjTe1NTx/TFQ0P5i/bHGQOqCPnCLssb8rOKaAG4JkPFJZsossr0z/ZTMnpP2Tid5jWju+/i0hCA==",
|
"integrity": "sha512-yl+c9vwvko9UZ0frmtsHuwOh3BRHvNjLrfelAp5Akpqe1+Ho1UWekr3nmjJ1D64CH0Yb0K0oRMV4i7npOFzsog==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT OR Apache-2.0"
|
"license": "MIT OR Apache-2.0"
|
||||||
},
|
},
|
||||||
@@ -717,9 +717,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@emnapi/runtime": {
|
"node_modules/@emnapi/runtime": {
|
||||||
"version": "1.11.0",
|
"version": "1.11.1",
|
||||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.0.tgz",
|
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz",
|
||||||
"integrity": "sha512-55coeOFKHv1ywEcUXJtWU5f+Jr/W5tZDvZig8DLKSwUN1JpROQ4rk/SNOQiFWmaR/VKF4zuFyW1B8JduOSv6Pg==",
|
"integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
@@ -1770,9 +1770,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@noble/hashes": {
|
"node_modules/@noble/hashes": {
|
||||||
"version": "2.0.1",
|
"version": "2.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.2.0.tgz",
|
||||||
"integrity": "sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw==",
|
"integrity": "sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 20.19.0"
|
"node": ">= 20.19.0"
|
||||||
@@ -2017,9 +2017,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@preact/preset-vite": {
|
"node_modules/@preact/preset-vite": {
|
||||||
"version": "2.10.3",
|
"version": "2.10.5",
|
||||||
"resolved": "https://registry.npmjs.org/@preact/preset-vite/-/preset-vite-2.10.3.tgz",
|
"resolved": "https://registry.npmjs.org/@preact/preset-vite/-/preset-vite-2.10.5.tgz",
|
||||||
"integrity": "sha512-1SiS+vFItpkNdBs7q585PSAIln0wBeBdcpJYbzPs1qipsb/FssnkUioNXuRsb8ZnU8YEQHr+3v8+/mzWSnTQmg==",
|
"integrity": "sha512-p0vJpxiVO7KWWazWny3LUZ+saXyZKWv6Ju0bYMWNJRp2YveufRPgSUB1C4MTqGJfz07EehMgfN+AJNwQy+w6Iw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -2029,12 +2029,14 @@
|
|||||||
"@rollup/pluginutils": "^5.0.0",
|
"@rollup/pluginutils": "^5.0.0",
|
||||||
"babel-plugin-transform-hook-names": "^1.0.2",
|
"babel-plugin-transform-hook-names": "^1.0.2",
|
||||||
"debug": "^4.4.3",
|
"debug": "^4.4.3",
|
||||||
|
"magic-string": "^0.30.21",
|
||||||
"picocolors": "^1.1.1",
|
"picocolors": "^1.1.1",
|
||||||
"vite-prerender-plugin": "^0.5.8"
|
"vite-prerender-plugin": "^0.5.8",
|
||||||
|
"zimmerframe": "^1.1.4"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@babel/core": "7.x",
|
"@babel/core": "7.x",
|
||||||
"vite": "2.x || 3.x || 4.x || 5.x || 6.x || 7.x"
|
"vite": "2.x || 3.x || 4.x || 5.x || 6.x || 7.x || 8.x"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@prefresh/babel-plugin": {
|
"node_modules/@prefresh/babel-plugin": {
|
||||||
@@ -2480,9 +2482,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@simplewebauthn/server": {
|
"node_modules/@simplewebauthn/server": {
|
||||||
"version": "13.3.1",
|
"version": "13.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/@simplewebauthn/server/-/server-13.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/@simplewebauthn/server/-/server-13.3.2.tgz",
|
||||||
"integrity": "sha512-GV/oM/qeycWn8p42JZIMJBsXWQcNFg+nJFzeQTnMA4gN8mXg0+HZFWJerHg8ZN/zlveMS3iV1wzuFpOVWS/46w==",
|
"integrity": "sha512-KEDhfcGP1PAKRVSDjA3npTQFqS2b/srm+ipoNBNHdkzrHAlaRQUTE+a5f4ywsx6thxAw1NU2rYcLEY1949RGbQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hexagon/base64": "^1.1.27",
|
"@hexagon/base64": "^1.1.27",
|
||||||
@@ -2512,16 +2514,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@speed-highlight/core": {
|
"node_modules/@speed-highlight/core": {
|
||||||
"version": "1.2.16",
|
"version": "1.2.17",
|
||||||
"resolved": "https://registry.npmjs.org/@speed-highlight/core/-/core-1.2.16.tgz",
|
"resolved": "https://registry.npmjs.org/@speed-highlight/core/-/core-1.2.17.tgz",
|
||||||
"integrity": "sha512-yNm/fYEcnpRjYduLMaddTK9XKYil6xB88+qFg79ZdZhHu1PadfoQmFW7pVTx7FZqMBNcUuThiAhxhENgtAO2/w==",
|
"integrity": "sha512-Z92FwKpCtfaW1V0jTU/fh3QzYEZN8wDwrzRIBoADCJfn4mJCNcJN/XegifX7BDrQ8/h9Xh/JnbyMchL0FqXrkg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "CC0-1.0"
|
"license": "CC0-1.0"
|
||||||
},
|
},
|
||||||
"node_modules/@tanstack/query-core": {
|
"node_modules/@tanstack/query-core": {
|
||||||
"version": "5.90.20",
|
"version": "5.101.2",
|
||||||
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.20.tgz",
|
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.101.2.tgz",
|
||||||
"integrity": "sha512-OMD2HLpNouXEfZJWcKeVKUgQ5n+n3A2JFmBaScpNDUqSrQSjiveC7dKMe53uJUg1nDG16ttFPz2xfilz6i2uVg==",
|
"integrity": "sha512-hH5MLoJhF7KaIGd7q3xTXGXvslI+GYlM1Z/35aSHHWaCJWB7XvTSHYuV3eM7tw+aE0mT/xMro4M4Q9rCGHT0lw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"funding": {
|
"funding": {
|
||||||
"type": "github",
|
"type": "github",
|
||||||
@@ -2529,12 +2531,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tanstack/react-query": {
|
"node_modules/@tanstack/react-query": {
|
||||||
"version": "5.90.21",
|
"version": "5.101.2",
|
||||||
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.21.tgz",
|
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.101.2.tgz",
|
||||||
"integrity": "sha512-0Lu6y5t+tvlTJMTO7oh5NSpJfpg/5D41LlThfepTixPYkJ0sE2Jj0m0f6yYqujBwIXlId87e234+MxG3D3g7kg==",
|
"integrity": "sha512-seDkr6kzGzX1okaaTtZPtgA688CDPlXUz1C6xSg0ESqn04Vuc8tlrYms1s3de+znBqhPVxFRfpAfUf+6XvfPWg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tanstack/query-core": "5.90.20"
|
"@tanstack/query-core": "5.101.2"
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"type": "github",
|
"type": "github",
|
||||||
@@ -2566,19 +2568,19 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "25.2.3",
|
"version": "26.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-26.0.1.tgz",
|
||||||
"integrity": "sha512-m0jEgYlYz+mDJZ2+F4v8D1AyQb+QzsNqRuI7xg1VQX/KlKS0qT9r1Mo16yo5F/MtifXFgaofIFsdFMox2SxIbQ==",
|
"integrity": "sha512-fc3KiUoBt6kie0N9bIW3E47vZsuaMf0PM2AaUpLCLT0s/LvX1nxAim6Fc049cNxODPpGm6qRAuUOB86SkRuPQw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": "~7.16.0"
|
"undici-types": "~8.3.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@zip.js/zip.js": {
|
"node_modules/@zip.js/zip.js": {
|
||||||
"version": "2.8.22",
|
"version": "2.8.26",
|
||||||
"resolved": "https://registry.npmjs.org/@zip.js/zip.js/-/zip.js-2.8.22.tgz",
|
"resolved": "https://registry.npmjs.org/@zip.js/zip.js/-/zip.js-2.8.26.tgz",
|
||||||
"integrity": "sha512-0KlzbVR6r8irIX2o3zvUlosBDef62VDl47oUfa1U/qgEs67h4/eGBrX/6HWa1RQbt+J6sAeVmtyFKbTHNdF8qQ==",
|
"integrity": "sha512-RQ4h9F6DOiHxpdocUDrOl6xBM+yOtz+LkUol47AVWcfebGBDpZ7w7Xvz9PS24JgXvLGiXXzSAfdCdVy1tPlaFA==",
|
||||||
"license": "BSD-3-Clause",
|
"license": "BSD-3-Clause",
|
||||||
"engines": {
|
"engines": {
|
||||||
"bun": ">=0.7.0",
|
"bun": ">=0.7.0",
|
||||||
@@ -2642,9 +2644,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/autoprefixer": {
|
"node_modules/autoprefixer": {
|
||||||
"version": "10.4.21",
|
"version": "10.5.2",
|
||||||
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz",
|
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.5.2.tgz",
|
||||||
"integrity": "sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==",
|
"integrity": "sha512-rD5t5DwOjJdmSORcTq64j8MawTC+tbQ+HHqjR4NDumamy/ambn1UJrlKL+KdwujWxMkFjPM3pPHOEA9tl4767Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@@ -2662,10 +2664,9 @@
|
|||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"browserslist": "^4.24.4",
|
"browserslist": "^4.28.4",
|
||||||
"caniuse-lite": "^1.0.30001702",
|
"caniuse-lite": "^1.0.30001799",
|
||||||
"fraction.js": "^4.3.7",
|
"fraction.js": "^5.3.4",
|
||||||
"normalize-range": "^0.1.2",
|
|
||||||
"picocolors": "^1.1.1",
|
"picocolors": "^1.1.1",
|
||||||
"postcss-value-parser": "^4.2.0"
|
"postcss-value-parser": "^4.2.0"
|
||||||
},
|
},
|
||||||
@@ -2679,6 +2680,40 @@
|
|||||||
"postcss": "^8.1.0"
|
"postcss": "^8.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/autoprefixer/node_modules/browserslist": {
|
||||||
|
"version": "4.28.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.4.tgz",
|
||||||
|
"integrity": "sha512-MTc8i/x9jBQd1iMw2CFGS+rwMa07eYjLR0CCTLDACl9xhxy+nIs3KeML/biicXtk9JrZ6dnnTatmc7ErPXIxqw==",
|
||||||
|
"dev": true,
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/browserslist"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "tidelift",
|
||||||
|
"url": "https://tidelift.com/funding/github/npm/browserslist"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ai"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"baseline-browser-mapping": "^2.10.38",
|
||||||
|
"caniuse-lite": "^1.0.30001799",
|
||||||
|
"electron-to-chromium": "^1.5.376",
|
||||||
|
"node-releases": "^2.0.48",
|
||||||
|
"update-browserslist-db": "^1.2.3"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"browserslist": "cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/babel-plugin-transform-hook-names": {
|
"node_modules/babel-plugin-transform-hook-names": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/babel-plugin-transform-hook-names/-/babel-plugin-transform-hook-names-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/babel-plugin-transform-hook-names/-/babel-plugin-transform-hook-names-1.0.2.tgz",
|
||||||
@@ -2690,9 +2725,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/baseline-browser-mapping": {
|
"node_modules/baseline-browser-mapping": {
|
||||||
"version": "2.10.0",
|
"version": "2.10.40",
|
||||||
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz",
|
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.40.tgz",
|
||||||
"integrity": "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==",
|
"integrity": "sha512-BSSLZ9/Cjjv7Gtj5B68ZzXcXUg8iOf3fme+FCuh8rC/Go+Kmh8cox7M3A8dolou16s64QjLPOSdngh7GxXvkSw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"bin": {
|
"bin": {
|
||||||
@@ -2787,9 +2822,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/caniuse-lite": {
|
"node_modules/caniuse-lite": {
|
||||||
"version": "1.0.30001774",
|
"version": "1.0.30001799",
|
||||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001774.tgz",
|
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001799.tgz",
|
||||||
"integrity": "sha512-DDdwPGz99nmIEv216hKSgLD+D4ikHQHjBC/seF98N9CPqRX4M5mSxT9eTV6oyisnJcuzxtZy4n17yKKQYmYQOA==",
|
"integrity": "sha512-hG1bReV+OUU+MOqK4t/ZWI0tZOyz3rqS9XuhOUz1cIcbwBKjOyJEJuw9ER5JuNyqxNk8u/JUVbGibBOL1yrjFw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@@ -3021,9 +3056,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/electron-to-chromium": {
|
"node_modules/electron-to-chromium": {
|
||||||
"version": "1.5.302",
|
"version": "1.5.381",
|
||||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.302.tgz",
|
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.381.tgz",
|
||||||
"integrity": "sha512-sM6HAN2LyK82IyPBpznDRqlTQAtuSaO+ShzFiWTvoMJLHyZ+Y39r8VMfHzwbU8MVBzQ4Wdn85+wlZl2TLGIlwg==",
|
"integrity": "sha512-n9Wa6yB+vDsGuA8AKbl/0z7HbvWqt5jxIdvr1IUicd0ryPrk7/xzwqLv8D9AbbvZ6avVNtXYLTfmgFHkwkyelg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
@@ -3188,9 +3223,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/fflate": {
|
"node_modules/fflate": {
|
||||||
"version": "0.8.2",
|
"version": "0.8.3",
|
||||||
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz",
|
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.3.tgz",
|
||||||
"integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==",
|
"integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/fill-range": {
|
"node_modules/fill-range": {
|
||||||
@@ -3207,16 +3242,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/fraction.js": {
|
"node_modules/fraction.js": {
|
||||||
"version": "4.3.7",
|
"version": "5.3.4",
|
||||||
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz",
|
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz",
|
||||||
"integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==",
|
"integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "*"
|
"node": "*"
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"type": "patreon",
|
"type": "github",
|
||||||
"url": "https://github.com/sponsors/rawify"
|
"url": "https://github.com/sponsors/rawify"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -3255,19 +3290,6 @@
|
|||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/get-tsconfig": {
|
|
||||||
"version": "4.13.6",
|
|
||||||
"resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.6.tgz",
|
|
||||||
"integrity": "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"resolve-pkg-maps": "^1.0.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/glob-parent": {
|
"node_modules/glob-parent": {
|
||||||
"version": "6.0.2",
|
"version": "6.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
|
||||||
@@ -3468,9 +3490,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/lucide-preact": {
|
"node_modules/lucide-preact": {
|
||||||
"version": "0.575.0",
|
"version": "1.22.0",
|
||||||
"resolved": "https://registry.npmjs.org/lucide-preact/-/lucide-preact-0.575.0.tgz",
|
"resolved": "https://registry.npmjs.org/lucide-preact/-/lucide-preact-1.22.0.tgz",
|
||||||
"integrity": "sha512-W8JZyQEkYv6DlbRrEgmZxVWFKL3zjoyEkFOOSxiX2VEU6Gou8cOqXZ5IAGmqAL4KiPx1tWgGT9awNjAH7MFknw==",
|
"integrity": "sha512-zFaBtoaWQgvapVEI96M3b5iUOlAEvpUfDuW3Gs8K1RHDyoOTrnXm+Cz5tupm8StKbRKn3W/YKIPolllf5voVDw==",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"preact": "^10.27.2"
|
"preact": "^10.27.2"
|
||||||
@@ -3524,17 +3546,17 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/miniflare": {
|
"node_modules/miniflare": {
|
||||||
"version": "4.20260603.0",
|
"version": "4.20260625.0",
|
||||||
"resolved": "https://registry.npmjs.org/miniflare/-/miniflare-4.20260603.0.tgz",
|
"resolved": "https://registry.npmjs.org/miniflare/-/miniflare-4.20260625.0.tgz",
|
||||||
"integrity": "sha512-+kMQYB82gC8MPOuojHur3icQsUeZUEJ+Sphuo5rVC3Ri9txBLAW/mH33b9OVrpmkogQeaaqPS4tPtugJZhk5Kw==",
|
"integrity": "sha512-3kKXwRUObJsnBYPBgR0NiNZYKF/yv8GFyha1cx2EeAEraxNODgRVcyeRo+F1ok1tg5Mg7iUpOWSkknQTHuFhwA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@cspotcode/source-map-support": "0.8.1",
|
"@cspotcode/source-map-support": "0.8.1",
|
||||||
"sharp": "0.34.5",
|
"sharp": "0.34.5",
|
||||||
"undici": "7.24.8",
|
"undici": "7.28.0",
|
||||||
"workerd": "1.20260603.1",
|
"workerd": "1.20260625.1",
|
||||||
"ws": "8.20.1",
|
"ws": "8.21.0",
|
||||||
"youch": "4.1.0-beta.10"
|
"youch": "4.1.0-beta.10"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
@@ -3600,11 +3622,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/node-releases": {
|
"node_modules/node-releases": {
|
||||||
"version": "2.0.27",
|
"version": "2.0.50",
|
||||||
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz",
|
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.50.tgz",
|
||||||
"integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==",
|
"integrity": "sha512-J6l92tKHX6w8Jy5nO1Vuc01NoIiRGi/d6qBKVxh+IQ8Cr3b6HbVNfKiF8ZpFKufTwpwxMmce2W3iQZ861ZRyTg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"node_modules/normalize-path": {
|
"node_modules/normalize-path": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
@@ -3616,16 +3641,6 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/normalize-range": {
|
|
||||||
"version": "0.1.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz",
|
|
||||||
"integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=0.10.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/nth-check": {
|
"node_modules/nth-check": {
|
||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz",
|
||||||
@@ -3674,11 +3689,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/opencc-js": {
|
"node_modules/opencc-js": {
|
||||||
"version": "1.0.5",
|
"version": "1.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/opencc-js/-/opencc-js-1.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/opencc-js/-/opencc-js-1.3.2.tgz",
|
||||||
"integrity": "sha512-LD+1SoNnZdlRwtYTjnQdFrSVCAaYpuDqL5CkmOaHOkKoKh7mFxUicLTRVNLU5C+Jmi1vXQ3QL4jWdgSaa4sKjg==",
|
"integrity": "sha512-lO4Kq8J4TcPTa8qHcx5qazQCn+NM68kNwLJOQ58DG9MV8v25XJxByMB74zDGmr3LjJMGqDdrvQfoCi3FO0SC2A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT AND Apache-2.0"
|
||||||
},
|
},
|
||||||
"node_modules/path-parse": {
|
"node_modules/path-parse": {
|
||||||
"version": "1.0.7",
|
"version": "1.0.7",
|
||||||
@@ -3742,9 +3757,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/postcss": {
|
"node_modules/postcss": {
|
||||||
"version": "8.5.15",
|
"version": "8.5.16",
|
||||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz",
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.16.tgz",
|
||||||
"integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==",
|
"integrity": "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@@ -3898,9 +3913,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/preact": {
|
"node_modules/preact": {
|
||||||
"version": "10.28.4",
|
"version": "10.29.3",
|
||||||
"resolved": "https://registry.npmjs.org/preact/-/preact-10.28.4.tgz",
|
"resolved": "https://registry.npmjs.org/preact/-/preact-10.29.3.tgz",
|
||||||
"integrity": "sha512-uKFfOHWuSNpRFVTnljsCluEFq57OKT+0QdOiQo8XWnQ/pSvg7OpX5eNOejELXJMWy+BwM2nobz0FkvzmnpCNsQ==",
|
"integrity": "sha512-D9NL1GAnJZhc3RndVs4gDdxEeU9TcHgywMrhhOsnpdlvFjdbx0gAsLUnH6JEhlJH5giL7Tx5biWPUSEXE/HPzw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"funding": {
|
"funding": {
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
@@ -4035,16 +4050,6 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/resolve-pkg-maps": {
|
|
||||||
"version": "1.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
|
|
||||||
"integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/reusify": {
|
"node_modules/reusify": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
|
||||||
@@ -4273,9 +4278,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/tailwindcss": {
|
"node_modules/tailwindcss": {
|
||||||
"version": "3.4.17",
|
"version": "3.4.19",
|
||||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz",
|
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz",
|
||||||
"integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==",
|
"integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -4287,7 +4292,7 @@
|
|||||||
"fast-glob": "^3.3.2",
|
"fast-glob": "^3.3.2",
|
||||||
"glob-parent": "^6.0.2",
|
"glob-parent": "^6.0.2",
|
||||||
"is-glob": "^4.0.3",
|
"is-glob": "^4.0.3",
|
||||||
"jiti": "^1.21.6",
|
"jiti": "^1.21.7",
|
||||||
"lilconfig": "^3.1.3",
|
"lilconfig": "^3.1.3",
|
||||||
"micromatch": "^4.0.8",
|
"micromatch": "^4.0.8",
|
||||||
"normalize-path": "^3.0.0",
|
"normalize-path": "^3.0.0",
|
||||||
@@ -4296,7 +4301,7 @@
|
|||||||
"postcss": "^8.4.47",
|
"postcss": "^8.4.47",
|
||||||
"postcss-import": "^15.1.0",
|
"postcss-import": "^15.1.0",
|
||||||
"postcss-js": "^4.0.1",
|
"postcss-js": "^4.0.1",
|
||||||
"postcss-load-config": "^4.0.2",
|
"postcss-load-config": "^4.0.2 || ^5.0 || ^6.0",
|
||||||
"postcss-nested": "^6.2.0",
|
"postcss-nested": "^6.2.0",
|
||||||
"postcss-selector-parser": "^6.1.2",
|
"postcss-selector-parser": "^6.1.2",
|
||||||
"resolve": "^1.22.8",
|
"resolve": "^1.22.8",
|
||||||
@@ -4377,14 +4382,13 @@
|
|||||||
"license": "0BSD"
|
"license": "0BSD"
|
||||||
},
|
},
|
||||||
"node_modules/tsx": {
|
"node_modules/tsx": {
|
||||||
"version": "4.21.0",
|
"version": "4.22.4",
|
||||||
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz",
|
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.22.4.tgz",
|
||||||
"integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==",
|
"integrity": "sha512-X8EX+XV4QR5xCsrgxaED954zTDfY8KqlDtskKEL0cHhyS/P8b4IFOvGDQpsC9Q1XnLq915wEfwwY/zzskCtmhg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "~0.27.0",
|
"esbuild": "~0.28.0"
|
||||||
"get-tsconfig": "^4.7.5"
|
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsx": "dist/cli.mjs"
|
"tsx": "dist/cli.mjs"
|
||||||
@@ -4415,9 +4419,9 @@
|
|||||||
"license": "0BSD"
|
"license": "0BSD"
|
||||||
},
|
},
|
||||||
"node_modules/typescript": {
|
"node_modules/typescript": {
|
||||||
"version": "5.9.3",
|
"version": "6.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz",
|
||||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
"integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"bin": {
|
"bin": {
|
||||||
@@ -4439,9 +4443,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/undici-types": {
|
"node_modules/undici-types": {
|
||||||
"version": "7.16.0",
|
"version": "8.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz",
|
||||||
"integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
|
"integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
@@ -4596,9 +4600,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/workerd": {
|
"node_modules/workerd": {
|
||||||
"version": "1.20260603.1",
|
"version": "1.20260625.1",
|
||||||
"resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20260603.1.tgz",
|
"resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20260625.1.tgz",
|
||||||
"integrity": "sha512-NPcbhI1++CS+fnELyXtsIR52en+5kwr/OrKeiQeYXGy10HxmPdsQBv9N+DU7hJIOOmBHhOGAAsoGDjyiQ2YCaA==",
|
"integrity": "sha512-GApQvFX52SDM6L4u0+RRnUDB1wJOnEwoXjinkmOPtIyofWBxrlZckdegJSYc1leg++lLZ3+DQ4zMVmBqYVtzfA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
@@ -4609,17 +4613,17 @@
|
|||||||
"node": ">=16"
|
"node": ">=16"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@cloudflare/workerd-darwin-64": "1.20260603.1",
|
"@cloudflare/workerd-darwin-64": "1.20260625.1",
|
||||||
"@cloudflare/workerd-darwin-arm64": "1.20260603.1",
|
"@cloudflare/workerd-darwin-arm64": "1.20260625.1",
|
||||||
"@cloudflare/workerd-linux-64": "1.20260603.1",
|
"@cloudflare/workerd-linux-64": "1.20260625.1",
|
||||||
"@cloudflare/workerd-linux-arm64": "1.20260603.1",
|
"@cloudflare/workerd-linux-arm64": "1.20260625.1",
|
||||||
"@cloudflare/workerd-windows-64": "1.20260603.1"
|
"@cloudflare/workerd-windows-64": "1.20260625.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/wouter": {
|
"node_modules/wouter": {
|
||||||
"version": "3.9.0",
|
"version": "3.10.0",
|
||||||
"resolved": "https://registry.npmjs.org/wouter/-/wouter-3.9.0.tgz",
|
"resolved": "https://registry.npmjs.org/wouter/-/wouter-3.10.0.tgz",
|
||||||
"integrity": "sha512-sF/od/PIgqEQBQcrN7a2x3MX6MQE6nW0ygCfy9hQuUkuB28wEZuu/6M5GyqkrrEu9M6jxdkgE12yDFsQMKos4Q==",
|
"integrity": "sha512-zTfddD80zc2/J5l8JKcdvzOK6AwP0kpyHEI3DxRN2bn8U1oJPnrSVm8v+X3WwDamvLAOxTO7ZvkxkpRWlyeJ1Q==",
|
||||||
"license": "Unlicense",
|
"license": "Unlicense",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"mitt": "^3.0.1",
|
"mitt": "^3.0.1",
|
||||||
@@ -4631,22 +4635,23 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/wrangler": {
|
"node_modules/wrangler": {
|
||||||
"version": "4.98.0",
|
"version": "4.105.0",
|
||||||
"resolved": "https://registry.npmjs.org/wrangler/-/wrangler-4.98.0.tgz",
|
"resolved": "https://registry.npmjs.org/wrangler/-/wrangler-4.105.0.tgz",
|
||||||
"integrity": "sha512-cXfFUuF4rMIvE0hiMnXjEAB27ERryaCgquBJdUoPIjFzYYE1rbRdMUkEdQ18qDPUtsPvhJdqxLntixT9OfSzQw==",
|
"integrity": "sha512-7dXFH6OLj1Fv0y6ZeRPUxFTkp+duWD7/xxVi/1c0vfOeEYwIFKWB7cdqnY05DvY1Ta3BnqAwRkXfLs8PDj538g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT OR Apache-2.0",
|
"license": "MIT OR Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@cloudflare/kv-asset-handler": "0.5.0",
|
"@cloudflare/kv-asset-handler": "0.5.0",
|
||||||
"@cloudflare/unenv-preset": "2.16.1",
|
"@cloudflare/unenv-preset": "2.16.1",
|
||||||
"blake3-wasm": "2.1.5",
|
"blake3-wasm": "2.1.5",
|
||||||
"esbuild": "0.27.3",
|
"esbuild": "0.28.1",
|
||||||
"miniflare": "4.20260603.0",
|
"miniflare": "4.20260625.0",
|
||||||
"path-to-regexp": "6.3.0",
|
"path-to-regexp": "6.3.0",
|
||||||
"unenv": "2.0.0-rc.24",
|
"unenv": "2.0.0-rc.24",
|
||||||
"workerd": "1.20260603.1"
|
"workerd": "1.20260625.1"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
|
"cf-wrangler": "bin/cf-wrangler.js",
|
||||||
"wrangler": "bin/wrangler.js",
|
"wrangler": "bin/wrangler.js",
|
||||||
"wrangler2": "bin/wrangler.js"
|
"wrangler2": "bin/wrangler.js"
|
||||||
},
|
},
|
||||||
@@ -4657,7 +4662,7 @@
|
|||||||
"fsevents": "2.3.3"
|
"fsevents": "2.3.3"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@cloudflare/workers-types": "^4.20260603.1"
|
"@cloudflare/workers-types": "^4.20260625.1"
|
||||||
},
|
},
|
||||||
"peerDependenciesMeta": {
|
"peerDependenciesMeta": {
|
||||||
"@cloudflare/workers-types": {
|
"@cloudflare/workers-types": {
|
||||||
@@ -4666,9 +4671,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/ws": {
|
"node_modules/ws": {
|
||||||
"version": "8.20.1",
|
"version": "8.21.0",
|
||||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.20.1.tgz",
|
"resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz",
|
||||||
"integrity": "sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==",
|
"integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -4727,6 +4732,13 @@
|
|||||||
"@poppinss/exception": "^1.2.2",
|
"@poppinss/exception": "^1.2.2",
|
||||||
"error-stack-parser-es": "^1.0.5"
|
"error-stack-parser-es": "^1.0.5"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"node_modules/zimmerframe": {
|
||||||
|
"version": "1.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.4.tgz",
|
||||||
|
"integrity": "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+21
-20
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "nodewarden",
|
"name": "nodewarden",
|
||||||
"version": "1.7.1",
|
"version": "1.7.2",
|
||||||
"description": "Minimal Bitwarden-compatible server running on Cloudflare Workers",
|
"description": "Minimal Bitwarden-compatible server running on Cloudflare Workers",
|
||||||
"author": "shuaiplus",
|
"author": "shuaiplus",
|
||||||
"license": "LGPL-3.0",
|
"license": "LGPL-3.0",
|
||||||
@@ -45,30 +45,31 @@
|
|||||||
"overrides": {
|
"overrides": {
|
||||||
"undici": ">=7.28.0",
|
"undici": ">=7.28.0",
|
||||||
"@babel/core": ">=7.29.6",
|
"@babel/core": ">=7.29.6",
|
||||||
"esbuild": ">=0.28.1"
|
"esbuild": ">=0.28.1",
|
||||||
|
"ws": "8.21.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@cloudflare/workers-types": "^4.20260131.0",
|
"@cloudflare/workers-types": "^4.20260630.1",
|
||||||
"@preact/preset-vite": "^2.10.3",
|
"@preact/preset-vite": "^2.10.5",
|
||||||
"@types/node": "^25.2.3",
|
"@types/node": "^26.0.1",
|
||||||
"autoprefixer": "^10.4.21",
|
"autoprefixer": "^10.5.2",
|
||||||
"opencc-js": "^1.0.5",
|
"opencc-js": "^1.3.2",
|
||||||
"postcss": "^8.5.6",
|
"postcss": "^8.5.16",
|
||||||
"tailwindcss": "^3.4.17",
|
"tailwindcss": "^3.4.19",
|
||||||
"tsx": "^4.21.0",
|
"tsx": "^4.22.4",
|
||||||
"typescript": "^5.9.3",
|
"typescript": "^6.0.3",
|
||||||
"vite": "^7.3.1",
|
"vite": "^7.3.1",
|
||||||
"wrangler": "^4.71.0"
|
"wrangler": "^4.105.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@noble/hashes": "^2.0.1",
|
"@noble/hashes": "^2.2.0",
|
||||||
"@simplewebauthn/server": "^13.3.1",
|
"@simplewebauthn/server": "^13.3.2",
|
||||||
"@tanstack/react-query": "^5.90.21",
|
"@tanstack/react-query": "^5.101.2",
|
||||||
"@zip.js/zip.js": "^2.8.22",
|
"@zip.js/zip.js": "^2.8.26",
|
||||||
"fflate": "^0.8.2",
|
"fflate": "^0.8.3",
|
||||||
"lucide-preact": "^0.575.0",
|
"lucide-preact": "^1.22.0",
|
||||||
"preact": "^10.28.4",
|
"preact": "^10.29.3",
|
||||||
"qrcode-generator": "^2.0.4",
|
"qrcode-generator": "^2.0.4",
|
||||||
"wouter": "^3.9.0"
|
"wouter": "^3.10.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
export const APP_VERSION = '1.7.1';
|
export const APP_VERSION = '1.7.2';
|
||||||
|
|||||||
+17
-6
@@ -249,7 +249,7 @@ export async function handleAdminListInvites(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// DELETE /api/admin/invites/:code
|
// DELETE /api/admin/invites/:code
|
||||||
export async function handleAdminRevokeInvite(
|
export async function handleAdminDeleteInvite(
|
||||||
request: Request,
|
request: Request,
|
||||||
env: Env,
|
env: Env,
|
||||||
actorUser: User,
|
actorUser: User,
|
||||||
@@ -260,12 +260,14 @@ export async function handleAdminRevokeInvite(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const storage = new StorageService(env.DB);
|
const storage = new StorageService(env.DB);
|
||||||
const revoked = await storage.revokeInvite(code);
|
const deleted = await storage.deleteInvite(code);
|
||||||
if (!revoked) {
|
if (!deleted) {
|
||||||
return errorResponse('Invite not found or already inactive', 404);
|
return errorResponse('Invite not found', 404);
|
||||||
}
|
}
|
||||||
|
|
||||||
await writeAuditLog(storage, actorUser.id, 'admin.invite.revoke', 'invite', null, null, request);
|
await writeAuditLog(storage, actorUser.id, 'admin.invite.delete', 'invite', null, {
|
||||||
|
code,
|
||||||
|
}, request);
|
||||||
return new Response(null, { status: 204 });
|
return new Response(null, { status: 204 });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -275,12 +277,21 @@ export async function handleAdminDeleteAllInvites(
|
|||||||
env: Env,
|
env: Env,
|
||||||
actorUser: User
|
actorUser: User
|
||||||
): Promise<Response> {
|
): Promise<Response> {
|
||||||
void request;
|
|
||||||
if (!isAdmin(actorUser)) {
|
if (!isAdmin(actorUser)) {
|
||||||
return errorResponse('Forbidden', 403);
|
return errorResponse('Forbidden', 403);
|
||||||
}
|
}
|
||||||
|
|
||||||
const storage = new StorageService(env.DB);
|
const storage = new StorageService(env.DB);
|
||||||
|
const url = new URL(request.url);
|
||||||
|
if (url.searchParams.get('scope') === 'invalid') {
|
||||||
|
const deleted = await storage.deleteInvalidInvites();
|
||||||
|
await writeAuditLog(storage, actorUser.id, 'admin.invite.delete_invalid', 'invite', null, {
|
||||||
|
deleted,
|
||||||
|
}, request);
|
||||||
|
|
||||||
|
return jsonResponse({ deleted }, 200);
|
||||||
|
}
|
||||||
|
|
||||||
const deleted = await storage.deleteAllInvites();
|
const deleted = await storage.deleteAllInvites();
|
||||||
await writeAuditLog(storage, actorUser.id, 'admin.invite.delete_all', 'invite', null, {
|
await writeAuditLog(storage, actorUser.id, 'admin.invite.delete_all', 'invite', null, {
|
||||||
deleted,
|
deleted,
|
||||||
|
|||||||
@@ -48,6 +48,8 @@ function buildDeviceResponse(device: Device): DeviceResponse {
|
|||||||
creationDate: device.createdAt,
|
creationDate: device.createdAt,
|
||||||
RevisionDate: device.updatedAt,
|
RevisionDate: device.updatedAt,
|
||||||
revisionDate: device.updatedAt,
|
revisionDate: device.updatedAt,
|
||||||
|
LastActivityDate: device.lastSeenAt,
|
||||||
|
lastActivityDate: device.lastSeenAt,
|
||||||
LastSeenAt: device.lastSeenAt,
|
LastSeenAt: device.lastSeenAt,
|
||||||
lastSeenAt: device.lastSeenAt,
|
lastSeenAt: device.lastSeenAt,
|
||||||
HasStoredDevice: true,
|
HasStoredDevice: true,
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
const EMPTY_FORMS_FILENAME = 'forms.v1.json';
|
||||||
|
const EMPTY_FORMS_BODY = JSON.stringify({
|
||||||
|
schemaVersion: '1.0.0',
|
||||||
|
hosts: {},
|
||||||
|
});
|
||||||
|
const EMPTY_MANIFEST_BODY = JSON.stringify({
|
||||||
|
maps: {
|
||||||
|
forms: {
|
||||||
|
v1: {
|
||||||
|
filename: EMPTY_FORMS_FILENAME,
|
||||||
|
cid: 'sha256:nodewarden-empty-fill-assist-v1',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
function fillAssistJsonResponse(body: string): Response {
|
||||||
|
return new Response(body, {
|
||||||
|
status: 200,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json; charset=utf-8',
|
||||||
|
'Cache-Control': 'public, max-age=3600',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function handleFillAssistManifest(): Response {
|
||||||
|
return fillAssistJsonResponse(EMPTY_MANIFEST_BODY);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function handleFillAssistForms(filename: string): Response {
|
||||||
|
if (String(filename || '').trim() !== EMPTY_FORMS_FILENAME) {
|
||||||
|
return new Response('Not found', { status: 404 });
|
||||||
|
}
|
||||||
|
return fillAssistJsonResponse(EMPTY_FORMS_BODY);
|
||||||
|
}
|
||||||
+2
-2
@@ -4,7 +4,7 @@ import {
|
|||||||
handleAdminCreateInvite,
|
handleAdminCreateInvite,
|
||||||
handleAdminListInvites,
|
handleAdminListInvites,
|
||||||
handleAdminDeleteAllInvites,
|
handleAdminDeleteAllInvites,
|
||||||
handleAdminRevokeInvite,
|
handleAdminDeleteInvite,
|
||||||
handleAdminSetUserStatus,
|
handleAdminSetUserStatus,
|
||||||
handleAdminDeleteUser,
|
handleAdminDeleteUser,
|
||||||
handleAdminListAuditLogs,
|
handleAdminListAuditLogs,
|
||||||
@@ -52,7 +52,7 @@ export async function handleAdminRoute(
|
|||||||
const adminInviteMatch = path.match(/^\/api\/admin\/invites\/([^/]+)$/i);
|
const adminInviteMatch = path.match(/^\/api\/admin\/invites\/([^/]+)$/i);
|
||||||
if (adminInviteMatch && method === 'DELETE') {
|
if (adminInviteMatch && method === 'DELETE') {
|
||||||
const inviteCode = decodeURIComponent(adminInviteMatch[1]);
|
const inviteCode = decodeURIComponent(adminInviteMatch[1]);
|
||||||
return handleAdminRevokeInvite(request, env, actorUser, inviteCode);
|
return handleAdminDeleteInvite(request, env, actorUser, inviteCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
const adminUserStatusMatch = path.match(/^\/api\/admin\/users\/([a-f0-9-]+)\/status$/i);
|
const adminUserStatusMatch = path.match(/^\/api\/admin\/users\/([a-f0-9-]+)\/status$/i);
|
||||||
|
|||||||
+18
-1
@@ -8,6 +8,7 @@ import {
|
|||||||
handleDownloadSendFile,
|
handleDownloadSendFile,
|
||||||
} from './handlers/sends';
|
} from './handlers/sends';
|
||||||
import { handleKnownDevice } from './handlers/devices';
|
import { handleKnownDevice } from './handlers/devices';
|
||||||
|
import { handleFillAssistForms, handleFillAssistManifest } from './handlers/fill-assist';
|
||||||
import { handleToken, handlePrelogin, handleRevocation } from './handlers/identity';
|
import { handleToken, handlePrelogin, handleRevocation } from './handlers/identity';
|
||||||
import { handleGetAccountPasskeyAssertionOptions } from './handlers/account-passkeys';
|
import { handleGetAccountPasskeyAssertionOptions } from './handlers/account-passkeys';
|
||||||
import {
|
import {
|
||||||
@@ -97,6 +98,7 @@ function buildIconServiceCsp(origin: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function buildConfigResponse(origin: string) {
|
function buildConfigResponse(origin: string) {
|
||||||
|
const fillAssistBase = `${origin}/fill-assist`;
|
||||||
return {
|
return {
|
||||||
version: LIMITS.compatibility.bitwardenServerVersion,
|
version: LIMITS.compatibility.bitwardenServerVersion,
|
||||||
gitHash: 'nodewarden',
|
gitHash: 'nodewarden',
|
||||||
@@ -109,7 +111,7 @@ function buildConfigResponse(origin: string) {
|
|||||||
notifications: origin + '/notifications',
|
notifications: origin + '/notifications',
|
||||||
icons: origin,
|
icons: origin,
|
||||||
sso: '',
|
sso: '',
|
||||||
fillAssistRules: null,
|
fillAssistRules: fillAssistBase,
|
||||||
},
|
},
|
||||||
push: {
|
push: {
|
||||||
pushTechnology: 0,
|
pushTechnology: 0,
|
||||||
@@ -125,8 +127,10 @@ function buildConfigResponse(origin: string) {
|
|||||||
'cipher-key-encryption': LIMITS.compatibility.cipherKeyEncryptionFeatureEnabled,
|
'cipher-key-encryption': LIMITS.compatibility.cipherKeyEncryptionFeatureEnabled,
|
||||||
'duo-redirect': true,
|
'duo-redirect': true,
|
||||||
'email-verification': true,
|
'email-verification': true,
|
||||||
|
'fill-assist-targeting-rules': true,
|
||||||
'pm-19051-send-email-verification': false,
|
'pm-19051-send-email-verification': false,
|
||||||
'pm-19148-innovation-archive': true,
|
'pm-19148-innovation-archive': true,
|
||||||
|
'pm-4516-devices-add-last-activity-date': true,
|
||||||
'pm-30529-webauthn-related-origins': true,
|
'pm-30529-webauthn-related-origins': true,
|
||||||
'unauth-ui-refresh': true,
|
'unauth-ui-refresh': true,
|
||||||
'web-push': false,
|
'web-push': false,
|
||||||
@@ -343,6 +347,19 @@ export async function handlePublicRoute(
|
|||||||
return jsonResponse(await buildWebBootstrapResponse(env));
|
return jsonResponse(await buildWebBootstrapResponse(env));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (path === '/fill-assist/manifest.json' && method === 'GET') {
|
||||||
|
const blocked = await enforcePublicRateLimit('public-read', LIMITS.rateLimit.publicReadRequestsPerMinute);
|
||||||
|
if (blocked) return blocked;
|
||||||
|
return handleFillAssistManifest();
|
||||||
|
}
|
||||||
|
|
||||||
|
const fillAssistFormsMatch = path.match(/^\/fill-assist\/([^/]+)$/i);
|
||||||
|
if (fillAssistFormsMatch && method === 'GET') {
|
||||||
|
const blocked = await enforcePublicRateLimit('public-read', LIMITS.rateLimit.publicReadRequestsPerMinute);
|
||||||
|
if (blocked) return blocked;
|
||||||
|
return handleFillAssistForms(fillAssistFormsMatch[1]);
|
||||||
|
}
|
||||||
|
|
||||||
const iconMatch = path.match(/^\/icons\/([^/]+)\/icon\.png$/i);
|
const iconMatch = path.match(/^\/icons\/([^/]+)\/icon\.png$/i);
|
||||||
if (iconMatch && method === 'GET') {
|
if (iconMatch && method === 'GET') {
|
||||||
const blocked = await enforcePublicRateLimit('public-icon', LIMITS.rateLimit.publicIconRequestsPerMinute);
|
const blocked = await enforcePublicRateLimit('public-icon', LIMITS.rateLimit.publicIconRequestsPerMinute);
|
||||||
|
|||||||
@@ -151,15 +151,23 @@ export async function revertInviteUsed(db: D1Database, code: string, userId: str
|
|||||||
return (result.meta.changes ?? 0) > 0;
|
return (result.meta.changes ?? 0) > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function revokeInvite(db: D1Database, code: string): Promise<boolean> {
|
export async function deleteInvite(db: D1Database, code: string): Promise<boolean> {
|
||||||
const now = new Date().toISOString();
|
|
||||||
const result = await db
|
const result = await db
|
||||||
.prepare("UPDATE invites SET status = 'revoked', updated_at = ? WHERE code = ? AND status = 'active'")
|
.prepare('DELETE FROM invites WHERE code = ?')
|
||||||
.bind(now, code)
|
.bind(code)
|
||||||
.run();
|
.run();
|
||||||
return (result.meta.changes ?? 0) > 0;
|
return (result.meta.changes ?? 0) > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function deleteInvalidInvites(db: D1Database): Promise<number> {
|
||||||
|
const now = new Date().toISOString();
|
||||||
|
const result = await db
|
||||||
|
.prepare("DELETE FROM invites WHERE status != 'active' OR expires_at <= ?")
|
||||||
|
.bind(now)
|
||||||
|
.run();
|
||||||
|
return Number(result.meta.changes ?? 0);
|
||||||
|
}
|
||||||
|
|
||||||
export async function deleteAllInvites(db: D1Database): Promise<number> {
|
export async function deleteAllInvites(db: D1Database): Promise<number> {
|
||||||
const result = await db.prepare('DELETE FROM invites').run();
|
const result = await db.prepare('DELETE FROM invites').run();
|
||||||
return Number(result.meta.changes ?? 0);
|
return Number(result.meta.changes ?? 0);
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ import {
|
|||||||
clearAuditLogs as clearStoredAuditLogs,
|
clearAuditLogs as clearStoredAuditLogs,
|
||||||
assignInviteUsedBy as assignStoredInviteUsedBy,
|
assignInviteUsedBy as assignStoredInviteUsedBy,
|
||||||
createInvite as createStoredInvite,
|
createInvite as createStoredInvite,
|
||||||
|
deleteInvite as deleteStoredInvite,
|
||||||
|
deleteInvalidInvites as deleteStoredInvalidInvites,
|
||||||
deleteAllInvites as deleteStoredInvites,
|
deleteAllInvites as deleteStoredInvites,
|
||||||
getInvite as findStoredInvite,
|
getInvite as findStoredInvite,
|
||||||
listAuditLogs as listStoredAuditLogs,
|
listAuditLogs as listStoredAuditLogs,
|
||||||
@@ -32,7 +34,6 @@ import {
|
|||||||
pruneAuditLogs as pruneStoredAuditLogs,
|
pruneAuditLogs as pruneStoredAuditLogs,
|
||||||
pruneAuditLogsToMax as pruneStoredAuditLogsToMax,
|
pruneAuditLogsToMax as pruneStoredAuditLogsToMax,
|
||||||
revertInviteUsed as revertStoredInviteUsed,
|
revertInviteUsed as revertStoredInviteUsed,
|
||||||
revokeInvite as revokeStoredInvite,
|
|
||||||
} from './storage-admin-repo';
|
} from './storage-admin-repo';
|
||||||
import {
|
import {
|
||||||
bulkDeleteFolders as deleteStoredFolders,
|
bulkDeleteFolders as deleteStoredFolders,
|
||||||
@@ -329,8 +330,12 @@ export class StorageService {
|
|||||||
return revertStoredInviteUsed(this.db, code, userId);
|
return revertStoredInviteUsed(this.db, code, userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
async revokeInvite(code: string): Promise<boolean> {
|
async deleteInvite(code: string): Promise<boolean> {
|
||||||
return revokeStoredInvite(this.db, code);
|
return deleteStoredInvite(this.db, code);
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteInvalidInvites(): Promise<number> {
|
||||||
|
return deleteStoredInvalidInvites(this.db);
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteAllInvites(): Promise<number> {
|
async deleteAllInvites(): Promise<number> {
|
||||||
|
|||||||
@@ -307,6 +307,7 @@ export interface DeviceResponse {
|
|||||||
type: number;
|
type: number;
|
||||||
creationDate: string;
|
creationDate: string;
|
||||||
revisionDate: string;
|
revisionDate: string;
|
||||||
|
lastActivityDate?: string | null;
|
||||||
lastSeenAt?: string | null;
|
lastSeenAt?: string | null;
|
||||||
hasStoredDevice?: boolean;
|
hasStoredDevice?: boolean;
|
||||||
isTrusted: boolean;
|
isTrusted: boolean;
|
||||||
|
|||||||
+4
-4
@@ -1115,8 +1115,6 @@ export default function App() {
|
|||||||
queryFn: () => listPendingAuthRequests(authedFetch, profile?.email || session?.email || ''),
|
queryFn: () => listPendingAuthRequests(authedFetch, profile?.email || session?.email || ''),
|
||||||
enabled: !IS_DEMO_MODE && phase === 'app' && !!session?.accessToken && !!session?.symEncKey && !!session?.symMacKey && !!(profile?.email || session?.email),
|
enabled: !IS_DEMO_MODE && phase === 'app' && !!session?.accessToken && !!session?.symEncKey && !!session?.symMacKey && !!(profile?.email || session?.email),
|
||||||
staleTime: 5_000,
|
staleTime: 5_000,
|
||||||
refetchInterval: 15_000,
|
|
||||||
refetchIntervalInBackground: true,
|
|
||||||
});
|
});
|
||||||
const pendingAuthRequests = (pendingAuthRequestsQuery.data || []).filter(isPendingAuthRequest);
|
const pendingAuthRequests = (pendingAuthRequestsQuery.data || []).filter(isPendingAuthRequest);
|
||||||
const latestPendingAuthRequest = pendingAuthRequests[0] || null;
|
const latestPendingAuthRequest = pendingAuthRequests[0] || null;
|
||||||
@@ -2015,7 +2013,8 @@ export default function App() {
|
|||||||
onEnableAccountPasskeyDirectUnlock: accountSecurityActions.enableAccountPasskeyDirectUnlock,
|
onEnableAccountPasskeyDirectUnlock: accountSecurityActions.enableAccountPasskeyDirectUnlock,
|
||||||
onDeleteAccountPasskey: accountSecurityActions.deleteAccountPasskey,
|
onDeleteAccountPasskey: accountSecurityActions.deleteAccountPasskey,
|
||||||
pendingAuthRequests,
|
pendingAuthRequests,
|
||||||
pendingAuthRequestsLoading: pendingAuthRequestsQuery.isFetching,
|
pendingAuthRequestsLoading: pendingAuthRequestsQuery.isLoading,
|
||||||
|
pendingAuthRequestsRefreshing: pendingAuthRequestsQuery.isFetching && !pendingAuthRequestsQuery.isLoading,
|
||||||
onRefreshPendingAuthRequests: async () => {
|
onRefreshPendingAuthRequests: async () => {
|
||||||
await pendingAuthRequestsQuery.refetch();
|
await pendingAuthRequestsQuery.refetch();
|
||||||
},
|
},
|
||||||
@@ -2037,10 +2036,11 @@ export default function App() {
|
|||||||
onRemoveAllDevices: accountSecurityActions.openRemoveAllDevices,
|
onRemoveAllDevices: accountSecurityActions.openRemoveAllDevices,
|
||||||
onRefreshAdmin: adminActions.refreshAdmin,
|
onRefreshAdmin: adminActions.refreshAdmin,
|
||||||
onCreateInvite: adminActions.createInvite,
|
onCreateInvite: adminActions.createInvite,
|
||||||
|
onDeleteInvalidInvites: adminActions.deleteInvalidInvites,
|
||||||
onDeleteAllInvites: adminActions.deleteAllInvites,
|
onDeleteAllInvites: adminActions.deleteAllInvites,
|
||||||
onToggleUserStatus: adminActions.toggleUserStatus,
|
onToggleUserStatus: adminActions.toggleUserStatus,
|
||||||
onDeleteUser: adminActions.deleteUser,
|
onDeleteUser: adminActions.deleteUser,
|
||||||
onRevokeInvite: adminActions.revokeInvite,
|
onDeleteInvite: adminActions.deleteInvite,
|
||||||
onLoadAuditLogs: (filters: AuditLogFilters) => listAuditLogs(authedFetch, filters),
|
onLoadAuditLogs: (filters: AuditLogFilters) => listAuditLogs(authedFetch, filters),
|
||||||
onLoadAuditLogSettings: () => getAuditLogSettings(authedFetch),
|
onLoadAuditLogSettings: () => getAuditLogSettings(authedFetch),
|
||||||
onSaveAuditLogSettings: (settings: AuditLogSettings) => saveAuditLogSettings(authedFetch, settings),
|
onSaveAuditLogSettings: (settings: AuditLogSettings) => saveAuditLogSettings(authedFetch, settings),
|
||||||
|
|||||||
@@ -13,10 +13,11 @@ interface AdminPageProps {
|
|||||||
error: string;
|
error: string;
|
||||||
onRefresh: () => void;
|
onRefresh: () => void;
|
||||||
onCreateInvite: (hours: number) => Promise<void>;
|
onCreateInvite: (hours: number) => Promise<void>;
|
||||||
|
onDeleteInvalidInvites: () => Promise<void>;
|
||||||
onDeleteAllInvites: () => Promise<void>;
|
onDeleteAllInvites: () => Promise<void>;
|
||||||
onToggleUserStatus: (userId: string, currentStatus: 'active' | 'banned') => Promise<void>;
|
onToggleUserStatus: (userId: string, currentStatus: 'active' | 'banned') => Promise<void>;
|
||||||
onDeleteUser: (userId: string) => Promise<void>;
|
onDeleteUser: (userId: string) => Promise<void>;
|
||||||
onRevokeInvite: (code: string) => Promise<void>;
|
onDeleteInvite: (code: string) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function AdminPage(props: AdminPageProps) {
|
export default function AdminPage(props: AdminPageProps) {
|
||||||
@@ -134,7 +135,10 @@ export default function AdminPage(props: AdminPageProps) {
|
|||||||
<h3>{t('txt_invites')}</h3>
|
<h3>{t('txt_invites')}</h3>
|
||||||
<div className="actions admin-invites-head-actions">
|
<div className="actions admin-invites-head-actions">
|
||||||
<button type="button" className="btn btn-secondary small" disabled={props.loading} onClick={props.onRefresh}>
|
<button type="button" className="btn btn-secondary small" disabled={props.loading} onClick={props.onRefresh}>
|
||||||
<RefreshCw size={14} className="btn-icon" /> {t('txt_sync')}
|
<RefreshCw size={14} className="btn-icon" /> {t('txt_refresh')}
|
||||||
|
</button>
|
||||||
|
<button type="button" className="btn btn-danger small" onClick={() => void props.onDeleteInvalidInvites()}>
|
||||||
|
<Trash2 size={14} className="btn-icon" /> {t('txt_delete_invalid')}
|
||||||
</button>
|
</button>
|
||||||
<button type="button" className="btn btn-danger small" onClick={() => void props.onDeleteAllInvites()}>
|
<button type="button" className="btn btn-danger small" onClick={() => void props.onDeleteAllInvites()}>
|
||||||
<Trash2 size={14} className="btn-icon" /> {t('txt_delete_all')}
|
<Trash2 size={14} className="btn-icon" /> {t('txt_delete_all')}
|
||||||
@@ -184,11 +188,9 @@ export default function AdminPage(props: AdminPageProps) {
|
|||||||
>
|
>
|
||||||
<Clipboard size={14} className="btn-icon" /> {t('txt_copy_link')}
|
<Clipboard size={14} className="btn-icon" /> {t('txt_copy_link')}
|
||||||
</button>
|
</button>
|
||||||
{invite.status === 'active' && (
|
<button type="button" className="btn btn-danger" onClick={() => void props.onDeleteInvite(invite.code)}>
|
||||||
<button type="button" className="btn btn-danger" onClick={() => void props.onRevokeInvite(invite.code)}>
|
<Trash2 size={14} className="btn-icon" /> {t('txt_delete')}
|
||||||
<Trash2 size={14} className="btn-icon" /> {t('txt_revoke')}
|
|
||||||
</button>
|
</button>
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -119,6 +119,7 @@ export interface AppMainRoutesProps {
|
|||||||
onDeleteAccountPasskey: (id: string, masterPassword: string) => Promise<void>;
|
onDeleteAccountPasskey: (id: string, masterPassword: string) => Promise<void>;
|
||||||
pendingAuthRequests: AuthRequest[];
|
pendingAuthRequests: AuthRequest[];
|
||||||
pendingAuthRequestsLoading: boolean;
|
pendingAuthRequestsLoading: boolean;
|
||||||
|
pendingAuthRequestsRefreshing: boolean;
|
||||||
onRefreshPendingAuthRequests: () => Promise<void>;
|
onRefreshPendingAuthRequests: () => Promise<void>;
|
||||||
onApproveAuthRequest: (request: AuthRequest) => Promise<void>;
|
onApproveAuthRequest: (request: AuthRequest) => Promise<void>;
|
||||||
onDenyAuthRequest: (request: AuthRequest) => Promise<void>;
|
onDenyAuthRequest: (request: AuthRequest) => Promise<void>;
|
||||||
@@ -136,10 +137,11 @@ export interface AppMainRoutesProps {
|
|||||||
onRemoveAllDevices: () => void;
|
onRemoveAllDevices: () => void;
|
||||||
onCreateInvite: (hours: number) => Promise<void>;
|
onCreateInvite: (hours: number) => Promise<void>;
|
||||||
onRefreshAdmin: () => void;
|
onRefreshAdmin: () => void;
|
||||||
|
onDeleteInvalidInvites: () => Promise<void>;
|
||||||
onDeleteAllInvites: () => Promise<void>;
|
onDeleteAllInvites: () => Promise<void>;
|
||||||
onToggleUserStatus: (userId: string, status: 'active' | 'banned') => Promise<void>;
|
onToggleUserStatus: (userId: string, status: 'active' | 'banned') => Promise<void>;
|
||||||
onDeleteUser: (userId: string) => Promise<void>;
|
onDeleteUser: (userId: string) => Promise<void>;
|
||||||
onRevokeInvite: (code: string) => Promise<void>;
|
onDeleteInvite: (code: string) => Promise<void>;
|
||||||
onLoadAuditLogs: (filters: AuditLogFilters) => Promise<AuditLogListResult>;
|
onLoadAuditLogs: (filters: AuditLogFilters) => Promise<AuditLogListResult>;
|
||||||
onLoadAuditLogSettings: () => Promise<AuditLogSettings>;
|
onLoadAuditLogSettings: () => Promise<AuditLogSettings>;
|
||||||
onSaveAuditLogSettings: (settings: AuditLogSettings) => Promise<AuditLogSettings>;
|
onSaveAuditLogSettings: (settings: AuditLogSettings) => Promise<AuditLogSettings>;
|
||||||
@@ -354,6 +356,7 @@ export default function AppMainRoutes(props: AppMainRoutesProps) {
|
|||||||
error={props.authorizedDevicesError}
|
error={props.authorizedDevicesError}
|
||||||
pendingAuthRequests={props.pendingAuthRequests}
|
pendingAuthRequests={props.pendingAuthRequests}
|
||||||
pendingAuthRequestsLoading={props.pendingAuthRequestsLoading}
|
pendingAuthRequestsLoading={props.pendingAuthRequestsLoading}
|
||||||
|
pendingAuthRequestsRefreshing={props.pendingAuthRequestsRefreshing}
|
||||||
onRefresh={() => void props.onRefreshAuthorizedDevices()}
|
onRefresh={() => void props.onRefreshAuthorizedDevices()}
|
||||||
onRefreshPendingAuthRequests={props.onRefreshPendingAuthRequests}
|
onRefreshPendingAuthRequests={props.onRefreshPendingAuthRequests}
|
||||||
onApproveAuthRequest={props.onApproveAuthRequest}
|
onApproveAuthRequest={props.onApproveAuthRequest}
|
||||||
@@ -411,10 +414,11 @@ export default function AppMainRoutes(props: AppMainRoutesProps) {
|
|||||||
error={props.adminError}
|
error={props.adminError}
|
||||||
onRefresh={props.onRefreshAdmin}
|
onRefresh={props.onRefreshAdmin}
|
||||||
onCreateInvite={props.onCreateInvite}
|
onCreateInvite={props.onCreateInvite}
|
||||||
|
onDeleteInvalidInvites={props.onDeleteInvalidInvites}
|
||||||
onDeleteAllInvites={props.onDeleteAllInvites}
|
onDeleteAllInvites={props.onDeleteAllInvites}
|
||||||
onToggleUserStatus={props.onToggleUserStatus}
|
onToggleUserStatus={props.onToggleUserStatus}
|
||||||
onDeleteUser={props.onDeleteUser}
|
onDeleteUser={props.onDeleteUser}
|
||||||
onRevokeInvite={props.onRevokeInvite}
|
onDeleteInvite={props.onDeleteInvite}
|
||||||
/>
|
/>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { t } from '@/lib/i18n';
|
|||||||
interface PendingAuthRequestsPanelProps {
|
interface PendingAuthRequestsPanelProps {
|
||||||
pendingAuthRequests: AuthRequest[];
|
pendingAuthRequests: AuthRequest[];
|
||||||
pendingAuthRequestsLoading: boolean;
|
pendingAuthRequestsLoading: boolean;
|
||||||
|
pendingAuthRequestsRefreshing?: boolean;
|
||||||
onRefreshPendingAuthRequests: () => Promise<void>;
|
onRefreshPendingAuthRequests: () => Promise<void>;
|
||||||
onApproveAuthRequest: (request: AuthRequest) => Promise<void>;
|
onApproveAuthRequest: (request: AuthRequest) => Promise<void>;
|
||||||
onDenyAuthRequest: (request: AuthRequest) => Promise<void>;
|
onDenyAuthRequest: (request: AuthRequest) => Promise<void>;
|
||||||
@@ -22,6 +23,7 @@ function formatDateTime(value: string | null | undefined): string {
|
|||||||
|
|
||||||
export default function PendingAuthRequestsPanel(props: PendingAuthRequestsPanelProps) {
|
export default function PendingAuthRequestsPanel(props: PendingAuthRequestsPanelProps) {
|
||||||
const [authRequestSubmittingId, setAuthRequestSubmittingId] = useState<string | null>(null);
|
const [authRequestSubmittingId, setAuthRequestSubmittingId] = useState<string | null>(null);
|
||||||
|
const refreshing = props.pendingAuthRequestsLoading || !!props.pendingAuthRequestsRefreshing;
|
||||||
|
|
||||||
async function approveAuthRequest(authRequest: AuthRequest): Promise<void> {
|
async function approveAuthRequest(authRequest: AuthRequest): Promise<void> {
|
||||||
if (authRequestSubmittingId) return;
|
if (authRequestSubmittingId) return;
|
||||||
@@ -50,10 +52,10 @@ export default function PendingAuthRequestsPanel(props: PendingAuthRequestsPanel
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="btn btn-secondary small"
|
className="btn btn-secondary small"
|
||||||
disabled={props.pendingAuthRequestsLoading}
|
disabled={refreshing}
|
||||||
onClick={() => void props.onRefreshPendingAuthRequests()}
|
onClick={() => void props.onRefreshPendingAuthRequests()}
|
||||||
>
|
>
|
||||||
<RefreshCw size={14} className="btn-icon" />
|
<RefreshCw size={14} className={`btn-icon${refreshing ? ' btn-icon-spin' : ''}`} />
|
||||||
{t('txt_refresh')}
|
{t('txt_refresh')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ interface SecurityDevicesPageProps {
|
|||||||
error: string;
|
error: string;
|
||||||
pendingAuthRequests: AuthRequest[];
|
pendingAuthRequests: AuthRequest[];
|
||||||
pendingAuthRequestsLoading: boolean;
|
pendingAuthRequestsLoading: boolean;
|
||||||
|
pendingAuthRequestsRefreshing: boolean;
|
||||||
onRefresh: () => void;
|
onRefresh: () => void;
|
||||||
onRefreshPendingAuthRequests: () => Promise<void>;
|
onRefreshPendingAuthRequests: () => Promise<void>;
|
||||||
onApproveAuthRequest: (request: AuthRequest) => Promise<void>;
|
onApproveAuthRequest: (request: AuthRequest) => Promise<void>;
|
||||||
@@ -106,6 +107,7 @@ export default function SecurityDevicesPage(props: SecurityDevicesPageProps) {
|
|||||||
loadingVariant="compact"
|
loadingVariant="compact"
|
||||||
pendingAuthRequests={props.pendingAuthRequests}
|
pendingAuthRequests={props.pendingAuthRequests}
|
||||||
pendingAuthRequestsLoading={props.pendingAuthRequestsLoading}
|
pendingAuthRequestsLoading={props.pendingAuthRequestsLoading}
|
||||||
|
pendingAuthRequestsRefreshing={props.pendingAuthRequestsRefreshing}
|
||||||
onRefreshPendingAuthRequests={props.onRefreshPendingAuthRequests}
|
onRefreshPendingAuthRequests={props.onRefreshPendingAuthRequests}
|
||||||
onApproveAuthRequest={props.onApproveAuthRequest}
|
onApproveAuthRequest={props.onApproveAuthRequest}
|
||||||
onDenyAuthRequest={props.onDenyAuthRequest}
|
onDenyAuthRequest={props.onDenyAuthRequest}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useMemo } from 'preact/hooks';
|
import { useMemo } from 'preact/hooks';
|
||||||
import { createInvite, deleteAllInvites, deleteUser, revokeInvite, setUserStatus } from '@/lib/api/admin';
|
import { createInvite, deleteAllInvites, deleteInvalidInvites, deleteInvite, deleteUser, setUserStatus } from '@/lib/api/admin';
|
||||||
import { t } from '@/lib/i18n';
|
import { t } from '@/lib/i18n';
|
||||||
import type { AppConfirmState } from '@/components/AppGlobalOverlays';
|
import type { AppConfirmState } from '@/components/AppGlobalOverlays';
|
||||||
import type { AuthedFetch } from '@/lib/api/shared';
|
import type { AuthedFetch } from '@/lib/api/shared';
|
||||||
@@ -45,14 +45,44 @@ export default function useAdminActions(options: UseAdminActionsOptions) {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async revokeInvite(code: string) {
|
async deleteInvite(code: string) {
|
||||||
|
onSetConfirm({
|
||||||
|
title: t('txt_delete_invite'),
|
||||||
|
message: t('txt_delete_invite_confirm_message'),
|
||||||
|
danger: true,
|
||||||
|
onConfirm: () => {
|
||||||
|
onSetConfirm(null);
|
||||||
|
void (async () => {
|
||||||
try {
|
try {
|
||||||
await revokeInvite(authedFetch, code);
|
await deleteInvite(authedFetch, code);
|
||||||
await refetchInvites();
|
await refetchInvites();
|
||||||
onNotify('success', t('txt_invite_revoked'));
|
onNotify('success', t('txt_invite_deleted'));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
onNotify('error', error instanceof Error ? error.message : t('txt_revoke_invite_failed'));
|
onNotify('error', error instanceof Error ? error.message : t('txt_delete_invite_failed'));
|
||||||
}
|
}
|
||||||
|
})();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
async deleteInvalidInvites() {
|
||||||
|
onSetConfirm({
|
||||||
|
title: t('txt_delete_invalid_invites'),
|
||||||
|
message: t('txt_delete_invalid_invites_confirm_message'),
|
||||||
|
danger: true,
|
||||||
|
onConfirm: () => {
|
||||||
|
onSetConfirm(null);
|
||||||
|
void (async () => {
|
||||||
|
try {
|
||||||
|
await deleteInvalidInvites(authedFetch);
|
||||||
|
await refetchInvites();
|
||||||
|
onNotify('success', t('txt_invalid_invites_deleted'));
|
||||||
|
} catch (error) {
|
||||||
|
onNotify('error', error instanceof Error ? error.message : t('txt_delete_invalid_invites_failed'));
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
},
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
async deleteAllInvites() {
|
async deleteAllInvites() {
|
||||||
|
|||||||
@@ -24,9 +24,14 @@ export async function createInvite(authedFetch: AuthedFetch, hours: number): Pro
|
|||||||
if (!resp.ok) throw new Error('Create invite failed');
|
if (!resp.ok) throw new Error('Create invite failed');
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function revokeInvite(authedFetch: AuthedFetch, code: string): Promise<void> {
|
export async function deleteInvite(authedFetch: AuthedFetch, code: string): Promise<void> {
|
||||||
const resp = await authedFetch(`/api/admin/invites/${encodeURIComponent(code)}`, { method: 'DELETE' });
|
const resp = await authedFetch(`/api/admin/invites/${encodeURIComponent(code)}`, { method: 'DELETE' });
|
||||||
if (!resp.ok) throw new Error('Revoke invite failed');
|
if (!resp.ok) throw new Error('Delete invite failed');
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteInvalidInvites(authedFetch: AuthedFetch): Promise<void> {
|
||||||
|
const resp = await authedFetch('/api/admin/invites?scope=invalid', { method: 'DELETE' });
|
||||||
|
if (!resp.ok) throw new Error('Delete invalid invites failed');
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteAllInvites(authedFetch: AuthedFetch): Promise<void> {
|
export async function deleteAllInvites(authedFetch: AuthedFetch): Promise<void> {
|
||||||
|
|||||||
+10
-5
@@ -1127,6 +1127,13 @@ export function createDemoMainRoutesProps(base: AppMainRoutesProps, notify: Noti
|
|||||||
onRefreshAdmin: () => {
|
onRefreshAdmin: () => {
|
||||||
notify('success', t('txt_demo_admin_refreshed'));
|
notify('success', t('txt_demo_admin_refreshed'));
|
||||||
},
|
},
|
||||||
|
onDeleteInvalidInvites: async () => {
|
||||||
|
const now = Date.now();
|
||||||
|
state.setInvites((prev) => prev.filter((invite) => (
|
||||||
|
invite.status === 'active' && (!invite.expiresAt || new Date(invite.expiresAt).getTime() > now)
|
||||||
|
)));
|
||||||
|
notify('success', t('txt_invalid_invites_deleted'));
|
||||||
|
},
|
||||||
onDeleteAllInvites: async () => {
|
onDeleteAllInvites: async () => {
|
||||||
state.setInvites([]);
|
state.setInvites([]);
|
||||||
notify('success', t('txt_all_invites_deleted'));
|
notify('success', t('txt_all_invites_deleted'));
|
||||||
@@ -1141,11 +1148,9 @@ export function createDemoMainRoutesProps(base: AppMainRoutesProps, notify: Noti
|
|||||||
state.setUsers((prev) => prev.filter((user) => user.id !== userId));
|
state.setUsers((prev) => prev.filter((user) => user.id !== userId));
|
||||||
notify('success', t('txt_user_deleted'));
|
notify('success', t('txt_user_deleted'));
|
||||||
},
|
},
|
||||||
onRevokeInvite: async (code) => {
|
onDeleteInvite: async (code) => {
|
||||||
state.setInvites((prev) => prev.map((invite) => (
|
state.setInvites((prev) => prev.filter((invite) => invite.code !== code));
|
||||||
invite.code === code ? { ...invite, status: 'inactive' } : invite
|
notify('success', t('txt_invite_deleted'));
|
||||||
)));
|
|
||||||
notify('success', t('txt_invite_revoked'));
|
|
||||||
},
|
},
|
||||||
onLoadAuditLogSettings: async () => ({ retentionDays: 90, maxEntries: null }),
|
onLoadAuditLogSettings: async () => ({ retentionDays: 90, maxEntries: null }),
|
||||||
onSaveAuditLogSettings: async (settings) => {
|
onSaveAuditLogSettings: async (settings) => {
|
||||||
|
|||||||
@@ -449,6 +449,43 @@ function appendRecordFieldLines(lines: string[], prefix: string, value: unknown)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const BITWARDEN_CSV_OBJECT_FIELDS: Record<string, readonly string[]> = {
|
||||||
|
card: ['cardholderName', 'brand', 'number', 'expMonth', 'expYear', 'code'],
|
||||||
|
identity: [
|
||||||
|
'title',
|
||||||
|
'firstName',
|
||||||
|
'middleName',
|
||||||
|
'lastName',
|
||||||
|
'username',
|
||||||
|
'company',
|
||||||
|
'ssn',
|
||||||
|
'passportNumber',
|
||||||
|
'licenseNumber',
|
||||||
|
'email',
|
||||||
|
'phone',
|
||||||
|
'address1',
|
||||||
|
'address2',
|
||||||
|
'address3',
|
||||||
|
'city',
|
||||||
|
'state',
|
||||||
|
'postalCode',
|
||||||
|
'country',
|
||||||
|
],
|
||||||
|
sshKey: ['privateKey', 'publicKey', 'keyFingerprint', 'fingerprint'],
|
||||||
|
};
|
||||||
|
|
||||||
|
function appendKnownRecordFieldLines(lines: string[], prefix: string, value: unknown): void {
|
||||||
|
if (!isRecord(value)) return;
|
||||||
|
const keys = BITWARDEN_CSV_OBJECT_FIELDS[prefix];
|
||||||
|
if (!keys) {
|
||||||
|
appendRecordFieldLines(lines, prefix, value);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (const key of keys) {
|
||||||
|
appendFieldLine(lines, `${prefix}.${key}`, value[key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function buildBitwardenCsvFields(item: Record<string, unknown>, type: number): string {
|
function buildBitwardenCsvFields(item: Record<string, unknown>, type: number): string {
|
||||||
const lines: string[] = [];
|
const lines: string[] = [];
|
||||||
const fields = Array.isArray(item.fields) ? item.fields : [];
|
const fields = Array.isArray(item.fields) ? item.fields : [];
|
||||||
@@ -457,8 +494,9 @@ function buildBitwardenCsvFields(item: Record<string, unknown>, type: number): s
|
|||||||
appendFieldLine(lines, field.name, field.value);
|
appendFieldLine(lines, field.name, field.value);
|
||||||
}
|
}
|
||||||
if (type !== 1 && type !== 2) {
|
if (type !== 1 && type !== 2) {
|
||||||
appendFieldLine(lines, 'nodewardenType', sourceTypeLabel(type));
|
const sourceLabel = sourceTypeLabel(type);
|
||||||
appendRecordFieldLines(lines, sourceTypeLabel(type), item[sourceTypeLabel(type)]);
|
appendFieldLine(lines, 'nodewardenType', sourceLabel);
|
||||||
|
appendKnownRecordFieldLines(lines, sourceLabel, item[sourceLabel]);
|
||||||
}
|
}
|
||||||
return lines.join('\n');
|
return lines.join('\n');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -582,8 +582,17 @@ const en: Record<string, string> = {
|
|||||||
"txt_identity_details": "Identity Details",
|
"txt_identity_details": "Identity Details",
|
||||||
"txt_ie_browser": "IE Browser",
|
"txt_ie_browser": "IE Browser",
|
||||||
"txt_create_invite_failed": "Failed to create invite",
|
"txt_create_invite_failed": "Failed to create invite",
|
||||||
|
"txt_delete_invalid": "Delete Invalid",
|
||||||
|
"txt_delete_invalid_invites": "Delete invalid invites",
|
||||||
|
"txt_delete_invalid_invites_confirm_message": "Delete all invalid invite codes? Active, unexpired invite codes will be kept.",
|
||||||
|
"txt_delete_invalid_invites_failed": "Failed to delete invalid invites",
|
||||||
|
"txt_delete_invite": "Delete invite",
|
||||||
|
"txt_delete_invite_confirm_message": "Delete this invite code? This cannot be undone.",
|
||||||
|
"txt_delete_invite_failed": "Failed to delete invite",
|
||||||
"txt_invite_code_required": "Invite Code (Required)",
|
"txt_invite_code_required": "Invite Code (Required)",
|
||||||
"txt_invite_created": "Invite created",
|
"txt_invite_created": "Invite created",
|
||||||
|
"txt_invite_deleted": "Invite deleted",
|
||||||
|
"txt_invalid_invites_deleted": "Invalid invites deleted",
|
||||||
"txt_invite_revoked": "Invite revoked",
|
"txt_invite_revoked": "Invite revoked",
|
||||||
"txt_revoke_invite_failed": "Failed to revoke invite",
|
"txt_revoke_invite_failed": "Failed to revoke invite",
|
||||||
"txt_invite_validity_hours": "Invite validity (hours)",
|
"txt_invite_validity_hours": "Invite validity (hours)",
|
||||||
@@ -1156,7 +1165,9 @@ const en: Record<string, string> = {
|
|||||||
"txt_log_action_admin_backup_settings_repair": "Repair backup settings",
|
"txt_log_action_admin_backup_settings_repair": "Repair backup settings",
|
||||||
"txt_log_action_admin_backup_settings_update": "Update backup settings",
|
"txt_log_action_admin_backup_settings_update": "Update backup settings",
|
||||||
"txt_log_action_admin_invite_create": "Create invite",
|
"txt_log_action_admin_invite_create": "Create invite",
|
||||||
|
"txt_log_action_admin_invite_delete": "Delete invite",
|
||||||
"txt_log_action_admin_invite_delete_all": "Clear invites",
|
"txt_log_action_admin_invite_delete_all": "Clear invites",
|
||||||
|
"txt_log_action_admin_invite_delete_invalid": "Delete invalid invites",
|
||||||
"txt_log_action_admin_invite_revoke": "Revoke invite",
|
"txt_log_action_admin_invite_revoke": "Revoke invite",
|
||||||
"txt_log_action_admin_user_delete": "Delete user",
|
"txt_log_action_admin_user_delete": "Delete user",
|
||||||
"txt_log_action_admin_user_status": "Change user status",
|
"txt_log_action_admin_user_status": "Change user status",
|
||||||
|
|||||||
@@ -582,8 +582,17 @@ const es: Record<string, string> = {
|
|||||||
"txt_identity_details": "Detalles de identidad",
|
"txt_identity_details": "Detalles de identidad",
|
||||||
"txt_ie_browser": "Navegador Internet Explorer",
|
"txt_ie_browser": "Navegador Internet Explorer",
|
||||||
"txt_create_invite_failed": "Error al crear invitación",
|
"txt_create_invite_failed": "Error al crear invitación",
|
||||||
|
"txt_delete_invalid": "Eliminar inválidas",
|
||||||
|
"txt_delete_invalid_invites": "Eliminar invitaciones inválidas",
|
||||||
|
"txt_delete_invalid_invites_confirm_message": "¿Eliminar todos los códigos de invitación inválidos? Se conservarán los códigos activos y no vencidos.",
|
||||||
|
"txt_delete_invalid_invites_failed": "Error al eliminar invitaciones inválidas",
|
||||||
|
"txt_delete_invite": "Eliminar invitación",
|
||||||
|
"txt_delete_invite_confirm_message": "¿Eliminar este código de invitación? Esta acción no se puede deshacer.",
|
||||||
|
"txt_delete_invite_failed": "Error al eliminar invitación",
|
||||||
"txt_invite_code_required": "Código de invitación (obligatorio)",
|
"txt_invite_code_required": "Código de invitación (obligatorio)",
|
||||||
"txt_invite_created": "Invitación creada",
|
"txt_invite_created": "Invitación creada",
|
||||||
|
"txt_invite_deleted": "Invitación eliminada",
|
||||||
|
"txt_invalid_invites_deleted": "Invitaciones inválidas eliminadas",
|
||||||
"txt_invite_revoked": "Invitación revocada",
|
"txt_invite_revoked": "Invitación revocada",
|
||||||
"txt_revoke_invite_failed": "Error al revocar invitación",
|
"txt_revoke_invite_failed": "Error al revocar invitación",
|
||||||
"txt_invite_validity_hours": "Validez de la invitación en horas",
|
"txt_invite_validity_hours": "Validez de la invitación en horas",
|
||||||
@@ -1156,7 +1165,9 @@ const es: Record<string, string> = {
|
|||||||
"txt_log_action_admin_backup_settings_repair": "Repair backup settings",
|
"txt_log_action_admin_backup_settings_repair": "Repair backup settings",
|
||||||
"txt_log_action_admin_backup_settings_update": "Update backup settings",
|
"txt_log_action_admin_backup_settings_update": "Update backup settings",
|
||||||
"txt_log_action_admin_invite_create": "Create invite",
|
"txt_log_action_admin_invite_create": "Create invite",
|
||||||
|
"txt_log_action_admin_invite_delete": "Delete invite",
|
||||||
"txt_log_action_admin_invite_delete_all": "Clear invites",
|
"txt_log_action_admin_invite_delete_all": "Clear invites",
|
||||||
|
"txt_log_action_admin_invite_delete_invalid": "Delete invalid invites",
|
||||||
"txt_log_action_admin_invite_revoke": "Revoke invite",
|
"txt_log_action_admin_invite_revoke": "Revoke invite",
|
||||||
"txt_log_action_admin_user_delete": "Delete user",
|
"txt_log_action_admin_user_delete": "Delete user",
|
||||||
"txt_log_action_admin_user_status": "Change user status",
|
"txt_log_action_admin_user_status": "Change user status",
|
||||||
|
|||||||
@@ -582,8 +582,17 @@ const ru: Record<string, string> = {
|
|||||||
"txt_identity_details": "Данные личности",
|
"txt_identity_details": "Данные личности",
|
||||||
"txt_ie_browser": "IE-браузер",
|
"txt_ie_browser": "IE-браузер",
|
||||||
"txt_create_invite_failed": "Не удалось создать приглашение",
|
"txt_create_invite_failed": "Не удалось создать приглашение",
|
||||||
|
"txt_delete_invalid": "Удалить недействительные",
|
||||||
|
"txt_delete_invalid_invites": "Удалить недействительные приглашения",
|
||||||
|
"txt_delete_invalid_invites_confirm_message": "Удалить все недействительные пригласительные коды? Активные и не истекшие коды будут сохранены.",
|
||||||
|
"txt_delete_invalid_invites_failed": "Не удалось удалить недействительные приглашения",
|
||||||
|
"txt_delete_invite": "Удалить приглашение",
|
||||||
|
"txt_delete_invite_confirm_message": "Удалить этот пригласительный код? Это действие нельзя отменить.",
|
||||||
|
"txt_delete_invite_failed": "Не удалось удалить приглашение",
|
||||||
"txt_invite_code_required": "Пригласительный код (обязательно)",
|
"txt_invite_code_required": "Пригласительный код (обязательно)",
|
||||||
"txt_invite_created": "Приглашение создано",
|
"txt_invite_created": "Приглашение создано",
|
||||||
|
"txt_invite_deleted": "Приглашение удалено",
|
||||||
|
"txt_invalid_invites_deleted": "Недействительные приглашения удалены",
|
||||||
"txt_invite_revoked": "Приглашение отозвано",
|
"txt_invite_revoked": "Приглашение отозвано",
|
||||||
"txt_revoke_invite_failed": "Не удалось отозвать приглашение",
|
"txt_revoke_invite_failed": "Не удалось отозвать приглашение",
|
||||||
"txt_invite_validity_hours": "Срок действия приглашения (часы)",
|
"txt_invite_validity_hours": "Срок действия приглашения (часы)",
|
||||||
@@ -1156,7 +1165,9 @@ const ru: Record<string, string> = {
|
|||||||
"txt_log_action_admin_backup_settings_repair": "Repair backup settings",
|
"txt_log_action_admin_backup_settings_repair": "Repair backup settings",
|
||||||
"txt_log_action_admin_backup_settings_update": "Update backup settings",
|
"txt_log_action_admin_backup_settings_update": "Update backup settings",
|
||||||
"txt_log_action_admin_invite_create": "Create invite",
|
"txt_log_action_admin_invite_create": "Create invite",
|
||||||
|
"txt_log_action_admin_invite_delete": "Delete invite",
|
||||||
"txt_log_action_admin_invite_delete_all": "Clear invites",
|
"txt_log_action_admin_invite_delete_all": "Clear invites",
|
||||||
|
"txt_log_action_admin_invite_delete_invalid": "Delete invalid invites",
|
||||||
"txt_log_action_admin_invite_revoke": "Revoke invite",
|
"txt_log_action_admin_invite_revoke": "Revoke invite",
|
||||||
"txt_log_action_admin_user_delete": "Delete user",
|
"txt_log_action_admin_user_delete": "Delete user",
|
||||||
"txt_log_action_admin_user_status": "Change user status",
|
"txt_log_action_admin_user_status": "Change user status",
|
||||||
|
|||||||
@@ -582,8 +582,17 @@ const zhCN: Record<string, string> = {
|
|||||||
"txt_identity_details": "身份详情",
|
"txt_identity_details": "身份详情",
|
||||||
"txt_ie_browser": "IE 浏览器",
|
"txt_ie_browser": "IE 浏览器",
|
||||||
"txt_create_invite_failed": "创建邀请码失败",
|
"txt_create_invite_failed": "创建邀请码失败",
|
||||||
|
"txt_delete_invalid": "删除无效",
|
||||||
|
"txt_delete_invalid_invites": "删除无效邀请码",
|
||||||
|
"txt_delete_invalid_invites_confirm_message": "确定删除所有无效邀请码吗?仍有效且未过期的邀请码会保留。",
|
||||||
|
"txt_delete_invalid_invites_failed": "删除无效邀请码失败",
|
||||||
|
"txt_delete_invite": "删除邀请码",
|
||||||
|
"txt_delete_invite_confirm_message": "确定删除该邀请码吗?删除后无法恢复。",
|
||||||
|
"txt_delete_invite_failed": "删除邀请码失败",
|
||||||
"txt_invite_code_required": "邀请码(必填)",
|
"txt_invite_code_required": "邀请码(必填)",
|
||||||
"txt_invite_created": "邀请码已创建",
|
"txt_invite_created": "邀请码已创建",
|
||||||
|
"txt_invite_deleted": "邀请码已删除",
|
||||||
|
"txt_invalid_invites_deleted": "无效邀请码已删除",
|
||||||
"txt_invite_revoked": "邀请码已撤销",
|
"txt_invite_revoked": "邀请码已撤销",
|
||||||
"txt_revoke_invite_failed": "撤销邀请码失败",
|
"txt_revoke_invite_failed": "撤销邀请码失败",
|
||||||
"txt_invite_validity_hours": "邀请码有效期(小时)",
|
"txt_invite_validity_hours": "邀请码有效期(小时)",
|
||||||
@@ -1156,7 +1165,9 @@ const zhCN: Record<string, string> = {
|
|||||||
"txt_log_action_admin_backup_settings_repair": "修复备份设置",
|
"txt_log_action_admin_backup_settings_repair": "修复备份设置",
|
||||||
"txt_log_action_admin_backup_settings_update": "更新备份设置",
|
"txt_log_action_admin_backup_settings_update": "更新备份设置",
|
||||||
"txt_log_action_admin_invite_create": "创建邀请",
|
"txt_log_action_admin_invite_create": "创建邀请",
|
||||||
|
"txt_log_action_admin_invite_delete": "删除邀请",
|
||||||
"txt_log_action_admin_invite_delete_all": "清空邀请",
|
"txt_log_action_admin_invite_delete_all": "清空邀请",
|
||||||
|
"txt_log_action_admin_invite_delete_invalid": "删除无效邀请",
|
||||||
"txt_log_action_admin_invite_revoke": "撤销邀请",
|
"txt_log_action_admin_invite_revoke": "撤销邀请",
|
||||||
"txt_log_action_admin_user_delete": "删除用户",
|
"txt_log_action_admin_user_delete": "删除用户",
|
||||||
"txt_log_action_admin_user_status": "修改用户状态",
|
"txt_log_action_admin_user_status": "修改用户状态",
|
||||||
|
|||||||
@@ -582,8 +582,17 @@ const zhTW: Record<string, string> = {
|
|||||||
"txt_identity_details": "身份詳情",
|
"txt_identity_details": "身份詳情",
|
||||||
"txt_ie_browser": "IE 瀏覽器",
|
"txt_ie_browser": "IE 瀏覽器",
|
||||||
"txt_create_invite_failed": "創建邀請碼失敗",
|
"txt_create_invite_failed": "創建邀請碼失敗",
|
||||||
|
"txt_delete_invalid": "刪除無效",
|
||||||
|
"txt_delete_invalid_invites": "刪除無效邀請碼",
|
||||||
|
"txt_delete_invalid_invites_confirm_message": "確定刪除所有無效邀請碼嗎?仍有效且未過期的邀請碼會保留。",
|
||||||
|
"txt_delete_invalid_invites_failed": "刪除無效邀請碼失敗",
|
||||||
|
"txt_delete_invite": "刪除邀請碼",
|
||||||
|
"txt_delete_invite_confirm_message": "確定刪除此邀請碼嗎?刪除後無法復原。",
|
||||||
|
"txt_delete_invite_failed": "刪除邀請碼失敗",
|
||||||
"txt_invite_code_required": "邀請碼(必填)",
|
"txt_invite_code_required": "邀請碼(必填)",
|
||||||
"txt_invite_created": "邀請碼已創建",
|
"txt_invite_created": "邀請碼已創建",
|
||||||
|
"txt_invite_deleted": "邀請碼已刪除",
|
||||||
|
"txt_invalid_invites_deleted": "無效邀請碼已刪除",
|
||||||
"txt_invite_revoked": "邀請碼已撤銷",
|
"txt_invite_revoked": "邀請碼已撤銷",
|
||||||
"txt_revoke_invite_failed": "撤銷邀請碼失敗",
|
"txt_revoke_invite_failed": "撤銷邀請碼失敗",
|
||||||
"txt_invite_validity_hours": "邀請碼有效期(小時)",
|
"txt_invite_validity_hours": "邀請碼有效期(小時)",
|
||||||
@@ -1156,7 +1165,9 @@ const zhTW: Record<string, string> = {
|
|||||||
"txt_log_action_admin_backup_settings_repair": "修復備份設定",
|
"txt_log_action_admin_backup_settings_repair": "修復備份設定",
|
||||||
"txt_log_action_admin_backup_settings_update": "更新備份設定",
|
"txt_log_action_admin_backup_settings_update": "更新備份設定",
|
||||||
"txt_log_action_admin_invite_create": "建立邀請",
|
"txt_log_action_admin_invite_create": "建立邀請",
|
||||||
|
"txt_log_action_admin_invite_delete": "刪除邀請",
|
||||||
"txt_log_action_admin_invite_delete_all": "清空邀請",
|
"txt_log_action_admin_invite_delete_all": "清空邀請",
|
||||||
|
"txt_log_action_admin_invite_delete_invalid": "刪除無效邀請",
|
||||||
"txt_log_action_admin_invite_revoke": "撤銷邀請",
|
"txt_log_action_admin_invite_revoke": "撤銷邀請",
|
||||||
"txt_log_action_admin_user_delete": "刪除使用者",
|
"txt_log_action_admin_user_delete": "刪除使用者",
|
||||||
"txt_log_action_admin_user_status": "修改使用者狀態",
|
"txt_log_action_admin_user_status": "修改使用者狀態",
|
||||||
|
|||||||
@@ -1,6 +1,114 @@
|
|||||||
import type { CiphersImportPayload } from '@/lib/api/vault';
|
import type { CiphersImportPayload } from '@/lib/api/vault';
|
||||||
import { addFolder, cardBrand, makeLoginCipher, nameFromUrl, normalizeUri, parseCsv, parseSerializedUris, processKvp, txt, val } from '@/lib/import-format-shared';
|
import { addFolder, cardBrand, makeLoginCipher, nameFromUrl, normalizeUri, parseCsv, parseSerializedUris, processKvp, txt, val } from '@/lib/import-format-shared';
|
||||||
|
|
||||||
|
type BitwardenCsvFieldLine = {
|
||||||
|
key: string;
|
||||||
|
value: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const NODEWARDEN_CSV_TYPE_FIELD = 'nodewardenType';
|
||||||
|
const NODEWARDEN_CSV_PREFIX_TYPES: Record<string, number> = {
|
||||||
|
card: 3,
|
||||||
|
identity: 4,
|
||||||
|
sshkey: 5,
|
||||||
|
};
|
||||||
|
const NODEWARDEN_CSV_TYPE_PREFIXES: Record<number, 'card' | 'identity' | 'sshKey'> = {
|
||||||
|
3: 'card',
|
||||||
|
4: 'identity',
|
||||||
|
5: 'sshKey',
|
||||||
|
};
|
||||||
|
const NODEWARDEN_CSV_OBJECT_FIELDS: Record<'card' | 'identity' | 'sshKey', readonly string[]> = {
|
||||||
|
card: ['cardholderName', 'brand', 'number', 'expMonth', 'expYear', 'code'],
|
||||||
|
identity: [
|
||||||
|
'title',
|
||||||
|
'firstName',
|
||||||
|
'middleName',
|
||||||
|
'lastName',
|
||||||
|
'username',
|
||||||
|
'company',
|
||||||
|
'ssn',
|
||||||
|
'passportNumber',
|
||||||
|
'licenseNumber',
|
||||||
|
'email',
|
||||||
|
'phone',
|
||||||
|
'address1',
|
||||||
|
'address2',
|
||||||
|
'address3',
|
||||||
|
'city',
|
||||||
|
'state',
|
||||||
|
'postalCode',
|
||||||
|
'country',
|
||||||
|
],
|
||||||
|
sshKey: ['privateKey', 'publicKey', 'keyFingerprint', 'fingerprint'],
|
||||||
|
};
|
||||||
|
|
||||||
|
// Parse the `fields` CSV column into key-value pairs.
|
||||||
|
// Lines without a `: ` delimiter are treated as continuations of the previous
|
||||||
|
// line's value, preserving multiline content such as SSH private keys.
|
||||||
|
function parseBitwardenCsvFieldLines(rawFields: unknown): BitwardenCsvFieldLine[] {
|
||||||
|
return String(rawFields || '')
|
||||||
|
.split(/\r?\n/)
|
||||||
|
.map((line) => line.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
.reduce<BitwardenCsvFieldLine[]>((acc, line) => {
|
||||||
|
const delim = line.lastIndexOf(': ');
|
||||||
|
if (delim < 0) {
|
||||||
|
// Continuation line — append to the previous entry's value.
|
||||||
|
if (acc.length > 0) {
|
||||||
|
acc[acc.length - 1].value += '\n' + line;
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}
|
||||||
|
// New key-value line.
|
||||||
|
const key = txt(line.slice(0, delim));
|
||||||
|
const value = txt(line.slice(delim + 2));
|
||||||
|
if (key && value) {
|
||||||
|
acc.push({ key, value });
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getNodeWardenCsvType(lines: BitwardenCsvFieldLine[]): number | null {
|
||||||
|
const typeLine = lines.find((line) => line.key === NODEWARDEN_CSV_TYPE_FIELD);
|
||||||
|
const normalized = txt(typeLine?.value).toLowerCase().replace(/[\s_-]+/g, '');
|
||||||
|
const type = NODEWARDEN_CSV_PREFIX_TYPES[normalized] ?? null;
|
||||||
|
if (!type) return null;
|
||||||
|
const prefix = NODEWARDEN_CSV_TYPE_PREFIXES[type];
|
||||||
|
return lines.some((line) => line.key.startsWith(`${prefix}.`)) ? type : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyBitwardenCustomFields(cipher: Record<string, unknown>, lines: BitwardenCsvFieldLine[]): void {
|
||||||
|
for (const line of lines) {
|
||||||
|
processKvp(cipher, line.key, line.value, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function restoreNodeWardenObject(lines: BitwardenCsvFieldLine[], prefix: 'card' | 'identity' | 'sshKey'): Record<string, unknown> {
|
||||||
|
const out: Record<string, unknown> = {};
|
||||||
|
const fieldPrefix = `${prefix}.`;
|
||||||
|
const allowedKeys = new Set(NODEWARDEN_CSV_OBJECT_FIELDS[prefix]);
|
||||||
|
for (const line of lines) {
|
||||||
|
if (!line.key.startsWith(fieldPrefix)) continue;
|
||||||
|
const key = line.key.slice(fieldPrefix.length);
|
||||||
|
if (!allowedKeys.has(key)) continue;
|
||||||
|
out[key] = line.value;
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
function nodeWardenMetadataLines(lines: BitwardenCsvFieldLine[]): Set<BitwardenCsvFieldLine> {
|
||||||
|
return new Set(
|
||||||
|
lines.filter(
|
||||||
|
(line) =>
|
||||||
|
line.key === NODEWARDEN_CSV_TYPE_FIELD ||
|
||||||
|
line.key.startsWith('card.') ||
|
||||||
|
line.key.startsWith('identity.') ||
|
||||||
|
line.key.startsWith('sshKey.')
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function parseChromeCsv(textRaw: string): CiphersImportPayload {
|
export function parseChromeCsv(textRaw: string): CiphersImportPayload {
|
||||||
const rows = parseCsv(textRaw);
|
const rows = parseCsv(textRaw);
|
||||||
const result: CiphersImportPayload = { ciphers: [], folders: [], folderRelationships: [] };
|
const result: CiphersImportPayload = { ciphers: [], folders: [], folderRelationships: [] };
|
||||||
@@ -62,19 +170,33 @@ export function parseSafariCsv(textRaw: string): CiphersImportPayload {
|
|||||||
export function parseBitwardenCsv(textRaw: string): CiphersImportPayload {
|
export function parseBitwardenCsv(textRaw: string): CiphersImportPayload {
|
||||||
const rows = parseCsv(textRaw);
|
const rows = parseCsv(textRaw);
|
||||||
const result: CiphersImportPayload = { ciphers: [], folders: [], folderRelationships: [] };
|
const result: CiphersImportPayload = { ciphers: [], folders: [], folderRelationships: [] };
|
||||||
const applyBitwardenCustomFields = (cipher: Record<string, unknown>, rawFields: unknown) => {
|
|
||||||
const lines = String(rawFields || '')
|
|
||||||
.split(/\r?\n/)
|
|
||||||
.map((line) => line.trim())
|
|
||||||
.filter(Boolean);
|
|
||||||
for (const line of lines) {
|
|
||||||
const delim = line.lastIndexOf(': ');
|
|
||||||
if (delim < 0) continue;
|
|
||||||
processKvp(cipher, line.slice(0, delim), line.slice(delim + 2), false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
for (const row of rows) {
|
for (const row of rows) {
|
||||||
const type = txt(row.type).toLowerCase() || 'login';
|
const type = txt(row.type).toLowerCase() || 'login';
|
||||||
|
const fieldLines = parseBitwardenCsvFieldLines(row.fields);
|
||||||
|
const restoredNodeWardenType = type === 'note' ? getNodeWardenCsvType(fieldLines) : null;
|
||||||
|
if (restoredNodeWardenType === 3 || restoredNodeWardenType === 4 || restoredNodeWardenType === 5) {
|
||||||
|
const metadataLines = nodeWardenMetadataLines(fieldLines);
|
||||||
|
const customLines = fieldLines.filter((line) => !metadataLines.has(line));
|
||||||
|
const cipher: Record<string, unknown> = {
|
||||||
|
type: restoredNodeWardenType,
|
||||||
|
name: val(row.name, '--'),
|
||||||
|
notes: val(row.notes),
|
||||||
|
favorite: txt(row.favorite) === '1',
|
||||||
|
reprompt: Number(row.reprompt ?? 0) || 0,
|
||||||
|
key: null,
|
||||||
|
login: null,
|
||||||
|
card: restoredNodeWardenType === 3 ? restoreNodeWardenObject(fieldLines, 'card') : null,
|
||||||
|
identity: restoredNodeWardenType === 4 ? restoreNodeWardenObject(fieldLines, 'identity') : null,
|
||||||
|
secureNote: null,
|
||||||
|
fields: [],
|
||||||
|
passwordHistory: null,
|
||||||
|
sshKey: restoredNodeWardenType === 5 ? restoreNodeWardenObject(fieldLines, 'sshKey') : null,
|
||||||
|
};
|
||||||
|
applyBitwardenCustomFields(cipher, customLines);
|
||||||
|
const idx = result.ciphers.push(cipher) - 1;
|
||||||
|
addFolder(result, row.folder, idx);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (type === 'note' || type === 'secure note' || type === 'securenote') {
|
if (type === 'note' || type === 'secure note' || type === 'securenote') {
|
||||||
const cipher = {
|
const cipher = {
|
||||||
type: 2,
|
type: 2,
|
||||||
@@ -91,7 +213,7 @@ export function parseBitwardenCsv(textRaw: string): CiphersImportPayload {
|
|||||||
passwordHistory: null,
|
passwordHistory: null,
|
||||||
sshKey: null,
|
sshKey: null,
|
||||||
};
|
};
|
||||||
applyBitwardenCustomFields(cipher, row.fields);
|
applyBitwardenCustomFields(cipher, fieldLines);
|
||||||
const idx = result.ciphers.push(cipher) - 1;
|
const idx = result.ciphers.push(cipher) - 1;
|
||||||
addFolder(result, row.folder, idx);
|
addFolder(result, row.folder, idx);
|
||||||
continue;
|
continue;
|
||||||
@@ -101,7 +223,7 @@ export function parseBitwardenCsv(textRaw: string): CiphersImportPayload {
|
|||||||
cipher.notes = val(row.notes);
|
cipher.notes = val(row.notes);
|
||||||
cipher.favorite = txt(row.favorite) === '1';
|
cipher.favorite = txt(row.favorite) === '1';
|
||||||
cipher.reprompt = Number(row.reprompt ?? 0) || 0;
|
cipher.reprompt = Number(row.reprompt ?? 0) || 0;
|
||||||
applyBitwardenCustomFields(cipher, row.fields);
|
applyBitwardenCustomFields(cipher, fieldLines);
|
||||||
const login = cipher.login as Record<string, unknown>;
|
const login = cipher.login as Record<string, unknown>;
|
||||||
login.username = val(row.login_username, val(row.username));
|
login.username = val(row.login_username, val(row.username));
|
||||||
login.password = val(row.login_password, val(row.password));
|
login.password = val(row.login_password, val(row.password));
|
||||||
|
|||||||
@@ -172,6 +172,10 @@ input[type='file'].input::file-selector-button:hover {
|
|||||||
@apply shrink-0;
|
@apply shrink-0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.btn-icon-spin {
|
||||||
|
animation: spin 0.9s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
.btn.full {
|
.btn.full {
|
||||||
@apply my-2.5 h-12 w-full;
|
@apply my-2.5 h-12 w-full;
|
||||||
font-size: var(--font-md);
|
font-size: var(--font-md);
|
||||||
|
|||||||
Reference in New Issue
Block a user