- What reaches the bell also reaches the devices where the person
turned «Avisos en este dispositivo» on, with TaskStream closed: the
Notifier sends each new notification encrypted (aes128gcm) and signed
with VAPID, and forgets subscriptions the push service dropped
(404/410). A failure never blocks the event stream.
- The API keeps one subscription per device (push_subscriptions) and
exposes the VAPID public key in the notification settings; GraphQL
subscribePush/unsubscribePush, REST /me/push-subscriptions. Without
VAPID_PUBLIC_KEY and VAPID_PRIVATE_KEY push is off.
- The web registers /push-sw.js, a service worker that only shows the
notification and opens its ticket on a tap (no caching, served
without cache by nginx), and a manifest that makes it installable,
which iPhones need for push.
- The bell's toggles show their text next to them.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Pireon signs each webhook with the secret of the client that registered
it, and the API has two clients (the web app's and the password
grant's). Only PIREON_WEBHOOK_SECRET was accepted, so a token revoked
for someone signed in with a password went unheard. A delivery now
verifies with PIREON_WEBHOOK_SECRET or PIREON_PASSWORD_WEBHOOK_SECRET;
the same event arriving from both clients changes nothing.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every hour the API reminds whoever has a ticket, and whoever follows it,
the day before its due date, on the day and once when it is overdue, in
the app and by e-mail for those who asked. Cards on the board flag what
is due soon or late.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Owners and admins can point a project at other systems: a signed JSON
POST (HMAC-SHA256 of the body) when tickets, comments, releases or sprints
change, filtered by event or family. Deliveries are retried a couple of
times, the last one is shown, and a ping can be sent from the settings.
Private addresses are refused unless WEBHOOKS_ALLOW_PRIVATE is set.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Derived from events and kept per person: whoever a ticket is assigned to,
whoever reports or works on it when it moves, fails QA or gets a comment,
and whoever joins a project. Never about what you did yourself, once per
event. They arrive live through a subscription to a bell in the header,
and are copied by e-mail to whoever turns it on when SMTP_URL is set.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Files are uploaded over REST, stored on disk (a volume in Docker) and
recorded on the ticket as events; the bytes are written first, so no
attachment ever points at a missing file. Only images, video, audio, PDF
and plain text are shown inline, everything with nosniff and a sandbox CSP.
The web shows thumbnails, uploads by drag and drop or by pasting a
screenshot, and attaches files to new comments.
Also adds `nx run api:contracts`, which writes schema.gql and
openapi.json without starting the API or any database.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Logs default to warnings and errors (MCP_LOG_LEVEL): the library logged
every tool on every request (~48 lines per call).
- DNS rebinding protection on /mcp, as the MCP spec asks of HTTP servers:
the Host header must be in MCP_ALLOWED_HOSTS (default: localhost; compose
default mcp.taskstream.pireon.dev,localhost) and a browser Origin, when
present, too; otherwise 403 with a JSON-RPC error. Uses the official
validators from @modelcontextprotocol/server.
- Test for allowed/foreign Host and Origin; .env.example, compose and
docs/09-mcp.md document the variables; Word/PDF regenerated.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Pireon OAuth clients allow a single grant type. With one PIREON_CLIENT_ID of
type password, email + password worked but "Sign in with Pireon" failed with
`unauthorized_client`.
- PIREON_CLIENT_ID/SECRET: authorization_code client (Sign in with Pireon,
Swagger UI). New PIREON_PASSWORD_CLIENT_ID/SECRET: password client, falling
back to the former so existing setups keep working.
- Refresh tries both clients: a refresh token only works with its issuer.
- PIREON_PASSWORD_GRANT=false now also rejects password grants in the API
(before it only hid the form).
- /api/auth/config returns clientId: null when there is no authorization_code
client, and the web hides "Sign in with Pireon".
- docker-compose passes the new variables; docs 05/07, .env.example,
openapi.json; unit tests for the client selection.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A REST adapter next to GraphQL (presenters/http) that calls the same
application services, so validation, permissions and domain errors are the
same: projects (members, board, stats, activity), issues (search, workflow,
assignee, release, links, comments, history), comments, releases and users.
Projects and issues can be addressed by id or key (WEB, WEB-12).
- Swagger UI at /api/docs, document at /api/docs-json and apps/api/openapi.json
(rewritten by `nx serve api`, like schema.gql) for client generation.
- Every route but health/auth/webhook needs a Pireon token: the document
declares Pireon OAuth2 (authorization code + PKCE, password) through the
/api/auth/token proxy, and a bearer scheme for apps with their own client.
- configureApp() shares main.ts setup with the e2e, which now also covers REST.
- SWAGGER_ENABLED / OPENAPI_FILE config; docs 03/05 and library READMEs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Dokploy builds failed with "failed to stat /root/.npmrc": the compose build
secret pointed at ${HOME}/.npmrc, which only exists on a developer machine.
The token now comes from the NEXUS_TOKEN environment variable (Dokploy's
Environment tab or .env), is written to a temporary npmrc and removed in the
same RUN, so it never lands in an image layer.
Also removes a temporary UI dev-server spec that was committed by mistake,
and documents the variable (.env.example, docs 06/07, Word/PDF export).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>