mirror of
https://github.com/shuaiplus/nodewarden.git
synced 2026-06-20 13:00:39 +00:00
enhance cipher and identity handling with new fields and rate limit adjustments
This commit is contained in:
@@ -1,36 +0,0 @@
|
|||||||
# 自动同步上游更新
|
|
||||||
|
|
||||||
这个 GitHub Actions 工作流会自动将上游仓库(shuaiplus/nodewarden)的更新同步到你的 fork。
|
|
||||||
|
|
||||||
## 功能特性
|
|
||||||
|
|
||||||
- ✅ 每天自动检查并同步上游更新
|
|
||||||
- ✅ 支持手动触发同步
|
|
||||||
- ✅ 自动处理简单的合并
|
|
||||||
- ⚠️ 遇到冲突时会提醒你手动处理
|
|
||||||
|
|
||||||
## 如何启用
|
|
||||||
|
|
||||||
1. 在你 fork 的仓库中,进入 **Actions** 标签页
|
|
||||||
2. 点击 **I understand my workflows, go ahead and enable them**
|
|
||||||
3. 完成!工作流会自动运行
|
|
||||||
|
|
||||||
## 手动触发
|
|
||||||
|
|
||||||
1. 进入 **Actions** 标签页
|
|
||||||
2. 选择 **Sync Fork with Upstream**
|
|
||||||
3. 点击 **Run workflow** → **Run workflow**
|
|
||||||
|
|
||||||
## 注意事项
|
|
||||||
|
|
||||||
- 如果你修改了代码,可能会产生合并冲突
|
|
||||||
- 遇到冲突时,工作流会失败,需要你手动解决
|
|
||||||
- 建议不要修改核心代码,只修改配置文件
|
|
||||||
|
|
||||||
## 禁用自动同步
|
|
||||||
|
|
||||||
如果你不想自动同步:
|
|
||||||
|
|
||||||
1. 进入 **Actions** 标签页
|
|
||||||
2. 选择 **Sync Fork with Upstream**
|
|
||||||
3. 点击右上角的 **···** → **Disable workflow**
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
name: Sync Fork with Upstream
|
|
||||||
|
|
||||||
on:
|
|
||||||
schedule:
|
|
||||||
# Run every day at 2:00 AM UTC
|
|
||||||
- cron: '0 2 * * *'
|
|
||||||
workflow_dispatch:
|
|
||||||
# Allow manual trigger
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
sync:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Configure Git
|
|
||||||
run: |
|
|
||||||
git config user.name "github-actions[bot]"
|
|
||||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
|
||||||
|
|
||||||
- name: Sync with upstream
|
|
||||||
run: |
|
|
||||||
# Add upstream repository
|
|
||||||
git remote add upstream https://github.com/shuaiplus/nodewarden.git || true
|
|
||||||
|
|
||||||
# Fetch upstream changes
|
|
||||||
git fetch upstream
|
|
||||||
|
|
||||||
# Check if there are updates
|
|
||||||
BEHIND=$(git rev-list --count HEAD..upstream/main)
|
|
||||||
|
|
||||||
if [ "$BEHIND" -gt 0 ]; then
|
|
||||||
echo "Found $BEHIND commits behind upstream. Syncing..."
|
|
||||||
|
|
||||||
# Try to merge upstream/main into current branch
|
|
||||||
if git merge upstream/main --no-edit; then
|
|
||||||
echo "Merge successful!"
|
|
||||||
git push origin main
|
|
||||||
else
|
|
||||||
echo "Merge conflict detected. Please resolve manually."
|
|
||||||
echo "::warning::Failed to auto-sync due to merge conflicts. Manual intervention required."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
echo "Already up to date with upstream."
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Create summary
|
|
||||||
if: always()
|
|
||||||
run: |
|
|
||||||
echo "## Sync Summary" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "- **Upstream**: shuaiplus/nodewarden" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "- **Branch**: main" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "- **Status**: ${{ job.status }}" >> $GITHUB_STEP_SUMMARY
|
|
||||||
@@ -4,6 +4,7 @@ node_modules/
|
|||||||
# Wrangler
|
# Wrangler
|
||||||
.wrangler/
|
.wrangler/
|
||||||
.dev.vars
|
.dev.vars
|
||||||
|
wrangler.my.toml
|
||||||
RELEASE_NOTES.md
|
RELEASE_NOTES.md
|
||||||
|
|
||||||
# Build output
|
# Build output
|
||||||
|
|||||||
@@ -27,9 +27,9 @@ A **Bitwarden-compatible** server that runs on **Cloudflare Workers**, designed
|
|||||||
|
|
||||||
## Tested clients / platforms
|
## Tested clients / platforms
|
||||||
|
|
||||||
- ✅ Windows desktop client
|
- ✅ Windows desktop client(v2026.1.0)
|
||||||
- ✅ Mobile app (Android / iOS)
|
- ✅ Android app (v2026.1.0)
|
||||||
- ✅ Browser extension
|
- ✅ Browser extension(v2026.1.0)
|
||||||
- ⬜ macOS desktop client (not tested)
|
- ⬜ macOS desktop client (not tested)
|
||||||
- ⬜ Linux desktop client (not tested)
|
- ⬜ Linux desktop client (not tested)
|
||||||
|
|
||||||
|
|||||||
+3
-3
@@ -27,9 +27,9 @@ English:[`README.md`](./README.md)
|
|||||||
- ✅ 兼容常见的 Bitwarden 官方客户端
|
- ✅ 兼容常见的 Bitwarden 官方客户端
|
||||||
|
|
||||||
## 测试情况:
|
## 测试情况:
|
||||||
- ✅ Windows 客户端
|
- ✅ Windows 客户端(v2026.1.0)
|
||||||
- ✅ 手机 App(Android / iOS)
|
- ✅ Android App(v2026.1.0)
|
||||||
- ✅ 浏览器扩展
|
- ✅ 浏览器扩展(v2026.1.0)
|
||||||
- ⬜ macOS 客户端(未测试)
|
- ⬜ macOS 客户端(未测试)
|
||||||
- ⬜ Linux 客户端(未测试)
|
- ⬜ Linux 客户端(未测试)
|
||||||
---
|
---
|
||||||
|
|||||||
+3
-2
@@ -7,8 +7,9 @@
|
|||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "wrangler dev",
|
"dev": "wrangler dev -c wrangler.dev.toml",
|
||||||
"deploy": "wrangler deploy"
|
"deploymy": "wrangler deploy -c wrangler.my.toml",
|
||||||
|
"deploy": "wrangler deploy "
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"bitwarden",
|
"bitwarden",
|
||||||
|
|||||||
@@ -97,6 +97,7 @@ export async function handleGetProfile(request: Request, env: Env, userId: strin
|
|||||||
twoFactorEnabled: false,
|
twoFactorEnabled: false,
|
||||||
key: user.key,
|
key: user.key,
|
||||||
privateKey: user.privateKey,
|
privateKey: user.privateKey,
|
||||||
|
accountKeys: null,
|
||||||
securityStamp: user.securityStamp || user.id,
|
securityStamp: user.securityStamp || user.id,
|
||||||
organizations: [],
|
organizations: [],
|
||||||
providers: [],
|
providers: [],
|
||||||
|
|||||||
@@ -304,7 +304,7 @@ function formatCipherResponse(cipher: Cipher, attachments: Attachment[]): any {
|
|||||||
id: cipher.id,
|
id: cipher.id,
|
||||||
organizationId: null,
|
organizationId: null,
|
||||||
folderId: cipher.folderId,
|
folderId: cipher.folderId,
|
||||||
type: cipher.type,
|
type: Number(cipher.type) || 1,
|
||||||
name: cipher.name,
|
name: cipher.name,
|
||||||
notes: cipher.notes,
|
notes: cipher.notes,
|
||||||
favorite: cipher.favorite,
|
favorite: cipher.favorite,
|
||||||
@@ -312,6 +312,7 @@ function formatCipherResponse(cipher: Cipher, attachments: Attachment[]): any {
|
|||||||
card: cipher.card,
|
card: cipher.card,
|
||||||
identity: cipher.identity,
|
identity: cipher.identity,
|
||||||
secureNote: cipher.secureNote,
|
secureNote: cipher.secureNote,
|
||||||
|
sshKey: cipher.sshKey,
|
||||||
fields: cipher.fields,
|
fields: cipher.fields,
|
||||||
passwordHistory: cipher.passwordHistory,
|
passwordHistory: cipher.passwordHistory,
|
||||||
reprompt: cipher.reprompt,
|
reprompt: cipher.reprompt,
|
||||||
@@ -319,9 +320,13 @@ function formatCipherResponse(cipher: Cipher, attachments: Attachment[]): any {
|
|||||||
creationDate: cipher.createdAt,
|
creationDate: cipher.createdAt,
|
||||||
revisionDate: cipher.updatedAt,
|
revisionDate: cipher.updatedAt,
|
||||||
deletedDate: cipher.deletedAt,
|
deletedDate: cipher.deletedAt,
|
||||||
|
archivedDate: null,
|
||||||
edit: true,
|
edit: true,
|
||||||
viewPassword: true,
|
viewPassword: true,
|
||||||
permissions: null,
|
permissions: {
|
||||||
|
delete: true,
|
||||||
|
restore: true,
|
||||||
|
},
|
||||||
object: 'cipher',
|
object: 'cipher',
|
||||||
collectionIds: [],
|
collectionIds: [],
|
||||||
attachments: attachments.length > 0 ? attachments.map(a => ({
|
attachments: attachments.length > 0 ? attachments.map(a => ({
|
||||||
@@ -330,8 +335,11 @@ function formatCipherResponse(cipher: Cipher, attachments: Attachment[]): any {
|
|||||||
size: String(a.size),
|
size: String(a.size),
|
||||||
sizeName: a.sizeName,
|
sizeName: a.sizeName,
|
||||||
key: a.key,
|
key: a.key,
|
||||||
|
url: null,
|
||||||
object: 'attachment',
|
object: 'attachment',
|
||||||
})) : null,
|
})) : null,
|
||||||
|
key: cipher.key,
|
||||||
|
encryptedFor: null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+16
-6
@@ -13,6 +13,7 @@ function formatAttachments(attachments: Attachment[]): any[] | null {
|
|||||||
size: String(a.size),
|
size: String(a.size),
|
||||||
sizeName: a.sizeName,
|
sizeName: a.sizeName,
|
||||||
key: a.key,
|
key: a.key,
|
||||||
|
url: null,
|
||||||
object: 'attachment',
|
object: 'attachment',
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
@@ -23,7 +24,7 @@ function cipherToResponse(cipher: Cipher, attachments: Attachment[] = []): Ciphe
|
|||||||
id: cipher.id,
|
id: cipher.id,
|
||||||
organizationId: null,
|
organizationId: null,
|
||||||
folderId: cipher.folderId,
|
folderId: cipher.folderId,
|
||||||
type: cipher.type,
|
type: Number(cipher.type) || 1,
|
||||||
name: cipher.name,
|
name: cipher.name,
|
||||||
notes: cipher.notes,
|
notes: cipher.notes,
|
||||||
favorite: cipher.favorite,
|
favorite: cipher.favorite,
|
||||||
@@ -31,6 +32,7 @@ function cipherToResponse(cipher: Cipher, attachments: Attachment[] = []): Ciphe
|
|||||||
card: cipher.card,
|
card: cipher.card,
|
||||||
identity: cipher.identity,
|
identity: cipher.identity,
|
||||||
secureNote: cipher.secureNote,
|
secureNote: cipher.secureNote,
|
||||||
|
sshKey: cipher.sshKey,
|
||||||
fields: cipher.fields,
|
fields: cipher.fields,
|
||||||
passwordHistory: cipher.passwordHistory,
|
passwordHistory: cipher.passwordHistory,
|
||||||
reprompt: cipher.reprompt,
|
reprompt: cipher.reprompt,
|
||||||
@@ -38,16 +40,18 @@ function cipherToResponse(cipher: Cipher, attachments: Attachment[] = []): Ciphe
|
|||||||
creationDate: cipher.createdAt,
|
creationDate: cipher.createdAt,
|
||||||
revisionDate: cipher.updatedAt,
|
revisionDate: cipher.updatedAt,
|
||||||
deletedDate: cipher.deletedAt,
|
deletedDate: cipher.deletedAt,
|
||||||
|
archivedDate: null,
|
||||||
edit: true,
|
edit: true,
|
||||||
viewPassword: true,
|
viewPassword: true,
|
||||||
permissions: {
|
permissions: {
|
||||||
delete: true,
|
delete: true,
|
||||||
restore: true,
|
restore: true,
|
||||||
edit: true,
|
|
||||||
},
|
},
|
||||||
object: 'cipher',
|
object: 'cipher',
|
||||||
collectionIds: [],
|
collectionIds: [],
|
||||||
attachments: formatAttachments(attachments),
|
attachments: formatAttachments(attachments),
|
||||||
|
key: cipher.key,
|
||||||
|
encryptedFor: null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,13 +107,14 @@ export async function handleCreateCipher(request: Request, env: Env, userId: str
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Handle nested cipher object (from some clients)
|
// Handle nested cipher object (from some clients)
|
||||||
const cipherData = body.cipher || body;
|
// Android client sends PascalCase "Cipher" for organization ciphers
|
||||||
|
const cipherData = body.Cipher || body.cipher || body;
|
||||||
|
|
||||||
const now = new Date().toISOString();
|
const now = new Date().toISOString();
|
||||||
const cipher: Cipher = {
|
const cipher: Cipher = {
|
||||||
id: generateUUID(),
|
id: generateUUID(),
|
||||||
userId: userId,
|
userId: userId,
|
||||||
type: cipherData.type,
|
type: Number(cipherData.type) || 1,
|
||||||
folderId: cipherData.folderId || null,
|
folderId: cipherData.folderId || null,
|
||||||
name: cipherData.name,
|
name: cipherData.name,
|
||||||
notes: cipherData.notes || null,
|
notes: cipherData.notes || null,
|
||||||
@@ -118,9 +123,11 @@ export async function handleCreateCipher(request: Request, env: Env, userId: str
|
|||||||
card: cipherData.card || null,
|
card: cipherData.card || null,
|
||||||
identity: cipherData.identity || null,
|
identity: cipherData.identity || null,
|
||||||
secureNote: cipherData.secureNote || null,
|
secureNote: cipherData.secureNote || null,
|
||||||
|
sshKey: cipherData.sshKey || null,
|
||||||
fields: cipherData.fields || null,
|
fields: cipherData.fields || null,
|
||||||
passwordHistory: cipherData.passwordHistory || null,
|
passwordHistory: cipherData.passwordHistory || null,
|
||||||
reprompt: cipherData.reprompt || 0,
|
reprompt: cipherData.reprompt || 0,
|
||||||
|
key: cipherData.key || null,
|
||||||
createdAt: now,
|
createdAt: now,
|
||||||
updatedAt: now,
|
updatedAt: now,
|
||||||
deletedAt: null,
|
deletedAt: null,
|
||||||
@@ -149,11 +156,12 @@ export async function handleUpdateCipher(request: Request, env: Env, userId: str
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Handle nested cipher object
|
// Handle nested cipher object
|
||||||
const cipherData = body.cipher || body;
|
// Android client sends PascalCase "Cipher" for organization ciphers
|
||||||
|
const cipherData = body.Cipher || body.cipher || body;
|
||||||
|
|
||||||
const cipher: Cipher = {
|
const cipher: Cipher = {
|
||||||
...existingCipher,
|
...existingCipher,
|
||||||
type: cipherData.type ?? existingCipher.type,
|
type: Number(cipherData.type) || existingCipher.type,
|
||||||
folderId: cipherData.folderId !== undefined ? cipherData.folderId : existingCipher.folderId,
|
folderId: cipherData.folderId !== undefined ? cipherData.folderId : existingCipher.folderId,
|
||||||
name: cipherData.name ?? existingCipher.name,
|
name: cipherData.name ?? existingCipher.name,
|
||||||
notes: cipherData.notes !== undefined ? cipherData.notes : existingCipher.notes,
|
notes: cipherData.notes !== undefined ? cipherData.notes : existingCipher.notes,
|
||||||
@@ -162,9 +170,11 @@ export async function handleUpdateCipher(request: Request, env: Env, userId: str
|
|||||||
card: cipherData.card !== undefined ? cipherData.card : existingCipher.card,
|
card: cipherData.card !== undefined ? cipherData.card : existingCipher.card,
|
||||||
identity: cipherData.identity !== undefined ? cipherData.identity : existingCipher.identity,
|
identity: cipherData.identity !== undefined ? cipherData.identity : existingCipher.identity,
|
||||||
secureNote: cipherData.secureNote !== undefined ? cipherData.secureNote : existingCipher.secureNote,
|
secureNote: cipherData.secureNote !== undefined ? cipherData.secureNote : existingCipher.secureNote,
|
||||||
|
sshKey: cipherData.sshKey !== undefined ? cipherData.sshKey : existingCipher.sshKey,
|
||||||
fields: cipherData.fields !== undefined ? cipherData.fields : existingCipher.fields,
|
fields: cipherData.fields !== undefined ? cipherData.fields : existingCipher.fields,
|
||||||
passwordHistory: cipherData.passwordHistory !== undefined ? cipherData.passwordHistory : existingCipher.passwordHistory,
|
passwordHistory: cipherData.passwordHistory !== undefined ? cipherData.passwordHistory : existingCipher.passwordHistory,
|
||||||
reprompt: cipherData.reprompt ?? existingCipher.reprompt,
|
reprompt: cipherData.reprompt ?? existingCipher.reprompt,
|
||||||
|
key: cipherData.key !== undefined ? cipherData.key : existingCipher.key,
|
||||||
updatedAt: new Date().toISOString(),
|
updatedAt: new Date().toISOString(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+20
-11
@@ -31,22 +31,21 @@ export async function handleToken(request: Request, env: Env): Promise<Response>
|
|||||||
return errorResponse('Email and password are required', 400);
|
return errorResponse('Email and password are required', 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if login is rate limited
|
|
||||||
const loginCheck = await rateLimit.checkLoginAttempt(email);
|
|
||||||
if (!loginCheck.allowed) {
|
|
||||||
return errorResponse(
|
|
||||||
`Too many failed login attempts. Try again in ${Math.ceil(loginCheck.retryAfterSeconds! / 60)} minutes.`,
|
|
||||||
429
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const user = await storage.getUser(email);
|
const user = await storage.getUser(email);
|
||||||
if (!user) {
|
if (!user) {
|
||||||
// Record failed attempt even for non-existent user (prevent enumeration)
|
|
||||||
await rateLimit.recordFailedLogin(email);
|
|
||||||
return identityErrorResponse('Username or password is incorrect. Try again', 'invalid_grant', 400);
|
return identityErrorResponse('Username or password is incorrect. Try again', 'invalid_grant', 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if login is rate limited (only after confirming user exists)
|
||||||
|
const loginCheck = await rateLimit.checkLoginAttempt(email);
|
||||||
|
if (!loginCheck.allowed) {
|
||||||
|
return identityErrorResponse(
|
||||||
|
`Too many failed login attempts. Try again in ${Math.ceil(loginCheck.retryAfterSeconds! / 60)} minutes.`,
|
||||||
|
'TooManyRequests',
|
||||||
|
429
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const valid = await auth.verifyPassword(passwordHash, user.masterPasswordHash);
|
const valid = await auth.verifyPassword(passwordHash, user.masterPasswordHash);
|
||||||
if (!valid) {
|
if (!valid) {
|
||||||
// Record failed login attempt
|
// Record failed login attempt
|
||||||
@@ -136,6 +135,16 @@ export async function handleToken(request: Request, env: Env): Promise<Response>
|
|||||||
UserDecryptionOptions: {
|
UserDecryptionOptions: {
|
||||||
HasMasterPassword: true,
|
HasMasterPassword: true,
|
||||||
Object: 'userDecryptionOptions',
|
Object: 'userDecryptionOptions',
|
||||||
|
MasterPasswordUnlock: {
|
||||||
|
Kdf: {
|
||||||
|
KdfType: user.kdfType,
|
||||||
|
Iterations: user.kdfIterations,
|
||||||
|
Memory: user.kdfMemory || null,
|
||||||
|
Parallelism: user.kdfParallelism || null,
|
||||||
|
},
|
||||||
|
MasterKeyEncryptedUserKey: user.key,
|
||||||
|
Salt: user.email.toLowerCase(),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -134,6 +134,8 @@ export async function handleCiphersImport(request: Request, env: Env, userId: st
|
|||||||
totp: c.login.totp || null,
|
totp: c.login.totp || null,
|
||||||
autofillOnPageLoad: null,
|
autofillOnPageLoad: null,
|
||||||
fido2Credentials: null,
|
fido2Credentials: null,
|
||||||
|
uri: null,
|
||||||
|
passwordRevisionDate: null,
|
||||||
} : null,
|
} : null,
|
||||||
card: c.card ? {
|
card: c.card ? {
|
||||||
cardholderName: c.card.cardholderName || null,
|
cardholderName: c.card.cardholderName || null,
|
||||||
@@ -172,6 +174,8 @@ export async function handleCiphersImport(request: Request, env: Env, userId: st
|
|||||||
})) || null,
|
})) || null,
|
||||||
passwordHistory: c.passwordHistory || null,
|
passwordHistory: c.passwordHistory || null,
|
||||||
reprompt: c.reprompt || 0,
|
reprompt: c.reprompt || 0,
|
||||||
|
sshKey: null,
|
||||||
|
key: null,
|
||||||
createdAt: now,
|
createdAt: now,
|
||||||
updatedAt: now,
|
updatedAt: now,
|
||||||
deletedAt: null,
|
deletedAt: null,
|
||||||
|
|||||||
+19
-2
@@ -11,6 +11,7 @@ function formatAttachments(attachments: Attachment[]): any[] | null {
|
|||||||
size: String(a.size),
|
size: String(a.size),
|
||||||
sizeName: a.sizeName,
|
sizeName: a.sizeName,
|
||||||
key: a.key,
|
key: a.key,
|
||||||
|
url: null,
|
||||||
object: 'attachment',
|
object: 'attachment',
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
@@ -41,6 +42,7 @@ export async function handleSync(request: Request, env: Env, userId: string): Pr
|
|||||||
twoFactorEnabled: false,
|
twoFactorEnabled: false,
|
||||||
key: user.key,
|
key: user.key,
|
||||||
privateKey: user.privateKey,
|
privateKey: user.privateKey,
|
||||||
|
accountKeys: null,
|
||||||
securityStamp: user.securityStamp || user.id,
|
securityStamp: user.securityStamp || user.id,
|
||||||
organizations: [],
|
organizations: [],
|
||||||
providers: [],
|
providers: [],
|
||||||
@@ -59,7 +61,7 @@ export async function handleSync(request: Request, env: Env, userId: string): Pr
|
|||||||
id: cipher.id,
|
id: cipher.id,
|
||||||
organizationId: null,
|
organizationId: null,
|
||||||
folderId: cipher.folderId,
|
folderId: cipher.folderId,
|
||||||
type: cipher.type,
|
type: Number(cipher.type) || 1,
|
||||||
name: cipher.name,
|
name: cipher.name,
|
||||||
notes: cipher.notes,
|
notes: cipher.notes,
|
||||||
favorite: cipher.favorite,
|
favorite: cipher.favorite,
|
||||||
@@ -67,6 +69,7 @@ export async function handleSync(request: Request, env: Env, userId: string): Pr
|
|||||||
card: cipher.card,
|
card: cipher.card,
|
||||||
identity: cipher.identity,
|
identity: cipher.identity,
|
||||||
secureNote: cipher.secureNote,
|
secureNote: cipher.secureNote,
|
||||||
|
sshKey: cipher.sshKey,
|
||||||
fields: cipher.fields,
|
fields: cipher.fields,
|
||||||
passwordHistory: cipher.passwordHistory,
|
passwordHistory: cipher.passwordHistory,
|
||||||
reprompt: cipher.reprompt,
|
reprompt: cipher.reprompt,
|
||||||
@@ -74,16 +77,18 @@ export async function handleSync(request: Request, env: Env, userId: string): Pr
|
|||||||
creationDate: cipher.createdAt,
|
creationDate: cipher.createdAt,
|
||||||
revisionDate: cipher.updatedAt,
|
revisionDate: cipher.updatedAt,
|
||||||
deletedDate: cipher.deletedAt,
|
deletedDate: cipher.deletedAt,
|
||||||
|
archivedDate: null,
|
||||||
edit: true,
|
edit: true,
|
||||||
viewPassword: true,
|
viewPassword: true,
|
||||||
permissions: {
|
permissions: {
|
||||||
delete: true,
|
delete: true,
|
||||||
restore: true,
|
restore: true,
|
||||||
edit: true,
|
|
||||||
},
|
},
|
||||||
object: 'cipher',
|
object: 'cipher',
|
||||||
collectionIds: [],
|
collectionIds: [],
|
||||||
attachments: formatAttachments(attachments),
|
attachments: formatAttachments(attachments),
|
||||||
|
key: cipher.key,
|
||||||
|
encryptedFor: null,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -107,6 +112,18 @@ export async function handleSync(request: Request, env: Env, userId: string): Pr
|
|||||||
},
|
},
|
||||||
policies: [],
|
policies: [],
|
||||||
sends: [],
|
sends: [],
|
||||||
|
userDecryption: {
|
||||||
|
masterPasswordUnlock: {
|
||||||
|
salt: user.email,
|
||||||
|
kdf: {
|
||||||
|
kdfType: user.kdfType,
|
||||||
|
iterations: user.kdfIterations,
|
||||||
|
memory: user.kdfMemory || null,
|
||||||
|
parallelism: user.kdfParallelism || null,
|
||||||
|
},
|
||||||
|
masterKeyEncryptedUserKey: user.key,
|
||||||
|
},
|
||||||
|
},
|
||||||
object: 'sync',
|
object: 'sync',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -3,11 +3,11 @@ import { Env } from '../types';
|
|||||||
// Rate limit configuration
|
// Rate limit configuration
|
||||||
const CONFIG = {
|
const CONFIG = {
|
||||||
// Login attempt limits
|
// Login attempt limits
|
||||||
LOGIN_MAX_ATTEMPTS: 5, // Max failed login attempts
|
LOGIN_MAX_ATTEMPTS: 15, // Max failed login attempts
|
||||||
LOGIN_LOCKOUT_MINUTES: 15, // Lockout duration after max attempts
|
LOGIN_LOCKOUT_MINUTES: 5, // Lockout duration after max attempts
|
||||||
|
|
||||||
// API rate limits (per minute)
|
// API rate limits (per minute)
|
||||||
API_REQUESTS_PER_MINUTE: 60, // General API rate limit
|
API_REQUESTS_PER_MINUTE: 300, // General API rate limit
|
||||||
API_WINDOW_SECONDS: 60, // Rate limit window
|
API_WINDOW_SECONDS: 60, // Rate limit window
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+19
-4
@@ -54,6 +54,8 @@ export interface CipherLogin {
|
|||||||
totp: string | null;
|
totp: string | null;
|
||||||
autofillOnPageLoad: boolean | null;
|
autofillOnPageLoad: boolean | null;
|
||||||
fido2Credentials: any[] | null;
|
fido2Credentials: any[] | null;
|
||||||
|
uri: string | null;
|
||||||
|
passwordRevisionDate: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CipherCard {
|
export interface CipherCard {
|
||||||
@@ -65,6 +67,12 @@ export interface CipherCard {
|
|||||||
code: string | null;
|
code: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface CipherSshKey {
|
||||||
|
publicKey: string;
|
||||||
|
privateKey: string;
|
||||||
|
keyFingerprint: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface CipherIdentity {
|
export interface CipherIdentity {
|
||||||
title: string | null;
|
title: string | null;
|
||||||
firstName: string | null;
|
firstName: string | null;
|
||||||
@@ -114,9 +122,11 @@ export interface Cipher {
|
|||||||
card: CipherCard | null;
|
card: CipherCard | null;
|
||||||
identity: CipherIdentity | null;
|
identity: CipherIdentity | null;
|
||||||
secureNote: CipherSecureNote | null;
|
secureNote: CipherSecureNote | null;
|
||||||
|
sshKey: CipherSshKey | null;
|
||||||
fields: CipherField[] | null;
|
fields: CipherField[] | null;
|
||||||
passwordHistory: PasswordHistory[] | null;
|
passwordHistory: PasswordHistory[] | null;
|
||||||
reprompt: number;
|
reprompt: number;
|
||||||
|
key: string | null;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
deletedAt: string | null;
|
deletedAt: string | null;
|
||||||
@@ -190,20 +200,21 @@ export interface ProfileResponse {
|
|||||||
email: string;
|
email: string;
|
||||||
emailVerified: boolean;
|
emailVerified: boolean;
|
||||||
premium: boolean;
|
premium: boolean;
|
||||||
premiumFromOrganization: boolean; // required by mobile client
|
premiumFromOrganization: boolean;
|
||||||
usesKeyConnector: boolean; // required by mobile client
|
usesKeyConnector: boolean;
|
||||||
masterPasswordHint: string | null;
|
masterPasswordHint: string | null;
|
||||||
culture: string;
|
culture: string;
|
||||||
twoFactorEnabled: boolean;
|
twoFactorEnabled: boolean;
|
||||||
key: string;
|
key: string;
|
||||||
privateKey: string | null;
|
privateKey: string | null;
|
||||||
|
accountKeys: any | null;
|
||||||
securityStamp: string;
|
securityStamp: string;
|
||||||
organizations: any[];
|
organizations: any[];
|
||||||
providers: any[];
|
providers: any[];
|
||||||
providerOrganizations: any[];
|
providerOrganizations: any[];
|
||||||
forcePasswordReset: boolean;
|
forcePasswordReset: boolean;
|
||||||
avatarColor: string | null;
|
avatarColor: string | null;
|
||||||
creationDate: string; // required by mobile client
|
creationDate: string;
|
||||||
object: string;
|
object: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -219,6 +230,7 @@ export interface CipherResponse {
|
|||||||
card: CipherCard | null;
|
card: CipherCard | null;
|
||||||
identity: CipherIdentity | null;
|
identity: CipherIdentity | null;
|
||||||
secureNote: CipherSecureNote | null;
|
secureNote: CipherSecureNote | null;
|
||||||
|
sshKey: CipherSshKey | null;
|
||||||
fields: CipherField[] | null;
|
fields: CipherField[] | null;
|
||||||
passwordHistory: PasswordHistory[] | null;
|
passwordHistory: PasswordHistory[] | null;
|
||||||
reprompt: number;
|
reprompt: number;
|
||||||
@@ -226,18 +238,20 @@ export interface CipherResponse {
|
|||||||
creationDate: string;
|
creationDate: string;
|
||||||
revisionDate: string;
|
revisionDate: string;
|
||||||
deletedDate: string | null;
|
deletedDate: string | null;
|
||||||
|
archivedDate: string | null;
|
||||||
edit: boolean;
|
edit: boolean;
|
||||||
viewPassword: boolean;
|
viewPassword: boolean;
|
||||||
permissions: CipherPermissions | null;
|
permissions: CipherPermissions | null;
|
||||||
object: string;
|
object: string;
|
||||||
collectionIds: string[];
|
collectionIds: string[];
|
||||||
attachments: any[] | null;
|
attachments: any[] | null;
|
||||||
|
key: string | null;
|
||||||
|
encryptedFor: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CipherPermissions {
|
export interface CipherPermissions {
|
||||||
delete: boolean;
|
delete: boolean;
|
||||||
restore: boolean;
|
restore: boolean;
|
||||||
edit: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FolderResponse {
|
export interface FolderResponse {
|
||||||
@@ -255,5 +269,6 @@ export interface SyncResponse {
|
|||||||
domains: any;
|
domains: any;
|
||||||
policies: any[];
|
policies: any[];
|
||||||
sends: any[];
|
sends: any[];
|
||||||
|
userDecryption: any | null;
|
||||||
object: string;
|
object: string;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user