{"_id":"@camunda/session-heartbeat","_rev":"2-27d02c9e014f51f1edb16163fa4c25ac","name":"@camunda/session-heartbeat","dist-tags":{"latest":"0.0.1"},"versions":{"0.0.1":{"name":"@camunda/session-heartbeat","version":"0.0.1","keywords":["camunda","session","heartbeat"],"license":"LicenseRef-Camunda-1.0","_id":"@camunda/session-heartbeat@0.0.1","maintainers":[{"name":"nikku","email":"git_nikku@nixis.de"},{"name":"barmac","email":"maciejbarel@gmail.com"},{"name":"maxtru","email":"maximilian.trumpf@camunda.com"},{"name":"philippfromme","email":"philippfromme@outlook.com"},{"name":"marstamm","email":"martin.stamm@camunda.com"},{"name":"camunda_it","email":"it-tools@camunda.com"},{"name":"omranabazid","email":"omran.1994@gmail.com"},{"name":"husnauygur","email":"husna.uygur@camunda.com"},{"name":"skaiir-camunda","email":"valentin.serra@camunda.com"},{"name":"vsgoulart","email":"vinicius@vsgoulart.com"},{"name":"yhaskell","email":"dultsev.igor@gmail.com"},{"name":"jarekdanielak","email":"jarek.danielak@camunda.com"},{"name":"francesco.camunda","email":"francesco.esposito@camunda.com"},{"name":"urbanisierung","email":"adam.urban@gmail.com"},{"name":"jroquescamunda","email":"jonathan.roques@camunda.com"},{"name":"ev-camunda","email":"ev.bilske@camunda.com"},{"name":"ztefanie","email":"stefanie.metzger@camunda.com"},{"name":"emoloney","email":"eamonn.moloney@camunda.com"},{"name":"marcello.barile-camunda","email":"marcello.barile@camunda.com"}],"homepage":"https://github.com/camunda/camunda#readme","bugs":{"url":"https://github.com/camunda/camunda/issues"},"dist":{"shasum":"8f1bec5ac761847b19dca761113cc9854f10781b","tarball":"https://registry.npmjs.org/@camunda/session-heartbeat/-/session-heartbeat-0.0.1.tgz","fileCount":8,"integrity":"sha512-OnRYdSOKlzN0raGnyR3WNxAFYrHEJL3l+7+Nj55StkDbG5SmioDxYL549UQdmL7El0QUqyrDGwa//h9rHiz+GQ==","signatures":[{"sig":"MEUCIDnBfszeU5TKU9QAbX9GeHCsm1miPKejN36vCHvo9rIhAiEAqn/WtPuMNhT2DAZ6n50MaPlX3JQUxWnnW/iqI3XIH/I=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":12653},"type":"module","exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"}},"./react":{"import":{"types":"./dist/react.d.ts","default":"./dist/react.js"}},"./package.json":"./package.json"},"gitHead":"f86088c5105cd01d30d2e3eb15b5d474be23ca10","private":false,"scripts":{"build":"vite build","prepare":"npm run build","test:unit":"vitest","typecheck":"tsc"},"_npmUser":{"name":"vsgoulart","email":"vinicius@vsgoulart.com"},"repository":{"url":"git+https://github.com/camunda/camunda.git","type":"git"},"_npmVersion":"11.16.0","description":"Activity-driven session heartbeat for Camunda webapps backed by the Camunda security library","directories":{},"sideEffects":false,"_nodeVersion":"24.18.1","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"packageManager":"npm@12.0.2","devDependencies":{"vite":"8.2.1","react":"19.2.8","vitest":"4.1.10","react-dom":"19.2.8","typescript":"7.0.2","@types/react":"19.2.18","vite-plugin-dts":"5.0.3","@playwright/test":"1.62.1","@types/react-dom":"19.2.4","vitest-browser-react":"2.2.0","@vitest/browser-playwright":"4.1.10"},"peerDependencies":{"react":"^18.0.0 || ^19.0.0"},"peerDependenciesMeta":{"react":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/session-heartbeat_0.0.1_1786658056829_0.8616354086797033","host":"s3://npm-registry-packages-npm-production"}}},"time":{"created":"2026-08-13T21:54:16.623Z","modified":"2026-09-29T13:38:06.286Z","0.0.1":"2026-08-13T21:54:17.018Z"},"bugs":{"url":"https://github.com/camunda/camunda/issues"},"license":"LicenseRef-Camunda-1.0","homepage":"https://github.com/camunda/camunda#readme","keywords":["camunda","session","heartbeat"],"repository":{"url":"git+https://github.com/camunda/camunda.git","type":"git"},"description":"Activity-driven session heartbeat for Camunda webapps backed by the Camunda security library","maintainers":[{"email":"git_nikku@nixis.de","name":"nikku"},{"email":"maciejbarel@gmail.com","name":"barmac"},{"email":"maximilian.trumpf@camunda.com","name":"maxtru"},{"email":"philippfromme@outlook.com","name":"philippfromme"},{"email":"martin.stamm@camunda.com","name":"marstamm"},{"email":"it-tools@camunda.com","name":"camunda_it"},{"email":"omran.1994@gmail.com","name":"omranabazid"},{"email":"husna.uygur@camunda.com","name":"husnauygur"},{"email":"valentin.serra@camunda.com","name":"skaiir-camunda"},{"email":"vinicius@vsgoulart.com","name":"vsgoulart"},{"email":"dultsev.igor@gmail.com","name":"yhaskell"},{"email":"jarek.danielak@camunda.com","name":"jarekdanielak"},{"email":"francesco.esposito@camunda.com","name":"francesco.camunda"},{"email":"alekseyManetov@gmail.com","name":"alekseymanetov"},{"email":"adam.urban@gmail.com","name":"urbanisierung"},{"email":"jonathan.roques@camunda.com","name":"jroquescamunda"},{"email":"ev.bilske@camunda.com","name":"ev-camunda"},{"email":"stefanie.metzger@camunda.com","name":"ztefanie"},{"email":"eamonn.moloney@camunda.com","name":"emoloney"},{"email":"marcello.barile@camunda.com","name":"marcello.barile-camunda"}],"readme":"# @camunda/session-heartbeat\n\nActivity-driven session heartbeat for Camunda webapps whose sessions are managed by the\n[Camunda security library](https://github.com/camunda/camunda-security-library) (CSL).\n\nThe package tracks genuine browser activity — pointer, keyboard, wheel, scroll, and the tab\nregaining visibility — and calls CSL's `POST {basePath}/session/heartbeat` endpoint at most once per\ninterval, only when activity actually happened. It exists so Operate, Tasklist, Optimize, and any\nfuture adopter share one implementation instead of each reimplementing the same listener and\nthrottle logic.\n\nBackground: [CSL ADR-0042 — Configurable session idle timeout driven by client activity](https://github.com/camunda/camunda-security-library/blob/main/docs/adr/0042-configurable-activity-driven-session-idle-timeout.md).\n\n## Why a heartbeat is needed\n\nWith `camunda.security.session.heartbeat.enabled=true`, CSL stops treating ordinary backend traffic\nas proof of user presence: **only** a call to the heartbeat endpoint extends the session. A host that\nenables the flag without a frontend sending heartbeats logs its users out after\n`camunda.security.session.max-inactive-interval`, however actively they are using the application.\n\nThe flag defaults to `false`, so adopting this package is safe and inert until a host turns it on.\n\n## Installation\n\n```bash\nnpm install @camunda/session-heartbeat\n```\n\n`react` is an optional peer dependency, needed only for the `./react` entry point.\n\n## Usage\n\n### React\n\n```tsx\nimport {useSessionHeartbeat} from '@camunda/session-heartbeat/react';\n\nfunction AuthenticatedLayout() {\n\tuseSessionHeartbeat({\n\t\turl: '/session/heartbeat',\n\t\tcsrfToken: () => sessionStorage.getItem('X-CSRF-TOKEN'),\n\t\tonUnauthorized: () => {\n\t\t\tauthenticationStore.disableSession();\n\t\t},\n\t});\n\n\treturn <Outlet />;\n}\n```\n\nCall it once inside the authenticated part of the application, so the heartbeat starts after login\nand stops when the user leaves it. Callbacks and `csrfToken` are read fresh on every heartbeat, so\ninline functions do not restart the timer; changing `url`, `intervalMs`, or `enabled` does.\n\n### Without React\n\n```ts\nimport {createSessionHeartbeat} from '@camunda/session-heartbeat';\n\nconst stopSessionHeartbeat = createSessionHeartbeat({url: '/session/heartbeat'});\n\n// on teardown\nstopSessionHeartbeat();\n```\n\n## Options\n\n| Option           | Type                                         | Default     | Description                                                                                                |\n| ---------------- | -------------------------------------------- | ----------- | ---------------------------------------------------------------------------------------------------------- |\n| `url`            | `string`                                     | —           | The heartbeat endpoint. Must be the current scope's `{basePath}/session/heartbeat`, context path included. |\n| `intervalMs`     | `number`                                     | `60000`     | How often activity is checked, and therefore the shortest gap between two heartbeats.                      |\n| `csrfToken`      | `string \\| null \\| undefined \\| (() => …)`   | `undefined` | CSRF token, or a getter for it. Sent as `X-CSRF-TOKEN`; omitted when absent or empty.                      |\n| `onUnauthorized` | `() => void`                                 | `undefined` | Called when a heartbeat comes back `401` — an expired session, or a missing/stale CSRF token.              |\n| `onError`        | `(failure: SessionHeartbeatFailure) => void` | `undefined` | Called for network errors and for any other non-OK response.                                               |\n| `enabled`        | `boolean` (`./react` only)                   | `true`      | Set `false` to keep the hook mounted without sending heartbeats.                                           |\n\n### Choosing `intervalMs`\n\nKeep it well under the host's `camunda.security.session.max-inactive-interval` — a heartbeat is what\nresets that clock, and one lost request must not cost the user their session. A quarter of the\nconfigured interval or less is a good rule of thumb; the `60s` default suits CSL's `30m` default.\n\n## Behavior worth knowing\n\n- **The endpoint needs a CSRF token.** CSL exempts only `/login` and `/logout` from CSRF protection,\n  so a session-bearing `POST /session/heartbeat` without a valid `X-CSRF-TOKEN` is rejected — with\n  `401`, not `403`, since the webapp chain maps the CSRF denial onto its auth-failure handler. Pass\n  `csrfToken` from wherever the application keeps it.\n- **A `401` therefore means \"this heartbeat was not accepted\", not strictly \"the session is gone\".**\n  A missing or stale token produces the same status as an expired session, so `onUnauthorized` fires\n  in both cases. That matches how a webapp's own request layer usually treats `401`; if an\n  application needs to tell the two apart, the response body carries a CSRF-specific `detail`.\n- **Starting counts as activity**, so the first interval always sends one heartbeat. This surfaces\n  broken wiring immediately, at the cost of extending an abandoned session by one interval.\n- **At most one heartbeat per interval**, and none at all for an interval with no activity. A\n  heartbeat is also skipped while a previous one is still in flight.\n- **A `401` does not stop the heartbeat.** The consumer decides what an expired session means;\n  unmounting the hook (or setting `enabled: false`) is what stops it.\n- **Each tab heartbeats independently.** They share one session, so the cost is one extra request\n  per tab per interval; there is no cross-tab coordination.\n- **Requests are credentialed** (`credentials: 'include'`) and aborted on teardown.\n\n## Development\n\n```bash\nnpm run build -w @camunda/session-heartbeat        # bundle + type declarations\nnpm run typecheck -w @camunda/session-heartbeat\nnpm run test:unit -w @camunda/session-heartbeat    # Vitest browser mode\n```\n\nSee [Session heartbeat](../../../../docs/monorepo-docs/frontend/session-heartbeat.md) in the\nmonorepo docs for the adoption checklist and publishing steps.\n","readmeFilename":"README.md"}