Skip to content

Releases: tddworks/asc-cli

asc v0.18.0

13 May 11:41

Choose a tag to compare

Added

  • asc versions update now 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 underlying VersionRepository.updateVersion(...) already supported these fields; the CLI now wires them through. --version is 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

09 May 10:53

Choose a tag to compare

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 without ITSAppUsesNonExemptEncryption in Info.plist, ASC marks the build "Missing Compliance" and external testing is blocked. The new command PATCHes /v1/builds/{id} with usesNonExemptEncryption so the answer can be supplied post-upload from CI scripts that don't control the Info.plist. Build domain model now carries usesNonExemptEncryption: Bool? and a isMissingEncryptionCompliance semantic boolean (true when nil); usable builds in the missing state advertise a setEncryptionCompliance affordance with the ready-to-run command. Build's tableRow gains an "Encryption" column showing uses / exempt / missing. REST equivalent: PATCH /api/v1/builds/:buildId/encryption-compliance body {"usesNonExemptEncryption": true|false}. New BuildRepository.updateBuildEncryptionCompliance(buildId:usesNonExemptEncryption:). See docs/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's BetaAppLocalizations resource 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 existing asc builds update-beta-notes (which writes per-build "What to Test" notes) and asc beta-review (which writes review contact info). New BetaAppLocalization domain 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-localizations and GET /api/v1/beta-app-localizations/:id via BetaAppLocalizationsController. New REST path resolver entry for beta-app-localizations parented under apps. See docs/features/beta-app-localizations.md.

asc v0.17.8

29 Apr 12:22

Choose a tag to compare

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. New IrisClient.delete, new IrisInAppPurchaseSubmissionRepository.deleteSubmission(session:submissionId:). REST equivalent: DELETE /api/v1/iris/iap-submissions/:submissionId on IrisIAPSubmissionsController. removeFromNextVersion affordance 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.resourcePath now converts spaces in segment names to slashes so multi-word commands like iris iap-submissions produce clean URL segments. Removed the short-lived public-SDK alias POST /api/v1/iap/:iapId/unsubmit (incorrect — public DELETE doesn't accept iris-queued submissions).
  • removeFromNextVersion affordance on queued IAPs — when an IAP is in READY_TO_SUBMIT and Apple's iris listing reports submitWithNextAppStoreVersion: true (queued to ride along with the next app version), InAppPurchase.structuredAffordances surfaces removeFromNextVersionasc iap unsubmit --submission-id <iapId> and suppresses the submit affordance (mutually exclusive — Apple rejects re-submits on already-queued IAPs). The dequeue uses the existing public-SDK DELETE /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: IrisSDKInAppPurchaseStateRepository calls GET /iris/v1/apps/:appId/inAppPurchasesV2?fields[inAppPurchases]=submitWithNextAppStoreVersion&filter[state]=READY_TO_SUBMIT, mapping each IAP to its queued bit. SDKInAppPurchaseRepository.listInAppPurchases accepts an optional irisFlagsProvider closure; the factory wires it from IrisCookieProvider + IrisSDKInAppPurchaseStateRepository. CI scripts using API-key auth keep their existing JSON output (flag stays false, no removeFromNextVersion affordance). New InAppPurchase.submitWithNextAppStoreVersion: Bool field, encoded only when true to avoid snapshot churn. New IrisInAppPurchaseStateRepository @Mockable Domain protocol.

Changed

  • InAppPurchase submission affordances reorganized into iris-queue family vs. public-SDK direct submit. Two inverse pairs, all gated on state == .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.
    • removeFromNextVersionasc iap unsubmit --submission-id <iapId> — dequeues. Mutually exclusive with the others; emitted only when iris listing reports submitWithNextAppStoreVersion: true.
    • submitasc iap submit --iap-id <id> — public-SDK direct submit (standalone review). Now emitted only when isFirstTimeSubmission == false (Apple rejects the direct path for the first IAP for an app).
    • Established-app ready IAPs see both submit and addToNextVersion; agent picks based on release strategy.
    • Replaces the previous behavior where submit quietly auto-dispatched between iris and public SDK paths — keys are now explicit about what each call does. New InAppPurchaseState.hasBeenApproved semantic boolean. New iris iap-submissions REST route registered with the path resolver so addToNextVersion _links resolve 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's POST /v1/inAppPurchaseSubmissions, which has no attributes field and therefore can't carry submitWithNextAppStoreVersion. The new command posts to POST /iris/v1/inAppPurchaseSubmissions with 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-version to opt out. Lives under asc iris … because it needs iris cookies (either browser cookies or a session from asc iris auth login). Surfaced via IrisStatus.affordances.submitIAP so asc iris status advertises the path when authenticated. Existing asc iap submit stays untouched — CI scripts using key-auth keep working unchanged. See docs/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.viewIAP pointing back at the public IAP endpoint.

Fixed

  • iris 2FA verify-code no longer 401s with a valid codeIdmsaAPIClient.applyHeaders was sending X-Apple-OAuth-Client-Id, X-Apple-OAuth-Redirect-URI, X-Apple-OAuth-Response-Mode, X-Apple-OAuth-Response-Type, Origin, and Referer on signin/init and signin/complete. None of XcodesOrg/XcodesApp, XcodesOrg/XcodesLoginKit, or the rorkai/App-Store-Connect-CLI Go reference send those — they taint the SRP session so the post-409 GET /appleauth/auth returns 401, rotates scnt, and the subsequent POST /verify/trusteddevice/securitycode is rejected as out-of-sequence. Header set is now narrowed to Accept, Content-Type, X-Requested-With, X-Apple-Widget-Key, scnt, X-Apple-ID-Session-Id (+ X-Apple-HC on signin/complete). signinComplete also flips to ?isRememberMeEnabled=false with body rememberMe: false to match the same references — true puts 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 against idmsa.apple.com, handles 2FA (trusted-device push or phone code), then persists the resulting session to ~/.asc/iris/session.json (0600 perms). New CompositeIrisCookieProvider chains SRP-stored cookies before browser cookies, so any existing iris command immediately picks up the SRP session without code changes elsewhere. See docs/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, calls 2sv/trust, fetches olympus/v1/session for 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 with a/m1/m2 redacted, 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-derived x is layered on top in AppleSRPClient. 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 M1 shape gets validated only on the first real login attempt. If Apple's protocol shifts, expect churn here.

asc v0.17.7

29 Apr 04:49

Choose a tag to compare

Added

  • REST POST / PATCH / DELETE for subscription group localizationsSubscriptionGroupLocalizationsController previously exposed only GET, so POST /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}.
  • uploadReviewScreenshot and uploadImage affordances on InAppPurchase and Subscription — until now an agent looking at an IAP or subscription with no review screenshot had no _links path 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-Type image/png, image/jpeg, etc.); body is spooled to a temp file and uploaded through the existing uploadReviewScreenshot / uploadImage repository methods. 20MB body ceiling. Same path as the GET collection — verb selects read vs. upload.

Fixed

  • uploadImage / uploadReviewScreenshot now wait for ASC processing before returning — ASC's PATCH-commit response carries an empty imageAsset because processing is async; clients were getting {templateURL: "", width: 0, height: 0} and had no signal of when the asset URL would appear. Both SDKInAppPurchaseReviewRepository and SDKSubscriptionReviewRepository now mirror the iOS SDK pattern (AppStoreSdk verifyImageUpload / verifyReviewScreenshotUpload): after the commit PATCH, poll GET /v1/inAppPurchaseImages/{id} (or the screenshot equivalent) until state ∈ {PREPARE_FOR_SUBMISSION, WAITING_FOR_REVIEW, APPROVED} for images / assetDeliveryState == COMPLETE for screenshots. Defaults: 15 attempts × 2s = 30s ceiling; failure states (FAILED, REJECTED) throw immediately. The mapper additionally treats empty templateURL / zero dimensions as nil so any leftover not-ready shape is omitted from JSON via encodeIfPresent rather than leaking. Result: POST /api/v1/iap/:iapId/images and the three sibling upload endpoints return a record whose imageAsset is fully populated and ready to render — no client-side polling needed.

Changed

  • upload action resolves to bare collection path in RESTRESTPathResolver now treats upload like get/update/delete (no extra suffix) so an upload _links href is /api/v1/iap/:iapId/review-screenshot (POST), not /...review-screenshot/upload. Standard REST: the verb carries the intent, the path names the resource.
  • reviewNote now read-side on InAppPurchase and Subscription — the field was previously write-only via asc iap update --review-note / asc subscriptions update --review-note. List/get responses now expose reviewNote: 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 thread attributes.reviewNote from InAppPurchaseV2 and Subscription SDK responses; nil values are omitted from JSON output via encodeIfPresent. Existing JSON snapshots stay byte-identical when no review note is set.

asc v0.17.6

28 Apr 12:42

Choose a tag to compare

Fixed

  • PATCH /api/v1/iap/{iapId}/availability returned 404 — the route was never registered, so the web frontend's "save territories" call failed. IAPAvailabilityController now serves PATCH with body {territoryIds, availableInNewTerritories} and routes through repo.createAvailability (ASC's POST /v1/inAppPurchaseAvailabilities upserts — there is no separate update endpoint).

asc v0.17.5

28 Apr 06:26

Choose a tag to compare

Added

  • Production / sandbox split for offer codesInAppPurchaseOfferCode and SubscriptionOfferCode now expose productionCodeCount and sandboxCodeCount (Apple returns these alongside the existing total). Visible in asc iap-offer-codes list, asc subscription-offer-codes list, and on the existing GET /api/v1/iap/:iapId/offer-codes and GET /api/v1/subscriptions/:subscriptionId/offer-codes endpoints (flow through Codable).
  • environment on one-time-use code batchesInAppPurchaseOfferCodeOneTimeUseCode and SubscriptionOfferCodeOneTimeUseCode now carry environment: OfferCodeEnvironment? (PRODUCTION or SANDBOX). Apple separates redemption by environment; sandbox batches redeem against sandbox tester accounts and have a smaller per-quarter ceiling.
  • --environment flag on one-time-codes createasc iap-offer-code-one-time-codes create --environment sandbox and asc subscription-offer-code-one-time-codes create --environment sandbox. Default is production, matching prior behaviour. Maps to the SDK's Attributes(environment:) field on the create-request body so sandbox batches can be generated from CLI.
  • REST endpoints for one-time-codesGET/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 _links resolves listOneTimeCodes to the nested parent path.
  • createOfferCode affordance + POST endpointsInAppPurchase and Subscription now advertise createOfferCode in _links, mirroring the iOS app's "+ New offer code" affordance. POST /api/v1/iap/:iapId/offer-codes accepts {name, customerEligibilities[]}; POST /api/v1/subscriptions/:subscriptionId/offer-codes accepts {name, duration, mode, periods, customerEligibilities[], offerEligibility}. Previously these were CLI-only.
  • Per-territory prices on 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 — omit pricePointId for free. New shared OfferCodePriceInput domain type. SDK adapters compose the included[InAppPurchaseOfferPriceInlineCreate] / included[SubscriptionOfferCodePriceInlineCreate] payload with ${local-price-N} placeholder ids referenced from relationships.prices.data — same pattern as the existing IAP price-schedule create. Fixes a long-standing bug: previous createOfferCode calls sent an empty prices relationship, so every offer code created via asc-cli was missing per-territory pricing (and ASC's prices is read-only post-create — there was no recovery path).
  • --auto-renew on subscription offer-code create — CLI flag (default true) and REST body field isAutoRenewEnabled (also accepts autoRenew). Setting it false creates a non-renewing/one-time offer; ASC only accepts --mode FREE_TRIAL in that case. Previously not exposed.

asc v0.1.74

27 Apr 12:19

Choose a tag to compare

Added

  • HATEOAS _links for IAPs and SubscriptionsGET /api/v1/apps/{id}/iap and GET /api/v1/subscription-groups/{id}/subscriptions now embed _links per 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 detailsGET /api/v1/iap/:id/localizations, /availability, /offer-codes, /price-points. Each returns the agent-first data: [...] envelope with _links already populated.
  • REST controllers for Subscription detailsGET /api/v1/subscriptions/:id/localizations, /availability, /offer-codes, /introductory-offers.
  • InAppPurchasePriceSchedule enriched with baseTerritory + territoryPricesGET /api/v1/iap/:id/price-schedule now 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 primitiveasc iap-equalizations list --price-point-id <id> [--limit N] and GET /api/v1/iap-price-points/:id/equalizations. Exposes GET /v1/inAppPurchasePricePoints/{id}/equalizations (~175 territory prices auto-derived from a manual base price).
  • Subscription price schedule — new SubscriptionPriceSchedule domain type with territoryPrices and price(for:) lookup (no base territory — subscriptions are per-territory). asc subscription-price-schedule get --subscription-id <id> and GET /api/v1/subscriptions/:id/price-schedule. Composes GET /v1/subscriptions/{id}/prices + equalizations.
  • Subscription equalizations primitiveasc subscription-equalizations list --price-point-id <id> and GET /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 iOS setPrices(prices:). Returns the post-write schedule.
  • Subscription promotional imagesasc subscription-images list|upload|delete and GET /api/v1/subscriptions/:id/images. New SubscriptionPromotionalImage domain type with delete suppression while state.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's subscription.update(subscriptionPeriod:).
  • setPrice affordance + POST /api/v1/iap/{id}/prices/set — IAPs now advertise how to set or change their base price/territory. Calling with a new baseTerritory is how the iOS app implements "Change Base Territory" — ASC replaces the schedule and re-equalizes other territories.
  • setPrices affordance + 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 pointslistPricePoints now accepts limit + cursor and returns PaginatedResponse { 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 until nextCursor is absent. Mirrors iOS's loadPricePoints(territory:limit:cursor:) → PaginatedResult.

Fixed

  • POST/PATCH /api/v1/iap/{id}/localizations returned 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) trims name to 30 grapheme clusters and description to 45 before sending, matching the iOS app's LocaleCard limit: constants, and (2) catches any remaining ASC error (e.g., invalid locale) and surfaces it as 400 Bad Request with 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=availableTerritories truncates the relationship to a single page. getAvailability now issues two parallel calls (attributes + dedicated /availableTerritories?limit=200) so the full territory list reaches the frontend's Availability tab. Matches the iOS SDK's fetchAvailability composition.
  • SubscriptionGroup._links was emptyGET /api/v1/apps/{id}/subscription-groups items now embed populated _links for listSubscriptions, listLocalizations, createSubscription, createLocalization, update, delete. Migrated SubscriptionGroup from raw affordances to structuredAffordances so apiLinks auto-derives, and registered _subscriptionGroupLocalizationRoutes in RESTPathResolver.ensureInitialized so the nested localizations path resolves.
  • /api/v1/subscription-groups/{id}/subscriptions returned 404 — the _links.listSubscriptions URL had no controller. Added SubscriptionsController and wired it in RESTRoutes.swift. Each subscription's _links already 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/assetState but nothing renderable. Added imageAsset: ImageAsset? to SubscriptionReviewScreenshot, InAppPurchaseReviewScreenshot, SubscriptionPromotionalImage, InAppPurchasePromotionalImage — populated from the SDK's attributes.imageAsset (templateUrl + width + height). Frontend calls imageAsset.url(maxSize:format:) to substitute the {w}/{h}/{f} placeholders and produce a real CDN URL.
  • SubscriptionPrice._links was empty — migrated SubscriptionPrice from raw affordances to structuredAffordances so apiLinks auto-derives. listPricePoints now resolves to /api/v1/subscriptions/{id}/price-points over REST.
  • POST /iap/{id}/prices/set returned 500 — the inline manual-price payload omitted the inAppPurchaseV2 back-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 + inAppPurchaseV2 relationship pointing back at the IAP. Also wrapped IAPPricesController in 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}/iap returned 404 — the route wasn't registered. IAPController now serves POST (create — accepts referenceName, productId, and inAppPurchaseType in either CLI-style non-consumable or raw enum NON_CONSUMABLE), PATCH /iap/{id} (update), and DELETE /iap/{id}. Mirrors VersionsController's POST/PATCH conventions.
  • POST /api/v1/apps/{id}/subscription-groups returned 404 — same fix for subscription groups. SubscriptionGroupsController now serves POST (create — referenceName), PATCH /subscription-groups/{id} (rename), and DELETE /subscription-groups/{id}.
  • GET /api/v1/iap/{id}/availability returned 500 for newly-created IAPs — ASC returns 404 until the developer creates the availability resource. getAvailability(iapId:) and getAvailability(subscriptionId:) now return Optional and tolerate the 404 by returning nil; the SDK adapter mirrors iOS's refreshTerritoryStatuses 404 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. IAPAvailabilityController and SubscriptionAvailabilityController now synthesize a default-all-territories availability (with isAvailableInNewTerritories: true) by fetching /v1/territories whenever no resource exists. Frontend renders the same 175-territory grid iOS does.

Changed

  • RESTPathResolver resolves singleton-under-parent get to nested path — when an action is not list/create, the singularized own-id is missing, and a registered route's parent param matches one in params, the resolver now returns the nested /parent/{id}/segment path. This corrects _links.getReviewScreenshot for IAP and Subscription (was /api/v1/iap-review-screenshot/{id} → now /api/v1/iap/{id}/review-screenshot) and similar singletons (availability, age-rating).
  • AgeRatingController now serves the canonical nested path /api/v1/app-infos/{appInfoId}/age-rating matching the resolver's _links. The flat /api/v1/age-rating/{id} remains for back-compat.

asc v0.17.3

26 Apr 10:46

Choose a tag to compare

Added

  • State-aware affordances on the new aggregates — affordances no longer suggest illegal next actions:
    • PromotedPurchase exposes state.isLocked (true while WAITING_FOR_REVIEW / IN_REVIEW); update and delete are suppressed while locked, so an agent following affordances won't issue a 409 against App Review.
    • InAppPurchasePromotionalImage suppresses delete while state.isPendingReview is true.
    • InAppPurchaseReviewScreenshot.AssetState and SubscriptionReviewScreenshot.AssetState gain isComplete / hasFailed semantic booleans. delete is offered once the asset is reachable (uploadComplete / complete / failed); while awaitingUpload, only upload is 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. get returns an empty data: [] 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 (with ImageState enum), SubscriptionReviewScreenshot. New repository protocols InAppPurchaseReviewRepository + SubscriptionReviewRepository, both @Mockable.
    • InAppPurchase now advertises getReviewScreenshot + listImages; Subscription advertises getReviewScreenshot.
  • 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 PromotedPurchase domain type carrying appId + either inAppPurchaseId or subscriptionId (mutually exclusive at create time, validated in the command). State enum PromotedPurchaseState with raw values matching ASC plus isLocked / isApproved semantic booleans.
    • Backed by GET/POST /v1/apps/{id}/promotedPurchases and PATCH/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 incomplete WinBackOfferPriceInlineCreate (missing relationships) by encoding the body manually with type-erased AnyCodable.
    • Domain types: WinBackOffer (with customerEligibilityTimeSinceLastSubscribed min/max, WinBackOfferPriority, WinBackOfferPromotionIntent), WinBackOfferPrice, WinBackOfferPriceInput. Subscription advertises listWinBackOffers.
  • 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 the included array, matching the ASC web UI request shape.
    • Domain types: SubscriptionPromotionalOffer, SubscriptionPromotionalOfferPrice, shared PromotionalOfferPriceInput. Subscription advertises createPromotionalOffer + 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>], and delete --localization-id <id>.
    • New SubscriptionGroupLocalization domain type ({id, groupId, locale, name?, customAppName?, state?}) with listSiblings/update/delete affordances. New SubscriptionGroupLocalizationRepository @Mockable protocol, SDK adapter on /v1/subscriptionGroupLocalizations.
    • SubscriptionGroup now advertises createLocalization/listLocalizations so 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> and asc subscription-offer-codes prices list --offer-code-id <id> — returns each price with territory and pricePointId (IAP) or subscriptionPricePointId (Subscription). Backed by GET /v1/inAppPurchaseOfferCodes/{id}/prices and GET /v1/subscriptionOfferCodes/{id}/prices.
    • asc iap-offer-code-one-time-codes values --one-time-code-id <id> and asc 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 by GET /v1/.../oneTimeUseCodes/{id}/values (Request<String>).
    • Two new domain types: InAppPurchaseOfferCodePrice { id, offerCodeId, territory?, pricePointId? } and SubscriptionOfferCodePrice { id, offerCodeId, territory?, subscriptionPricePointId? }. Each advertises a listPrices affordance that points back at its parent offer code.
    • Repository protocols extended with listPrices(offerCodeId:) and fetchOneTimeUseCodeValues(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 proceedsYear2 and prices set is per-territory rather than per-base. New CLI commands (mirrored under asc subscriptions):
    • asc subscriptions price-points list --subscription-id <id> [--territory <code>] — list SubscriptionPricePoints with optional territory filter. Each result advertises setPrice only 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/subscriptionPrices to commit a price.
    • New domain models: SubscriptionPricePoint (id, subscriptionId, territory, customerPrice, proceeds, proceedsYear2) and SubscriptionPrice (id, subscriptionId).
    • New SubscriptionPriceRepository @Mockable protocol with listPricePoints + setPrice; SDK adapter wires GET /v1/subscriptions/{id}/pricePoints and POST /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>] and asc iap-localizations delete --localization-id <id>.
    • asc subscription-localizations update --localization-id <id> [--name <name>] [--description <desc>] and asc 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>, and asc iap unsubmit --submission-id <id> (the SDK lacks a generated DELETE, so it goes through a manual Request<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>, and asc subscriptions unsubmit --submission-id <id> (manual DELETE same as IAP).
    • asc subscription-groups update --group-id <id> --reference-name <name> and asc 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: InAppPurchase now advertises update/delete, Subscription advertises update/delete, SubscriptionGroup advertises update/delete, both localization models advertise update/delete, both submission models advertise unsubmit, and SubscriptionIntroductoryOffer advertises delete.

asc v0.17.2

25 Apr 09:48

Choose a tag to compare

Added

  • Plugin update workflow (Sparkle-style) — check for and apply plugin updates via CLI or REST:
    • asc plugins updates (CLI) and GET /api/v1/plugins/updates (REST) — list every installed plugin where the marketplace has a newer version. Each entry is a PluginUpdate { name, installedVersion, latestVersion, repositoryURL?, downloadURL? } with affordances pointing at asc plugins update --name X (CLI) and POST /api/v1/plugins/:name/update (REST).
    • asc plugins update --name X (CLI) and POST /api/v1/plugins/:name/update (REST) — uninstall the named plugin and reinstall the latest marketplace version. Returns the freshly installed Plugin.
    • Plugin.affordances adds checkUpdate → asc plugins updates for installed plugins so frontends can wire a "Check for updates" entry without hard-coding the path.
    • New PluginRepository.listOutdated() and update(name:) methods on the repository protocol; implemented in PluginMarketRepository by zipping listInstalled() with listAvailable() on name.
  • Plugins REST install/uninstall/searchPluginsController now mirrors the full asc plugins CLI:
    • POST /api/v1/plugins — install a plugin from the marketplace. Body: { "name": "Hello.plugin" }. Returns the installed Plugin with isInstalled: true.
    • DELETE /api/v1/plugins/:name — uninstall by name (or slug). Returns 204 No Content.
    • GET /api/v1/plugins/market?q=<query> — search the marketplace. Without q, behaves like the existing list. With q, calls PluginRepository.searchAvailable. Same response shape as GET /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 endpointsasc auth is now drivable from a web client. New AuthController exposes:
    • POST /api/v1/auth/accounts (login) — body { keyId, issuerId, privateKeyPEM, name?, vendorNumber? }. Saves to ~/.asc/credentials.json and marks active. Returns the new AuthStatus.
    • GET /api/v1/auth/accounts (list) — returns all saved ConnectAccount records.
    • GET /api/v1/auth/accounts/active (check) — returns the active account's AuthStatus.
    • PATCH /api/v1/auth/accounts/active (use) — body { name } switches the active account, returns updated AuthStatus.
    • PATCH /api/v1/auth/accounts/:name (update) — body { vendorNumber } updates a stored account.
    • DELETE /api/v1/auth/accounts/active and DELETE /api/v1/auth/accounts/:name (logout) — return 204 No Content.
    • Security: the controller writes API key PEMs to disk via AuthStorage. Run asc web-server bound to loopback only when this controller is enabled; the request body is sensitive.
    • AuthStatus and ConnectAccount now conform to Presentable so REST responses share the {"data":[…]} shape used elsewhere.

asc v0.17.1

24 Apr 14:38

Choose a tag to compare

Added

  • createVersion affordance on App — every app response now advertises asc 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.
  • updateVersion affordance on editable AppStoreVersion — versions in prepareForSubmission state expose asc 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 new VersionRepository.updateVersion(id:versionString:) which maps to the ASC SDK AppStoreVersionUpdateRequest.
  • POST /api/v1/apps/{appId}/versions and PATCH /api/v1/versions/{versionId} — REST endpoints for version create/update on VersionsController. Request body: { "versionString": "...", "platform": "IOS" } for create, { "versionString": "..." } for update.
  • createLocalization affordance on AppInfo — 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 an AppInfoLocalization via REST. Body: { "locale": "fr-FR", "name": "..." }. Returns the new row with its _links (updateLocalization, delete, listLocalizations).
  • PATCH /api/v1/app-info-localizations/{localizationId} — update name, subtitle, privacyPolicyUrl, privacyChoicesUrl, privacyPolicyText. Missing keys mean "don't change"; sending an empty string clears the field. Fixes the 404 the frontend was seeing when saving per-locale name / subtitle / privacy URLs.
  • DELETE /api/v1/app-info-localizations/{localizationId} — delete a locale, returns 204 No Content. Backs the "trash" button on the localization row.
  • App.contentRightsDeclaration + ContentRightsDeclaration enum — 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 on App, not on AppInfo.
  • 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 new AppRepository.updateContentRights(appId:declaration:) which maps to the ASC SDK AppUpdateRequest.
  • updateContentRights affordance on App — 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 the AgeRatingDeclarationUpdate fields (boolean flags, ContentIntensity values, kidsAgeBand, ageRatingOverride, koreaAgeRatingOverride). Fixes the 404 the frontend was seeing when following the update _link on an AgeRatingDeclaration response. Matches the affordance key already advertised by AgeRatingDeclaration.structuredAffordances.