From 37368c667d02e67c2cfed0a2f7f6a57c1f0fb535 Mon Sep 17 00:00:00 2001 From: Nate Kelley Date: Mon, 14 Sep 2026 18:13:44 -0600 Subject: [PATCH 1/3] fix: narrow device-flow OAuth scope to fix login access_denied Requesting "profile offline_access openid" during device-code login causes Yoto's auth server to reject the authorization with access_denied as soon as the user confirms the code (see #6). The CLI only needs an access/refresh token to call the Yoto API, so trim the request to the minimal "offline_access" scope. Verified end-to-end: device flow now reaches the consent screen and completes successfully. Fixes #6 Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_0182auKxRRCcLdZrBotvbJ6K --- src/api/client.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/api/client.ts b/src/api/client.ts index 7943b37..48477fa 100644 --- a/src/api/client.ts +++ b/src/api/client.ts @@ -71,7 +71,7 @@ export class YotoClient { }, body: new URLSearchParams({ client_id: this.clientId, - scope: "profile offline_access openid", + scope: "offline_access", audience: "https://api.yotoplay.com", }), }); From 2a93409219685ea14556967cf1e369108a335dfa Mon Sep 17 00:00:00 2001 From: Nate Kelley Date: Mon, 14 Sep 2026 18:13:54 -0600 Subject: [PATCH 2/3] fix: allow null icon16x16 in display schema The Yoto API returns display.icon16x16 as null for chapters/tracks that don't have a custom icon set, but DisplaySchema only allowed string|undefined for that field. This made `playlist show` (and any other command that parses card content) throw a Zod validation error on any playlist with unset icons instead of rendering. Verified against a real 25-chapter playlist that previously failed to display. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_0182auKxRRCcLdZrBotvbJ6K --- src/api/schemas.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/api/schemas.ts b/src/api/schemas.ts index 199ab18..5c32085 100644 --- a/src/api/schemas.ts +++ b/src/api/schemas.ts @@ -29,7 +29,7 @@ export const AuthErrorSchema = z.object({ // ============ Content Schemas ============ const DisplaySchema = z.object({ - icon16x16: z.string().optional(), + icon16x16: z.string().nullable().optional(), }).nullable().optional(); const TrackEventsSchema = z.object({ From 6432dc31c2d0ef7afc16149929904c06d7f56039 Mon Sep 17 00:00:00 2001 From: Nate Kelley Date: Mon, 14 Sep 2026 18:38:49 -0600 Subject: [PATCH 3/3] fix: show mediaId instead of displayIconId in icon commands MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit \`icon list\`, \`icon list --mine\`, and \`icon upload\` all displayed \`displayIconId\` in their "ID" column / success message. But \`entry|chapter|track --icon\` (per its own --help text: "file path, mediaId, or yoto:#mediaId") requires the \`mediaId\`, a different, longer token — not the displayIconId. Copy-pasting the ID shown by these commands into --icon silently builds an invalid \`yoto:#{displayIconId}\` reference, which the API rejects with: icon16x16 must be in format "yoto:#{mediaId}" where mediaId is 43 characters Fix: print \`mediaId\` (renamed column to "Media ID") everywhere an icon reference is surfaced to the user, since that's the value --icon actually consumes. ## Verification Reproduced end-to-end against the live API: copying a displayIconId from \`icon list\` into \`entry update --icon\` failed with the error above. After this fix, \`icon list\`/\`icon upload\` print the mediaId, and passing that value to \`entry update --icon\` succeeds. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_0182auKxRRCcLdZrBotvbJ6K --- src/commands/icons.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/commands/icons.ts b/src/commands/icons.ts index 123dd1c..74493ec 100644 --- a/src/commands/icons.ts +++ b/src/commands/icons.ts @@ -31,8 +31,8 @@ export async function listPublicIcons(options: { } table( - ["Title", "ID", "Tags"], - icons.map((icon) => [icon.title ?? "", icon.displayIconId, icon.publicTags.join(", ")]) + ["Title", "Media ID", "Tags"], + icons.map((icon) => [icon.title ?? "", icon.mediaId, icon.publicTags.join(", ")]) ); } @@ -51,8 +51,8 @@ export async function listUserIcons(options: { json?: boolean }): Promise } table( - ["ID", "URL"], - response.displayIcons.map((icon) => [icon.displayIconId, icon.url]) + ["Media ID", "URL"], + response.displayIcons.map((icon) => [icon.mediaId, icon.url]) ); } @@ -75,7 +75,7 @@ export async function uploadIcon( } const icon = response.displayIcon; - success(`Uploaded icon: ${icon.displayIconId}`); + success(`Uploaded icon: ${icon.mediaId}`); if (typeof icon.url === "string" && icon.url) { info(`URL: ${icon.url}`); }