@nomac/cli 0.2.4 → 0.2.5
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/package.json +1 -1
- package/skill/SKILL.md +54 -5
- package/src/lifecycle-schemas.json +416 -0
- package/src/mcp.mjs +24 -0
- package/src/store-upload.mjs +127 -0
package/package.json
CHANGED
package/skill/SKILL.md
CHANGED
|
@@ -12,7 +12,7 @@ description: >
|
|
|
12
12
|
# nomac — ship iOS without a Mac
|
|
13
13
|
|
|
14
14
|
nomac is the hands; you are the brain. It builds, signs, uploads, lints for
|
|
15
|
-
review-readiness, writes store metadata
|
|
15
|
+
review-readiness, writes store metadata, manages pricing/products and submits for review —
|
|
16
16
|
via MCP tools (preferred) or the `nomac` CLI.
|
|
17
17
|
|
|
18
18
|
## Setup check
|
|
@@ -81,8 +81,9 @@ too, and use `get_screenshot_upload` to find or poll the existing operation.
|
|
|
81
81
|
Fix issues before marking items resolved. Every rejected item must be resolved
|
|
82
82
|
or removed before resubmitting. Removed items cannot be added back to that submission.
|
|
83
83
|
- Poll `get_review`; approval and release are separate. Release requires an
|
|
84
|
-
app-version item in Pending Developer Release.
|
|
85
|
-
and
|
|
84
|
+
app-version item in Pending Developer Release. Edit subscription/IAP products,
|
|
85
|
+
events and custom pages through the store catalog below. Apple/account/legal
|
|
86
|
+
decisions still require the relevant Apple workflow.
|
|
86
87
|
|
|
87
88
|
## External TestFlight
|
|
88
89
|
|
|
@@ -103,8 +104,56 @@ requires its own review and distribution:
|
|
|
103
104
|
5. All actions preview without confirm and apply with confirm:true. Pending
|
|
104
105
|
requests use their original key/body; get_store_operation recovers them.
|
|
105
106
|
6. Relay an accurate human-provided uses_non_exempt_encryption with action=encryption
|
|
106
|
-
when appropriate. Never guess.
|
|
107
|
-
|
|
107
|
+
when appropriate. Never guess. Encryption declarations/documents use the
|
|
108
|
+
store catalog; rejected beta messages require App Store Connect. Fix the cause
|
|
109
|
+
before resubmitting a rejected binary.
|
|
110
|
+
|
|
111
|
+
## Pricing, products, customer reviews and the wider store API
|
|
112
|
+
|
|
113
|
+
1. `get_store_capabilities` lists operations by category/search with next_offset.
|
|
114
|
+
Pass `operation` to get that operation's exact Apple request/query schema.
|
|
115
|
+
Categories: listing, releases, pricing, purchases, subscriptions, reviews,
|
|
116
|
+
marketing, assets, testflight, analytics, compliance, webhooks.
|
|
117
|
+
2. `read_store` executes listed GET operations. Begin with an `apps_*` relationship
|
|
118
|
+
and `scope:[]` for the linked app. Every child has a reusable `scope`; copy it
|
|
119
|
+
into the next read/write. Keep the query when following `next_cursor`.
|
|
120
|
+
3. `update_store` takes the operation, scope and exact JSON:API `body`. For creation,
|
|
121
|
+
scope the parent. Every existing relationship ID needs its own verified scope
|
|
122
|
+
in `references`; the target and its ancestors are already verified. Territories
|
|
123
|
+
and categories are global. Inline creates use local IDs such as `${price1}`.
|
|
124
|
+
4. Preview without confirm. `confirm:true` applies real paid prices, availability,
|
|
125
|
+
products/offers, public responses, release settings or event subscriptions.
|
|
126
|
+
Honor the human's existing authorization; do not ask again for authorized work.
|
|
127
|
+
Do not invent prices, legal answers or factual claims in public replies.
|
|
128
|
+
5. `customerReviewResponses_createInstance` replies to a public customer review.
|
|
129
|
+
It does not send a message to Apple reviewers. Read their actual messages in
|
|
130
|
+
App Store Connect; `prepare_review_reply` only drafts the response.
|
|
131
|
+
6. Product management covers subscriptions/IAP versions, localization, prices,
|
|
132
|
+
availability, grace periods and offers. First IAP approval may require the
|
|
133
|
+
app version submission. Draft review submission items can attach products;
|
|
134
|
+
use `publish` / `update_review` to submit, resolve, resubmit and release.
|
|
135
|
+
7. Events, custom pages, experiments, accessibility and phased releases use their
|
|
136
|
+
catalog operations. Apple still validates account permissions and valid states.
|
|
137
|
+
Analytics report segments supply download URLs; offer-code CSV exports return
|
|
138
|
+
bounded base64 downloads. Webhooks send app events to an owner-selected receiver;
|
|
139
|
+
that receiver is responsible for running an agent when events arrive.
|
|
140
|
+
|
|
141
|
+
`upload_store_asset` handles new Apple file resources: previews, screenshot files,
|
|
142
|
+
purchase images, supplemental review attachments and encryption documents. Choose
|
|
143
|
+
the asset create operation, provide its parent relationship/body, and pass a local
|
|
144
|
+
`file_path` (CLI, up to 500 MiB) or `data_base64` (hosted, up to 2 MiB). Apple also
|
|
145
|
+
has asset-specific limits. The tool reserves, uploads each requested byte range,
|
|
146
|
+
then commits its checksum. Keep the same request_key and original bytes to resume.
|
|
147
|
+
`uploaded:true` is not processing success: poll `read_store` until
|
|
148
|
+
`assetDeliveryState.state` is `COMPLETE`. FAILED requires addressing Apple's errors.
|
|
149
|
+
This adds a new file; use `upload_screenshots` for managed screenshot replacement.
|
|
150
|
+
|
|
151
|
+
Keep pending write keys and unchanged bodies. `get_store_operation` exposes captured
|
|
152
|
+
targets and receipts. Unknown Apple outcomes remain pending and block other writes
|
|
153
|
+
to the app until reconciled; never use a fresh key to bypass one. Corrected inputs
|
|
154
|
+
or a new intentional action require a fresh key. Correspondence, appeals, App Privacy
|
|
155
|
+
questionnaire answers and account-wide financial/admin APIs are not exposed.
|
|
156
|
+
This catalog does not run simulators or Xcode Cloud jobs.
|
|
108
157
|
|
|
109
158
|
## When stuck
|
|
110
159
|
|
|
@@ -1,4 +1,420 @@
|
|
|
1
1
|
{
|
|
2
|
+
"get_store_capabilities": {
|
|
3
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
4
|
+
"type": "object",
|
|
5
|
+
"properties": {
|
|
6
|
+
"category": {
|
|
7
|
+
"type": "string",
|
|
8
|
+
"maxLength": 80
|
|
9
|
+
},
|
|
10
|
+
"operation": {
|
|
11
|
+
"type": "string",
|
|
12
|
+
"maxLength": 160
|
|
13
|
+
},
|
|
14
|
+
"search": {
|
|
15
|
+
"type": "string",
|
|
16
|
+
"maxLength": 100
|
|
17
|
+
},
|
|
18
|
+
"offset": {
|
|
19
|
+
"type": "integer",
|
|
20
|
+
"minimum": 0,
|
|
21
|
+
"maximum": 1000
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"additionalProperties": false
|
|
25
|
+
},
|
|
26
|
+
"read_store": {
|
|
27
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
28
|
+
"type": "object",
|
|
29
|
+
"properties": {
|
|
30
|
+
"operation": {
|
|
31
|
+
"type": "string",
|
|
32
|
+
"minLength": 1,
|
|
33
|
+
"maxLength": 160
|
|
34
|
+
},
|
|
35
|
+
"scope": {
|
|
36
|
+
"maxItems": 8,
|
|
37
|
+
"type": "array",
|
|
38
|
+
"items": {
|
|
39
|
+
"type": "object",
|
|
40
|
+
"properties": {
|
|
41
|
+
"relationship": {
|
|
42
|
+
"type": "string",
|
|
43
|
+
"maxLength": 100,
|
|
44
|
+
"pattern": "^[A-Za-z][A-Za-z0-9]*$"
|
|
45
|
+
},
|
|
46
|
+
"id": {
|
|
47
|
+
"type": "string",
|
|
48
|
+
"minLength": 1,
|
|
49
|
+
"maxLength": 512
|
|
50
|
+
},
|
|
51
|
+
"query": {
|
|
52
|
+
"type": "object",
|
|
53
|
+
"propertyNames": {
|
|
54
|
+
"type": "string",
|
|
55
|
+
"maxLength": 120
|
|
56
|
+
},
|
|
57
|
+
"additionalProperties": {
|
|
58
|
+
"anyOf": [
|
|
59
|
+
{
|
|
60
|
+
"type": "string",
|
|
61
|
+
"maxLength": 4096
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
"type": "number"
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
"type": "boolean"
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
"maxItems": 200,
|
|
71
|
+
"type": "array",
|
|
72
|
+
"items": {
|
|
73
|
+
"type": "string",
|
|
74
|
+
"maxLength": 1024
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
]
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
"required": [
|
|
82
|
+
"relationship",
|
|
83
|
+
"id"
|
|
84
|
+
],
|
|
85
|
+
"additionalProperties": false
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
"query": {
|
|
89
|
+
"type": "object",
|
|
90
|
+
"propertyNames": {
|
|
91
|
+
"type": "string",
|
|
92
|
+
"maxLength": 120
|
|
93
|
+
},
|
|
94
|
+
"additionalProperties": {
|
|
95
|
+
"anyOf": [
|
|
96
|
+
{
|
|
97
|
+
"type": "string",
|
|
98
|
+
"maxLength": 4096
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
"type": "number"
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
"type": "boolean"
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
"maxItems": 200,
|
|
108
|
+
"type": "array",
|
|
109
|
+
"items": {
|
|
110
|
+
"type": "string",
|
|
111
|
+
"maxLength": 1024
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
]
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
"cursor": {
|
|
118
|
+
"type": "string",
|
|
119
|
+
"maxLength": 4096
|
|
120
|
+
},
|
|
121
|
+
"resource_id": {
|
|
122
|
+
"type": "string",
|
|
123
|
+
"minLength": 1,
|
|
124
|
+
"maxLength": 512
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
"required": [
|
|
128
|
+
"operation"
|
|
129
|
+
],
|
|
130
|
+
"additionalProperties": false
|
|
131
|
+
},
|
|
132
|
+
"update_store": {
|
|
133
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
134
|
+
"type": "object",
|
|
135
|
+
"properties": {
|
|
136
|
+
"operation": {
|
|
137
|
+
"type": "string",
|
|
138
|
+
"minLength": 1,
|
|
139
|
+
"maxLength": 160
|
|
140
|
+
},
|
|
141
|
+
"scope": {
|
|
142
|
+
"maxItems": 8,
|
|
143
|
+
"type": "array",
|
|
144
|
+
"items": {
|
|
145
|
+
"type": "object",
|
|
146
|
+
"properties": {
|
|
147
|
+
"relationship": {
|
|
148
|
+
"type": "string",
|
|
149
|
+
"maxLength": 100,
|
|
150
|
+
"pattern": "^[A-Za-z][A-Za-z0-9]*$"
|
|
151
|
+
},
|
|
152
|
+
"id": {
|
|
153
|
+
"type": "string",
|
|
154
|
+
"minLength": 1,
|
|
155
|
+
"maxLength": 512
|
|
156
|
+
},
|
|
157
|
+
"query": {
|
|
158
|
+
"type": "object",
|
|
159
|
+
"propertyNames": {
|
|
160
|
+
"type": "string",
|
|
161
|
+
"maxLength": 120
|
|
162
|
+
},
|
|
163
|
+
"additionalProperties": {
|
|
164
|
+
"anyOf": [
|
|
165
|
+
{
|
|
166
|
+
"type": "string",
|
|
167
|
+
"maxLength": 4096
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
"type": "number"
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
"type": "boolean"
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
"maxItems": 200,
|
|
177
|
+
"type": "array",
|
|
178
|
+
"items": {
|
|
179
|
+
"type": "string",
|
|
180
|
+
"maxLength": 1024
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
]
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
},
|
|
187
|
+
"required": [
|
|
188
|
+
"relationship",
|
|
189
|
+
"id"
|
|
190
|
+
],
|
|
191
|
+
"additionalProperties": false
|
|
192
|
+
}
|
|
193
|
+
},
|
|
194
|
+
"references": {
|
|
195
|
+
"maxItems": 30,
|
|
196
|
+
"type": "array",
|
|
197
|
+
"items": {
|
|
198
|
+
"maxItems": 8,
|
|
199
|
+
"type": "array",
|
|
200
|
+
"items": {
|
|
201
|
+
"type": "object",
|
|
202
|
+
"properties": {
|
|
203
|
+
"relationship": {
|
|
204
|
+
"type": "string",
|
|
205
|
+
"maxLength": 100,
|
|
206
|
+
"pattern": "^[A-Za-z][A-Za-z0-9]*$"
|
|
207
|
+
},
|
|
208
|
+
"id": {
|
|
209
|
+
"type": "string",
|
|
210
|
+
"minLength": 1,
|
|
211
|
+
"maxLength": 512
|
|
212
|
+
},
|
|
213
|
+
"query": {
|
|
214
|
+
"type": "object",
|
|
215
|
+
"propertyNames": {
|
|
216
|
+
"type": "string",
|
|
217
|
+
"maxLength": 120
|
|
218
|
+
},
|
|
219
|
+
"additionalProperties": {
|
|
220
|
+
"anyOf": [
|
|
221
|
+
{
|
|
222
|
+
"type": "string",
|
|
223
|
+
"maxLength": 4096
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
"type": "number"
|
|
227
|
+
},
|
|
228
|
+
{
|
|
229
|
+
"type": "boolean"
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
"maxItems": 200,
|
|
233
|
+
"type": "array",
|
|
234
|
+
"items": {
|
|
235
|
+
"type": "string",
|
|
236
|
+
"maxLength": 1024
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
]
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
},
|
|
243
|
+
"required": [
|
|
244
|
+
"relationship",
|
|
245
|
+
"id"
|
|
246
|
+
],
|
|
247
|
+
"additionalProperties": false
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
},
|
|
251
|
+
"body": {
|
|
252
|
+
"type": "object",
|
|
253
|
+
"propertyNames": {
|
|
254
|
+
"type": "string"
|
|
255
|
+
},
|
|
256
|
+
"additionalProperties": {}
|
|
257
|
+
},
|
|
258
|
+
"confirm": {
|
|
259
|
+
"type": "boolean"
|
|
260
|
+
},
|
|
261
|
+
"asset_sha256": {
|
|
262
|
+
"description": "Set by upload_store_asset for exact file-reservation recovery.",
|
|
263
|
+
"type": "string",
|
|
264
|
+
"pattern": "^[a-f0-9]{64}$"
|
|
265
|
+
}
|
|
266
|
+
},
|
|
267
|
+
"required": [
|
|
268
|
+
"operation"
|
|
269
|
+
],
|
|
270
|
+
"additionalProperties": false
|
|
271
|
+
},
|
|
272
|
+
"upload_store_asset": {
|
|
273
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
274
|
+
"type": "object",
|
|
275
|
+
"properties": {
|
|
276
|
+
"operation": {
|
|
277
|
+
"type": "string",
|
|
278
|
+
"minLength": 1,
|
|
279
|
+
"maxLength": 160
|
|
280
|
+
},
|
|
281
|
+
"scope": {
|
|
282
|
+
"maxItems": 8,
|
|
283
|
+
"type": "array",
|
|
284
|
+
"items": {
|
|
285
|
+
"type": "object",
|
|
286
|
+
"properties": {
|
|
287
|
+
"relationship": {
|
|
288
|
+
"type": "string",
|
|
289
|
+
"maxLength": 100,
|
|
290
|
+
"pattern": "^[A-Za-z][A-Za-z0-9]*$"
|
|
291
|
+
},
|
|
292
|
+
"id": {
|
|
293
|
+
"type": "string",
|
|
294
|
+
"minLength": 1,
|
|
295
|
+
"maxLength": 512
|
|
296
|
+
},
|
|
297
|
+
"query": {
|
|
298
|
+
"type": "object",
|
|
299
|
+
"propertyNames": {
|
|
300
|
+
"type": "string",
|
|
301
|
+
"maxLength": 120
|
|
302
|
+
},
|
|
303
|
+
"additionalProperties": {
|
|
304
|
+
"anyOf": [
|
|
305
|
+
{
|
|
306
|
+
"type": "string",
|
|
307
|
+
"maxLength": 4096
|
|
308
|
+
},
|
|
309
|
+
{
|
|
310
|
+
"type": "number"
|
|
311
|
+
},
|
|
312
|
+
{
|
|
313
|
+
"type": "boolean"
|
|
314
|
+
},
|
|
315
|
+
{
|
|
316
|
+
"maxItems": 200,
|
|
317
|
+
"type": "array",
|
|
318
|
+
"items": {
|
|
319
|
+
"type": "string",
|
|
320
|
+
"maxLength": 1024
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
]
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
},
|
|
327
|
+
"required": [
|
|
328
|
+
"relationship",
|
|
329
|
+
"id"
|
|
330
|
+
],
|
|
331
|
+
"additionalProperties": false
|
|
332
|
+
}
|
|
333
|
+
},
|
|
334
|
+
"references": {
|
|
335
|
+
"maxItems": 30,
|
|
336
|
+
"type": "array",
|
|
337
|
+
"items": {
|
|
338
|
+
"maxItems": 8,
|
|
339
|
+
"type": "array",
|
|
340
|
+
"items": {
|
|
341
|
+
"type": "object",
|
|
342
|
+
"properties": {
|
|
343
|
+
"relationship": {
|
|
344
|
+
"type": "string",
|
|
345
|
+
"maxLength": 100,
|
|
346
|
+
"pattern": "^[A-Za-z][A-Za-z0-9]*$"
|
|
347
|
+
},
|
|
348
|
+
"id": {
|
|
349
|
+
"type": "string",
|
|
350
|
+
"minLength": 1,
|
|
351
|
+
"maxLength": 512
|
|
352
|
+
},
|
|
353
|
+
"query": {
|
|
354
|
+
"type": "object",
|
|
355
|
+
"propertyNames": {
|
|
356
|
+
"type": "string",
|
|
357
|
+
"maxLength": 120
|
|
358
|
+
},
|
|
359
|
+
"additionalProperties": {
|
|
360
|
+
"anyOf": [
|
|
361
|
+
{
|
|
362
|
+
"type": "string",
|
|
363
|
+
"maxLength": 4096
|
|
364
|
+
},
|
|
365
|
+
{
|
|
366
|
+
"type": "number"
|
|
367
|
+
},
|
|
368
|
+
{
|
|
369
|
+
"type": "boolean"
|
|
370
|
+
},
|
|
371
|
+
{
|
|
372
|
+
"maxItems": 200,
|
|
373
|
+
"type": "array",
|
|
374
|
+
"items": {
|
|
375
|
+
"type": "string",
|
|
376
|
+
"maxLength": 1024
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
]
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
},
|
|
383
|
+
"required": [
|
|
384
|
+
"relationship",
|
|
385
|
+
"id"
|
|
386
|
+
],
|
|
387
|
+
"additionalProperties": false
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
},
|
|
391
|
+
"body": {
|
|
392
|
+
"type": "object",
|
|
393
|
+
"propertyNames": {
|
|
394
|
+
"type": "string"
|
|
395
|
+
},
|
|
396
|
+
"additionalProperties": {}
|
|
397
|
+
},
|
|
398
|
+
"confirm": {
|
|
399
|
+
"type": "boolean"
|
|
400
|
+
},
|
|
401
|
+
"asset_sha256": {
|
|
402
|
+
"description": "Set by upload_store_asset for exact file-reservation recovery.",
|
|
403
|
+
"type": "string",
|
|
404
|
+
"pattern": "^[a-f0-9]{64}$"
|
|
405
|
+
},
|
|
406
|
+
"data_base64": {
|
|
407
|
+
"type": "string",
|
|
408
|
+
"minLength": 1,
|
|
409
|
+
"maxLength": 3145728
|
|
410
|
+
}
|
|
411
|
+
},
|
|
412
|
+
"required": [
|
|
413
|
+
"operation",
|
|
414
|
+
"data_base64"
|
|
415
|
+
],
|
|
416
|
+
"additionalProperties": false
|
|
417
|
+
},
|
|
2
418
|
"update_review": {
|
|
3
419
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
4
420
|
"type": "object",
|
package/src/mcp.mjs
CHANGED
|
@@ -10,6 +10,7 @@ import { api } from "./api.mjs";
|
|
|
10
10
|
import { loadProjectState, saveProjectState } from "./config.mjs";
|
|
11
11
|
import { packTarball } from "./pack.mjs";
|
|
12
12
|
import { uploadSourceSnapshot } from "./upload.mjs";
|
|
13
|
+
import { uploadStoreAsset } from "./store-upload.mjs";
|
|
13
14
|
|
|
14
15
|
const PROTOCOL_FALLBACK = "2025-06-18";
|
|
15
16
|
|
|
@@ -26,6 +27,29 @@ async function resolveProjectId(args) {
|
|
|
26
27
|
}
|
|
27
28
|
|
|
28
29
|
export const TOOLS = [
|
|
30
|
+
{
|
|
31
|
+
name: "upload_store_asset",
|
|
32
|
+
description: "Upload a new Apple screenshot, preview, purchase image, supplemental reviewer attachment or encryption document from file_path (up to 500 MiB, subject to Apple limits) or data_base64 (2 MiB). Discover the createInstance operation/body and parent scope with get_store_capabilities/read_store. Preview by default; confirm:true reserves, uploads and commits. Preserve request_key and identical bytes/body to resume. ready:true requires Apple processing COMPLETE. Message attachments require App Store Connect.",
|
|
33
|
+
inputSchema: { ...lifecycleSchemas.upload_store_asset, required: ["operation"], properties: { ...lifecycleSchemas.upload_store_asset.properties, project_id: { type: "string" }, file_path: { type: "string" }, request_key: { type: "string" } } },
|
|
34
|
+
handler: async args => text(await uploadStoreAsset({ ...args, project_id: await resolveProjectId(args) }, api)),
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
name: "get_store_capabilities",
|
|
38
|
+
description: "Discover useful App Store Connect operations: listing, paid pricing/availability, IAPs/subscriptions/offers, public customer-review replies, phased releases, marketing, files, analytics and webhooks. Pass category/search to list; pass operation for the exact Apple JSON:API request schema. Reviewer correspondence is a separate browser workflow.",
|
|
39
|
+
inputSchema: lifecycleSchemas.get_store_capabilities,
|
|
40
|
+
handler: async args => text(await api("GET", `/api/v1/store-capabilities?${new URLSearchParams(Object.entries(args ?? {}).filter(([, value]) => value !== undefined).map(([key, value]) => [key, String(value)]))}`)),
|
|
41
|
+
},
|
|
42
|
+
...[
|
|
43
|
+
["read_store", "read", "Read a discovered Apple GET operation. Start with an apps_* relationship and scope:[]; reuse the returned resource scopes. Paginate with next_cursor and unchanged query. Raw IDs are accepted only for global lookups. Reads live Apple state, file upload operations and analytics report URLs."],
|
|
44
|
+
["update_store", "write", "Execute a discovered Apple write using its exact JSON:API body. Copy scope and relationship references from read_store. Preview by default; confirm:true changes real Apple resources. Use owner-authorized prices and attestations. On pending, retry identical arguments with request_key; get_store_operation recovers lost keys. Use publish/update_review/manage_testflight for their dedicated lifecycle actions."],
|
|
45
|
+
].map(([name, route, description]) => ({
|
|
46
|
+
name, description,
|
|
47
|
+
inputSchema: { ...lifecycleSchemas[name], properties: { project_id: { type: "string" }, ...lifecycleSchemas[name].properties, ...(route === "write" ? { request_key: { type: "string" } } : {}) } },
|
|
48
|
+
handler: async args => {
|
|
49
|
+
const { project_id: _project, request_key, ...body } = args;
|
|
50
|
+
return text(await api("POST", `/api/v1/projects/${encodeURIComponent(await resolveProjectId(args))}/store/${route}`, { body, ...(route === "write" ? { idempotencyKey: request_key ?? `mcp-store-${randomUUID()}` } : {}) }));
|
|
51
|
+
},
|
|
52
|
+
})),
|
|
29
53
|
{
|
|
30
54
|
name: "connect_status",
|
|
31
55
|
description:
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
// Shared by the hosted upload endpoint and the published CLI (copied by the
|
|
2
|
+
// schema generator). Provider upload URLs come only from an authenticated,
|
|
3
|
+
// app-scoped read_store response; Nomac credentials never go to those URLs.
|
|
4
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
5
|
+
import { open } from 'node:fs/promises';
|
|
6
|
+
import { basename } from 'node:path';
|
|
7
|
+
import { Readable } from 'node:stream';
|
|
8
|
+
|
|
9
|
+
function unwrap(schema, components) {
|
|
10
|
+
if (schema?.$ref) return unwrap(components.schemas[schema.$ref.split('/').at(-1)], components);
|
|
11
|
+
if (schema?.anyOf) return unwrap(schema.anyOf.find(s => s.type !== 'null'), components);
|
|
12
|
+
return schema;
|
|
13
|
+
}
|
|
14
|
+
function fields(detail) {
|
|
15
|
+
const root = unwrap(detail.operation.body, detail.operation.components);
|
|
16
|
+
return unwrap(unwrap(root.properties.data, detail.operation.components).properties.attributes, detail.operation.components)?.properties ?? {};
|
|
17
|
+
}
|
|
18
|
+
const digest = (name, bytes) => createHash(name).update(bytes).digest('hex');
|
|
19
|
+
|
|
20
|
+
export async function uploadStoreAsset(args, call) {
|
|
21
|
+
const requestKey = args.request_key ?? `store-upload-${randomUUID()}`;
|
|
22
|
+
let handle, current, source, md5, sha256, size, filename, reservation;
|
|
23
|
+
let stage = 'prepare';
|
|
24
|
+
try {
|
|
25
|
+
if (Boolean(args.file_path) === Boolean(args.data_base64)) throw new Error('Supply exactly one local file_path (CLI) or data_base64 (hosted).');
|
|
26
|
+
if (!args.operation?.endsWith('_createInstance')) throw new Error('Choose an asset createInstance operation from get_store_capabilities.');
|
|
27
|
+
if (args.file_path) {
|
|
28
|
+
handle = await open(args.file_path, 'r');
|
|
29
|
+
current = await handle.stat({ bigint: true });
|
|
30
|
+
if (!current.isFile() || current.size < 1n || current.size > 500n * 1024n * 1024n) throw new Error('Upload a regular file between 1 byte and 500 MiB. Apple may impose a smaller asset-specific limit.');
|
|
31
|
+
size = Number(current.size); filename = basename(args.file_path);
|
|
32
|
+
const range = async function* (offset, length) {
|
|
33
|
+
for (let position = offset; position < offset + length;) {
|
|
34
|
+
const buffer = Buffer.allocUnsafe(Math.min(1024 * 1024, offset + length - position));
|
|
35
|
+
const { bytesRead } = await handle.read(buffer, 0, buffer.length, position);
|
|
36
|
+
if (!bytesRead) throw new Error('The source file was truncated during upload.');
|
|
37
|
+
position += bytesRead;
|
|
38
|
+
yield buffer.subarray(0, bytesRead);
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
const sha = createHash('sha256'), checksum = createHash('md5');
|
|
42
|
+
for await (const bytes of range(0, size)) { sha.update(bytes); checksum.update(bytes); }
|
|
43
|
+
sha256 = sha.digest('hex'); md5 = checksum.digest('hex');
|
|
44
|
+
source = async (offset, length) => {
|
|
45
|
+
const latest = await handle.stat({ bigint: true });
|
|
46
|
+
if (['size', 'mtimeNs', 'ctimeNs', 'ino'].some(k => latest[k] !== current[k])) throw new Error('The source file changed. Resume with the original bytes or start a new upload.');
|
|
47
|
+
return Readable.from(range(offset, length));
|
|
48
|
+
};
|
|
49
|
+
} else {
|
|
50
|
+
if (typeof args.data_base64 !== 'string' || args.data_base64.length > 3 * 1024 * 1024 || !/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(args.data_base64)) throw new Error('Provide canonical base64 for a file up to 2 MiB; use the CLI for larger files.');
|
|
51
|
+
const bytes = Buffer.from(args.data_base64, 'base64');
|
|
52
|
+
size = bytes.length;
|
|
53
|
+
if (!size || size > 2 * 1024 * 1024) throw new Error('Hosted inline uploads support files between 1 byte and 2 MiB. Use the CLI for larger files.');
|
|
54
|
+
md5 = digest('md5', bytes); sha256 = digest('sha256', bytes); filename = 'asset.bin';
|
|
55
|
+
source = async (offset, length) => bytes.subarray(offset, offset + length);
|
|
56
|
+
}
|
|
57
|
+
const detail = await call('GET', `/api/v1/store-capabilities?operation=${encodeURIComponent(args.operation)}`);
|
|
58
|
+
const createFields = fields(detail);
|
|
59
|
+
if (!createFields.fileName || !createFields.fileSize) throw new Error('This operation does not create an uploadable Apple file.');
|
|
60
|
+
const commitOperation = args.operation.replace(/_createInstance$/, '_updateInstance');
|
|
61
|
+
const commitDetail = await call('GET', `/api/v1/store-capabilities?operation=${encodeURIComponent(commitOperation)}`);
|
|
62
|
+
const commitFields = fields(commitDetail);
|
|
63
|
+
if (!commitFields.uploaded) throw new Error('This asset does not support the standard Apple upload commit operation.');
|
|
64
|
+
const body = structuredClone(args.body ?? {});
|
|
65
|
+
if (!body.data || !body.data.relationships) throw new Error('Supply the asset JSON:API body and its parent relationship from get_store_capabilities.');
|
|
66
|
+
body.data.attributes ??= {};
|
|
67
|
+
if (body.data.attributes.fileSize !== undefined && body.data.attributes.fileSize !== size) throw new Error('The declared fileSize does not match the source file.');
|
|
68
|
+
filename = basename(body.data.attributes.fileName || filename).normalize('NFC').replace(/[\r\n]/g, '_');
|
|
69
|
+
const tag = digest('sha256', `${args.project_id}\n${requestKey}\n${args.operation}\n${sha256}`).slice(0, 32);
|
|
70
|
+
body.data.attributes.fileName = `nomac-${tag}-${filename}`;
|
|
71
|
+
body.data.attributes.fileSize = size;
|
|
72
|
+
const base = `/api/v1/projects/${encodeURIComponent(args.project_id)}/store`;
|
|
73
|
+
const write = (payload, key) => call('POST', `${base}/write`, { body: payload, idempotencyKey: key });
|
|
74
|
+
stage = 'reserve';
|
|
75
|
+
reservation = await write({ operation: args.operation, scope: args.scope, references: args.references, body, ...(args.confirm ? { asset_sha256: sha256 } : {}), confirm: Boolean(args.confirm) }, args.confirm ? requestKey : `${requestKey}:preview`);
|
|
76
|
+
if (!args.confirm || reservation.state === 'pending') return { ...reservation, request_key: requestKey, stage: 'reserve', file_size: size, sha256 };
|
|
77
|
+
if (!reservation.resource?.id || !reservation.scope) throw new Error('Apple has not returned an app-owned file reservation. Resume this request key; do not create another file.');
|
|
78
|
+
const read = async () => call('POST', `${base}/read`, { body: { operation: args.operation.replace(/_createInstance$/, '_getInstance'), scope: reservation.scope } });
|
|
79
|
+
let state = await read();
|
|
80
|
+
let attributes = state.data?.attributes;
|
|
81
|
+
if (!attributes || attributes.fileSize !== size || attributes.fileName !== body.data.attributes.fileName) throw new Error('The reserved Apple file does not match this upload.');
|
|
82
|
+
stage = 'upload';
|
|
83
|
+
const completed = ['UPLOAD_COMPLETE', 'COMPLETE', 'FAILED'].includes(attributes.assetDeliveryState?.state);
|
|
84
|
+
if (!completed) {
|
|
85
|
+
const operations = attributes.uploadOperations;
|
|
86
|
+
if (!Array.isArray(operations) || !operations.length) throw new Error('Apple has not exposed upload operations yet. Retry this same upload.');
|
|
87
|
+
let end = 0;
|
|
88
|
+
for (const op of [...operations].sort((a, b) => a.offset - b.offset)) {
|
|
89
|
+
if (op.method !== 'PUT' || !Number.isSafeInteger(op.offset) || !Number.isSafeInteger(op.length) || op.length < 1 || op.offset !== end || op.offset + op.length > size) throw new Error('Apple returned invalid or incomplete upload byte ranges.');
|
|
90
|
+
const url = new URL(op.url);
|
|
91
|
+
if (url.protocol !== 'https:' || url.username || url.password || !['apple.com', 'icloud.com', 'mzstatic.com', 'amazonaws.com'].some(domain => url.hostname === domain || url.hostname.endsWith(`.${domain}`))) throw new Error('Apple returned an unexpected upload destination.');
|
|
92
|
+
end += op.length;
|
|
93
|
+
}
|
|
94
|
+
if (end !== size) throw new Error('Apple upload operations do not cover the complete source file.');
|
|
95
|
+
for (const op of operations) {
|
|
96
|
+
const headers = new Headers();
|
|
97
|
+
for (const header of op.requestHeaders ?? []) {
|
|
98
|
+
if (typeof header.name !== 'string' || typeof header.value !== 'string') throw new Error('Apple returned invalid upload headers.');
|
|
99
|
+
headers.set(header.name, header.value);
|
|
100
|
+
}
|
|
101
|
+
if (headers.has('content-length') && headers.get('content-length') !== String(op.length)) throw new Error('Apple returned a Content-Length that does not match the upload byte range.');
|
|
102
|
+
// A Readable body otherwise uses chunked transfer encoding, which
|
|
103
|
+
// Apple's fixed-length upload destinations may reject.
|
|
104
|
+
headers.set('content-length', String(op.length));
|
|
105
|
+
const part = await source(op.offset, op.length);
|
|
106
|
+
try {
|
|
107
|
+
const response = await fetch(op.url, { method: 'PUT', headers, body: part, duplex: 'half', redirect: 'error', signal: AbortSignal.timeout(120_000) });
|
|
108
|
+
if (!response.ok) throw new Error(`Apple asset upload returned HTTP ${response.status}. Retry with this same request key to refresh its upload URLs.`);
|
|
109
|
+
await response.body?.cancel();
|
|
110
|
+
} finally { if (typeof part.destroy === 'function') part.destroy(); }
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if (handle) {
|
|
114
|
+
const latest = await handle.stat({ bigint: true });
|
|
115
|
+
if (['size', 'mtimeNs', 'ctimeNs', 'ino'].some(k => latest[k] !== current[k])) throw new Error('The source file changed during upload; it was not committed.');
|
|
116
|
+
}
|
|
117
|
+
stage = 'commit';
|
|
118
|
+
const committed = await write({ operation: commitOperation, scope: reservation.scope, body: { data: { type: reservation.resource.type, id: reservation.resource.id, attributes: { uploaded: true, ...(commitFields.sourceFileChecksum ? { sourceFileChecksum: md5 } : {}) } } }, confirm: true }, `${requestKey}:commit`);
|
|
119
|
+
if (committed.state === 'pending') return { ...committed, request_key: requestKey, commit_request_key: `${requestKey}:commit`, stage, resource: reservation.resource, scope: reservation.scope };
|
|
120
|
+
stage = 'processing';
|
|
121
|
+
state = await read(); attributes = state.data?.attributes;
|
|
122
|
+
const deliveryState = attributes?.assetDeliveryState?.state;
|
|
123
|
+
return { request_key: requestKey, resource: reservation.resource, scope: reservation.scope, file_size: size, sha256, uploaded: true, ready: deliveryState === 'COMPLETE', state: deliveryState === 'COMPLETE' ? 'complete' : deliveryState === 'FAILED' ? 'failed' : 'processing', asset_delivery_state: attributes?.assetDeliveryState, next_steps: 'Poll read_store with this scope until assetDeliveryState is COMPLETE. A committed upload may still be processing or rejected by Apple. This uploaded a new file; existing assets were not replaced.' };
|
|
124
|
+
} catch (error) {
|
|
125
|
+
return { state: reservation?.resource ? 'needs_attention' : 'failed', stage, request_key: requestKey, ...(reservation?.resource ? { resource: reservation.resource, scope: reservation.scope } : {}), error: { code: error.code ?? 'store_upload_failed', message: error.friendly ?? error.message }, next_steps: 'Keep the original file and request_key to resume. Completed reservation/commit requests are replayed; upload PUTs may repeat the identical byte ranges. Do not start another reservation for an unconfirmed request.' };
|
|
126
|
+
} finally { await handle?.close(); }
|
|
127
|
+
}
|