{"_id":"@beesolve/action-tokens","_rev":"7-3be559d37025b95e9657b2e98823bacd","name":"@beesolve/action-tokens","dist-tags":{"latest":"0.5.2"},"versions":{"0.1.0":{"name":"@beesolve/action-tokens","version":"0.1.0","license":"MIT","_id":"@beesolve/action-tokens@0.1.0","maintainers":[{"name":"ivanbarlog","email":"ivan@barlog.sk"}],"homepage":"https://github.com/beesolve/packages/tree/main/packages/action-tokens#readme","bugs":{"url":"https://github.com/beesolve/packages/issues"},"dist":{"shasum":"447603c08c7ac0f5a1747d92c4726800ff743450","tarball":"https://registry.npmjs.org/@beesolve/action-tokens/-/action-tokens-0.1.0.tgz","fileCount":9,"integrity":"sha512-J0+dtW+Bj3XjoFzpDjnFXz9hUqrXdWRdEsRwz4LnPera7OaNg5RhYgoVpVNxT//sjsO/S7wuGcGjrjzCy0YbXw==","signatures":[{"sig":"MEQCIEL/eZMZAbW0RL3XYEOE/6BuEyywXNmD98kQ/YmvraywAiAG546RLPcWt5Z7N70+LzcD0pU5+TB30DDabGjAfmo7YA==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":22720},"type":"module","_from":"file:packages/action-tokens/beesolve-action-tokens-0.1.0.tgz","exports":{"./cdk":{"import":{"types":"./dist/cdk.d.ts","default":"./dist/cdk.js"}},"./sdk":{"import":{"types":"./dist/sdk.d.ts","default":"./dist/sdk.js"}},"./model":{"import":{"types":"./dist/model.d.ts","default":"./dist/model.js"}},"./package.json":"./package.json"},"scripts":{"test":"bun test","type-check":"tsc --noEmit"},"_npmUser":{"name":"ivanbarlog","email":"ivan@barlog.sk"},"_resolved":"/Users/ivan/data/work/github.com/beesolve/packages/packages/action-tokens/beesolve-action-tokens-0.1.0.tgz","_integrity":"sha512-J0+dtW+Bj3XjoFzpDjnFXz9hUqrXdWRdEsRwz4LnPera7OaNg5RhYgoVpVNxT//sjsO/S7wuGcGjrjzCy0YbXw==","repository":{"url":"git+https://github.com/beesolve/packages.git","type":"git"},"_npmVersion":"11.12.1","description":"Generic one-time action token store backed by DynamoDB","directories":{},"_nodeVersion":"26.0.0","dependencies":{"valibot":"^1.4.0","constructs":"^10.6.0","aws-cdk-lib":"^2.254.0","@aws-sdk/lib-dynamodb":"^3.1047.0","@aws-sdk/client-dynamodb":"^3.1047.0","@aws-sdk/credential-providers":"^3.978.0"},"_hasShrinkwrap":false,"devDependencies":{"@types/node":"^25.8.0"},"peerDependencies":{"typescript":"^6.0.3"},"peerDependenciesMeta":{"typescript":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/action-tokens_0.1.0_1779365872263_0.8824975232255863","host":"s3://npm-registry-packages-npm-production"}},"0.2.0":{"name":"@beesolve/action-tokens","version":"0.2.0","license":"MIT","_id":"@beesolve/action-tokens@0.2.0","maintainers":[{"name":"ivanbarlog","email":"ivan@barlog.sk"}],"homepage":"https://github.com/beesolve/packages/tree/main/packages/action-tokens#readme","bugs":{"url":"https://github.com/beesolve/packages/issues"},"dist":{"shasum":"f4e08c9e2328e9cd77c989c3698283ba9d0e229b","tarball":"https://registry.npmjs.org/@beesolve/action-tokens/-/action-tokens-0.2.0.tgz","fileCount":9,"integrity":"sha512-Lss5tC6guM0WI6kNUtT6dy7FYLVkgx7ybYTMPNV9JPF81yhT/x1Z8SKFrEJnpGDvGqMRGGCysBV9XG1Jdu/c2A==","signatures":[{"sig":"MEUCIQDauki8j+BRn7KaDwAXUkvAQnFSoi8kFOAeexXG/HJgpwIgSLJT9jx51L/Xt6KX3Ok0z+LQaVKgpOrnkN+49v12DVA=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":22720},"type":"module","_from":"file:beesolve-action-tokens-0.2.0.tgz","exports":{"./cdk":{"import":{"types":"./dist/cdk.d.ts","default":"./dist/cdk.js"}},"./sdk":{"import":{"types":"./dist/sdk.d.ts","default":"./dist/sdk.js"}},"./model":{"import":{"types":"./dist/model.d.ts","default":"./dist/model.js"}},"./package.json":"./package.json"},"scripts":{"test":"bun test","type-check":"tsc --noEmit"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:da0cf492-4997-4937-8d23-64995abdaf8f"}},"_resolved":"/home/runner/work/packages/packages/packages/action-tokens/beesolve-action-tokens-0.2.0.tgz","_integrity":"sha512-Lss5tC6guM0WI6kNUtT6dy7FYLVkgx7ybYTMPNV9JPF81yhT/x1Z8SKFrEJnpGDvGqMRGGCysBV9XG1Jdu/c2A==","repository":{"url":"git+https://github.com/beesolve/packages.git","type":"git"},"_npmVersion":"11.12.1","description":"Generic one-time action token store backed by DynamoDB","directories":{},"_nodeVersion":"24.15.0","dependencies":{"valibot":"^1.4.0","constructs":"^10.6.0","aws-cdk-lib":"^2.254.0","@aws-sdk/lib-dynamodb":"^3.1047.0","@aws-sdk/client-dynamodb":"^3.1047.0","@aws-sdk/credential-providers":"^3.978.0"},"_hasShrinkwrap":false,"devDependencies":{"@types/node":"^25.8.0"},"peerDependencies":{"typescript":"^6.0.3"},"peerDependenciesMeta":{"typescript":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/action-tokens_0.2.0_1779374304545_0.7601213534445357","host":"s3://npm-registry-packages-npm-production"}},"0.3.0":{"name":"@beesolve/action-tokens","version":"0.3.0","license":"MIT","_id":"@beesolve/action-tokens@0.3.0","maintainers":[{"name":"ivanbarlog","email":"ivan@barlog.sk"}],"homepage":"https://github.com/beesolve/packages/tree/main/packages/action-tokens#readme","bugs":{"url":"https://github.com/beesolve/packages/issues"},"dist":{"shasum":"dcb1564f327ab90de09660e3252e8b74ef851e5e","tarball":"https://registry.npmjs.org/@beesolve/action-tokens/-/action-tokens-0.3.0.tgz","fileCount":9,"integrity":"sha512-7hdcpN0/tn5oSkdoCMw4h0U/yWMIjUpMP/SxjH0hBozsn055SqjvPLS4JdSaJnNz4bD8VHqJzPt4fp6t1cn9gg==","signatures":[{"sig":"MEUCIQCErYfZ89f6YeRVKoheu39Jy52Dzoi7pUvg0YnOdI4HrwIgMw+/YkTVut1iqUlUXmjwdRDGmKNFquEgcA5a/dJie+g=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":29261},"type":"module","_from":"file:beesolve-action-tokens-0.3.0.tgz","exports":{"./cdk":{"import":{"types":"./dist/cdk.d.ts","default":"./dist/cdk.js"}},"./sdk":{"import":{"types":"./dist/sdk.d.ts","default":"./dist/sdk.js"}},"./model":{"import":{"types":"./dist/model.d.ts","default":"./dist/model.js"}},"./package.json":"./package.json"},"scripts":{"test":"bun test","type-check":"tsc --noEmit"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:da0cf492-4997-4937-8d23-64995abdaf8f"}},"_resolved":"/home/runner/work/packages/packages/packages/action-tokens/beesolve-action-tokens-0.3.0.tgz","_integrity":"sha512-7hdcpN0/tn5oSkdoCMw4h0U/yWMIjUpMP/SxjH0hBozsn055SqjvPLS4JdSaJnNz4bD8VHqJzPt4fp6t1cn9gg==","repository":{"url":"git+https://github.com/beesolve/packages.git","type":"git"},"_npmVersion":"11.12.1","description":"Generic one-time action token store backed by DynamoDB","directories":{},"_nodeVersion":"24.15.0","dependencies":{"valibot":"^1.4.0","constructs":"^10.6.0","aws-cdk-lib":"^2.254.0","@aws-sdk/lib-dynamodb":"^3.1051.0","@aws-sdk/client-dynamodb":"^3.1051.0","@aws-sdk/credential-providers":"^3.1051.0"},"_hasShrinkwrap":false,"devDependencies":{"@types/node":"^25.8.0"},"peerDependencies":{"typescript":"^6.0.3"},"peerDependenciesMeta":{"typescript":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/action-tokens_0.3.0_1779468411955_0.020893231820697578","host":"s3://npm-registry-packages-npm-production"}},"0.4.0":{"name":"@beesolve/action-tokens","version":"0.4.0","license":"MIT","_id":"@beesolve/action-tokens@0.4.0","maintainers":[{"name":"ivanbarlog","email":"ivan@barlog.sk"}],"homepage":"https://github.com/beesolve/packages/tree/main/packages/action-tokens#readme","bugs":{"url":"https://github.com/beesolve/packages/issues"},"dist":{"shasum":"4dd3fa5949b3b2e2fcec022fc02d660333d595ba","tarball":"https://registry.npmjs.org/@beesolve/action-tokens/-/action-tokens-0.4.0.tgz","fileCount":9,"integrity":"sha512-Wa6mKOPn1Cc1KFXNjgFlNp7BG3RPaHrqSjCcS0GynhkfD623CGV7itXxVFL3JOvhmgGsrd95vs6T5Yt6K+IjVA==","signatures":[{"sig":"MEUCIQC6aA8eUp7/AY4hGQuC9/OujdyVbLCBD2jgnPxvXdltPgIgEN+/3agglSq07HnZkoZSzzCs9JpUQFm0HCQUniBI+fQ=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":30652},"type":"module","_from":"file:beesolve-action-tokens-0.4.0.tgz","exports":{"./cdk":{"import":{"types":"./dist/cdk.d.ts","default":"./dist/cdk.js"}},"./sdk":{"import":{"types":"./dist/sdk.d.ts","default":"./dist/sdk.js"}},"./model":{"import":{"types":"./dist/model.d.ts","default":"./dist/model.js"}},"./package.json":"./package.json"},"scripts":{"test":"bun test","type-check":"tsc --noEmit"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:da0cf492-4997-4937-8d23-64995abdaf8f"}},"_resolved":"/home/runner/work/packages/packages/packages/action-tokens/beesolve-action-tokens-0.4.0.tgz","_integrity":"sha512-Wa6mKOPn1Cc1KFXNjgFlNp7BG3RPaHrqSjCcS0GynhkfD623CGV7itXxVFL3JOvhmgGsrd95vs6T5Yt6K+IjVA==","repository":{"url":"git+https://github.com/beesolve/packages.git","type":"git"},"_npmVersion":"11.13.0","description":"Generic one-time action token store backed by DynamoDB","directories":{},"_nodeVersion":"24.16.0","dependencies":{"valibot":"^1.4.0","constructs":"^10.6.0","aws-cdk-lib":"^2.254.0","@aws-sdk/lib-dynamodb":"^3.1051.0","@aws-sdk/client-dynamodb":"^3.1051.0","@aws-sdk/credential-providers":"^3.1051.0"},"_hasShrinkwrap":false,"devDependencies":{"@types/node":"^25.8.0"},"peerDependencies":{"typescript":"^6.0.3"},"peerDependenciesMeta":{"typescript":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/action-tokens_0.4.0_1780402703895_0.5205223986816632","host":"s3://npm-registry-packages-npm-production"}},"0.5.0":{"name":"@beesolve/action-tokens","version":"0.5.0","license":"MIT","_id":"@beesolve/action-tokens@0.5.0","maintainers":[{"name":"ivanbarlog","email":"ivan@barlog.sk"}],"homepage":"https://github.com/beesolve/packages/tree/main/packages/action-tokens#readme","bugs":{"url":"https://github.com/beesolve/packages/issues"},"dist":{"shasum":"2e5260dc4e9a53c3e212cae06724458ab204a2d7","tarball":"https://registry.npmjs.org/@beesolve/action-tokens/-/action-tokens-0.5.0.tgz","fileCount":9,"integrity":"sha512-PU+D/ObIQ1peAfR35tXPHmKpOxad0t2PgXYVIwCBq1kBqDru+wRc0bbllIoaw+kQxD9AId1PgKSTkpN4OjaIfw==","signatures":[{"sig":"MEUCIQC0by9Ouw7QNBdQM1BNAEF0ogUPG3xUraKgLRj9k7hXugIgAgUHolCyLOEpX1+Tt8zC+aay0bZhiFvZWEk3glNa7VI=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":32420},"type":"module","_from":"file:beesolve-action-tokens-0.5.0.tgz","exports":{"./cdk":{"import":{"types":"./dist/cdk.d.ts","default":"./dist/cdk.js"}},"./sdk":{"import":{"types":"./dist/sdk.d.ts","default":"./dist/sdk.js"}},"./model":{"import":{"types":"./dist/model.d.ts","default":"./dist/model.js"}},"./package.json":"./package.json"},"scripts":{"test":"bun test","type-check":"tsc --noEmit"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:da0cf492-4997-4937-8d23-64995abdaf8f"}},"_resolved":"/home/runner/work/packages/packages/packages/action-tokens/beesolve-action-tokens-0.5.0.tgz","_integrity":"sha512-PU+D/ObIQ1peAfR35tXPHmKpOxad0t2PgXYVIwCBq1kBqDru+wRc0bbllIoaw+kQxD9AId1PgKSTkpN4OjaIfw==","repository":{"url":"git+https://github.com/beesolve/packages.git","type":"git"},"_npmVersion":"11.13.0","description":"Generic one-time action token store backed by DynamoDB","directories":{},"_nodeVersion":"24.16.0","dependencies":{"valibot":"^1.4.0","constructs":"^10.6.0","aws-cdk-lib":"^2.254.0","@aws-sdk/lib-dynamodb":"^3.1051.0","@aws-sdk/client-dynamodb":"^3.1051.0","@aws-sdk/credential-providers":"^3.1051.0"},"_hasShrinkwrap":false,"devDependencies":{"@types/node":"^25.8.0"},"peerDependencies":{"typescript":"^6.0.3"},"peerDependenciesMeta":{"typescript":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/action-tokens_0.5.0_1780500678332_0.6147471868667231","host":"s3://npm-registry-packages-npm-production"}},"0.5.1":{"name":"@beesolve/action-tokens","version":"0.5.1","license":"MIT","_id":"@beesolve/action-tokens@0.5.1","maintainers":[{"name":"ivanbarlog","email":"ivan@barlog.sk"}],"homepage":"https://github.com/beesolve/packages/tree/main/packages/action-tokens#readme","bugs":{"url":"https://github.com/beesolve/packages/issues"},"dist":{"shasum":"2538dd7c02cf791dd43f4b2bd23a43c02b9c7d5e","tarball":"https://registry.npmjs.org/@beesolve/action-tokens/-/action-tokens-0.5.1.tgz","fileCount":9,"integrity":"sha512-FPVmjTOlrpO7ZkIpDSZCXy8eYwlJJlcvobaWAJHIa0Gnt7AoEYqcSn7M9822/BFAvKnOIqpyOOrzGNx2goP6ug==","signatures":[{"sig":"MEYCIQDOdLCWtYEBF7EOvzx3sxxzPKZsggb4yR0CcJv6FHcRSgIhAKm/6FRmRWo3fZksSjuGccAsaQwBegNC912t60A/5CF3","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":32465},"type":"module","_from":"file:beesolve-action-tokens-0.5.1.tgz","exports":{"./cdk":{"import":{"types":"./dist/cdk.d.ts","default":"./dist/cdk.js"}},"./sdk":{"import":{"types":"./dist/sdk.d.ts","default":"./dist/sdk.js"}},"./model":{"import":{"types":"./dist/model.d.ts","default":"./dist/model.js"}},"./package.json":"./package.json"},"scripts":{"test":"bun test","type-check":"tsc --noEmit"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:da0cf492-4997-4937-8d23-64995abdaf8f"}},"_resolved":"/home/runner/work/packages/packages/packages/action-tokens/beesolve-action-tokens-0.5.1.tgz","_integrity":"sha512-FPVmjTOlrpO7ZkIpDSZCXy8eYwlJJlcvobaWAJHIa0Gnt7AoEYqcSn7M9822/BFAvKnOIqpyOOrzGNx2goP6ug==","repository":{"url":"git+https://github.com/beesolve/packages.git","type":"git"},"_npmVersion":"11.16.0","description":"Generic one-time action token store backed by DynamoDB","directories":{},"_nodeVersion":"24.18.0","dependencies":{"valibot":"^1.4.2","constructs":"^10.6.0","aws-cdk-lib":"^2.261.0","@aws-sdk/lib-dynamodb":"^3.1080.0","@aws-sdk/client-dynamodb":"^3.1080.0","@aws-sdk/credential-providers":"^3.1080.0"},"_hasShrinkwrap":false,"devDependencies":{"@types/node":"^26.1.0"},"peerDependencies":{"typescript":"^6.0.3"},"peerDependenciesMeta":{"typescript":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/action-tokens_0.5.1_1784848163927_0.30466387570486075","host":"s3://npm-registry-packages-npm-production"}},"0.5.2":{"name":"@beesolve/action-tokens","version":"0.5.2","description":"Generic one-time action token store backed by DynamoDB","homepage":"https://github.com/beesolve/packages/tree/main/packages/action-tokens#readme","license":"MIT","repository":{"type":"git","url":"git+https://github.com/beesolve/packages.git"},"type":"module","exports":{"./cdk":{"import":{"types":"./dist/cdk.d.ts","default":"./dist/cdk.js"}},"./sdk":{"import":{"types":"./dist/sdk.d.ts","default":"./dist/sdk.js"}},"./model":{"import":{"types":"./dist/model.d.ts","default":"./dist/model.js"}},"./package.json":"./package.json"},"scripts":{"type-check":"tsc --noEmit","test":"bun test"},"dependencies":{"@aws-sdk/client-dynamodb":"^3.1094.0","@aws-sdk/credential-providers":"^3.1094.0","@aws-sdk/lib-dynamodb":"^3.1094.0","valibot":"^1.4.2"},"devDependencies":{"@types/node":"^26.1.1","aws-cdk-lib":"^2.262.0","constructs":"^10.7.1"},"peerDependencies":{"aws-cdk-lib":"^2.262.0","constructs":"^10.7.1","typescript":"^7.0.2"},"peerDependenciesMeta":{"typescript":{"optional":true}},"_id":"@beesolve/action-tokens@0.5.2","bugs":{"url":"https://github.com/beesolve/packages/issues"},"_integrity":"sha512-XVzsTxjlV8ODNwX04oU4vMXnKl1WFzYnnmGtCazgK7Ci3QuKKD4xVx9r2WyPH17Vkw3aMsYe+Ziz4hVxAZcm4A==","_resolved":"/home/runner/work/packages/packages/packages/action-tokens/beesolve-action-tokens-0.5.2.tgz","_from":"file:beesolve-action-tokens-0.5.2.tgz","_nodeVersion":"24.18.0","_npmVersion":"11.16.0","dist":{"integrity":"sha512-XVzsTxjlV8ODNwX04oU4vMXnKl1WFzYnnmGtCazgK7Ci3QuKKD4xVx9r2WyPH17Vkw3aMsYe+Ziz4hVxAZcm4A==","shasum":"9336e247f46becb63752f7ea138b029a5837cdbb","tarball":"https://registry.npmjs.org/@beesolve/action-tokens/-/action-tokens-0.5.2.tgz","fileCount":9,"unpackedSize":32525,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEYCIQC3sxV6/yUTafualS4BIhP01fJVaxhZ724ujU9pGVLlRgIhAIAYG4HRGF2ouqEBkM6DZw10iJ4xeI4yUkj3FLAiqQuA"}]},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:da0cf492-4997-4937-8d23-64995abdaf8f"}},"directories":{},"maintainers":[{"name":"ivanbarlog","email":"ivan@barlog.sk"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/action-tokens_0.5.2_1784886568442_0.21787547311649624"},"_hasShrinkwrap":false}},"time":{"created":"2026-05-21T12:17:52.138Z","modified":"2026-07-24T09:49:28.731Z","0.1.0":"2026-05-21T12:17:52.417Z","0.2.0":"2026-05-21T14:38:24.703Z","0.3.0":"2026-05-22T16:46:52.098Z","0.4.0":"2026-06-02T12:18:24.048Z","0.5.0":"2026-06-03T15:31:18.490Z","0.5.1":"2026-07-23T23:09:24.068Z","0.5.2":"2026-07-24T09:49:28.562Z"},"bugs":{"url":"https://github.com/beesolve/packages/issues"},"license":"MIT","homepage":"https://github.com/beesolve/packages/tree/main/packages/action-tokens#readme","repository":{"type":"git","url":"git+https://github.com/beesolve/packages.git"},"description":"Generic one-time action token store backed by DynamoDB","maintainers":[{"name":"ivanbarlog","email":"ivan@barlog.sk"}],"readme":"# @beesolve/action-tokens\n\nGeneric one-time action token store backed by AWS DynamoDB.\n\n- Single-table design with `(owner, action)` primary key and a `(value, action)` GSI\n- Remaining-use counter decremented atomically on each attempt (including invalid ones)\n- TTL-based automatic cleanup — no cron required\n- Optional per-identity throttling via `createNewWithThrottling`\n- CDK construct with `grantAccess` that wires IAM + env vars in one call\n- Works with `@beesolve/auth-service` for sign-in codes, email verification, and magic links\n\n## What This Is\n\nA low-level building block for any short-lived, single-use (or limited-use) credential flow: email verification codes, magic login links, password reset tokens, OTP challenges, QR code scans. It handles storage, expiry, use-counting, and brute-force protection — you bring your own token generation and delivery logic.\n\n## What This Is NOT\n\n- **Not an auth framework** — it stores and validates tokens; it does not send emails, render UIs, or manage sessions.\n- **Not a rate limiter** — `createNewWithThrottling` prevents rapid token re-creation for a single identity, but it is not a general-purpose rate-limiting solution.\n- **Not multi-region** — the DynamoDB table is single-region. Use DynamoDB global tables externally if you need replication.\n\n## Installation\n\n```sh\nnpm install @beesolve/action-tokens\n```\n\n```sh\nbun add @beesolve/action-tokens\n```\n\n## CDK Setup\n\nImport from `@beesolve/action-tokens/cdk`. Call `grantAccess` on each Lambda that needs token operations — it grants IAM read/write and injects `BEESOLVE_ACTION_TOKENS_TABLE_NAME` and `BEESOLVE_ACTION_TOKENS_INDEX_NAME` as environment variables.\n\n```ts\nimport { ActionTokens } from \"@beesolve/action-tokens/cdk\";\nimport { RemovalPolicy } from \"aws-cdk-lib\";\n\nconst tokens = new ActionTokens(this, \"ActionTokens\", {\n  removalPolicy: isProd ? RemovalPolicy.RETAIN : RemovalPolicy.DESTROY,\n  deletionProtection: isProd, // defaults to true when removalPolicy is RETAIN\n  pointInTimeRecoveryEnabled: isProd, // defaults to true when deletionProtection is true\n  encryptionKey: myKmsKey, // optional, defaults to AWS-managed encryption\n  contributorInsights: isProd, // optional, defaults to false\n});\n\ntokens.grantAccess(myLambdaFunction);\n```\n\n### CDK Props\n\n| Prop                         | Type            | Default                                    |\n| ---------------------------- | --------------- | ------------------------------------------ |\n| `removalPolicy`              | `RemovalPolicy` | `RETAIN`                                   |\n| `deletionProtection`         | `boolean`       | `true` when `removalPolicy` is `RETAIN`    |\n| `pointInTimeRecoveryEnabled` | `boolean`       | `true` when `deletionProtection` is `true` |\n| `encryptionKey`              | `IKey`          | `undefined` (AWS-managed encryption)       |\n| `contributorInsights`        | `boolean`       | `false`                                    |\n\n## Usage\n\n### SDK client (Lambda handlers)\n\nImport from `@beesolve/action-tokens/sdk`. The client reads the env vars injected by `grantAccess` at module load time — if they are missing, it throws immediately at cold start.\n\n```ts\nimport { ActionTokensClient } from \"@beesolve/action-tokens/sdk\";\n\nconst tokens = new ActionTokensClient();\n```\n\nYou can also pass your own `DynamoDBDocumentClient`:\n\n```ts\nconst tokens = new ActionTokensClient({ dynamoDbClient: myDocClient });\n```\n\n### Create a token\n\n```ts\nconst token = await tokens.createNew({\n  owner: \"user-123\",\n  action: \"verify-email\",\n  value: String(Math.floor(100000 + Math.random() * 900000)), // 6-digit OTP\n  remainingUses: 5, // max attempts before lockout\n  expiresAt: new Date(Date.now() + 10 * 60_000), // 10 minutes\n  data: { emailAddress: \"user@example.com\" }, // arbitrary metadata\n  overwrite: false, // throw if token already exists\n});\n```\n\n### Create a token with throttling\n\nAtomically writes the token and a throttle record. If the identity already has a non-expired throttle record for this action, throws `TokenThrottledError`.\n\n```ts\nimport { TokenThrottledError } from \"@beesolve/action-tokens/model\";\n\ntry {\n  const token = await tokens.createNewWithThrottling({\n    owner: \"session-abc\",\n    action: \"signInRequest\",\n    value: code,\n    remainingUses: 3,\n    expiresAt: new Date(Date.now() + 5 * 60_000),\n    data: { emailAddress },\n    overwrite: true,\n    throttle: {\n      id: emailAddress, // identity to throttle (e.g., email)\n      windowSeconds: 60, // minimum gap between creations\n    },\n  });\n} catch (error) {\n  if (error instanceof TokenThrottledError) {\n    // Called again before the 60s window elapsed — return 429 to the client\n    return new Response(\"Too many requests\", { status: 429 });\n  }\n  throw error;\n}\n```\n\n### Use (validate) a token\n\n```ts\nimport {\n  TokenDoesNotExistError,\n  ExpiredTokenError,\n  TokenAlreadyUsedUpError,\n  TokenInvalidError,\n} from \"@beesolve/action-tokens/model\";\n\ntry {\n  const used = await tokens.use({\n    owner: \"user-123\", // omit to look up by (value, action) via GSI\n    action: \"verify-email\",\n    value: submittedCode,\n    drainWhenValid: false, // true → zero out remaining uses atomically\n  });\n\n  console.log(\"verified:\", used.data?.emailAddress);\n} catch (error) {\n  if (error instanceof TokenDoesNotExistError) {\n    /* not found */\n  }\n  if (error instanceof ExpiredTokenError) {\n    /* past expiresAt */\n  }\n  if (error instanceof TokenAlreadyUsedUpError) {\n    /* remainingUses is 0 */\n  }\n  if (error instanceof TokenInvalidError) {\n    /* wrong value (use was still consumed) */\n  }\n}\n```\n\n### Peek (read without consuming)\n\n```ts\nconst token = await tokens.peek({ owner: \"user-123\", action: \"verify-email\" });\n// Throws TokenDoesNotExistError, ExpiredTokenError, or TokenAlreadyUsedUpError\n```\n\n### Drain (delete immediately)\n\n```ts\nawait tokens.drain({ owner: \"user-123\", action: \"verify-email\" });\n```\n\n### Direct model usage (advanced)\n\nImport from `@beesolve/action-tokens/model` when you want to supply your own DynamoDB client or skip the env-var machinery.\n\n```ts\nimport { ActionTokens } from \"@beesolve/action-tokens/model\";\nimport { DynamoDBDocumentClient } from \"@aws-sdk/lib-dynamodb\";\nimport { DynamoDBClient } from \"@aws-sdk/client-dynamodb\";\n\nconst dynamo = DynamoDBDocumentClient.from(new DynamoDBClient({}));\n\nconst tokens = new ActionTokens({\n  dynamo,\n  tableName: process.env.MY_TABLE_NAME!,\n  valueIndexName: process.env.MY_INDEX_NAME!,\n});\n```\n\n## Local Development\n\nTests use mocked DynamoDB calls — no real AWS resources needed:\n\n```sh\nbun test\n```\n\n## Caveats & Constraints\n\n- **Incorrect values still consume a use.** This is intentional brute-force protection. An attacker cannot enumerate values without burning through `remainingUses`.\n- **GSI lookups are eventually consistent.** When `owner` is omitted in `use()`, the query goes through the GSI which does not support strongly consistent reads. Provide `owner` when you have it.\n- **TTL cleanup is not instant.** DynamoDB deletes expired items within minutes, not immediately. Application code checks `expiresAt` before the TTL sweep runs, so expired tokens are rejected in real-time.\n- **One token per `(owner, action)` pair.** If you need multiple concurrent tokens for the same owner and purpose, use distinct `action` values (e.g., `verify-email:attempt-1`).\n- **Throttle records share the same table.** They use the key pattern `owner: \"throttle#<id>\"` and are cleaned up by TTL like regular tokens.\n\n## Troubleshooting\n\n| Problem                                                                        | Cause                                                                                       | Fix                                                                    |\n| ------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- |\n| Lambda crashes at cold start with \"ActionTokens has not been set up correctly\" | Missing env vars `BEESOLVE_ACTION_TOKENS_TABLE_NAME` or `BEESOLVE_ACTION_TOKENS_INDEX_NAME` | Call `tokens.grantAccess(fn)` in your CDK stack and redeploy           |\n| `TokenAlreadyExistsError` when creating                                        | A token for this `(owner, action)` already exists and `overwrite` is `false`                | Set `overwrite: true` or `drain` the old token first                   |\n| `TokenThrottledError` on `createNewWithThrottling`                             | A non-expired throttle record exists for this identity + action                             | Wait for the throttle window to pass, or use a different throttle `id` |\n| `UnexpectedError: Token was modified concurrently`                             | Another process modified the token between the read and the update                          | Retry the `use()` call — the conditional check ensures correctness     |\n\n## FAQ\n\n**Why does an incorrect value still consume a use?**\n\nIntentional brute-force protection. If incorrect values were free, an attacker could enumerate all possibilities without limit.\n\n**What is `drainWhenValid` for?**\n\nIt atomically sets `remainingUses` to `0` when the value matches, ensuring no concurrent caller can squeeze in another use. Use it for single-use flows (magic links, email verification) where a token must be consumed exactly once.\n\n**Can I look up a token without knowing the owner?**\n\nYes — omit `owner` in `use()`. The call uses the GSI to query by `(value, action)`. This works for magic-link clicks where the owner is not yet known.\n\n**Why both a primary key and a GSI lookup in `use()`?**\n\nThe primary key path (`owner` + `action`) is strongly consistent. The GSI path (`value` + `action`) is eventually consistent but does not require knowing the owner upfront. Provide `owner` when available for the stronger guarantee.\n\n## Package Exports\n\n| Export path                     | Entry point     | Description                                                    |\n| ------------------------------- | --------------- | -------------------------------------------------------------- |\n| `@beesolve/action-tokens/cdk`   | `dist/cdk.js`   | CDK construct (`ActionTokens`) with `grantAccess`              |\n| `@beesolve/action-tokens/sdk`   | `dist/sdk.js`   | `ActionTokensClient` — reads env vars, creates DynamoDB client |\n| `@beesolve/action-tokens/model` | `dist/model.js` | `ActionTokens` class + all error types — BYO DynamoDB client   |\n","readmeFilename":"README.md"}