{"_id":"@arlotv/embed-core","_rev":"10-56edba869e7b0d2b203014875bfa20ec","name":"@arlotv/embed-core","dist-tags":{"latest":"0.10.0"},"versions":{"0.1.0":{"name":"@arlotv/embed-core","version":"0.1.0","license":"MIT","_id":"@arlotv/embed-core@0.1.0","maintainers":[{"name":"jojopas1971!","email":"joepas@gmail.com"}],"dist":{"shasum":"e8668e80149084153a7899acf9220443bc3c532c","tarball":"https://registry.npmjs.org/@arlotv/embed-core/-/embed-core-0.1.0.tgz","fileCount":15,"integrity":"sha512-PEZcEsHD1J5M4VMyGwn2hpuoXjIHlcYJfc6zNSCSXCGEqNN3crLX9trnyncTy9Kc8TilhBuWg4sdWeXJlKhpdQ==","signatures":[{"sig":"MEUCIQCNUDYdwtxCJsXHi4Lc6b+x1BxEWqboqCYycKLf6x3azwIgFYvs7SIoP8NfRgz7m2GHpjNKaRFvn0kFk4lPLOMcLo8=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":36663},"main":"dist/index.js","type":"module","types":"dist/index.d.ts","engines":{"node":">=18"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"},"./iife":"./dist/arlo-embed.iife.js"},"gitHead":"8ca6f80daa30d8235cb56abadf3a31a66511f468","scripts":{"build":"tsc -p tsconfig.json && node build.mjs"},"_npmUser":{"name":"jojopas1971!","email":"joepas@gmail.com"},"_npmVersion":"10.8.2","description":"Headless client SDK for Arlo embed rails, chat, and telemetry","directories":{},"_nodeVersion":"20.20.2","dependencies":{},"_hasShrinkwrap":false,"_npmOperationalInternal":{"tmp":"tmp/embed-core_0.1.0_1783362222998_0.6513396549486603","host":"s3://npm-registry-packages-npm-production"}},"0.1.1":{"name":"@arlotv/embed-core","version":"0.1.1","license":"MIT","_id":"@arlotv/embed-core@0.1.1","maintainers":[{"name":"jojopas1971!","email":"joepas@gmail.com"}],"dist":{"shasum":"89c1ec79442021aa8f3ee245cd3b387abaeffba6","tarball":"https://registry.npmjs.org/@arlotv/embed-core/-/embed-core-0.1.1.tgz","fileCount":15,"integrity":"sha512-Qeg5Nqboh2ZAF2IPQ3dsK9K4/38B4/YmJBskVWOWYG8wG1gh6Sr/CBUTOUHjDkg6uM2gVmVDquu4knuwvHF5/A==","signatures":[{"sig":"MEUCIB3moBC8fZLvJBBJ3wqsItVc0lIGCv04hD3UIti6k3EPAiEAv5x3xxqvYKQzXPDsQD7IPZaW26T6YLBGSTdLuMJO1rU=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":37186},"main":"dist/index.js","type":"module","types":"dist/index.d.ts","engines":{"node":">=18"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"},"./iife":"./dist/arlo-embed.iife.js"},"gitHead":"e64f2036e65a4cc1e0257cda37595bcd8d0d362b","scripts":{"build":"tsc -p tsconfig.json && node build.mjs"},"_npmUser":{"name":"jojopas1971!","email":"joepas@gmail.com"},"_npmVersion":"10.8.2","description":"Headless client SDK for Arlo embed rails, chat, and telemetry","directories":{},"_nodeVersion":"20.20.2","dependencies":{},"_hasShrinkwrap":false,"_npmOperationalInternal":{"tmp":"tmp/embed-core_0.1.1_1783389456553_0.9572650272493268","host":"s3://npm-registry-packages-npm-production"}},"0.2.0":{"name":"@arlotv/embed-core","version":"0.2.0","license":"MIT","_id":"@arlotv/embed-core@0.2.0","maintainers":[{"name":"jojopas1971!","email":"joepas@gmail.com"}],"dist":{"shasum":"15b973bec00ec5b88cd12bcecf5d8fbb85ea2a1e","tarball":"https://registry.npmjs.org/@arlotv/embed-core/-/embed-core-0.2.0.tgz","fileCount":15,"integrity":"sha512-oDr26LcJQzEVcxqZWVCMBg88ZTJ0lcQbhcg51h52eZZTNV3TPvhyphltUBIG/jFARQJKqCPp3ViYV+xD6emehw==","signatures":[{"sig":"MEUCIQD3tW+xvKzq+QL+VzhAGd2P8nWRMzFib5+GkUzHSsvP7wIgPhT3oxL8cRPYo1YPh5QmKUVm93gtbJy2KwxAc7ffBbQ=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":37514},"main":"dist/index.js","type":"module","types":"dist/index.d.ts","engines":{"node":">=18"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"},"./iife":"./dist/arlo-embed.iife.js"},"gitHead":"eba60ab5b0c064f2fbd38cc0e05af3112ef00ce4","scripts":{"build":"tsc -p tsconfig.json && node build.mjs"},"_npmUser":{"name":"jojopas1971!","email":"joepas@gmail.com"},"_npmVersion":"10.8.2","description":"Headless client SDK for Arlo embed rails, chat, and telemetry","directories":{},"_nodeVersion":"20.20.2","dependencies":{},"_hasShrinkwrap":false,"_npmOperationalInternal":{"tmp":"tmp/embed-core_0.2.0_1783507525144_0.31625970847397444","host":"s3://npm-registry-packages-npm-production"}},"0.3.0":{"name":"@arlotv/embed-core","version":"0.3.0","license":"MIT","_id":"@arlotv/embed-core@0.3.0","maintainers":[{"name":"jojopas1971!","email":"joepas@gmail.com"}],"dist":{"shasum":"a47d1b5034f955296512da8ae3d3d6b54a7e541f","tarball":"https://registry.npmjs.org/@arlotv/embed-core/-/embed-core-0.3.0.tgz","fileCount":17,"integrity":"sha512-LWrx5F6G9brWsflyasc7NyzckuVIFF6zPmrZxNu/LoIhWKm3csdFUZbBbbJcPkcbRLQsjpb0elpugdq/IWOxhQ==","signatures":[{"sig":"MEUCIHINPnJkbYUrQZx7JeJGr7s3Zc3S6ADpinQc2kxGvay5AiEAmkUJBvToVeMhBhO+oN6SLmySKAnjRsjRALx4BmDpaoA=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":46681},"main":"dist/index.js","type":"module","types":"dist/index.d.ts","engines":{"node":">=18"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"},"./iife":"./dist/arlo-embed.iife.js"},"gitHead":"ca8968b1652be99308c39839cdafba6700e896ca","scripts":{"build":"tsc -p tsconfig.json && node build.mjs"},"_npmUser":{"name":"jojopas1971!","email":"joepas@gmail.com"},"_npmVersion":"10.8.2","description":"Headless client SDK for Arlo embed rails, chat, and telemetry","directories":{},"_nodeVersion":"20.20.2","dependencies":{},"_hasShrinkwrap":false,"_npmOperationalInternal":{"tmp":"tmp/embed-core_0.3.0_1784125885429_0.5772560995040437","host":"s3://npm-registry-packages-npm-production"}},"0.4.0":{"name":"@arlotv/embed-core","version":"0.4.0","license":"MIT","_id":"@arlotv/embed-core@0.4.0","maintainers":[{"name":"jojopas1971!","email":"joepas@gmail.com"}],"dist":{"shasum":"72d125f3556e4e52c86c8e31a10dfa995bf63fed","tarball":"https://registry.npmjs.org/@arlotv/embed-core/-/embed-core-0.4.0.tgz","fileCount":17,"integrity":"sha512-eO5EqkelpjIWHMkNQROJYrZVT1lA/kcAE5S2pZECK4ltfRxfWv3d9BNdOp/G4opAuXgHPM2k9AJLAVb7Rw9rTQ==","signatures":[{"sig":"MEYCIQCjoUsYSN4cH00LNXikCS6cqh2VsAqiY/hUQUVvM5EO3gIhAKIKXbNEoGYOMMnY7FU5PzC/AZZz4tkSVP0N1FE5gOl1","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":48589},"main":"dist/index.js","type":"module","types":"dist/index.d.ts","engines":{"node":">=18"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"},"./iife":"./dist/arlo-embed.iife.js"},"gitHead":"84f7de5aa9264632e33fa616ba1dc8b442a162c6","scripts":{"build":"tsc -p tsconfig.json && node build.mjs"},"_npmUser":{"name":"jojopas1971!","email":"joepas@gmail.com"},"_npmVersion":"10.8.2","description":"Headless client SDK for Arlo embed rails, chat, and telemetry","directories":{},"_nodeVersion":"20.20.2","dependencies":{},"_hasShrinkwrap":false,"_npmOperationalInternal":{"tmp":"tmp/embed-core_0.4.0_1784287165360_0.9581219652427009","host":"s3://npm-registry-packages-npm-production"}},"0.5.0":{"name":"@arlotv/embed-core","version":"0.5.0","license":"MIT","_id":"@arlotv/embed-core@0.5.0","maintainers":[{"name":"jojopas1971!","email":"joepas@gmail.com"}],"dist":{"shasum":"4f438fefc7cafffe0b2662fe7aa35982018c066f","tarball":"https://registry.npmjs.org/@arlotv/embed-core/-/embed-core-0.5.0.tgz","fileCount":21,"integrity":"sha512-0tw4KKj1t42w37351NrH+fQVSWgTO3F+HBI0U0qdc3XufI0phK81MLHw4lBwDxix6F3ZP8OPy98E+u7LQwXB/w==","signatures":[{"sig":"MEYCIQDAZ40CkNKQjD5QF3mtoRB08XqqzxlEohoFGnLnUYY2LwIhANiqAfby4bD1KPPLlSY0PTnFy8+sTnirr/7Ak0qpG78r","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":62170},"main":"dist/index.js","type":"module","types":"dist/index.d.ts","engines":{"node":">=18"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"},"./iife":"./dist/arlo-embed.iife.js"},"gitHead":"65399cc297d0420414d7b41ce7950e5f91ef7599","scripts":{"build":"tsc -p tsconfig.json && node build.mjs"},"_npmUser":{"name":"jojopas1971!","email":"joepas@gmail.com"},"_npmVersion":"10.8.2","description":"Headless client SDK for Arlo embed rails, chat, and telemetry","directories":{},"_nodeVersion":"20.20.2","dependencies":{},"_hasShrinkwrap":false,"_npmOperationalInternal":{"tmp":"tmp/embed-core_0.5.0_1784290884895_0.40155350541315427","host":"s3://npm-registry-packages-npm-production"}},"0.6.0":{"name":"@arlotv/embed-core","version":"0.6.0","license":"MIT","_id":"@arlotv/embed-core@0.6.0","maintainers":[{"name":"jojopas1971!","email":"joepas@gmail.com"}],"dist":{"shasum":"29d1cd6fdb552c6904b820e6026596a7d1918fad","tarball":"https://registry.npmjs.org/@arlotv/embed-core/-/embed-core-0.6.0.tgz","fileCount":21,"integrity":"sha512-XIPfZJG6PkTIbFLwSO5kgJIUgGUiYYEPqDrILxAnrPgc7/Le+KWMl8waVzNKxo8X+ZsXz2+jRXoAoIO5bCUgqw==","signatures":[{"sig":"MEUCIQDhrf6S6e/huAlJLJxNCeFvJ1GAT2cdrxLW+pBEDDo4hgIgP6oIgTsLvGc5eiH3JXThiDchlK+zCdU2n6B2ucZryXs=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":63405},"main":"dist/index.js","type":"module","types":"dist/index.d.ts","engines":{"node":">=18"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"},"./iife":"./dist/arlo-embed.iife.js"},"gitHead":"13c62898316590ef3a691eed5e837801fa2f5863","scripts":{"build":"tsc -p tsconfig.json && node build.mjs"},"_npmUser":{"name":"jojopas1971!","email":"joepas@gmail.com"},"_npmVersion":"10.8.2","description":"Headless client SDK for Arlo embed rails, chat, and telemetry","directories":{},"_nodeVersion":"20.20.2","dependencies":{},"_hasShrinkwrap":false,"_npmOperationalInternal":{"tmp":"tmp/embed-core_0.6.0_1784306227157_0.7439343339338416","host":"s3://npm-registry-packages-npm-production"}},"0.7.0":{"name":"@arlotv/embed-core","version":"0.7.0","license":"MIT","_id":"@arlotv/embed-core@0.7.0","maintainers":[{"name":"jojopas1971!","email":"joepas@gmail.com"}],"dist":{"shasum":"7041a3d00482332ea09e29694440f2afddd9bdc3","tarball":"https://registry.npmjs.org/@arlotv/embed-core/-/embed-core-0.7.0.tgz","fileCount":21,"integrity":"sha512-jtLgRYZkED6JeNvB7BDcI97OQlK5fz2RzmpZij2+gBu12f/Ns8ROMqUidYlXmEf2l47NGnJNLftG9ZliZOH9+Q==","signatures":[{"sig":"MEYCIQC033chDSRCVR7nsh0Ath5YOIzHqqDyA8IyZwiVV25KxQIhAJnMi+uHFMgZ7LlS0gGmxiiEUOUptG5o7RT8L5hr7MvP","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":63788},"main":"dist/index.js","type":"module","types":"dist/index.d.ts","engines":{"node":">=18"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"},"./iife":"./dist/arlo-embed.iife.js"},"gitHead":"455aac67c8e7d291331cddc629a7a96c1056f803","scripts":{"build":"tsc -p tsconfig.json && node build.mjs"},"_npmUser":{"name":"jojopas1971!","email":"joepas@gmail.com"},"_npmVersion":"10.8.2","description":"Headless client SDK for Arlo embed rails, chat, and telemetry","directories":{},"_nodeVersion":"20.20.2","dependencies":{},"_hasShrinkwrap":false,"_npmOperationalInternal":{"tmp":"tmp/embed-core_0.7.0_1784307939633_0.4545702464701411","host":"s3://npm-registry-packages-npm-production"}},"0.9.0":{"name":"@arlotv/embed-core","version":"0.9.0","license":"MIT","_id":"@arlotv/embed-core@0.9.0","maintainers":[{"name":"jojopas1971!","email":"joepas@gmail.com"}],"dist":{"shasum":"d1714ef7424e547896a66412de6279641327f65e","tarball":"https://registry.npmjs.org/@arlotv/embed-core/-/embed-core-0.9.0.tgz","fileCount":23,"integrity":"sha512-tb+xcDDPAK+L+ybmwP7++1Bs1PUrVHxlLtG4+h4hCLpsJ+aqk0qT5CEYWD2ifs++wBOM1NY91MWCC6x2M2LKBw==","signatures":[{"sig":"MEUCIQD6d1+s+VvKLccVcedWz8hBt5YzZPZvQ1r671dHrvkxtQIgDdwhTtMZuYjH7XJ+adwshuUvi6ESF6jJzQwcrqh5M74=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":71661},"main":"dist/index.js","type":"module","types":"dist/index.d.ts","engines":{"node":">=18"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"},"./iife":"./dist/arlo-embed.iife.js"},"gitHead":"00039d849a4c21199bc494ec9352b11da61e6732","scripts":{"build":"tsc -p tsconfig.json && node build.mjs"},"_npmUser":{"name":"jojopas1971!","email":"joepas@gmail.com"},"_npmVersion":"10.8.2","description":"Headless client SDK for Arlo embed rails, chat, and telemetry","directories":{},"_nodeVersion":"20.20.2","dependencies":{},"_hasShrinkwrap":false,"_npmOperationalInternal":{"tmp":"tmp/embed-core_0.9.0_1784842140788_0.2799297099616713","host":"s3://npm-registry-packages-npm-production"}},"0.10.0":{"name":"@arlotv/embed-core","version":"0.10.0","description":"Headless client SDK for Arlo embed rails, chat, and telemetry","license":"MIT","type":"module","engines":{"node":">=18"},"main":"dist/index.js","types":"dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"},"./iife":"./dist/arlo-embed.iife.js"},"scripts":{"build":"tsc -p tsconfig.json && node build.mjs"},"dependencies":{},"_id":"@arlotv/embed-core@0.10.0","gitHead":"57e3856808cae552654066d467d1f5d1860283f7","_nodeVersion":"20.20.2","_npmVersion":"10.8.2","dist":{"integrity":"sha512-kg8HImvPcZiO/s+huMYJTVQNi31KnYpqVtSpVTxgnjnz4/A94ekYYk8y2A3+hp8tPEtc9VkZGmaQkzGFYCkzDQ==","shasum":"0e43cc7053e3912c2317294ea74fd47d4084fdc2","tarball":"https://registry.npmjs.org/@arlotv/embed-core/-/embed-core-0.10.0.tgz","fileCount":23,"unpackedSize":72330,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIQDVszbf8M7uDedZZkbCIwWAjwWFfaPHG7PkHO88wO+KWgIgas2+lqLKj6ECSwzghZenHeFp6ClMP2VeQk+0qFSSxOc="}]},"_npmUser":{"name":"jojopas1971!","email":"joepas@gmail.com"},"directories":{},"maintainers":[{"name":"jojopas1971!","email":"joepas@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/embed-core_0.10.0_1786369214132_0.8222153916160524"},"_hasShrinkwrap":false}},"time":{"created":"2026-07-06T18:23:42.836Z","modified":"2026-08-10T13:40:14.451Z","0.1.0":"2026-07-06T18:23:43.137Z","0.1.1":"2026-07-07T01:57:36.688Z","0.2.0":"2026-07-08T10:45:25.361Z","0.3.0":"2026-07-15T14:31:25.585Z","0.4.0":"2026-07-17T11:19:25.527Z","0.5.0":"2026-07-17T12:21:25.044Z","0.6.0":"2026-07-17T16:37:07.324Z","0.7.0":"2026-07-17T17:05:39.773Z","0.9.0":"2026-07-23T21:29:00.950Z","0.10.0":"2026-08-10T13:40:14.272Z"},"license":"MIT","description":"Headless client SDK for Arlo embed rails, chat, and telemetry","maintainers":[{"name":"jojopas1971!","email":"joepas@gmail.com"}],"readme":"# @arlotv/embed-core\n\nHeadless client SDK for Arlo embed rails, chat, and telemetry. Zero runtime\ndependencies, ships as ESM (`dist/index.js` + `.d.ts`) for bundler consumers\nand as an ES5 IIFE (`dist/arlo-embed.iife.js`, ~3kB gzipped) for direct\n`<script>` embedding on TV/legacy browser engines.\n\n## Install\n\n```bash\nnpm install @arlotv/embed-core\n```\n\n```ts\nimport { createArloEmbed } from '@arlotv/embed-core';\n\nconst arlo = createArloEmbed({\n  apiKey: 'YOUR_API_KEY',\n  customerId: 'customer-123',\n  // baseUrl defaults to https://api.arlotv.ai\n});\n```\n\n## The signals rule: always `{id, title}`\n\nEvery signals array (`likes`, `dislikes`, `favorites`) is normalized before it\nleaves the SDK. **Always pass `{id, title}` pairs** — bare content ids lose\nthe human-readable title the backend needs for cold-start/attribution\nmatching:\n\n```ts\nawait arlo.rails({\n  likes: [\n    { id: 'tt0111161', title: 'The Shawshank Redemption' },\n    { id: 'tt0068646', title: 'The Godfather' },\n  ],\n  dislikes: [{ id: 'tt0120655', title: 'Spawn' }],\n});\n```\n\nBare strings (e.g. `likes: ['tt0111161']`) are still accepted for backward\ncompatibility and get coerced to `{ id: v, title: v }`, but the title will\njust be the id — do not rely on this in new integrations.\n\n### `watched` — viewing history\n\nA fourth signals bucket carries viewing history (`WatchedSig`): `{ id, title?,\nprogress?, completed?, last_watched_at? }`. Finished watches (`completed: true`\nor `progress >= ~0.95`) act as implicit taste — the ranker naturally down-ranks\nwhat the user already saw (never hard-suppresses). Capped at 50 server-side.\n\n```ts\nawait arlo.rails({\n  watched: [\n    { id: 'tt0111161', title: 'The Shawshank Redemption', completed: true },\n    { id: 'tt0068646', title: 'The Godfather', progress: 0.4 },\n  ],\n});\n```\n\n### Server-side taste persistence (v2)\n\nBy default the server now **persists** signals per `customer_id` (opaque, no\nPII; org-controllable, right-to-erasure via `DELETE /v1/embed/users/:id`). Two\nconsequences:\n\n- `rails({})` with an empty signals object is still personalized once the\n  server has stored taste for that customer — you no longer need to assemble\n  the full signal blob on every call.\n- Injected signals always win on conflict and are merged with stored state, so\n  existing full-injection integrations behave exactly as before.\n\nPrefer the delta emitters (`reaction` / `favorite` / `watchProgress`, below)\nfor keeping the store current as the user acts.\n\n## `rails(signals, opts?)`\n\nReturns `Promise<RailsResponse>`. Fetches personalized rails for the given\nsignals.\n\n```ts\nconst { rails, attribution } = await arlo.rails(\n  { likes: [{ id: 'tt0111161', title: 'The Shawshank Redemption' }] },\n  { max: 20, country: 'US' },\n);\n```\n\n- `opts.max?: number` — cap the number of items returned.\n- `opts.country?: string` — country-scope availability.\n- Rejects with `{ code: 'rails_http_<status>', message }` on a non-2xx\n  response — callers should `await`/`.catch()` it.\n\nThe response is served stale-while-revalidate: the server answers instantly\nfrom its cache and recomputes personalization in the background. Two additive\nfields tell you what you got:\n\n- `rails_meta?: { stale?: boolean; refreshing?: boolean }` — `stale` means the\n  rails predate the customer's newest signals; `refreshing` means a fresh\n  personalized set is composing server-side (refetch shortly if you care).\n- each rail carries `personalized?: boolean` — `false` for generic\n  cold-start/editorial fill, `true` when shaped by this customer's own taste.\n\nEach rail item also carries the display `title` and an `images` object, so you render a full\nrail from this one call — no per-title hydration:\n\n```ts\nconst { rails } = await arlo.rails(signals);\nrails.forEach((rail) => {\n  renderRailHeader(rail.title);\n  rail.items.forEach(({ content_id, rec_id, title, images }) => {\n    renderCard({ id: content_id, rec_id, title, poster: images?.poster, backdrop: images?.backdrop });\n  });\n});\n```\n\n`images` carries `poster`, `poster_large`, `backdrop`, and `title_card` — the last is a 16:9\nimage with the title **baked into the artwork** (render it as-is; don't overlay your own title).\nAny `images` field may be `null` (catalog art gaps are real) — supply your own placeholder.\n`content_id` remains the stable id for telemetry and for anything not in the response.\n\n### `railsWithCache(client, customerId, signals, opts?)` — instant first paint\n\nOptional client-side layer over the same endpoint: returns the last-known rails\nfrom storage synchronously (`cached`), refetches (`fresh`), stores the result,\nand calls `opts.onUpdate(fresh)`. Storage is injectable (RN: AsyncStorage\nadapter); defaults to `localStorage`.\n\n```ts\nimport { railsWithCache } from '@arlotv/embed-core';\nconst { cached, fresh } = railsWithCache(arlo, customerId, {}, { onUpdate: render });\nif (cached) render(cached);   // paints in the same frame as the host app\n```\n\n### `anonymousCustomerId(storage?)` — logged-out visitors\n\nMints one UUID per browser and persists it under `arlo:customer_id` so signals\naccrue to a stable identity across sessions (the blessed convention for\nanonymous visitors). Pass a storage adapter on RN; falls back to a per-session\nid when storage is unavailable.\n\n## `similar(opts)`\n\nReturns `Promise<SimilarResponse>`. A content-to-content \"More Like This\" rail\nfor a single title — ideal for a PDP. The response carries only ids; the host\nhydrates each card from its own catalog.\n\n```ts\nconst { items, attribution } = await arlo.similar({\n  contentId: 'the-shawshank-redemption', // your catalog id / slug (required)\n  max: 12,        // capped at 20 server-side; default 12\n  country: 'US',\n});\n// items: [{ content_id, rec_id, position }, ...]\nitems.forEach(({ content_id, rec_id }) => renderCard(myCatalog[content_id], rec_id));\n```\n\n- `opts.contentId: string` — the title to find neighbors for (required).\n- `opts.max?: number` — cap the number of neighbors (server caps at 20).\n- `opts.country?: string` — country-scope availability.\n- `opts.customerId?: string` — attribution-only session id (**not** a login);\n  defaults to the configured `customerId`. Present ⇒ PDP clicks attribute via\n  the rec_id ledger; absent ⇒ the rail still renders, clicks just aren't\n  attributed.\n- Fail-silent: an unknown/unindexed title resolves to `{ items: [], reason }`\n  (never throws) so a PDP never breaks. Rejects with\n  `{ code: 'similar_http_<status>', message }` only on a non-2xx response, and\n  `{ code: 'similar_bad_content_id' }` if `contentId` is missing.\n\n## `chat(message, opts?)`\n\n```ts\nimport { DEFAULT_MOOD_CHIPS } from '@arlotv/embed-core';\n// Cold start: render DEFAULT_MOOD_CHIPS; on tap call chat(chip).\n\nconst handle = arlo.chat('recommend something like Shawshank', {\n  signals: { likes: [{ id: 'tt0111161', title: 'The Shawshank Redemption' }] },\n  history: [{ role: 'user', content: 'hi' }],\n  onToken: (t) => appendToTranscript(t),\n  onPicks: (cards) => renderPicks(cards),\n  onChips: (chips) => renderChips(chips), // per-turn quick replies; clear on next user action\n  onAttribution: (a) => renderAttribution(a), // e.g. \"Powered by Arlo\" — render when a.required\n  onDone: (usage) => console.log('done', usage),\n  onError: (e) => console.error(e.code, e.message),\n});\n\n// later, e.g. on unmount or a new message starting\nhandle.cancel();\n```\n\n`chat()` returns a plain `{ cancel(): void }` handle synchronously — it is\n**not** a Promise. All results arrive via the `onToken` / `onPicks` /\n`onChips` / `onAttribution` / `onDone` / `onError` callbacks, which are safe\nto call with no `opts` at all (`arlo.chat('hi')` does not throw).\n\n**`DEFAULT_MOOD_CHIPS`** is a static, host-overridable `string[]` the host\nrenders *before* the first message (there's no prior turn to derive\nper-turn chips from). Tapping one is an ordinary `chat(chipText)` call —\nnothing special on the wire. Hosts are free to render their own list\ninstead; this is only a sensible default, not configuration.\n\n**`onChips(chips)`** delivers per-turn quick-reply suggestions for the\n*next* thing the user might say (distinct from `DEFAULT_MOOD_CHIPS`, which\nonly appears pre-first-message). Fires from the stream's `chips` frame (and\nfrom the poll response's `suggested_replies`); omitted or empty when the\nturn has none.\n\n**`onAttribution(a)`** delivers the attribution payload the host must render\non the chat surface — `{ required, text, logo_url, link, placement_hint }`.\nWhen `a.required` is `true`, rendering it (e.g. \"Powered by Arlo\") is a\ncompliance requirement, same as the `attribution` object returned by\n`rails()`. It fires from the stream's `attribution` frame (and from the poll\nresponse's `attribution`).\n\n### Transport ladder\n\n`chat()` auto-selects a transport and silently falls back if a rung isn't\navailable in the current runtime:\n\n1. **`xhr`** (default) — progressive XHR against `POST /v1/embed/chat`,\n   parsing newline-delimited `data:` frames out of `responseText` as they\n   arrive. Chosen by default whenever `XMLHttpRequest` exists.\n2. **`sse`** — opt-in only (`opts.transport = 'sse'`), uses native\n   `EventSource` against a GET+query-encoded variant of the same endpoint.\n   Not the default because the current `/v1/embed/chat` endpoint is\n   POST-only; this rung is for a future GET variant. If `EventSource` isn't\n   available, it downgrades to `xhr` (then `poll`).\n3. **`poll`** — one-shot JSON `fetch` (`POST`, `stream:false`), used when\n   neither `xhr` nor `sse` is viable, or as the final fallback of the ladder.\n   Always available.\n\nErrors are routed to `onError` with a transport-specific code\n(`sse_error` | `poll_error`) — `chat()` never throws into the host page.\n\n## `track(type, data?)`\n\nFire-and-forget, synchronous — queues an event in an in-memory batch.\n\n```ts\narlo.track('tile_tapped', { content_id: 'tt0111161' });\narlo.track('pick_tapped', { rec_id: 'rec_abc123' });\n```\n\n`type` is one of: `'attribution_rendered' | 'rail_impression' |\n'chat_opened' | 'pick_tapped' | 'tile_tapped'` (engagement), the funnel types\n`'video_start' | 'ad_fill' | 'video_complete'` (rec_id required), or the signal\ndeltas `'reaction_set' | 'favorite_set' | 'watch_progress'`. `data` is optional:\n`{ rec_id?: string; content_id?: string; payload?: Record<string, unknown> }`.\n\n### `reaction(contentId, reaction, title?)` / `favorite(contentId, favorited, title?)` / `watchProgress(contentId, {progress, completed, title?})`\n\nTyped emitters for the server-side taste store — prefer these over raw\n`track()` for taste updates; they shape payloads correctly. `reaction(id, null)`\nclears a previous like/dislike; `favorite(id, false)` removes a favorite.\nBatched with the rest of telemetry; delivered on `flush()`.\n\n```ts\narlo.reaction('tt0111161', 'like', 'The Shawshank Redemption');\narlo.watchProgress('tt0111161', { completed: true });\narlo.favorite('tt0068646', true, 'The Godfather');\n```\n\n## `attributionRendered()`\n\nShorthand for `track('attribution_rendered')` — call once when the\nattribution/branding block is actually painted on screen.\n\n```ts\narlo.attributionRendered();\n```\n\n## `flush()`\n\nSynchronously sends the current queued-event batch via\n`fetch(url, { keepalive: true })` so it survives page unload, then clears the\nbuffer. Best-effort — a failed or dropped batch never throws. Call it\nproactively (e.g. on `visibilitychange`/`pagehide`) if you don't want to wait\nfor the SDK's own batching.\n\n```ts\nwindow.addEventListener('pagehide', () => arlo.flush());\n```\n\n## `captureRecId(url?)`\n\nPure, synchronous helper — pulls the referral id out of `?src=arlo&aid=...`\nquery params (defaults to `window.location.href` when no `url` is given).\nReturns `string | null`.\n\n```ts\nconst recId = arlo.captureRecId(); // or captureRecId(location.href)\nif (recId) arlo.track('pick_tapped', { rec_id: recId });\n```\n\n## Promises and old TV engines\n\n`rails()` is `async` and returns a real `Promise`. `chat()`'s `poll`\nfallback and `flush()`'s telemetry send both use `fetch(...).then(...)`\ninternally even though their public surface is callback-based. **Any TV or\nlegacy browser engine consuming this SDK needs global `Promise` and `fetch`\nimplementations** (polyfill them before loading the SDK if the target engine\nlacks either) — `createArloEmbed()` reads the global `fetch` at construction\nunless you pass your own via `{ fetch }`. Of the public API, only `track`,\n`attributionRendered`, and `captureRecId` are Promise-free (they enqueue or\nparse synchronously). `flush()` is fire-and-forget — it needs `fetch` +\n`Promise` to send, but is best-effort and never throws, so on an engine\nwithout them it silently drops the batch rather than erroring.\n\n## Browser `<script>` / IIFE usage\n\nFor engines without a bundler (Vizio/webOS/Tizen/FireTV WebViews, older Smart\nTV browsers), load the ES5 IIFE build directly:\n\n```html\n<script src=\"https://unpkg.com/@arlotv/embed-core/dist/arlo-embed.iife.js\"></script>\n<script>\n  var arlo = ArloEmbed.createArloEmbed({\n    apiKey: 'YOUR_API_KEY',\n    customerId: 'customer-123',\n  });\n\n  arlo.rails({\n    likes: [{ id: 'tt0111161', title: 'The Shawshank Redemption' }],\n  }).then(function (res) {\n    renderRails(res.rails);\n  });\n\n  var chatHandle = arlo.chat('what should I watch tonight?', {\n    onToken: function (t) { appendToTranscript(t); },\n    onPicks: function (cards) { renderPicks(cards); },\n    onError: function (e) { console.error(e.code, e.message); },\n  });\n  // chatHandle.cancel() to abort\n</script>\n```\n\nThe global is `ArloEmbed`, exposing the same named exports as the ESM\nentrypoint (`createArloEmbed`, `captureRecId`, `normalizeSignals`, types are\nerased at runtime). The bundle is built with `target: 'es5'` and has no\nruntime dependencies, so it runs unmodified on old TV JS engines — provided\na `Promise` polyfill is present (see above).\n","readmeFilename":"README.md"}