Source & license
Upstream license: MIT
License TL;DR
You can use it, change it, self-host it and sell it. Keep the original copyright and license notice with copies of the code. You don’t have to publish your changes. The authors don’t promise it will work.
Explain MIT in plain English →Summary of the main license. Separate packages and assets can have different terms.
Inspect repository ↗Read this project’s actual license ↗Repository owner
See the upstream repository for the original creator and contributors.
Maintain this project? Maintainer verification →Cloudflare hosting
Free tier eligible within limits
The documented 2FA deployment can use Cloudflare Free allowances for a small workload under the request, CPU and service-specific quotas below. This is conditional eligibility, not a measured zero-cost deployment; optional features, domains and external providers can add costs.
Hosting requirements
- Workers Free allows 100,000 requests per day shared across the account and 10 ms CPU per invocation; measure CPU-heavy authentication, parsing and rendering before assuming it fits.
- Keep KV below 100,000 reads/day, 1,000 writes, deletes and list operations/day each, and 1 GB; cache refreshes and backups consume writes.
- Use a small personal or team workload; domain registration and optional third-party providers are separate costs. Provision your own IDs, secrets and migrations.
Sources checked 01/10/2026
Repository snapshot: 7b8060e. Hosting eligibility reflects the deployment documentation and listed assumptions.
- google-authenticator ↗
基于 Cloudflare Workers 的两步验证密钥管理系统。免费部署、全球加速、支持 PWA 离线使用。
- 2fas ↗
基于 Cloudflare Workers 的两步验证密钥管理系统。免费部署、全球加速、支持 PWA 离线使用。
- workers ↗
name = "2fa" main = "src/worker.js" compatibility_date = "2024-01-13" workers_dev = true # KV 命名空间 # 始终声明 SECRETS_KV,避免 Git 自动构建部署时丢失绑定。 # 新版 Wrangler 会在首次部署时自动创建或保持已链接的 KV 资源。 # 如需固定到指定资源,可在本地补充 id / preview_id。 [[kv_namespaces]] binding = "SECRETS_KV" # 环境变量 (Secrets) # 注意:敏感信息使用 Cloudflare Secrets 安全存储 # # 生产环境(默认)设置: # npx wrangler secret put ENCRYPTION_KEY
- kv ↗
name = "2fa" main = "src/worker.js" compatibility_date = "2024-01-13" workers_dev = true # KV 命名空间 # 始终声明 SECRETS_KV,避免 Git 自动构建部署时丢失绑定。 # 新版 Wrangler 会在首次部署时自动创建或保持已链接的 KV 资源。 # 如需固定到指定资源,可在本地补充 id / preview_id。 [[kv_namespaces]] binding = "SECRETS_KV" # 环境变量 (Secrets) # 注意:敏感信息使用 Cloudflare Secrets 安全存储 # # 生产环境(默认)设置: # npx wrangler secret put ENCRYPTION_KEY # 加密密钥(可选,用于数据加密) # # 开发环境设置: # npx wrangler secret put ENCRYPTION_KEY --env development # # 详
- free-tier-eligible ↗
name = "2fa" main = "src/worker.js" compatibility_date = "2024-01-13" workers_dev = true # KV 命名空间 # 始终声明 SECRETS_KV,避免 Git 自动构建部署时丢失绑定。 # 新版 Wrangler 会在首次部署时自动创建或保持已链接的 KV 资源。 # 如需固定到指定资源,可在本地补充 id / preview_id。 [[kv_namespaces]] binding = "SECRETS_KV" # 环境变量 (Secrets) # 注意:敏感信息使用 Cloudflare Secrets 安全存储 # # 生产环境(默认)设置: # npx wrangler secret put ENCRYPTION_KEY
- free-tier-eligible ↗
name = "2fa" main = "src/worker.js" compatibility_date = "2024-01-13" workers_dev = true # KV 命名空间 # 始终声明 SECRETS_KV,避免 Git 自动构建部署时丢失绑定。 # 新版 Wrangler 会在首次部署时自动创建或保持已链接的 KV 资源。 # 如需固定到指定资源,可在本地补充 id / preview_id。 [[kv_namespaces]] binding = "SECRETS_KV" # 环境变量 (Secrets) # 注意:敏感信息使用 Cloudflare Secrets 安全存储 # # 生产环境(默认)设置: # npx wrangler secret put ENCRYPTION_KEY # 加密密钥(可选,用于数据加密) # # 开发环境设置: # npx wrangler secret put ENCRYPTION_KEY --env development # # 详
- free-tier-eligible ↗
up>1, 2, 3, 4</sup> | Duration | CPU time | | --- | --- | --- | --- | | **Free** | 100,000 per day | No charge for duration | 10 milliseconds of CPU time per invocation | | **Standard** | 10 million included per month <br> +$0.30 per additional million | No charge or limit for duration | 30 million CPU milliseconds included per month<br> +$0.02 per additional million CPU milliseconds<br><br> Max of [5 minutes of CPU time](https://developers.cloudflare.com/workers/platform/limits/#account-plan-limits) per invocation (default: 30 seconds)<br> Max of 15 minutes of CPU time per [Cron Trigger](https://developers.cloudflare.com/workers/configuration/cron-triggers/) or [Queue Consumer](https://developers.cloudflare.co
- free-tier-eligible ↗
cing/). | | Free plan<sup>1</sup> | Paid plan | | --- | --- | --- | | Keys read | 100,000 / day | 10 million/month, + $0.50/million | | Keys written | 1,000 / day | 1 million/month, + $5.00/million | | Keys deleted | 1,000 / day | 1 million/month, + $5.00/million | | List requests | 1,000 / day | 1 million/month, + $5.00/million | | Stored data | 1 GB | 1 GB, + $0.50/ GB-month | <sup>1</sup> The Workers Free plan includes limited Workers KV usage. All limits reset daily at 00:00 UTC. If you exceed any one of these limits, further operations of that type will fail with an error. Note Workers KV pricing for read, write and delete operations is on a per-key basis. Bulk read operations are billed by the amount
- MIT ↗
MIT License Copyright (c) 2024 wuzf Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
- architecture ↗
name = "2fa" main = "src/worker.js" compatibility_date = "2024-01-13" workers_dev = true # KV 命名空间 # 始终声明 SECRETS_KV,避免 Git 自动构建部署时丢失绑定。 # 新版 Wrangler 会在首次部署时自动创建或保持已链接的 KV 资源。 # 如需固定到指定资源,可在本地补充 id / preview_id。 [[kv_namespaces]] binding = "SECRETS_KV" # 环境变量 (Secrets) # 注意:敏感信息使用 Cloudflare Secrets 安全存储 # # 生产环境(默认)设置: # npx wrangler secret put ENCRYPTION_KEY
- architecture ↗
name = "2fa" main = "src/worker.js" compatibility_date = "2024-01-13" workers_dev = true # KV 命名空间 # 始终声明 SECRETS_KV,避免 Git 自动构建部署时丢失绑定。 # 新版 Wrangler 会在首次部署时自动创建或保持已链接的 KV 资源。 # 如需固定到指定资源,可在本地补充 id / preview_id。 [[kv_namespaces]] binding = "SECRETS_KV" # 环境变量 (Secrets) # 注意:敏感信息使用 Cloudflare Secrets 安全存储 # # 生产环境(默认)设置: # npx wrangler secret put ENCRYPTION_KEY # 加密密钥(可选,用于数据加密) # # 开发环境设置: # npx wrangler secret put ENCRYPTION_KEY --env development # # 详
Upstream screenshot · wuzf/2fa repository contributors ↗. Depicts the upstream project. We have not deployed and tested a fresh installation here.
What it can replace
Compare the workflow you need. These mappings describe overlap; full feature parity requires a separate comparison.
TOTP/HOTP code generation and seed import through a self-hosted browser interface; mobile client ecosystems and complete import compatibility are excluded.
See supporting source ↗TOTP/HOTP code generation and seed import through a self-hosted browser interface; mobile client ecosystems and complete import compatibility are excluded.
See supporting source ↗How it works
The shape of 2FA on Cloudflare, and how it stacks up against the rented tools it replaces.
Architecture
Diagram of deployment declarations at the reviewed commit. Each app has its own entrypoint; declared resources do not prove runtime calls. Follow file and line sources below.
View upstream source ↗Configuration and workflow sources
Reviewed commit 7b8060e88f7a. Files were read as data; upstream applications and CI jobs were not executed.
Partial source coverage: 70 files outside collection bounds; 0 collection or parsing issues. Dynamic imports and generated entrypoints may need manual review.
Deployment configuration · 1 files
Cloudflare Workers · compatibility 2024-01-13
2fa · default
Entrypoint: src/worker.js
Cron triggers (UTC): 0 16 * * *
SECRETS_KV→ KV
2fa-dev · env.development
Inherited from default: main, triggers, compatibility_date
Entrypoint: src/worker.js
Cron triggers (UTC): 0 16 * * *
SECRETS_KV→ KV
Named environments are separate deployments. Bindings are shown only where declared. Configured routes are URL patterns, not verified application endpoints.
Runtime source · handlers, binding usage and workflow steps
Observed TypeScript/JavaScript declarations from Worker entrypoints and resolved relative imports. Calls and workflow steps may run conditionally; their listed order is not a proven end-to-end request flow. Router declarations may be mounted under a prefix or may not be registered. This shows code wiring, not a successful deployment or runtime test. Dynamic wiring, aliases and generated code may not resolve.
- L229 · fetch handler exported · calls getLogger, createRequestLogger, getMonitoring, catch, monitoring.initialize, logger.warn, requestLogger.logRequest, startTrace, monitoring.getPerformanceMonitor, request.headers.get, handleCORS, endTrace, handleRequest, requestLogger.logResponse, logger.error, captureError, monitoring.getErrorMonitor, JSON.stringify, toISOString
- L331 · scheduled handler exported · references SECRETS_KV · calls getLogger, Date.now, logger.info, toISOString, getAllSecrets, map, secrets.slice, logger.debug, timer.checkpoint, generateDataHash, Promise.all, env.SECRETS_KV.get, getPendingDataHash, isPendingDataHashFresh, hasCommittedBackupSince, currentHash.substring, lastHash.substring, pendingHashEntry.hash.substring, timer.end, resolveConfiguredBackupFormat, createBackupEntry, logger.warn, putBackupRecord, ctx.waitUntil, pushToAllWebDAV, pushToAllS3, pushToAllOneDrive, pushToAllGoogleDrive, saveDataHash, cleanupOldBackups, logger.error, timer.getDuration
- L37 · _hasDataChanged calls (conditional paths may differ): getLogger, generateDataHash, env.SECRETS_KV.get, logger.info, currentHash.substring, lastHash.substring, env.SECRETS_KV.list, list.keys.filter, isValidBackupKey, backupKeys.sort, b.name.localeCompare, Number.parseInt, Number.isInteger, logger.debug, logger.warn, logger.error
- L119 · cleanupOldBackups calls (conditional paths may differ): getLogger, env.SECRETS_KV.get, JSON.parse, sanitizeMaxBackups, logger.debug, sort, listAllBackupKeys, b.name.localeCompare, logger.info, backupKeys.sort, backupKeys.slice, deleteBackupRecord, logger.warn, logger.error
- L194 · hasCommittedBackupSince calls (conditional paths may differ): Date.parse, Number.isFinite, listAllBackupKeys, Number.parseInt, parseBackupTimeFromKey, warn, getLogger
Environment references: env.SECRETS_KV
- L80 · handleRequest calls (conditional paths may differ): getLogger, handleGetTime, createErrorResponse, response.headers.set, checkIfSetupRequired, Response.redirect, toString, createSetupPage, handleFirstTimeSetup, requiresAuth, verifyAuthWithDetails, env.SECRETS_KV.get, createUnauthorizedResponse, createMainPage, createManifest, createServiceWorker, pathname.includes, createDefaultIcon, pathname.startsWith, replace
- L281 · handleApiRequest calls (conditional paths may differ): handleGetSecrets, handleAddSecret, createErrorResponse, handleBatchAddSecrets, handleExportSecrets, handleCompactHOTPCounters, test, handleAdvanceHOTPCounter, pathname.startsWith, pathname.substring, secretId.includes, handleUpdateSecret, handleDeleteSecret, handleBackupSecrets, handleGetBackups, handleRestoreBackup, pathname.replace, handleExportBackup, handleChangePassword, handleGetSettings
- L528 · handleCORS calls (conditional paths may differ): createPreflightResponse
Environment references: env.SECRETS_KV · env.ENVIRONMENT
- L22 · decodeSecretIdSegment calls (conditional paths may differ): decodeURIComponent, String
- L37 · saveSecretsToKV calls (conditional paths may differ): getLogger, Boolean, encryptSecrets, env.SECRETS_KV.put, logger.info, logger.warn, stageDataHash, logger.debug, triggerBackup, catch, then, clearPendingDataHash, ctx.waitUntil, logger.error
- L128 · getAllSecrets calls (conditional paths may differ): getLogger, env.SECRETS_KV.get, decryptSecrets, overlayHOTPCounterStates, logger.error
- L144 · getSecretByIdWithHOTPState calls (conditional paths may differ): env.SECRETS_KV.get, decryptSecrets, secrets.find, String, overlaySingleHOTPCounterState
Environment references: env.SECRETS_KV · env.ENCRYPTION_KEY
- L37 · isValidLogLevel calls (conditional paths may differ): Number.isInteger
- L294 · resolveLoggerOptions calls (conditional paths may differ): env.LOG_LEVEL.toUpperCase
- L317 · getLogger calls (conditional paths may differ): resolveLoggerOptions, Boolean
- L407 · createRequestLogger calls (conditional paths may differ): getLogger, log.info, _sanitizeHeaders, request.headers.get, Object.fromEntries, log.error, log.warn, timer.end, headers.forEach, key.toLowerCase, sensitiveHeaders.includes
Environment references: env.LOG_LEVEL · env.ENVIRONMENT · env.LOG_REMOTE_ENDPOINT
- L329 · resolveMonitoringOptions calls (conditional paths may differ): parseInt
- L346 · getMonitoring calls (conditional paths may differ): resolveMonitoringOptions, Boolean, Object.assign, getLogger
Environment references: env.ENABLE_PERFORMANCE_MONITORING · env.SLOW_REQUEST_THRESHOLD · env.ENVIRONMENT · env.VERSION
- L24 · getWebDAVConfigs calls (conditional paths may differ): getLogger, env.SECRETS_KV.get, isEncrypted, decryptData, JSON.parse, logger.info, crypto.randomUUID, toISOString, Promise.all, Object.keys, env.SECRETS_KV.put, JSON.stringify, _saveConfigsToKV, env.SECRETS_KV.delete, logger.error
- L103 · saveWebDAVConfigs calls (conditional paths may differ): _saveConfigsToKV
- L113 · saveWebDAVSingleConfig calls (conditional paths may differ): getWebDAVConfigs, configs.findIndex, crypto.randomUUID, toISOString, configs.push, _saveConfigsToKV
- L141 · deleteWebDAVSingleConfig calls (conditional paths may differ): getWebDAVConfigs, configs.findIndex, configs.splice, _saveConfigsToKV, env.SECRETS_KV.delete
- L168 · getWebDAVStatus calls (conditional paths may differ): env.SECRETS_KV.get
- L186 · pushToAllWebDAV calls (conditional paths may differ): getLogger, getWebDAVConfigs, configs.filter, logger.debug, logger.info, Promise.all, enabledConfigs.map, _pushToSingleWebDAV, results.filter, logger.warn
- L225 · _pushToSingleWebDAV calls (conditional paths may differ): getLogger, config.url.replace, config.path.replace, _encodeBasicAuth, logger.info, setTimeout, controller.abort, fetch, getBackupContentType, backupContent.startsWith, _ensureDirectory, clearTimeout, _recordWebDAVStatus, toISOString, logger.warn, _recordWebDAVStatusError
- L328 · _friendlyFetchError calls (conditional paths may differ): test, msg.slice
- L353 · testWebDAVConnection calls (conditional paths may differ): config.url.replace, path.replace, _encodeBasicAuth, setTimeout, controller.abort, fetch, clearTimeout, JSON.stringify, toISOString, _ensureDirectory, _friendlyFetchError
- L513 · _saveConfigsToKV calls (conditional paths may differ): encryptData, env.SECRETS_KV.put, JSON.stringify
- L533 · _recordWebDAVStatus calls (conditional paths may differ): getWebDAVStatus, env.SECRETS_KV.put, JSON.stringify
- L547 · _recordWebDAVStatusError calls (conditional paths may differ): _recordWebDAVStatus, toISOString
- L561 · _ensureDirectory calls (conditional paths may differ): filter, path.split, fetch, logger.info, logger.debug, logger.warn
- L597 · _encodeBasicAuth calls (conditional paths may differ): encoder.encode, String.fromCharCode, btoa
- L613 · getWebDAVConfig calls (conditional paths may differ): getWebDAVConfigs, configs.find
- L622 · saveWebDAVConfig calls (conditional paths may differ): saveWebDAVSingleConfig
- L630 · pushToWebDAV calls (conditional paths may differ): pushToAllWebDAV
Environment references: env.SECRETS_KV · env.ENCRYPTION_KEY
- L25 · getS3Configs calls (conditional paths may differ): getLogger, env.SECRETS_KV.get, isEncrypted, decryptData, JSON.parse, logger.info, crypto.randomUUID, toISOString, Promise.all, Object.keys, env.SECRETS_KV.put, JSON.stringify, _saveConfigsToKV, env.SECRETS_KV.delete, logger.error
- L106 · saveS3Configs calls (conditional paths may differ): _saveConfigsToKV
- L116 · saveS3SingleConfig calls (conditional paths may differ): getS3Configs, configs.findIndex, crypto.randomUUID, toISOString, configs.push, _saveConfigsToKV
- L144 · deleteS3SingleConfig calls (conditional paths may differ): getS3Configs, configs.findIndex, configs.splice, _saveConfigsToKV, env.SECRETS_KV.delete
- L171 · getS3Status calls (conditional paths may differ): env.SECRETS_KV.get
- L189 · pushToAllS3 calls (conditional paths may differ): getLogger, getS3Configs, configs.filter, logger.debug, logger.info, Promise.all, enabledConfigs.map, _pushToSingleS3, results.filter, logger.warn
- L228 · _pushToSingleS3 calls (conditional paths may differ): getLogger, config.endpoint.replace, config.prefix.replace, logger.info, setTimeout, controller.abort, client.fetch, getBackupContentType, backupContent.startsWith, clearTimeout, _recordS3Status, toISOString, logger.warn, _recordS3StatusError
- L317 · _friendlyFetchError calls (conditional paths may differ): test, msg.slice
- L343 · testS3Connection calls (conditional paths may differ): config.endpoint.replace, setTimeout, controller.abort, client.fetch, clearTimeout, fetchWithTimeout, _friendlyFetchError, config.prefix.replace, JSON.stringify, toISOString
- L438 · _saveConfigsToKV calls (conditional paths may differ): encryptData, env.SECRETS_KV.put, JSON.stringify
- L458 · _recordS3Status calls (conditional paths may differ): getS3Status, env.SECRETS_KV.put, JSON.stringify
- L472 · _recordS3StatusError calls (conditional paths may differ): _recordS3Status, toISOString
- L488 · getS3Config calls (conditional paths may differ): getS3Configs, configs.find
- L497 · saveS3Config calls (conditional paths may differ): saveS3SingleConfig
- L505 · pushToS3 calls (conditional paths may differ): pushToAllS3
Environment references: env.SECRETS_KV · env.ENCRYPTION_KEY
- L32 · buildOneDriveAuthorizeUrl calls (conditional paths may differ): isOneDriveOAuthConfigured, ONEDRIVE_SCOPES.join, params.toString
- L49 · exchangeOneDriveCode calls (conditional paths may differ): exchangeOneDriveToken
- L57 · refreshOneDriveToken calls (conditional paths may differ): exchangeOneDriveToken
- L64 · fetchOneDriveProfile calls (conditional paths may differ): fetch, parseJsonResponse, extractProviderError
- L82 · getOneDriveConfigs calls (conditional paths may differ): getLogger, env.SECRETS_KV.get, isEncrypted, decryptData, JSON.parse, logger.error
- L102 · saveOneDriveConfigs calls (conditional paths may differ): saveConfigsToKv
- L106 · saveOneDriveSingleConfig calls (conditional paths may differ): getOneDriveConfigs, configs.findIndex, toISOString, crypto.randomUUID, getOneDriveDefaultFolderPath, configs.push, saveConfigsToKv
- L134 · deleteOneDriveSingleConfig calls (conditional paths may differ): getOneDriveConfigs, configs.findIndex, configs.splice, saveConfigsToKv, catch, env.SECRETS_KV.delete
- L148 · getOneDriveStatus calls (conditional paths may differ): env.SECRETS_KV.get
- L157 · pushToAllOneDrive calls (conditional paths may differ): getLogger, getOneDriveConfigs, configs.filter, logger.debug, logger.info, Promise.all, enabledConfigs.map, pushToSingleOneDrive, results.filter, logger.warn
- L191 · completeOneDriveAuthorization calls (conditional paths may differ): getOneDriveConfigs, configs.find, saveOneDriveSingleConfig, toISOString
- L229 · testOneDriveConnectionById calls (conditional paths may differ): getOneDriveConfigs, configs.find, JSON.stringify, toISOString, uploadOneDriveFile, saveOneDriveSingleConfig
- L270 · pushToSingleOneDrive calls (conditional paths may differ): getLogger, uploadOneDriveFile, logger.warn
- L280 · uploadOneDriveFile calls (conditional paths may differ): ensureOneDriveAccessToken, ensureOneDriveFolder, getOneDriveDefaultFolderPath, encodeURIComponent, fetch, getBackupContentType, fileContent.startsWith, parseJsonResponse, extractProviderError, recordOneDriveStatus, toISOString, catch, recordOneDriveStatusError
- L336 · ensureOneDriveAccessToken calls (conditional paths may differ): Date.parse, Number.isFinite, Date.now, refreshOneDriveToken, saveOneDriveSingleConfig
- L366 · ensureOneDriveFolder calls (conditional paths may differ): fetch, parseJsonResponse, extractProviderError, getPathSegments, findOneDriveChildFolder, JSON.stringify
- L417 · findOneDriveChildFolder calls (conditional paths may differ): fetch, parseJsonResponse, extractProviderError, find
- L431 · exchangeOneDriveToken calls (conditional paths may differ): isOneDriveOAuthConfigured, ONEDRIVE_SCOPES.join, fetch, body.toString, parseJsonResponse, extractProviderError, normalizeOneDriveTokenData
- L459 · normalizeOneDriveTokenData calls (conditional paths may differ): Number, toISOString, Date.now, Math.max
- L470 · recordOneDriveStatus calls (conditional paths may differ): getOneDriveStatus, env.SECRETS_KV.put, JSON.stringify
- L480 · recordOneDriveStatusError calls (conditional paths may differ): recordOneDriveStatus, toISOString
- L490 · getPathSegments calls (conditional paths may differ): filter, map, split, String, getOneDriveDefaultFolderPath, segment.trim
- L498 · pushToOneDrive calls (conditional paths may differ): pushToAllOneDrive
Environment references: env.ONEDRIVE_CLIENT_ID · env.ONEDRIVE_CLIENT_SECRET · env.SECRETS_KV
- L39 · buildGoogleDriveAuthorizeUrl calls (conditional paths may differ): isGoogleDriveOAuthConfigured, GDRIVE_SCOPES.join, params.toString
- L58 · exchangeGoogleDriveCode calls (conditional paths may differ): exchangeGoogleDriveToken
- L66 · refreshGoogleDriveToken calls (conditional paths may differ): exchangeGoogleDriveToken
- L73 · fetchGoogleDriveProfile calls (conditional paths may differ): fetch, parseJsonResponse, extractProviderError
- L91 · getGoogleDriveConfigs calls (conditional paths may differ): getLogger, env.SECRETS_KV.get, isEncrypted, decryptData, JSON.parse, logger.error
- L111 · saveGoogleDriveConfigs calls (conditional paths may differ): saveConfigsToKv
- L115 · saveGoogleDriveSingleConfig calls (conditional paths may differ): getGoogleDriveConfigs, configs.findIndex, toISOString, crypto.randomUUID, getGoogleDriveDefaultFolderPath, configs.push, saveConfigsToKv
- L143 · deleteGoogleDriveSingleConfig calls (conditional paths may differ): getGoogleDriveConfigs, configs.findIndex, configs.splice, saveConfigsToKv, catch, env.SECRETS_KV.delete
- L157 · getGoogleDriveStatus calls (conditional paths may differ): env.SECRETS_KV.get
- L166 · pushToAllGoogleDrive calls (conditional paths may differ): getLogger, getGoogleDriveConfigs, configs.filter, logger.debug, logger.info, Promise.all, enabledConfigs.map, pushToSingleGoogleDrive, results.filter, logger.warn
- L200 · completeGoogleDriveAuthorization calls (conditional paths may differ): getGoogleDriveConfigs, configs.find, saveGoogleDriveSingleConfig, toISOString
- L238 · testGoogleDriveConnectionById calls (conditional paths may differ): getGoogleDriveConfigs, configs.find, JSON.stringify, toISOString, uploadGoogleDriveFile, saveGoogleDriveSingleConfig
- L279 · pushToSingleGoogleDrive calls (conditional paths may differ): getLogger, uploadGoogleDriveFile, logger.warn
- L289 · uploadGoogleDriveFile calls (conditional paths may differ): ensureGoogleDriveAccessToken, ensureGoogleDriveFolder, getGoogleDriveDefaultFolderPath, findGoogleDriveFile, fetch, toString, getBackupContentType, fileContent.startsWith, parseJsonResponse, extractProviderError, crypto.randomUUID, JSON.stringify, split, recordGoogleDriveStatus, toISOString, catch, recordGoogleDriveStatusError
- L382 · ensureGoogleDriveAccessToken calls (conditional paths may differ): Date.parse, Number.isFinite, Date.now, refreshGoogleDriveToken, saveGoogleDriveSingleConfig
- L412 · ensureGoogleDriveFolder calls (conditional paths may differ): getPathSegments, findGoogleDriveFolder, fetch, toString, JSON.stringify, parseJsonResponse, extractProviderError
- L447 · findGoogleDriveFolder calls (conditional paths may differ): escapeDriveQueryValue, fetch, toString, parseJsonResponse, extractProviderError
- L470 · findGoogleDriveFile calls (conditional paths may differ): escapeDriveQueryValue, fetch, toString, parseJsonResponse, extractProviderError
- L493 · exchangeGoogleDriveToken calls (conditional paths may differ): isGoogleDriveOAuthConfigured, fetch, body.toString, parseJsonResponse, extractProviderError, normalizeGoogleTokenData
- L520 · normalizeGoogleTokenData calls (conditional paths may differ): Number, toISOString, Date.now, Math.max
- L531 · recordGoogleDriveStatus calls (conditional paths may differ): getGoogleDriveStatus, env.SECRETS_KV.put, JSON.stringify
- L541 · recordGoogleDriveStatusError calls (conditional paths may differ): recordGoogleDriveStatus, toISOString
- L551 · extractProviderError calls (conditional paths may differ): extractOAuthProviderError
- L555 · translateGoogleDriveError calls (conditional paths may differ): String, toUpperCase, Array.isArray, errorList.map, details.map, test, reasons.includes
- L590 · getPathSegments calls (conditional paths may differ): filter, map, split, String, getGoogleDriveDefaultFolderPath, segment.trim
- L597 · escapeDriveQueryValue calls (conditional paths may differ): replace, String
- L602 · pushToGoogleDrive calls (conditional paths may differ): pushToAllGoogleDrive
Environment references: env.GOOGLE_DRIVE_CLIENT_ID · env.GOOGLE_DRIVE_CLIENT_SECRET · env.SECRETS_KV
- L16 · resolveBackupCreatedAt calls (conditional paths may differ): parseBackupTimeFromKey
- L20 · sanitizeSkippedInvalidCount calls (conditional paths may differ): Number.parseInt, Number.isInteger
- L25 · parseIndexState calls (conditional paths may differ): JSON.parse
- L37 · getBackupIndexState calls (conditional paths may differ): parseIndexState, catch, env.SECRETS_KV.get
- L41 · writeBackupIndexState calls (conditional paths may differ): env.SECRETS_KV.put, JSON.stringify
- L45 · runBackupIndexStateMutation calls (conditional paths may differ): operation, backupIndexStateMutationState.get, Promise.resolve, backupIndexStateMutationState.set, then, state.tail.catch, run.catch, backupIndexStateMutationState.delete
- L74 · adjustBackupIndexStateCount calls (conditional paths may differ): runBackupIndexStateMutation, getBackupIndexState, Number.isInteger, writeBackupIndexState, Math.max, toISOString
- L89 · invalidateBackupIndexState calls (conditional paths may differ): runBackupIndexStateMutation, catch, env.SECRETS_KV.delete
- L95 · createBackupIndexKey calls (conditional paths may differ): resolveBackupCreatedAt, Date.parse, Number.isFinite, padStart, String
- L104 · getBackupKeyFromIndexKey calls (conditional paths may differ): indexOf, String, indexKey.slice
- L109 · buildBackupIndexMetadata calls (conditional paths may differ): resolveBackupCreatedAt, getBackupFormatFromKey, Number.isInteger, Object.prototype.hasOwnProperty.call, sanitizeSkippedInvalidCount
- L137 · putBackupRecord calls (conditional paths may differ): env.SECRETS_KV.put, createBackupIndexKey, buildBackupIndexMetadata, invalidateBackupIndexState, adjustBackupIndexStateCount
- L172 · deleteBackupRecord calls (conditional paths may differ): createBackupIndexKey, Promise.allSettled, env.SECRETS_KV.delete, invalidateBackupIndexState, deleteResults.find, adjustBackupIndexStateCount
- L210 · listBackupIndexPage calls (conditional paths may differ): env.SECRETS_KV.list
- L222 · listAllBackupKeys calls (conditional paths may differ): env.SECRETS_KV.list, backupKeys.push, hasKvListMore, sort, backupKeys.filter, isValidBackupKey, a.name.localeCompare
- L242 · listAllBackupIndexEntries calls (conditional paths may differ): listBackupIndexPage, indexEntries.push, hasKvListMore
- L261 · getBackupIndexCoverage calls (conditional paths may differ): Promise.all, listAllBackupKeys, listAllBackupIndexEntries, backupKeys.map, getBackupKeyFromIndexKey, isValidBackupKey, backupKeySet.has, orphanedIndexEntries.push, indexedBackupKeys.add, backupKeys.filter, indexedBackupKeys.has
- L288 · ensureBackupIndexesInternal calls (conditional paths may differ): getBackupIndexState, listAllBackupIndexEntries, Promise.all, existingIndexEntries.map, env.SECRETS_KV.delete, listAllBackupKeys, env.SECRETS_KV.put, createBackupIndexKey, buildBackupIndexMetadata, writeBackupIndexState, toISOString
- L313 · ensureBackupIndexes calls (conditional paths may differ): ensureBackupIndexesState.get, ensureBackupIndexesInternal, ensureBackupIndexesState.delete, run, ensureBackupIndexesState.set
Environment references: env.SECRETS_KV
- L49 · resolveConfiguredBackupFormat calls (conditional paths may differ): getDefaultExportFormat
- L66 · sanitizeMaxBackups calls (conditional paths may differ): Number.isInteger
- L504 · triggerBackup calls (conditional paths may differ): getBackupManager, manager.triggerBackup
- L512 · executeImmediateBackup calls (conditional paths may differ): getBackupManager, manager.executeBackup
Environment references: env.SECRETS_KV
- L38 · sanitizeSkippedInvalidCount calls (conditional paths may differ): Number.parseInt, Number.isInteger
- L43 · sanitizeBackupSecretValue calls (conditional paths may differ): toUpperCase, replace, String
- L49 · isValidBackupSecretValue calls (conditional paths may differ): sanitizeBackupSecretValue, Boolean, validateBase32
- L54 · sanitizeBackupFormat calls (conditional paths may differ): resolveBackupFormat
- L58 · sanitizeDownloadContentProfile calls (conditional paths may differ): toLowerCase, trim, String, DOWNLOAD_CONTENT_PROFILES.includes
- L69 · isValidBackupKey calls (conditional paths may differ): BACKUP_KEY_REGEX.test
- L73 · getBackupFormatFromKey calls (conditional paths may differ): backupKey.match, resolveBackupFormat
- L78 · generateBackupKey calls (conditional paths may differ): split, now.toISOString, replace, slice, toString, Math.random, sanitizeBackupFormat
- L89 · parseBackupTimeFromKey calls (conditional paths may differ): backupKey.match, timeStr.replace
- L110 · getBackupContentType calls (conditional paths may differ): getBackupFormatFromKey, sanitizeBackupFormat
- L130 · getPortableSkippedInvalidCount calls (conditional paths may differ): getBackupFormatFromKey, sanitizeBackupFormat, extractInlineBackupMetadata, extractHtmlBackupMetadata, extractJsonBackupMetadata
- L144 · normalizeBackupSecrets calls (conditional paths may differ): toISOString, filter, secrets.map, trim, String, sanitizeBackupSecretValue, isValidBackupSecretValue, invalidSecrets.push, Number.isSafeInteger, crypto.randomUUID, parseBackupType, pickBackupValue, parseBackupDefaultedInteger, parseBackupAlgorithm, parseBackupInteger, buildInvalidBackupSecretsError
- L190 · encodeBackupContent calls (conditional paths may differ): sanitizeBackupFormat, normalizeLanguage, toISOString, normalizeBackupSecrets, invalidSecrets.push, Array.isArray, buildInvalidBackupSecretsError, embedInlineBackupMetadata, buildOTPAuthText, buildCSVContent, buildHTMLContent, JSON.stringify
- L263 · createBackupEntry calls (conditional paths may differ): sanitizeBackupFormat, toISOString, encodeBackupContent, generateBackupKey, encryptData
- L325 · decodeBackupEntry calls (conditional paths may differ): startsWith, String, decryptData, sanitizeSkippedInvalidCount, decodeBackupContent, resolveBackupFormat, getBackupFormatFromKey, finalizeDecodedBackup, Array.isArray, parseBackupTimeFromKey, normalizeBackupSecrets, invalidSecrets.push, excludeNonRestorableSecrets, JSON.stringify
- L417 · decodeBackupContent calls (conditional paths may differ): excludeNonRestorableSecrets, decodeBackupContentByFormat
- L421 · decodeBackupContentByFormat calls (conditional paths may differ): sanitizeBackupFormat, decodeTextBackupContent, decodeCsvBackupContent, decodeHtmlBackupContent, decodeJsonBackupContent
- L436 · buildDownloadContent calls (conditional paths may differ): encodeBackupContent, getBackupContentType, getDownloadFilename
- L450 · getDownloadFilename calls (conditional paths may differ): sanitizeBackupFormat, getDownloadDateString, trim, String
- L474 · decodeJsonBackupContent calls (conditional paths may differ): JSON.parse, Array.isArray, toISOString, normalizeBackupSecrets, invalidSecrets.push, sanitizeSkippedInvalidCount
- L508 · decodeTextBackupContent calls (conditional paths may differ): extractInlineBackupMetadata, filter, map, split, String, line.trim, toISOString, lines.forEach, parseOTPAuthUrl, invalidLines.push, secrets.push, entryNumbers.push, buildInvalidBackupRowsError
- L545 · decodeCsvBackupContent calls (conditional paths may differ): extractInlineBackupMetadata, replace, String, parseCSVRows, toISOString, map, trim, findHeaderIndex, Object.values, sanitizeBackupSecretValue, isValidBackupSecretValue, invalidRows.push, entryNumbers.push, secrets.push, crypto.randomUUID, parseBackupType, parseBackupDefaultedInteger, parseBackupAlgorithm, parseBackupInteger, buildInvalidBackupRowsError
- L647 · decodeHtmlBackupContent calls (conditional paths may differ): extractHtmlBackupMetadata, extractEmbeddedJsonFromHtml, decodeJsonBackupContent, Math.max, sanitizeSkippedInvalidCount, parseErrors.push, extractOTPAuthUrlsFromHtml, decodeTextBackupContent, embeddedOtpauthUrls.join, decodeHtmlTableBackupContent
- L693 · decodeHtmlTableBackupContent calls (conditional paths may differ): toISOString, sanitizeSkippedInvalidCount, extractHtmlTableRows, rows.forEach, invalidRows.push, sanitizeBackupSecretValue, isValidBackupSecretValue, entryNumbers.push, normalizeLegacyHtmlAccount, secrets.push, crypto.randomUUID, trim, String, parseBackupType, parseBackupDefaultedInteger, parseBackupAlgorithm, parseBackupInteger, buildInvalidBackupRowsError
- L751 · buildOTPAuthText calls (conditional paths may differ): join, secrets.map, buildOTPAuthUrl
- L755 · buildCSVContent calls (conditional paths may differ): join, headers.map, test, escapeCSV, secrets.forEach, rows.push, rows.join
- L778 · embedInlineBackupMetadata calls (conditional paths may differ): sanitizeSkippedInvalidCount, String, textContent.startsWith, textContent.slice
- L794 · extractInlineBackupMetadata calls (conditional paths may differ): String, rawContent.startsWith, rawContent.slice, textContent.split, trim, firstLine.startsWith, firstLine.match, sanitizeSkippedInvalidCount, join, lines.slice
- L818 · extractHtmlBackupMetadata calls (conditional paths may differ): String, rawContent.match, sanitizeSkippedInvalidCount
- L836 · extractJsonBackupMetadata calls (conditional paths may differ): match, String, sanitizeSkippedInvalidCount
- L843 · buildHTMLContent calls (conditional paths may differ): getBackupDocumentText, escapeHtml, JSON.stringify, text, Object.fromEntries, map, Object.entries, flatMap, value.map, sanitizeSkippedInvalidCount, buildHtmlQrRows, payload.secrets.map, label, secretRows.join, Number.isNaN, date.getTime, date.toLocaleString, getStandaloneHead, getBackupDocumentStyles, getStandaloneLanguageSelect
- L943 · buildHtmlQrRows calls (conditional paths may differ): secrets.slice, Promise.all, batch.map, QRCode.create, buildOTPAuthUrl, buildSvgDataUrl, SvgRenderer.render, escapeHtml, getBackupDocumentText, rows.push
- L982 · buildSvgDataUrl calls (conditional paths may differ): toBase64
- L986 · toBase64 calls (conditional paths may differ): String, toString, globalThis.Buffer.from, encode, String.fromCharCode, bytes.slice, btoa
- L1008 · buildOTPAuthUrl calls (conditional paths may differ): trim, String, encodeURIComponent, params.set, toUpperCase, replace, params.toString
- L1039 · parseOTPAuthUrl calls (conditional paths may differ): replace, trim, String, normalized.startsWith, parseBackupType, url.pathname.replace, rawLabel.indexOf, url.searchParams.get, safeDecodeURIComponent, rawLabel.slice, sanitizeBackupSecretValue, isValidBackupSecretValue, crypto.randomUUID, parseBackupDefaultedInteger, parseBackupAlgorithm, parseBackupInteger
- L1077 · parseCSVRows calls (conditional paths may differ): row.push, pushCSVRow
- L1123 · findHeaderIndex calls (conditional paths may differ): candidates.map, item.toLowerCase, headers.findIndex, normalizedCandidates.includes, toLowerCase, String
- L1128 · pushCSVRow calls (conditional paths may differ): row.some, trim, String, rows.push
- L1140 · escapeCSV calls (conditional paths may differ): String, text.includes, text.replace
Environment references: env.ENCRYPTION_KEY
- L12 · buildPendingDataHashPayload calls (conditional paths may differ): JSON.stringify, toISOString
- L19 · parsePendingDataHashPayload calls (conditional paths may differ): JSON.parse
- L44 · sanitizeSkippedInvalidCount calls (conditional paths may differ): Number.parseInt, Number.isInteger
- L49 · generateDataHash calls (conditional paths may differ): getLogger, secrets.map, hashData.sort, localeCompare, JSON.stringify, map, hashData.slice, logger.debug, encoder.encode, crypto.subtle.digest, Array.from, join, hashArray.map, padStart, b.toString, hashHex.substring
- L94 · isPendingDataHashFresh calls (conditional paths may differ): Date.now, Date.parse, Number.isFinite
- L103 · getPendingDataHash calls (conditional paths may differ): parsePendingDataHashPayload, env.SECRETS_KV.get, warn, getLogger
- L116 · stageDataHash calls (conditional paths may differ): getLogger, generateDataHash, env.SECRETS_KV.put, buildPendingDataHashPayload, logger.info, hash.substring, logger.error
- L133 · clearPendingDataHash calls (conditional paths may differ): getLogger, env.SECRETS_KV.delete, logger.debug, logger.warn
- L150 · saveDataHash calls (conditional paths may differ): getLogger, sanitizeSkippedInvalidCount, logger.warn, generateDataHash, getPendingDataHash, env.SECRETS_KV.put, Date.parse, Number.isFinite, clearPendingDataHash, logger.debug, hash.substring, pendingHashEntry.hash.substring, logger.info, logger.error
Environment references: env.SECRETS_KV
- L50 · handleFaviconProxy calls (conditional paths may differ): getLogger, isValidDomain, createErrorResponse, source.url, logger.debug, setTimeout, controller.abort, fetch, clearTimeout, response.headers.get, logger.info, logger.warn, logger.error
- L138 · isValidDomain calls (conditional paths may differ): domain.includes, domainRegex.test
- L27 · handleGetWebDAVConfigs calls (conditional paths may differ): getLogger, getWebDAVConfigs, Promise.all, configs.map, getWebDAVStatus, createJsonResponse, logger.error, createErrorResponse
- L76 · handleSaveWebDAVConfig calls (conditional paths may differ): getLogger, getClientIdentifier, checkRateLimit, createRateLimitResponse, validateRequest, getWebDAVConfigs, configs.find, createErrorResponse, saveWebDAVSingleConfig, logger.info, createJsonResponse, logger.error
- L144 · handleDeleteWebDAVConfig calls (conditional paths may differ): getLogger, getClientIdentifier, checkRateLimit, createRateLimitResponse, url.searchParams.get, createErrorResponse, deleteWebDAVSingleConfig, logger.info, createJsonResponse, logger.error
- L188 · handleTestWebDAV calls (conditional paths may differ): getLogger, getClientIdentifier, checkRateLimit, createRateLimitResponse, validateRequest, getWebDAVConfigs, configs.find, createErrorResponse, logger.info, testWebDAVConnection, logger.warn, createJsonResponse, logger.error
- L238 · handleToggleWebDAV calls (conditional paths may differ): getLogger, getClientIdentifier, checkRateLimit, createRateLimitResponse, validateRequest, saveWebDAVSingleConfig, createErrorResponse, logger.info, createJsonResponse, logger.error
- L21 · handleGetS3Configs calls (conditional paths may differ): getLogger, getS3Configs, Promise.all, configs.map, getS3Status, createJsonResponse, logger.error, createErrorResponse
- L72 · handleSaveS3Config calls (conditional paths may differ): getLogger, getClientIdentifier, checkRateLimit, createRateLimitResponse, validateRequest, getS3Configs, configs.find, createErrorResponse, saveS3SingleConfig, logger.info, createJsonResponse, logger.error
- L139 · handleDeleteS3Config calls (conditional paths may differ): getLogger, getClientIdentifier, checkRateLimit, createRateLimitResponse, url.searchParams.get, createErrorResponse, deleteS3SingleConfig, logger.info, createJsonResponse, logger.error
- L183 · handleTestS3 calls (conditional paths may differ): getLogger, getClientIdentifier, checkRateLimit, createRateLimitResponse, validateRequest, getS3Configs, configs.find, createErrorResponse, logger.info, testS3Connection, logger.warn, createJsonResponse, logger.error
- L233 · handleToggleS3 calls (conditional paths may differ): getLogger, getClientIdentifier, checkRateLimit, createRateLimitResponse, validateRequest, saveS3SingleConfig, createErrorResponse, logger.info, createJsonResponse, logger.error
- L32 · handleGetOneDriveConfigs calls (conditional paths may differ): getLogger, getOneDriveConfigs, Promise.all, configs.map, getOneDriveStatus, createJsonResponse, isOneDriveOAuthConfigured, logger.error, createErrorResponse
- L77 · handleSaveOneDriveConfig calls (conditional paths may differ): getLogger, checkSensitiveRateLimit, validateRequest, getOneDriveConfigs, createErrorResponse, saveOneDriveSingleConfig, createJsonResponse, logger.error
- L120 · handleDeleteOneDriveConfig calls (conditional paths may differ): getLogger, checkSensitiveRateLimit, url.searchParams.get, createErrorResponse, deleteOneDriveSingleConfig, createJsonResponse, logger.error
- L154 · handleToggleOneDrive calls (conditional paths may differ): getLogger, checkSensitiveRateLimit, validateRequest, getOneDriveConfigs, configs.find, createErrorResponse, saveOneDriveSingleConfig, createJsonResponse, logger.error
- L197 · handleStartOneDriveOAuth calls (conditional paths may differ): getLogger, checkSensitiveRateLimit, validateRequest, isOneDriveOAuthConfigured, createErrorResponse, getOneDriveConfigs, configs.find, getOAuthRedirectBase, createOAuthState, getRequestLanguage, buildOneDriveAuthorizeUrl, createJsonResponse, logger.error
- L246 · handleOneDriveOAuthCallback calls (conditional paths may differ): getLogger, url.searchParams.get, extractOAuthStatePreview, consumeOAuthState, createOAuthPopupResponse, getOneDriveConfigs, configs.find, getOAuthRedirectBase, exchangeOneDriveCode, fetchOneDriveProfile, completeOneDriveAuthorization, testOneDriveConnectionById, filter, messageParts.join, logger.error
- L325 · checkSensitiveRateLimit calls (conditional paths may differ): getClientIdentifier, checkRateLimit, createRateLimitResponse
- L32 · handleGetGoogleDriveConfigs calls (conditional paths may differ): getLogger, getGoogleDriveConfigs, Promise.all, configs.map, getGoogleDriveStatus, createJsonResponse, isGoogleDriveOAuthConfigured, logger.error, createErrorResponse
- L77 · handleSaveGoogleDriveConfig calls (conditional paths may differ): getLogger, checkSensitiveRateLimit, validateRequest, getGoogleDriveConfigs, createErrorResponse, saveGoogleDriveSingleConfig, createJsonResponse, logger.error
- L120 · handleDeleteGoogleDriveConfig calls (conditional paths may differ): getLogger, checkSensitiveRateLimit, url.searchParams.get, createErrorResponse, deleteGoogleDriveSingleConfig, createJsonResponse, logger.error
- L154 · handleToggleGoogleDrive calls (conditional paths may differ): getLogger, checkSensitiveRateLimit, validateRequest, getGoogleDriveConfigs, configs.find, createErrorResponse, saveGoogleDriveSingleConfig, createJsonResponse, logger.error
- L197 · handleStartGoogleDriveOAuth calls (conditional paths may differ): getLogger, checkSensitiveRateLimit, validateRequest, isGoogleDriveOAuthConfigured, createErrorResponse, getGoogleDriveConfigs, configs.find, getOAuthRedirectBase, createOAuthState, getRequestLanguage, buildGoogleDriveAuthorizeUrl, createJsonResponse, logger.error
- L246 · handleGoogleDriveOAuthCallback calls (conditional paths may differ): getLogger, url.searchParams.get, extractOAuthStatePreview, consumeOAuthState, createOAuthPopupResponse, getGoogleDriveConfigs, configs.find, getOAuthRedirectBase, exchangeGoogleDriveCode, fetchGoogleDriveProfile, completeGoogleDriveAuthorization, testGoogleDriveConnectionById, filter, messageParts.join, logger.error
- L325 · checkSensitiveRateLimit calls (conditional paths may differ): getClientIdentifier, checkRateLimit, createRateLimitResponse
- L20 · handleChangePassword calls (conditional paths may differ): getLogger, getClientIdentifier, checkRateLimit, logger.warn, createRateLimitResponse, request.json, filter, env.SECRETS_KV.get, verifyPassword, ErrorFactory.passwordIncorrect, validatePasswordStrength, ErrorFactory.passwordWeak, hashPassword, env.SECRETS_KV.put, logger.info, toISOString, createJsonResponse, logError, errorToResponse, logger.error
Environment references: env.SECRETS_KV
- L70 · createSettingsFallbackHandler calls (conditional paths may differ): logger.warn
- L81 · handleGetSettings calls (conditional paths may differ): getLogger, getSettings, createSettingsFallbackHandler, createJsonResponse, logger.error, createErrorResponse
- L99 · handleSaveSettings calls (conditional paths may differ): getLogger, getClientIdentifier, checkRateLimit, logger.warn, createRateLimitResponse, request.json, getSettings, createSettingsFallbackHandler, Object.entries, sanitizeDefaultExportFormat, sanitizeLanguage, Number, env.SECRETS_KV.put, JSON.stringify, logger.info, Object.keys, createJsonResponse, logError, errorToResponse, logger.error
Environment references: env.SECRETS_KV
- L9 · handleGetTime calls (conditional paths may differ): createJsonResponse, Date.now
- L19 · createMainPage calls (conditional paths may differ): buildCompleteHTML
- L39 · buildCompleteHTML calls (conditional paths may differ): getHTMLStart, getStyles, getHTMLBody, getHTMLScripts, getHTMLEnd
- L46 · getHTMLStart calls (conditional paths may differ): normalizeLanguage.toString
- L140 · getHTMLBody calls (conditional paths may differ): dialogIcon, join, LANGUAGE_OPTIONS.map
- L1802 · getHTMLScripts calls (conditional paths may differ): getInlineScripts
- L1821 · getInlineScripts calls (conditional paths may differ): console.log, getCoreScripts, getScripts
- L17 · createSetupPage calls (conditional paths may differ): getRequestLanguage, serverMessages.filter, test, setupMessages.map, translatedSetupMessages.flatMap, map, Object.values, template.matchAll, value.replace, escapePattern, template.slice, parameters.map, Object.entries, k.startsWith, getSetupStyles, dialogIcon, join, LANGUAGE_OPTIONS.map, JSON.stringify, replace
Build and deployment pipeline · 4 GitHub Actions workflows
Repository CI declarations, separate from runtime request processing. Job dependencies and conditions are shown as written; long commands are shortened with an ellipsis; a workflow file does not prove a recent successful run.
Triggers: push, pull_request, workflow_dispatch
extension · no job dependencies declared
- Checkout
actions/checkout@v7 - Set up Node.js
actions/setup-node@v7 - Install dependencies
npm ci - Lint
npm run lint - Extension tests and coverage
npm run test:extension:coverage - Build Chrome and Edge extensions
npm run build:extension - Save coverage report
actions/upload-artifact@v7Condition: ${{ !cancelled() }}
browser-e2e · no job dependencies declared
- Checkout
actions/checkout@v7 - Set up Node.js
actions/setup-node@v7 - Install dependencies
npm ci - Install Chromium and system dependencies
npx playwright install --with-deps chromium - Build and test Chrome and Edge extensions in Chromium
npm run test:extension:e2e - Save browser test report and failure diagnostics
actions/upload-artifact@v7Condition: ${{ !cancelled() }}
Triggers: push
release · no job dependencies declared
- Validate release tag
if [[ ! "$RELEASE_TAG" =~ ^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$ ]]; then echo "Expected a stable version tag such as v1.10.0" exit 1 fi - Checkout tagged source
actions/checkout@v7 - Set up Node.js
actions/setup-node@v7 - Install dependencies
npm ci --ignore-scripts - Lint
npm run lint - Test
npm test -- --run - Build Worker release assets
npm run build - Package browser extensions
npm run package:extension - Publish notes and assets
node scripts/publish-release.js --tag "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY"
Triggers: workflow_dispatch
sync · no job dependencies declared
- Checkout current repository
actions/checkout@v4 - Clone upstream repository
git clone --depth=1 --branch "$UPSTREAM_REF" "https://github.com/$UPSTREAM_REPO.git" ../upstream - Preserve local deployment config
cp wrangler.toml /tmp/local-wrangler.toml - Sync files from upstream
rsync -a --checksum --delete \ --exclude '/.git/' \ --exclude '/.github/workflows/' \ ../upstream/ ./ - Merge deployment config
node scripts/merge-wrangler-config.js /tmp/local-wrangler.toml wrangler.toml wrangler.toml - Summarize wrangler.toml differences
{ echo '### In-place upgrade' echo '' echo 'This workflow upgrades the current repository in place and keeps the same Cloudflare Worker, KV bindings, and existing Secrets.' echo 'The upgrade path is the same whether `ENCRYPTION_KEY` is configured or not.' echo 'Do not delete the Worker, GitHub repository, KV namespace, or re-create `ENCRYPTION_KEY` for a normal upgrade.' echo '' } >> "$GITHUB_STEP_SUMMARY" if ! diff -u ../upstream/wrangler.toml wrangler.toml > /tmp/wrangler.diff; then { echo '#… - Commit and push changes
git add -A # Application upgrades must not change the caller's workflows. git diff --cached --exit-code -- .github/workflows/ if git diff --cached --quiet --exit-code; then echo "No upstream changes to commit." exit 0 fi git config user.name "github-actions[bot]" git config user.email "41898282+github-actions[bot]@users.noreply.github.com" git commit -m "chore: sync upstream ($UPSTREAM_REF)" git push
Triggers: push, schedule, workflow_dispatch
update · no job dependencies declared
Condition: github.repository == 'wuzf/2fa'
- Checkout
actions/checkout@v7 - Set up Node.js
actions/setup-node@v7 - Test generator
node .github/scripts/star-history.mjs self-test - Prepare data branch
set -euo pipefail target="$RUNNER_TEMP/star-history" mode="bootstrap" if git ls-remote --exit-code --heads origin refs/heads/star-history >/dev/null; then git fetch --no-tags origin refs/heads/star-history:refs/remotes/origin/star-history git worktree add --detach "$target" refs/remotes/origin/star-history if [[ -f "$target/history.json" ]]; then mode="snapshot" fi else status=$? if [[ "$status" -ne 2 ]]; then echo "Unable to check the star-history branch" >&2 exit "$status" fi git worktree add… - Generate snapshot
node .github/scripts/star-history.mjs "$STAR_HISTORY_MODE" "$STAR_HISTORY_DIR" - Publish snapshot
set -euo pipefail git -C "$STAR_HISTORY_DIR" config user.name "github-actions[bot]" git -C "$STAR_HISTORY_DIR" config user.email "41898282+github-actions[bot]@users.noreply.github.com" git -C "$STAR_HISTORY_DIR" add -- history.json star-history.svg if git -C "$STAR_HISTORY_DIR" diff --cached --quiet; then echo "Star history is already up to date" exit 0 fi git -C "$STAR_HISTORY_DIR" commit -m "chore: update star history snapshot" git -C "$STAR_HISTORY_DIR" push origin HEAD:refs/heads/star-histo…
deploy: node scripts/deploy.jsdeploy:git: node scripts/deploy.js --gitdeploy:package: node scripts/deploy.js --packagedeploy:dev: node scripts/deploy.js --env developmentdeploy:direct: node scripts/deploy.jsbuild: node scripts/build-release.jsbuild:minify: node scripts/build-release.js --minifybuild:extension: node scripts/build-extension.jsrelease:patch: node scripts/release.js patchrelease:minor: node scripts/release.js minorrelease:major: node scripts/release.js majorrelease:sync: node scripts/release.js --sync
Repository README
View original on GitHub ↗Full upstream document by @wuzf · README.md · snapshot 7b8060e
🔐 2FA
基于 Cloudflare Workers 的两步验证密钥管理系统。免费部署、全球加速、支持 PWA 离线使用。
简体中文 · 繁體中文 · English · 日本語 · 한국어 · Deutsch · Français · Español · Português (Brasil) · Italiano · Русский · Türkçe · Bahasa Indonesia · Tiếng Việt · ไทย
主要特性: TOTP/HOTP 验证码自动生成 · 二维码扫描/图片识别/粘贴截图/拖拽图片添加密钥 · AES-GCM 256 位加密存储 · 从 Google Authenticator、Aegis、2FAS、Bitwarden 等应用批量导入 · 多格式导出(TXT/JSON/CSV/HTML/Google 迁移二维码) · 自动备份与还原 · WebDAV/S3/OneDrive/Google Drive 远程备份同步 · 账户安全/同步/偏好设置 · 项目全模块 15 语支持(自动检测 / 手动切换) · 浅色/深色/跟随系统主题 · Fluent 2 风格响应式界面
网页、浏览器扩展、首次设置、公开 OTP 页面、接口提示及备份文档统一支持:简体中文、繁體中文、English、日本語、한국어、Deutsch、Français、Español、Português (Brasil)、Italiano、Русский、Türkçe、Bahasa Indonesia、Tiếng Việt、ไทย。界面可跟随浏览器或手动选择,未支持的浏览器语言回退英文;不同语言导出的 CSV/HTML 备份可相互导入。
🧩 浏览器扩展
安装「2FA 验证助手」:Chrome 应用商店 · Microsoft Edge 商店 · Firefox 附加组件商店。
请使用对应浏览器打开安装链接。安装后,在扩展设置中填写自己的 2FA 实例地址,并在同一浏览器中登录实例,即可查看、复制和填充 TOTP 验证码;自动填充需在目标验证页面单独开启并授权。扩展需配合已部署的本项目使用,界面支持上述 15 种语言。Firefox 需使用 153 及以上桌面版本的普通标签页,不支持容器标签页、隐私窗口或 Android。
安装与使用指南 · Chrome / Edge 隐私政策 · Firefox 隐私政策
📸 截图预览
| 桌面端 | 平板端 | 手机端 |
|---|---|---|
![]() |
![]() |
![]() |
🚀 快速部署
在线体验
访问演示站点(密码 2fa-Demo.):https://2fa-dev.wzf.workers.dev
一键部署(推荐)
推荐一键部署;所有用户统一通过 Sync Upstream 原地升级,禁止通过删除 Worker、删除仓库或重装方式升级。
- 点击上方按钮,使用 GitHub 登录并授权
- 登录 Cloudflare 账户,点击 Deploy 等待部署完成(KV 存储自动创建)
- 打开 Cloudflare 给你的 Workers 链接,设置管理密码即可开始使用
Git 自动构建会直接使用仓库中的
wrangler.toml部署;当前配置已显式声明SECRETS_KV,Wrangler 会在首次部署时自动创建所需 KV,并在后续部署中继续复用当前 Worker 已绑定的资源。 如果你在 Cloudflare Dashboard 中手动配置 Git 构建命令,部署命令请使用npm run deploy,不要直接写npx wrangler deploy,这样会保留项目里的版本注入流程,并和仓库默认部署入口保持一致。
推荐:启用数据加密
部署后,在 Cloudflare Dashboard → Worker → Settings → Variables 中添加 Secret ENCRYPTION_KEY:
# 生成加密密钥(任选一种)
openssl rand -base64 32
node -e "console.log(require('crypto').randomBytes(32).toString('base64'))"
ENCRYPTION_KEY是解密现有数据的主密钥。推荐设置,前提是你会把原始值立即保存到密码管理器、离线备份或其他安全位置。如果你无法确保保存原值,宁可暂时不设置,也不要设置后丢失:
- 设置后:密钥列表、自动备份、WebDAV/S3/OneDrive/Google Drive 凭据都会加密存储
- 丢失后:Cloudflare 不会再次显示原值,已有加密数据和加密备份将无法读取或恢复
- 当前程序行为:检测到已有加密数据但缺少
ENCRYPTION_KEY时,会直接锁定读取和修改,避免误覆盖旧数据
版本更新
一键部署生成的是独立仓库(非 Fork),升级统一使用 Sync Upstream 工作流原地完成。
⚠️ 升级前务必先备份数据:在执行版本更新前,请先通过 批量导出 或 还原配置 → 导出备份 将当前数据导出到本地,以防操作失败导致数据丢失。
- 打开一键部署时在你 GitHub 上生成的 2fa 仓库
- 进入 Actions → Sync Upstream
- 点击 Run workflow,上游分支保持默认的
main,发起一次新运行 - 等待同步完成及 Cloudflare 自动部署,之后刷新应用即可
工作流会自动保留你当前仓库里的 Worker 名称、KV 绑定和常见部署配置,并重新部署同一个 Worker。仓库中已有的工作流文件也会保留。
没有 Sync Upstream 入口时:一键部署创建的仓库可能不包含工作流。此时才需要在自己的仓库中新增
.github/workflows/sync-upstream.yml,内容复制自上游文件:https://github.com/wuzf/2fa/blob/main/.github/workflows/sync-upstream.yml,并提交一次。之后按上面步骤升级。
之前因
without workflows permission升级失败:修复发布到上游main后,已有自动合并部署配置步骤的 Sync Upstream 可以直接按上面步骤升级,无需修改 YAML 或配置 PAT。请选择main发起新运行,不要选择不含修复的旧版本标签。其他情况见升级故障排查。
这种方式不会动现有 Worker、KV 绑定或 Secrets。如果你已经设置了 ENCRYPTION_KEY,升级时无需重新填写;如果你没设置,也照样用这套流程升级。
⚠️
ENCRYPTION_KEY是解密现有数据的主密钥,请务必在首次创建时保存到密码管理器。Cloudflare Secret 保存后不会再次显示原值;正常升级不需要重新填写,但如果你把它删了又没保存原值,已有加密数据将无法恢复。
⚠️ 回滚到 1.8.0 之前的版本:1.8.0 起 HOTP 计数器的递增单独存储,回滚前需要先调用一次压实接口把计数器写回主数据,否则 HOTP 计数器会退回到升级时的值。步骤见回滚到 1.8.0 之前的版本。只用 TOTP 的部署不受影响。
如果你想检查合并结果
Sync Upstream 的设计目标是始终在同一仓库、同一 Worker上完成升级。现在工作流会自动合并 wrangler.toml,并在摘要中展示与上游的差异,便于你确认哪些值来自本地部署配置:
- 在 GitHub Actions 的运行摘要里查看
wrangler.tomldiff - 打开当前仓库里的
wrangler.toml - 确认 Worker 名称、KV 绑定、路由和现有部署设置仍然正确
- 如果你自己维护了非常特殊的
wrangler.toml配置,再按需要补充提交
如果 Cloudflare 没有自动开始重新部署,也是在 Deployments 页面重新部署当前仓库的最新提交,而不是删除后重装。
📖 使用指南
添加密钥
点击右下角 ➕ 悬浮按钮:
- 扫二维码 — 摄像头扫描 2FA 二维码,自动填入
- 选择图片 — 上传二维码截图,自动识别
- 粘贴截图 — Ctrl+V 粘贴剪贴板中的二维码截图(适合无摄像头的 PC 用户)
- 拖拽图片 — 直接将二维码图片拖入弹窗,自动识别
- 手动添加 — 输入服务名称和 Base32 密钥(可展开高级设置调整位数/周期/算法)
日常使用
- 复制验证码:直接点击验证码数字
- 管理密钥:点击卡片右上角 ⋯ → 查看二维码 / 复制 URI / 复制网页链接 / 编辑 / 删除
- 搜索:顶部搜索框按服务名或账户名实时搜索
- 智能聚合:默认按服务家族自动聚合,同一服务的多个账户归在一起,也可切换为全部平铺
- 排序:按添加时间或名称排序
- 主题:悬浮按钮 → 设置 → 偏好设置 → 主题模式,选择浅色、深色或跟随系统
批量导入
点击悬浮按钮 → 📥 批量导入,支持文件导入或文本粘贴。
兼容格式:
| 来源 | 格式 |
|---|---|
| 通用 | otpauth:// URI 文本(TXT)、CSV、HTML |
| Google Authenticator | 迁移二维码(otpauth-migration://) |
| Aegis | JSON 导出文件 |
| 2FAS | .2fas 导出文件 |
| Bitwarden | JSON、Authenticator CSV 导出文件 |
| LastPass Authenticator | JSON 导出文件 |
| andOTP | JSON 导出文件 |
| Ente Auth | 导出文件 |
批量导出
点击悬浮按钮 → 📤 批量导出,支持 TXT、JSON、CSV、HTML 格式,以及生成 Google Authenticator 迁移二维码(可直接扫码导入)。 标准 TXT / JSON / CSV / HTML 导出在在线时优先使用统一后端格式;离线或请求体过大时会自动回退到本地兼容导出,继续保证 PWA 可用性。
备份与还原
系统自动备份(数据变化后自动触发 + 每天定时检查),保留最近 100 个备份(可在设置中调整)。
新创建的备份文件格式会跟随 设置 → 默认导出格式;远程自动备份也会使用相同的扩展名(txt / json / csv / html)。
点击悬浮按钮 → 🔄 还原配置 查看备份列表、预览内容、还原或导出;也可以上传从 WebDAV/S3/OneDrive/Google Drive 下载的 backup_*.(txt|json|csv|html) 文件进行预览和恢复。
远程备份
支持将备份同步到远程存储,数据变更时自动推送,可配置多个备份目标:
- WebDAV — 支持标准 WebDAV 协议的网盘或自建服务(⚠️ 不支持经 Cloudflare 代理的服务如坚果云,会触发 520 回环错误)
- S3 兼容存储 — 支持 AWS S3、Cloudflare R2、MinIO、阿里云 OSS 等 S3 兼容服务
- OneDrive — 通过 Microsoft OAuth 授权后,将备份写入 OneDrive 应用专用目录下的子路径
- Google Drive — 通过 Google OAuth 授权后,将备份写入 Google Drive 指定目录
在 设置 → 同步设置 中添加和管理远程备份目标。
远程备份保存的是应用生成的同一份备份内容。若创建备份时已配置 ENCRYPTION_KEY,远程文件内容也是加密密文;恢复时需在 Worker 中保留同一个 ENCRYPTION_KEY。
详细配置步骤见:网盘备份配置指南
设置
点击悬浮按钮 → ⚙️ 设置:
- 修改密码 — 更改管理密码
- 主题模式 — 选择浅色、深色或跟随系统
- 验证码交接动效 — 关闭或选择流转、翻牌、聚光动效
- 登录有效期 — 自定义 JWT 过期时间
- 默认导出格式 — 控制导出按钮默认格式,也用于新建备份文件和远程自动备份的文件扩展名
- 备份保留数量 — 调整自动备份保留份数
- 远程备份 — 配置 WebDAV/S3/OneDrive/Google Drive 备份目标
- 退出登录 — 一键清除当前会话 Cookie 与本地缓存,离线/服务端故障时仍能本地登出
安装为手机应用(PWA)
- iOS:Safari 打开 → 分享按钮 → 添加到主屏幕
- Android:Chrome 打开 → 菜单(⋮)→ 添加到主屏幕
安装后可像原生应用一样全屏使用,支持离线访问。
Chrome / Edge / Firefox 验证码辅助填充
在目标网站点击扩展选择账户,或按 Ctrl+Shift+U 填入已绑定账户的当前 TOTP。按页面授权后,可自动检测验证码框并填充;多个账户匹配时显示选择面板。支持单框和 6/8 格输入,不主动提交表单。
扩展使用网页登录会话,支持明确启用离线缓存,均无需保持主网页打开。网页登录模式在后台临时读取密钥,离线模式则在本机保留独立密钥缓存,断网后仍可取码。种子不传给弹窗或目标网站,离线缓存没有额外密码加密。支持开放 Shadow DOM 与同源 iframe;暂不支持 HOTP、跨域 iframe、关闭的 Shadow DOM 或隐私模式。
详见安装与使用指南、Chrome / Edge 隐私说明及Firefox 隐私说明。
🔒 安全
- 密码:PBKDF2-SHA256(100,000 次迭代)加盐哈希,JWT 存储在 HttpOnly + Secure + SameSite=Strict Cookie 中
- 数据加密:配置
ENCRYPTION_KEY后所有密钥、备份以及 WebDAV/S3/OneDrive/Google Drive 凭据使用 AES-GCM 256 位加密;请务必保存原始密钥,丢失后无法解密已有数据 - 传输:全程 HTTPS,TLS 1.2+
- 隐私:OTP 在客户端生成,不收集使用数据,完全开源
- 登录有效期:默认 30 天,可在设置中自定义,活跃使用自动续期(剩余 < 7 天时自动延长)
🔗 公开 OTP API
无需登录,通过 URL 直接生成验证码:
https://your-worker.workers.dev/otp/YOUR_SECRET_KEY
https://your-worker.workers.dev/otp/YOUR_SECRET_KEY?digits=8&period=60
https://your-worker.workers.dev/otp/YOUR_SECRET_KEY?type=hotp&counter=5
参数:type(totp/hotp)、digits(6/8)、period(30/60/120)、algorithm(sha1/sha256/sha512)、counter(HOTP 用)
TOTP 网页同时显示当前和下一个验证码,均可点击复制,到期后原地更新。HOTP 网页显示链接中指定计数器的验证码,复制不会推进计数器。
📚 更多文档
| 文档 | 说明 |
|---|---|
| 部署指南 | 手动部署、KV 配置、Secrets 管理 |
| 网盘备份配置指南 | OneDrive / Google Drive 中文配置步骤与简化设计建议 |
| API 参考 | 完整 API 端点文档 |
| 架构设计 | 系统架构与技术实现 |
| 开发指南 | 本地开发、测试、代码规范 |
| PWA 指南 | PWA 安装与离线功能 |
| 浏览器扩展指南 | Chrome / Edge / Firefox 扩展安装、使用与权限 |
🤝 参与贡献
欢迎提交 Issue 和 Pull Request。开发相关请参考 开发指南。
📄 许可证
🌟 Star History
如果这个项目对您有帮助,请给一个 ⭐
Made with ❤️ by wuzf
Frequently asked about 2FA
What is 2FA?+
2FA is a self-hosted 2FAS/Google Authenticator alternative built on the Cloudflare developer platform. Manage TOTP and HOTP codes in a self-hosted web app on Workers and KV.
What does 2FA replace?+
2FA is listed as an alternative to 2FAS, Google Authenticator. Compare the features and tradeoffs before migrating.
What Cloudflare primitives does 2FA use?+
2FA is built on KV, Workers.
How much does 2FA cost to run?+
The documented 2FA deployment can use Cloudflare Free allowances for a small workload under the request, CPU and service-specific quotas below. This is conditional eligibility, not a measured zero-cost deployment; optional features, domains and external providers can add costs. Workers Free allows 100,000 requests per day shared across the account and 10 ms CPU per invocation; measure CPU-heavy authentication, parsing and rendering before assuming it fits. Keep KV below 100,000 reads/day, 1,000 writes, deletes and list operations/day each, and 1 GB; cache refreshes and backups consume writes. Use a small personal or team workload; domain registration and optional third-party providers are separate costs. Provision your own IDs, secrets and migrations. Check current Cloudflare pricing before deploying.
Is 2FA open source?+
The upstream repository declares the MIT license. Read its terms at https://raw.githubusercontent.com/wuzf/2fa/7b8060e88f7a67bb87cc00bb339dbeec25833fac/LICENSE. Source code and contributor credit are available at https://github.com/wuzf/2fa.






Discussion · 0
sign in to comment →