vydanne 0.6.0 → 0.7.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.
package/src/upload.mjs CHANGED
@@ -2,7 +2,9 @@ import crypto from "node:crypto";
2
2
  import fs from "node:fs";
3
3
  import path from "node:path";
4
4
 
5
- const md5 = (buf) => crypto.createHash("md5").update(buf).digest("hex");
5
+ // Exported because `diff` compares against it: the checksum committed here is the only thing that lets
6
+ // a local file be compared to what Apple actually holds, rather than to how MANY things it holds.
7
+ export const md5 = (buf) => crypto.createHash("md5").update(buf).digest("hex");
6
8
 
7
9
  // Native ASC asset upload (what spaceship does internally): reserve the asset (returns pre-signed
8
10
  // uploadOperations) -> PUT each chunk -> commit with uploaded:true + the MD5 checksum.
package/types/index.d.ts CHANGED
@@ -5,6 +5,19 @@ export type Platform = 'IOS' | 'MAC_OS';
5
5
 
6
6
  /** Every vydanne CLI command (kept in sync with src/registry.mjs by scripts/check-types.mjs). */
7
7
  export type CommandName =
8
+ /**
9
+ * Create (or reuse) the App Store version being prepared and attach the newest build to it.
10
+ * Lands in PREPARE_FOR_SUBMISSION with `releaseType: MANUAL` — it does not submit for review.
11
+ * Required first on an app with a version already on sale: only then does a draft exist for
12
+ * `fill` to write into. Override the version with VYDANNE_VERSION.
13
+ */
14
+ | 'prepare'
15
+ /**
16
+ * The whole release pipeline in its one working order: prepare → fill → previews → age-rating →
17
+ * review-contact → accessibility → preflight. Dry-run without --apply; stops at the first failure;
18
+ * never submits — Add to Review and Submit stay manual.
19
+ */
20
+ | 'push'
8
21
  | 'fill'
9
22
  | 'age-rating'
10
23
  | 'review-contact'
@@ -13,6 +26,12 @@ export type CommandName =
13
26
  | 'previews'
14
27
  | 'iap'
15
28
  | 'compliance'
29
+ /**
30
+ * Map zdymak's output (`store-assets/<locale>/<target>/NN-name.png`) onto the folders `fill`
31
+ * uploads from — ASC locale codes, device-slot filename prefixes, and the Play read paths.
32
+ * Local files only; `--dry-run` previews. Run after `zdymak screenshots`/`build`, before `fill`.
33
+ */
34
+ | 'bridge'
16
35
  | 'inspect'
17
36
  | 'diff'
18
37
  | 'preflight'
@@ -97,10 +116,103 @@ export interface GoogleConfig {
97
116
  defaultLocale?: string;
98
117
  /** `.aab` for `prerelease` — a file, or a directory whose NEWEST .aab is taken. Override: VYDANNE_AAB. */
99
118
  aab?: string;
100
- /** Testing track for `prerelease`: 'internal' (default) | 'alpha' | 'beta'. 'production' is refused. */
101
- track?: "internal" | "alpha" | "beta";
119
+ /**
120
+ * Testing track for `prerelease`. 'internal' (default), 'alpha', 'beta', or the name of any closed
121
+ * track you created in Play Console. Only 'production' is refused — that release is a human's.
122
+ */
123
+ track?: string;
124
+ /**
125
+ * Play image type -> local source path. Merged over the defaults, so declare only what differs.
126
+ * A type whose source does not exist is skipped; a missing local set never deletes the live one.
127
+ */
128
+ images?: Partial<Record<PlayImageType, string>>;
129
+ /**
130
+ * Locales to upload graphics to. Play holds images per language; the default is one set at
131
+ * `defaultLocale`. Pass a list, or '*' for every local listing folder. A `<source>/<lang>`
132
+ * subdirectory, when present, overrides the shared source for that language.
133
+ */
134
+ imageLocales?: string[] | "*";
135
+ }
136
+
137
+ /** Play listing image slots. */
138
+ export type PlayImageType =
139
+ | "icon"
140
+ | "featureGraphic"
141
+ | "tvBanner"
142
+ | "phoneScreenshots"
143
+ | "sevenInchScreenshots"
144
+ | "tenInchScreenshots"
145
+ | "wearScreenshots"
146
+ | "tvScreenshots";
147
+
148
+ /** Where `bridge` reads zdymak's output from, and which directory feeds which store slot. */
149
+ export interface BridgeConfig {
150
+ /** zdymak's output root. Read from zdymak.config.mjs when absent; './store-assets' otherwise. */
151
+ out?: string;
152
+ /**
153
+ * Screenshot slot token -> the zdymak output directory it comes from (a name, or names in
154
+ * preference order). Defaults cover zdymak's own target names; override when `dir:` in
155
+ * zdymak.config.mjs makes the directory name differ from the target name.
156
+ */
157
+ apple?: Record<string, string | string[]>;
158
+ /** Play image type -> the zdymak output directory it comes from. Same shape and reason. */
159
+ play?: Partial<Record<PlayImageType, string | string[]>>;
160
+ }
161
+
162
+ /** Overrides for the App Review contact. The PII itself stays in gitignored .txt files. */
163
+ export interface ReviewContactConfig {
164
+ /**
165
+ * Whether App Review needs a demo account. Inferred from the presence of
166
+ * `<metadataDir>/review_information/demo_user.txt` when omitted — set it only to disagree.
167
+ */
168
+ demoAccountRequired?: boolean;
169
+ }
170
+
171
+ /** `push` defaults. `--skip` adds to this per invocation. */
172
+ export interface PushConfig {
173
+ /** Steps this app never runs. 'prepare' and 'preflight' cannot be skipped. */
174
+ skip?: CommandName[];
175
+ }
176
+
177
+ /**
178
+ * Age-rating content descriptors, for any rating other than '4+'.
179
+ *
180
+ * Merged over an all-NONE base, so declare only what applies. Apple computes the rating band from
181
+ * these — you describe the content, it decides the number. Enum keys take
182
+ * 'NONE' | 'INFREQUENT_OR_MILD' | 'FREQUENT_OR_INTENSE'; the rest are booleans.
183
+ */
184
+ export interface AgeRatingConfig {
185
+ advertising?: boolean;
186
+ alcoholTobaccoOrDrugUseOrReferences?: AgeRatingLevel;
187
+ contests?: AgeRatingLevel;
188
+ gambling?: boolean;
189
+ gamblingSimulated?: AgeRatingLevel;
190
+ gunsOrOtherWeapons?: AgeRatingLevel;
191
+ healthOrWellnessTopics?: boolean;
192
+ kidsAgeBand?: "FIVE_AND_UNDER" | "SIX_TO_EIGHT" | "NINE_TO_ELEVEN" | null;
193
+ lootBox?: boolean;
194
+ medicalOrTreatmentInformation?: AgeRatingLevel;
195
+ messagingAndChat?: boolean;
196
+ parentalControls?: boolean;
197
+ profanityOrCrudeHumor?: AgeRatingLevel;
198
+ ageAssurance?: boolean;
199
+ sexualContentGraphicAndNudity?: AgeRatingLevel;
200
+ sexualContentOrNudity?: AgeRatingLevel;
201
+ socialMedia?: boolean;
202
+ socialMediaAgeRestricted?: boolean;
203
+ horrorOrFearThemes?: AgeRatingLevel;
204
+ matureOrSuggestiveThemes?: AgeRatingLevel;
205
+ unrestrictedWebAccess?: boolean;
206
+ userGeneratedContent?: boolean;
207
+ violenceCartoonOrFantasy?: AgeRatingLevel;
208
+ violenceRealisticProlongedGraphicOrSadistic?: AgeRatingLevel;
209
+ violenceRealistic?: AgeRatingLevel;
210
+ ageRatingOverrideV2?: AgeRatingLevel;
211
+ koreaAgeRatingOverride?: AgeRatingLevel;
102
212
  }
103
213
 
214
+ export type AgeRatingLevel = "NONE" | "INFREQUENT_OR_MILD" | "FREQUENT_OR_INTENSE";
215
+
104
216
  export interface ExportConfig {
105
217
  /** 'standard' → self-classify (ECCN 5D002, ENC 740.17(b)(1)); else no compliance doc is generated. */
106
218
  encryption: string;
@@ -109,6 +221,19 @@ export interface ExportConfig {
109
221
  appName?: string;
110
222
  version?: string;
111
223
  teamId?: string;
224
+ /**
225
+ * The app's cryptography inventory: [purpose, algorithm, keySize] per row. REQUIRED when
226
+ * `encryption` is 'standard' — `compliance` will not invent one, because the PDF it generates makes
227
+ * factual claims to a US export authority.
228
+ */
229
+ algorithms?: Array<[string, string, string?]>;
230
+ /** The statement paragraph, in your own words. REQUIRED when `encryption` is 'standard'. */
231
+ statement?: string;
232
+ /**
233
+ * Set true only once the report has ACTUALLY been emailed to BIS and the NSA. When false (default)
234
+ * the generated PDF does not claim it was submitted.
235
+ */
236
+ filed?: boolean;
112
237
  }
113
238
 
114
239
  /** The `vydanne.config.mjs` default export. */
@@ -126,9 +251,23 @@ export interface VydanneConfig {
126
251
  platforms?: Platform[];
127
252
  /** App UI locales; mapped to ASC codes (unsupported ones fall back to primary). */
128
253
  uiLocales?: string[];
254
+ /**
255
+ * App locale code -> App Store locale code, for codes Apple spells differently or does not know.
256
+ * Merged over the built-in table, so declare only your exceptions. An override naming a code Apple
257
+ * does not have is reported by `locales` and `preflight` rather than silently ignored.
258
+ */
259
+ localeMap?: Record<string, string>;
129
260
  metadataDir?: string;
130
- /** e.g. '4+'. */
261
+ /**
262
+ * Where each platform's screenshots live. Defaults to fastlane's supply convention
263
+ * ('fastlane/screenshots' and 'fastlane/screenshots-macos'). Read by `fill`, `diff`, `preflight`
264
+ * and written by `bridge`.
265
+ */
266
+ screenshots?: { IOS?: string; MAC_OS?: string };
267
+ /** e.g. '4+'. Anything else needs `ageRating` to say what makes it that. */
131
268
  rating?: string;
269
+ /** Content descriptors for a rating above '4+'. Merged over an all-NONE base. */
270
+ ageRating?: AgeRatingConfig;
132
271
  privacy?: PrivacyConfig;
133
272
  iaps?: IapConfig[];
134
273
  previews?: PreviewSpec[];
@@ -139,6 +278,12 @@ export interface VydanneConfig {
139
278
  ios?: IosConfig;
140
279
  /** Google Play (`--store google`): listings, screenshots, feature graphic via the Edits API. */
141
280
  google?: GoogleConfig;
281
+ /** Where `bridge` reads zdymak's output from, and which directory feeds which store slot. */
282
+ bridge?: BridgeConfig;
283
+ /** `push` defaults — steps this app never runs. */
284
+ push?: PushConfig;
285
+ /** App Review contact overrides (the PII itself stays in gitignored .txt files). */
286
+ reviewContact?: ReviewContactConfig;
142
287
  /**
143
288
  * Terms the cross-store check must not flag for this app.
144
289
  *
@@ -165,20 +310,90 @@ export declare class Client {
165
310
  post(path: string, body: unknown): Promise<{ status: number; json: any }>;
166
311
  patch(path: string, body: unknown): Promise<{ status: number; json: any }>;
167
312
  del(path: string): Promise<{ status: number; json: any }>;
168
- editVersion(platform: Platform): Promise<any>;
169
- appInfo(): Promise<any>;
313
+ /**
314
+ * The version being prepared, or null when none exists. `allowLive` (read-only commands only) falls
315
+ * back to the version on sale; writes must never target it.
316
+ */
317
+ editVersion(platform: Platform, opts?: { allowLive?: boolean }): Promise<any>;
318
+ /**
319
+ * The app-info being prepared (name/subtitle, age rating), or null when none exists. `allowLive`
320
+ * (read-only commands only) falls back to the live record; writes must never target it.
321
+ */
322
+ appInfo(opts?: { allowLive?: boolean }): Promise<any>;
170
323
  versionLocalizations(versionId: string): Promise<any[]>;
171
324
  localization(id: string, kind?: string): Promise<Record<string, unknown>>;
172
325
  }
173
326
 
174
- export declare function loadConfig(path?: string): Promise<VydanneConfig & { resolvedLocales: { supported: Record<string, string>; unsupported: string[] } }>;
327
+ /** Play Developer Edits API client. Every mutation happens inside an Edit; an uncommitted Edit is a no-op. */
328
+ export declare class PlayClient {
329
+ static create(opts: { keyPath: string; packageName: string; dryRun?: boolean }): Promise<PlayClient>;
330
+ dryRun: boolean;
331
+ newEdit(): Promise<string>;
332
+ validate(editId: string): Promise<{ status: number; json: any }>;
333
+ commit(editId: string): Promise<{ status: number; json: any }>;
334
+ deleteEdit(editId: string): Promise<unknown>;
335
+ getListings(editId: string): Promise<{ status: number; json: any }>;
336
+ putListing(editId: string, lang: string, body: Record<string, unknown>): Promise<{ status: number; json: any }>;
337
+ listImages(editId: string, lang: string, type: string): Promise<{ status: number; json: any }>;
338
+ deleteAllImages(editId: string, lang: string, type: string): Promise<unknown>;
339
+ uploadImage(editId: string, lang: string, type: string, file: string): Promise<unknown>;
340
+ }
341
+
342
+ export type Store = "apple" | "google";
343
+
344
+ /**
345
+ * Run one command, the way the CLI runs it — minus argv parsing and process.exit.
346
+ *
347
+ * `apply` defaults to FALSE, the same safety gate `--apply` drives: a caller that forgets it gets a
348
+ * dry run, never a write. `planned` is the machine-readable form of the writes a dry run withheld.
349
+ */
350
+ export declare function runCommand(
351
+ name: string,
352
+ opts?: {
353
+ config?: VydanneConfig;
354
+ configPath?: string;
355
+ store?: Store;
356
+ apply?: boolean;
357
+ },
358
+ ): Promise<{ ok: boolean; planned: Array<{ method: string; path: string; attributes: Record<string, unknown> }> }>;
359
+
360
+ export declare function loadConfig(path?: string): Promise<ResolvedConfig>;
175
361
  export declare function makeToken(opts: { keyId: string; issuerId: string; keyPath?: string }): string;
176
- export declare function resolveLocales(uiCodes: string[]): { supported: Record<string, string>; unsupported: string[] };
177
- export declare function toAsc(code: string): string | null;
362
+ export declare function resolveLocales(uiCodes: string[], extra?: Record<string, string>): ResolvedLocales;
363
+ export declare function toAsc(code: string, extra?: Record<string, string>): string | null;
178
364
  export declare const VALID: Set<string>;
365
+ export declare const UI_TO_ASC: Record<string, string>;
179
366
  export declare const CONFIG_KEYS: readonly string[];
180
367
  export declare const COMMAND_NAMES: readonly CommandName[];
181
- /** `writes` marks a command that mutates the STORE — those are dry-run unless the CLI gets `--apply`. */
182
- export declare const COMMANDS: Record<string, { mod: string; client: boolean; writes?: boolean }>;
183
368
 
369
+ /** Screenshot filename prefix -> ASC display type, per platform. */
370
+ export declare const IOS_DEVICE: Record<string, string>;
371
+ export declare const MAC_DEVICE: Record<string, string>;
372
+ export declare const DEFAULT_SCREENSHOT_BASE: { IOS: string; MAC_OS: string };
373
+ /** Where this platform's screenshots live: `screenshots` in the config, else the supply convention. */
374
+ export declare function screenshotBase(platform: Platform, config?: VydanneConfig): string;
375
+
376
+ export declare const DEFAULT_PLAY_IMAGES: Partial<Record<PlayImageType, string>>;
377
+ export declare const PLAY_IMAGE_KIND: Record<PlayImageType, "file" | "dir">;
378
+ /** The resolved Play image table for one app: [type, localSource, kind][]. */
379
+ export declare function playImages(config: VydanneConfig): Array<[string, string, "file" | "dir"]>;
380
+
381
+ /** `writes` marks a command that mutates the STORE — those are dry-run unless the CLI gets `--apply`.
382
+ * `credentials` marks the one command (prerelease) whose altool child authenticates itself. */
383
+ export declare const COMMANDS: Record<string, { mod: string; client?: boolean; credentials?: boolean; writes?: boolean }>;
384
+ /** The same, for `--store google`. Same names, different backend. */
385
+ export declare const PLAY_COMMANDS: Record<string, { mod: string; writes?: boolean }>;
386
+
387
+ export interface ResolvedLocales {
388
+ supported: Record<string, string>;
389
+ /** Codes with no App Store language — they fall back to the primary listing. */
390
+ unsupported: string[];
391
+ /** `localeMap` entries pointing at a code Apple does not have. A config mistake, not a missing language. */
392
+ invalid: string[];
393
+ }
394
+
395
+ export type ResolvedConfig = VydanneConfig & { resolvedLocales: ResolvedLocales };
396
+
397
+ // NOTE: this is a TYPE-only default export — `import type cfg from "vydanne"`. There is no runtime
398
+ // default export, so `import cfg from "vydanne"` in JS gets undefined. Import the named values.
184
399
  export default VydanneConfig;
@@ -12,9 +12,23 @@ export default {
12
12
  // The app's UI locales. vydanne maps each to its ASC App Store code (de -> de-DE, ar -> ar-SA …) and
13
13
  // flags any with no App Store language (e.g. Belarusian `be`) so you never abort an upload on a bad name.
14
14
  uiLocales: ["en", "de", "es", "fr", "ja", "zh-Hans"],
15
+ // Only for codes the built-in table doesn't cover — e.g. if your resource folders use Android's
16
+ // spellings. Merged over the defaults, and an entry naming a code Apple doesn't have is reported.
17
+ // localeMap: { nb: "no", iw: "he" },
15
18
  metadataDir: "fastlane/metadata",
19
+ // Defaults to fastlane's supply convention; set it if your repo puts them somewhere else.
20
+ // screenshots: { IOS: "fastlane/screenshots", MAC_OS: "fastlane/screenshots-macos" },
16
21
 
17
22
  rating: "4+",
23
+ // Only needed when `rating` is above 4+. Describe the CONTENT — Apple computes the band from it.
24
+ // Merged over an all-NONE base, so declare just what applies. Enum keys take
25
+ // NONE | INFREQUENT_OR_MILD | FREQUENT_OR_INTENSE; the rest are booleans.
26
+ // ageRating: { violenceCartoonOrFantasy: "INFREQUENT_OR_MILD", userGeneratedContent: false },
27
+
28
+ // App Review contact. The PII stays in the gitignored fastlane/metadata/review_information/*.txt —
29
+ // including demo_user.txt / demo_password.txt, which is how vydanne decides whether a demo account is
30
+ // required. Set this only to disagree with what's on disk.
31
+ // reviewContact: { demoAccountRequired: false },
18
32
 
19
33
  // Honest, minimal privacy — "accesses" is not "collects"; E2EE content the developer can't read is not
20
34
  // collected. Declare only what actually leaves the device to you.
@@ -36,7 +50,38 @@ export default {
36
50
  { platform: "IOS", type: "IPHONE_67", file: "marketing/out/appstore-preview.mp4", poster: "00:00:05:00", locales: ["en-GB", "en-US"] },
37
51
  ],
38
52
 
39
- export: { encryption: "standard", france: true, appName: "Example", version: "1.0", teamId: "ABCDE12345" },
53
+ // Export compliance. `algorithms` and `statement` are REQUIRED when encryption is "standard" — this
54
+ // generates a US export-compliance PDF that makes factual claims, so it will not guess them for you.
55
+ // `filed` stays false until you have actually emailed the report to BIS and the NSA; while it is
56
+ // false the PDF does not claim it was submitted.
57
+ export: {
58
+ encryption: "standard",
59
+ france: true,
60
+ appName: "Example",
61
+ version: "1.0",
62
+ teamId: "ABCDE12345",
63
+ algorithms: [
64
+ ["Transport", "TLS 1.2 / 1.3", "standard"],
65
+ ],
66
+ statement:
67
+ "The product uses TLS for network transport only. It is a mass-market consumer application " +
68
+ "distributed through public app stores, uses only standard published algorithms, and qualifies " +
69
+ "for export under License Exception ENC, EAR 740.17(b)(1), ECCN 5D002.",
70
+ filed: false,
71
+ },
72
+
73
+ // `bridge` maps zdymak's output onto the folders `fill` reads. Defaults cover zdymak's own directory
74
+ // names; override only when a `dir:` in zdymak.config.mjs makes the folder name differ from the
75
+ // target name (e.g. Play's 7" slot, which can only exist as `{ target: 'play-tablet', dir: … }`).
76
+ // bridge: {
77
+ // out: "./store-assets",
78
+ // apple: { iphone69: "appstore-iphone-6.9" },
79
+ // play: { sevenInchScreenshots: "play-tablet7-plain" },
80
+ // },
81
+
82
+ // Pipeline steps this app never runs. `--skip` adds to it per invocation; either way every skip is
83
+ // reported at the end, so a green `push` never overstates what it checked.
84
+ // push: { skip: ["accessibility"] },
40
85
 
41
86
  // Google Play (optional). Add this block + set PLAY_JSON_KEY_FILE to the service-account JSON, then run
42
87
  // any command with `--store google`. Package-SCOPED: vydanne only ever touches THIS packageName — a
@@ -52,5 +97,13 @@ export default {
52
97
  packageName: "com.example.app",
53
98
  metadataDir: "fastlane/metadata/android",
54
99
  defaultLocale: "en-GB",
100
+ // 'internal' (default), 'alpha', 'beta', or the name of any closed track you made in Play Console.
101
+ // Only 'production' is refused.
102
+ // track: "internal",
103
+ // Play image type -> local source. Merged over the defaults, so override only what differs.
104
+ // images: { icon: "brand/icons/play/icon-512.png", phoneScreenshots: "marketing/out/play-phone-plain" },
105
+ // Play holds graphics PER LANGUAGE. Default is one set at `defaultLocale`; list locales (or "*")
106
+ // to localize them, and put per-language files in `<source>/<lang>/`.
107
+ // imageLocales: ["en-GB", "de-DE"],
55
108
  },
56
109
  };