Releases: tddworks/asc-cli
Releases · tddworks/asc-cli
asc v0.18.0
Added
asc versions updatenow accepts--copyright,--release-type, and--earliest-release-date— closes the App Store submission gap where the version's copyright line, release type (MANUAL/AFTER_APPROVAL/SCHEDULED), and earliest release date could only be set via the web. The underlyingVersionRepository.updateVersion(...)already supported these fields; the CLI now wires them through.--versionis now optional, so any subset of these fields can be patched independently (e.g.asc versions update --version-id v-1 --copyright "© 2026 Acme").
asc v0.17.9
Added
asc builds set-encryption-compliance --build-id <id> --uses-non-exempt-encryption <true|false>— set Apple's export-compliance answer on a build — closes the second TestFlight-blocking gap: when an IPA is uploaded withoutITSAppUsesNonExemptEncryptionin Info.plist, ASC marks the build "Missing Compliance" and external testing is blocked. The new command PATCHes/v1/builds/{id}withusesNonExemptEncryptionso the answer can be supplied post-upload from CI scripts that don't control the Info.plist.Builddomain model now carriesusesNonExemptEncryption: Bool?and aisMissingEncryptionCompliancesemantic boolean (truewhen nil); usable builds in the missing state advertise asetEncryptionComplianceaffordance with the ready-to-run command. Build'stableRowgains an "Encryption" column showinguses/exempt/missing. REST equivalent:PATCH /api/v1/builds/:buildId/encryption-compliancebody{"usesNonExemptEncryption": true|false}. NewBuildRepository.updateBuildEncryptionCompliance(buildId:usesNonExemptEncryption:). Seedocs/features/builds-upload.md.asc beta-app-localizations {list,get,create,update,delete}— TestFlight Beta App Description per locale — closes the parity gap that blocked TestFlight submissions: Apple'sBetaAppLocalizationsresource holds the per-locale beta description, tester feedback email, marketing URL, and privacy policy URL shown in TestFlight before external testing can be enabled. Distinct from the existingasc builds update-beta-notes(which writes per-build "What to Test" notes) andasc beta-review(which writes review contact info). NewBetaAppLocalizationdomain model +@Mockable BetaAppLocalizationRepository(Domain/Apps/TestFlight/),SDKBetaAppLocalizationRepository(Infrastructure) backed by/v1/apps/{id}/betaAppLocalizations(list),/v1/betaAppLocalizations/{id}(get/update/delete), and/v1/betaAppLocalizations(create). Affordances:delete,get,listSiblings,update. REST equivalents:GET /api/v1/apps/:appId/beta-app-localizationsandGET /api/v1/beta-app-localizations/:idviaBetaAppLocalizationsController. New REST path resolver entry forbeta-app-localizationsparented underapps. Seedocs/features/beta-app-localizations.md.
asc v0.17.8
Added
asc iris iap-submissions delete --submission-id <id>— iris-side dequeue of an IAP submission (DELETE /iris/v1/inAppPurchaseSubmissions/{id}). Iris-queued submissions don't round-trip through the public-SDK delete; they can only be removed via this iris-cookie-authed path. NewIrisClient.delete, newIrisInAppPurchaseSubmissionRepository.deleteSubmission(session:submissionId:). REST equivalent:DELETE /api/v1/iris/iap-submissions/:submissionIdonIrisIAPSubmissionsController.removeFromNextVersionaffordance now resolves end-to-end through iris (CLI:asc iris iap-submissions delete --submission-id <id>; REST:DELETE /api/v1/iris/iap-submissions/<id>).RESTPathResolver.resourcePathnow converts spaces in segment names to slashes so multi-word commands likeiris iap-submissionsproduce clean URL segments. Removed the short-lived public-SDK aliasPOST /api/v1/iap/:iapId/unsubmit(incorrect — public DELETE doesn't accept iris-queued submissions).removeFromNextVersionaffordance on queued IAPs — when an IAP is inREADY_TO_SUBMITand Apple's iris listing reportssubmitWithNextAppStoreVersion: true(queued to ride along with the next app version),InAppPurchase.structuredAffordancessurfacesremoveFromNextVersion→asc iap unsubmit --submission-id <iapId>and suppresses thesubmitaffordance (mutually exclusive — Apple rejects re-submits on already-queued IAPs). The dequeue uses the existing public-SDKDELETE /v1/inAppPurchaseSubmissions/{id}(Apple's iris flow keys the submission resource by parent IAP id, so--submission-id <iapId>works). Underpinned by a new best-effort iris enrichment:IrisSDKInAppPurchaseStateRepositorycallsGET /iris/v1/apps/:appId/inAppPurchasesV2?fields[inAppPurchases]=submitWithNextAppStoreVersion&filter[state]=READY_TO_SUBMIT, mapping each IAP to its queued bit.SDKInAppPurchaseRepository.listInAppPurchasesaccepts an optionalirisFlagsProviderclosure; the factory wires it fromIrisCookieProvider+IrisSDKInAppPurchaseStateRepository. CI scripts using API-key auth keep their existing JSON output (flag staysfalse, noremoveFromNextVersionaffordance). NewInAppPurchase.submitWithNextAppStoreVersion: Boolfield, encoded only whentrueto avoid snapshot churn. NewIrisInAppPurchaseStateRepository@MockableDomain protocol.
Changed
InAppPurchasesubmission affordances reorganized into iris-queue family vs. public-SDK direct submit. Two inverse pairs, all gated onstate == .readyToSubmit:addToNextVersion(NEW) →asc iris iap-submissions create --iap-id <id>— queues the IAP to ride along with the next App Store version submission. Always available for ready, not-yet-queued IAPs.removeFromNextVersion→asc iap unsubmit --submission-id <iapId>— dequeues. Mutually exclusive with the others; emitted only when iris listing reportssubmitWithNextAppStoreVersion: true.submit→asc iap submit --iap-id <id>— public-SDK direct submit (standalone review). Now emitted only whenisFirstTimeSubmission == false(Apple rejects the direct path for the first IAP for an app).- Established-app ready IAPs see both
submitandaddToNextVersion; agent picks based on release strategy. - Replaces the previous behavior where
submitquietly auto-dispatched between iris and public SDK paths — keys are now explicit about what each call does. NewInAppPurchaseState.hasBeenApprovedsemantic boolean. Newiris iap-submissionsREST route registered with the path resolver soaddToNextVersion_linksresolve to/api/v1/iris/iap/<id>/submissions.
Added
asc iris iap-submissions create --iap-id <id>— IAP submission via the iris private API — closes the parity gap with the public SDK'sPOST /v1/inAppPurchaseSubmissions, which has noattributesfield and therefore can't carrysubmitWithNextAppStoreVersion. The new command posts toPOST /iris/v1/inAppPurchaseSubmissionswith that attribute set, which is required for first-time IAP submissions (Apple binds the first IAP to the next App Store version). Defaults to--with-next-version; pass--no-with-next-versionto opt out. Lives underasc iris …because it needs iris cookies (either browser cookies or a session fromasc iris auth login). Surfaced viaIrisStatus.affordances.submitIAPsoasc iris statusadvertises the path when authenticated. Existingasc iap submitstays untouched — CI scripts using key-auth keep working unchanged. Seedocs/features/iap-subscriptions/submission-iris-parity.md.- REST:
POST /api/v1/iris/iap/:iapId/submissions— body{"submitWithNextAppStoreVersion": true}(default true); returns the submission resource with_links.viewIAPpointing back at the public IAP endpoint.
Fixed
- iris 2FA
verify-codeno longer 401s with a valid code —IdmsaAPIClient.applyHeaderswas sendingX-Apple-OAuth-Client-Id,X-Apple-OAuth-Redirect-URI,X-Apple-OAuth-Response-Mode,X-Apple-OAuth-Response-Type,Origin, andRefereronsignin/initandsignin/complete. None ofXcodesOrg/XcodesApp,XcodesOrg/XcodesLoginKit, or therorkai/App-Store-Connect-CLIGo reference send those — they taint the SRP session so the post-409GET /appleauth/authreturns 401, rotatesscnt, and the subsequentPOST /verify/trusteddevice/securitycodeis rejected as out-of-sequence. Header set is now narrowed toAccept,Content-Type,X-Requested-With,X-Apple-Widget-Key,scnt,X-Apple-ID-Session-Id(+X-Apple-HConsignin/complete).signinCompletealso flips to?isRememberMeEnabled=falsewith bodyrememberMe: falseto match the same references —trueputs Apple in the "trust this browser" path, which mismatches a CLI session.
Added
asc iris auth login/verify-code/logout— Apple ID SRP login for the iris private API — until now iris features (asc iris apps list,asc iris status) only worked when the user had logged into App Store Connect in their browser; we'd borrow cookies from there. Headless CI had no path. The new command performs a full Apple SRP-6a handshake againstidmsa.apple.com, handles 2FA (trusted-device push or phone code), then persists the resulting session to~/.asc/iris/session.json(0600 perms). NewCompositeIrisCookieProviderchains SRP-stored cookies before browser cookies, so any existing iris command immediately picks up the SRP session without code changes elsewhere. Seedocs/features/iris/iris-srp-login.md.asc iris auth login --apple-id user@example.com— prompts for password on stdin, runs SRP, persists session on success or pending-2FA state on 409.asc iris auth verify-code 123456— submits the 2FA code, calls2sv/trust, fetchesolympus/v1/sessionfor team metadata, persists the full session.asc iris auth logout— deletes the persisted session.ASC_IRIS_DEBUG=1— dumps every idmsa request/response to stderr witha/m1/m2redacted, so failures self-capture safely-shareable test data.- Built on
adam-fowler/swift-srp(Apache 2.0) for RFC 5054 group-2048 constants, BigNum, and key generation. Apple's PBKDF2-derivedxis layered on top inAppleSRPClient. PBKDF2-HMAC-SHA256 implementation cross-checked against RFC 7914 §11 vectors. - Caveat: while every layer is unit-tested with mocked HTTP and synthesized vectors, real-world correctness against Apple's specific
M1shape gets validated only on the first real login attempt. If Apple's protocol shifts, expect churn here.
asc v0.17.7
Added
- REST
POST/PATCH/DELETEfor subscription group localizations —SubscriptionGroupLocalizationsControllerpreviously exposed onlyGET, soPOST /api/v1/subscription-groups/:groupId/subscription-group-localizations(the call the AppNexus web UI emits when creating a new locale) returned 404. Now wired:POST /api/v1/subscription-groups/:groupId/subscription-group-localizations— body{locale, name, customAppName?}.PATCH /api/v1/subscription-group-localizations/:localizationId— body{name?, customAppName?}.DELETE /api/v1/subscription-group-localizations/:localizationId— returns{"deleted": true}.
uploadReviewScreenshotanduploadImageaffordances onInAppPurchaseandSubscription— until now an agent looking at an IAP or subscription with no review screenshot had no_linkspath to discover the upload command. The four new affordances close that gap (CLI surface, REST_links, REST endpoints).- REST upload endpoints (raw body) —
POST /api/v1/iap/:iapId/review-screenshot,POST /api/v1/iap/:iapId/images,POST /api/v1/subscriptions/:subscriptionId/review-screenshot,POST /api/v1/subscriptions/:subscriptionId/images. Send the image bytes as the request body (Content-Typeimage/png,image/jpeg, etc.); body is spooled to a temp file and uploaded through the existinguploadReviewScreenshot/uploadImagerepository methods. 20MB body ceiling. Same path as theGETcollection — verb selects read vs. upload.
Fixed
uploadImage/uploadReviewScreenshotnow wait for ASC processing before returning — ASC's PATCH-commit response carries an emptyimageAssetbecause processing is async; clients were getting{templateURL: "", width: 0, height: 0}and had no signal of when the asset URL would appear. BothSDKInAppPurchaseReviewRepositoryandSDKSubscriptionReviewRepositorynow mirror the iOS SDK pattern (AppStoreSdkverifyImageUpload/verifyReviewScreenshotUpload): after the commit PATCH, pollGET /v1/inAppPurchaseImages/{id}(or the screenshot equivalent) untilstate ∈ {PREPARE_FOR_SUBMISSION, WAITING_FOR_REVIEW, APPROVED}for images /assetDeliveryState == COMPLETEfor screenshots. Defaults: 15 attempts × 2s = 30s ceiling; failure states (FAILED,REJECTED) throw immediately. The mapper additionally treats emptytemplateURL/ zero dimensions as nil so any leftover not-ready shape is omitted from JSON viaencodeIfPresentrather than leaking. Result:POST /api/v1/iap/:iapId/imagesand the three sibling upload endpoints return a record whoseimageAssetis fully populated and ready to render — no client-side polling needed.
Changed
uploadaction resolves to bare collection path in REST —RESTPathResolvernow treatsuploadlikeget/update/delete(no extra suffix) so an upload_linkshref is/api/v1/iap/:iapId/review-screenshot(POST), not/...review-screenshot/upload. Standard REST: the verb carries the intent, the path names the resource.reviewNotenow read-side onInAppPurchaseandSubscription— the field was previously write-only viaasc iap update --review-note/asc subscriptions update --review-note. List/get responses now exposereviewNote: String?so an agent (or web UI like AppNexus's IAP/Subscription review tabs) can read the existing value without a separate fetch. SDK mappers threadattributes.reviewNotefromInAppPurchaseV2andSubscriptionSDK responses; nil values are omitted from JSON output viaencodeIfPresent. Existing JSON snapshots stay byte-identical when no review note is set.
asc v0.17.6
Fixed
PATCH /api/v1/iap/{iapId}/availabilityreturned 404 — the route was never registered, so the web frontend's "save territories" call failed.IAPAvailabilityControllernow servesPATCHwith body{territoryIds, availableInNewTerritories}and routes throughrepo.createAvailability(ASC'sPOST /v1/inAppPurchaseAvailabilitiesupserts — there is no separate update endpoint).
asc v0.17.5
Added
- Production / sandbox split for offer codes —
InAppPurchaseOfferCodeandSubscriptionOfferCodenow exposeproductionCodeCountandsandboxCodeCount(Apple returns these alongside the existing total). Visible inasc iap-offer-codes list,asc subscription-offer-codes list, and on the existingGET /api/v1/iap/:iapId/offer-codesandGET /api/v1/subscriptions/:subscriptionId/offer-codesendpoints (flow through Codable). environmenton one-time-use code batches —InAppPurchaseOfferCodeOneTimeUseCodeandSubscriptionOfferCodeOneTimeUseCodenow carryenvironment: OfferCodeEnvironment?(PRODUCTION or SANDBOX). Apple separates redemption by environment; sandbox batches redeem against sandbox tester accounts and have a smaller per-quarter ceiling.--environmentflag on one-time-codes create —asc iap-offer-code-one-time-codes create --environment sandboxandasc subscription-offer-code-one-time-codes create --environment sandbox. Default isproduction, matching prior behaviour. Maps to the SDK'sAttributes(environment:)field on the create-request body so sandbox batches can be generated from CLI.- REST endpoints for one-time-codes —
GET/POST /api/v1/iap-offer-codes/:offerCodeId/one-time-codes,PATCH /api/v1/iap-offer-code-one-time-codes/:oneTimeCodeId, and the subscription mirrors. POST body accepts{numberOfCodes, expirationDate, environment?}so REST clients can generate sandbox redemption batches without dropping to CLI. Each batch row's_linksresolveslistOneTimeCodesto the nested parent path. createOfferCodeaffordance + POST endpoints —InAppPurchaseandSubscriptionnow advertisecreateOfferCodein_links, mirroring the iOS app's "+ New offer code" affordance.POST /api/v1/iap/:iapId/offer-codesaccepts{name, customerEligibilities[]};POST /api/v1/subscriptions/:subscriptionId/offer-codesaccepts{name, duration, mode, periods, customerEligibilities[], offerEligibility}. Previously these were CLI-only.- Per-territory
priceson offer-code create (IAP + Subscription) — both CLI and REST now accept the full territory price list at creation time, matching ASC's actual contract. CLI:--price <territory>=<price-point-id>(repeatable, paid) and--free-territory <territory>(repeatable). REST:prices: [{territory, pricePointId?}]in POST body — omitpricePointIdfor free. New sharedOfferCodePriceInputdomain type. SDK adapters compose theincluded[InAppPurchaseOfferPriceInlineCreate]/included[SubscriptionOfferCodePriceInlineCreate]payload with${local-price-N}placeholder ids referenced fromrelationships.prices.data— same pattern as the existing IAP price-schedule create. Fixes a long-standing bug: previouscreateOfferCodecalls sent an emptypricesrelationship, so every offer code created via asc-cli was missing per-territory pricing (and ASC'spricesis read-only post-create — there was no recovery path). --auto-renewon subscription offer-code create — CLI flag (defaulttrue) and REST body fieldisAutoRenewEnabled(also acceptsautoRenew). Setting itfalsecreates a non-renewing/one-time offer; ASC only accepts--mode FREE_TRIALin that case. Previously not exposed.
asc v0.1.74
Added
- HATEOAS
_linksfor IAPs and Subscriptions —GET /api/v1/apps/{id}/iapandGET /api/v1/subscription-groups/{id}/subscriptionsnow embed_linksper item so an agent can navigate to localizations, availability, offer codes, price points, review screenshots, promotional offers, win-back offers (subscriptions only), and intro offers without knowing the URL conventions. - REST controllers for IAP details —
GET /api/v1/iap/:id/localizations,/availability,/offer-codes,/price-points. Each returns the agent-firstdata: [...]envelope with_linksalready populated. - REST controllers for Subscription details —
GET /api/v1/subscriptions/:id/localizations,/availability,/offer-codes,/introductory-offers. InAppPurchasePriceScheduleenriched withbaseTerritory+territoryPrices—GET /api/v1/iap/:id/price-schedulenow returns the developer-set base territory plus all auto-equalized per-territory prices (currency + customerPrice + proceeds) so the iOS Pricing tab has data to render. Composes three ASC API calls under the hood: schedule (with base territory), manual prices, and equalizations.- IAP equalizations primitive —
asc iap-equalizations list --price-point-id <id> [--limit N]andGET /api/v1/iap-price-points/:id/equalizations. ExposesGET /v1/inAppPurchasePricePoints/{id}/equalizations(~175 territory prices auto-derived from a manual base price). - Subscription price schedule — new
SubscriptionPriceScheduledomain type withterritoryPricesandprice(for:)lookup (no base territory — subscriptions are per-territory).asc subscription-price-schedule get --subscription-id <id>andGET /api/v1/subscriptions/:id/price-schedule. ComposesGET /v1/subscriptions/{id}/prices+ equalizations. - Subscription equalizations primitive —
asc subscription-equalizations list --price-point-id <id>andGET /api/v1/subscription-price-points/:id/equalizations. - Multi-territory subscription
setPrices(batch) —asc subscriptions prices set-batch --subscription-id <id> --price USA=spp-1 --price JPN=spp-2 .... Mirrors iOSsetPrices(prices:). Returns the post-write schedule. - Subscription promotional images —
asc subscription-images list|upload|deleteandGET /api/v1/subscriptions/:id/images. NewSubscriptionPromotionalImagedomain type withdeletesuppression whilestate.isPendingReview(mirrors IAP images). asc subscriptions update --period <PERIOD>— change billing period (ONE_WEEK, ONE_MONTH, …, ONE_YEAR) on an existing subscription. Mirrors the iOS app'ssubscription.update(subscriptionPeriod:).setPriceaffordance +POST /api/v1/iap/{id}/prices/set— IAPs now advertise how to set or change their base price/territory. Calling with a newbaseTerritoryis how the iOS app implements "Change Base Territory" — ASC replaces the schedule and re-equalizes other territories.setPricesaffordance +POST /api/v1/subscriptions/{id}/prices/set-batch— subscriptions now advertise the batch multi-territory price-set operation. Body accepts{ prices: [{territory, pricePointId, startDate?, preserveCurrentPrice?}] }(preferred) or a flat{ "USA": "spp-1", "JPN": "spp-2" }shorthand.- Cursor pagination for IAP/Subscription price points —
listPricePointsnow acceptslimit+cursorand returnsPaginatedResponse { data, nextCursor, totalCount }. CLI:asc iap price-points list --iap-id X --limit 200 --cursor <token>. REST:GET /api/v1/iap/{id}/price-points?territory=CHN&limit=200&cursor=<token>returns{ data, nextCursor, totalCount }. Frontends loop untilnextCursoris absent. Mirrors iOS'sloadPricePoints(territory:limit:cursor:) → PaginatedResult.
Fixed
POST/PATCH /api/v1/iap/{id}/localizationsreturned bare 500 on ASC validation failures + dropped over-limit content — when the description exceeded ASC's 45-character cap (or name exceeded 30), Hummingbird's default error path returned 500 with no body, browsers showed "Failed to fetch", and the user lost the typed text. The controller now (1) trimsnameto 30 grapheme clusters anddescriptionto 45 before sending, matching the iOS app'sLocaleCardlimit:constants, and (2) catches any remaining ASC error (e.g., invalid locale) and surfaces it as400 Bad Requestwith the underlying message so the UI can render a meaningful inline error.- IAP/Subscription availability returned only ~10 territories instead of all ~175 — the parent endpoint's
include=availableTerritoriestruncates the relationship to a single page.getAvailabilitynow issues two parallel calls (attributes + dedicated/availableTerritories?limit=200) so the full territory list reaches the frontend's Availability tab. Matches the iOS SDK'sfetchAvailabilitycomposition. SubscriptionGroup._linkswas empty —GET /api/v1/apps/{id}/subscription-groupsitems now embed populated_linksforlistSubscriptions,listLocalizations,createSubscription,createLocalization,update,delete. MigratedSubscriptionGroupfrom rawaffordancestostructuredAffordancessoapiLinksauto-derives, and registered_subscriptionGroupLocalizationRoutesinRESTPathResolver.ensureInitializedso the nested localizations path resolves./api/v1/subscription-groups/{id}/subscriptionsreturned 404 — the_links.listSubscriptionsURL had no controller. AddedSubscriptionsControllerand wired it inRESTRoutes.swift. Each subscription's_linksalready advertise the full child surface (localizations, availability, price-schedule, offers, etc.).- Review screenshots and promotional images returned no image URL — the responses included
id/fileName/fileSize/assetStatebut nothing renderable. AddedimageAsset: ImageAsset?toSubscriptionReviewScreenshot,InAppPurchaseReviewScreenshot,SubscriptionPromotionalImage,InAppPurchasePromotionalImage— populated from the SDK'sattributes.imageAsset(templateUrl + width + height). Frontend callsimageAsset.url(maxSize:format:)to substitute the{w}/{h}/{f}placeholders and produce a real CDN URL. SubscriptionPrice._linkswas empty — migratedSubscriptionPricefrom rawaffordancestostructuredAffordancessoapiLinksauto-derives.listPricePointsnow resolves to/api/v1/subscriptions/{id}/price-pointsover REST.POST /iap/{id}/prices/setreturned 500 — the inline manual-price payload omitted theinAppPurchaseV2back-reference and used a plain"p1"placeholder, both of which ASC rejects. The request now mirrors the iOS app shape:${local-manual-price-1}placeholder +inAppPurchaseV2relationship pointing back at the IAP. Also wrappedIAPPricesControllerin try/catch so future failures log the underlying ASC error to stderr and return a JSON error body instead of an empty 500.POST /api/v1/apps/{id}/iapreturned 404 — the route wasn't registered.IAPControllernow servesPOST(create — acceptsreferenceName,productId, andinAppPurchaseTypein either CLI-stylenon-consumableor raw enumNON_CONSUMABLE),PATCH /iap/{id}(update), andDELETE /iap/{id}. MirrorsVersionsController's POST/PATCH conventions.POST /api/v1/apps/{id}/subscription-groupsreturned 404 — same fix for subscription groups.SubscriptionGroupsControllernow servesPOST(create —referenceName),PATCH /subscription-groups/{id}(rename), andDELETE /subscription-groups/{id}.GET /api/v1/iap/{id}/availabilityreturned 500 for newly-created IAPs — ASC returns 404 until the developer creates the availability resource.getAvailability(iapId:)andgetAvailability(subscriptionId:)now returnOptionaland tolerate the 404 by returning nil; the SDK adapter mirrors iOS'srefreshTerritoryStatuses404 tolerance.- Availability tab showed empty list on fresh IAP/subscription — iOS shows all 175 territories preselected as the default UX; the web frontend got
data: []and rendered nothing.IAPAvailabilityControllerandSubscriptionAvailabilityControllernow synthesize a default-all-territories availability (withisAvailableInNewTerritories: true) by fetching/v1/territorieswhenever no resource exists. Frontend renders the same 175-territory grid iOS does.
Changed
RESTPathResolverresolves singleton-under-parentgetto nested path — when an action is notlist/create, the singularized own-id is missing, and a registered route's parent param matches one inparams, the resolver now returns the nested/parent/{id}/segmentpath. This corrects_links.getReviewScreenshotfor IAP and Subscription (was/api/v1/iap-review-screenshot/{id}→ now/api/v1/iap/{id}/review-screenshot) and similar singletons (availability, age-rating).AgeRatingControllernow serves the canonical nested path/api/v1/app-infos/{appInfoId}/age-ratingmatching the resolver's_links. The flat/api/v1/age-rating/{id}remains for back-compat.
asc v0.17.3
Added
- State-aware affordances on the new aggregates — affordances no longer suggest illegal next actions:
PromotedPurchaseexposesstate.isLocked(true whileWAITING_FOR_REVIEW/IN_REVIEW);updateanddeleteare suppressed while locked, so an agent following affordances won't issue a 409 against App Review.InAppPurchasePromotionalImagesuppressesdeletewhilestate.isPendingReviewis true.InAppPurchaseReviewScreenshot.AssetStateandSubscriptionReviewScreenshot.AssetStategainisComplete/hasFailedsemantic booleans.deleteis offered once the asset is reachable (uploadComplete/complete/failed); whileawaitingUpload, onlyuploadis offered as the recovery path.
- IAP & Subscription review screenshots + IAP promotional images — closes the upload-heavy feature parity gap. New CLI commands implement the standard ASC reserve → upload chunks → commit-with-MD5 protocol on top of
URLSession:asc iap-review-screenshot get|upload|delete— single review screenshot per IAP.getreturns an emptydata: []array when no screenshot is present, mirroring CAEOAS conventions.asc iap-images list|upload|delete— 1024×1024 promotional images for an IAP, with state semantic booleans (isApproved,isPendingReview).asc subscription-review-screenshot get|upload|delete— single review screenshot per subscription.- New domain types:
InAppPurchaseReviewScreenshot,InAppPurchasePromotionalImage(withImageStateenum),SubscriptionReviewScreenshot. New repository protocolsInAppPurchaseReviewRepository+SubscriptionReviewRepository, both@Mockable. InAppPurchasenow advertisesgetReviewScreenshot+listImages;SubscriptionadvertisesgetReviewScreenshot.
- Promoted purchases — App Store product page promoted slot CRUD. New CLI commands under
asc promoted-purchases:list --app-id <id>,create --app-id <id> (--iap-id <id> | --subscription-id <id>) [--visible|--hidden] [--enabled|--disabled],update --promoted-id <id> ...,delete --promoted-id <id>.- New
PromotedPurchasedomain type carryingappId+ eitherinAppPurchaseIdorsubscriptionId(mutually exclusive at create time, validated in the command). State enumPromotedPurchaseStatewith raw values matching ASC plusisLocked/isApprovedsemantic booleans. - Backed by
GET/POST /v1/apps/{id}/promotedPurchasesandPATCH/DELETE /v1/promotedPurchases/{id}.
- Win-back offers — full CRUD with eligibility rules, priority, promotion intent, and per-territory pricing. New CLI command tree under
asc win-back-offers:list --subscription-id <id>,delete --offer-id <id>,update --offer-id <id> [--priority HIGH|NORMAL] [--start-date ...] [--end-date ...] [--paid-months n] [--since-min n] [--since-max n] [--wait-months n] [--promotion-intent ...],prices list --offer-id <id>.create --subscription-id <id> --reference-name <name> --offer-id <id> --duration <d> --mode <m> --periods <n> --paid-months <n> --since-min <n> --since-max <n> --start-date <YYYY-MM-DD> --priority HIGH|NORMAL [--end-date ...] [--wait-months n] [--promotion-intent ...] [--price USA=spp-1 --price GBR=spp-2 ...]— bypasses the generated SDK's incompleteWinBackOfferPriceInlineCreate(missing relationships) by encoding the body manually with type-erasedAnyCodable.- Domain types:
WinBackOffer(withcustomerEligibilityTimeSinceLastSubscribedmin/max,WinBackOfferPriority,WinBackOfferPromotionIntent),WinBackOfferPrice,WinBackOfferPriceInput.SubscriptionadvertiseslistWinBackOffers.
- Subscription promotional offers — CRUD plus per-territory inline price creation. New CLI commands under
asc subscription-promotional-offers:list --subscription-id <id>,delete --offer-id <id>,prices list --offer-id <id>.create --subscription-id <id> --name <name> --offer-code <code> --duration <d> --mode <m> --periods <n> [--price USA=spp-1 ...]— uses${newPromoOfferPrice-N}1-based local IDs in theincludedarray, matching the ASC web UI request shape.- Domain types:
SubscriptionPromotionalOffer,SubscriptionPromotionalOfferPrice, sharedPromotionalOfferPriceInput.SubscriptionadvertisescreatePromotionalOffer+listPromotionalOffers.
- Subscription group localizations — per-locale display name and Custom App Name for a subscription group. New CLI command tree under
asc subscription-group-localizations:list --group-id <id>,create --group-id <id> --locale <code> --name <name> [--custom-app-name <name>],update --localization-id <id> [--name <name>] [--custom-app-name <name>], anddelete --localization-id <id>.- New
SubscriptionGroupLocalizationdomain type ({id, groupId, locale, name?, customAppName?, state?}) withlistSiblings/update/deleteaffordances. NewSubscriptionGroupLocalizationRepository@Mockableprotocol, SDK adapter on/v1/subscriptionGroupLocalizations. SubscriptionGroupnow advertisescreateLocalization/listLocalizationsso an agent navigating from a group can discover the localization tree.
- Offer code prices + one-time code values — completes the offer-code feature so an agent can read back per-territory pricing and download distributable redemption codes:
asc iap-offer-codes prices list --offer-code-id <id>andasc subscription-offer-codes prices list --offer-code-id <id>— returns each price withterritoryandpricePointId(IAP) orsubscriptionPricePointId(Subscription). Backed byGET /v1/inAppPurchaseOfferCodes/{id}/pricesandGET /v1/subscriptionOfferCodes/{id}/prices.asc iap-offer-code-one-time-codes values --one-time-code-id <id>andasc subscription-offer-code-one-time-codes values --one-time-code-id <id>— fetches the raw CSV body of one-time-use redemption codes for distribution. Backed byGET /v1/.../oneTimeUseCodes/{id}/values(Request<String>).- Two new domain types:
InAppPurchaseOfferCodePrice { id, offerCodeId, territory?, pricePointId? }andSubscriptionOfferCodePrice { id, offerCodeId, territory?, subscriptionPricePointId? }. Each advertises alistPricesaffordance that points back at its parent offer code. - Repository protocols extended with
listPrices(offerCodeId:)andfetchOneTimeUseCodeValues(oneTimeCodeId:) -> String; SDK adapters implement them on top of the appstoreconnect-swift-sdk.
- Subscription pricing parity — subscriptions now match IAP for browsing tiers and committing per-territory prices. Subscriptions don't have a base territory (Apple auto-equalizes), so the model has
proceedsYear2andprices setis per-territory rather than per-base. New CLI commands (mirrored underasc subscriptions):asc subscriptions price-points list --subscription-id <id> [--territory <code>]— listSubscriptionPricePoints with optional territory filter. Each result advertisessetPriceonly when a territory is attached.asc subscriptions prices set --subscription-id <id> --territory <code> --price-point-id <id> [--start-date YYYY-MM-DD] [--preserve-current-price]— POST/v1/subscriptionPricesto commit a price.- New domain models:
SubscriptionPricePoint(id, subscriptionId, territory, customerPrice, proceeds, proceedsYear2) andSubscriptionPrice(id, subscriptionId). - New
SubscriptionPriceRepository@Mockableprotocol withlistPricePoints+setPrice; SDK adapter wiresGET /v1/subscriptions/{id}/pricePointsandPOST /v1/subscriptionPrices.
- IAP & Subscription lifecycle parity — symmetric update/delete/unsubmit across the in-app-purchase and subscription aggregates so an agent can drive the full lifecycle, not just create-then-submit. New CLI commands:
asc iap-localizations update --localization-id <id> [--name <name>] [--description <desc>]andasc iap-localizations delete --localization-id <id>.asc subscription-localizations update --localization-id <id> [--name <name>] [--description <desc>]andasc subscription-localizations delete --localization-id <id>.asc iap update --iap-id <id> [--reference-name <name>] [--review-note <note>] [--family-sharable | --not-family-sharable],asc iap delete --iap-id <id>, andasc iap unsubmit --submission-id <id>(the SDK lacks a generated DELETE, so it goes through a manualRequest<Void>).asc subscriptions update --subscription-id <id> [--name <name>] [--family-sharable | --not-family-sharable] [--group-level <n>] [--review-note <note>],asc subscriptions delete --subscription-id <id>, andasc subscriptions unsubmit --submission-id <id>(manual DELETE same as IAP).asc subscription-groups update --group-id <id> --reference-name <name>andasc subscription-groups delete --group-id <id>.asc subscription-offers delete --offer-id <id>to drop an introductory offer.
- Affordance updates so the new commands surface in the JSON output of every list/create/submit response:
InAppPurchasenow advertisesupdate/delete,Subscriptionadvertisesupdate/delete,SubscriptionGroupadvertisesupdate/delete, both localization models advertiseupdate/delete, both submission models advertiseunsubmit, andSubscriptionIntroductoryOfferadvertisesdelete.
asc v0.17.2
Added
- Plugin update workflow (Sparkle-style) — check for and apply plugin updates via CLI or REST:
asc plugins updates(CLI) andGET /api/v1/plugins/updates(REST) — list every installed plugin where the marketplace has a newer version. Each entry is aPluginUpdate { name, installedVersion, latestVersion, repositoryURL?, downloadURL? }with affordances pointing atasc plugins update --name X(CLI) andPOST /api/v1/plugins/:name/update(REST).asc plugins update --name X(CLI) andPOST /api/v1/plugins/:name/update(REST) — uninstall the named plugin and reinstall the latest marketplace version. Returns the freshly installedPlugin.Plugin.affordancesaddscheckUpdate → asc plugins updatesfor installed plugins so frontends can wire a "Check for updates" entry without hard-coding the path.- New
PluginRepository.listOutdated()andupdate(name:)methods on the repository protocol; implemented inPluginMarketRepositoryby zippinglistInstalled()withlistAvailable()onname.
- Plugins REST install/uninstall/search —
PluginsControllernow mirrors the fullasc pluginsCLI:POST /api/v1/plugins— install a plugin from the marketplace. Body:{ "name": "Hello.plugin" }. Returns the installedPluginwithisInstalled: true.DELETE /api/v1/plugins/:name— uninstall by name (or slug). Returns204 No Content.GET /api/v1/plugins/market?q=<query>— search the marketplace. Withoutq, behaves like the existing list. Withq, callsPluginRepository.searchAvailable. Same response shape asGET /api/v1/plugins/market.- These endpoints back the install/uninstall/search affordances already advertised on
Plugin.affordances— frontends following affordances no longer hit a 404.
- Auth REST endpoints —
asc authis now drivable from a web client. NewAuthControllerexposes:POST /api/v1/auth/accounts(login) — body{ keyId, issuerId, privateKeyPEM, name?, vendorNumber? }. Saves to~/.asc/credentials.jsonand marks active. Returns the newAuthStatus.GET /api/v1/auth/accounts(list) — returns all savedConnectAccountrecords.GET /api/v1/auth/accounts/active(check) — returns the active account'sAuthStatus.PATCH /api/v1/auth/accounts/active(use) — body{ name }switches the active account, returns updatedAuthStatus.PATCH /api/v1/auth/accounts/:name(update) — body{ vendorNumber }updates a stored account.DELETE /api/v1/auth/accounts/activeandDELETE /api/v1/auth/accounts/:name(logout) — return204 No Content.- Security: the controller writes API key PEMs to disk via
AuthStorage. Runasc web-serverbound to loopback only when this controller is enabled; the request body is sensitive. AuthStatusandConnectAccountnow conform toPresentableso REST responses share the{"data":[…]}shape used elsewhere.
asc v0.17.1
Added
createVersionaffordance onApp— every app response now advertisesasc versions create --app-id <id>(REST:POST /api/v1/apps/{appId}/versions). Frontends driven by affordances (e.g. the command center UI) can use the presence of this key to enable a "Create Version" action without hard-coding capabilities.updateVersionaffordance on editableAppStoreVersion— versions inprepareForSubmissionstate exposeasc versions update --version-id <id>(REST:PATCH /api/v1/versions/{id}). Live and pending versions omit it, giving the UI a state-aware signal for the edit dialog.asc versions update --version-id <id> --version <string>— new CLI command to update an existing App Store version's version string. Backed by newVersionRepository.updateVersion(id:versionString:)which maps to the ASC SDKAppStoreVersionUpdateRequest.POST /api/v1/apps/{appId}/versionsandPATCH /api/v1/versions/{versionId}— REST endpoints for version create/update onVersionsController. Request body:{ "versionString": "...", "platform": "IOS" }for create,{ "versionString": "..." }for update.createLocalizationaffordance onAppInfo— every app-info response now advertises the locale-add endpoint. Unblocks the frontend's "+ Add Locale" button, which previously had no link to POST against.POST /api/v1/app-infos/{appInfoId}/localizations— create anAppInfoLocalizationvia REST. Body:{ "locale": "fr-FR", "name": "..." }. Returns the new row with its_links(updateLocalization,delete,listLocalizations).PATCH /api/v1/app-info-localizations/{localizationId}— updatename,subtitle,privacyPolicyUrl,privacyChoicesUrl,privacyPolicyText. Missing keys mean "don't change"; sending an empty string clears the field. Fixes the404the frontend was seeing when saving per-locale name / subtitle / privacy URLs.DELETE /api/v1/app-info-localizations/{localizationId}— delete a locale, returns204 No Content. Backs the "trash" button on the localization row.App.contentRightsDeclaration+ContentRightsDeclarationenum — apps now carry the third-party-content declaration (USES_THIRD_PARTY_CONTENT/DOES_NOT_USE_THIRD_PARTY_CONTENT). Field is optional and omitted from JSON when unset. This is the ASC-accurate mapping — the declaration lives onApp, not onAppInfo.asc apps update --app-id <id> --content-rights-declaration <value>+PATCH /api/v1/apps/{appId}— update content rights declaration via CLI or REST. Backed by newAppRepository.updateContentRights(appId:declaration:)which maps to the ASC SDKAppUpdateRequest.updateContentRightsaffordance onApp— every app response advertises the new PATCH endpoint so frontends can wire the declaration switch without hard-coding the URL.PATCH /api/v1/age-rating/{declarationId}— update an age rating declaration via REST. Body accepts any subset of theAgeRatingDeclarationUpdatefields (boolean flags,ContentIntensityvalues,kidsAgeBand,ageRatingOverride,koreaAgeRatingOverride). Fixes the404the frontend was seeing when following theupdate_linkon anAgeRatingDeclarationresponse. Matches the affordance key already advertised byAgeRatingDeclaration.structuredAffordances.