create-walle 0.9.21 → 0.9.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -5
- package/package.json +2 -2
- package/template/CLAUDE.md +2 -2
- package/template/LICENSE +1 -1
- package/template/bin/ctm-dev-cleanup.js +24 -3
- package/template/bin/ctm-launch.sh +13 -0
- package/template/bin/dev.sh +156 -18
- package/template/bin/node-bin.sh +84 -0
- package/template/bin/pin-node.sh +51 -0
- package/template/claude-task-manager/api-prompts.js +1203 -182
- package/template/claude-task-manager/api-reviews.js +109 -15
- package/template/claude-task-manager/approval-agent.js +1360 -280
- package/template/claude-task-manager/bin/restart-ctm.sh +64 -23
- package/template/claude-task-manager/bin/storage-migration-supervisor.js +338 -0
- package/template/claude-task-manager/db.js +4417 -295
- package/template/claude-task-manager/docs/app-update-refresh-protocol.md +69 -0
- package/template/claude-task-manager/docs/approval-ai-refinement.md +138 -0
- package/template/claude-task-manager/docs/approval-rescue-loop.md +74 -0
- package/template/claude-task-manager/docs/codex-operational-warning-health.md +107 -0
- package/template/claude-task-manager/docs/codex-resume-state-guard-design.md +17 -12
- package/template/claude-task-manager/docs/codex-terminal-render-controller-handoff.md +311 -0
- package/template/claude-task-manager/docs/coding-agent-hooks-architecture.md +418 -0
- package/template/claude-task-manager/docs/conversation-import-freshness.md +20 -0
- package/template/claude-task-manager/docs/google-workspace-auth-health.md +77 -0
- package/template/claude-task-manager/docs/image-paste-ux.md +13 -0
- package/template/claude-task-manager/docs/ipad-web-preview.md +88 -0
- package/template/claude-task-manager/docs/main-loop-offload-architecture.md +66 -0
- package/template/claude-task-manager/docs/microsoft-dev-tunnel-phone-access-design.md +274 -519
- package/template/claude-task-manager/docs/mobile-live-streaming.md +27 -5
- package/template/claude-task-manager/docs/mobile-remote-submission-lifecycle.md +69 -0
- package/template/claude-task-manager/docs/phone-access-design.md +53 -15
- package/template/claude-task-manager/docs/phone-passkey-identity.md +122 -0
- package/template/claude-task-manager/docs/phone-setup.md +3 -0
- package/template/claude-task-manager/docs/prompt-editing-tree-design.md +25 -1
- package/template/claude-task-manager/docs/remote-desktop-access-design.md +268 -0
- package/template/claude-task-manager/docs/restart-lifecycle-architecture.md +95 -0
- package/template/claude-task-manager/docs/runtime-work-control-plane.md +53 -0
- package/template/claude-task-manager/docs/session-interactive-wait-surfaces.md +38 -0
- package/template/claude-task-manager/docs/session-needs-you-dismissal.md +84 -0
- package/template/claude-task-manager/docs/session-render-state-management-design.md +91 -3
- package/template/claude-task-manager/docs/session-standup-command-center-design.md +25 -1
- package/template/claude-task-manager/docs/session-title-authority.md +32 -0
- package/template/claude-task-manager/docs/session-workspace-binding.md +33 -0
- package/template/claude-task-manager/docs/skill-intent-resolution-design.md +72 -0
- package/template/claude-task-manager/docs/walle-mcp-supervisor-health.md +86 -0
- package/template/claude-task-manager/docs/walle-relay-phone-access-design.md +24 -15
- package/template/claude-task-manager/docs/walle-session-history-hydration.md +114 -0
- package/template/claude-task-manager/docs/walle-session-input-queue.md +104 -0
- package/template/claude-task-manager/docs/walle-session-model-catalog.md +90 -0
- package/template/claude-task-manager/docs/walle-session-model-preferences.md +15 -6
- package/template/claude-task-manager/git-utils.js +897 -27
- package/template/claude-task-manager/lib/agent-capabilities.js +33 -0
- package/template/claude-task-manager/lib/agent-cli-cache.js +37 -7
- package/template/claude-task-manager/lib/agent-hooks-installer.js +26 -2
- package/template/claude-task-manager/lib/agent-presets.js +17 -1
- package/template/claude-task-manager/lib/all-sessions-query.js +108 -0
- package/template/claude-task-manager/lib/approval-ai-refinement.js +488 -0
- package/template/claude-task-manager/lib/approval-self-adapt.js +168 -0
- package/template/claude-task-manager/lib/async-semaphore.js +44 -0
- package/template/claude-task-manager/lib/auth-context.js +5 -0
- package/template/claude-task-manager/lib/auth-rate-limit.js +47 -4
- package/template/claude-task-manager/lib/auth-rules.js +29 -2
- package/template/claude-task-manager/lib/auto-approval-verifier.js +129 -16
- package/template/claude-task-manager/lib/background-llm.js +144 -17
- package/template/claude-task-manager/lib/branch-inventory.js +212 -0
- package/template/claude-task-manager/lib/claude-desktop-sessions.js +15 -3
- package/template/claude-task-manager/lib/coalesce-sync-frames.js +151 -0
- package/template/claude-task-manager/lib/codex-launch-health.js +762 -0
- package/template/claude-task-manager/lib/codex-transcript-pager.js +51 -0
- package/template/claude-task-manager/lib/codex-zst.js +124 -0
- package/template/claude-task-manager/lib/coding-agent-models.js +233 -30
- package/template/claude-task-manager/lib/connection-health.js +232 -0
- package/template/claude-task-manager/lib/conversation-blob-parser.js +42 -0
- package/template/claude-task-manager/lib/conversation-tail-merge.js +89 -26
- package/template/claude-task-manager/lib/ctm-session-context-api.js +39 -10
- package/template/claude-task-manager/lib/cursor-conversation-store.js +354 -0
- package/template/claude-task-manager/lib/db-owner-worker-client.js +315 -0
- package/template/claude-task-manager/lib/document-review.js +141 -6
- package/template/claude-task-manager/lib/escalation-review.js +152 -0
- package/template/claude-task-manager/lib/graceful-shutdown.js +159 -0
- package/template/claude-task-manager/lib/headless-term-service.js +678 -0
- package/template/claude-task-manager/lib/heavy-worker-fallback.js +38 -0
- package/template/claude-task-manager/lib/jsonl-conversation-parser.js +542 -0
- package/template/claude-task-manager/lib/jsonl-range-reader.js +112 -0
- package/template/claude-task-manager/lib/main-db-census.js +216 -0
- package/template/claude-task-manager/lib/message-pagination.js +106 -4
- package/template/claude-task-manager/lib/microsoft-dev-tunnel-setup.js +750 -26
- package/template/claude-task-manager/lib/mobile-auth-api.js +274 -7
- package/template/claude-task-manager/lib/mobile-auth-store.js +592 -10
- package/template/claude-task-manager/lib/mobile-notification-dispatcher.js +15 -0
- package/template/claude-task-manager/lib/model-overview-brain-fallback.js +311 -0
- package/template/claude-task-manager/lib/model-overview-cache.js +141 -0
- package/template/claude-task-manager/lib/models-health-routing-notice.js +126 -0
- package/template/claude-task-manager/lib/node-pin-guard.js +93 -0
- package/template/claude-task-manager/lib/perf-tracker.js +242 -6
- package/template/claude-task-manager/lib/permission-match.js +76 -0
- package/template/claude-task-manager/lib/permission-sync.js +133 -20
- package/template/claude-task-manager/lib/process-title.js +35 -0
- package/template/claude-task-manager/lib/prompt-executions-query.js +25 -0
- package/template/claude-task-manager/lib/prompt-index-disk-cache.js +44 -0
- package/template/claude-task-manager/lib/prompt-intent.js +132 -0
- package/template/claude-task-manager/lib/provider-user-context.js +34 -0
- package/template/claude-task-manager/lib/read-pool-client.js +313 -0
- package/template/claude-task-manager/lib/readpool-breaker.js +31 -0
- package/template/claude-task-manager/lib/recent-sessions-breaker.js +12 -0
- package/template/claude-task-manager/lib/remote-feedback-client.js +72 -0
- package/template/claude-task-manager/lib/remote-relay-protocol.js +37 -4
- package/template/claude-task-manager/lib/remote-relay-store.js +159 -0
- package/template/claude-task-manager/lib/remote-submission-observer.js +278 -0
- package/template/claude-task-manager/lib/restart-guard.js +109 -0
- package/template/claude-task-manager/lib/restore-interruption-detector.js +439 -0
- package/template/claude-task-manager/lib/restore-policy.js +13 -0
- package/template/claude-task-manager/lib/restore-resume-batch.js +74 -0
- package/template/claude-task-manager/lib/restore-runtime.js +68 -0
- package/template/claude-task-manager/lib/restore-storm.js +34 -0
- package/template/claude-task-manager/lib/resume-cwd.js +36 -0
- package/template/claude-task-manager/lib/resume-preflight.js +313 -0
- package/template/claude-task-manager/lib/runtime-work-registry.js +444 -0
- package/template/claude-task-manager/lib/sanitize-openai-auth.js +31 -0
- package/template/claude-task-manager/lib/scheduler.js +21 -1
- package/template/claude-task-manager/lib/scrollback-snapshot-store.js +159 -0
- package/template/claude-task-manager/lib/serial-task-queue.js +64 -0
- package/template/claude-task-manager/lib/server-listeners.js +239 -0
- package/template/claude-task-manager/lib/session-capture.js +42 -7
- package/template/claude-task-manager/lib/session-content-backfill.js +131 -0
- package/template/claude-task-manager/lib/session-history.js +388 -43
- package/template/claude-task-manager/lib/session-host-manager.js +287 -0
- package/template/claude-task-manager/lib/session-image-refs.js +209 -0
- package/template/claude-task-manager/lib/session-jobs.js +399 -59
- package/template/claude-task-manager/lib/session-prompt-index.js +137 -0
- package/template/claude-task-manager/lib/session-restore.js +53 -0
- package/template/claude-task-manager/lib/session-standup.js +123 -23
- package/template/claude-task-manager/lib/session-state-bus.js +14 -0
- package/template/claude-task-manager/lib/session-stream.js +64 -16
- package/template/claude-task-manager/lib/session-timeline-summary.js +260 -0
- package/template/claude-task-manager/lib/session-token-usage.js +494 -0
- package/template/claude-task-manager/lib/session-workspace-binding.js +356 -0
- package/template/claude-task-manager/lib/setup-network-config.js +9 -0
- package/template/claude-task-manager/lib/size-cap.js +45 -0
- package/template/claude-task-manager/lib/size-cap.test.js +62 -0
- package/template/claude-task-manager/lib/skill-autocomplete.js +180 -1
- package/template/claude-task-manager/lib/skill-intent-resolver.js +304 -0
- package/template/claude-task-manager/lib/sqlite-driver.js +19 -3
- package/template/claude-task-manager/lib/standup-attention.js +7 -3
- package/template/claude-task-manager/lib/status-authority.js +39 -0
- package/template/claude-task-manager/lib/status-hooks.js +4 -0
- package/template/claude-task-manager/lib/storage-migration.js +235 -0
- package/template/claude-task-manager/lib/structured-capture.js +298 -0
- package/template/claude-task-manager/lib/sync-io-census.js +163 -0
- package/template/claude-task-manager/lib/tailscale-setup.js +6 -0
- package/template/claude-task-manager/lib/terminal-activity-evidence.js +33 -0
- package/template/claude-task-manager/lib/terminal-choice.js +364 -0
- package/template/claude-task-manager/lib/terminal-control-sanitize.js +17 -0
- package/template/claude-task-manager/lib/terminal-fingerprint.js +48 -0
- package/template/claude-task-manager/lib/terminal-output-flush.js +84 -0
- package/template/claude-task-manager/lib/timeline-order.js +122 -0
- package/template/claude-task-manager/lib/transcript-store.js +348 -43
- package/template/claude-task-manager/lib/transport-security.js +84 -1
- package/template/claude-task-manager/lib/wait-state.js +184 -0
- package/template/claude-task-manager/lib/walle-client.js +47 -5
- package/template/claude-task-manager/lib/walle-ctm-history.js +564 -4
- package/template/claude-task-manager/lib/walle-external-actions.js +135 -16
- package/template/claude-task-manager/lib/walle-history-hydration.js +46 -0
- package/template/claude-task-manager/lib/walle-native-health.js +403 -0
- package/template/claude-task-manager/lib/walle-repair.js +701 -0
- package/template/claude-task-manager/lib/walle-session-cache.js +109 -0
- package/template/claude-task-manager/lib/walle-session-context.js +57 -21
- package/template/claude-task-manager/lib/walle-session-model-catalog.js +34 -0
- package/template/claude-task-manager/lib/walle-supervisor.js +539 -63
- package/template/claude-task-manager/lib/walle-transcript.js +52 -0
- package/template/claude-task-manager/lib/worktree-active-sync.js +11 -7
- package/template/claude-task-manager/lib/worktree-cwd.js +32 -1
- package/template/claude-task-manager/package.json +1 -1
- package/template/claude-task-manager/prompt-harvest.js +89 -66
- package/template/claude-task-manager/providers/claude-code.js +51 -3
- package/template/claude-task-manager/providers/cursor.js +140 -45
- package/template/claude-task-manager/public/css/reviews.css +551 -61
- package/template/claude-task-manager/public/css/setup.css +191 -0
- package/template/claude-task-manager/public/css/walle-session.css +865 -10
- package/template/claude-task-manager/public/css/walle.css +154 -0
- package/template/claude-task-manager/public/designs/ai-providers-consolidation-v2.html +830 -0
- package/template/claude-task-manager/public/index.html +18516 -2058
- package/template/claude-task-manager/public/ipad.html +363 -0
- package/template/claude-task-manager/public/js/document-review-links.js +301 -0
- package/template/claude-task-manager/public/js/image-normalize.js +69 -36
- package/template/claude-task-manager/public/js/message-renderer.js +1265 -77
- package/template/claude-task-manager/public/js/prompts.js +66 -29
- package/template/claude-task-manager/public/js/reviews.js +901 -133
- package/template/claude-task-manager/public/js/session-activity-utils.js +11 -1
- package/template/claude-task-manager/public/js/session-search-utils.js +94 -10
- package/template/claude-task-manager/public/js/session-status-precedence.js +23 -5
- package/template/claude-task-manager/public/js/setup.js +1273 -176
- package/template/claude-task-manager/public/js/stream-view.js +691 -73
- package/template/claude-task-manager/public/js/terminal-reconciler.js +210 -0
- package/template/claude-task-manager/public/js/walle-session.js +2455 -158
- package/template/claude-task-manager/public/js/walle.js +455 -28
- package/template/claude-task-manager/public/m/app.css +2909 -262
- package/template/claude-task-manager/public/m/app.js +6601 -398
- package/template/claude-task-manager/public/m/claim.html +224 -17
- package/template/claude-task-manager/public/m/index.html +117 -21
- package/template/claude-task-manager/public/m/sw.js +3 -1
- package/template/claude-task-manager/public/manifest.json +2 -2
- package/template/claude-task-manager/public/prompts.html +30 -14
- package/template/claude-task-manager/queue-engine.js +507 -28
- package/template/claude-task-manager/scripts/repair-claude-session-images.js +27 -8
- package/template/claude-task-manager/server.js +14341 -2197
- package/template/claude-task-manager/session-integrity.js +160 -18
- package/template/claude-task-manager/session-search-ranking.js +1 -0
- package/template/claude-task-manager/session-utils.js +25 -5
- package/template/claude-task-manager/workers/approval-blocklist.js +96 -6
- package/template/claude-task-manager/workers/approval-widget-validator.js +14 -8
- package/template/claude-task-manager/workers/conversation-import-worker.js +11 -50
- package/template/claude-task-manager/workers/db-owner-worker.js +386 -0
- package/template/claude-task-manager/workers/harvest-worker.js +9 -55
- package/template/claude-task-manager/workers/headless-term-worker.js +9 -530
- package/template/claude-task-manager/workers/read-pool-worker.js +387 -0
- package/template/claude-task-manager/workers/scrollback-worker.js +11 -72
- package/template/claude-task-manager/workers/session-host-process.js +146 -0
- package/template/claude-task-manager/workers/session-integrity-worker.js +10 -54
- package/template/claude-task-manager/workers/state-detectors/base.js +18 -1
- package/template/claude-task-manager/workers/state-detectors/claude-code.js +182 -9
- package/template/claude-task-manager/workers/state-detectors/codex.js +150 -2
- package/template/claude-task-manager/workers/state-detectors/cursor.js +127 -0
- package/template/claude-task-manager/workers/state-detectors/gemini.js +21 -0
- package/template/claude-task-manager/workers/state-detectors/index.js +29 -0
- package/template/claude-task-manager/workers/state-detectors/opencode.js +103 -0
- package/template/docs/design/markdown-review-pane.md +206 -0
- package/template/docs/designs/2026-05-17-portkey-gateway-provider-ux.md +129 -38
- package/template/docs/designs/2026-05-20-mobile-worktree-finish-command.md +27 -0
- package/template/docs/designs/2026-05-22-ai-configuration-consolidation.md +248 -0
- package/template/docs/designs/ai-configuration-consolidation-mock.html +812 -0
- package/template/docs/private-memory-and-pii-policy.md +69 -0
- package/template/package.json +2 -1
- package/template/scripts/check-private-data.js +201 -0
- package/template/shared/sqlite-owner-guard.js +30 -0
- package/template/shared/sqlite-owner-write-queue.js +225 -0
- package/template/shared/sqlite-storage-policy.js +111 -0
- package/template/shared/sqlite-write-lock.js +428 -0
- package/template/wall-e/agent-runners/claude-code.js +5 -0
- package/template/wall-e/agent.js +166 -22
- package/template/wall-e/api-walle.js +524 -70
- package/template/wall-e/auth/provider-flows.js +11 -1
- package/template/wall-e/bin/walle-mcp-stdio.js +341 -17
- package/template/wall-e/brain.js +1614 -141
- package/template/wall-e/chat/attachment-blocks.js +96 -0
- package/template/wall-e/chat/attachments.js +2 -1
- package/template/wall-e/chat/capability-resolver.js +7 -7
- package/template/wall-e/chat/context-messages.js +28 -0
- package/template/wall-e/chat/conversation-frame.js +630 -0
- package/template/wall-e/chat/provider-messages.js +125 -0
- package/template/wall-e/chat.js +1002 -233
- package/template/wall-e/coding/acceptance-contract.js +170 -0
- package/template/wall-e/coding/acp-adapter.js +1 -1
- package/template/wall-e/coding/agent-catalog.js +3 -0
- package/template/wall-e/coding/artifact-store.js +93 -0
- package/template/wall-e/coding/capability-router.js +120 -0
- package/template/wall-e/coding/coding-run-controller.js +423 -0
- package/template/wall-e/coding/compaction-service.js +157 -12
- package/template/wall-e/coding/frontend-verification.js +258 -0
- package/template/wall-e/coding/lifecycle-hooks.js +75 -0
- package/template/wall-e/coding/local-preview-contract.js +157 -0
- package/template/wall-e/coding/permission-service.js +57 -13
- package/template/wall-e/coding/prompt-bundle.js +19 -1
- package/template/wall-e/coding/prompt-section-registry.js +227 -0
- package/template/wall-e/coding/provider-compat.js +15 -0
- package/template/wall-e/coding/runtime-events.js +224 -0
- package/template/wall-e/coding/runtime-mode.js +3 -0
- package/template/wall-e/coding/side-git-snapshot.js +160 -4
- package/template/wall-e/coding/snapshot-service.js +143 -1
- package/template/wall-e/coding/stream-processor.js +388 -34
- package/template/wall-e/coding/task-tool.js +141 -4
- package/template/wall-e/coding/tool-execution-controller.js +365 -0
- package/template/wall-e/coding/tool-registry.js +43 -5
- package/template/wall-e/coding/user-hooks.js +217 -0
- package/template/wall-e/coding-orchestrator.js +1330 -221
- package/template/wall-e/coding-prompts.js +20 -4
- package/template/wall-e/context/context-builder.js +15 -2
- package/template/wall-e/decision/confidence.js +1 -1
- package/template/wall-e/docs/coding-acceptance-contract.md +41 -0
- package/template/wall-e/docs/external-action-controller.md +26 -6
- package/template/wall-e/docs/telemetry-lifecycle.md +8 -2
- package/template/wall-e/embeddings.js +591 -53
- package/template/wall-e/external-action-controller.js +12 -0
- package/template/wall-e/http/auth.js +1 -0
- package/template/wall-e/http/chat-api.js +46 -11
- package/template/wall-e/http/model-admin.js +836 -34
- package/template/wall-e/lib/boot-profile.js +88 -0
- package/template/wall-e/lib/event-loop-monitor.js +93 -0
- package/template/wall-e/lib/service-health.js +194 -0
- package/template/wall-e/llm/anthropic.js +130 -5
- package/template/wall-e/llm/client.js +266 -63
- package/template/wall-e/llm/default-fallback.js +382 -0
- package/template/wall-e/llm/health.js +19 -0
- package/template/wall-e/llm/message-guard.js +78 -0
- package/template/wall-e/llm/model-catalog.js +252 -1
- package/template/wall-e/llm/openai.js +26 -4
- package/template/wall-e/llm/portkey-sync.js +654 -0
- package/template/wall-e/llm/provider-error.js +30 -2
- package/template/wall-e/llm/registry.js +5 -1
- package/template/wall-e/llm/request-compat.js +67 -0
- package/template/wall-e/loops/backfill.js +79 -23
- package/template/wall-e/loops/brain-optimize.js +67 -0
- package/template/wall-e/loops/ingest.js +25 -10
- package/template/wall-e/loops/question-digest.js +160 -0
- package/template/wall-e/loops/reflect.js +6 -4
- package/template/wall-e/loops/think.js +39 -12
- package/template/wall-e/mcp-server.js +318 -36
- package/template/wall-e/memory/ctm-context-client.js +52 -14
- package/template/wall-e/memory/ctm-operational-context.js +237 -0
- package/template/wall-e/memory/ctm-prompt-executions-client.js +128 -0
- package/template/wall-e/memory/ctm-session-context.js +111 -63
- package/template/wall-e/prompts/coding/deepseek.txt +3 -0
- package/template/wall-e/prompts/coding/gemini.txt +6 -0
- package/template/wall-e/prompts/coding/gpt.txt +6 -0
- package/template/wall-e/prompts/coding/local.txt +7 -0
- package/template/wall-e/runtime/decision-hooks.js +115 -0
- package/template/wall-e/runtime/devbox-gateway.js +82 -8
- package/template/wall-e/runtime/prompt-manifest.js +86 -0
- package/template/wall-e/runtime/tool-executor.js +269 -0
- package/template/wall-e/runtime/tool-result-envelope.js +138 -0
- package/template/wall-e/runtime/transcript-projection.js +60 -0
- package/template/wall-e/runtime/walle-runtime.js +224 -0
- package/template/wall-e/scripts/db-optimize/migrate.js +162 -0
- package/template/wall-e/scripts/db-optimize/recall-eval.js +117 -0
- package/template/wall-e/server.js +15 -0
- package/template/wall-e/session-files.js +9 -0
- package/template/wall-e/skills/_bundled/google-calendar/run.js +1 -1
- package/template/wall-e/skills/_bundled/gws-workspace/run.js +1 -1
- package/template/wall-e/skills/_bundled/slack-mentions/run.js +76 -6
- package/template/wall-e/skills/claude-code-reader.js +7 -3
- package/template/wall-e/skills/script-skill-runner.js +10 -0
- package/template/wall-e/skills/skill-planner.js +38 -0
- package/template/wall-e/tools/builtin-middleware.js +19 -9
- package/template/wall-e/tools/local-tools.js +1428 -16
- package/template/wall-e/tools/permission-checker.js +73 -5
- package/template/wall-e/tools/question-manager.js +117 -7
- package/template/wall-e/training/harvester.js +12 -28
- package/template/wall-e/training/replay.js +25 -80
- package/template/website/index.html +10 -10
- package/template/wall-e/eval/ab-test.js +0 -203
- package/template/wall-e/eval/agent-runner.js +0 -772
- package/template/wall-e/eval/agent-scorer.js +0 -461
- package/template/wall-e/eval/aggregator.js +0 -414
- package/template/wall-e/eval/allowed-test-commands.js +0 -34
- package/template/wall-e/eval/benchmark-generator.js +0 -113
- package/template/wall-e/eval/benchmarks/chat-eval.json +0 -1662
- package/template/wall-e/eval/benchmarks/chat.json +0 -82
- package/template/wall-e/eval/benchmarks/coding-agent-real.json +0 -1
- package/template/wall-e/eval/benchmarks/coding-agent.json +0 -1581
- package/template/wall-e/eval/benchmarks/coding.json +0 -122
- package/template/wall-e/eval/benchmarks/memory-retrieval.json +0 -234
- package/template/wall-e/eval/benchmarks/reasoning.json +0 -82
- package/template/wall-e/eval/benchmarks/swebench-lite-30.json +0 -212
- package/template/wall-e/eval/benchmarks.js +0 -669
- package/template/wall-e/eval/cc-replay.js +0 -719
- package/template/wall-e/eval/chat-eval.js +0 -525
- package/template/wall-e/eval/check-keys.js +0 -15
- package/template/wall-e/eval/check-providers.js +0 -42
- package/template/wall-e/eval/codex-cli-baseline.js +0 -669
- package/template/wall-e/eval/coding-agent-real.js +0 -570
- package/template/wall-e/eval/context-compactor.js +0 -251
- package/template/wall-e/eval/debug-agent003.js +0 -68
- package/template/wall-e/eval/diagnostics.js +0 -216
- package/template/wall-e/eval/eval-orchestrator.js +0 -642
- package/template/wall-e/eval/evaluate.js +0 -202
- package/template/wall-e/eval/evaluator.js +0 -373
- package/template/wall-e/eval/exporter.js +0 -212
- package/template/wall-e/eval/fixtures/express-basic/package.json +0 -9
- package/template/wall-e/eval/fixtures/express-basic/server.js +0 -115
- package/template/wall-e/eval/fixtures/express-basic/test.js +0 -83
- package/template/wall-e/eval/fixtures/express-buggy/package.json +0 -9
- package/template/wall-e/eval/fixtures/express-buggy/server.js +0 -113
- package/template/wall-e/eval/fixtures/express-buggy/test.js +0 -83
- package/template/wall-e/eval/fixtures/express-buggy-items/package.json +0 -9
- package/template/wall-e/eval/fixtures/express-buggy-items/server.js +0 -112
- package/template/wall-e/eval/fixtures/express-buggy-items/test.js +0 -83
- package/template/wall-e/eval/fixtures/express-buggy-search/package.json +0 -9
- package/template/wall-e/eval/fixtures/express-buggy-search/server.js +0 -121
- package/template/wall-e/eval/fixtures/express-buggy-search/test.js +0 -83
- package/template/wall-e/eval/fixtures/express-rename-data/data.js +0 -34
- package/template/wall-e/eval/fixtures/express-rename-data/package.json +0 -9
- package/template/wall-e/eval/fixtures/express-rename-data/server.js +0 -97
- package/template/wall-e/eval/fixtures/express-rename-data/test.js +0 -88
- package/template/wall-e/eval/fixtures/express-xss/package.json +0 -12
- package/template/wall-e/eval/fixtures/express-xss/server.js +0 -90
- package/template/wall-e/eval/fixtures/express-xss/test.js +0 -67
- package/template/wall-e/eval/fixtures/express-xss/views/profile.ejs +0 -9
- package/template/wall-e/eval/fixtures/fullstack-app/config/default.js +0 -9
- package/template/wall-e/eval/fixtures/fullstack-app/config/test.js +0 -13
- package/template/wall-e/eval/fixtures/fullstack-app/package.json +0 -11
- package/template/wall-e/eval/fixtures/fullstack-app/public/css/style.css +0 -137
- package/template/wall-e/eval/fixtures/fullstack-app/public/index.html +0 -46
- package/template/wall-e/eval/fixtures/fullstack-app/public/js/app.js +0 -121
- package/template/wall-e/eval/fixtures/fullstack-app/public/js/auth.js +0 -71
- package/template/wall-e/eval/fixtures/fullstack-app/public/js/items.js +0 -80
- package/template/wall-e/eval/fixtures/fullstack-app/public/js/users.js +0 -46
- package/template/wall-e/eval/fixtures/fullstack-app/public/login.html +0 -45
- package/template/wall-e/eval/fixtures/fullstack-app/public/register.html +0 -38
- package/template/wall-e/eval/fixtures/fullstack-app/scripts/migrate.js +0 -23
- package/template/wall-e/eval/fixtures/fullstack-app/scripts/seed.js +0 -46
- package/template/wall-e/eval/fixtures/fullstack-app/server/db.js +0 -99
- package/template/wall-e/eval/fixtures/fullstack-app/server/index.js +0 -94
- package/template/wall-e/eval/fixtures/fullstack-app/server/middleware/auth.js +0 -19
- package/template/wall-e/eval/fixtures/fullstack-app/server/middleware/logger.js +0 -19
- package/template/wall-e/eval/fixtures/fullstack-app/server/router.js +0 -50
- package/template/wall-e/eval/fixtures/fullstack-app/server/routes/auth.js +0 -69
- package/template/wall-e/eval/fixtures/fullstack-app/server/routes/health.js +0 -23
- package/template/wall-e/eval/fixtures/fullstack-app/server/routes/items.js +0 -88
- package/template/wall-e/eval/fixtures/fullstack-app/server/routes/users.js +0 -75
- package/template/wall-e/eval/fixtures/fullstack-app/server/test.js +0 -198
- package/template/wall-e/eval/fixtures/fullstack-app/server/utils/response.js +0 -34
- package/template/wall-e/eval/fixtures/fullstack-app/server/utils/validate.js +0 -26
- package/template/wall-e/eval/fixtures/fullstack-app/server.js +0 -8
- package/template/wall-e/eval/fixtures/fullstack-app/test.js +0 -12
- package/template/wall-e/eval/fixtures/monorepo-basic/package.json +0 -8
- package/template/wall-e/eval/fixtures/monorepo-basic/packages/api/data.js +0 -58
- package/template/wall-e/eval/fixtures/monorepo-basic/packages/api/middleware.js +0 -46
- package/template/wall-e/eval/fixtures/monorepo-basic/packages/api/package.json +0 -8
- package/template/wall-e/eval/fixtures/monorepo-basic/packages/api/routes.js +0 -64
- package/template/wall-e/eval/fixtures/monorepo-basic/packages/api/server.js +0 -56
- package/template/wall-e/eval/fixtures/monorepo-basic/packages/api/test.js +0 -116
- package/template/wall-e/eval/fixtures/monorepo-basic/packages/cli/commands.js +0 -61
- package/template/wall-e/eval/fixtures/monorepo-basic/packages/cli/index.js +0 -62
- package/template/wall-e/eval/fixtures/monorepo-basic/packages/cli/output.js +0 -43
- package/template/wall-e/eval/fixtures/monorepo-basic/packages/cli/package.json +0 -11
- package/template/wall-e/eval/fixtures/monorepo-basic/packages/cli/test.js +0 -44
- package/template/wall-e/eval/fixtures/monorepo-basic/packages/shared/formatters.js +0 -43
- package/template/wall-e/eval/fixtures/monorepo-basic/packages/shared/index.js +0 -12
- package/template/wall-e/eval/fixtures/monorepo-basic/packages/shared/package.json +0 -5
- package/template/wall-e/eval/fixtures/monorepo-basic/packages/shared/test.js +0 -55
- package/template/wall-e/eval/fixtures/monorepo-basic/packages/shared/validators.js +0 -29
- package/template/wall-e/eval/fixtures/monorepo-basic/test.js +0 -46
- package/template/wall-e/eval/fixtures/node-cli/index.js +0 -78
- package/template/wall-e/eval/fixtures/node-cli/package.json +0 -10
- package/template/wall-e/eval/fixtures/node-cli/test.js +0 -57
- package/template/wall-e/eval/fixtures/node-typed/package.json +0 -8
- package/template/wall-e/eval/fixtures/node-typed/src/handlers.js +0 -31
- package/template/wall-e/eval/fixtures/node-typed/src/utils.js +0 -33
- package/template/wall-e/eval/fixtures/node-typed/test.js +0 -36
- package/template/wall-e/eval/fixtures/python-flask/app.py +0 -14
- package/template/wall-e/eval/fixtures/python-flask/requirements.txt +0 -2
- package/template/wall-e/eval/fixtures/python-flask/test_app.py +0 -25
- package/template/wall-e/eval/fixtures/wall-e-subset/brain.js +0 -105
- package/template/wall-e/eval/fixtures/wall-e-subset/eval/aggregator.js +0 -101
- package/template/wall-e/eval/fixtures/wall-e-subset/eval/benchmarks/chat.json +0 -20
- package/template/wall-e/eval/fixtures/wall-e-subset/eval/benchmarks/coding.json +0 -32
- package/template/wall-e/eval/fixtures/wall-e-subset/eval/benchmarks.js +0 -64
- package/template/wall-e/eval/fixtures/wall-e-subset/eval/fixtures/simple-project/package.json +0 -6
- package/template/wall-e/eval/fixtures/wall-e-subset/eval/fixtures/simple-project/server.js +0 -31
- package/template/wall-e/eval/fixtures/wall-e-subset/eval/fixtures/simple-project/test.js +0 -18
- package/template/wall-e/eval/fixtures/wall-e-subset/eval/fixtures/simple-project/utils.js +0 -34
- package/template/wall-e/eval/fixtures/wall-e-subset/eval/runner.js +0 -104
- package/template/wall-e/eval/fixtures/wall-e-subset/eval/scorer.js +0 -73
- package/template/wall-e/eval/fixtures/wall-e-subset/eval/test.js +0 -134
- package/template/wall-e/eval/fixtures/wall-e-subset/llm/client.js +0 -99
- package/template/wall-e/eval/fixtures/wall-e-subset/llm/providers.js +0 -63
- package/template/wall-e/eval/fixtures/wall-e-subset/llm/test.js +0 -70
- package/template/wall-e/eval/fixtures/wall-e-subset/package.json +0 -10
- package/template/wall-e/eval/fixtures/wall-e-subset/test.js +0 -86
- package/template/wall-e/eval/harvester.js +0 -685
- package/template/wall-e/eval/head-to-head.js +0 -388
- package/template/wall-e/eval/humaneval-adapter.js +0 -321
- package/template/wall-e/eval/list-models.js +0 -31
- package/template/wall-e/eval/livecodebench-adapter.js +0 -291
- package/template/wall-e/eval/mail-integration.js +0 -443
- package/template/wall-e/eval/manifest.js +0 -186
- package/template/wall-e/eval/meta-harness/adapters/coding-agent.js +0 -57
- package/template/wall-e/eval/meta-harness/bootstrap-snapshot.js +0 -149
- package/template/wall-e/eval/meta-harness/candidate-store.js +0 -117
- package/template/wall-e/eval/meta-harness/cli.js +0 -86
- package/template/wall-e/eval/meta-harness/domain-spec.js +0 -154
- package/template/wall-e/eval/meta-harness/domains/coding-agent.domain.json +0 -84
- package/template/wall-e/eval/meta-harness/examples/env-bootstrap-candidate.js +0 -29
- package/template/wall-e/eval/meta-harness/experience-store.js +0 -174
- package/template/wall-e/eval/meta-harness/frontier.js +0 -96
- package/template/wall-e/eval/meta-harness/harness-interface.js +0 -90
- package/template/wall-e/eval/meta-harness/leakage-guard.js +0 -80
- package/template/wall-e/eval/meta-harness/optimizer.js +0 -207
- package/template/wall-e/eval/meta-harness/proposer-runner.js +0 -110
- package/template/wall-e/eval/meta-harness/reporting.js +0 -58
- package/template/wall-e/eval/meta-harness/telemetry.js +0 -27
- package/template/wall-e/eval/meta-harness/validation.js +0 -81
- package/template/wall-e/eval/promoter.js +0 -228
- package/template/wall-e/eval/provider-normalizer.js +0 -33
- package/template/wall-e/eval/replay.js +0 -395
- package/template/wall-e/eval/run-agent-benchmarks.js +0 -386
- package/template/wall-e/eval/run-codex-cli-baseline.js +0 -177
- package/template/wall-e/eval/run-coding-agent-real.js +0 -187
- package/template/wall-e/eval/run-eval.js +0 -435
- package/template/wall-e/eval/run-model-comparison.js +0 -142
- package/template/wall-e/eval/session-evaluator.js +0 -187
- package/template/wall-e/eval/session-miner.js +0 -207
- package/template/wall-e/eval/session-retrieval-benchmark.js +0 -150
- package/template/wall-e/eval/session-transcripts.js +0 -509
- package/template/wall-e/eval/shadow.js +0 -161
- package/template/wall-e/eval/swebench-adapter.js +0 -345
- package/template/wall-e/eval/swebench-docker.js +0 -192
- package/template/wall-e/eval/train.py +0 -320
- package/template/wall-e/eval/trainer.js +0 -232
- package/template/wall-e/eval/weekly-eval-loop.js +0 -241
|
@@ -5,14 +5,46 @@ const { ALL_SCOPES } = require('./auth-rules');
|
|
|
5
5
|
|
|
6
6
|
const DEFAULT_CLAIM_TTL_MS = 10 * 60 * 1000;
|
|
7
7
|
const DEFAULT_CLAIM_RETENTION_MS = 24 * 60 * 60 * 1000;
|
|
8
|
-
const
|
|
8
|
+
const DEFAULT_PAIRING_REQUEST_TTL_MS = 10 * 60 * 1000;
|
|
9
|
+
const DEFAULT_PAIRING_REQUEST_RETENTION_MS = 24 * 60 * 60 * 1000;
|
|
10
|
+
// Device tokens expire after this much INACTIVITY (a sliding window refreshed on
|
|
11
|
+
// each use) and are hard-capped at DEVICE_TOKEN_ABSOLUTE_MAX_MS from issuance.
|
|
12
|
+
// A copied-but-unused token dies within the idle window; an actively used token
|
|
13
|
+
// is still bounded by the absolute cap, after which the device must re-pair.
|
|
14
|
+
// (Previously a flat 365 days from issuance with no renewal, so a leaked
|
|
15
|
+
// read-scope cookie was good for a year.)
|
|
16
|
+
const DEFAULT_DEVICE_TOKEN_TTL_MS = 30 * 24 * 60 * 60 * 1000;
|
|
17
|
+
const DEVICE_TOKEN_ABSOLUTE_MAX_MS = 180 * 24 * 60 * 60 * 1000;
|
|
18
|
+
// Warn the phone this far before the absolute cap so re-pairing is a planned
|
|
19
|
+
// one-tap, not a surprise lockout.
|
|
20
|
+
const DEVICE_TOKEN_REPAIR_SOON_MS = 7 * 24 * 60 * 60 * 1000;
|
|
9
21
|
const DEFAULT_STEP_UP_TTL_MS = 10 * 60 * 1000;
|
|
22
|
+
const DEFAULT_WEBAUTHN_CHALLENGE_RETENTION_MS = 24 * 60 * 60 * 1000;
|
|
10
23
|
const RECENT_DEVICE_USE_MS = 2 * 60 * 1000;
|
|
24
|
+
// Sliding-renewal writes are throttled to at most once per window per token. The TTL is
|
|
25
|
+
// 30 days, so losing a minute of sliding precision is irrelevant — but skipping the write
|
|
26
|
+
// on the hot auth path keeps token validation a pure read and off the write lock.
|
|
27
|
+
const RENEWAL_WRITE_THROTTLE_MS = 60 * 1000;
|
|
11
28
|
|
|
12
29
|
function nowMs() {
|
|
13
30
|
return Date.now();
|
|
14
31
|
}
|
|
15
32
|
|
|
33
|
+
function _envDaysMs(name, fallbackMs) {
|
|
34
|
+
const days = Number(process.env[name]);
|
|
35
|
+
return Number.isFinite(days) && days > 0 ? days * 24 * 60 * 60 * 1000 : fallbackMs;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Both token windows are env-configurable (in DAYS). The absolute cap can never
|
|
39
|
+
// be shorter than the idle window.
|
|
40
|
+
function deviceTokenTtlMs() {
|
|
41
|
+
return _envDaysMs('CTM_DEVICE_TOKEN_TTL_DAYS', DEFAULT_DEVICE_TOKEN_TTL_MS);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function deviceTokenAbsoluteMaxMs() {
|
|
45
|
+
return Math.max(deviceTokenTtlMs(), _envDaysMs('CTM_DEVICE_TOKEN_MAX_DAYS', DEVICE_TOKEN_ABSOLUTE_MAX_MS));
|
|
46
|
+
}
|
|
47
|
+
|
|
16
48
|
function randomId(prefix, bytes = 16) {
|
|
17
49
|
return `${prefix}_${crypto.randomBytes(bytes).toString('base64url')}`;
|
|
18
50
|
}
|
|
@@ -80,6 +112,16 @@ function deviceProfileKey(device) {
|
|
|
80
112
|
return `${label}|${hint}`;
|
|
81
113
|
}
|
|
82
114
|
|
|
115
|
+
function deviceProfileMatches(device, label, userAgent) {
|
|
116
|
+
const deviceLabel = normalizeDeviceLabel(device?.label || '').toLowerCase();
|
|
117
|
+
const targetLabel = normalizeDeviceLabel(label || '').toLowerCase();
|
|
118
|
+
if (!deviceLabel || deviceLabel !== targetLabel) return false;
|
|
119
|
+
const deviceHint = deviceHintFromUserAgent(device?.last_used_ua || '').toLowerCase();
|
|
120
|
+
const targetHint = deviceHintFromUserAgent(userAgent || '').toLowerCase();
|
|
121
|
+
if (!deviceHint || !targetHint) return true;
|
|
122
|
+
return deviceHint === targetHint;
|
|
123
|
+
}
|
|
124
|
+
|
|
83
125
|
function authorizationStatus(device, atMs = nowMs()) {
|
|
84
126
|
if (device.revoked_at) return 'revoked';
|
|
85
127
|
if (device.expires_at && device.expires_at <= atMs) return 'expired';
|
|
@@ -94,7 +136,15 @@ function connectionStatus(device, connectedSet, atMs = nowMs()) {
|
|
|
94
136
|
return 'offline';
|
|
95
137
|
}
|
|
96
138
|
|
|
139
|
+
// Per-db-handle guard: the schema DDL (all CREATE … IF NOT EXISTS, idempotent) only needs
|
|
140
|
+
// to run once per process per connection. Re-running it on every call is what put every
|
|
141
|
+
// device-token validation through `db.exec` → the write lock, so a busy lock fail-fast threw
|
|
142
|
+
// out of the read path as `device_token_lookup_failed`. Keyed on the handle so a reinit
|
|
143
|
+
// (fresh db object) re-ensures automatically.
|
|
144
|
+
const _schemaEnsured = new WeakSet();
|
|
145
|
+
|
|
97
146
|
function ensureMobileAuthSchema(db) {
|
|
147
|
+
if (_schemaEnsured.has(db)) return;
|
|
98
148
|
db.exec(`
|
|
99
149
|
CREATE TABLE IF NOT EXISTS ctm_device_tokens (
|
|
100
150
|
id TEXT PRIMARY KEY,
|
|
@@ -125,6 +175,24 @@ function ensureMobileAuthSchema(db) {
|
|
|
125
175
|
created_by TEXT NOT NULL DEFAULT 'loopback'
|
|
126
176
|
);
|
|
127
177
|
|
|
178
|
+
CREATE TABLE IF NOT EXISTS ctm_pairing_requests (
|
|
179
|
+
id TEXT PRIMARY KEY,
|
|
180
|
+
request_secret_hash TEXT NOT NULL UNIQUE,
|
|
181
|
+
code TEXT NOT NULL,
|
|
182
|
+
label TEXT NOT NULL,
|
|
183
|
+
scopes TEXT NOT NULL,
|
|
184
|
+
origin TEXT NOT NULL,
|
|
185
|
+
remote_ip TEXT,
|
|
186
|
+
user_agent TEXT,
|
|
187
|
+
created_at INTEGER NOT NULL,
|
|
188
|
+
expires_at INTEGER NOT NULL,
|
|
189
|
+
approved_at INTEGER,
|
|
190
|
+
rejected_at INTEGER,
|
|
191
|
+
claim_id TEXT REFERENCES ctm_device_claims(id),
|
|
192
|
+
created_by TEXT NOT NULL DEFAULT 'phone',
|
|
193
|
+
decision_by TEXT
|
|
194
|
+
);
|
|
195
|
+
|
|
128
196
|
CREATE TABLE IF NOT EXISTS ctm_webauthn_credentials (
|
|
129
197
|
id TEXT PRIMARY KEY,
|
|
130
198
|
device_token_id TEXT NOT NULL REFERENCES ctm_device_tokens(id) ON DELETE CASCADE,
|
|
@@ -176,6 +244,8 @@ function ensureMobileAuthSchema(db) {
|
|
|
176
244
|
|
|
177
245
|
CREATE INDEX IF NOT EXISTS idx_ctm_device_tokens_hash ON ctm_device_tokens(token_hash);
|
|
178
246
|
CREATE INDEX IF NOT EXISTS idx_ctm_device_claims_secret ON ctm_device_claims(secret_hash);
|
|
247
|
+
CREATE INDEX IF NOT EXISTS idx_ctm_pairing_requests_created ON ctm_pairing_requests(created_at DESC);
|
|
248
|
+
CREATE INDEX IF NOT EXISTS idx_ctm_pairing_requests_pending ON ctm_pairing_requests(expires_at, approved_at, rejected_at);
|
|
179
249
|
CREATE INDEX IF NOT EXISTS idx_ctm_webauthn_device ON ctm_webauthn_credentials(device_token_id);
|
|
180
250
|
CREATE INDEX IF NOT EXISTS idx_ctm_webauthn_challenge_lookup ON ctm_webauthn_challenges(kind, device_token_id, claim_id);
|
|
181
251
|
CREATE INDEX IF NOT EXISTS idx_ctm_step_up_device ON ctm_step_up_sessions(device_token_id);
|
|
@@ -186,6 +256,9 @@ function ensureMobileAuthSchema(db) {
|
|
|
186
256
|
if (!claimColumns.has('origin')) {
|
|
187
257
|
db.prepare("ALTER TABLE ctm_device_claims ADD COLUMN origin TEXT NOT NULL DEFAULT ''").run();
|
|
188
258
|
}
|
|
259
|
+
// Mark ensured only after the DDL + migration succeed — if a busy write lock throws above,
|
|
260
|
+
// we leave the handle unmarked so the next call retries instead of skipping a real migration.
|
|
261
|
+
_schemaEnsured.add(db);
|
|
189
262
|
}
|
|
190
263
|
|
|
191
264
|
function deviceFromRow(row) {
|
|
@@ -221,6 +294,36 @@ function claimFromRow(row) {
|
|
|
221
294
|
};
|
|
222
295
|
}
|
|
223
296
|
|
|
297
|
+
function pairingRequestStatus(row, atMs = nowMs()) {
|
|
298
|
+
if (!row) return 'not_found';
|
|
299
|
+
if (row.rejected_at) return 'rejected';
|
|
300
|
+
if (row.expires_at <= atMs) return 'expired';
|
|
301
|
+
if (row.approved_at) return 'approved';
|
|
302
|
+
return 'pending';
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function pairingRequestFromRow(row, atMs = nowMs()) {
|
|
306
|
+
if (!row) return null;
|
|
307
|
+
return {
|
|
308
|
+
id: row.id,
|
|
309
|
+
code: row.code,
|
|
310
|
+
label: row.label,
|
|
311
|
+
scopes: parseScopes(row.scopes),
|
|
312
|
+
origin: row.origin,
|
|
313
|
+
remote_ip: row.remote_ip || '',
|
|
314
|
+
user_agent: row.user_agent || '',
|
|
315
|
+
device_hint: deviceHintFromUserAgent(row.user_agent || ''),
|
|
316
|
+
created_at: row.created_at,
|
|
317
|
+
expires_at: row.expires_at,
|
|
318
|
+
approved_at: row.approved_at || null,
|
|
319
|
+
rejected_at: row.rejected_at || null,
|
|
320
|
+
claim_id: row.claim_id || null,
|
|
321
|
+
created_by: row.created_by || 'phone',
|
|
322
|
+
decision_by: row.decision_by || '',
|
|
323
|
+
status: pairingRequestStatus(row, atMs),
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
|
|
224
327
|
function credentialFromRow(row) {
|
|
225
328
|
if (!row) return null;
|
|
226
329
|
return {
|
|
@@ -242,7 +345,7 @@ function createDeviceClaim(db, options = {}) {
|
|
|
242
345
|
cleanupExpiredDeviceClaims(db);
|
|
243
346
|
const origin = new URL(options.origin || 'http://localhost:3456').origin;
|
|
244
347
|
const id = randomId('claim');
|
|
245
|
-
const secret = randomId('claim_secret', 24);
|
|
348
|
+
const secret = options.secret ? String(options.secret) : randomId('claim_secret', 24);
|
|
246
349
|
const createdAt = nowMs();
|
|
247
350
|
const expiresAt = createdAt + Math.max(1000, Number(options.ttlMs || DEFAULT_CLAIM_TTL_MS));
|
|
248
351
|
const scopes = normalizeScopes(options.scopes);
|
|
@@ -259,6 +362,206 @@ function createDeviceClaim(db, options = {}) {
|
|
|
259
362
|
return { id, secret, claimUrl: claimUrl.toString(), label, scopes, origin, expiresAt };
|
|
260
363
|
}
|
|
261
364
|
|
|
365
|
+
function cleanupExpiredPairingRequests(db, options = {}) {
|
|
366
|
+
ensureMobileAuthSchema(db);
|
|
367
|
+
const atMs = Number(options.nowMs || nowMs());
|
|
368
|
+
const retentionMs = Math.max(0, Number(options.retentionMs ?? DEFAULT_PAIRING_REQUEST_RETENTION_MS));
|
|
369
|
+
const cutoff = atMs - retentionMs;
|
|
370
|
+
const limit = Math.max(1, Math.min(1000, Number(options.limit || 250)));
|
|
371
|
+
const rows = db.prepare(`
|
|
372
|
+
SELECT id FROM ctm_pairing_requests
|
|
373
|
+
WHERE (
|
|
374
|
+
claim_id IS NULL
|
|
375
|
+
OR approved_at IS NOT NULL
|
|
376
|
+
OR rejected_at IS NOT NULL
|
|
377
|
+
OR expires_at <= ?
|
|
378
|
+
)
|
|
379
|
+
AND (
|
|
380
|
+
(expires_at > 0 AND expires_at <= ?)
|
|
381
|
+
OR (approved_at IS NOT NULL AND approved_at > 0 AND approved_at <= ?)
|
|
382
|
+
OR (rejected_at IS NOT NULL AND rejected_at > 0 AND rejected_at <= ?)
|
|
383
|
+
)
|
|
384
|
+
ORDER BY expires_at ASC
|
|
385
|
+
LIMIT ?
|
|
386
|
+
`).all(atMs, cutoff, cutoff, cutoff, limit);
|
|
387
|
+
if (!rows.length) return { deleted: 0 };
|
|
388
|
+
const del = db.prepare('DELETE FROM ctm_pairing_requests WHERE id = ?');
|
|
389
|
+
const txn = db.transaction((items) => {
|
|
390
|
+
for (const row of items) del.run(row.id);
|
|
391
|
+
});
|
|
392
|
+
txn(rows);
|
|
393
|
+
return { deleted: rows.length };
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function createPairingRequest(db, options = {}) {
|
|
397
|
+
ensureMobileAuthSchema(db);
|
|
398
|
+
cleanupExpiredPairingRequests(db);
|
|
399
|
+
const origin = new URL(options.origin || 'http://localhost:3456').origin;
|
|
400
|
+
const remoteIp = String(options.remoteIp || '');
|
|
401
|
+
const userAgent = String(options.userAgent || '');
|
|
402
|
+
const at = nowMs();
|
|
403
|
+
const activeWindowMs = Math.max(1000, Number(options.activeWindowMs || DEFAULT_PAIRING_REQUEST_TTL_MS));
|
|
404
|
+
const maxActive = Math.max(1, Math.min(25, Number(options.maxActive || 5)));
|
|
405
|
+
const recentCount = db.prepare(`
|
|
406
|
+
SELECT COUNT(*) AS count FROM ctm_pairing_requests
|
|
407
|
+
WHERE created_at >= ?
|
|
408
|
+
AND expires_at > ?
|
|
409
|
+
AND approved_at IS NULL
|
|
410
|
+
AND rejected_at IS NULL
|
|
411
|
+
AND (
|
|
412
|
+
(? <> '' AND remote_ip = ?)
|
|
413
|
+
OR origin = ?
|
|
414
|
+
)
|
|
415
|
+
`).get(at - activeWindowMs, at, remoteIp, remoteIp, origin)?.count || 0;
|
|
416
|
+
if (recentCount >= maxActive) throw new Error('pairing_request_rate_limited');
|
|
417
|
+
|
|
418
|
+
const id = randomId('pair');
|
|
419
|
+
const secret = randomId('pair_secret', 24);
|
|
420
|
+
const code = String(crypto.randomInt(100000, 1000000));
|
|
421
|
+
const hint = deviceHintFromUserAgent(userAgent);
|
|
422
|
+
const label = normalizeDeviceLabel(options.label || hint || 'Phone');
|
|
423
|
+
const scopes = normalizeScopes(options.scopes || ['read', 'respond']);
|
|
424
|
+
const expiresAt = at + Math.max(1000, Number(options.ttlMs || DEFAULT_PAIRING_REQUEST_TTL_MS));
|
|
425
|
+
db.prepare(`
|
|
426
|
+
INSERT INTO ctm_pairing_requests
|
|
427
|
+
(id, request_secret_hash, code, label, scopes, origin, remote_ip, user_agent, created_at, expires_at, created_by)
|
|
428
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
429
|
+
`).run(id, hashValue(secret), code, label, JSON.stringify(scopes), origin, remoteIp, userAgent, at, expiresAt, options.createdBy || 'phone');
|
|
430
|
+
return { request: pairingRequestFromRow({
|
|
431
|
+
id,
|
|
432
|
+
request_secret_hash: hashValue(secret),
|
|
433
|
+
code,
|
|
434
|
+
label,
|
|
435
|
+
scopes: JSON.stringify(scopes),
|
|
436
|
+
origin,
|
|
437
|
+
remote_ip: remoteIp,
|
|
438
|
+
user_agent: userAgent,
|
|
439
|
+
created_at: at,
|
|
440
|
+
expires_at: expiresAt,
|
|
441
|
+
created_by: options.createdBy || 'phone',
|
|
442
|
+
}, at), secret };
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
function listPairingRequests(db, options = {}) {
|
|
446
|
+
ensureMobileAuthSchema(db);
|
|
447
|
+
cleanupExpiredPairingRequests(db);
|
|
448
|
+
const at = Number(options.nowMs || nowMs());
|
|
449
|
+
const limit = Math.max(1, Math.min(100, Number(options.limit || 20)));
|
|
450
|
+
const includeResolved = !!options.includeResolved;
|
|
451
|
+
const rows = includeResolved
|
|
452
|
+
? db.prepare('SELECT * FROM ctm_pairing_requests ORDER BY created_at DESC LIMIT ?').all(limit)
|
|
453
|
+
: db.prepare(`
|
|
454
|
+
SELECT * FROM ctm_pairing_requests
|
|
455
|
+
WHERE expires_at > ?
|
|
456
|
+
AND approved_at IS NULL
|
|
457
|
+
AND rejected_at IS NULL
|
|
458
|
+
ORDER BY created_at DESC
|
|
459
|
+
LIMIT ?
|
|
460
|
+
`).all(at, limit);
|
|
461
|
+
return rows.map((row) => pairingRequestFromRow(row, at));
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function verifyPairingRequestSecret(db, requestId, secret, atMs = nowMs()) {
|
|
465
|
+
ensureMobileAuthSchema(db);
|
|
466
|
+
const row = db.prepare('SELECT * FROM ctm_pairing_requests WHERE id = ?').get(String(requestId || ''));
|
|
467
|
+
if (!row || row.request_secret_hash !== hashValue(secret)) throw new Error('pairing_request_not_found');
|
|
468
|
+
return pairingRequestFromRow(row, atMs);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function approvePairingRequest(db, requestId, options = {}) {
|
|
472
|
+
ensureMobileAuthSchema(db);
|
|
473
|
+
const id = String(requestId || '');
|
|
474
|
+
const at = nowMs();
|
|
475
|
+
const row = db.prepare('SELECT * FROM ctm_pairing_requests WHERE id = ?').get(id);
|
|
476
|
+
if (!row) throw new Error('pairing_request_not_found');
|
|
477
|
+
const status = pairingRequestStatus(row, at);
|
|
478
|
+
if (status === 'expired') throw new Error('pairing_request_expired');
|
|
479
|
+
if (status === 'rejected') throw new Error('pairing_request_rejected');
|
|
480
|
+
const nextLabel = Object.prototype.hasOwnProperty.call(options, 'label')
|
|
481
|
+
? normalizeDeviceLabel(options.label)
|
|
482
|
+
: row.label;
|
|
483
|
+
const nextScopes = Object.prototype.hasOwnProperty.call(options, 'scopes')
|
|
484
|
+
? normalizeScopes(options.scopes)
|
|
485
|
+
: parseScopes(row.scopes);
|
|
486
|
+
const nextExpiresAt = Math.max(Number(row.expires_at || 0), at + DEFAULT_CLAIM_TTL_MS);
|
|
487
|
+
db.prepare(`
|
|
488
|
+
UPDATE ctm_pairing_requests
|
|
489
|
+
SET approved_at = COALESCE(approved_at, ?),
|
|
490
|
+
rejected_at = NULL,
|
|
491
|
+
expires_at = ?,
|
|
492
|
+
label = ?,
|
|
493
|
+
scopes = ?,
|
|
494
|
+
decision_by = ?
|
|
495
|
+
WHERE id = ?
|
|
496
|
+
`).run(at, nextExpiresAt, nextLabel, JSON.stringify(nextScopes), options.decisionBy || 'loopback', id);
|
|
497
|
+
return pairingRequestFromRow({
|
|
498
|
+
...row,
|
|
499
|
+
approved_at: row.approved_at || at,
|
|
500
|
+
rejected_at: null,
|
|
501
|
+
expires_at: nextExpiresAt,
|
|
502
|
+
label: nextLabel,
|
|
503
|
+
scopes: JSON.stringify(nextScopes),
|
|
504
|
+
decision_by: options.decisionBy || 'loopback',
|
|
505
|
+
}, at);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
function rejectPairingRequest(db, requestId, options = {}) {
|
|
509
|
+
ensureMobileAuthSchema(db);
|
|
510
|
+
const id = String(requestId || '');
|
|
511
|
+
const at = nowMs();
|
|
512
|
+
const row = db.prepare('SELECT * FROM ctm_pairing_requests WHERE id = ?').get(id);
|
|
513
|
+
if (!row) throw new Error('pairing_request_not_found');
|
|
514
|
+
db.prepare(`
|
|
515
|
+
UPDATE ctm_pairing_requests
|
|
516
|
+
SET rejected_at = COALESCE(rejected_at, ?),
|
|
517
|
+
decision_by = ?
|
|
518
|
+
WHERE id = ? AND approved_at IS NULL
|
|
519
|
+
`).run(at, options.decisionBy || 'loopback', id);
|
|
520
|
+
return pairingRequestFromRow({ ...row, rejected_at: row.rejected_at || at, decision_by: options.decisionBy || 'loopback' }, at);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
function pairingClaimSecret(requestSecret, request) {
|
|
524
|
+
return crypto.createHmac('sha256', String(requestSecret || ''))
|
|
525
|
+
.update(`${request.id}:${request.approved_at || 0}:ctm-phone-claim`)
|
|
526
|
+
.digest('base64url')
|
|
527
|
+
.slice(0, 40);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
function ensurePairingRequestClaim(db, requestId, requestSecret, options = {}) {
|
|
531
|
+
ensureMobileAuthSchema(db);
|
|
532
|
+
const request = verifyPairingRequestSecret(db, requestId, requestSecret);
|
|
533
|
+
if (request.status !== 'approved') return { status: request.status, request };
|
|
534
|
+
const secret = pairingClaimSecret(requestSecret, request);
|
|
535
|
+
let claimId = request.claim_id;
|
|
536
|
+
if (!claimId) {
|
|
537
|
+
const claim = createDeviceClaim(db, {
|
|
538
|
+
label: request.label,
|
|
539
|
+
scopes: request.scopes,
|
|
540
|
+
origin: request.origin,
|
|
541
|
+
secret,
|
|
542
|
+
ttlMs: options.claimTtlMs || DEFAULT_CLAIM_TTL_MS,
|
|
543
|
+
createdBy: `pairing_request:${request.id}`,
|
|
544
|
+
});
|
|
545
|
+
claimId = claim.id;
|
|
546
|
+
db.prepare('UPDATE ctm_pairing_requests SET claim_id = COALESCE(claim_id, ?) WHERE id = ?')
|
|
547
|
+
.run(claimId, request.id);
|
|
548
|
+
}
|
|
549
|
+
const claimUrl = new URL('/m/claim', request.origin);
|
|
550
|
+
claimUrl.searchParams.set('claim', claimId);
|
|
551
|
+
claimUrl.searchParams.set('secret', secret);
|
|
552
|
+
return {
|
|
553
|
+
status: 'approved',
|
|
554
|
+
request: { ...request, claim_id: claimId },
|
|
555
|
+
claim: {
|
|
556
|
+
id: claimId,
|
|
557
|
+
claimUrl: claimUrl.toString(),
|
|
558
|
+
label: request.label,
|
|
559
|
+
scopes: request.scopes,
|
|
560
|
+
origin: request.origin,
|
|
561
|
+
},
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
|
|
262
565
|
function verifyDeviceClaimSecret(db, claimId, secret, atMs = nowMs()) {
|
|
263
566
|
ensureMobileAuthSchema(db);
|
|
264
567
|
const row = db.prepare('SELECT * FROM ctm_device_claims WHERE id = ?').get(String(claimId || ''));
|
|
@@ -340,7 +643,7 @@ function finishDeviceClaim(db, options = {}) {
|
|
|
340
643
|
issuedAt,
|
|
341
644
|
remoteIp,
|
|
342
645
|
userAgent,
|
|
343
|
-
issuedAt +
|
|
646
|
+
issuedAt + deviceTokenTtlMs(),
|
|
344
647
|
);
|
|
345
648
|
db.prepare(`
|
|
346
649
|
INSERT INTO ctm_webauthn_credentials
|
|
@@ -376,6 +679,164 @@ function finishDeviceClaim(db, options = {}) {
|
|
|
376
679
|
};
|
|
377
680
|
}
|
|
378
681
|
|
|
682
|
+
function listClaimRecoveryCredentials(db, options = {}) {
|
|
683
|
+
ensureMobileAuthSchema(db);
|
|
684
|
+
const atMs = Number(options.nowMs || nowMs());
|
|
685
|
+
const label = normalizeDeviceLabel(options.label || '');
|
|
686
|
+
const userAgent = String(options.userAgent || '');
|
|
687
|
+
const rpId = String(options.rpId || '').trim();
|
|
688
|
+
if (!label || !rpId) return [];
|
|
689
|
+
const rows = db.prepare(`
|
|
690
|
+
SELECT
|
|
691
|
+
d.id AS device_id,
|
|
692
|
+
d.label,
|
|
693
|
+
d.scopes,
|
|
694
|
+
d.webauthn_required,
|
|
695
|
+
d.created_at AS device_created_at,
|
|
696
|
+
d.claimed_at,
|
|
697
|
+
d.passkey_bound_at,
|
|
698
|
+
d.last_used_at AS device_last_used_at,
|
|
699
|
+
d.last_used_ip,
|
|
700
|
+
d.last_used_ua,
|
|
701
|
+
d.revoked_at,
|
|
702
|
+
d.expires_at,
|
|
703
|
+
c.id AS credential_row_id,
|
|
704
|
+
c.rp_id,
|
|
705
|
+
c.origin,
|
|
706
|
+
c.credential_id,
|
|
707
|
+
c.public_key,
|
|
708
|
+
c.counter,
|
|
709
|
+
c.transports,
|
|
710
|
+
c.created_at AS credential_created_at,
|
|
711
|
+
c.last_used_at AS credential_last_used_at
|
|
712
|
+
FROM ctm_device_tokens d
|
|
713
|
+
JOIN ctm_webauthn_credentials c ON c.device_token_id = d.id
|
|
714
|
+
WHERE c.rp_id = ?
|
|
715
|
+
AND d.revoked_at IS NULL
|
|
716
|
+
AND (d.expires_at IS NULL OR d.expires_at > ?)
|
|
717
|
+
ORDER BY COALESCE(d.last_used_at, d.created_at) DESC, c.created_at DESC
|
|
718
|
+
`).all(rpId, atMs);
|
|
719
|
+
const out = [];
|
|
720
|
+
for (const row of rows) {
|
|
721
|
+
const device = deviceFromRow({
|
|
722
|
+
id: row.device_id,
|
|
723
|
+
label: row.label,
|
|
724
|
+
scopes: row.scopes,
|
|
725
|
+
webauthn_required: row.webauthn_required,
|
|
726
|
+
created_at: row.device_created_at,
|
|
727
|
+
claimed_at: row.claimed_at,
|
|
728
|
+
passkey_bound_at: row.passkey_bound_at,
|
|
729
|
+
last_used_at: row.device_last_used_at,
|
|
730
|
+
last_used_ip: row.last_used_ip,
|
|
731
|
+
last_used_ua: row.last_used_ua,
|
|
732
|
+
revoked_at: row.revoked_at,
|
|
733
|
+
expires_at: row.expires_at,
|
|
734
|
+
});
|
|
735
|
+
if (!deviceProfileMatches(device, label, userAgent)) continue;
|
|
736
|
+
out.push({
|
|
737
|
+
device,
|
|
738
|
+
credential: credentialFromRow({
|
|
739
|
+
id: row.credential_row_id,
|
|
740
|
+
device_token_id: row.device_id,
|
|
741
|
+
rp_id: row.rp_id,
|
|
742
|
+
origin: row.origin,
|
|
743
|
+
credential_id: row.credential_id,
|
|
744
|
+
public_key: row.public_key,
|
|
745
|
+
counter: row.counter,
|
|
746
|
+
transports: row.transports,
|
|
747
|
+
created_at: row.credential_created_at,
|
|
748
|
+
last_used_at: row.credential_last_used_at,
|
|
749
|
+
}),
|
|
750
|
+
});
|
|
751
|
+
}
|
|
752
|
+
return out;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
function listActiveDeviceProfiles(db, options = {}) {
|
|
756
|
+
ensureMobileAuthSchema(db);
|
|
757
|
+
const atMs = Number(options.nowMs || nowMs());
|
|
758
|
+
const label = normalizeDeviceLabel(options.label || '');
|
|
759
|
+
const userAgent = String(options.userAgent || '');
|
|
760
|
+
const rows = db.prepare(`
|
|
761
|
+
SELECT * FROM ctm_device_tokens
|
|
762
|
+
WHERE revoked_at IS NULL
|
|
763
|
+
AND (expires_at IS NULL OR expires_at > ?)
|
|
764
|
+
ORDER BY COALESCE(last_used_at, created_at) DESC
|
|
765
|
+
`).all(atMs).map(deviceFromRow);
|
|
766
|
+
return rows.filter((device) => deviceProfileMatches(device, label, userAgent));
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
function getClaimRecoveryCredential(db, options = {}) {
|
|
770
|
+
const credentialId = String(options.credentialId || '');
|
|
771
|
+
if (!credentialId) return null;
|
|
772
|
+
return listClaimRecoveryCredentials(db, options)
|
|
773
|
+
.find((entry) => entry.credential.credential_id === credentialId) || null;
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
function recoverDeviceClaim(db, options = {}) {
|
|
777
|
+
ensureMobileAuthSchema(db);
|
|
778
|
+
const claimId = String(options.claimId || '');
|
|
779
|
+
const secret = String(options.secret || '');
|
|
780
|
+
const deviceId = String(options.deviceId || '');
|
|
781
|
+
const credentialId = String(options.credentialId || '');
|
|
782
|
+
const recoveredAt = nowMs();
|
|
783
|
+
const claim = verifyDeviceClaimSecret(db, claimId, secret, recoveredAt);
|
|
784
|
+
const device = getDeviceToken(db, deviceId);
|
|
785
|
+
if (!device) throw new Error('device_not_found');
|
|
786
|
+
if (device.revoked_at) throw new Error('token_revoked');
|
|
787
|
+
if (device.expires_at && device.expires_at <= recoveredAt) throw new Error('token_expired');
|
|
788
|
+
if (!deviceProfileMatches(device, claim.label, options.userAgent || device.last_used_ua || '')) {
|
|
789
|
+
throw new Error('device_profile_mismatch');
|
|
790
|
+
}
|
|
791
|
+
const credential = getCredentialForDevice(db, deviceId, credentialId);
|
|
792
|
+
if (!credential) throw new Error('credential_not_found');
|
|
793
|
+
const token = randomId('ctm_device_token', 32);
|
|
794
|
+
const remoteIp = options.remoteIp || '';
|
|
795
|
+
const userAgent = options.userAgent || device.last_used_ua || '';
|
|
796
|
+
const tx = db.transaction(() => {
|
|
797
|
+
const fresh = db.prepare('SELECT claimed_at FROM ctm_device_claims WHERE id = ?').get(claimId);
|
|
798
|
+
if (!fresh || fresh.claimed_at) throw new Error('claim_already_used');
|
|
799
|
+
db.prepare(`
|
|
800
|
+
UPDATE ctm_device_tokens
|
|
801
|
+
SET label = ?,
|
|
802
|
+
scopes = ?,
|
|
803
|
+
token_hash = ?,
|
|
804
|
+
claimed_at = COALESCE(claimed_at, ?),
|
|
805
|
+
passkey_bound_at = COALESCE(passkey_bound_at, ?),
|
|
806
|
+
last_used_at = ?,
|
|
807
|
+
last_used_ip = ?,
|
|
808
|
+
last_used_ua = ?,
|
|
809
|
+
expires_at = ?
|
|
810
|
+
WHERE id = ?
|
|
811
|
+
`).run(
|
|
812
|
+
claim.label,
|
|
813
|
+
JSON.stringify(claim.scopes),
|
|
814
|
+
hashValue(token),
|
|
815
|
+
recoveredAt,
|
|
816
|
+
recoveredAt,
|
|
817
|
+
recoveredAt,
|
|
818
|
+
remoteIp,
|
|
819
|
+
userAgent,
|
|
820
|
+
recoveredAt + deviceTokenTtlMs(),
|
|
821
|
+
deviceId,
|
|
822
|
+
);
|
|
823
|
+
db.prepare('DELETE FROM ctm_step_up_sessions WHERE device_token_id = ?').run(deviceId);
|
|
824
|
+
db.prepare(`
|
|
825
|
+
UPDATE ctm_device_claims SET claimed_at = ?, device_token_id = ? WHERE id = ?
|
|
826
|
+
`).run(recoveredAt, deviceId, claimId);
|
|
827
|
+
insertAudit(db, {
|
|
828
|
+
deviceTokenId: deviceId,
|
|
829
|
+
action: 'claim_recover',
|
|
830
|
+
decision: 'allow',
|
|
831
|
+
remoteIp,
|
|
832
|
+
userAgent,
|
|
833
|
+
details: { credentialId: credential.id, rpId: credential.rp_id },
|
|
834
|
+
});
|
|
835
|
+
});
|
|
836
|
+
tx();
|
|
837
|
+
return { token, device: getDeviceToken(db, deviceId), credential };
|
|
838
|
+
}
|
|
839
|
+
|
|
379
840
|
function getDeviceToken(db, deviceId) {
|
|
380
841
|
ensureMobileAuthSchema(db);
|
|
381
842
|
return deviceFromRow(db.prepare('SELECT * FROM ctm_device_tokens WHERE id = ?').get(String(deviceId || '')));
|
|
@@ -482,13 +943,43 @@ function resolveDeviceToken(db, token, context = {}) {
|
|
|
482
943
|
const at = nowMs();
|
|
483
944
|
if (!row) return { authenticated: false, code: token ? 'invalid_token' : 'missing_token' };
|
|
484
945
|
if (row.revoked_at) return { authenticated: false, code: 'token_revoked', deviceId: row.id };
|
|
946
|
+
// Hard cap: a device token cannot outlive the absolute window from issuance,
|
|
947
|
+
// regardless of activity. After this the device must re-pair (fresh passkey).
|
|
948
|
+
const absoluteDeadline = Number(row.created_at || 0) + deviceTokenAbsoluteMaxMs();
|
|
949
|
+
if (row.created_at && at >= absoluteDeadline) {
|
|
950
|
+
return { authenticated: false, code: 'token_expired', deviceId: row.id };
|
|
951
|
+
}
|
|
485
952
|
if (row.expires_at && row.expires_at <= at) return { authenticated: false, code: 'token_expired', deviceId: row.id };
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
953
|
+
// Sliding renewal: extend the idle window on each use, bounded by the absolute
|
|
954
|
+
// cap, so an active device stays paired while an abandoned (or quietly copied
|
|
955
|
+
// and unused) token expires within the idle window.
|
|
956
|
+
const slidExpiresAt = row.created_at
|
|
957
|
+
? Math.min(at + deviceTokenTtlMs(), absoluteDeadline)
|
|
958
|
+
: at + deviceTokenTtlMs();
|
|
959
|
+
// Throttle the renewal write: skip it only when it would be a no-op — the token was used
|
|
960
|
+
// within the throttle window AND its expiry is already at the slide target. A burst of
|
|
961
|
+
// requests (e.g. a phone loading a timeline full of image thumbnails) would otherwise fire
|
|
962
|
+
// one write-lock acquisition per request; one per minute is plenty to keep the 30-day
|
|
963
|
+
// sliding window alive. The expiry guard means a near-expiry token is still renewed even if
|
|
964
|
+
// used recently. Already wrapped in try/catch so a busy lock is never fatal.
|
|
965
|
+
const lastUsed = Number(row.last_used_at || 0);
|
|
966
|
+
const currentExpiry = Number(row.expires_at || 0);
|
|
967
|
+
const renewalRedundant = lastUsed
|
|
968
|
+
&& (at - lastUsed) <= RENEWAL_WRITE_THROTTLE_MS
|
|
969
|
+
&& currentExpiry >= (slidExpiresAt - RENEWAL_WRITE_THROTTLE_MS);
|
|
970
|
+
if (!renewalRedundant) {
|
|
971
|
+
try {
|
|
972
|
+
db.prepare(`
|
|
973
|
+
UPDATE ctm_device_tokens
|
|
974
|
+
SET last_used_at = ?, last_used_ip = ?, last_used_ua = ?, expires_at = ?
|
|
975
|
+
WHERE id = ?
|
|
976
|
+
`).run(at, context.remoteIp || '', context.userAgent || '', slidExpiresAt, row.id);
|
|
977
|
+
} catch {}
|
|
978
|
+
}
|
|
979
|
+
let stepUp = null;
|
|
980
|
+
if (context.stepUpToken) {
|
|
981
|
+
try { stepUp = validateStepUpSession(db, row.id, context.stepUpToken, at); } catch {}
|
|
982
|
+
}
|
|
492
983
|
return {
|
|
493
984
|
authenticated: true,
|
|
494
985
|
isLoopback: false,
|
|
@@ -501,6 +992,10 @@ function resolveDeviceToken(db, token, context = {}) {
|
|
|
501
992
|
source: 'device-token',
|
|
502
993
|
tokenHashPrefix: tokenHash.slice(0, 12),
|
|
503
994
|
device: deviceFromRow(row),
|
|
995
|
+
// Absolute-cap visibility: lets the phone show "re-pair soon" while the
|
|
996
|
+
// token still works instead of a surprise lockout at the hard deadline.
|
|
997
|
+
hardExpiresAt: row.created_at ? absoluteDeadline : null,
|
|
998
|
+
repairSoon: !!row.created_at && (absoluteDeadline - at) <= DEVICE_TOKEN_REPAIR_SOON_MS,
|
|
504
999
|
};
|
|
505
1000
|
}
|
|
506
1001
|
|
|
@@ -518,6 +1013,42 @@ function revokeDeviceToken(db, deviceId) {
|
|
|
518
1013
|
return result;
|
|
519
1014
|
}
|
|
520
1015
|
|
|
1016
|
+
function revokeDuplicateDeviceTokens(db, keepDeviceId, options = {}) {
|
|
1017
|
+
ensureMobileAuthSchema(db);
|
|
1018
|
+
const keepId = String(keepDeviceId || '');
|
|
1019
|
+
const devices = listDeviceTokenDetails(db, { nowMs: options.nowMs });
|
|
1020
|
+
const keep = devices.find((device) => device.id === keepId);
|
|
1021
|
+
if (!keep) throw new Error('device_not_found');
|
|
1022
|
+
const candidates = devices.filter((device) =>
|
|
1023
|
+
device.id !== keepId &&
|
|
1024
|
+
device.duplicate_group_id &&
|
|
1025
|
+
device.duplicate_group_id === keep.duplicate_group_id &&
|
|
1026
|
+
authorizationStatus(device, Number(options.nowMs || nowMs())) === 'authorized'
|
|
1027
|
+
);
|
|
1028
|
+
if (!candidates.length) return { count: 0, deviceIds: [] };
|
|
1029
|
+
const at = nowMs();
|
|
1030
|
+
const action = options.action || 'device_revoke_duplicate';
|
|
1031
|
+
const reason = options.reason || 'manual_keep_newest';
|
|
1032
|
+
const tx = db.transaction(() => {
|
|
1033
|
+
for (const device of candidates) {
|
|
1034
|
+
db.prepare(`
|
|
1035
|
+
UPDATE ctm_device_tokens
|
|
1036
|
+
SET revoked_at = COALESCE(revoked_at, ?)
|
|
1037
|
+
WHERE id = ?
|
|
1038
|
+
`).run(at, device.id);
|
|
1039
|
+
db.prepare('DELETE FROM ctm_step_up_sessions WHERE device_token_id = ?').run(device.id);
|
|
1040
|
+
insertAudit(db, {
|
|
1041
|
+
deviceTokenId: device.id,
|
|
1042
|
+
action,
|
|
1043
|
+
decision: 'allow',
|
|
1044
|
+
details: { keepDeviceId: keepId, reason },
|
|
1045
|
+
});
|
|
1046
|
+
}
|
|
1047
|
+
});
|
|
1048
|
+
tx();
|
|
1049
|
+
return { count: candidates.length, deviceIds: candidates.map((device) => device.id) };
|
|
1050
|
+
}
|
|
1051
|
+
|
|
521
1052
|
function revokeAllDeviceTokens(db) {
|
|
522
1053
|
ensureMobileAuthSchema(db);
|
|
523
1054
|
const at = nowMs();
|
|
@@ -563,6 +1094,40 @@ function cleanupExpiredDeviceClaims(db, options = {}) {
|
|
|
563
1094
|
return { deleted: rows.length };
|
|
564
1095
|
}
|
|
565
1096
|
|
|
1097
|
+
function cleanupExpiredWebAuthnChallenges(db, options = {}) {
|
|
1098
|
+
ensureMobileAuthSchema(db);
|
|
1099
|
+
const atMs = Number(options.nowMs || nowMs());
|
|
1100
|
+
const retentionMs = Math.max(0, Number(options.retentionMs ?? DEFAULT_WEBAUTHN_CHALLENGE_RETENTION_MS));
|
|
1101
|
+
const cutoff = atMs - retentionMs;
|
|
1102
|
+
const limit = Math.max(1, Math.min(1000, Number(options.limit || 250)));
|
|
1103
|
+
const rows = db.prepare(`
|
|
1104
|
+
SELECT id FROM ctm_webauthn_challenges
|
|
1105
|
+
WHERE expires_at <= ?
|
|
1106
|
+
AND (
|
|
1107
|
+
consumed_at IS NOT NULL
|
|
1108
|
+
OR expires_at <= ?
|
|
1109
|
+
)
|
|
1110
|
+
ORDER BY expires_at ASC
|
|
1111
|
+
LIMIT ?
|
|
1112
|
+
`).all(cutoff, cutoff, limit);
|
|
1113
|
+
if (!rows.length) return { deleted: 0 };
|
|
1114
|
+
const del = db.prepare('DELETE FROM ctm_webauthn_challenges WHERE id = ?');
|
|
1115
|
+
const txn = db.transaction((items) => {
|
|
1116
|
+
for (const row of items) del.run(row.id);
|
|
1117
|
+
});
|
|
1118
|
+
txn(rows);
|
|
1119
|
+
return { deleted: rows.length };
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
function cleanupMobileAuthArtifacts(db, options = {}) {
|
|
1123
|
+
ensureMobileAuthSchema(db);
|
|
1124
|
+
return {
|
|
1125
|
+
claims: cleanupExpiredDeviceClaims(db, options),
|
|
1126
|
+
pairing_requests: cleanupExpiredPairingRequests(db, options),
|
|
1127
|
+
webauthn_challenges: cleanupExpiredWebAuthnChallenges(db, options),
|
|
1128
|
+
};
|
|
1129
|
+
}
|
|
1130
|
+
|
|
566
1131
|
function createStepUpSession(db, deviceId, options = {}) {
|
|
567
1132
|
ensureMobileAuthSchema(db);
|
|
568
1133
|
const id = String(deviceId || '');
|
|
@@ -588,8 +1153,8 @@ function createStepUpSession(db, deviceId, options = {}) {
|
|
|
588
1153
|
}
|
|
589
1154
|
|
|
590
1155
|
function validateStepUpSession(db, deviceId, token, atMs = nowMs()) {
|
|
591
|
-
ensureMobileAuthSchema(db);
|
|
592
1156
|
if (!deviceId || !token) return null;
|
|
1157
|
+
ensureMobileAuthSchema(db);
|
|
593
1158
|
const row = db.prepare(`
|
|
594
1159
|
SELECT * FROM ctm_step_up_sessions
|
|
595
1160
|
WHERE id_hash = ? AND device_token_id = ?
|
|
@@ -601,6 +1166,7 @@ function validateStepUpSession(db, deviceId, token, atMs = nowMs()) {
|
|
|
601
1166
|
|
|
602
1167
|
function saveWebAuthnChallenge(db, options = {}) {
|
|
603
1168
|
ensureMobileAuthSchema(db);
|
|
1169
|
+
cleanupExpiredWebAuthnChallenges(db);
|
|
604
1170
|
const id = options.id || randomId('challenge');
|
|
605
1171
|
const createdAt = nowMs();
|
|
606
1172
|
db.prepare(`
|
|
@@ -776,12 +1342,21 @@ function listAuthAudit(db, options = {}) {
|
|
|
776
1342
|
|
|
777
1343
|
module.exports = {
|
|
778
1344
|
cancelDeviceClaim,
|
|
1345
|
+
deviceTokenAbsoluteMaxMs,
|
|
1346
|
+
deviceTokenTtlMs,
|
|
779
1347
|
cleanupExpiredDeviceClaims,
|
|
1348
|
+
cleanupExpiredPairingRequests,
|
|
1349
|
+
cleanupExpiredWebAuthnChallenges,
|
|
1350
|
+
cleanupMobileAuthArtifacts,
|
|
1351
|
+
approvePairingRequest,
|
|
1352
|
+
createPairingRequest,
|
|
780
1353
|
createStepUpSession,
|
|
781
1354
|
createDeviceClaim,
|
|
782
1355
|
ensureMobileAuthSchema,
|
|
1356
|
+
ensurePairingRequestClaim,
|
|
783
1357
|
finishDeviceClaim,
|
|
784
1358
|
getCredentialForDevice,
|
|
1359
|
+
getClaimRecoveryCredential,
|
|
785
1360
|
getDeviceToken,
|
|
786
1361
|
getWebAuthnChallenge,
|
|
787
1362
|
hashValue,
|
|
@@ -789,12 +1364,18 @@ module.exports = {
|
|
|
789
1364
|
listAuthAudit,
|
|
790
1365
|
listDeviceTokens,
|
|
791
1366
|
listDeviceTokenDetails,
|
|
1367
|
+
listActiveDeviceProfiles,
|
|
1368
|
+
listClaimRecoveryCredentials,
|
|
1369
|
+
listPairingRequests,
|
|
792
1370
|
listCredentialsForDevice,
|
|
793
1371
|
normalizeScopes,
|
|
794
1372
|
parseScopes,
|
|
795
1373
|
registerWebAuthnCredential,
|
|
796
1374
|
resolveDeviceToken,
|
|
1375
|
+
rejectPairingRequest,
|
|
1376
|
+
recoverDeviceClaim,
|
|
797
1377
|
revokeAllDeviceTokens,
|
|
1378
|
+
revokeDuplicateDeviceTokens,
|
|
798
1379
|
revokeDeviceToken,
|
|
799
1380
|
saveWebAuthnChallenge,
|
|
800
1381
|
consumeWebAuthnChallenge,
|
|
@@ -804,4 +1385,5 @@ module.exports = {
|
|
|
804
1385
|
updateDeviceToken,
|
|
805
1386
|
validateStepUpSession,
|
|
806
1387
|
verifyDeviceClaimSecret,
|
|
1388
|
+
verifyPairingRequestSecret,
|
|
807
1389
|
};
|