Compare commits

...
25 Commits
Author SHA1 Message Date
shuaiplus ce3674669e feat: update version to 1.7.2 in package.json, package-lock.json, and app-version.ts 2026-07-01 13:36:59 +08:00
shuaiplus aa7b87e041 Merge branch 'main' of https://github.com/shuaiplus/nodewarden 2026-07-01 13:28:26 +08:00
shuaiplus e4215b4025 feat: add fill-assist handlers and update device response type 2026-07-01 13:28:22 +08:00
dependabot[bot]andShuai 8d292ca7b8 chore(deps): bump the npm-minor-and-patch group across 1 directory with 15 updates
Bumps the npm-minor-and-patch group with 15 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [@noble/hashes](https://github.com/paulmillr/noble-hashes) | `2.0.1` | `2.2.0` |
| [@simplewebauthn/server](https://github.com/MasterKale/SimpleWebAuthn/tree/HEAD/packages/server) | `13.3.1` | `13.3.2` |
| [@tanstack/react-query](https://github.com/TanStack/query/tree/HEAD/packages/react-query) | `5.90.21` | `5.101.2` |
| [@zip.js/zip.js](https://github.com/gildas-lormeau/zip.js) | `2.8.22` | `2.8.26` |
| [fflate](https://github.com/101arrowz/fflate) | `0.8.2` | `0.8.3` |
| [preact](https://github.com/preactjs/preact) | `10.28.4` | `10.29.3` |
| [wouter](https://github.com/molefrog/wouter) | `3.9.0` | `3.10.0` |
| [@cloudflare/workers-types](https://github.com/cloudflare/workerd) | `4.20260609.1` | `4.20260630.1` |
| [@preact/preset-vite](https://github.com/preactjs/preset-vite) | `2.10.3` | `2.10.5` |
| [autoprefixer](https://github.com/postcss/autoprefixer) | `10.4.21` | `10.5.2` |
| [opencc-js](https://github.com/nk2028/opencc-js) | `1.0.5` | `1.3.2` |
| [postcss](https://github.com/postcss/postcss) | `8.5.15` | `8.5.16` |
| [tailwindcss](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/tailwindcss) | `3.4.17` | `3.4.19` |
| [tsx](https://github.com/privatenumber/tsx) | `4.21.0` | `4.22.4` |
| [wrangler](https://github.com/cloudflare/workers-sdk/tree/HEAD/packages/wrangler) | `4.98.0` | `4.105.0` |



Updates `@noble/hashes` from 2.0.1 to 2.2.0
- [Release notes](https://github.com/paulmillr/noble-hashes/releases)
- [Commits](https://github.com/paulmillr/noble-hashes/compare/2.0.1...2.2.0)

Updates `@simplewebauthn/server` from 13.3.1 to 13.3.2
- [Release notes](https://github.com/MasterKale/SimpleWebAuthn/releases)
- [Changelog](https://github.com/MasterKale/SimpleWebAuthn/blob/master/CHANGELOG.md)
- [Commits](https://github.com/MasterKale/SimpleWebAuthn/commits/v13.3.2/packages/server)

Updates `@tanstack/react-query` from 5.90.21 to 5.101.2
- [Release notes](https://github.com/TanStack/query/releases)
- [Changelog](https://github.com/TanStack/query/blob/main/packages/react-query/CHANGELOG.md)
- [Commits](https://github.com/TanStack/query/commits/@tanstack/react-query@5.101.2/packages/react-query)

Updates `@zip.js/zip.js` from 2.8.22 to 2.8.26
- [Release notes](https://github.com/gildas-lormeau/zip.js/releases)
- [Commits](https://github.com/gildas-lormeau/zip.js/compare/v2.8.22...v2.8.26)

Updates `fflate` from 0.8.2 to 0.8.3
- [Release notes](https://github.com/101arrowz/fflate/releases)
- [Changelog](https://github.com/101arrowz/fflate/blob/master/CHANGELOG.md)
- [Commits](https://github.com/101arrowz/fflate/compare/v0.8.2...v0.8.3)

Updates `preact` from 10.28.4 to 10.29.3
- [Release notes](https://github.com/preactjs/preact/releases)
- [Commits](https://github.com/preactjs/preact/compare/10.28.4...10.29.3)

Updates `wouter` from 3.9.0 to 3.10.0
- [Release notes](https://github.com/molefrog/wouter/releases)
- [Commits](https://github.com/molefrog/wouter/commits)

Updates `@cloudflare/workers-types` from 4.20260609.1 to 4.20260630.1
- [Release notes](https://github.com/cloudflare/workerd/releases)
- [Changelog](https://github.com/cloudflare/workerd/blob/main/RELEASE.md)
- [Commits](https://github.com/cloudflare/workerd/commits)

Updates `@preact/preset-vite` from 2.10.3 to 2.10.5
- [Release notes](https://github.com/preactjs/preset-vite/releases)
- [Commits](https://github.com/preactjs/preset-vite/compare/2.10.3...2.10.5)

Updates `autoprefixer` from 10.4.21 to 10.5.2
- [Release notes](https://github.com/postcss/autoprefixer/releases)
- [Changelog](https://github.com/postcss/autoprefixer/blob/main/CHANGELOG.md)
- [Commits](https://github.com/postcss/autoprefixer/compare/10.4.21...10.5.2)

Updates `opencc-js` from 1.0.5 to 1.3.2
- [Release notes](https://github.com/nk2028/opencc-js/releases)
- [Changelog](https://github.com/nk2028/opencc-js/blob/main/CHANGELOG.md)
- [Commits](https://github.com/nk2028/opencc-js/compare/v1.0.5...v1.3.2)

Updates `postcss` from 8.5.15 to 8.5.16
- [Release notes](https://github.com/postcss/postcss/releases)
- [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/postcss/postcss/compare/8.5.15...8.5.16)

Updates `tailwindcss` from 3.4.17 to 3.4.19
- [Release notes](https://github.com/tailwindlabs/tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss/commits/v3.4.19/packages/tailwindcss)

Updates `tsx` from 4.21.0 to 4.22.4
- [Release notes](https://github.com/privatenumber/tsx/releases)
- [Changelog](https://github.com/privatenumber/tsx/blob/master/release.config.cjs)
- [Commits](https://github.com/privatenumber/tsx/compare/v4.21.0...v4.22.4)

Updates `wrangler` from 4.98.0 to 4.105.0
- [Release notes](https://github.com/cloudflare/workers-sdk/releases)
- [Commits](https://github.com/cloudflare/workers-sdk/commits/wrangler@4.105.0/packages/wrangler)

---
updated-dependencies:
- dependency-name: "@cloudflare/workers-types"
  dependency-version: 4.20260630.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm-minor-and-patch
- dependency-name: "@noble/hashes"
  dependency-version: 2.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-minor-and-patch
- dependency-name: "@preact/preset-vite"
  dependency-version: 2.10.5
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm-minor-and-patch
- dependency-name: "@simplewebauthn/server"
  dependency-version: 13.3.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-minor-and-patch
- dependency-name: "@tanstack/react-query"
  dependency-version: 5.101.2
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-minor-and-patch
- dependency-name: "@zip.js/zip.js"
  dependency-version: 2.8.26
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-minor-and-patch
- dependency-name: autoprefixer
  dependency-version: 10.5.2
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm-minor-and-patch
- dependency-name: fflate
  dependency-version: 0.8.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-minor-and-patch
- dependency-name: opencc-js
  dependency-version: 1.3.2
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm-minor-and-patch
- dependency-name: postcss
  dependency-version: 8.5.16
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm-minor-and-patch
- dependency-name: preact
  dependency-version: 10.29.3
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-minor-and-patch
- dependency-name: tailwindcss
  dependency-version: 3.4.19
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm-minor-and-patch
- dependency-name: tsx
  dependency-version: 4.22.4
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm-minor-and-patch
- dependency-name: wouter
  dependency-version: 3.10.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-minor-and-patch
- dependency-name: wrangler
  dependency-version: 4.105.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm-minor-and-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-30 13:55:20 +08:00
shuaiplus 5dd9dff045 ci: update actions/checkout and other actions to specific versions 2026-06-30 13:49:33 +08:00
shuaiplus 709a8c1768 Merge pull request #279 from dependabot/github_actions/github-actions-db669df06a 2026-06-30 13:44:53 +08:00
shuaiplus e2c3516ce9 Merge pull request #284 from dependabot/npm_and_yarn/lucide-preact-1.22.0 2026-06-30 13:43:17 +08:00
shuaiplus 55b5c57f9e Merge pull request #283 from dependabot/npm_and_yarn/typescript-6.0.3 2026-06-30 13:43:07 +08:00
shuaiplus b6fb62603b Merge pull request #282 from dependabot/npm_and_yarn/types/node-26.0.1 2026-06-30 13:42:57 +08:00
shuaiplus 35071c2719 ci: limit risky dependabot updates 2026-06-30 13:36:00 +08:00
dependabot[bot]andGitHub 5bd7dab277 chore(deps): bump lucide-preact from 0.575.0 to 1.22.0
Bumps [lucide-preact](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-preact) from 0.575.0 to 1.22.0.
- [Release notes](https://github.com/lucide-icons/lucide/releases)
- [Commits](https://github.com/lucide-icons/lucide/commits/1.22.0/packages/lucide-preact)

---
updated-dependencies:
- dependency-name: lucide-preact
  dependency-version: 1.22.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-30 05:28:03 +00:00
dependabot[bot]andGitHub 99f2d7f444 chore(deps-dev): bump typescript from 5.9.3 to 6.0.3
Bumps [typescript](https://github.com/microsoft/TypeScript) from 5.9.3 to 6.0.3.
- [Release notes](https://github.com/microsoft/TypeScript/releases)
- [Commits](https://github.com/microsoft/TypeScript/compare/v5.9.3...v6.0.3)

---
updated-dependencies:
- dependency-name: typescript
  dependency-version: 6.0.3
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-30 05:28:02 +00:00
dependabot[bot]andGitHub fb9a2aeda1 chore(deps-dev): bump @types/node from 25.2.3 to 26.0.1
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 25.2.3 to 26.0.1.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-version: 26.0.1
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-30 05:27:57 +00:00
dependabot[bot]andGitHub c87e6ac984 chore(deps): bump the github-actions group with 3 updates
Bumps the github-actions group with 3 updates: [actions/checkout](https://github.com/actions/checkout), [actions/setup-node](https://github.com/actions/setup-node) and [peter-evans/create-pull-request](https://github.com/peter-evans/create-pull-request).


Updates `actions/checkout` from 4 to 7
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v7)

Updates `actions/setup-node` from 4 to 6
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/v4...v6)

Updates `peter-evans/create-pull-request` from 6 to 8
- [Release notes](https://github.com/peter-evans/create-pull-request/releases)
- [Commits](https://github.com/peter-evans/create-pull-request/compare/v6...v8)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
- dependency-name: actions/setup-node
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
- dependency-name: peter-evans/create-pull-request
  dependency-version: '8'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-30 05:26:28 +00:00
shuaiplus a6f1c6dea2 Merge branch 'beta' 2026-06-30 13:25:22 +08:00
shuaiplus 49c872a8ec ci: fix sync workflow shell lint 2026-06-30 13:18:02 +08:00
shuaiplus 78af1f9bdd ci: skip scorecard outside main 2026-06-30 13:14:23 +08:00
shuaiplus 32b3d2ade1 chore: override ws vulnerability 2026-06-30 13:11:07 +08:00
shuaiplus 64f26e76f6 chore: add security automation workflows 2026-06-30 13:05:04 +08:00
rootphantomerandShuai 68c42a0330 fix: preserve multiline values (e.g. SSH private keys) during CSV import
parseBitwardenCsvFieldLines previously discarded any field line that did not
contain the ': ' delimiter, truncating multiline values like OpenSSH private
keys to only their first line.

Replace the map+filter pipeline with a reduce that accumulates continuation
lines (lines without ': ') into the previous entry's value, joined by '\n'.
This preserves the full private key content through a CSV round-trip.

Fixes: CSV export to import of SSH key items where the private key body was
silently dropped.
2026-06-30 11:44:03 +08:00
shuaiplus 0d1bb196e2 feat: add functionality to delete invalid invites and update related components 2026-06-29 12:12:13 +08:00
shuaiplus e31f82c0d6 feat: update project wiki link and remove obsolete security scripts and workflows 2026-06-29 11:57:53 +08:00
shuaiplus f82dcc3c17 feat: rename revokeInvite to deleteInvite and update related functionality 2026-06-29 11:47:05 +08:00
shuaiplus 4378e1b430 feat: add pendingAuthRequestsRefreshing state to improve loading feedback in auth request components 2026-06-29 11:29:35 +08:00
shuaiplus 5eeaf4e32e feat: enhance Bitwarden CSV parsing with custom field handling and metadata restoration 2026-06-29 11:10:41 +08:00
36 changed files with 938 additions and 899 deletions
+1 -1
View File
@@ -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.
请先查看文档,常见问题和排查步骤可能已经覆盖了你的问题。 请先查看文档,常见问题和排查步骤可能已经覆盖了你的问题。
+33
View File
@@ -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:
- "*"
-467
View File
@@ -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) => `![${label}](https://img.shields.io/badge/${label.replace(/ /g, '_')}-${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 '![Pass](https://img.shields.io/badge/Status-PASS-success?style=for-the-badge)';
if (status === 'WARN' || status === 'INFO') return '![Warning](https://img.shields.io/badge/Status-NOTICE-orange?style=for-the-badge)';
return '![Fail](https://img.shields.io/badge/Status-FAIL-red?style=for-the-badge)';
}
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);
});
+44
View File
@@ -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 }}"
+200
View File
@@ -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
-142
View File
@@ -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
+3 -3
View File
@@ -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
+26 -18
View File
@@ -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
+1
View File
@@ -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
+197 -185
View File
@@ -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
View File
@@ -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
View File
@@ -1 +1 @@
export const APP_VERSION = '1.7.1'; export const APP_VERSION = '1.7.2';
+17 -6
View File
@@ -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,
+2
View File
@@ -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,
+36
View File
@@ -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
View File
@@ -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
View File
@@ -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);
+12 -4
View File
@@ -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);
+8 -3
View File
@@ -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> {
+1
View File
@@ -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
View File
@@ -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),
+9 -7
View File
@@ -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>
+6 -2
View File
@@ -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}
+39 -9
View File
@@ -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) {
try { onSetConfirm({
await revokeInvite(authedFetch, code); title: t('txt_delete_invite'),
await refetchInvites(); message: t('txt_delete_invite_confirm_message'),
onNotify('success', t('txt_invite_revoked')); danger: true,
} catch (error) { onConfirm: () => {
onNotify('error', error instanceof Error ? error.message : t('txt_revoke_invite_failed')); onSetConfirm(null);
} void (async () => {
try {
await deleteInvite(authedFetch, code);
await refetchInvites();
onNotify('success', t('txt_invite_deleted'));
} catch (error) {
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() {
+7 -2
View File
@@ -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
View File
@@ -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) => {
+40 -2
View File
@@ -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');
} }
+11
View File
@@ -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",
+11
View File
@@ -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",
+11
View File
@@ -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",
+11
View File
@@ -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": "修改用户状态",
+11
View File
@@ -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": "修改使用者狀態",
+135 -13
View File
@@ -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));
+4
View File
@@ -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);