From df6fb99f7a39c295c88b8bd08e74b4137bc31c3b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 10 Sep 2026 01:26:19 +0000 Subject: [PATCH 1/4] build(deps): bump nodemailer Bumps the npm_and_yarn group with 1 update in the / directory: [nodemailer](https://github.com/nodemailer/nodemailer). Updates `nodemailer` from 9.0.3 to 9.1.1 - [Release notes](https://github.com/nodemailer/nodemailer/releases) - [Changelog](https://github.com/nodemailer/nodemailer/blob/master/CHANGELOG.md) - [Commits](https://github.com/nodemailer/nodemailer/compare/v9.0.3...v9.1.1) --- updated-dependencies: - dependency-name: nodemailer dependency-version: 9.1.1 dependency-type: direct:production dependency-group: npm_and_yarn ... Signed-off-by: dependabot[bot] --- package.json | 2 +- pnpm-lock.yaml | 168 ++++++++++++++++++++++++++++--------------------- 2 files changed, 98 insertions(+), 72 deletions(-) diff --git a/package.json b/package.json index f3c4cc0..96ac4e6 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,7 @@ "jszip": "^3.10.1", "nanoid": "^6.0.0", "next": "^16.2.12", - "nodemailer": "^9.0.3", + "nodemailer": "^9.1.1", "passkit-generator": "^3.5.7", "pdf-lib": "^1.17.1", "pino": "^10.3.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 47aa867..a250d44 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -39,8 +39,8 @@ importers: specifier: ^16.2.12 version: 16.2.12(@types/node@26.1.2)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) nodemailer: - specifier: ^9.0.3 - version: 9.0.3 + specifier: ^9.1.1 + version: 9.1.1 passkit-generator: specifier: ^3.5.7 version: 3.5.7 @@ -759,8 +759,8 @@ packages: cpu: [x64] os: [win32] - '@oxc-project/types@0.142.0': - resolution: {integrity: sha512-7W+2q5AKQVU36fkaryontrHn3YDt1RyUYXatw9i5H8ocYe2sPKSFB6eS8WNPeRKiN1qAWWZUPm7gwFzJGrccqQ==} + '@oxc-project/types@0.149.0': + resolution: {integrity: sha512-Efcc+iF0j3Bf67YjEqIqWXbX5XddXoK/Mw4K1/JuXwRCZ8N16VR7iT23nlCc9XrveFVh/E5Rqs2StT0V8v9LdA==} '@pdf-lib/standard-fonts@1.0.0': resolution: {integrity: sha512-hU30BK9IUN/su0Mn9VdlVKsWBS6GyhVfqjwl1FjZN4TxP6cCw0jP2w7V3Hf5uX7M0AZJ16vey9yE0ny7Sa59ZA==} @@ -780,92 +780,98 @@ packages: '@poppinss/exception@1.2.3': resolution: {integrity: sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw==} - '@rolldown/binding-android-arm64@1.2.2': - resolution: {integrity: sha512-l7x215OGvo1s52JWmR8U/DAVzEDWBCIbTm28aeJV/WDTSHgcKXaZTuBT0hJMs5NggilfJTW3clZVvd24yfKJxA==} + '@rolldown/binding-android-arm-eabi@1.2.8': + resolution: {integrity: sha512-tN5aztYkKCte4i5SIrrz5yK/HMjEuCqCSCJa418jOV8tZ1cBY3YF2otxB1ktPxzsLA1BeTqwapK0bfjxNvHJVw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [android] + + '@rolldown/binding-android-arm64@1.2.8': + resolution: {integrity: sha512-dIYTWl9XprMUiQFoc55KUyk/oS8SKYH3zFl0LTR7RT0Xj4hgSVyuJcroH8JUu8RcpF8fTB6E0aOwCkZoYPcDSQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@rolldown/binding-darwin-arm64@1.2.2': - resolution: {integrity: sha512-9u9Xv6c1AJZT0FfwH5vrMG5Jjcwhc1MlyrPu0XfTqkzsmqfks2M6W/o5XwAJgVVN/jHpqqngC1WevHKKTIUtIA==} + '@rolldown/binding-darwin-arm64@1.2.8': + resolution: {integrity: sha512-PCSDQGXD2IyTEFrcgPyBM8jJuGmrbCMuoIOXdbEGVemruKACXoLQJrb+A45Z0L5t1RQkdfJprAYPkikbh7dzdA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@rolldown/binding-darwin-x64@1.2.2': - resolution: {integrity: sha512-9W1mbGZAfW3oqd85bhBkmpyHCCzL1TeG/zFFP3vg7b0rlly8cxOcre5nXwz+LHazCwac2MNWgPdPCHndABjpWQ==} + '@rolldown/binding-darwin-x64@1.2.8': + resolution: {integrity: sha512-Uk7lRsGhPFHVX/sAUC6D5H9Ol30dFHd6iquokll2th3LpdJ3F5CzQB+7DHn0Ri2mG+U7k2zXiPHDrwZenXhwSA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@rolldown/binding-freebsd-x64@1.2.2': - resolution: {integrity: sha512-0p1lhiCSCyaerFwtrdZQUx7NqGk6LQnaRKWX7tFQqwQgvX0rjM15cIkm3pax1UpEakK14C4mOxx/jSqCBdBRqQ==} + '@rolldown/binding-freebsd-x64@1.2.8': + resolution: {integrity: sha512-DjszaTEVogPqA5bYzsEeqDCQxbcp2fexQwKcRspYji2yzR68fCf+e4fx6kBSRDwX5/brZaHw/hWS9+A/+/w9sQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@rolldown/binding-linux-arm-gnueabihf@1.2.2': - resolution: {integrity: sha512-e+cOJXrJ2L3zx6YzqPg+f6Wbk3V1cKB8bOhbaYdVYN3DdquzNdRAmrbETz1qnt5yp/c7JNlNjmITiA2cVneQ7w==} + '@rolldown/binding-linux-arm-gnueabihf@1.2.8': + resolution: {integrity: sha512-zmwa7FTmdzB6aaEEuuls18H6Ap5JmJPSoPTuXixeJZV6tG40SyLkApQtz1g8ptZtiEKqj9OM0oNLPh1AgvE31Q==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@rolldown/binding-linux-arm64-gnu@1.2.2': - resolution: {integrity: sha512-JsSMsj6sNat/MuhG5fnBD7QgbtpHKVe30x5/bAVirDHdhoQRXJkF6xc0Jqk8O4fiCUQAzMOoH9wZi3m60c8wtg==} + '@rolldown/binding-linux-arm64-gnu@1.2.8': + resolution: {integrity: sha512-KdYQDPHwJVnbFwdTGMgxsI9SqblBlz6STGM+w1We/d5B8OWWidYH0MwkU/uA1wM5fIpO2MkOVxXrNzzuZhw9ew==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-arm64-musl@1.2.2': - resolution: {integrity: sha512-B5G/zJdHaoJn9vD50eGHWkiWfmq8Uhi3IiLPJTzmZTrAalk1bztUikSXo0qga18ibE0IXboyeMUnhPjhAJ45wQ==} + '@rolldown/binding-linux-arm64-musl@1.2.8': + resolution: {integrity: sha512-jFJTifHnNPY+yzOoNZQfSIysrVyXzEQPhPnOUjmD1bcQGHH6s7c8cViKWar8YplQImE5N9JRqMCLrM2CdxOrZA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [musl] - '@rolldown/binding-linux-ppc64-gnu@1.2.2': - resolution: {integrity: sha512-6mC/awzKka8W6EoekjegpfGkjz8jXWDX63pqu/HYVpyKtZfu65Jsh4QAH3Kej3CAv/c1oGX7psTmFEbr0mDxLA==} + '@rolldown/binding-linux-ppc64-gnu@1.2.8': + resolution: {integrity: sha512-FhiOziBDWPBjbcmRzfLyIJnaP7AVMFXT7YCXPjXxj7wKU3vx24RjrCNN/zjvVa+N2vVoHJwCoUBvsrN/DG3zIA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-s390x-gnu@1.2.2': - resolution: {integrity: sha512-412MX9fJLdA1IK28EZnc8jYv2HRTleOZgfLQumJ5zy7OeJLZlg/CETwFaXjNmGVxG51cFHpKLqb5LKvBC+HsHA==} + '@rolldown/binding-linux-s390x-gnu@1.2.8': + resolution: {integrity: sha512-WnHfADMzOV2Y55wlx1hzzQnar/wDt/VdvWSD99r18Mz9ylNieIGOkRx3UV21h7m/eJvjySYJkO26VvGNFkwsIQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] libc: [glibc] - '@rolldown/binding-linux-x64-gnu@1.2.2': - resolution: {integrity: sha512-Q/+HI/ToJafZ1iCqGgVQXUEkIjufHCTF0gBQ2a5o3cg7GJ2h0qyq3nvvSmU+bGda2/7ygXpTY4TM6gO9OhQ0ZA==} + '@rolldown/binding-linux-x64-gnu@1.2.8': + resolution: {integrity: sha512-H9tRr5ibfXFVLxbPOseVewewFpl28zcEdjRDt2FTUZU7odxP0gEv1ki4/kGmcGOh78oRwZuuQllGLZ9zTJp84g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-x64-musl@1.2.2': - resolution: {integrity: sha512-ZKp/w41n6wCvxzxQHtQSbuphfX3Y4cCvbjkKHusrLx4lh+JWLTU7StSltO/DKARISzbj368d+qUaCjI8K2wzXw==} + '@rolldown/binding-linux-x64-musl@1.2.8': + resolution: {integrity: sha512-UefiqfM3D6IVNlZ8tSGs9+Ejjud2T+oxO0IHADU45Y+lyEjD2dVFyZHbkfX0LUb5Zugo/oIv1eCO/KVYhgYJYA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [musl] - '@rolldown/binding-openharmony-arm64@1.2.2': - resolution: {integrity: sha512-pxE6xD4KS3eAROkKK5yrhB9/3+vhlhVGMvlQLbdpzrBGDbKrnzx3RLwPaHvasLo6jgaiBLn7e04Df9C4tYhjmA==} + '@rolldown/binding-openharmony-arm64@1.2.8': + resolution: {integrity: sha512-637Ke4kWSy6rp9cxQ9gMOXlxPgIw/c1beASV4M//3+9I4uwBVOOl74G+e3zyU3u19U7RkRl/HuewixZ/Z6+Rjg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] - '@rolldown/binding-win32-arm64-msvc@1.2.2': - resolution: {integrity: sha512-4MqEue5re+xIZzAWsB8sj0P1kqZySWqIuN4t6QaIO/YA6SFwySOLruvWQFzfmqk8LBK2P30KCSJwf6mJCZZ5/A==} + '@rolldown/binding-win32-arm64-msvc@1.2.8': + resolution: {integrity: sha512-xWBkPOF1Q9k/Gv1nQXnVdLxKu74jXppuOM4Z3mnypVUJJJwLsMl7hNJGRAUJoG8A5MgOI1ACKM+wBFxSJzKy4A==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@rolldown/binding-win32-x64-msvc@1.2.2': - resolution: {integrity: sha512-NweNxxD0Nf9t8v7kodun45Ijp3EIwYY+uydPP6qBEYvfBqhIjN6dZMzlQja3tqX/aLs3F3Uz+AxDpKgRhpOZQg==} + '@rolldown/binding-win32-x64-msvc@1.2.8': + resolution: {integrity: sha512-uz2ZvfgXbxqNwijjjbxrnvALwpyODDcgc1T1N8N3rf/DXKQmaFwmB4LX4yyjggpwN2obdQLb2rgirX5ffCWYng==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] @@ -1706,8 +1712,8 @@ packages: resolution: {integrity: sha512-LarFH0+6VfriEhqMMcLX2F7SwSXeWwnEAJEsYm5QKWchiVYVvJyV9v7UDvUv+w5HO23ZpQTXDv/GxdDdMyOuoQ==} engines: {node: '>= 6.13.0'} - nodemailer@9.0.3: - resolution: {integrity: sha512-n+YP+NKwR5zRWa60k3GiQ6Q3B4KXCoAw40dAKeCtYn020iNN74aWK2liXIC3ZEATeGql7we3tE3t8QwhY0eskw==} + nodemailer@9.1.1: + resolution: {integrity: sha512-izw9mVKFix6YSnC9eLgV6g1opl9DUlRio9ZNcq+Wu9Ujn2UwF+8Nl0B8nz22kEC+CTZCvinkxwJ0DeFbb6NwcQ==} engines: {node: '>=6.0.0'} obug@2.1.4: @@ -1764,6 +1770,10 @@ packages: resolution: {integrity: sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==} engines: {node: '>=12'} + picomatch@4.0.7: + resolution: {integrity: sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==} + engines: {node: '>=12'} + pino-abstract-transport@3.0.0: resolution: {integrity: sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==} @@ -1786,6 +1796,10 @@ packages: resolution: {integrity: sha512-DTPx3RWSSnWyzLxQnlH0rJP+EW5ekl16ZU4/psbIhA0e53kJfdgaN5vKM+xP7yJtXVu+nfdVFmlgFDEKAe4Pyw==} engines: {node: ^10 || ^12 || >=14} + postcss@8.5.28: + resolution: {integrity: sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==} + engines: {node: ^10 || ^12 || >=14} + postgres@3.4.9: resolution: {integrity: sha512-GD3qdB0x1z9xgFI6cdRD6xu2Sp2WCOEoe3mtnyB5Ee0XrrL5Pe+e4CCnJrRMnL1zYtRDZmQQVbvOttLnKDLnaw==} engines: {node: '>=12'} @@ -1850,8 +1864,8 @@ packages: retimer@3.0.0: resolution: {integrity: sha512-WKE0j11Pa0ZJI5YIk0nflGI7SQsfl2ljihVy7ogh7DeQSeYAUi0ubZ/yEueGtDfUPk6GH5LRw1hBdLq4IwUBWA==} - rolldown@1.2.2: - resolution: {integrity: sha512-opwpo1tQBAcpSUJDt94B7hhLNGOKjCdE//XXjeLrnx9b83bjnw45tXdg1b09yEw/VLFBJGZpwRULMmOZo7ol+A==} + rolldown@1.2.8: + resolution: {integrity: sha512-Z67nTmhZe7anqnM/EjI392w5i/ANUinjip7QYsOyN37oayduxt3ksdX0hf5OOamkAd53BiIHfbfSzfUmzKFQqQ==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true @@ -2539,7 +2553,7 @@ snapshots: '@next/swc-win32-x64-msvc@16.2.12': optional: true - '@oxc-project/types@0.142.0': {} + '@oxc-project/types@0.149.0': {} '@pdf-lib/standard-fonts@1.0.0': dependencies: @@ -2563,46 +2577,49 @@ snapshots: '@poppinss/exception@1.2.3': {} - '@rolldown/binding-android-arm64@1.2.2': + '@rolldown/binding-android-arm-eabi@1.2.8': + optional: true + + '@rolldown/binding-android-arm64@1.2.8': optional: true - '@rolldown/binding-darwin-arm64@1.2.2': + '@rolldown/binding-darwin-arm64@1.2.8': optional: true - '@rolldown/binding-darwin-x64@1.2.2': + '@rolldown/binding-darwin-x64@1.2.8': optional: true - '@rolldown/binding-freebsd-x64@1.2.2': + '@rolldown/binding-freebsd-x64@1.2.8': optional: true - '@rolldown/binding-linux-arm-gnueabihf@1.2.2': + '@rolldown/binding-linux-arm-gnueabihf@1.2.8': optional: true - '@rolldown/binding-linux-arm64-gnu@1.2.2': + '@rolldown/binding-linux-arm64-gnu@1.2.8': optional: true - '@rolldown/binding-linux-arm64-musl@1.2.2': + '@rolldown/binding-linux-arm64-musl@1.2.8': optional: true - '@rolldown/binding-linux-ppc64-gnu@1.2.2': + '@rolldown/binding-linux-ppc64-gnu@1.2.8': optional: true - '@rolldown/binding-linux-s390x-gnu@1.2.2': + '@rolldown/binding-linux-s390x-gnu@1.2.8': optional: true - '@rolldown/binding-linux-x64-gnu@1.2.2': + '@rolldown/binding-linux-x64-gnu@1.2.8': optional: true - '@rolldown/binding-linux-x64-musl@1.2.2': + '@rolldown/binding-linux-x64-musl@1.2.8': optional: true - '@rolldown/binding-openharmony-arm64@1.2.2': + '@rolldown/binding-openharmony-arm64@1.2.8': optional: true - '@rolldown/binding-win32-arm64-msvc@1.2.2': + '@rolldown/binding-win32-arm64-msvc@1.2.8': optional: true - '@rolldown/binding-win32-x64-msvc@1.2.2': + '@rolldown/binding-win32-x64-msvc@1.2.8': optional: true '@rolldown/pluginutils@1.0.1': {} @@ -3308,7 +3325,7 @@ snapshots: node-forge@1.4.0: {} - nodemailer@9.0.3: {} + nodemailer@9.1.1: {} obug@2.1.4: {} @@ -3356,6 +3373,8 @@ snapshots: picomatch@4.0.5: {} + picomatch@4.0.7: {} + pino-abstract-transport@3.0.0: dependencies: split2: 4.2.0 @@ -3400,6 +3419,12 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + postcss@8.5.28: + dependencies: + nanoid: 3.3.18 + picocolors: 1.1.1 + source-map-js: 1.2.1 + postgres@3.4.9: {} pretty-bytes@5.6.0: {} @@ -3454,25 +3479,26 @@ snapshots: retimer@3.0.0: {} - rolldown@1.2.2: + rolldown@1.2.8: dependencies: - '@oxc-project/types': 0.142.0 + '@oxc-project/types': 0.149.0 '@rolldown/pluginutils': 1.0.1 optionalDependencies: - '@rolldown/binding-android-arm64': 1.2.2 - '@rolldown/binding-darwin-arm64': 1.2.2 - '@rolldown/binding-darwin-x64': 1.2.2 - '@rolldown/binding-freebsd-x64': 1.2.2 - '@rolldown/binding-linux-arm-gnueabihf': 1.2.2 - '@rolldown/binding-linux-arm64-gnu': 1.2.2 - '@rolldown/binding-linux-arm64-musl': 1.2.2 - '@rolldown/binding-linux-ppc64-gnu': 1.2.2 - '@rolldown/binding-linux-s390x-gnu': 1.2.2 - '@rolldown/binding-linux-x64-gnu': 1.2.2 - '@rolldown/binding-linux-x64-musl': 1.2.2 - '@rolldown/binding-openharmony-arm64': 1.2.2 - '@rolldown/binding-win32-arm64-msvc': 1.2.2 - '@rolldown/binding-win32-x64-msvc': 1.2.2 + '@rolldown/binding-android-arm-eabi': 1.2.8 + '@rolldown/binding-android-arm64': 1.2.8 + '@rolldown/binding-darwin-arm64': 1.2.8 + '@rolldown/binding-darwin-x64': 1.2.8 + '@rolldown/binding-freebsd-x64': 1.2.8 + '@rolldown/binding-linux-arm-gnueabihf': 1.2.8 + '@rolldown/binding-linux-arm64-gnu': 1.2.8 + '@rolldown/binding-linux-arm64-musl': 1.2.8 + '@rolldown/binding-linux-ppc64-gnu': 1.2.8 + '@rolldown/binding-linux-s390x-gnu': 1.2.8 + '@rolldown/binding-linux-x64-gnu': 1.2.8 + '@rolldown/binding-linux-x64-musl': 1.2.8 + '@rolldown/binding-openharmony-arm64': 1.2.8 + '@rolldown/binding-win32-arm64-msvc': 1.2.8 + '@rolldown/binding-win32-x64-msvc': 1.2.8 safe-buffer@5.1.2: {} @@ -3629,9 +3655,9 @@ snapshots: vite@8.2.0(@types/node@26.1.2)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.1): dependencies: lightningcss: 1.33.0 - picomatch: 4.0.5 - postcss: 8.5.25 - rolldown: 1.2.2 + picomatch: 4.0.7 + postcss: 8.5.28 + rolldown: 1.2.8 tinyglobby: 0.2.17 optionalDependencies: '@types/node': 26.1.2 From 44482ce92207986f8468a552af88d8bcfca3a906 Mon Sep 17 00:00:00 2001 From: adamXbot <111877622+adamXbot@users.noreply.github.com> Date: Wed, 23 Sep 2026 23:01:04 +1000 Subject: [PATCH 2/4] Fix remaining CLI and dashboard experience issues --- cli/src/commands/hits.ts | 20 ++-- cli/src/commands/new.ts | 8 +- cli/src/commands/watch.ts | 13 +-- cli/src/lib/api.ts | 7 +- cli/src/lib/hit-anchor.ts | 24 +++++ cli/tests/hit-anchor.test.ts | 34 +++++++ cli/tests/new-installer.test.ts | 41 ++++++++ src/app/(app)/keys/new/actions.ts | 16 ++-- src/app/(app)/keys/new/form.tsx | 20 ++-- src/app/api/hits/recent/route.ts | 7 ++ src/lib/notify/destinations.ts | 93 ++++++++++--------- tests/integration/experience-recovery.test.ts | 45 ++++++++- tests/new-key-form.test.ts | 19 ++++ 13 files changed, 266 insertions(+), 81 deletions(-) create mode 100644 cli/src/lib/hit-anchor.ts create mode 100644 cli/tests/hit-anchor.test.ts create mode 100644 tests/new-key-form.test.ts diff --git a/cli/src/commands/hits.ts b/cli/src/commands/hits.ts index c47375e..f244470 100644 --- a/cli/src/commands/hits.ts +++ b/cli/src/commands/hits.ts @@ -9,6 +9,7 @@ import { truncate, } from "../lib/out.js"; import { parseIntervalMs, parseLimit } from "../lib/parse.js"; +import { primeHitAnchor } from "../lib/hit-anchor.js"; import { resolveKeyRef } from "../lib/resolve.js"; import { withClient, type GlobalOpts } from "../lib/runner.js"; @@ -86,15 +87,14 @@ async function followHits( const seen = new Set(); const seenOrder: string[] = []; - // Anchor at start time. A one-millisecond overlap covers hits sharing a - // timestamp; the ID set prevents reprinting them on the next poll. - let watermarkMs = Date.now(); - const initial = await client.listRecentHits({ key_id: id, limit: 500 }); - for (const h of initial.data) { - if (new Date(h.occurred_at).getTime() < watermarkMs) { - seen.add(h.id); - seenOrder.push(h.id); - } + // A one-millisecond overlap covers hits sharing the anchor timestamp; IDs + // prevent reprinting them on the next poll. + const initial = await client.listRecentHits({ key_id: id, limit: 500, anchor: 1 }); + const anchor = primeHitAnchor(initial); + let watermarkMs = anchor.watermarkMs; + for (const id of anchor.seenIds) { + seen.add(id); + seenOrder.push(id); } process.stderr.write( @@ -111,7 +111,7 @@ async function followHits( await new Promise((r) => setTimeout(r, intervalMs)); if (stop) break; try { - const since = new Date(watermarkMs - 1).toISOString(); + const since = new Date(Math.max(0, watermarkMs - 1)).toISOString(); const arrived: Hit[] = []; let cursor: string | undefined; do { diff --git a/cli/src/commands/new.ts b/cli/src/commands/new.ts index 584fcde..9f23263 100644 --- a/cli/src/commands/new.ts +++ b/cli/src/commands/new.ts @@ -181,6 +181,9 @@ export async function newCmd( `unknown installer type "${effectiveOpts.install}". Available: ${ALL_INSTALL_TYPES.join(", ")}`, ); } + if (effectiveOpts.install && !effectiveOpts.out && (effectiveOpts.idOnly || effectiveOpts.urlOnly)) { + fail("--install needs --out when used with --id-only or --url-only; otherwise the snippet has nowhere to go"); + } await withClient(effectiveOpts, async (client) => { const key = await client.createKey({ @@ -326,7 +329,10 @@ export async function newCmd( ); } catch (err) { const reason = err instanceof Error ? err.message : String(err); - throw new Error(`key ${key.id} was created, but setup did not finish: ${reason}. Resume with \`mantis show ${key.id}\`.`); + const recovery = effectiveOpts.install + ? `Finish the installer with \`mantis install ${key.id} --type ${effectiveOpts.install}\`.` + : `Inspect the key with \`mantis show ${key.id}\`.`; + throw new Error(`key ${key.id} was created, but setup did not finish: ${reason}. ${recovery}`); } }); } diff --git a/cli/src/commands/watch.ts b/cli/src/commands/watch.ts index 9de6fcd..8a0a251 100644 --- a/cli/src/commands/watch.ts +++ b/cli/src/commands/watch.ts @@ -1,6 +1,7 @@ import type { MantisClient, RecentHit } from "../lib/api.js"; import { c, formatTime, isJsonMode } from "../lib/out.js"; import { parseIntervalMs } from "../lib/parse.js"; +import { primeHitAnchor } from "../lib/hit-anchor.js"; import { resolveKeyRef } from "../lib/resolve.js"; import { withClient, type GlobalOpts } from "../lib/runner.js"; @@ -19,14 +20,14 @@ export async function watchCmd(opts: WatchOpts): Promise { ); const seen = new Set(); - let since = oneSecondAgo(); - const prime = await client.listRecentHits({ ...(keyId ? { key_id: keyId } : {}), limit: 500, + anchor: 1, }); - for (const hit of prime.data) seen.add(hit.id); - since = backUpOneMs(newestOccurredAt(prime.data) ?? since); + const anchor = primeHitAnchor(prime); + for (const id of anchor.seenIds) seen.add(id); + let since = new Date(Math.max(0, anchor.watermarkMs - 1)).toISOString(); const tick = async () => { try { @@ -108,7 +109,3 @@ function backUpOneMs(iso: string): string { if (Number.isNaN(t)) return iso; return new Date(t - 1).toISOString(); } - -function oneSecondAgo(): string { - return new Date(Date.now() - 1000).toISOString(); -} diff --git a/cli/src/lib/api.ts b/cli/src/lib/api.ts index eb02b37..86007d2 100644 --- a/cli/src/lib/api.ts +++ b/cli/src/lib/api.ts @@ -137,6 +137,7 @@ export type AuditEvent = { }; export type Page = { data: T[]; next_cursor: string | null }; +export type RecentHitsPage = Page & { server_time?: string }; export type Health = { status: "ok" | "degraded"; @@ -400,9 +401,9 @@ export class MantisClient { } listRecentHits( - query: { limit?: number; since?: string; since_id?: string; cursor?: string; key_id?: string } = {}, - ): Promise> { - return this.req>("/api/hits/recent", { query }); + query: { limit?: number; since?: string; since_id?: string; cursor?: string; key_id?: string; anchor?: number } = {}, + ): Promise { + return this.req("/api/hits/recent", { query }); } async fetchInstaller( diff --git a/cli/src/lib/hit-anchor.ts b/cli/src/lib/hit-anchor.ts new file mode 100644 index 0000000..2f9198f --- /dev/null +++ b/cli/src/lib/hit-anchor.ts @@ -0,0 +1,24 @@ +import type { RecentHitsPage } from "./api.js"; + +/** Seed a live stream from database time, never from the operator's clock. */ +export function primeHitAnchor(page: RecentHitsPage): { + watermarkMs: number; + seenIds: string[]; +} { + const serverMs = Date.parse(page.server_time ?? ""); + if (Number.isFinite(serverMs)) { + return { + watermarkMs: serverMs, + seenIds: page.data + .filter((hit) => Date.parse(hit.occurred_at) < serverMs) + .map((hit) => hit.id), + }; + } + + // Older servers do not return server_time. Anchor at their newest hit, or + // the epoch for an empty feed, so local clock skew still cannot skip hits. + return { + watermarkMs: Math.max(0, ...page.data.map((hit) => Date.parse(hit.occurred_at))), + seenIds: page.data.map((hit) => hit.id), + }; +} diff --git a/cli/tests/hit-anchor.test.ts b/cli/tests/hit-anchor.test.ts new file mode 100644 index 0000000..ca2ee88 --- /dev/null +++ b/cli/tests/hit-anchor.test.ts @@ -0,0 +1,34 @@ +import { describe, expect, it } from "vitest"; +import type { RecentHitsPage } from "../src/lib/api.js"; +import { primeHitAnchor } from "../src/lib/hit-anchor.js"; + +function page(serverTime?: string): RecentHitsPage { + return { + data: [ + { id: "older", occurred_at: "2026-09-23T10:00:00.000Z" }, + { id: "during-prime", occurred_at: "2026-09-23T10:00:01.000Z" }, + ] as RecentHitsPage["data"], + next_cursor: null, + ...(serverTime ? { server_time: serverTime } : {}), + }; +} + +describe("live hit anchor", () => { + it("uses server time and leaves hits arriving during priming unseen", () => { + expect(primeHitAnchor(page("2026-09-23T10:00:00.500Z"))).toEqual({ + watermarkMs: Date.parse("2026-09-23T10:00:00.500Z"), + seenIds: ["older"], + }); + }); + + it("uses existing hits when an older server omits the anchor", () => { + expect(primeHitAnchor(page())).toEqual({ + watermarkMs: Date.parse("2026-09-23T10:00:01.000Z"), + seenIds: ["older", "during-prime"], + }); + expect(primeHitAnchor({ data: [], next_cursor: null })).toEqual({ + watermarkMs: 0, + seenIds: [], + }); + }); +}); diff --git a/cli/tests/new-installer.test.ts b/cli/tests/new-installer.test.ts index 1ee0d0d..41224e7 100644 --- a/cli/tests/new-installer.test.ts +++ b/cli/tests/new-installer.test.ts @@ -9,6 +9,7 @@ vi.mock("../src/commands/install.js", () => ({ })); import { newCmd } from "../src/commands/new.js"; +import { runInstaller } from "../src/commands/install.js"; afterEach(() => { vi.restoreAllMocks(); @@ -38,4 +39,44 @@ describe("create and install", () => { expect(output.join("")).toContain("curl https://mantis.example.com/c/abc123\n"); }); + + it("rejects output modes that would discard a generated snippet before creating a key", async () => { + const fetch = vi.fn(); + vi.stubGlobal("fetch", fetch); + vi.spyOn(process, "exit").mockImplementation(() => { throw new Error("exited"); }); + vi.spyOn(process.stderr, "write").mockImplementation(() => true); + + await expect(newCmd("first key", { + baseUrl: "https://mantis.example.com", + key: "test-key", + install: "shell", + idOnly: true, + })).rejects.toThrow("exited"); + expect(fetch).not.toHaveBeenCalled(); + expect(vi.mocked(process.stderr.write).mock.calls.join(" ")).toContain("--install needs --out"); + }); + + it("gives a runnable recovery command after an installer fails", async () => { + vi.mocked(runInstaller).mockRejectedValueOnce(new Error("write failed")); + const errors: string[] = []; + vi.spyOn(process.stderr, "write").mockImplementation((chunk) => { + errors.push(String(chunk)); + return true; + }); + vi.spyOn(process, "exit").mockImplementation(() => { throw new Error("exited"); }); + vi.stubGlobal("fetch", async () => new Response(JSON.stringify({ + id: "00000000-0000-4000-8000-000000000001", + public_id: "abc123", + url: "https://mantis.example.com/c/abc123", + memo: "first key", + destinations: [], + }), { status: 201, headers: { "content-type": "application/json" } })); + + await expect(newCmd("first key", { + baseUrl: "https://mantis.example.com", + key: "test-key", + install: "shell", + })).rejects.toThrow("exited"); + expect(errors.join(" ")).toContain("mantis install 00000000-0000-4000-8000-000000000001 --type shell"); + }); }); diff --git a/src/app/(app)/keys/new/actions.ts b/src/app/(app)/keys/new/actions.ts index 6c317fb..c38f570 100644 --- a/src/app/(app)/keys/new/actions.ts +++ b/src/app/(app)/keys/new/actions.ts @@ -9,7 +9,7 @@ import { createKeyWithDestinations, type DestinationInput, } from "@/lib/notify/destinations"; -import { isPresetId } from "@/lib/presets"; +import { getPreset, isPresetId } from "@/lib/presets"; export type CreateState = { error?: string; @@ -39,10 +39,13 @@ export async function createKeyAction( if (!memo) return { error: "memo is required" }; if (memo.length > 500) return { error: "memo too long (max 500)" }; - const responseKindRaw = String(formData.get("response_kind") ?? "gif"); - const responseKind = (VALID_KINDS as string[]).includes(responseKindRaw) - ? (responseKindRaw as ResponseKind) - : "gif"; + const presetRaw = String(formData.get("preset") ?? ""); + const preset = getPreset(isPresetId(presetRaw) ? presetRaw : null); + const responseKindRaw = String(formData.get("response_kind") ?? preset.responseKind); + if (!(VALID_KINDS as string[]).includes(responseKindRaw)) { + return { error: "invalid trigger response" }; + } + const responseKind = responseKindRaw as ResponseKind; let responsePayload: unknown = null; if (responseKind === "redirect") { @@ -97,7 +100,7 @@ export async function createKeyAction( destinations.push({ channel, target }); } - const dedupRaw = String(formData.get("dedupe_window_seconds") ?? "60"); + const dedupRaw = String(formData.get("dedupe_window_seconds") ?? preset.dedupeWindowSeconds); const dedupeWindowSeconds = Number.parseInt(dedupRaw, 10); if (!Number.isFinite(dedupeWindowSeconds) || dedupeWindowSeconds < 0 || dedupeWindowSeconds > 86_400) { return { error: "dedupe window must be 0–86400 seconds" }; @@ -117,7 +120,6 @@ export async function createKeyAction( // Carry the preset through so the key page can surface the matching // download format and deployment hint instead of a generic format list. - const presetRaw = String(formData.get("preset") ?? ""); const presetQuery = isPresetId(presetRaw) ? `?preset=${presetRaw}` : ""; redirect(`/keys/${row.id}${presetQuery}`); } diff --git a/src/app/(app)/keys/new/form.tsx b/src/app/(app)/keys/new/form.tsx index 5cb4a09..ecd6bf8 100644 --- a/src/app/(app)/keys/new/form.tsx +++ b/src/app/(app)/keys/new/form.tsx @@ -40,6 +40,9 @@ export function NewKeyForm({ ); const [touched, setTouched] = useState({ kind: false, dedupe: false }); const [showAdvanced, setShowAdvanced] = useState(false); + const [redirectUrl, setRedirectUrl] = useState(""); + const [htmlBody, setHtmlBody] = useState(""); + const [jsonBody, setJsonBody] = useState(""); const [destinations, setDestinations] = useState([]); const choosePreset = (id: string) => { @@ -60,6 +63,11 @@ export function NewKeyForm({ return (
+ + + + +

A mantis key is a tripwire. Pick what you're planting and mantis fills in sensible defaults — you can change any of them. @@ -106,7 +114,6 @@ export function NewKeyForm({ required maxLength={500} defaultValue={defaultMemo} - key={preset.id} // re-render placeholder when the preset changes placeholder={preset.memoExample || "e.g. honeypot doc in /finance"} className={`${inputBase} w-full`} /> @@ -140,7 +147,6 @@ export function NewKeyForm({ hint="What the mantis URL returns when fetched. The preset picks the least conspicuous option for that medium." > setRedirectUrl(e.target.value)} placeholder="https://example.com" className={`${inputBase} w-full`} /> @@ -170,7 +177,8 @@ export function NewKeyForm({ {kind === "html" && (