storemeta 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/.env.release.example +8 -0
  2. package/LICENSE +21 -0
  3. package/README.md +94 -0
  4. package/dist/auth/apple/credential-state.js +14 -0
  5. package/dist/auth/apple/jwt.js +56 -0
  6. package/dist/auth/apple/load-credentials.js +36 -0
  7. package/dist/auth/credential-state.js +6 -0
  8. package/dist/auth/google/credential-state.js +10 -0
  9. package/dist/auth/google/load-credentials.js +18 -0
  10. package/dist/auth/google/service-account-auth.js +25 -0
  11. package/dist/auth/redact.js +17 -0
  12. package/dist/cli/auth-check.js +46 -0
  13. package/dist/cli/config-doctor.js +40 -0
  14. package/dist/cli/context.js +23 -0
  15. package/dist/cli/errors.js +10 -0
  16. package/dist/cli/exit-code.js +6 -0
  17. package/dist/cli/init.js +60 -0
  18. package/dist/cli/locales-list.js +37 -0
  19. package/dist/cli/metadata-diff.js +57 -0
  20. package/dist/cli/metadata-pull.js +52 -0
  21. package/dist/cli/metadata-push.js +95 -0
  22. package/dist/cli/render.js +57 -0
  23. package/dist/cli/result-types.js +1 -0
  24. package/dist/cli/scaffold.js +76 -0
  25. package/dist/cli/screenshots-diff.js +65 -0
  26. package/dist/cli/screenshots-pull.js +72 -0
  27. package/dist/cli/screenshots-push.js +96 -0
  28. package/dist/cli/validate.js +74 -0
  29. package/dist/cli.js +222 -0
  30. package/dist/config/apps.js +21 -0
  31. package/dist/config/load-config.js +30 -0
  32. package/dist/config/schema.js +97 -0
  33. package/dist/config/select-app.js +10 -0
  34. package/dist/config/select-platforms.js +17 -0
  35. package/dist/config/single-app.js +16 -0
  36. package/dist/config/types.js +1 -0
  37. package/dist/config/validate-base-dirs.js +33 -0
  38. package/dist/config/validate-credential-env-names.js +32 -0
  39. package/dist/config/validate-platform-identifiers.js +24 -0
  40. package/dist/formats/load-metadata.js +38 -0
  41. package/dist/formats/metadata-types.js +1 -0
  42. package/dist/formats/screenshot-types.js +1 -0
  43. package/dist/formats/serialize-metadata.js +20 -0
  44. package/dist/locales/groups.js +12 -0
  45. package/dist/locales/map.js +15 -0
  46. package/dist/locales/normalize.js +22 -0
  47. package/dist/locales/types.js +1 -0
  48. package/dist/locales/validate-groups.js +14 -0
  49. package/dist/platforms/apple/client.js +109 -0
  50. package/dist/platforms/apple/metadata/pull.js +120 -0
  51. package/dist/platforms/apple/metadata/push.js +272 -0
  52. package/dist/platforms/apple/metadata/types.js +1 -0
  53. package/dist/platforms/apple/screenshots/pull.js +160 -0
  54. package/dist/platforms/apple/screenshots/push.js +418 -0
  55. package/dist/platforms/google/client.js +42 -0
  56. package/dist/platforms/google/edits.js +38 -0
  57. package/dist/platforms/google/metadata/pull.js +23 -0
  58. package/dist/platforms/google/metadata/push.js +73 -0
  59. package/dist/platforms/google/metadata/types.js +1 -0
  60. package/dist/platforms/google/screenshots/pull.js +129 -0
  61. package/dist/platforms/google/screenshots/push.js +252 -0
  62. package/dist/platforms/google/screenshots/types.js +7 -0
  63. package/dist/validation/metadata/apple.js +31 -0
  64. package/dist/validation/metadata/files.js +58 -0
  65. package/dist/validation/metadata/google.js +46 -0
  66. package/dist/validation/screenshots/extensions.js +17 -0
  67. package/dist/validation/screenshots/files.js +55 -0
  68. package/dist/validation/screenshots/folders.js +41 -0
  69. package/dist/validation/screenshots/order.js +25 -0
  70. package/dist/writers/ensure-directory.js +16 -0
  71. package/dist/writers/order-screenshots.js +19 -0
  72. package/dist/writers/resolve-screenshot-path.js +8 -0
  73. package/dist/writers/resolve-within-base-dir.js +17 -0
  74. package/dist/writers/write-metadata.js +17 -0
  75. package/docs/API_EDITABLE_METADATA_SURFACE.md +319 -0
  76. package/docs/AUTH_SETUP.md +67 -0
  77. package/docs/DOCUMENTATION.md +285 -0
  78. package/docs/PROJECT_PLAN.md +687 -0
  79. package/docs/RELEASE_VERIFICATION.md +70 -0
  80. package/docs/TODO.md +308 -0
  81. package/examples/README.md +8 -0
  82. package/examples/metadata/apple/en-US.yml +11 -0
  83. package/examples/metadata/apple/tr.yml +11 -0
  84. package/examples/metadata/google/en-US.yml +6 -0
  85. package/examples/metadata/google/tr.yml +6 -0
  86. package/examples/screenshots/apple/en-US/APP_IPHONE_65/1.png +1 -0
  87. package/examples/screenshots/apple/tr/APP_IPHONE_65/1.png +1 -0
  88. package/examples/screenshots/google/en-US/phoneScreenshots/1.png +1 -0
  89. package/examples/screenshots/google/tr/phoneScreenshots/1.png +1 -0
  90. package/examples/storemeta.yml +29 -0
  91. package/package.json +55 -0
  92. package/storemeta.release.example.yml +27 -0
@@ -0,0 +1,16 @@
1
+ import { mkdir } from "node:fs/promises";
2
+ import { dirname, resolve } from "node:path";
3
+ import { StoremetaError } from "../cli/errors.js";
4
+ export async function ensureDirectory(directoryPath) {
5
+ const resolvedPath = resolve(directoryPath);
6
+ try {
7
+ await mkdir(resolvedPath, { recursive: true });
8
+ }
9
+ catch (cause) {
10
+ throw new StoremetaError("FILESYSTEM_ERROR", `Failed to create directory at ${resolvedPath}`, { cause });
11
+ }
12
+ return resolvedPath;
13
+ }
14
+ export async function ensureParentDirectory(filePath) {
15
+ return ensureDirectory(dirname(resolve(filePath)));
16
+ }
@@ -0,0 +1,19 @@
1
+ import { extname } from "node:path";
2
+ function getCanonicalScreenshotExtension(fileName) {
3
+ const extension = extname(fileName).toLowerCase();
4
+ return extension.length > 0 ? extension : ".png";
5
+ }
6
+ export function orderScreenshotsForWrite(screenshots) {
7
+ return [...screenshots]
8
+ .sort((left, right) => {
9
+ if (left.position !== right.position) {
10
+ return left.position - right.position;
11
+ }
12
+ return left.fileName.localeCompare(right.fileName);
13
+ })
14
+ .map((screenshot, index) => ({
15
+ ...screenshot,
16
+ position: index + 1,
17
+ fileName: `${index + 1}${getCanonicalScreenshotExtension(screenshot.fileName)}`,
18
+ }));
19
+ }
@@ -0,0 +1,8 @@
1
+ import { normalizeLocaleCode } from "../locales/normalize.js";
2
+ import { resolvePathWithinBaseDir } from "./resolve-within-base-dir.js";
3
+ export function resolveScreenshotSetDirectory(baseDir, screenshotSet) {
4
+ return resolvePathWithinBaseDir(baseDir, screenshotSet.platform, normalizeLocaleCode(screenshotSet.locale), screenshotSet.assetType);
5
+ }
6
+ export function resolveScreenshotFilePath(baseDir, screenshot) {
7
+ return resolvePathWithinBaseDir(baseDir, screenshot.platform, normalizeLocaleCode(screenshot.locale), screenshot.assetType, screenshot.fileName);
8
+ }
@@ -0,0 +1,17 @@
1
+ import { isAbsolute, relative, resolve } from "node:path";
2
+ import { StoremetaError } from "../cli/errors.js";
3
+ export function assertPathWithinBaseDir(baseDir, targetPath) {
4
+ const resolvedBaseDir = resolve(baseDir);
5
+ const resolvedTargetPath = resolve(targetPath);
6
+ const relativePath = relative(resolvedBaseDir, resolvedTargetPath);
7
+ if (relativePath === "" ||
8
+ (!relativePath.startsWith("..") && !isAbsolute(relativePath))) {
9
+ return resolvedTargetPath;
10
+ }
11
+ throw new StoremetaError("FILESYSTEM_ERROR", `Resolved path escapes base directory: ${resolvedTargetPath}`);
12
+ }
13
+ export function resolvePathWithinBaseDir(baseDir, ...pathSegments) {
14
+ const resolvedBaseDir = resolve(baseDir);
15
+ const resolvedTargetPath = resolve(resolvedBaseDir, ...pathSegments);
16
+ return assertPathWithinBaseDir(resolvedBaseDir, resolvedTargetPath);
17
+ }
@@ -0,0 +1,17 @@
1
+ import { writeFile } from "node:fs/promises";
2
+ import { StoremetaError } from "../cli/errors.js";
3
+ import { serializeMetadataDocument } from "../formats/serialize-metadata.js";
4
+ import { ensureParentDirectory } from "./ensure-directory.js";
5
+ import { resolvePathWithinBaseDir } from "./resolve-within-base-dir.js";
6
+ export async function writeMetadataFile(baseDir, filePath, document) {
7
+ const resolvedPath = resolvePathWithinBaseDir(baseDir, filePath);
8
+ const serialized = serializeMetadataDocument(document);
9
+ try {
10
+ await ensureParentDirectory(resolvedPath);
11
+ await writeFile(resolvedPath, serialized, "utf8");
12
+ }
13
+ catch (cause) {
14
+ throw new StoremetaError("FILESYSTEM_ERROR", `Failed to write metadata file at ${resolvedPath}`, { cause });
15
+ }
16
+ return resolvedPath;
17
+ }
@@ -0,0 +1,319 @@
1
+ # API Editable Metadata Surface
2
+
3
+ This document lists the app metadata and store-asset areas that are editable through the public Apple and Google APIs, based on the official documentation reviewed on March 27, 2026.
4
+
5
+ Scope:
6
+ - app/store metadata and assets
7
+ - localized and non-localized surfaces
8
+ - public API-editable items only
9
+
10
+ Out of scope:
11
+ - binaries and track rollout mechanics except where directly tied to metadata
12
+ - in-app purchases and subscriptions
13
+ - account administration
14
+ - analytics and reporting
15
+
16
+ This document is intentionally conservative:
17
+ - `Confirmed` means the official docs reviewed clearly expose a writable public API path or request body.
18
+ - `Likely` means the reviewed docs strongly imply API support, but the exact writable field list was not fully visible from the sources reviewed.
19
+ - `Not confirmed` means the field exists in product/help docs, but public writable API support was not confirmed from the reviewed sources.
20
+
21
+ ## Apple
22
+
23
+ ### Localizable And Confirmed API-Editable
24
+
25
+ These are confirmed as part of the App Store Connect API metadata surface.
26
+
27
+ #### App info localization surface
28
+
29
+ - `name`
30
+ - `subtitle`
31
+
32
+ Resource model:
33
+ - `appInfoLocalizations`
34
+
35
+ Notes:
36
+ - These are shared app-info fields localized per locale.
37
+
38
+ #### App version localization surface
39
+
40
+ - `description`
41
+ - `keywords`
42
+ - `marketingUrl`
43
+ - `promotionalText`
44
+ - `supportUrl`
45
+ - `whatsNew`
46
+
47
+ Resource model:
48
+ - `appStoreVersionLocalizations`
49
+
50
+ Notes:
51
+ - These are localized per app version and locale.
52
+ - `whatsNew` is specifically version-oriented.
53
+
54
+ #### Localized visual assets
55
+
56
+ - screenshots
57
+ - app previews
58
+
59
+ Resource model:
60
+ - `appScreenshotSets`
61
+ - `appScreenshots`
62
+ - `appPreviewSets`
63
+ - `appPreviews`
64
+
65
+ Notes:
66
+ - These hang off app store version localizations.
67
+ - Screenshots are clearly part of the localized product-page surface.
68
+ - App previews are API-editable and are attached through preview sets.
69
+
70
+ ### Non-Localized And Confirmed API-Editable
71
+
72
+ #### App store version surface
73
+
74
+ - `versionString`
75
+ - `copyright`
76
+ - `releaseType`
77
+ - `earliestReleaseDate`
78
+
79
+ Resource model:
80
+ - `appStoreVersions`
81
+
82
+ Notes:
83
+ - These are version-scoped rather than localization-scoped.
84
+
85
+ #### Build linkage
86
+
87
+ - attached build for a version
88
+
89
+ Resource model:
90
+ - `appStoreVersions/{id}/relationships/build`
91
+
92
+ Notes:
93
+ - This is not a textual metadata field, but it is part of the version configuration surface.
94
+
95
+ #### App review information
96
+
97
+ - `contactFirstName`
98
+ - `contactLastName`
99
+ - `contactPhone`
100
+ - `contactEmail`
101
+ - `demoAccountName`
102
+ - `demoAccountPassword`
103
+ - `demoAccountRequired`
104
+ - `notes`
105
+
106
+ Resource model:
107
+ - `appStoreReviewDetails`
108
+
109
+ #### Age rating declaration
110
+
111
+ Confirmed API-addressable fields include:
112
+ - `advertising`
113
+ - `alcoholTobaccoOrDrugUseOrReferences`
114
+ - `contests`
115
+ - `gambling`
116
+ - `gamblingSimulated`
117
+ - `gunsOrOtherWeapons`
118
+ - `healthOrWellnessTopics`
119
+ - `kidsAgeBand`
120
+ - `lootBox`
121
+ - `medicalOrTreatmentInformation`
122
+ - `messagingAndChat`
123
+ - `parentalControls`
124
+ - `profanityOrCrudeHumor`
125
+ - `ageAssurance`
126
+ - `sexualContentGraphicAndNudity`
127
+ - `sexualContentOrNudity`
128
+ - `horrorOrFearThemes`
129
+ - `matureOrSuggestiveThemes`
130
+ - `unrestrictedWebAccess`
131
+ - `userGeneratedContent`
132
+ - `violenceCartoonOrFantasy`
133
+ - `violenceRealisticProlongedGraphicOrSadistic`
134
+ - `violenceRealistic`
135
+ - `ageRatingOverride`
136
+ - `ageRatingOverrideV2`
137
+ - `koreaAgeRatingOverride`
138
+ - `developerAgeRatingInfoUrl`
139
+
140
+ Resource model:
141
+ - `ageRatingDeclarations`
142
+
143
+ #### Accessibility declarations
144
+
145
+ Confirmed API-addressable fields include:
146
+ - `deviceFamily`
147
+ - `state`
148
+ - `supportsAudioDescriptions`
149
+ - `supportsCaptions`
150
+ - `supportsDarkInterface`
151
+ - `supportsDifferentiateWithoutColorAlone`
152
+ - `supportsLargerText`
153
+ - `supportsReducedMotion`
154
+ - `supportsSufficientContrast`
155
+ - `supportsVoiceControl`
156
+ - `supportsVoiceover`
157
+
158
+ Resource model:
159
+ - `accessibilityDeclarations`
160
+
161
+ ### Likely API-Editable, But Exact Writable Field List Not Fully Confirmed From Reviewed Sources
162
+
163
+ These appear to be API-addressable from the official docs reviewed, but the exact writable field list was not fully visible in the source snippets reviewed.
164
+
165
+ - primary category
166
+ - primary subcategories
167
+ - secondary category
168
+ - secondary subcategories
169
+
170
+ Resource hints:
171
+ - `appInfos`
172
+ - `appCategories`
173
+
174
+ Reason for caution:
175
+ - Apple’s category docs explicitly say to update categories through the `App Infos` resource, but the exact request schema was not fully visible in the reviewed sources.
176
+
177
+ ### Not Confirmed From Reviewed Sources
178
+
179
+ These exist in Apple’s product/help documentation, but writable public API support was not confirmed from the reviewed sources.
180
+
181
+ - `privacyPolicyUrl`
182
+ - `privacyChoicesUrl`
183
+ - custom license agreement text
184
+ - app privacy questionnaire data types
185
+
186
+ Reason for caution:
187
+ - They may be writable through other public endpoints, but that was not confirmed from the sources reviewed for this document.
188
+
189
+ ## Google Play
190
+
191
+ ### Localizable And Confirmed API-Editable
192
+
193
+ #### Listing text
194
+
195
+ - `title`
196
+ - `shortDescription`
197
+ - `fullDescription`
198
+ - `video`
199
+
200
+ Resource model:
201
+ - `edits.listings`
202
+
203
+ Notes:
204
+ - The listing resource is locale-specific.
205
+ - `video` is the promotional YouTube URL in the listing resource.
206
+
207
+ #### Localized graphic assets
208
+
209
+ - `phoneScreenshots`
210
+ - `sevenInchScreenshots`
211
+ - `tenInchScreenshots`
212
+ - `tvScreenshots`
213
+ - `wearScreenshots`
214
+ - `icon`
215
+ - `featureGraphic`
216
+ - `tvBanner`
217
+
218
+ Resource model:
219
+ - `edits.images`
220
+ - `AppImageType`
221
+
222
+ Notes:
223
+ - The images API is language-scoped.
224
+ - This means screenshots and the listed graphic asset types are part of the localized editable API surface.
225
+
226
+ ### Non-Localized And Confirmed API-Editable
227
+
228
+ - `defaultLanguage`
229
+ - `contactWebsite`
230
+ - `contactEmail`
231
+ - `contactPhone`
232
+
233
+ Resource model:
234
+ - `edits.details`
235
+
236
+ Notes:
237
+ - These are app-level details rather than locale-specific listing text.
238
+
239
+ ### Explicitly Not Supported Or Not Fully Editable Through The Publishing API
240
+
241
+ The official Google docs explicitly note some limits.
242
+
243
+ Notable examples:
244
+ - legal consents required for publishing are not fillable through the API
245
+ - changing app state from published to unpublished is not supported through this API
246
+
247
+ ## Practical V1 Implications For `storemeta`
248
+
249
+ ### Confirmed API Surface Worth Targeting
250
+
251
+ Apple localized:
252
+ - `name`
253
+ - `subtitle`
254
+ - `description`
255
+ - `keywords`
256
+ - `marketingUrl`
257
+ - `promotionalText`
258
+ - `supportUrl`
259
+ - `whatsNew`
260
+ - screenshots
261
+ - app previews
262
+
263
+ Apple non-localized:
264
+ - `versionString`
265
+ - `copyright`
266
+ - app review details
267
+
268
+ Google localized:
269
+ - `title`
270
+ - `shortDescription`
271
+ - `fullDescription`
272
+ - `video`
273
+ - screenshots
274
+ - `icon`
275
+ - `featureGraphic`
276
+ - `tvBanner`
277
+
278
+ Google non-localized:
279
+ - `defaultLanguage`
280
+ - `contactWebsite`
281
+ - `contactEmail`
282
+ - `contactPhone`
283
+
284
+ ### Current CLI Implementation Cut
285
+
286
+ The current `storemeta` CLI implements this subset:
287
+
288
+ Apple:
289
+ - app-info localization text
290
+ - app-version localization text
291
+ - screenshots
292
+
293
+ Google:
294
+ - listing text
295
+ - screenshots
296
+
297
+ Everything beyond that should be implemented only after confirming the exact write semantics in code and tests.
298
+
299
+ ## Sources
300
+
301
+ Apple:
302
+ - App information help: https://developer.apple.com/help/app-store-connect/reference/app-information/app-information
303
+ - Required, localizable, and editable properties: https://developer.apple.com/help/app-store-connect/reference/app-information/required-localizable-and-editable-properties
304
+ - App Store localizations: https://developer.apple.com/help/app-store-connect/reference/app-information/app-store-localizations
305
+ - App Store version localizations fields: https://developer.apple.com/documentation/appstoreconnectapi/appstoreversionlocalizationcreaterequest/data-data.dictionary/attributes-data.dictionary
306
+ - App Store review detail fields: https://developer.apple.com/documentation/appstoreconnectapi/appstorereviewdetail/attributes-data.dictionary
307
+ - App Store versions fields and related resources: https://developer.apple.com/documentation/appstoreconnectapi/get-v1-apps-_id_-appstoreversions
308
+ - App previews modify endpoint: https://developer.apple.com/documentation/appstoreconnectapi/patch-v1-apppreviews-_id_
309
+ - Build linkage endpoint: https://developer.apple.com/documentation/appstoreconnectapi/patch-v1-appstoreversions-_id_-relationships-build
310
+ - Categories overview: https://developer.apple.com/documentation/appstoreconnectapi/app-categories
311
+ - Accessibility declarations fields: https://developer.apple.com/documentation/appstoreconnectapi/get-v1-apps-_id_-accessibilitydeclarations
312
+ - Age rating modify endpoint: https://developer.apple.com/documentation/appstoreconnectapi/patch-v1-ageratingdeclarations-_id_
313
+
314
+ Google:
315
+ - Edits overview: https://developers.google.com/android-publisher/edits
316
+ - Listings resource: https://developers.google.com/android-publisher/api-ref/rest/v3/edits.listings
317
+ - Images resource: https://developers.google.com/android-publisher/api-ref/rest/v3/edits.images
318
+ - App image types: https://developers.google.com/android-publisher/api-ref/rest/v3/AppImageType
319
+ - App details resource: https://developers.google.com/android-publisher/api-ref/rest/v3/edits.details
@@ -0,0 +1,67 @@
1
+ # Auth Setup
2
+
3
+ `storemeta` reads credentials from environment variables. Do not put secret values directly in `storemeta.yml`.
4
+
5
+ ## Apple App Store Connect
6
+
7
+ Create an API key:
8
+
9
+ 1. Open App Store Connect.
10
+ 2. Go to Users and Access.
11
+ 3. Open Integrations.
12
+ 4. Open App Store Connect API.
13
+ 5. Create or select an API key with access to the target app.
14
+ 6. Copy the Issuer ID.
15
+ 7. Copy the Key ID.
16
+ 8. Download the `.p8` private key file.
17
+
18
+ Configure your shell:
19
+
20
+ ```bash
21
+ export STORE_APPLE_ISSUER_ID=00000000-0000-0000-0000-000000000000
22
+ export STORE_APPLE_KEY_ID=XXXXXXXXXX
23
+ export STORE_APPLE_PRIVATE_KEY_PATH=/absolute/path/to/AuthKey_XXXXXXXXXX.p8
24
+ ```
25
+
26
+ Common Apple errors:
27
+
28
+ - `401 Unauthorized`: issuer id, key id, and `.p8` file do not match, or the key does not have access to the app.
29
+ - Missing credential env var: the environment variable name in `storemeta.yml` is not exported in the current shell.
30
+ - File read failure: `STORE_APPLE_PRIVATE_KEY_PATH` does not point to a readable `.p8` file.
31
+
32
+ ## Google Play
33
+
34
+ Create a service account:
35
+
36
+ 1. Open Google Cloud Console.
37
+ 2. Create or select a project.
38
+ 3. Enable the Google Play Developer API.
39
+ 4. Create a service account.
40
+ 5. Create and download a JSON key for that service account.
41
+ 6. Open Google Play Console.
42
+ 7. Go to Users and permissions.
43
+ 8. Invite the service account email as a user.
44
+ 9. Grant access to the target app and the permissions needed for metadata and screenshots.
45
+
46
+ Configure your shell:
47
+
48
+ ```bash
49
+ export STORE_GOOGLE_SERVICE_ACCOUNT_PATH=/absolute/path/to/google-service-account.json
50
+ ```
51
+
52
+ Common Google errors:
53
+
54
+ - `401 Unauthorized`: service account JSON is invalid or cannot exchange an OAuth token.
55
+ - `403 Forbidden`: service account is not added to Play Console or lacks permission for the app.
56
+ - `400 Bad Request` for locale: Google Play expects locale codes such as `tr-TR`, not just `tr`.
57
+
58
+ ## Check Credentials
59
+
60
+ Run:
61
+
62
+ ```bash
63
+ storemeta auth check
64
+ storemeta validate
65
+ ```
66
+
67
+ `auth check` confirms that configured credential environment variables are present. `validate` checks config, metadata files, and screenshot layout.