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.
Files changed (500) hide show
  1. package/README.md +27 -5
  2. package/package.json +2 -2
  3. package/template/CLAUDE.md +2 -2
  4. package/template/LICENSE +1 -1
  5. package/template/bin/ctm-dev-cleanup.js +24 -3
  6. package/template/bin/ctm-launch.sh +13 -0
  7. package/template/bin/dev.sh +156 -18
  8. package/template/bin/node-bin.sh +84 -0
  9. package/template/bin/pin-node.sh +51 -0
  10. package/template/claude-task-manager/api-prompts.js +1203 -182
  11. package/template/claude-task-manager/api-reviews.js +109 -15
  12. package/template/claude-task-manager/approval-agent.js +1360 -280
  13. package/template/claude-task-manager/bin/restart-ctm.sh +64 -23
  14. package/template/claude-task-manager/bin/storage-migration-supervisor.js +338 -0
  15. package/template/claude-task-manager/db.js +4417 -295
  16. package/template/claude-task-manager/docs/app-update-refresh-protocol.md +69 -0
  17. package/template/claude-task-manager/docs/approval-ai-refinement.md +138 -0
  18. package/template/claude-task-manager/docs/approval-rescue-loop.md +74 -0
  19. package/template/claude-task-manager/docs/codex-operational-warning-health.md +107 -0
  20. package/template/claude-task-manager/docs/codex-resume-state-guard-design.md +17 -12
  21. package/template/claude-task-manager/docs/codex-terminal-render-controller-handoff.md +311 -0
  22. package/template/claude-task-manager/docs/coding-agent-hooks-architecture.md +418 -0
  23. package/template/claude-task-manager/docs/conversation-import-freshness.md +20 -0
  24. package/template/claude-task-manager/docs/google-workspace-auth-health.md +77 -0
  25. package/template/claude-task-manager/docs/image-paste-ux.md +13 -0
  26. package/template/claude-task-manager/docs/ipad-web-preview.md +88 -0
  27. package/template/claude-task-manager/docs/main-loop-offload-architecture.md +66 -0
  28. package/template/claude-task-manager/docs/microsoft-dev-tunnel-phone-access-design.md +274 -519
  29. package/template/claude-task-manager/docs/mobile-live-streaming.md +27 -5
  30. package/template/claude-task-manager/docs/mobile-remote-submission-lifecycle.md +69 -0
  31. package/template/claude-task-manager/docs/phone-access-design.md +53 -15
  32. package/template/claude-task-manager/docs/phone-passkey-identity.md +122 -0
  33. package/template/claude-task-manager/docs/phone-setup.md +3 -0
  34. package/template/claude-task-manager/docs/prompt-editing-tree-design.md +25 -1
  35. package/template/claude-task-manager/docs/remote-desktop-access-design.md +268 -0
  36. package/template/claude-task-manager/docs/restart-lifecycle-architecture.md +95 -0
  37. package/template/claude-task-manager/docs/runtime-work-control-plane.md +53 -0
  38. package/template/claude-task-manager/docs/session-interactive-wait-surfaces.md +38 -0
  39. package/template/claude-task-manager/docs/session-needs-you-dismissal.md +84 -0
  40. package/template/claude-task-manager/docs/session-render-state-management-design.md +91 -3
  41. package/template/claude-task-manager/docs/session-standup-command-center-design.md +25 -1
  42. package/template/claude-task-manager/docs/session-title-authority.md +32 -0
  43. package/template/claude-task-manager/docs/session-workspace-binding.md +33 -0
  44. package/template/claude-task-manager/docs/skill-intent-resolution-design.md +72 -0
  45. package/template/claude-task-manager/docs/walle-mcp-supervisor-health.md +86 -0
  46. package/template/claude-task-manager/docs/walle-relay-phone-access-design.md +24 -15
  47. package/template/claude-task-manager/docs/walle-session-history-hydration.md +114 -0
  48. package/template/claude-task-manager/docs/walle-session-input-queue.md +104 -0
  49. package/template/claude-task-manager/docs/walle-session-model-catalog.md +90 -0
  50. package/template/claude-task-manager/docs/walle-session-model-preferences.md +15 -6
  51. package/template/claude-task-manager/git-utils.js +897 -27
  52. package/template/claude-task-manager/lib/agent-capabilities.js +33 -0
  53. package/template/claude-task-manager/lib/agent-cli-cache.js +37 -7
  54. package/template/claude-task-manager/lib/agent-hooks-installer.js +26 -2
  55. package/template/claude-task-manager/lib/agent-presets.js +17 -1
  56. package/template/claude-task-manager/lib/all-sessions-query.js +108 -0
  57. package/template/claude-task-manager/lib/approval-ai-refinement.js +488 -0
  58. package/template/claude-task-manager/lib/approval-self-adapt.js +168 -0
  59. package/template/claude-task-manager/lib/async-semaphore.js +44 -0
  60. package/template/claude-task-manager/lib/auth-context.js +5 -0
  61. package/template/claude-task-manager/lib/auth-rate-limit.js +47 -4
  62. package/template/claude-task-manager/lib/auth-rules.js +29 -2
  63. package/template/claude-task-manager/lib/auto-approval-verifier.js +129 -16
  64. package/template/claude-task-manager/lib/background-llm.js +144 -17
  65. package/template/claude-task-manager/lib/branch-inventory.js +212 -0
  66. package/template/claude-task-manager/lib/claude-desktop-sessions.js +15 -3
  67. package/template/claude-task-manager/lib/coalesce-sync-frames.js +151 -0
  68. package/template/claude-task-manager/lib/codex-launch-health.js +762 -0
  69. package/template/claude-task-manager/lib/codex-transcript-pager.js +51 -0
  70. package/template/claude-task-manager/lib/codex-zst.js +124 -0
  71. package/template/claude-task-manager/lib/coding-agent-models.js +233 -30
  72. package/template/claude-task-manager/lib/connection-health.js +232 -0
  73. package/template/claude-task-manager/lib/conversation-blob-parser.js +42 -0
  74. package/template/claude-task-manager/lib/conversation-tail-merge.js +89 -26
  75. package/template/claude-task-manager/lib/ctm-session-context-api.js +39 -10
  76. package/template/claude-task-manager/lib/cursor-conversation-store.js +354 -0
  77. package/template/claude-task-manager/lib/db-owner-worker-client.js +315 -0
  78. package/template/claude-task-manager/lib/document-review.js +141 -6
  79. package/template/claude-task-manager/lib/escalation-review.js +152 -0
  80. package/template/claude-task-manager/lib/graceful-shutdown.js +159 -0
  81. package/template/claude-task-manager/lib/headless-term-service.js +678 -0
  82. package/template/claude-task-manager/lib/heavy-worker-fallback.js +38 -0
  83. package/template/claude-task-manager/lib/jsonl-conversation-parser.js +542 -0
  84. package/template/claude-task-manager/lib/jsonl-range-reader.js +112 -0
  85. package/template/claude-task-manager/lib/main-db-census.js +216 -0
  86. package/template/claude-task-manager/lib/message-pagination.js +106 -4
  87. package/template/claude-task-manager/lib/microsoft-dev-tunnel-setup.js +750 -26
  88. package/template/claude-task-manager/lib/mobile-auth-api.js +274 -7
  89. package/template/claude-task-manager/lib/mobile-auth-store.js +592 -10
  90. package/template/claude-task-manager/lib/mobile-notification-dispatcher.js +15 -0
  91. package/template/claude-task-manager/lib/model-overview-brain-fallback.js +311 -0
  92. package/template/claude-task-manager/lib/model-overview-cache.js +141 -0
  93. package/template/claude-task-manager/lib/models-health-routing-notice.js +126 -0
  94. package/template/claude-task-manager/lib/node-pin-guard.js +93 -0
  95. package/template/claude-task-manager/lib/perf-tracker.js +242 -6
  96. package/template/claude-task-manager/lib/permission-match.js +76 -0
  97. package/template/claude-task-manager/lib/permission-sync.js +133 -20
  98. package/template/claude-task-manager/lib/process-title.js +35 -0
  99. package/template/claude-task-manager/lib/prompt-executions-query.js +25 -0
  100. package/template/claude-task-manager/lib/prompt-index-disk-cache.js +44 -0
  101. package/template/claude-task-manager/lib/prompt-intent.js +132 -0
  102. package/template/claude-task-manager/lib/provider-user-context.js +34 -0
  103. package/template/claude-task-manager/lib/read-pool-client.js +313 -0
  104. package/template/claude-task-manager/lib/readpool-breaker.js +31 -0
  105. package/template/claude-task-manager/lib/recent-sessions-breaker.js +12 -0
  106. package/template/claude-task-manager/lib/remote-feedback-client.js +72 -0
  107. package/template/claude-task-manager/lib/remote-relay-protocol.js +37 -4
  108. package/template/claude-task-manager/lib/remote-relay-store.js +159 -0
  109. package/template/claude-task-manager/lib/remote-submission-observer.js +278 -0
  110. package/template/claude-task-manager/lib/restart-guard.js +109 -0
  111. package/template/claude-task-manager/lib/restore-interruption-detector.js +439 -0
  112. package/template/claude-task-manager/lib/restore-policy.js +13 -0
  113. package/template/claude-task-manager/lib/restore-resume-batch.js +74 -0
  114. package/template/claude-task-manager/lib/restore-runtime.js +68 -0
  115. package/template/claude-task-manager/lib/restore-storm.js +34 -0
  116. package/template/claude-task-manager/lib/resume-cwd.js +36 -0
  117. package/template/claude-task-manager/lib/resume-preflight.js +313 -0
  118. package/template/claude-task-manager/lib/runtime-work-registry.js +444 -0
  119. package/template/claude-task-manager/lib/sanitize-openai-auth.js +31 -0
  120. package/template/claude-task-manager/lib/scheduler.js +21 -1
  121. package/template/claude-task-manager/lib/scrollback-snapshot-store.js +159 -0
  122. package/template/claude-task-manager/lib/serial-task-queue.js +64 -0
  123. package/template/claude-task-manager/lib/server-listeners.js +239 -0
  124. package/template/claude-task-manager/lib/session-capture.js +42 -7
  125. package/template/claude-task-manager/lib/session-content-backfill.js +131 -0
  126. package/template/claude-task-manager/lib/session-history.js +388 -43
  127. package/template/claude-task-manager/lib/session-host-manager.js +287 -0
  128. package/template/claude-task-manager/lib/session-image-refs.js +209 -0
  129. package/template/claude-task-manager/lib/session-jobs.js +399 -59
  130. package/template/claude-task-manager/lib/session-prompt-index.js +137 -0
  131. package/template/claude-task-manager/lib/session-restore.js +53 -0
  132. package/template/claude-task-manager/lib/session-standup.js +123 -23
  133. package/template/claude-task-manager/lib/session-state-bus.js +14 -0
  134. package/template/claude-task-manager/lib/session-stream.js +64 -16
  135. package/template/claude-task-manager/lib/session-timeline-summary.js +260 -0
  136. package/template/claude-task-manager/lib/session-token-usage.js +494 -0
  137. package/template/claude-task-manager/lib/session-workspace-binding.js +356 -0
  138. package/template/claude-task-manager/lib/setup-network-config.js +9 -0
  139. package/template/claude-task-manager/lib/size-cap.js +45 -0
  140. package/template/claude-task-manager/lib/size-cap.test.js +62 -0
  141. package/template/claude-task-manager/lib/skill-autocomplete.js +180 -1
  142. package/template/claude-task-manager/lib/skill-intent-resolver.js +304 -0
  143. package/template/claude-task-manager/lib/sqlite-driver.js +19 -3
  144. package/template/claude-task-manager/lib/standup-attention.js +7 -3
  145. package/template/claude-task-manager/lib/status-authority.js +39 -0
  146. package/template/claude-task-manager/lib/status-hooks.js +4 -0
  147. package/template/claude-task-manager/lib/storage-migration.js +235 -0
  148. package/template/claude-task-manager/lib/structured-capture.js +298 -0
  149. package/template/claude-task-manager/lib/sync-io-census.js +163 -0
  150. package/template/claude-task-manager/lib/tailscale-setup.js +6 -0
  151. package/template/claude-task-manager/lib/terminal-activity-evidence.js +33 -0
  152. package/template/claude-task-manager/lib/terminal-choice.js +364 -0
  153. package/template/claude-task-manager/lib/terminal-control-sanitize.js +17 -0
  154. package/template/claude-task-manager/lib/terminal-fingerprint.js +48 -0
  155. package/template/claude-task-manager/lib/terminal-output-flush.js +84 -0
  156. package/template/claude-task-manager/lib/timeline-order.js +122 -0
  157. package/template/claude-task-manager/lib/transcript-store.js +348 -43
  158. package/template/claude-task-manager/lib/transport-security.js +84 -1
  159. package/template/claude-task-manager/lib/wait-state.js +184 -0
  160. package/template/claude-task-manager/lib/walle-client.js +47 -5
  161. package/template/claude-task-manager/lib/walle-ctm-history.js +564 -4
  162. package/template/claude-task-manager/lib/walle-external-actions.js +135 -16
  163. package/template/claude-task-manager/lib/walle-history-hydration.js +46 -0
  164. package/template/claude-task-manager/lib/walle-native-health.js +403 -0
  165. package/template/claude-task-manager/lib/walle-repair.js +701 -0
  166. package/template/claude-task-manager/lib/walle-session-cache.js +109 -0
  167. package/template/claude-task-manager/lib/walle-session-context.js +57 -21
  168. package/template/claude-task-manager/lib/walle-session-model-catalog.js +34 -0
  169. package/template/claude-task-manager/lib/walle-supervisor.js +539 -63
  170. package/template/claude-task-manager/lib/walle-transcript.js +52 -0
  171. package/template/claude-task-manager/lib/worktree-active-sync.js +11 -7
  172. package/template/claude-task-manager/lib/worktree-cwd.js +32 -1
  173. package/template/claude-task-manager/package.json +1 -1
  174. package/template/claude-task-manager/prompt-harvest.js +89 -66
  175. package/template/claude-task-manager/providers/claude-code.js +51 -3
  176. package/template/claude-task-manager/providers/cursor.js +140 -45
  177. package/template/claude-task-manager/public/css/reviews.css +551 -61
  178. package/template/claude-task-manager/public/css/setup.css +191 -0
  179. package/template/claude-task-manager/public/css/walle-session.css +865 -10
  180. package/template/claude-task-manager/public/css/walle.css +154 -0
  181. package/template/claude-task-manager/public/designs/ai-providers-consolidation-v2.html +830 -0
  182. package/template/claude-task-manager/public/index.html +18516 -2058
  183. package/template/claude-task-manager/public/ipad.html +363 -0
  184. package/template/claude-task-manager/public/js/document-review-links.js +301 -0
  185. package/template/claude-task-manager/public/js/image-normalize.js +69 -36
  186. package/template/claude-task-manager/public/js/message-renderer.js +1265 -77
  187. package/template/claude-task-manager/public/js/prompts.js +66 -29
  188. package/template/claude-task-manager/public/js/reviews.js +901 -133
  189. package/template/claude-task-manager/public/js/session-activity-utils.js +11 -1
  190. package/template/claude-task-manager/public/js/session-search-utils.js +94 -10
  191. package/template/claude-task-manager/public/js/session-status-precedence.js +23 -5
  192. package/template/claude-task-manager/public/js/setup.js +1273 -176
  193. package/template/claude-task-manager/public/js/stream-view.js +691 -73
  194. package/template/claude-task-manager/public/js/terminal-reconciler.js +210 -0
  195. package/template/claude-task-manager/public/js/walle-session.js +2455 -158
  196. package/template/claude-task-manager/public/js/walle.js +455 -28
  197. package/template/claude-task-manager/public/m/app.css +2909 -262
  198. package/template/claude-task-manager/public/m/app.js +6601 -398
  199. package/template/claude-task-manager/public/m/claim.html +224 -17
  200. package/template/claude-task-manager/public/m/index.html +117 -21
  201. package/template/claude-task-manager/public/m/sw.js +3 -1
  202. package/template/claude-task-manager/public/manifest.json +2 -2
  203. package/template/claude-task-manager/public/prompts.html +30 -14
  204. package/template/claude-task-manager/queue-engine.js +507 -28
  205. package/template/claude-task-manager/scripts/repair-claude-session-images.js +27 -8
  206. package/template/claude-task-manager/server.js +14341 -2197
  207. package/template/claude-task-manager/session-integrity.js +160 -18
  208. package/template/claude-task-manager/session-search-ranking.js +1 -0
  209. package/template/claude-task-manager/session-utils.js +25 -5
  210. package/template/claude-task-manager/workers/approval-blocklist.js +96 -6
  211. package/template/claude-task-manager/workers/approval-widget-validator.js +14 -8
  212. package/template/claude-task-manager/workers/conversation-import-worker.js +11 -50
  213. package/template/claude-task-manager/workers/db-owner-worker.js +386 -0
  214. package/template/claude-task-manager/workers/harvest-worker.js +9 -55
  215. package/template/claude-task-manager/workers/headless-term-worker.js +9 -530
  216. package/template/claude-task-manager/workers/read-pool-worker.js +387 -0
  217. package/template/claude-task-manager/workers/scrollback-worker.js +11 -72
  218. package/template/claude-task-manager/workers/session-host-process.js +146 -0
  219. package/template/claude-task-manager/workers/session-integrity-worker.js +10 -54
  220. package/template/claude-task-manager/workers/state-detectors/base.js +18 -1
  221. package/template/claude-task-manager/workers/state-detectors/claude-code.js +182 -9
  222. package/template/claude-task-manager/workers/state-detectors/codex.js +150 -2
  223. package/template/claude-task-manager/workers/state-detectors/cursor.js +127 -0
  224. package/template/claude-task-manager/workers/state-detectors/gemini.js +21 -0
  225. package/template/claude-task-manager/workers/state-detectors/index.js +29 -0
  226. package/template/claude-task-manager/workers/state-detectors/opencode.js +103 -0
  227. package/template/docs/design/markdown-review-pane.md +206 -0
  228. package/template/docs/designs/2026-05-17-portkey-gateway-provider-ux.md +129 -38
  229. package/template/docs/designs/2026-05-20-mobile-worktree-finish-command.md +27 -0
  230. package/template/docs/designs/2026-05-22-ai-configuration-consolidation.md +248 -0
  231. package/template/docs/designs/ai-configuration-consolidation-mock.html +812 -0
  232. package/template/docs/private-memory-and-pii-policy.md +69 -0
  233. package/template/package.json +2 -1
  234. package/template/scripts/check-private-data.js +201 -0
  235. package/template/shared/sqlite-owner-guard.js +30 -0
  236. package/template/shared/sqlite-owner-write-queue.js +225 -0
  237. package/template/shared/sqlite-storage-policy.js +111 -0
  238. package/template/shared/sqlite-write-lock.js +428 -0
  239. package/template/wall-e/agent-runners/claude-code.js +5 -0
  240. package/template/wall-e/agent.js +166 -22
  241. package/template/wall-e/api-walle.js +524 -70
  242. package/template/wall-e/auth/provider-flows.js +11 -1
  243. package/template/wall-e/bin/walle-mcp-stdio.js +341 -17
  244. package/template/wall-e/brain.js +1614 -141
  245. package/template/wall-e/chat/attachment-blocks.js +96 -0
  246. package/template/wall-e/chat/attachments.js +2 -1
  247. package/template/wall-e/chat/capability-resolver.js +7 -7
  248. package/template/wall-e/chat/context-messages.js +28 -0
  249. package/template/wall-e/chat/conversation-frame.js +630 -0
  250. package/template/wall-e/chat/provider-messages.js +125 -0
  251. package/template/wall-e/chat.js +1002 -233
  252. package/template/wall-e/coding/acceptance-contract.js +170 -0
  253. package/template/wall-e/coding/acp-adapter.js +1 -1
  254. package/template/wall-e/coding/agent-catalog.js +3 -0
  255. package/template/wall-e/coding/artifact-store.js +93 -0
  256. package/template/wall-e/coding/capability-router.js +120 -0
  257. package/template/wall-e/coding/coding-run-controller.js +423 -0
  258. package/template/wall-e/coding/compaction-service.js +157 -12
  259. package/template/wall-e/coding/frontend-verification.js +258 -0
  260. package/template/wall-e/coding/lifecycle-hooks.js +75 -0
  261. package/template/wall-e/coding/local-preview-contract.js +157 -0
  262. package/template/wall-e/coding/permission-service.js +57 -13
  263. package/template/wall-e/coding/prompt-bundle.js +19 -1
  264. package/template/wall-e/coding/prompt-section-registry.js +227 -0
  265. package/template/wall-e/coding/provider-compat.js +15 -0
  266. package/template/wall-e/coding/runtime-events.js +224 -0
  267. package/template/wall-e/coding/runtime-mode.js +3 -0
  268. package/template/wall-e/coding/side-git-snapshot.js +160 -4
  269. package/template/wall-e/coding/snapshot-service.js +143 -1
  270. package/template/wall-e/coding/stream-processor.js +388 -34
  271. package/template/wall-e/coding/task-tool.js +141 -4
  272. package/template/wall-e/coding/tool-execution-controller.js +365 -0
  273. package/template/wall-e/coding/tool-registry.js +43 -5
  274. package/template/wall-e/coding/user-hooks.js +217 -0
  275. package/template/wall-e/coding-orchestrator.js +1330 -221
  276. package/template/wall-e/coding-prompts.js +20 -4
  277. package/template/wall-e/context/context-builder.js +15 -2
  278. package/template/wall-e/decision/confidence.js +1 -1
  279. package/template/wall-e/docs/coding-acceptance-contract.md +41 -0
  280. package/template/wall-e/docs/external-action-controller.md +26 -6
  281. package/template/wall-e/docs/telemetry-lifecycle.md +8 -2
  282. package/template/wall-e/embeddings.js +591 -53
  283. package/template/wall-e/external-action-controller.js +12 -0
  284. package/template/wall-e/http/auth.js +1 -0
  285. package/template/wall-e/http/chat-api.js +46 -11
  286. package/template/wall-e/http/model-admin.js +836 -34
  287. package/template/wall-e/lib/boot-profile.js +88 -0
  288. package/template/wall-e/lib/event-loop-monitor.js +93 -0
  289. package/template/wall-e/lib/service-health.js +194 -0
  290. package/template/wall-e/llm/anthropic.js +130 -5
  291. package/template/wall-e/llm/client.js +266 -63
  292. package/template/wall-e/llm/default-fallback.js +382 -0
  293. package/template/wall-e/llm/health.js +19 -0
  294. package/template/wall-e/llm/message-guard.js +78 -0
  295. package/template/wall-e/llm/model-catalog.js +252 -1
  296. package/template/wall-e/llm/openai.js +26 -4
  297. package/template/wall-e/llm/portkey-sync.js +654 -0
  298. package/template/wall-e/llm/provider-error.js +30 -2
  299. package/template/wall-e/llm/registry.js +5 -1
  300. package/template/wall-e/llm/request-compat.js +67 -0
  301. package/template/wall-e/loops/backfill.js +79 -23
  302. package/template/wall-e/loops/brain-optimize.js +67 -0
  303. package/template/wall-e/loops/ingest.js +25 -10
  304. package/template/wall-e/loops/question-digest.js +160 -0
  305. package/template/wall-e/loops/reflect.js +6 -4
  306. package/template/wall-e/loops/think.js +39 -12
  307. package/template/wall-e/mcp-server.js +318 -36
  308. package/template/wall-e/memory/ctm-context-client.js +52 -14
  309. package/template/wall-e/memory/ctm-operational-context.js +237 -0
  310. package/template/wall-e/memory/ctm-prompt-executions-client.js +128 -0
  311. package/template/wall-e/memory/ctm-session-context.js +111 -63
  312. package/template/wall-e/prompts/coding/deepseek.txt +3 -0
  313. package/template/wall-e/prompts/coding/gemini.txt +6 -0
  314. package/template/wall-e/prompts/coding/gpt.txt +6 -0
  315. package/template/wall-e/prompts/coding/local.txt +7 -0
  316. package/template/wall-e/runtime/decision-hooks.js +115 -0
  317. package/template/wall-e/runtime/devbox-gateway.js +82 -8
  318. package/template/wall-e/runtime/prompt-manifest.js +86 -0
  319. package/template/wall-e/runtime/tool-executor.js +269 -0
  320. package/template/wall-e/runtime/tool-result-envelope.js +138 -0
  321. package/template/wall-e/runtime/transcript-projection.js +60 -0
  322. package/template/wall-e/runtime/walle-runtime.js +224 -0
  323. package/template/wall-e/scripts/db-optimize/migrate.js +162 -0
  324. package/template/wall-e/scripts/db-optimize/recall-eval.js +117 -0
  325. package/template/wall-e/server.js +15 -0
  326. package/template/wall-e/session-files.js +9 -0
  327. package/template/wall-e/skills/_bundled/google-calendar/run.js +1 -1
  328. package/template/wall-e/skills/_bundled/gws-workspace/run.js +1 -1
  329. package/template/wall-e/skills/_bundled/slack-mentions/run.js +76 -6
  330. package/template/wall-e/skills/claude-code-reader.js +7 -3
  331. package/template/wall-e/skills/script-skill-runner.js +10 -0
  332. package/template/wall-e/skills/skill-planner.js +38 -0
  333. package/template/wall-e/tools/builtin-middleware.js +19 -9
  334. package/template/wall-e/tools/local-tools.js +1428 -16
  335. package/template/wall-e/tools/permission-checker.js +73 -5
  336. package/template/wall-e/tools/question-manager.js +117 -7
  337. package/template/wall-e/training/harvester.js +12 -28
  338. package/template/wall-e/training/replay.js +25 -80
  339. package/template/website/index.html +10 -10
  340. package/template/wall-e/eval/ab-test.js +0 -203
  341. package/template/wall-e/eval/agent-runner.js +0 -772
  342. package/template/wall-e/eval/agent-scorer.js +0 -461
  343. package/template/wall-e/eval/aggregator.js +0 -414
  344. package/template/wall-e/eval/allowed-test-commands.js +0 -34
  345. package/template/wall-e/eval/benchmark-generator.js +0 -113
  346. package/template/wall-e/eval/benchmarks/chat-eval.json +0 -1662
  347. package/template/wall-e/eval/benchmarks/chat.json +0 -82
  348. package/template/wall-e/eval/benchmarks/coding-agent-real.json +0 -1
  349. package/template/wall-e/eval/benchmarks/coding-agent.json +0 -1581
  350. package/template/wall-e/eval/benchmarks/coding.json +0 -122
  351. package/template/wall-e/eval/benchmarks/memory-retrieval.json +0 -234
  352. package/template/wall-e/eval/benchmarks/reasoning.json +0 -82
  353. package/template/wall-e/eval/benchmarks/swebench-lite-30.json +0 -212
  354. package/template/wall-e/eval/benchmarks.js +0 -669
  355. package/template/wall-e/eval/cc-replay.js +0 -719
  356. package/template/wall-e/eval/chat-eval.js +0 -525
  357. package/template/wall-e/eval/check-keys.js +0 -15
  358. package/template/wall-e/eval/check-providers.js +0 -42
  359. package/template/wall-e/eval/codex-cli-baseline.js +0 -669
  360. package/template/wall-e/eval/coding-agent-real.js +0 -570
  361. package/template/wall-e/eval/context-compactor.js +0 -251
  362. package/template/wall-e/eval/debug-agent003.js +0 -68
  363. package/template/wall-e/eval/diagnostics.js +0 -216
  364. package/template/wall-e/eval/eval-orchestrator.js +0 -642
  365. package/template/wall-e/eval/evaluate.js +0 -202
  366. package/template/wall-e/eval/evaluator.js +0 -373
  367. package/template/wall-e/eval/exporter.js +0 -212
  368. package/template/wall-e/eval/fixtures/express-basic/package.json +0 -9
  369. package/template/wall-e/eval/fixtures/express-basic/server.js +0 -115
  370. package/template/wall-e/eval/fixtures/express-basic/test.js +0 -83
  371. package/template/wall-e/eval/fixtures/express-buggy/package.json +0 -9
  372. package/template/wall-e/eval/fixtures/express-buggy/server.js +0 -113
  373. package/template/wall-e/eval/fixtures/express-buggy/test.js +0 -83
  374. package/template/wall-e/eval/fixtures/express-buggy-items/package.json +0 -9
  375. package/template/wall-e/eval/fixtures/express-buggy-items/server.js +0 -112
  376. package/template/wall-e/eval/fixtures/express-buggy-items/test.js +0 -83
  377. package/template/wall-e/eval/fixtures/express-buggy-search/package.json +0 -9
  378. package/template/wall-e/eval/fixtures/express-buggy-search/server.js +0 -121
  379. package/template/wall-e/eval/fixtures/express-buggy-search/test.js +0 -83
  380. package/template/wall-e/eval/fixtures/express-rename-data/data.js +0 -34
  381. package/template/wall-e/eval/fixtures/express-rename-data/package.json +0 -9
  382. package/template/wall-e/eval/fixtures/express-rename-data/server.js +0 -97
  383. package/template/wall-e/eval/fixtures/express-rename-data/test.js +0 -88
  384. package/template/wall-e/eval/fixtures/express-xss/package.json +0 -12
  385. package/template/wall-e/eval/fixtures/express-xss/server.js +0 -90
  386. package/template/wall-e/eval/fixtures/express-xss/test.js +0 -67
  387. package/template/wall-e/eval/fixtures/express-xss/views/profile.ejs +0 -9
  388. package/template/wall-e/eval/fixtures/fullstack-app/config/default.js +0 -9
  389. package/template/wall-e/eval/fixtures/fullstack-app/config/test.js +0 -13
  390. package/template/wall-e/eval/fixtures/fullstack-app/package.json +0 -11
  391. package/template/wall-e/eval/fixtures/fullstack-app/public/css/style.css +0 -137
  392. package/template/wall-e/eval/fixtures/fullstack-app/public/index.html +0 -46
  393. package/template/wall-e/eval/fixtures/fullstack-app/public/js/app.js +0 -121
  394. package/template/wall-e/eval/fixtures/fullstack-app/public/js/auth.js +0 -71
  395. package/template/wall-e/eval/fixtures/fullstack-app/public/js/items.js +0 -80
  396. package/template/wall-e/eval/fixtures/fullstack-app/public/js/users.js +0 -46
  397. package/template/wall-e/eval/fixtures/fullstack-app/public/login.html +0 -45
  398. package/template/wall-e/eval/fixtures/fullstack-app/public/register.html +0 -38
  399. package/template/wall-e/eval/fixtures/fullstack-app/scripts/migrate.js +0 -23
  400. package/template/wall-e/eval/fixtures/fullstack-app/scripts/seed.js +0 -46
  401. package/template/wall-e/eval/fixtures/fullstack-app/server/db.js +0 -99
  402. package/template/wall-e/eval/fixtures/fullstack-app/server/index.js +0 -94
  403. package/template/wall-e/eval/fixtures/fullstack-app/server/middleware/auth.js +0 -19
  404. package/template/wall-e/eval/fixtures/fullstack-app/server/middleware/logger.js +0 -19
  405. package/template/wall-e/eval/fixtures/fullstack-app/server/router.js +0 -50
  406. package/template/wall-e/eval/fixtures/fullstack-app/server/routes/auth.js +0 -69
  407. package/template/wall-e/eval/fixtures/fullstack-app/server/routes/health.js +0 -23
  408. package/template/wall-e/eval/fixtures/fullstack-app/server/routes/items.js +0 -88
  409. package/template/wall-e/eval/fixtures/fullstack-app/server/routes/users.js +0 -75
  410. package/template/wall-e/eval/fixtures/fullstack-app/server/test.js +0 -198
  411. package/template/wall-e/eval/fixtures/fullstack-app/server/utils/response.js +0 -34
  412. package/template/wall-e/eval/fixtures/fullstack-app/server/utils/validate.js +0 -26
  413. package/template/wall-e/eval/fixtures/fullstack-app/server.js +0 -8
  414. package/template/wall-e/eval/fixtures/fullstack-app/test.js +0 -12
  415. package/template/wall-e/eval/fixtures/monorepo-basic/package.json +0 -8
  416. package/template/wall-e/eval/fixtures/monorepo-basic/packages/api/data.js +0 -58
  417. package/template/wall-e/eval/fixtures/monorepo-basic/packages/api/middleware.js +0 -46
  418. package/template/wall-e/eval/fixtures/monorepo-basic/packages/api/package.json +0 -8
  419. package/template/wall-e/eval/fixtures/monorepo-basic/packages/api/routes.js +0 -64
  420. package/template/wall-e/eval/fixtures/monorepo-basic/packages/api/server.js +0 -56
  421. package/template/wall-e/eval/fixtures/monorepo-basic/packages/api/test.js +0 -116
  422. package/template/wall-e/eval/fixtures/monorepo-basic/packages/cli/commands.js +0 -61
  423. package/template/wall-e/eval/fixtures/monorepo-basic/packages/cli/index.js +0 -62
  424. package/template/wall-e/eval/fixtures/monorepo-basic/packages/cli/output.js +0 -43
  425. package/template/wall-e/eval/fixtures/monorepo-basic/packages/cli/package.json +0 -11
  426. package/template/wall-e/eval/fixtures/monorepo-basic/packages/cli/test.js +0 -44
  427. package/template/wall-e/eval/fixtures/monorepo-basic/packages/shared/formatters.js +0 -43
  428. package/template/wall-e/eval/fixtures/monorepo-basic/packages/shared/index.js +0 -12
  429. package/template/wall-e/eval/fixtures/monorepo-basic/packages/shared/package.json +0 -5
  430. package/template/wall-e/eval/fixtures/monorepo-basic/packages/shared/test.js +0 -55
  431. package/template/wall-e/eval/fixtures/monorepo-basic/packages/shared/validators.js +0 -29
  432. package/template/wall-e/eval/fixtures/monorepo-basic/test.js +0 -46
  433. package/template/wall-e/eval/fixtures/node-cli/index.js +0 -78
  434. package/template/wall-e/eval/fixtures/node-cli/package.json +0 -10
  435. package/template/wall-e/eval/fixtures/node-cli/test.js +0 -57
  436. package/template/wall-e/eval/fixtures/node-typed/package.json +0 -8
  437. package/template/wall-e/eval/fixtures/node-typed/src/handlers.js +0 -31
  438. package/template/wall-e/eval/fixtures/node-typed/src/utils.js +0 -33
  439. package/template/wall-e/eval/fixtures/node-typed/test.js +0 -36
  440. package/template/wall-e/eval/fixtures/python-flask/app.py +0 -14
  441. package/template/wall-e/eval/fixtures/python-flask/requirements.txt +0 -2
  442. package/template/wall-e/eval/fixtures/python-flask/test_app.py +0 -25
  443. package/template/wall-e/eval/fixtures/wall-e-subset/brain.js +0 -105
  444. package/template/wall-e/eval/fixtures/wall-e-subset/eval/aggregator.js +0 -101
  445. package/template/wall-e/eval/fixtures/wall-e-subset/eval/benchmarks/chat.json +0 -20
  446. package/template/wall-e/eval/fixtures/wall-e-subset/eval/benchmarks/coding.json +0 -32
  447. package/template/wall-e/eval/fixtures/wall-e-subset/eval/benchmarks.js +0 -64
  448. package/template/wall-e/eval/fixtures/wall-e-subset/eval/fixtures/simple-project/package.json +0 -6
  449. package/template/wall-e/eval/fixtures/wall-e-subset/eval/fixtures/simple-project/server.js +0 -31
  450. package/template/wall-e/eval/fixtures/wall-e-subset/eval/fixtures/simple-project/test.js +0 -18
  451. package/template/wall-e/eval/fixtures/wall-e-subset/eval/fixtures/simple-project/utils.js +0 -34
  452. package/template/wall-e/eval/fixtures/wall-e-subset/eval/runner.js +0 -104
  453. package/template/wall-e/eval/fixtures/wall-e-subset/eval/scorer.js +0 -73
  454. package/template/wall-e/eval/fixtures/wall-e-subset/eval/test.js +0 -134
  455. package/template/wall-e/eval/fixtures/wall-e-subset/llm/client.js +0 -99
  456. package/template/wall-e/eval/fixtures/wall-e-subset/llm/providers.js +0 -63
  457. package/template/wall-e/eval/fixtures/wall-e-subset/llm/test.js +0 -70
  458. package/template/wall-e/eval/fixtures/wall-e-subset/package.json +0 -10
  459. package/template/wall-e/eval/fixtures/wall-e-subset/test.js +0 -86
  460. package/template/wall-e/eval/harvester.js +0 -685
  461. package/template/wall-e/eval/head-to-head.js +0 -388
  462. package/template/wall-e/eval/humaneval-adapter.js +0 -321
  463. package/template/wall-e/eval/list-models.js +0 -31
  464. package/template/wall-e/eval/livecodebench-adapter.js +0 -291
  465. package/template/wall-e/eval/mail-integration.js +0 -443
  466. package/template/wall-e/eval/manifest.js +0 -186
  467. package/template/wall-e/eval/meta-harness/adapters/coding-agent.js +0 -57
  468. package/template/wall-e/eval/meta-harness/bootstrap-snapshot.js +0 -149
  469. package/template/wall-e/eval/meta-harness/candidate-store.js +0 -117
  470. package/template/wall-e/eval/meta-harness/cli.js +0 -86
  471. package/template/wall-e/eval/meta-harness/domain-spec.js +0 -154
  472. package/template/wall-e/eval/meta-harness/domains/coding-agent.domain.json +0 -84
  473. package/template/wall-e/eval/meta-harness/examples/env-bootstrap-candidate.js +0 -29
  474. package/template/wall-e/eval/meta-harness/experience-store.js +0 -174
  475. package/template/wall-e/eval/meta-harness/frontier.js +0 -96
  476. package/template/wall-e/eval/meta-harness/harness-interface.js +0 -90
  477. package/template/wall-e/eval/meta-harness/leakage-guard.js +0 -80
  478. package/template/wall-e/eval/meta-harness/optimizer.js +0 -207
  479. package/template/wall-e/eval/meta-harness/proposer-runner.js +0 -110
  480. package/template/wall-e/eval/meta-harness/reporting.js +0 -58
  481. package/template/wall-e/eval/meta-harness/telemetry.js +0 -27
  482. package/template/wall-e/eval/meta-harness/validation.js +0 -81
  483. package/template/wall-e/eval/promoter.js +0 -228
  484. package/template/wall-e/eval/provider-normalizer.js +0 -33
  485. package/template/wall-e/eval/replay.js +0 -395
  486. package/template/wall-e/eval/run-agent-benchmarks.js +0 -386
  487. package/template/wall-e/eval/run-codex-cli-baseline.js +0 -177
  488. package/template/wall-e/eval/run-coding-agent-real.js +0 -187
  489. package/template/wall-e/eval/run-eval.js +0 -435
  490. package/template/wall-e/eval/run-model-comparison.js +0 -142
  491. package/template/wall-e/eval/session-evaluator.js +0 -187
  492. package/template/wall-e/eval/session-miner.js +0 -207
  493. package/template/wall-e/eval/session-retrieval-benchmark.js +0 -150
  494. package/template/wall-e/eval/session-transcripts.js +0 -509
  495. package/template/wall-e/eval/shadow.js +0 -161
  496. package/template/wall-e/eval/swebench-adapter.js +0 -345
  497. package/template/wall-e/eval/swebench-docker.js +0 -192
  498. package/template/wall-e/eval/train.py +0 -320
  499. package/template/wall-e/eval/trainer.js +0 -232
  500. 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 DEFAULT_DEVICE_TOKEN_TTL_MS = 365 * 24 * 60 * 60 * 1000;
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 + DEFAULT_DEVICE_TOKEN_TTL_MS,
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
- db.prepare(`
487
- UPDATE ctm_device_tokens
488
- SET last_used_at = ?, last_used_ip = ?, last_used_ua = ?
489
- WHERE id = ?
490
- `).run(at, context.remoteIp || '', context.userAgent || '', row.id);
491
- const stepUp = validateStepUpSession(db, row.id, context.stepUpToken, at);
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
  };