From 1b065e21597e0ac20bc919e55e184955510c7c79 Mon Sep 17 00:00:00 2001 From: Kody Wildfeuer <1735900+kody-w@users.noreply.github.com> Date: Thu, 15 Oct 2026 11:49:11 -0400 Subject: [PATCH] Fix ASC skills crashing on gzip-encoded iris responses App Store Connect's iris API can return gzip-compressed bodies even when the request doesn't ask for them, and urllib does not decompress. The skill scripts called resp.read().decode() and crashed with "UnicodeDecodeError: 'utf-8' codec can't decode byte 0x8b". For asc-team-key-create the crash came after the key was created, so the agent lost the key ID and a rerun created a duplicate. - Add a read_body() helper to every inline script in asc-team-key-create, asc-app-create-ui and asc-iap-attach, and use it for both success and HTTPError bodies. - asc-team-key-create: document recovering a key that was created when the script failed, instead of re-running it. Co-Authored-By: Claude Opus 5.5 --- .../skills/asc-app-create-ui/SKILL.md | 11 +++++-- src/resources/skills/asc-iap-attach/SKILL.md | 29 ++++++++++++++----- .../skills/asc-team-key-create/SKILL.md | 22 ++++++++++---- 3 files changed, 46 insertions(+), 16 deletions(-) diff --git a/src/resources/skills/asc-app-create-ui/SKILL.md b/src/resources/skills/asc-app-create-ui/SKILL.md index fbd855c..08990f7 100644 --- a/src/resources/skills/asc-app-create-ui/SKILL.md +++ b/src/resources/skills/asc-app-create-ui/SKILL.md @@ -41,7 +41,12 @@ Key differences from the public REST API: ```bash python3 -c " -import json, os, urllib.request, sys +import json, os, urllib.request, sys, gzip + +def read_body(r): + # iris may gzip responses even unasked; urllib does not decompress + b = r.read() + return (gzip.decompress(b) if b[:2] == bytes([31, 139]) else b).decode() BUNDLE_ID = 'BUNDLE_ID_HERE' SKU = 'SKU_HERE' @@ -127,7 +132,7 @@ req = urllib.request.Request( data=create_body, method='POST', headers=headers) try: resp = urllib.request.urlopen(req) - result = json.loads(resp.read().decode()) + result = json.loads(read_body(resp)) app_id = result['data']['id'] print(f'App created successfully!') print(f'App ID: {app_id}') @@ -135,7 +140,7 @@ try: print(f'Name: {APP_NAME}') print(f'SKU: {SKU}') except urllib.error.HTTPError as e: - body = e.read().decode() + body = read_body(e) if e.code == 401: print('ERROR: Session expired. Call asc_web_auth MCP tool to re-authenticate.') elif e.code == 409: diff --git a/src/resources/skills/asc-iap-attach/SKILL.md b/src/resources/skills/asc-iap-attach/SKILL.md index a5b7723..02bea14 100644 --- a/src/resources/skills/asc-iap-attach/SKILL.md +++ b/src/resources/skills/asc-iap-attach/SKILL.md @@ -44,7 +44,12 @@ Use the iris API to list subscription groups (with subscriptions) and in-app pur ```bash python3 -c " -import json, os, urllib.request, sys +import json, os, urllib.request, sys, gzip + +def read_body(r): + # iris may gzip responses even unasked; urllib does not decompress + b = r.read() + return (gzip.decompress(b) if b[:2] == bytes([31, 139]) else b).decode() APP_ID = 'APP_ID_HERE' @@ -76,12 +81,12 @@ def iris_get(path): req = urllib.request.Request(url, method='GET', headers=headers) try: resp = urllib.request.urlopen(req) - return json.loads(resp.read().decode()) + return json.loads(read_body(resp)) except urllib.error.HTTPError as e: if e.code == 401: print('ERROR: Session expired. Call asc_web_auth MCP tool to re-authenticate.') sys.exit(1) - print(f'ERROR: HTTP {e.code} — {e.read().decode()[:200]}') + print(f'ERROR: HTTP {e.code} — {read_body(e)[:200]}') sys.exit(1) # List subscription groups with subscriptions included @@ -114,7 +119,12 @@ Use the following script to attach subscriptions. **Do not print or log the cook ```bash python3 -c " -import json, os, urllib.request, sys +import json, os, urllib.request, sys, gzip + +def read_body(r): + # iris may gzip responses even unasked; urllib does not decompress + b = r.read() + return (gzip.decompress(b) if b[:2] == bytes([31, 139]) else b).decode() session_path = os.path.expanduser('~/.blitz/asc-agent/web-session.json') if not os.path.isfile(session_path): @@ -152,7 +162,7 @@ def iris_attach_subscription(sub_id): resp = urllib.request.urlopen(req) print(f'Attached subscription {sub_id}: HTTP {resp.status}') except urllib.error.HTTPError as e: - body = e.read().decode() + body = read_body(e) if 'already set to submit' in body: print(f'Subscription {sub_id} already attached (OK)') elif e.code == 401: @@ -170,7 +180,12 @@ For in-app purchases (non-subscription), change the type and relationship: ```bash python3 -c " -import json, os, urllib.request, sys +import json, os, urllib.request, sys, gzip + +def read_body(r): + # iris may gzip responses even unasked; urllib does not decompress + b = r.read() + return (gzip.decompress(b) if b[:2] == bytes([31, 139]) else b).decode() session_path = os.path.expanduser('~/.blitz/asc-agent/web-session.json') if not os.path.isfile(session_path): @@ -208,7 +223,7 @@ def iris_attach_iap(iap_id): resp = urllib.request.urlopen(req) print(f'Attached IAP {iap_id}: HTTP {resp.status}') except urllib.error.HTTPError as e: - body = e.read().decode() + body = read_body(e) if 'already set to submit' in body: print(f'IAP {iap_id} already attached (OK)') elif e.code == 401: diff --git a/src/resources/skills/asc-team-key-create/SKILL.md b/src/resources/skills/asc-team-key-create/SKILL.md index 5aa3b37..9af5c3e 100644 --- a/src/resources/skills/asc-team-key-create/SKILL.md +++ b/src/resources/skills/asc-team-key-create/SKILL.md @@ -41,7 +41,12 @@ Use the following self-contained script. Replace `KEY_NAME` with the user's chos ```bash python3 -c " -import json, urllib.request, base64, os, sys, time +import json, urllib.request, base64, os, sys, time, gzip + +def read_body(r): + # iris may gzip responses even unasked; urllib does not decompress + b = r.read() + return (gzip.decompress(b) if b[:2] == bytes([31, 139]) else b).decode() KEY_NAME = 'KEY_NAME_HERE' @@ -88,9 +93,9 @@ req = urllib.request.Request( data=create_body, method='POST', headers=headers) try: resp = urllib.request.urlopen(req) - create_data = json.loads(resp.read().decode()) + create_data = json.loads(read_body(resp)) except urllib.error.HTTPError as e: - body = e.read().decode() + body = read_body(e) if e.code == 401: print('ERROR: Session expired. Call asc_web_auth MCP tool to re-authenticate.') elif e.code == 409: @@ -116,9 +121,9 @@ req = urllib.request.Request( method='GET', headers=dl_headers) try: resp = urllib.request.urlopen(req) - dl_data = json.loads(resp.read().decode()) + dl_data = json.loads(read_body(resp)) except urllib.error.HTTPError as e: - print(f'ERROR downloading key: HTTP {e.code} — {e.read().decode()[:300]}') + print(f'ERROR downloading key: HTTP {e.code} — {read_body(e)[:300]}') sys.exit(1) pk_b64 = dl_data['data']['attributes'].get('privateKey') @@ -135,7 +140,7 @@ req = urllib.request.Request( method='GET', headers=dl_headers) try: resp = urllib.request.urlopen(req) - provider_data = json.loads(resp.read().decode()) + provider_data = json.loads(read_body(resp)) issuer_id = None for inc in provider_data.get('included', []): if inc['type'] == 'contentProviders': @@ -189,6 +194,11 @@ After the script runs, report: ### 401 Not Authorized The web session has expired or doesn't exist. Call the `asc_web_auth` MCP tool — this opens the Apple ID login window in Blitz and refreshes `~/.blitz/asc-agent/web-session.json` automatically. Then retry the key creation script. +### Script fails after "Created API key" (or crashes with no output) +The key may already exist in ASC. Do NOT re-run the script, which creates a duplicate. List keys +(`GET /iris/v1/apiKeys?limit=200`), find the one whose `nickname` matches, and if `canDownload` is still +true, run steps 2–4 of the script against that key ID. + ### 409 Conflict A key with the same name may already exist, or another conflict occurred. Try a different name.