{"_id":"@cyberscaling/secure-audio-stream-client","_rev":"11-3d0df4c364335c63d7b8b97bd0c45061","name":"@cyberscaling/secure-audio-stream-client","dist-tags":{"latest":"0.7.0"},"versions":{"0.1.0":{"name":"@cyberscaling/secure-audio-stream-client","version":"0.1.0","keywords":["audio","streaming","media-source","managed-media-source","ios","aes-ctr","musicme","cyberscaling"],"author":{"name":"Cyberscaling","email":"pierre@tech-leads.net"},"license":"MIT","_id":"@cyberscaling/secure-audio-stream-client@0.1.0","maintainers":[{"name":"fondation-io","email":"pierre@tech-leads.net"}],"homepage":"https://github.com/cyberscaling/secure-audio-stream/tree/main/client","bugs":{"url":"https://github.com/cyberscaling/secure-audio-stream/issues"},"dist":{"shasum":"e4a60aa643344d46da508a7a3af128c5742d990a","tarball":"https://registry.npmjs.org/@cyberscaling/secure-audio-stream-client/-/secure-audio-stream-client-0.1.0.tgz","fileCount":9,"integrity":"sha512-GlGSyX0XH37YjKxxVW5a6AIdgKkwZJfFqJ6jMjlLwjT3AP9epT9jGcxw2zHbvcTL1Sc7VH1tIdljYW5qTCS/vw==","signatures":[{"sig":"MEUCIBJ7afkv8HTzilNFfrjXMwDdCTzepaAx8UWasSRnt42QAiEA7o64HYwB5CxyT/eCGK6xl9ajt9PLDuJdB5EHaimDMvE=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":225019},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"}},"gitHead":"397f7a8366b0899d2e32fc138bf0a897bf433266","scripts":{"dev":"vite","test":"vitest run","build":"tsup","preview":"vite preview","typecheck":"tsc --noEmit","build:demo":"vite build","prepublishOnly":"bun run build && bun run typecheck && bun run test"},"_npmUser":{"name":"fondation-io","email":"pierre@tech-leads.net"},"repository":{"url":"git+https://github.com/cyberscaling/secure-audio-stream.git","type":"git","directory":"client"},"_npmVersion":"10.8.2","description":"JavaScript SDK for the Cyberscaling secure audio streaming worker. AES-CTR transit decryption + MSE / ManagedMediaSource playback.","directories":{},"_nodeVersion":"20.20.2","dependencies":{"mp4box":"^0.5.2"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.5.1","vite":"^6.0.0","vitest":"^2.0.0","typescript":"^5.6.0"},"_npmOperationalInternal":{"tmp":"tmp/secure-audio-stream-client_0.1.0_1778439335486_0.6530709546894702","host":"s3://npm-registry-packages-npm-production"}},"0.2.0":{"name":"@cyberscaling/secure-audio-stream-client","version":"0.2.0","keywords":["audio","streaming","media-source","managed-media-source","ios","aes-ctr","musicme","cyberscaling"],"author":{"name":"Cyberscaling","email":"pierre@tech-leads.net"},"license":"MIT","_id":"@cyberscaling/secure-audio-stream-client@0.2.0","maintainers":[{"name":"fondation-io","email":"pierre@tech-leads.net"}],"homepage":"https://github.com/cyberscaling/secure-audio-stream/tree/main/client","bugs":{"url":"https://github.com/cyberscaling/secure-audio-stream/issues"},"dist":{"shasum":"e7429c9966df1eea1dacb7d0d62a6fdbf560dbcb","tarball":"https://registry.npmjs.org/@cyberscaling/secure-audio-stream-client/-/secure-audio-stream-client-0.2.0.tgz","fileCount":9,"integrity":"sha512-cxd2D9ktF9OtBrCyWoCd0+w4Q52GAXXK7n4ZwH1kBJOYh1xqCFcCxI6Vb6cFy+GieRk0wjA4E7nyH/BvQ1bgBg==","signatures":[{"sig":"MEUCIDkhr8f5IRsKXpJn6xKB6ryqaPaPHS/mUnlTfcrfWrXeAiEAnYP/fDoMOp5FiSRKiUUZuzq59Q8Z8it/YQcBmo1e28A=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":236496},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"}},"gitHead":"85a91d67541881ff1bbe0ff1d1510e2ff5dbeacc","scripts":{"dev":"vite","test":"vitest run","build":"tsup","preview":"vite preview","typecheck":"tsc --noEmit","build:demo":"vite build","prepublishOnly":"bun run build && bun run typecheck && bun run test"},"_npmUser":{"name":"fondation-io","email":"pierre@tech-leads.net"},"repository":{"url":"git+https://github.com/cyberscaling/secure-audio-stream.git","type":"git","directory":"client"},"_npmVersion":"10.8.2","description":"JavaScript SDK for the Cyberscaling secure audio streaming worker. AES-CTR transit decryption + MSE / ManagedMediaSource playback.","directories":{},"_nodeVersion":"20.20.2","dependencies":{"mp4box":"^0.5.2"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.5.1","vite":"^6.0.0","vitest":"^2.0.0","typescript":"^5.6.0"},"_npmOperationalInternal":{"tmp":"tmp/secure-audio-stream-client_0.2.0_1778507977126_0.878210014567335","host":"s3://npm-registry-packages-npm-production"}},"0.2.1":{"name":"@cyberscaling/secure-audio-stream-client","version":"0.2.1","keywords":["audio","streaming","media-source","managed-media-source","ios","aes-ctr","musicme","cyberscaling"],"author":{"name":"Cyberscaling","email":"pierre@tech-leads.net"},"license":"MIT","_id":"@cyberscaling/secure-audio-stream-client@0.2.1","maintainers":[{"name":"fondation-io","email":"pierre@tech-leads.net"}],"homepage":"https://github.com/cyberscaling/secure-audio-stream/tree/main/client","bugs":{"url":"https://github.com/cyberscaling/secure-audio-stream/issues"},"dist":{"shasum":"fb59272bf4f2c48cbcf01ae52c53f88d7eac34e8","tarball":"https://registry.npmjs.org/@cyberscaling/secure-audio-stream-client/-/secure-audio-stream-client-0.2.1.tgz","fileCount":9,"integrity":"sha512-3IM31mFKh4jwsoNDGB6P2HvS1IqBReGqnK18bMhbNGKlI0AT5wNF0Rz9BKF2zkAhmTq2nuk3xefZUi+wciHCdw==","signatures":[{"sig":"MEQCIGkU/hM+TOWHAUQ2qOOcFVNk6YQ5ffBxGYzI5Vaan3qZAiBIDyJrX57zIZcJG1dAzKNiVsNU3y8sk/scK/DelP95lw==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":243329},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"}},"gitHead":"b85356f2c5b1bf5626bd307ac555bda17c2a07c1","scripts":{"dev":"vite","test":"vitest run","build":"tsup","preview":"vite preview","typecheck":"tsc --noEmit","build:demo":"vite build","prepublishOnly":"bun run build && bun run typecheck && bun run test"},"_npmUser":{"name":"fondation-io","email":"pierre@tech-leads.net"},"repository":{"url":"git+https://github.com/cyberscaling/secure-audio-stream.git","type":"git","directory":"client"},"_npmVersion":"10.8.2","description":"JavaScript SDK for the Cyberscaling secure audio streaming worker. AES-CTR transit decryption + MSE / ManagedMediaSource playback.","directories":{},"_nodeVersion":"20.20.2","dependencies":{"mp4box":"^0.5.2"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.5.1","vite":"^6.0.0","vitest":"^2.0.0","typescript":"^5.6.0"},"_npmOperationalInternal":{"tmp":"tmp/secure-audio-stream-client_0.2.1_1778511478141_0.1823260573391765","host":"s3://npm-registry-packages-npm-production"}},"0.3.0":{"name":"@cyberscaling/secure-audio-stream-client","version":"0.3.0","keywords":["audio","streaming","media-source","managed-media-source","ios","aes-ctr","musicme","cyberscaling"],"author":{"name":"Cyberscaling","email":"pierre@tech-leads.net"},"license":"MIT","_id":"@cyberscaling/secure-audio-stream-client@0.3.0","maintainers":[{"name":"fondation-io","email":"pierre@tech-leads.net"}],"homepage":"https://github.com/cyberscaling/secure-audio-stream/tree/main/client","bugs":{"url":"https://github.com/cyberscaling/secure-audio-stream/issues"},"dist":{"shasum":"114febf5a1284b15d07cd2fb2933434be0b7f26d","tarball":"https://registry.npmjs.org/@cyberscaling/secure-audio-stream-client/-/secure-audio-stream-client-0.3.0.tgz","fileCount":9,"integrity":"sha512-zPHpNJOgCzfD3WF5QbkJZKBl+aoKR8DmuUjtW6WRxLOtgGT02S2C8F298/ZFvoJC93xq1MGgiVWP655vaAynYA==","signatures":[{"sig":"MEUCIQDvHfoKbegpkHezMAK73Glkpjcb3U9G+8yofGxAs3z/UAIgU3Dosi9cPqHQFE6RMrHIGcGzayfs0Igx3WWDEFDPV2k=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":344432},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"}},"gitHead":"518ea34495f224f911b4d802c8e0a8c133f35bb1","scripts":{"dev":"vite","test":"vitest run","build":"tsup","preview":"vite preview","typecheck":"tsc --noEmit","build:demo":"vite build","prepublishOnly":"bun run build && bun run typecheck && bun run test"},"_npmUser":{"name":"fondation-io","email":"pierre@tech-leads.net"},"repository":{"url":"git+https://github.com/cyberscaling/secure-audio-stream.git","type":"git","directory":"client"},"_npmVersion":"10.8.2","description":"JavaScript SDK for the Cyberscaling secure audio streaming worker. AES-CTR transit decryption + MSE / ManagedMediaSource playback.","directories":{},"_nodeVersion":"20.20.2","dependencies":{"mp4box":"^0.5.2"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.5.1","vite":"^6.0.0","vitest":"^2.0.0","typescript":"^5.6.0"},"_npmOperationalInternal":{"tmp":"tmp/secure-audio-stream-client_0.3.0_1778517919993_0.6956526486814874","host":"s3://npm-registry-packages-npm-production"}},"0.3.1":{"name":"@cyberscaling/secure-audio-stream-client","version":"0.3.1","keywords":["audio","streaming","media-source","managed-media-source","ios","aes-ctr","musicme","cyberscaling"],"author":{"name":"Cyberscaling","email":"pierre@tech-leads.net"},"license":"MIT","_id":"@cyberscaling/secure-audio-stream-client@0.3.1","maintainers":[{"name":"fondation-io","email":"pierre@tech-leads.net"}],"homepage":"https://github.com/cyberscaling/secure-audio-stream/tree/main/client","bugs":{"url":"https://github.com/cyberscaling/secure-audio-stream/issues"},"dist":{"shasum":"b4a08070178e4a17d83ab7b27facd3b7b0f5ff90","tarball":"https://registry.npmjs.org/@cyberscaling/secure-audio-stream-client/-/secure-audio-stream-client-0.3.1.tgz","fileCount":9,"integrity":"sha512-n8l0OuYLk2Kqq0uL4firAtS5TZxxwi1R1APo7iopintfUjBswq2aykswf4/zn3FKkxigGzrvDO0+vpOIsOgiOQ==","signatures":[{"sig":"MEYCIQCfy2pGXz3P/7CDnc6Gif05445mkvDiGvQoMrc9Bt1w7AIhAINp6EVkP1kpx9CPdzj79LcvieEHrZL817wqwQUe9/zw","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":345663},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"}},"gitHead":"adb61e1dfaaaa2c0925ed8351883bca422b7c62d","scripts":{"dev":"vite","test":"vitest run","build":"tsup","preview":"vite preview","typecheck":"tsc --noEmit","build:demo":"vite build","prepublishOnly":"bun run build && bun run typecheck && bun run test"},"_npmUser":{"name":"fondation-io","email":"pierre@tech-leads.net"},"repository":{"url":"git+https://github.com/cyberscaling/secure-audio-stream.git","type":"git","directory":"client"},"_npmVersion":"10.8.2","description":"JavaScript SDK for the Cyberscaling secure audio streaming worker. AES-CTR transit decryption + MSE / ManagedMediaSource playback.","directories":{},"_nodeVersion":"20.20.2","dependencies":{"mp4box":"^0.5.2"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.5.1","vite":"^6.0.0","vitest":"^2.0.0","typescript":"^5.6.0"},"_npmOperationalInternal":{"tmp":"tmp/secure-audio-stream-client_0.3.1_1778534561842_0.8162194366400186","host":"s3://npm-registry-packages-npm-production"}},"0.4.0":{"name":"@cyberscaling/secure-audio-stream-client","version":"0.4.0","keywords":["audio","streaming","media-source","managed-media-source","ios","aes-ctr","musicme","cyberscaling"],"author":{"name":"Cyberscaling","email":"pierre@tech-leads.net"},"license":"MIT","_id":"@cyberscaling/secure-audio-stream-client@0.4.0","maintainers":[{"name":"fondation-io","email":"pierre@tech-leads.net"}],"homepage":"https://github.com/cyberscaling/secure-audio-stream/tree/main/client","bugs":{"url":"https://github.com/cyberscaling/secure-audio-stream/issues"},"dist":{"shasum":"ea8d8bde68cab08ad5053c41784c2014c9d365a1","tarball":"https://registry.npmjs.org/@cyberscaling/secure-audio-stream-client/-/secure-audio-stream-client-0.4.0.tgz","fileCount":10,"integrity":"sha512-DeMdxeCpp28uMTdHO2sP8y/bOs9wOeCQt1waQwXbEIbBN5zfl6YlRMBFnhmLd9qgxOke8/1IKa+zbJmkN0NX+g==","signatures":[{"sig":"MEUCICuhmbGnSIIln+vCPF6IRNY8Ku7GUCLHQCZjICJ6TVqvAiEAz7uUfShsuVNem4LOloq3lTxfzmvdoQN1JCFnpzSJ2P8=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":467991},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"}},"gitHead":"2fc836eb7cec7ab0b1859b051da21cc45a464af6","scripts":{"dev":"vite","test":"vitest run","build":"tsup","preview":"vite preview","typecheck":"tsc --noEmit","build:demo":"vite build","prepublishOnly":"bun run build && bun run typecheck && bun run test"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:7a9aac62-22e2-43d9-bdc9-af98456740f4"}},"repository":{"url":"git+https://github.com/cyberscaling/secure-audio-stream.git","type":"git","directory":"client"},"_npmVersion":"12.0.2","description":"JavaScript SDK for the Cyberscaling secure audio streaming worker. AES-CTR transit decryption + MSE / ManagedMediaSource playback.","directories":{},"_nodeVersion":"24.18.0","dependencies":{"mp4box":"^0.5.2"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.5.1","vite":"^6.0.0","vitest":"^2.0.0","typescript":"^5.6.0"},"_npmOperationalInternal":{"tmp":"tmp/secure-audio-stream-client_0.4.0_1785881128943_0.5222989848616919","host":"s3://npm-registry-packages-npm-production"}},"0.4.1":{"name":"@cyberscaling/secure-audio-stream-client","version":"0.4.1","keywords":["audio","streaming","media-source","managed-media-source","ios","aes-ctr","musicme","cyberscaling"],"author":{"name":"Cyberscaling","email":"pierre@tech-leads.net"},"license":"MIT","_id":"@cyberscaling/secure-audio-stream-client@0.4.1","maintainers":[{"name":"fondation-io","email":"pierre@tech-leads.net"}],"homepage":"https://github.com/cyberscaling/secure-audio-stream/tree/main/client","bugs":{"url":"https://github.com/cyberscaling/secure-audio-stream/issues"},"dist":{"shasum":"f8bdb3b3492be9b015ceaad1a4bd7ca03279f8e2","tarball":"https://registry.npmjs.org/@cyberscaling/secure-audio-stream-client/-/secure-audio-stream-client-0.4.1.tgz","fileCount":10,"integrity":"sha512-jLRcnaLBQGfmRwUlkHSMlsrVl8kWsENBmKCZIfqt+KO3oGo6VhAilBWPIUsM743K1bw7Dxf/omdlBistzaEvrw==","signatures":[{"sig":"MEYCIQCUsngfBQjfgU1b3b3NAE86dPVfBvwyfHwsUMnY2JjYZwIhAOCJGc4R3YQKLhGETh8xn+voBjdkWI/dGl70xFDmq9LT","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":472649},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"}},"gitHead":"40b07eb947346b9a8014e3b0eabd5dc08181d138","scripts":{"dev":"vite","test":"vitest run","build":"tsup","preview":"vite preview","typecheck":"tsc --noEmit","build:demo":"vite build","prepublishOnly":"bun run build && bun run typecheck && bun run test"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:7a9aac62-22e2-43d9-bdc9-af98456740f4"}},"repository":{"url":"git+https://github.com/cyberscaling/secure-audio-stream.git","type":"git","directory":"client"},"_npmVersion":"12.0.2","description":"JavaScript SDK for the Cyberscaling secure audio streaming worker. AES-CTR transit decryption + MSE / ManagedMediaSource playback.","directories":{},"_nodeVersion":"24.18.0","dependencies":{"mp4box":"^0.5.2"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.5.1","vite":"^6.0.0","vitest":"^2.0.0","typescript":"^5.6.0"},"_npmOperationalInternal":{"tmp":"tmp/secure-audio-stream-client_0.4.1_1785915694977_0.7594574733928265","host":"s3://npm-registry-packages-npm-production"}},"0.4.2":{"name":"@cyberscaling/secure-audio-stream-client","version":"0.4.2","keywords":["audio","streaming","media-source","managed-media-source","ios","aes-ctr","musicme","cyberscaling"],"author":{"name":"Cyberscaling","email":"pierre@tech-leads.net"},"license":"MIT","_id":"@cyberscaling/secure-audio-stream-client@0.4.2","maintainers":[{"name":"fondation-io","email":"pierre@tech-leads.net"}],"homepage":"https://github.com/cyberscaling/secure-audio-stream/tree/main/client","bugs":{"url":"https://github.com/cyberscaling/secure-audio-stream/issues"},"dist":{"shasum":"3196161c01a63525ce8ee5564ee85a79f792bd71","tarball":"https://registry.npmjs.org/@cyberscaling/secure-audio-stream-client/-/secure-audio-stream-client-0.4.2.tgz","fileCount":10,"integrity":"sha512-hrfyfpkdUVfgJiCEkFCqbx+IIa4IyqxDcdDNB1SZNd4aNWS+FdBKMsdFqVbS4B+d6x8gPK0FWlbNLR58+8QoTQ==","signatures":[{"sig":"MEYCIQCOHNDFI42KM/Zu5neZfQeett7K0WvDKZDUuQj5plWWPAIhAJKnF/I/3Vf5YWMshVzEWyrH6MPOAlfrTWjed+7/BRg0","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":497622},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"}},"gitHead":"9006e4f853bc421d5dc2e2ab1262ee78576c94f7","scripts":{"dev":"vite","test":"vitest run","build":"tsup","preview":"vite preview","typecheck":"tsc --noEmit","build:demo":"vite build","prepublishOnly":"bun run build && bun run typecheck && bun run test"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:7a9aac62-22e2-43d9-bdc9-af98456740f4"}},"repository":{"url":"git+https://github.com/cyberscaling/secure-audio-stream.git","type":"git","directory":"client"},"_npmVersion":"12.0.2","description":"JavaScript SDK for the Cyberscaling secure audio streaming worker. AES-CTR transit decryption + MSE / ManagedMediaSource playback.","directories":{},"_nodeVersion":"24.18.0","dependencies":{"mp4box":"^0.5.2"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.5.1","vite":"^6.0.0","vitest":"^2.0.0","typescript":"^5.6.0"},"_npmOperationalInternal":{"tmp":"tmp/secure-audio-stream-client_0.4.2_1785919257985_0.5453348641867783","host":"s3://npm-registry-packages-npm-production"}},"0.5.0":{"name":"@cyberscaling/secure-audio-stream-client","version":"0.5.0","keywords":["audio","streaming","media-source","managed-media-source","ios","aes-ctr","musicme","cyberscaling"],"author":{"name":"Cyberscaling","email":"pierre@tech-leads.net"},"license":"MIT","_id":"@cyberscaling/secure-audio-stream-client@0.5.0","maintainers":[{"name":"fondation-io","email":"pierre@tech-leads.net"}],"homepage":"https://github.com/cyberscaling/secure-audio-stream/tree/main/client","bugs":{"url":"https://github.com/cyberscaling/secure-audio-stream/issues"},"dist":{"shasum":"58691d6302717f575fec43a3f7da3c69005dbd0a","tarball":"https://registry.npmjs.org/@cyberscaling/secure-audio-stream-client/-/secure-audio-stream-client-0.5.0.tgz","fileCount":11,"integrity":"sha512-KrBpVoLPA2isO6zCZrEwIm+yLN6WlB5XeVPYnCKsNhPuaUORuP0vsUTE/3jsvKMdhcKiwLvAfDUpOqFtTD/YHw==","signatures":[{"sig":"MEUCIQD+SlQ83bmaKIGusvdvEl1f++LLovYB1I+J4rTrucmCyAIgLzUi+NAxOp/m/JjsEqFwNZ1By7AzWGuvLiCBB4L8w1U=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":578698},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}}},"gitHead":"9274fac0f63e81e9b72f4e65c60049c9d9c5d4e4","scripts":{"dev":"vite","test":"vitest run","build":"tsup","preview":"vite preview","typecheck":"tsc --noEmit","build:demo":"vite build","prepublishOnly":"bun run build && bun run verify:package && bun run typecheck && bun run test","verify:package":"node scripts/verify-package-types.mjs"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:7a9aac62-22e2-43d9-bdc9-af98456740f4"}},"repository":{"url":"git+https://github.com/cyberscaling/secure-audio-stream.git","type":"git","directory":"client"},"_npmVersion":"12.0.2","description":"JavaScript SDK for the Cyberscaling secure audio streaming worker. AES-CTR transit decryption + MSE / ManagedMediaSource playback.","directories":{},"_nodeVersion":"24.18.0","dependencies":{"mp4box":"^0.5.2"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.5.1","vite":"^6.0.0","vitest":"^2.0.0","typescript":"^5.6.0"},"_npmOperationalInternal":{"tmp":"tmp/secure-audio-stream-client_0.5.0_1786098064952_0.9589271443989456","host":"s3://npm-registry-packages-npm-production"}},"0.6.0":{"name":"@cyberscaling/secure-audio-stream-client","version":"0.6.0","keywords":["audio","streaming","media-source","managed-media-source","ios","aes-ctr","musicme","cyberscaling"],"author":{"name":"Cyberscaling","email":"pierre@tech-leads.net"},"license":"MIT","_id":"@cyberscaling/secure-audio-stream-client@0.6.0","maintainers":[{"name":"fondation-io","email":"pierre@tech-leads.net"}],"homepage":"https://github.com/cyberscaling/secure-audio-stream/tree/main/client","bugs":{"url":"https://github.com/cyberscaling/secure-audio-stream/issues"},"dist":{"shasum":"18da8f54bab6ea33427e19c13eebbbdf802ebf31","tarball":"https://registry.npmjs.org/@cyberscaling/secure-audio-stream-client/-/secure-audio-stream-client-0.6.0.tgz","fileCount":11,"integrity":"sha512-F68KfJisVHcghDll7J5nk2nmmv7mBLm/Wl/sE6daAyC8GfNiMgvryxlYHaS4wuHG56J6EhHDOtMc0Be88ZqavQ==","signatures":[{"sig":"MEYCIQCPr2ymwCA7gOPG6AFAeVQ+u1uHLk5QkROE6j0B+UNByAIhAO3vKFKJRp/2Kuc5aYQXHYc/3ptbmyOw6PKd4wssiwaN","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":592990},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}}},"gitHead":"0b7c07b4f02238adb12d0bca098138bc7da9f649","scripts":{"dev":"vite","test":"vitest run","build":"tsup","preview":"vite preview","typecheck":"tsc --noEmit","build:demo":"vite build","prepublishOnly":"bun run build && bun run verify:package && bun run typecheck && bun run test","verify:package":"node scripts/verify-package-types.mjs"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:7a9aac62-22e2-43d9-bdc9-af98456740f4"}},"repository":{"url":"git+https://github.com/cyberscaling/secure-audio-stream.git","type":"git","directory":"client"},"_npmVersion":"12.0.2","description":"JavaScript SDK for the Cyberscaling secure audio streaming worker. AES-CTR transit decryption + MSE / ManagedMediaSource playback.","directories":{},"_nodeVersion":"24.18.0","dependencies":{"mp4box":"^0.5.2"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.5.1","vite":"^6.0.0","vitest":"^2.0.0","typescript":"^5.6.0"},"_npmOperationalInternal":{"tmp":"tmp/secure-audio-stream-client_0.6.0_1786213499235_0.1433615620827431","host":"s3://npm-registry-packages-npm-production"}},"0.7.0":{"name":"@cyberscaling/secure-audio-stream-client","version":"0.7.0","description":"JavaScript SDK for the Cyberscaling secure audio streaming worker. AES-CTR transit decryption + MSE / ManagedMediaSource playback.","license":"MIT","author":{"name":"Cyberscaling","email":"pierre@tech-leads.net"},"homepage":"https://github.com/cyberscaling/secure-audio-stream/tree/main/client","repository":{"type":"git","url":"git+https://github.com/cyberscaling/secure-audio-stream.git","directory":"client"},"bugs":{"url":"https://github.com/cyberscaling/secure-audio-stream/issues"},"keywords":["audio","streaming","media-source","managed-media-source","ios","aes-ctr","musicme","cyberscaling"],"type":"module","main":"./dist/index.cjs","module":"./dist/index.js","types":"./dist/index.d.ts","exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}}},"publishConfig":{"access":"public"},"scripts":{"dev":"vite","build":"tsup","build:demo":"vite build","verify:package":"node scripts/verify-package-types.mjs","preview":"vite preview","typecheck":"tsc --noEmit","test":"vitest run","prepublishOnly":"bun run build && bun run verify:package && bun run typecheck && bun run test"},"dependencies":{"mp4box":"^0.5.2"},"devDependencies":{"tsup":"^8.5.1","typescript":"^5.6.0","vite":"^6.0.0","vitest":"^2.0.0"},"gitHead":"5b83cf732385b945a3e3f043911445c6c3329680","_id":"@cyberscaling/secure-audio-stream-client@0.7.0","_nodeVersion":"24.19.0","_npmVersion":"12.0.2","dist":{"integrity":"sha512-CG/Fw4bGaNMgdQEJhiVe3fXZ4TtaYVWS9FM9LHEIwStbOP6gqRMOH068szZmAngXj6zk6nTyQ5+JYC1Hl+27Ew==","shasum":"59812f1c7921e2ba3331b6afade370c2027608d8","tarball":"https://registry.npmjs.org/@cyberscaling/secure-audio-stream-client/-/secure-audio-stream-client-0.7.0.tgz","fileCount":11,"unpackedSize":613946,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEQCIHHtp7mwTNlyrQsKZlBqzCqkrm6qw2GYDzsJPeVd4ciIAiBnbg9Q0+2E7Ks6DRDKs6yc4vHyU/YHTasruF9s71PWRg=="}]},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:7a9aac62-22e2-43d9-bdc9-af98456740f4"}},"directories":{},"maintainers":[{"name":"fondation-io","email":"pierre@tech-leads.net"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/secure-audio-stream-client_0.7.0_1787994828129_0.9239147691778336"},"_hasShrinkwrap":false}},"time":{"created":"2026-05-10T18:55:35.356Z","modified":"2026-08-29T09:13:48.446Z","0.1.0":"2026-05-10T18:55:35.628Z","0.2.0":"2026-05-11T13:59:37.275Z","0.2.1":"2026-05-11T14:57:58.320Z","0.3.0":"2026-05-11T16:45:20.237Z","0.3.1":"2026-05-11T21:22:42.072Z","0.4.0":"2026-08-04T22:05:29.089Z","0.4.1":"2026-08-05T07:41:35.120Z","0.4.2":"2026-08-05T08:40:58.165Z","0.5.0":"2026-08-07T10:21:05.126Z","0.6.0":"2026-08-08T18:24:59.385Z","0.7.0":"2026-08-29T09:13:48.268Z"},"bugs":{"url":"https://github.com/cyberscaling/secure-audio-stream/issues"},"author":{"name":"Cyberscaling","email":"pierre@tech-leads.net"},"license":"MIT","homepage":"https://github.com/cyberscaling/secure-audio-stream/tree/main/client","keywords":["audio","streaming","media-source","managed-media-source","ios","aes-ctr","musicme","cyberscaling"],"repository":{"type":"git","url":"git+https://github.com/cyberscaling/secure-audio-stream.git","directory":"client"},"description":"JavaScript SDK for the Cyberscaling secure audio streaming worker. AES-CTR transit decryption + MSE / ManagedMediaSource playback.","maintainers":[{"name":"fondation-io","email":"pierre@tech-leads.net"}],"readme":"# `@cyberscaling/secure-audio-stream-client`\n\nJavaScript SDK pour le lecteur audio chiffré musicme / Cyberscaling. Charge un morceau identifié par `(cb, disc, track)`, ouvre une session sur le worker de streaming, télécharge les chunks chiffrés en `Range`, déchiffre AES-CTR au vol, alimente une `MediaSource` (ou `ManagedMediaSource` sur iOS Safari 17.1+) via `mp4box.js`. Expose un `<audio>` HTML standard.\n\nLes extraits audio nécessitent le SDK **0.5.0** et un endpoint Worker compatible\navec `/init-preview`. Voir le [guide d'intégration autonome des\nextraits](./docs/sdk-audio-preview-integration.md) pour le contrat complet,\nles erreurs, CORS, métriques, royalties, rollout et tests QA.\n\n## Installation\n\n```bash\nbun add @cyberscaling/secure-audio-stream-client@0.7.0\n# ou: pnpm add / npm install / yarn add\n```\n\n## Usage minimal\n\n```typescript\nimport { SecureAudioPlayer } from '@cyberscaling/secure-audio-stream-client'\n\nconst player = new SecureAudioPlayer({\n  workerUrl: 'https://secure-stream.musicme.com',\n  getToken: async () => {\n    const r = await fetch('/api/player-token', { method: 'POST', credentials: 'include' })\n    const { token } = await r.json()\n    return token\n  },\n  mode: 'mse', // 'blob' uniquement pour debug / fallback explicite\n})\n\nconst playerContainer = document.querySelector<HTMLElement>('#player-container')\nif (!playerContainer) throw new Error('Missing #player-container')\nplayerContainer.append(player.audio)\n\n// `context` (requis depuis 0.7.0) déclare le mode d'écoute pour les\n// déclaratifs de royalties : 'on_demand' | 'radio' | 'artist_mix'.\nawait player.load({ cb: 5400863209100, disc: 1, track: 1, context: 'on_demand' })\nawait player.play()\n```\n\n## Extraits audio avec `loadPreview()`\n\nPour une preview unitaire, réutilise le même `SecureAudioPlayer` et appelle la\nméthode publique du SDK. N'appelle pas directement `/init-preview` depuis le\ncode applicatif : le SDK gère la session, la clé, les Range, AES-CTR,\nMSE/MMS/blob, le heartbeat, les métriques et l'annulation.\n\n```typescript\nconst result = await player.loadPreview({\n  cb: 5400863209100,\n  disc: 1,\n  track: 1,\n  context: 'on_demand', // requis par le type ; jamais compté pour une preview\n})\n\n// Exactement :\n// {\n//   sessionId: string,\n//   expiresAt: number,\n//   previewSeconds: 60 | 90,\n// }\n\n// loadPreview() ne lance jamais la lecture automatiquement.\nawait player.play()\n```\n\n`previewSeconds` est un maximum de politique : 60 s par défaut, 90 s\nuniquement quand le catalogue fournit explicitement `isClassical === true`.\nUne piste courte peut produire moins de média. Quand toute la fenêtre tient\naprès 30 s (source ≥90 s pour une preview 60, ≥120 s pour une preview 90), le\nWorker part de la dernière frontière de sample AAC à ou avant 30 s ; sinon il\npart de zéro.\n\nLa clé et l'IV ne sont jamais retournés dans `PreviewLoadResult`. Le SDK\nconsomme la paire inline en interne ou utilise `/key` comme fallback de\ncompatibilité. Un extrait n'est jamais anonyme : `getToken` doit toujours\npasser par le backend partenaire et son contrôle d'entitlement.\n\nUn JWT sans claim `scope` donne l'accès complet (comportement historique) et\nautorise donc aussi les extraits. Pour un utilisateur qui n'a droit qu'aux\nextraits, le backend mint un token portant la claim top-level\n`scope: \"preview\"` :\n\n```ts\n// trois issues : full, preview, ou aucun token\nif (!user.canStream && !user.canPreview) throw forbidden()\nconst scope = user.canStream ? undefined : 'preview'\n\nbody: JSON.stringify({\n  sub: user.id,\n  ttl_seconds: 300,\n  ...(scope === 'preview' ? { scope } : {}),\n})\n```\n\nN'écris **jamais** `scope: \"full\"` : les deux seuls états valides sont l'absence\ndu champ et `\"preview\"`. L'omission est volontaire pour les comptes autorisés au\nfull-stream. Un token preview reçoit `403 forbidden_scope` sur `/init-stream`,\n`/offline/license*` et les warmups — le SDK le remonte en `ForbiddenScopeError`\n(fatal, jamais retenté), à partir de la version 0.6.0.\n\nLe token doit être frais, porter une claim `exp` obligatoire et rester court :\n5 minutes sont recommandées. À la vérification, le Worker refuse un JWT dont\n`exp` est à plus de 3 600 secondes dans le futur. En production, utilise RS256\navec clé privée backend et JWKS public ; HS256 est\nréservé aux chemins legacy/dev. Une entrée `allowed_origins` est une origine\nracine stricte (`scheme://host[:port]`) sans path, query, fragment ni userinfo.\nUne config legacy avec path ne matche plus. Sans header `Origin`, ou avec une\nancienne allowlist vide, la porte Origin laisse passer mais la validation JWT\nreste obligatoire.\n\nUn cache miss de génération peut ajouter plusieurs secondes. Affiche un état\nde chargement et désactive/debounce l'action identique. Ne boucle pas sur les\nretries. Pour un 502 `preview_generation_failed` ou une erreur réseau, propose\nseulement une tentative utilisateur bornée. Un 401 autorise au plus une\nreprise contrôlée après renouvellement backend d'un JWT frais ; ne relance\njamais le même token. Quota, 400, 403 et 404 ne sont jamais retentés.\n\n### Usage React (player réutilisable)\n\nDepuis **0.4.0**, `load()` est rappelable sur un player déjà utilisé (teardown implicite du morceau précédent) — un seul `SecureAudioPlayer` par composant suffit pour jouer plusieurs pistes successives. (Avant 0.4.0, un `load()` sur un player déjà chargé se comportait de façon non garantie — il fallait recréer un player par piste.)\n\n```tsx\nimport { useEffect, useRef } from 'react'\nimport { SecureAudioPlayer } from '@cyberscaling/secure-audio-stream-client'\n\nfunction Player() {\n  const containerRef = useRef<HTMLDivElement>(null)\n  const playerRef = useRef<SecureAudioPlayer | null>(null)\n\n  useEffect(() => {\n    const p = new SecureAudioPlayer({\n      workerUrl: 'https://secure-stream.musicme.com',\n      getToken: async () => {\n        const r = await fetch('/api/player-token', { method: 'POST', credentials: 'include' })\n        const { token } = await r.json()\n        return token\n      },\n      onError: console.error,\n    })\n    containerRef.current?.append(p.audio)\n    playerRef.current = p\n    return () => p.destroy() // démonte le player ; la session expire côté Worker\n  }, [])\n\n  async function playTrack(cb: number, disc: number, track: number) {\n    const p = playerRef.current\n    if (!p) return\n    await p.load({ cb, disc, track, context: 'on_demand' }) // ré-utilise le même player — requiert ≥0.4.0\n    await p.play()\n  }\n\n  return <div ref={containerRef} />\n}\n```\n\n## Plateformes & modes de lecture\n\nLe partenaire passe **toujours** `mode: 'mse'`. Le SDK choisit le backend au runtime selon les capacités du navigateur :\n\n| Plateforme | Backend résolu | Comportement |\n|---|---|---|\n| iOS Safari 17.1+, macOS Safari 17.1+ | `ManagedMediaSource` (MMS) | Streaming progressif, économe en cellulaire. Pause/reprise du fetch loop sur `endstreaming`/`startstreaming`. |\n| Chrome / Firefox / Edge / Safari desktop pré-17.1 | `MediaSource` (MSE classique) | Streaming progressif standard, comportement inchangé. |\n| iOS Safari pré-17.1 | `blob` (fallback auto) | Téléchargement + déchiffrement intégral avant lecture. `onError` reçoit un avertissement non-fatal `mms_fallback: no_media_source`. |\n| Browser sans `MediaSource` ni `ManagedMediaSource` | `blob` (fallback auto) | Idem ci-dessus. |\n\n**Aucune modif du code partenaire** pour activer iOS. Le SDK auto-détecte `ManagedMediaSource` et choisit le bon chemin.\n\n## Erreurs\n\nToute erreur surfacée par le SDK est une `SecureAudioError` (ou une sous-classe), avec deux champs stables :\n\n- **`code`** — identifiant machine, aligné avec `error_code` de la télémétrie :\n  - `session_expired` — session expirée/évincée (410) ou fingerprint mismatch (403 non-quota) sur `/key`·`/stream`. Un 404 y donne `stream_404` (seul le heartbeat traite le 404 comme session morte). Ré-`load()` résout.\n  - `quota_exceeded` — 403 `quota_exceeded` sur `/stream` (`StreamQuotaError`). **Ne pas rouvrir de session** : chaque tentative facturerait un nouveau `/init-stream` sans lever le quota.\n  - `forbidden_scope` — 403 `forbidden_scope` sur `/init-stream`, `/offline/license*` ou un warmup (`ForbiddenScopeError`, `severity: 'fatal'`, depuis 0.6.0). Le token est limité aux extraits : **aucun retry** ne peut réussir, seul un token de périmètre plus large le peut. Bascule l'UI en mode extrait ou propose l'abonnement.\n  - `stream_<status>` — autre statut HTTP inattendu sur `/stream` (`StreamError`).\n  - `load_error` — erreur non classée pendant `load()`/`loadPreview()` (parse mp4box, réseau, etc.).\n  - `aborted` — chargement volontairement annulé ou supersédé (`LoadAbortedError`).\n  - `mms_fallback` — repli automatique vers le mode `blob` (`FallbackWarning`, voir ci-dessous).\n- **`severity`** — `'fatal'` (échec réel) ou `'warning'` (avertissement non bloquant).\n\n### Signal unique pour l'expiration de session\n\nSi `onSessionExpired` est fourni, une `SessionExpiredError` (`code: 'session_expired'`) est acheminée **uniquement** vers ce callback — elle ne repasse plus par `onError`. Sans `onSessionExpired`, elle continue d'arriver via `onError` comme avant. N'attends pas les deux pour la même erreur.\n\n### Annulation volontaire d'un chargement\n\nUn `load()`/`loadPreview()`/`loadPrefetched()` interrompu par `destroy()` ou par un nouveau\nchargement rejette une `LoadAbortedError` (`code: 'aborted'`,\n`severity: 'warning'`). Cette annulation attendue ne passe pas par `onError` et\nne déclenche pas `onStreamEnded({ reason: 'error' })` ; le chargement remplacé\nreçoit uniquement son signal `onStreamEnded({ reason: 'aborted' })`.\n\n### `FallbackWarning` : le son continue de jouer\n\n`FallbackWarning` (`severity: 'warning'`, `code: 'mms_fallback'`) est délivrée via `onError` pour compat arrière — teste `severity === 'warning'` (ou `err instanceof FallbackWarning`) avant d'afficher une UI d'erreur fatale : la lecture continue normalement via le repli `blob`.\n\n```typescript\nimport { FallbackWarning } from '@cyberscaling/secure-audio-stream-client'\n\nonError: (err) => {\n  if (err instanceof FallbackWarning || (err as { severity?: string }).severity === 'warning') {\n    console.info('[player] repli non-fatal', err.message)\n    return\n  }\n  showFatalErrorUi(err)\n},\n```\n\n### Nouveaux callbacks : `onSessionOpened`, `onStreamEnded`\n\n`onSessionOpened` est déclenché juste après un `/init-stream` **facturé**, avant `/key` — donc signalé même si `/key` échoue ensuite. Pas déclenché par `loadPrefetched()` (déjà signalé au moment du `prefetchSession`, avec `origin: 'prefetch'`) et **jamais déclenché par `loadPreview()`** : une preview n'est pas une session full-stream facturée.\n\n```typescript\nnew SecureAudioPlayer({\n  workerUrl, getToken, mode: 'mse',\n  onSessionOpened: ({ sessionId, ref, expiresAt, origin }) => {\n    console.info('[player] session facturée', sessionId, origin)\n  },\n})\n```\n\n`onStreamEnded` signale la fin de flux, au plus une fois par chargement\n(`load()`, `loadPreview()` ou `loadPrefetched()`) : `'complete'` (fichier livré\nintégralement), `'error'` (échec fatal), ou `'aborted'` (`destroy()` ou nouveau\nchargement en cours de route).\n\n```typescript\nnew SecureAudioPlayer({\n  workerUrl, getToken, mode: 'mse',\n  onStreamEnded: ({ reason }) => console.info('[player] flux terminé :', reason),\n})\n```\n\n## Prefetch & cache warm-up\n\nLe `play→canplay` froid coûte typiquement 1.5-2s (cold DO, cold KV catalogue, cold edge cache). Deux helpers réduisent radicalement cette latence dans le scénario réel partenaire — un user qui ouvre une page album puis joue un morceau.\n\n### `prefetchAlbum(workerUrl, token, cb)`\n\nÀ appeler **dès le mount d'une page album**, en fire-and-forget. Le SDK orchestre une série de calls `/warmup-album` côté worker — chaque call traite jusqu'à 8 tracks en pools parallèles :\n\n1. **Résolution catalogue** — walk de l'index `tracks-db` pour le `cb`, écrit dans `album:<cb>` KV (TTL 24h). Tous les `(disc, track) → mid` du `cb` sont alors instantanément disponibles cross-isolate.\n2. **Métadonnées objet** — `HEAD` Scaleway pour chaque `mid` en parallèle, écrit dans `head:<mid>` KV (TTL 24h). Évite la HEAD upstream (~150ms) à chaque `/init-stream`.\n3. **Cache edge block 0** — `Range 0-1048575` pour chaque `mid` en parallèle, `await caches.default.put`. Cloudflare Cache API persistant 7 jours par PoP. Le premier `/stream` Range du listener final est alors un HIT (~80ms au lieu de ~400-700ms).\n\n```typescript\nimport { prefetchAlbum } from '@cyberscaling/secure-audio-stream-client'\n\n// Appel typique sur ouverture d'une page album\nuseEffect(() => {\n  void prefetchAlbum(workerUrl, token, cb).catch(() => {})  // non-fatal\n}, [cb])\n```\n\n**Pourquoi le SDK chunke**: la limite Cloudflare Workers de **50 subrequests par invocation** (Bundled plan) est atteinte dès qu'un album dépasse ~10 tracks (chaque track ≈ 4 subrequests : KV head get/put + Scaleway HEAD + Scaleway GET). `prefetchAlbum` issue donc le 1er batch synchrone (qui retourne `tracks` = compte total), puis fanout les batches restants en parallèle. Chaque batch HTTP = une invocation Worker séparée avec son propre budget de 50. Albums de toute taille marchent de manière transparente.\n\nRetourne `AlbumWarmupReport` agrégé (`{tracks, head_cached, edge_filled, edge_errors, batches, phases_ms}`).\n\n### `prefetchSession(workerUrl, token, ref)`\n\nPre-crée la **session N+1** pendant que N joue. Renvoie un `PrefetchedSession` activable via `player.loadPrefetched(...)` → switch instantané sur fin de track (auto-advance gapless).\n\n```typescript\nimport { prefetchSession } from '@cyberscaling/secure-audio-stream-client'\n\n// Pendant la lecture de la track N, dès que `currentTime > duration - 5s` :\nconst next = await prefetchSession(workerUrl, token, { cb, disc: 1, track: N + 1, context: 'on_demand' })\n\n// Sur audio.ended :\nawait player.loadPrefetched(next)\n```\n\nCombiné avec `prefetchAlbum` (déjà appelé sur mount), `prefetchSession` se contente d'un `/init-stream` warm — `~50ms`.\n\nDepuis 0.5.0, `PrefetchedSession.ref` est optionnel. Le helper\n`prefetchSession()` ajoute un snapshot de la référence afin que\n`loadPrefetched()` renseigne ses métriques ; un bundle produit par un ancien\nSDK reste accepté, avec `cb`/`track_ref` vides dans ce rapport.\n\n### `prefetchTracks(workerUrl, token, refs)`\n\nPré-chauffe une liste de refs arbitraires — cas radio / file multi-albums / \"à écouter plus tard\" — sans dépendre d'un `cb` unique comme `prefetchAlbum`. Chunke transparemment par lots de 8 refs côté client (même contrainte de 50 subrequests/invocation), via l'endpoint `/warmup-tracks`.\n\n```typescript\nimport { prefetchTracks } from '@cyberscaling/secure-audio-stream-client'\n\nconst report = await prefetchTracks(workerUrl, token, [\n  { cb: 5400863209100, disc: 1, track: 1, context: 'on_demand' },\n  { cb: 3663729427441, disc: 1, track: 7, context: 'on_demand' },\n  // … jusqu'à N, chunké par 8 côté client\n])\n// report: TracksWarmupReport — { refs_total, refs_warmed, head_cached, edge_filled, edge_errors, not_found, batches, phases_ms }\n```\n\n`Playlist` l'utilise automatiquement pour son `kvLookahead` — appelle-le directement seulement pour chauffer une liste que tu ne joues pas (encore).\n\n### Caches serveur (transparents)\n\nAucune action client requise, à connaître pour le debug :\n\n| Cache | Clé | Couverture | TTL | Effet |\n|---|---|---|---|---|\n| `album:<cb>` (KV) | code-barres | cross-isolate | 24h | Skip b-tree walk (~300ms cold) |\n| `head:<mid>` (KV) | mid | cross-isolate | 24h | Skip Scaleway HEAD (~150ms cold) |\n| `scaleway/full/<mid>` (Cache API) | mid | per-PoP | 7 jours | Fichiers ≤10 MiB cached whole — toute Range = HIT |\n| `scaleway/blk/<mid>/<n>` (Cache API) | mid+blockIdx | per-PoP | 7 jours | Fichiers >10 MiB cached en blocks 1 MiB |\n| `previews/<generator>/<mid>/<source-version>/<60\\|90>` (Cache API) | générateur+source+durée | per-PoP | 30 jours | MP4/AAC remuxé à la demande, sans transcodage |\n| `SessionPoolDO` warmth alarm | 4 pool instances | global | self-rearm 15s | `putSession` reste warm (~6-10ms) malgré inactivité |\n| `lookupCache` (module-scoped) | (cb,disc,track) | per-isolate | 5min | Skip KV/R2 sur replay même isolate |\n\n`X-Cache: HIT|MISS|PARTIAL` est exposé sur les responses `/stream` pour observer le edge cache. Le header `Server-Timing` détaille les phases serveur de chaque endpoint (`/init-stream`, `/init-preview`, `/key`, `/stream`, `/warmup-album`). La déduplication de génération preview est locale à l'isolate : deux PoP froids peuvent générer le même extrait en parallèle, de façon sûre.\n\n## Télémétrie\n\nActive la collecte de métriques côté worker (dataset `play_performance`) :\n\n```typescript\nnew SecureAudioPlayer({\n  workerUrl: 'https://secure-stream.musicme.com',\n  getToken,\n  mode: 'mse',\n  metrics: { enabled: true, sampleRate: 1.0 },\n  onMetrics: (report) => {\n    // report.mode = 'mse' | 'mms' | 'blob' (le backend réellement utilisé)\n    // report.asset_kind = 'source' | 'preview'\n    // report.outcome = 'canplay' | 'error' | 'aborted'\n    // report.phases_ms.* = breakdown latence (get_token, init_session, fetch_key, mse_setup, mp4box_ready, canplay, total)\n    console.info('[player] metrics', report)\n  },\n})\n```\n\n`report.mode` permet de slicer les latences par plateforme et\n`report.asset_kind` de séparer source et preview. Depuis 0.5.0,\n`PlayMetricsReport.asset_kind: 'source' | 'preview'` est requis dans le type\npublic. Le Worker l'ajoute à la fin du tableau `play_performance` (blob index\n10) ; un ancien rapport qui l'omet reste accepté comme `source`.\n\nLe SDK normalise `sampleRate` dans l'intervalle fini `[0, 1]`; une valeur non\nfinie (`NaN`, `Infinity`) échoue fermée à `0`. Les rapports partent en\nbest-effort via `fetch(..., { keepalive: true, credentials: 'omit' })` : pas\nde `sendBeacon`, jamais de JWT en query string et aucun cookie, quel que soit\nl'endpoint. Le Bearer streaming est joint seulement à\n`${workerUrl}/metrics` ou à un endpoint custom de la même origine que le\nWorker. Un collecteur custom cross-origin reçoit le rapport sans ce Bearer et\nle SDK ne peut lui ajouter ni headers custom, ni credentials. Il doit accepter\nun POST non authentifié avec validation CORS/body et rate limiting. Un proxy\nsame-origin reçoit lui aussi le POST sans cookies : il ne bénéficie d'aucune\nauth navigateur implicite et sert seulement de relais server-side, avec\nvalidation/rate limit et authentification sortante vers le collecteur. L'auth\nquery historique reste tolérée côté Worker pour les SDK 0.4.x, mais ne doit\npas être utilisée ; `/metrics` limite le body à 64 KiB et valide chaînes,\ntailles et timings bornés/finis.\n\n## Heartbeat & facturation\n\nPendant la lecture effective, le SDK ping `/heartbeat` toutes les 10s avec\n`currentTime` cumulé. Avant `play()` — notamment après `loadPreview()` sans\nautoplay —, pendant une pause et après `ended`, aucun heartbeat périodique ne\nprolonge le TTL. Si la page passe à l'état `hidden` pendant qu'elle joue,\n`visibilitychange` tente une progression `complete:false` pour couvrir le\nthrottling des timers en arrière-plan. Sur `ended`, le SDK arrête d'abord le\ntimer puis **tente en best-effort** un terminal `complete:true`. Les terminaux\nde `pagehide`, `destroy()` et du remplacement par un nouveau chargement sont\négalement best-effort. Un navigateur peut interrompre ces envois : leur\nlivraison et la mesure associée ne sont pas garanties.\n\n**`complete:true` est un marqueur terminal de mesure, pas une fermeture de\nsession et pas la preuve que la piste a été écoutée jusqu'au bout.** Le worker\ndérive `play_full` vs `play_extract` **server-side**, à partir d'un seuil fixe de\n**30 secondes** de `duration_ms` cumulé : le premier franchissement produit\n`play_full` ; un heartbeat terminal sous ce seuil produit `play_extract`, qui\npeut être promu plus tard si la même session reprend et franchit 30 secondes.\nSeul un heartbeat effectivement reçu pendant la lecture prolonge le TTL ; la\nsession expire naturellement après le dernier heartbeat effectif. `pause`,\nl'état non-autoplay et `ended` ne la maintiennent plus artificiellement.\n`destroy()` démonte l'état local mais ne ferme ni ne libère la session serveur.\nSans son joué, il tente seulement un terminal best-effort avec\n`duration_ms: 0`.\n\nLe heartbeat s'arrête automatiquement dès que le worker répond 410/404 (session déjà morte/évincée) — inutile de le gérer côté intégrateur.\n\nPour une preview en lecture effective, le heartbeat maintient la session et sa\nprogression. Avant `play()`, en pause ou après `ended`, aucun périodique ne\nprolonge le TTL. Le Worker n'émet ni `play_extract`, ni `play_full`, ni\n`complete`, et n'envoie aucun message au ledger de redevances. L'observabilité\nreste séparée via `init_preview` et\n`PlayMetricsReport.asset_kind === 'preview'`.\n\n## Plateformes non-web\n\nLe SDK est **web-only** — il dépend de `MediaSource`/`ManagedMediaSource`, `fetch`, et `crypto.subtle` du navigateur. Pour les apps mobiles natives ou React Native, voir le [guide d'intégration partenaire du repo `musicme-onboarding-mcp`](https://github.com/cyberscaling/musicme-onboarding-mcp/blob/main/docs/integration-guide.md), section \"Plateformes non-web\". En résumé :\n\n- **React Native** : recommandé via `react-native-webview`. Deux patterns possibles : (1) WebView qui charge ta webapp partenaire en distant (UI = ta page web), ou (2) WebView **invisible** (1×1px) qui héberge un bundle SDK minimal, bridgée par message-passing avec une UI 100% React Native — [démo complète](https://github.com/cyberscaling/secure-audio-stream/tree/main/demos/react-native). Alternative lourde : bridge natif réimplémentant le flow init/key/stream + déchiffrement (cf. [guide partenaire §6.5.3-4](https://github.com/cyberscaling/secure-audio-stream/blob/main/system-design/09-partner-integration-guide.md)).\n- **Native iOS (Swift)** : `AVPlayer` + `AVAssetResourceLoaderDelegate` qui appelle `/init-stream`, intercepte les range requests AVPlayer et déchiffre AES-CTR via `CryptoKit`. Réimplémente le SDK en ~150 lignes Swift.\n- **Native Android (Kotlin)** : ExoPlayer + `DataSource.Factory` custom, déchiffrement via `javax.crypto.Cipher`. Mêmes endpoints HTTP que le SDK web.\n\n## Playlist (auto-advance + dynamic queue)\n\n`Playlist` n'a pas de mode preview natif. Pour une UI d'extrait unitaire,\nappelle directement `SecureAudioPlayer.loadPreview()`. La démo conserve un\nbridge applicatif mince : player factory vers `loadPreview`,\n`sessionLookahead: 0` en mode preview, relecture du mode à la piste suivante et\nCast désactivé. Ce bridge ne fait pas partie de l'API `Playlist`.\n\n**Costs to know before integrating**: `sessionLookahead` defaults to **2** — the next 2 tracks each get a billed `/init-stream` session kept warm, on top of the current track. `prefetchLeadSeconds` defaults to **8** (seconds before `audio.ended` when the N+1 session prefetch fires) — now actually honored. **There is no session-release endpoint**: a prefetched-but-unused session (fast skip, queue swap) stays billed until it expires naturally. If that cost matters for your integration, guard against queue churn on your side — e.g. debounce rapid insert/remove/setItems calls that recompute the lookahead window.\n\n```typescript\nimport { Playlist } from '@cyberscaling/secure-audio-stream-client'\n\nconst playlist = new Playlist({\n  workerUrl: 'https://secure-stream.musicme.com',\n  getToken: async () => (await fetch('/api/player-token', { method: 'POST', credentials: 'include' }).then(r => r.json())).token,\n  items: [\n    { cb: 5400863209100, disc: 1, track: 1, context: 'on_demand' },\n    { cb: 5400863209100, disc: 1, track: 2, context: 'on_demand' },\n    { cb: 3663729427441, disc: 1, track: 7, context: 'on_demand' },\n  ],\n  onCurrentChange: (curr, prev) => console.info('[playlist] now playing', curr?.ref),\n})\n\nconst playlistContainer = document.querySelector<HTMLElement>('#player')\nif (!playlistContainer) throw new Error('Missing #player')\nplaylistContainer.append(playlist.audio)\nawait playlist.play()\n\n// Mutate live:\nplaylist.insert({ cb: 5400863209100, disc: 1, track: 5, context: 'on_demand' }, /* position */ 1)\nplaylist.move(playlist.items[2].id, 0)\nplaylist.remove(playlist.items[1].id)\n```\n\nEach item gets a stable `id` (auto-generated on insert if you pass a bare `TrackRef`). Mutations are synchronous; the Playlist re-computes its lookahead window on every change and adapts the prefetch state transparently.\n\n**Lookahead policy** (defaults — override via constructor opts):\n\n- **`sessionLookahead = 2`** — the next 2 tracks have their session + key pre-created via `prefetchSession`. Track-to-track latency is ~50ms.\n- **`kvLookahead = 5`** — the next 5 tracks have their mid + head + edge cache pre-warmed via the new `/warmup-tracks` endpoint. Cross-album playlists feel as fast as in-album ones.\n- **`prefetchLeadSeconds = 8`** — how early before `audio.ended` the next-track session prefetch fires.\n\n**Events** (all optional):\n\n- `onItemsChange(items)` — fired after every mutation.\n- `onCurrentChange(curr, prev)` — fired on `play()`, `next()`, `prev()`, auto-advance.\n- `onPrefetchState(e)` — `{itemId, ref, layer, state}` where `layer ∈ {session, kv}` and `state ∈ {pending, ready, error, invalidated}`. Use for observability.\n- `onError(err, ctx)` — `ctx?.itemId` correlates the failure to the offending track.\n\n**Backwards compatibility**: `SecureAudioPlayer`, `prefetchSession`, `prefetchAlbum`, `prefetchTracks` remain exported and unchanged. `Playlist` is purely additive.\n\n### Gapless, not crossfade\n\n`Playlist` drives a single `<audio>` element (one `SecureAudioPlayer`, rebuilt on every advance) — two tracks can never sound simultaneously. Auto-advance is gapless (thanks to session prefetch), but it is not an audio crossfade.\n\nFor a real crossfade (two tracks overlapping), drive two `SecureAudioPlayer`s (or two `Playlist`s) yourself — this is supported, just outside `Playlist`'s scope. `currentPlayer` gives you access to the outgoing track's player during the transition:\n\n```typescript\nconst outgoing = playlist.currentPlayer // current track's player, before advancing\n// drive fade-out on outgoing.audio.volume while a second SecureAudioPlayer /\n// Playlist ramps up the incoming track in parallel.\n```\n\n## Migration — de `0.2.x` vers `0.3.x`\n\nAucun breaking change. Deux nouveautés à intégrer si tu veux profiter du gain UX :\n\n### 1) Warmup album à l'ouverture d'une page album\n\nSi ton intégration actuelle attend que l'utilisateur clique pour lancer la lecture, tu peux pré-chauffer côté serveur dès qu'il atterrit sur la page album. Le premier `canplay` passe de ~1.5 s à ~400 ms.\n\n```typescript\nimport { prefetchAlbum } from '@cyberscaling/secure-audio-stream-client'\n\n// Au mount de la page album, fire-and-forget :\nvoid prefetchAlbum(workerUrl, token, cb).catch(console.warn)\n```\n\nLe helper chunke transparemment côté client si l'album a plus de 8 tracks (limite CF subrequest). Aucune action côté backend partenaire.\n\n### 2) Remplacer la gestion manuelle \"next track\" par `Playlist`\n\nAvant (0.2.x) tu écrivais :\n\n```typescript\nconst player = new SecureAudioPlayer({ workerUrl, getToken, audioElement: audio })\nplayer.audio.addEventListener('ended', () => {\n  const next = myQueue.shift()\n  if (next) void player.load(next)\n})\nawait player.load(myQueue[0])\n```\n\nAvec 0.3.x — pré-fetch + auto-advance gapless + mutations live offertes :\n\n```typescript\nimport { Playlist } from '@cyberscaling/secure-audio-stream-client'\n\nconst playlist = new Playlist({\n  workerUrl,\n  getToken,\n  audioElement: audio,           // optionnel ; sinon Playlist en crée un\n  items: myQueue,                // [{cb, disc, track}, ...] ou [{ref, meta}, ...]\n  sessionLookahead: 2,           // défauts ; ajuste si besoin\n  kvLookahead: 5,\n  onCurrentChange: (curr, prev) => updateUi(curr),\n})\n\nawait playlist.play()\n```\n\nLe `prefetchSession` du prochain track est lancé pendant la lecture du courant, donc `audio.ended → loadPrefetched()` est ~50 ms au lieu de ~1.5 s. La queue peut muter pendant la lecture sans interruption.\n\n### 3) Pattern UX \"playlist persistante + lectures ponctuelles\"\n\nCe que la démo `demos/webapp` fait : conserver la playlist utilisateur indépendante des lectures ad-hoc (un album → \"Play all\", un clic sur un titre seul). Idée : un seul `<audio>` global + une seule instance `Playlist`, mais une logique applicative qui distingue **\"sauvegardée\"** (LS-backed, modifiée seulement par `+`) et **\"éphémère\"** (album entier, ou track standalone, qui ne touche pas la sauvegardée).\n\n```typescript\n// Dans ton store applicatif :\nplayTrack(ref, meta) {\n  // ne touche pas savedQueue\n  playlist.setItems([{ ref, meta }])\n  void playlist.play()\n}\n\nenqueue(ref, meta) {\n  savedQueue.push({ id: genId(), ref, meta })\n  if (mode === 'queue') playlist.insert({ ref, meta }, savedQueue.length - 1)\n  persistToLocalStorage()\n}\n\nplayFromSavedQueue() {\n  mode = 'queue'\n  playlist.setItems(savedQueue.map(it => ({ ref: it.ref, meta: it.meta })))\n  void playlist.play()\n}\n```\n\nCode complet à recopier : `demos/webapp/public/playlist-store.ts` (singleton, < 200 lignes, sans framework).\n\n### 4) Pré-warmup pour playlists cross-album\n\nSi ta playlist contient des refs venant de plusieurs albums (radio, mix, recommandations), `Playlist` chauffe automatiquement les 5 prochaines via `/warmup-tracks`. Aucune intégration supplémentaire. Si tu veux warmer une liste sans la jouer (ex. page \"à écouter plus tard\") :\n\n```typescript\nimport { prefetchTracks } from '@cyberscaling/secure-audio-stream-client'\n\nvoid prefetchTracks(workerUrl, token, [\n  { cb: 5400863209100, disc: 1, track: 1, context: 'on_demand' },\n  { cb: 3663729427441, disc: 1, track: 7, context: 'on_demand' },\n  // … jusqu'à N — le helper chunke par 8 côté client\n]).catch(console.warn)\n```\n\n## API\n\nVoir les exports du package et `dist/index.d.ts` pour la surface typée complète :\n\n- `SecureAudioPlayer(options)` — constructeur\n- `player.load(ref)` — ouvre une session pour `(cb, disc, track)`\n- `player.loadPreview(ref) : Promise<PreviewLoadResult>` — charge sans autoplay\n  une preview 60/90 s et retourne seulement `{ sessionId, expiresAt,\n  previewSeconds }`\n- `player.loadPrefetched(prefetched)` — active une session pré-créée (gapless)\n- `player.play()` / `player.pause()` / `player.seek(time)` — wrappers `<audio>`\n- `player.audio` — le `HTMLAudioElement` à insérer dans le DOM\n- `player.destroy()` — annule les chargements, démonte l'audio et les listeners,\n  puis tente le heartbeat terminal en best-effort ; la session facturée n'est\n  ni fermée ni libérée et expire naturellement côté worker après le dernier\n  heartbeat effectivement reçu (player réutilisable ensuite via `load()`)\n- `prefetchSession(workerUrl, token, ref) : PrefetchedSession` — pre-crée la session N+1 pour `player.loadPrefetched` (gapless auto-advance)\n- `prefetchAlbum(workerUrl, token, cb) : AlbumWarmupReport` — fire-and-forget sur mount d'album, warm les caches album KV + head KV + edge block 0 de toutes les tracks\n- types preview exportés : `PreviewSeconds`, `InitPreviewResponse`,\n  `PreviewLoadResult`, `AssetKind`; `PlayMetricsReport.asset_kind` est requis\n- `ForbiddenScopeError` — erreur exportée (depuis 0.6.0) pour un token\n  `scope: \"preview\"` refusé sur une route full\n\n## Tests\n\n```bash\nbun run test       # vitest, helpers purs (detectMediaSourceCtor, WaitGate)\nbun run typecheck  # tsc --noEmit\nbun run build      # tsup → dist/ (lib publish artifacts: ESM + CJS + .d.ts)\nbun run build:demo # vite build → ../dist-demo/ (demo SPA, dev only)\n```\n\nLes chemins DOM-heavy (`loadMse`, MMS event wiring) sont validés manuellement sur la [matrice browser du plan iOS/MMS](https://github.com/cyberscaling/secure-audio-stream/blob/main/docs/superpowers/plans/2026-05-10-ios-mms-streaming.md).\n\n## Changelog\n\nVoir [`CHANGELOG.md`](./CHANGELOG.md) — livré dans le package npm.\n\n## Publication (mainteneurs)\n\nProcédure de release (npm org, token, workflow tag-triggered) :\n[`client/RELEASING.md` sur GitHub](https://github.com/cyberscaling/secure-audio-stream/blob/main/client/RELEASING.md).\n\n## License\n\nMIT — voir `LICENSE`.\n","readmeFilename":"README.md"}