@imagina-build/ias-cli 0.0.0 → 0.1.1
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/README.md +406 -2
- package/bin/ias.mjs +19 -0
- package/package.json +31 -12
- package/schema/ias.schema.json +535 -0
- package/shared/credentials/cms.mjs +105 -0
- package/shared/credentials/index.mjs +354 -0
- package/shared/credentials/plist.mjs +93 -0
- package/shared/vault/approvers-vectors.mjs +188 -0
- package/shared/vault/approvers.mjs +234 -0
- package/shared/vault/index.mjs +583 -0
- package/shared/vault/plaintext.mjs +115 -0
- package/shared/vault/test-vectors/ias-vault-2/approvers.json +2122 -0
- package/shared/vault/test-vectors/ias-vault-2/keys.json +43 -0
- package/shared/vault/test-vectors/ias-vault-2/malformed.json +1739 -0
- package/shared/vault/test-vectors/ias-vault-2/plaintexts.json +149 -0
- package/shared/vault/test-vectors/ias-vault-2/runners.json +580 -0
- package/shared/vault/test-vectors/ias-vault-2/sealed.json +567 -0
- package/shared/vault/test-vectors/ias-vault-2/tampered.json +989 -0
- package/shared/vault/test-vectors/keys.json +31 -0
- package/shared/vault/test-vectors/malformed.json +1341 -0
- package/shared/vault/test-vectors/sealed.json +384 -0
- package/shared/vault/test-vectors/tampered.json +610 -0
- package/shared/vault/vectors.mjs +720 -0
- package/src/cli.mjs +231 -0
- package/src/commands/apps/index.mjs +36 -0
- package/src/commands/auth/index.mjs +141 -0
- package/src/commands/build/follow.mjs +168 -0
- package/src/commands/build/index.mjs +110 -0
- package/src/commands/build/local.mjs +69 -0
- package/src/commands/build/profile.mjs +50 -0
- package/src/commands/build/source.mjs +71 -0
- package/src/commands/builds/index.mjs +148 -0
- package/src/commands/credentials/index.mjs +256 -0
- package/src/commands/credentials/material.mjs +159 -0
- package/src/commands/devices/asc.mjs +116 -0
- package/src/commands/devices/index.mjs +65 -0
- package/src/commands/devices/sync.mjs +223 -0
- package/src/commands/dist/index.mjs +108 -0
- package/src/commands/doctor/index.mjs +158 -0
- package/src/commands/env/index.mjs +163 -0
- package/src/commands/github/index.mjs +53 -0
- package/src/commands/init/index.mjs +224 -0
- package/src/commands/migrate/credentials.mjs +213 -0
- package/src/commands/migrate/eas-cli.mjs +153 -0
- package/src/commands/migrate/index.mjs +73 -0
- package/src/commands/migrate/numbers.mjs +85 -0
- package/src/commands/migrate/profiles.mjs +174 -0
- package/src/commands/migrate/project.mjs +249 -0
- package/src/commands/migrate/survey.mjs +205 -0
- package/src/commands/migrate/variables.mjs +183 -0
- package/src/commands/push/index.mjs +334 -0
- package/src/commands/sdk/index.mjs +81 -0
- package/src/commands/submit/index.mjs +85 -0
- package/src/commands/update/index.mjs +526 -0
- package/src/commands/usage/index.mjs +57 -0
- package/src/commands/vault/approvers.mjs +129 -0
- package/src/commands/vault/index.mjs +159 -0
- package/src/commands/vault/runners.mjs +115 -0
- package/src/commands/vault/team.mjs +111 -0
- package/src/commands/version/index.mjs +71 -0
- package/src/commands/webhooks/index.mjs +104 -0
- package/src/commands/workflows/index.mjs +88 -0
- package/src/config.mjs +261 -0
- package/src/dist.mjs +99 -0
- package/src/errors.mjs +158 -0
- package/src/http.mjs +45 -0
- package/src/legacy.mjs +73 -0
- package/src/manage.mjs +120 -0
- package/src/ota.mjs +122 -0
- package/src/output.mjs +36 -0
- package/src/process.mjs +45 -0
- package/src/schema.mjs +131 -0
- package/src/sdk.mjs +114 -0
- package/src/secret-input.mjs +27 -0
- package/src/secrets.mjs +37 -0
- package/src/shared.mjs +5 -0
- package/src/team-vault.mjs +152 -0
- package/src/token-store.mjs +93 -0
|
@@ -0,0 +1,535 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://ias.imagina.build/schema/ias.schema.json",
|
|
4
|
+
"title": "ias.json",
|
|
5
|
+
"description": "The configuration of an app in Imagina Application Services (IAS): at the root of the app, next to app.json, and in git. It holds no secrets, only where they are: op:// references to 1Password, or the IAS_TOKEN variable. It replaces ota.config.json, dist.config.json and eas.json (PLAN-APPLICATION-SERVICES.md §2.7).",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"required": [
|
|
8
|
+
"app"
|
|
9
|
+
],
|
|
10
|
+
"additionalProperties": false,
|
|
11
|
+
"properties": {
|
|
12
|
+
"$schema": {
|
|
13
|
+
"description": "Where editors find this schema.",
|
|
14
|
+
"type": "string"
|
|
15
|
+
},
|
|
16
|
+
"app": {
|
|
17
|
+
"description": "The code of the application in IAS (php artisan apps:create), such as jscheduler. It is also the code of its distribution project unless distribution.project says another.",
|
|
18
|
+
"type": "string",
|
|
19
|
+
"pattern": "^[a-z0-9][a-z0-9-]{0,31}$"
|
|
20
|
+
},
|
|
21
|
+
"server": {
|
|
22
|
+
"description": "The origin of IAS. OTA updates go to the server of updates.url in app.json, which is the one the builds ask (without it, the config plugin of @imagina-build/ias gives the builds this one's); the rest goes here. IAS_SERVER wins over it, except for that URL.",
|
|
23
|
+
"type": "string",
|
|
24
|
+
"pattern": "^https?://[^/?#\\s]+/?$",
|
|
25
|
+
"default": "https://ias.imagina.build"
|
|
26
|
+
},
|
|
27
|
+
"build": {
|
|
28
|
+
"description": "The build profiles, by name, as in eas.json. Phase 2 (ias build) completes them.",
|
|
29
|
+
"type": "object",
|
|
30
|
+
"propertyNames": {
|
|
31
|
+
"$ref": "#/$defs/profileName"
|
|
32
|
+
},
|
|
33
|
+
"additionalProperties": {
|
|
34
|
+
"$ref": "#/$defs/buildProfile"
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
"updates": {
|
|
38
|
+
"$ref": "#/$defs/updates"
|
|
39
|
+
},
|
|
40
|
+
"distribution": {
|
|
41
|
+
"$ref": "#/$defs/distribution"
|
|
42
|
+
},
|
|
43
|
+
"push": {
|
|
44
|
+
"$ref": "#/$defs/push"
|
|
45
|
+
},
|
|
46
|
+
"vault": {
|
|
47
|
+
"description": "The sealed vault of the team (ias vault, ias credentials, ias env; docs/technical/vault.md): where its keys are in 1Password.",
|
|
48
|
+
"type": "object",
|
|
49
|
+
"additionalProperties": false,
|
|
50
|
+
"properties": {
|
|
51
|
+
"item": {
|
|
52
|
+
"description": "The 1Password item that ias vault init made, as op://<vault>/<item> (op://Private/IAS vault <host> by default). It holds the private keys of the team; only the reference goes here.",
|
|
53
|
+
"type": "string",
|
|
54
|
+
"pattern": "^op://[^/\\s]+/[^/]+$"
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
"submit": {
|
|
59
|
+
"description": "How each profile is sent to the stores, as in eas.json, without credentials: they go to the vault (ias credentials, track 1D).",
|
|
60
|
+
"type": "object",
|
|
61
|
+
"propertyNames": {
|
|
62
|
+
"$ref": "#/$defs/profileName"
|
|
63
|
+
},
|
|
64
|
+
"additionalProperties": {
|
|
65
|
+
"$ref": "#/$defs/submitProfile"
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
"workflows": {
|
|
69
|
+
"description": "What GitHub starts (docs/technical/manage-api.md §16.3), by name: a tag, a branch or a pull request starts a series of steps, each one after the one before it, and the first that fails stops the workflow. A step builds (the build profile and platforms) or submits to the stores what the build before it made; there is no step that publishes an OTA update, because its signing key lives on a Mac. Store builds, and every submission, still wait for an owner's approval with a passkey. A pull request from a fork never builds.",
|
|
70
|
+
"type": "object",
|
|
71
|
+
"propertyNames": {
|
|
72
|
+
"$ref": "#/$defs/profileName"
|
|
73
|
+
},
|
|
74
|
+
"additionalProperties": {
|
|
75
|
+
"$ref": "#/$defs/workflow"
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
"$defs": {
|
|
80
|
+
"profileName": {
|
|
81
|
+
"type": "string",
|
|
82
|
+
"pattern": "^[A-Za-z0-9][A-Za-z0-9._-]{0,49}$"
|
|
83
|
+
},
|
|
84
|
+
"projectCode": {
|
|
85
|
+
"type": "string",
|
|
86
|
+
"pattern": "^[a-z0-9][a-z0-9-]{0,31}$"
|
|
87
|
+
},
|
|
88
|
+
"environment": {
|
|
89
|
+
"description": "The three environments of every application: each brings its OTA channel, build profile and variables.",
|
|
90
|
+
"enum": [
|
|
91
|
+
"development",
|
|
92
|
+
"preview",
|
|
93
|
+
"production"
|
|
94
|
+
]
|
|
95
|
+
},
|
|
96
|
+
"projectPath": {
|
|
97
|
+
"description": "A path inside the project: relative, without ...",
|
|
98
|
+
"type": "string",
|
|
99
|
+
"minLength": 1,
|
|
100
|
+
"maxLength": 200,
|
|
101
|
+
"pattern": "^(?!/)(?!.*\\.\\.)[^\\\\\\n]+$"
|
|
102
|
+
},
|
|
103
|
+
"secretReference": {
|
|
104
|
+
"description": "Where a secret is: a 1Password reference (op://vault/item/field), read with the op CLI when needed. Never the secret itself.",
|
|
105
|
+
"type": "string",
|
|
106
|
+
"pattern": "^op://[^/\\s]+/[^/]+/.+$"
|
|
107
|
+
},
|
|
108
|
+
"buildProfile": {
|
|
109
|
+
"type": "object",
|
|
110
|
+
"additionalProperties": false,
|
|
111
|
+
"properties": {
|
|
112
|
+
"extends": {
|
|
113
|
+
"description": "The profile this one takes its fields from.",
|
|
114
|
+
"$ref": "#/$defs/profileName"
|
|
115
|
+
},
|
|
116
|
+
"type": {
|
|
117
|
+
"description": "expo (the default): an Expo app, built with npm ci, prebuild and the fingerprint. native: a project without Expo, built as the commit has it (no npm, prebuild or fingerprint): on iOS ios.workspace or ios.project and ios.scheme are needed; on Android, android.gradleTask and android.outputPath default to the release of the app module.",
|
|
118
|
+
"enum": [
|
|
119
|
+
"expo",
|
|
120
|
+
"native"
|
|
121
|
+
]
|
|
122
|
+
},
|
|
123
|
+
"distribution": {
|
|
124
|
+
"description": "store: for the App Store and Google Play. internal: for the install pages (ias dist), ad hoc on iOS.",
|
|
125
|
+
"enum": [
|
|
126
|
+
"store",
|
|
127
|
+
"internal"
|
|
128
|
+
]
|
|
129
|
+
},
|
|
130
|
+
"environment": {
|
|
131
|
+
"$ref": "#/$defs/environment"
|
|
132
|
+
},
|
|
133
|
+
"channel": {
|
|
134
|
+
"description": "The OTA channel the build asks for (expo-channel-name). Without it, the project's default channel.",
|
|
135
|
+
"type": "string",
|
|
136
|
+
"pattern": "^[A-Za-z0-9][A-Za-z0-9._-]{0,49}$"
|
|
137
|
+
},
|
|
138
|
+
"developmentClient": {
|
|
139
|
+
"description": "A development build (expo-dev-client).",
|
|
140
|
+
"type": "boolean"
|
|
141
|
+
},
|
|
142
|
+
"autoIncrement": {
|
|
143
|
+
"description": "Raise the build number (true, buildNumber or versionCode) or the version before each build.",
|
|
144
|
+
"anyOf": [
|
|
145
|
+
{
|
|
146
|
+
"type": "boolean"
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
"enum": [
|
|
150
|
+
"version",
|
|
151
|
+
"buildNumber",
|
|
152
|
+
"versionCode"
|
|
153
|
+
]
|
|
154
|
+
}
|
|
155
|
+
]
|
|
156
|
+
},
|
|
157
|
+
"ios": {
|
|
158
|
+
"type": "object",
|
|
159
|
+
"additionalProperties": false,
|
|
160
|
+
"properties": {
|
|
161
|
+
"simulator": {
|
|
162
|
+
"description": "A build for the iOS Simulator.",
|
|
163
|
+
"type": "boolean"
|
|
164
|
+
},
|
|
165
|
+
"workspace": {
|
|
166
|
+
"description": "Native projects (type native): the .xcworkspace to build, relative to the root of the repository. Use this or project.",
|
|
167
|
+
"$ref": "#/$defs/projectPath"
|
|
168
|
+
},
|
|
169
|
+
"project": {
|
|
170
|
+
"description": "Native projects (type native): the .xcodeproj to build, relative to the root of the repository. Use this or workspace.",
|
|
171
|
+
"$ref": "#/$defs/projectPath"
|
|
172
|
+
},
|
|
173
|
+
"scheme": {
|
|
174
|
+
"description": "Native projects (type native): the Xcode scheme to archive.",
|
|
175
|
+
"type": "string",
|
|
176
|
+
"pattern": "^[A-Za-z0-9_][A-Za-z0-9_. -]{0,99}$"
|
|
177
|
+
},
|
|
178
|
+
"configuration": {
|
|
179
|
+
"description": "Native projects (type native): the Xcode build configuration. Release by default.",
|
|
180
|
+
"type": "string",
|
|
181
|
+
"pattern": "^[A-Za-z0-9_][A-Za-z0-9_. -]{0,99}$"
|
|
182
|
+
},
|
|
183
|
+
"exportMethod": {
|
|
184
|
+
"description": "Native projects (type native): the method of xcodebuild -exportArchive. It must agree with the distribution: app-store-connect or app-store for the stores, release-testing or ad-hoc for the internal builds. By default, the one of the distribution.",
|
|
185
|
+
"enum": [
|
|
186
|
+
"app-store-connect",
|
|
187
|
+
"app-store",
|
|
188
|
+
"release-testing",
|
|
189
|
+
"ad-hoc"
|
|
190
|
+
]
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
},
|
|
194
|
+
"android": {
|
|
195
|
+
"type": "object",
|
|
196
|
+
"additionalProperties": false,
|
|
197
|
+
"properties": {
|
|
198
|
+
"format": {
|
|
199
|
+
"description": "apk installs directly (internal); aab is what Google Play takes (store).",
|
|
200
|
+
"enum": [
|
|
201
|
+
"apk",
|
|
202
|
+
"aab"
|
|
203
|
+
]
|
|
204
|
+
},
|
|
205
|
+
"directory": {
|
|
206
|
+
"description": "Native projects (type native): the folder of the Gradle project (the one with gradlew), relative to the root of the repository. android by default.",
|
|
207
|
+
"$ref": "#/$defs/projectPath"
|
|
208
|
+
},
|
|
209
|
+
"gradleTask": {
|
|
210
|
+
"description": "Native projects (type native): the Gradle task that makes the release, such as :app:bundleRelease. By default :app:bundleRelease for an aab and :app:assembleRelease for an apk. The runner signs it with the upload key (android.injected.signing.*) and passes the build number as -Pias.buildNumber and IAS_BUILD_NUMBER.",
|
|
211
|
+
"type": "string",
|
|
212
|
+
"pattern": "^:?[A-Za-z0-9_][A-Za-z0-9_.:-]{0,99}$"
|
|
213
|
+
},
|
|
214
|
+
"outputPath": {
|
|
215
|
+
"description": "Native projects (type native): where the task leaves the apk or aab, relative to android.directory. app/build/outputs/bundle/release/app-release.aab (or apk/release/app-release.apk) by default.",
|
|
216
|
+
"$ref": "#/$defs/projectPath"
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
},
|
|
222
|
+
"updates": {
|
|
223
|
+
"description": "OTA updates (ias update). The server and project come from updates.url in app.json, which is what the builds ask, or without it from server and app, as the config plugin of @imagina-build/ias writes it; the certificate and keyid, from its updates.codeSigning* or from signing.",
|
|
224
|
+
"type": "object",
|
|
225
|
+
"additionalProperties": false,
|
|
226
|
+
"properties": {
|
|
227
|
+
"signing": {
|
|
228
|
+
"type": "object",
|
|
229
|
+
"additionalProperties": false,
|
|
230
|
+
"properties": {
|
|
231
|
+
"keyId": {
|
|
232
|
+
"description": "The keyid of the certificate: the one of updates.codeSigningMetadata.keyid in app.json. Without it there, the config plugin of @imagina-build/ias writes this one into the builds.",
|
|
233
|
+
"type": "string",
|
|
234
|
+
"pattern": "^[A-Za-z0-9._-]{1,64}$"
|
|
235
|
+
},
|
|
236
|
+
"certificate": {
|
|
237
|
+
"description": "The certificate (public) of the default channel, a file of the repository: the one of updates.codeSigningCertificate in app.json. Without it there, the config plugin of @imagina-build/ias writes this one into the builds; if both are written, they must be the same file.",
|
|
238
|
+
"type": "string",
|
|
239
|
+
"minLength": 1
|
|
240
|
+
},
|
|
241
|
+
"privateKey": {
|
|
242
|
+
"description": "The private key of the certificate, in 1Password. It never leaves the Mac that publishes; IAS_UPDATES_PRIVATE_KEY (a reference or a file outside the project) wins over it.",
|
|
243
|
+
"$ref": "#/$defs/secretReference"
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
},
|
|
247
|
+
"channels": {
|
|
248
|
+
"description": "The other channels of the OTA project, by name (API.md §36.8), each with a key of its own: what ias update signs with for --channel <name> and promote --to <name>, and the certificate its builds carry. The default channel (app.json's certificate, or signing) needs no entry.",
|
|
249
|
+
"type": "object",
|
|
250
|
+
"propertyNames": {
|
|
251
|
+
"$ref": "#/$defs/profileName"
|
|
252
|
+
},
|
|
253
|
+
"additionalProperties": {
|
|
254
|
+
"type": "object",
|
|
255
|
+
"additionalProperties": false,
|
|
256
|
+
"required": [
|
|
257
|
+
"keyId",
|
|
258
|
+
"privateKey",
|
|
259
|
+
"certificate"
|
|
260
|
+
],
|
|
261
|
+
"properties": {
|
|
262
|
+
"keyId": {
|
|
263
|
+
"description": "The keyid of the channel's certificate, the one its builds carry in updates.codeSigningMetadata.keyid.",
|
|
264
|
+
"type": "string",
|
|
265
|
+
"pattern": "^[A-Za-z0-9._-]{1,64}$"
|
|
266
|
+
},
|
|
267
|
+
"privateKey": {
|
|
268
|
+
"description": "The private key of the channel's certificate, in 1Password. It never leaves the Mac that publishes.",
|
|
269
|
+
"$ref": "#/$defs/secretReference"
|
|
270
|
+
},
|
|
271
|
+
"certificate": {
|
|
272
|
+
"description": "The channel's certificate (public), a file of the repository: the one its builds carry.",
|
|
273
|
+
"type": "string",
|
|
274
|
+
"minLength": 1
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
},
|
|
279
|
+
"token": {
|
|
280
|
+
"description": "The publishing token of the OTA project (ota_…, php artisan ota:token). IAS_TOKEN wins over it.",
|
|
281
|
+
"$ref": "#/$defs/secretReference"
|
|
282
|
+
},
|
|
283
|
+
"envFile": {
|
|
284
|
+
"description": "The environment file of production, ignored by git, that is copied to the export: what is not in the commit does not reach the bundle.",
|
|
285
|
+
"type": "string",
|
|
286
|
+
"minLength": 1
|
|
287
|
+
},
|
|
288
|
+
"bundleEnv": {
|
|
289
|
+
"description": "Variables of envFile whose value the bundle must carry. If one is missing, nothing is published.",
|
|
290
|
+
"type": "array",
|
|
291
|
+
"items": {
|
|
292
|
+
"type": "string",
|
|
293
|
+
"pattern": "^[A-Za-z_][A-Za-z0-9_]*$"
|
|
294
|
+
},
|
|
295
|
+
"uniqueItems": true
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
},
|
|
299
|
+
"distribution": {
|
|
300
|
+
"description": "Internal distribution (ias dist): ad hoc IPAs and APKs on the install pages.",
|
|
301
|
+
"type": "object",
|
|
302
|
+
"additionalProperties": false,
|
|
303
|
+
"properties": {
|
|
304
|
+
"project": {
|
|
305
|
+
"description": "The code of the distribution project (php artisan dist:project), when it is not the one of app.",
|
|
306
|
+
"$ref": "#/$defs/projectCode"
|
|
307
|
+
},
|
|
308
|
+
"token": {
|
|
309
|
+
"description": "The publishing token of the distribution project (dist_…, php artisan dist:token). IAS_TOKEN wins over it.",
|
|
310
|
+
"$ref": "#/$defs/secretReference"
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
},
|
|
314
|
+
"push": {
|
|
315
|
+
"description": "Push notifications in the app (API.md §38): what ias.push of @imagina-build/ias and its config plugin (@imagina-build/ias/plugin) need. Nothing here is a secret.",
|
|
316
|
+
"type": "object",
|
|
317
|
+
"additionalProperties": false,
|
|
318
|
+
"properties": {
|
|
319
|
+
"installKey": {
|
|
320
|
+
"description": "The install key of the application (iasi_…), from the panel: the application › Notificaciones › Dispositivos. With it the app registers its device without a session of Sync. It is not a secret: anyone with the app can read it (API.md §38.1). The plugin puts it in extra.ias of the app config; after changing it in the panel, the builds that carry the old one no longer register devices.",
|
|
321
|
+
"type": "string",
|
|
322
|
+
"pattern": "^iasi_[A-Za-z0-9_-]{43}$"
|
|
323
|
+
},
|
|
324
|
+
"background": {
|
|
325
|
+
"description": "Whether the sync notices (API.md §39) wake the app in the background: the remote-notification background mode of iOS. true by default.",
|
|
326
|
+
"type": "boolean",
|
|
327
|
+
"default": true
|
|
328
|
+
},
|
|
329
|
+
"ios": {
|
|
330
|
+
"type": "object",
|
|
331
|
+
"additionalProperties": false,
|
|
332
|
+
"properties": {
|
|
333
|
+
"apsEnvironment": {
|
|
334
|
+
"description": "The aps-environment entitlement: development by default, as expo-notifications; exporting a store or ad hoc build signs it with the profile's production.",
|
|
335
|
+
"enum": [
|
|
336
|
+
"development",
|
|
337
|
+
"production"
|
|
338
|
+
]
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
},
|
|
342
|
+
"android": {
|
|
343
|
+
"type": "object",
|
|
344
|
+
"additionalProperties": false,
|
|
345
|
+
"properties": {
|
|
346
|
+
"googleServicesFile": {
|
|
347
|
+
"description": "The google-services.json of the app's Firebase project, a file of the repository: android.googleServicesFile of the app config, which FCM needs.",
|
|
348
|
+
"type": "string",
|
|
349
|
+
"minLength": 1
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
},
|
|
355
|
+
"workflow": {
|
|
356
|
+
"type": "object",
|
|
357
|
+
"additionalProperties": false,
|
|
358
|
+
"required": [
|
|
359
|
+
"on",
|
|
360
|
+
"steps"
|
|
361
|
+
],
|
|
362
|
+
"properties": {
|
|
363
|
+
"description": {
|
|
364
|
+
"type": "string",
|
|
365
|
+
"maxLength": 200
|
|
366
|
+
},
|
|
367
|
+
"on": {
|
|
368
|
+
"description": "What starts it: a tag or a branch pushed that matches the pattern (* matches any characters), or any pull request opened or updated from this repository. A workflow can also be started by hand (ias workflows run).",
|
|
369
|
+
"type": "object",
|
|
370
|
+
"additionalProperties": false,
|
|
371
|
+
"minProperties": 1,
|
|
372
|
+
"properties": {
|
|
373
|
+
"tag": {
|
|
374
|
+
"type": "string",
|
|
375
|
+
"pattern": "^[A-Za-z0-9._/*?-]{1,100}$"
|
|
376
|
+
},
|
|
377
|
+
"branch": {
|
|
378
|
+
"type": "string",
|
|
379
|
+
"pattern": "^[A-Za-z0-9._/*?-]{1,100}$"
|
|
380
|
+
},
|
|
381
|
+
"pull_request": {
|
|
382
|
+
"type": "boolean"
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
},
|
|
386
|
+
"steps": {
|
|
387
|
+
"description": "In order. A submit step sends the store builds of the build step before it.",
|
|
388
|
+
"type": "array",
|
|
389
|
+
"minItems": 1,
|
|
390
|
+
"maxItems": 10,
|
|
391
|
+
"items": {
|
|
392
|
+
"oneOf": [
|
|
393
|
+
{
|
|
394
|
+
"$ref": "#/$defs/workflowBuildStep"
|
|
395
|
+
},
|
|
396
|
+
{
|
|
397
|
+
"$ref": "#/$defs/workflowSubmitStep"
|
|
398
|
+
}
|
|
399
|
+
]
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
},
|
|
404
|
+
"workflowBuildStep": {
|
|
405
|
+
"type": "object",
|
|
406
|
+
"additionalProperties": false,
|
|
407
|
+
"required": [
|
|
408
|
+
"build"
|
|
409
|
+
],
|
|
410
|
+
"properties": {
|
|
411
|
+
"build": {
|
|
412
|
+
"type": "object",
|
|
413
|
+
"additionalProperties": false,
|
|
414
|
+
"required": [
|
|
415
|
+
"profile"
|
|
416
|
+
],
|
|
417
|
+
"properties": {
|
|
418
|
+
"profile": {
|
|
419
|
+
"description": "A build profile of build.",
|
|
420
|
+
"$ref": "#/$defs/profileName"
|
|
421
|
+
},
|
|
422
|
+
"platform": {
|
|
423
|
+
"description": "ios, android or all (both, the default).",
|
|
424
|
+
"enum": [
|
|
425
|
+
"ios",
|
|
426
|
+
"android",
|
|
427
|
+
"all"
|
|
428
|
+
]
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
},
|
|
434
|
+
"workflowSubmitStep": {
|
|
435
|
+
"type": "object",
|
|
436
|
+
"additionalProperties": false,
|
|
437
|
+
"required": [
|
|
438
|
+
"submit"
|
|
439
|
+
],
|
|
440
|
+
"properties": {
|
|
441
|
+
"submit": {
|
|
442
|
+
"description": "Where each platform goes. A platform without a build in the step before is left out. Releases are always drafts: they are sent for review in the store.",
|
|
443
|
+
"type": "object",
|
|
444
|
+
"additionalProperties": false,
|
|
445
|
+
"minProperties": 1,
|
|
446
|
+
"properties": {
|
|
447
|
+
"ios": {
|
|
448
|
+
"type": "object",
|
|
449
|
+
"additionalProperties": false,
|
|
450
|
+
"required": [
|
|
451
|
+
"target"
|
|
452
|
+
],
|
|
453
|
+
"properties": {
|
|
454
|
+
"target": {
|
|
455
|
+
"enum": [
|
|
456
|
+
"app_store_connect"
|
|
457
|
+
]
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
},
|
|
461
|
+
"android": {
|
|
462
|
+
"type": "object",
|
|
463
|
+
"additionalProperties": false,
|
|
464
|
+
"required": [
|
|
465
|
+
"target",
|
|
466
|
+
"track"
|
|
467
|
+
],
|
|
468
|
+
"properties": {
|
|
469
|
+
"target": {
|
|
470
|
+
"enum": [
|
|
471
|
+
"google_play"
|
|
472
|
+
]
|
|
473
|
+
},
|
|
474
|
+
"track": {
|
|
475
|
+
"description": "The Google Play track: production, beta, alpha, internal or a custom one.",
|
|
476
|
+
"type": "string",
|
|
477
|
+
"pattern": "^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$"
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
},
|
|
485
|
+
"submitProfile": {
|
|
486
|
+
"type": "object",
|
|
487
|
+
"additionalProperties": false,
|
|
488
|
+
"properties": {
|
|
489
|
+
"ios": {
|
|
490
|
+
"type": "object",
|
|
491
|
+
"additionalProperties": false,
|
|
492
|
+
"properties": {
|
|
493
|
+
"ascAppId": {
|
|
494
|
+
"description": "The Apple ID of the app in App Store Connect.",
|
|
495
|
+
"type": "string",
|
|
496
|
+
"pattern": "^[0-9]+$"
|
|
497
|
+
},
|
|
498
|
+
"appleTeamId": {
|
|
499
|
+
"type": "string",
|
|
500
|
+
"pattern": "^[A-Z0-9]{10}$"
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
},
|
|
504
|
+
"android": {
|
|
505
|
+
"type": "object",
|
|
506
|
+
"additionalProperties": false,
|
|
507
|
+
"properties": {
|
|
508
|
+
"track": {
|
|
509
|
+
"description": "The Google Play track: production, beta, alpha, internal or a custom one.",
|
|
510
|
+
"type": "string",
|
|
511
|
+
"minLength": 1
|
|
512
|
+
},
|
|
513
|
+
"releaseStatus": {
|
|
514
|
+
"enum": [
|
|
515
|
+
"completed",
|
|
516
|
+
"draft",
|
|
517
|
+
"halted",
|
|
518
|
+
"inProgress"
|
|
519
|
+
]
|
|
520
|
+
},
|
|
521
|
+
"rollout": {
|
|
522
|
+
"description": "The fraction of users for a release in progress.",
|
|
523
|
+
"type": "number",
|
|
524
|
+
"minimum": 0,
|
|
525
|
+
"maximum": 1
|
|
526
|
+
},
|
|
527
|
+
"changesNotSentForReview": {
|
|
528
|
+
"type": "boolean"
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// The signed content of a CMS SignedData message (RFC 5652), in DER or in BER with indefinite lengths: a provisioning
|
|
2
|
+
// profile is one, with its property list inside. The signature is not verified: the profile is only read for what it
|
|
3
|
+
// says, and codesign checks it when a build uses it.
|
|
4
|
+
import { Buffer } from 'node:buffer';
|
|
5
|
+
|
|
6
|
+
const SIGNED_DATA = '1.2.840.113549.1.7.2';
|
|
7
|
+
const OCTET_STRING = 4;
|
|
8
|
+
const MAX_DEPTH = 32;
|
|
9
|
+
|
|
10
|
+
const fail = () => {
|
|
11
|
+
throw new Error('not a CMS signed message');
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/** One BER node at `offset`: its tag, whether it is constructed, and where its content and the node end. */
|
|
15
|
+
function readNode(bytes, offset, depth) {
|
|
16
|
+
if (depth > MAX_DEPTH || offset + 2 > bytes.length) fail();
|
|
17
|
+
const first = bytes[offset];
|
|
18
|
+
let position = offset + 1;
|
|
19
|
+
let tag = first & 0x1f;
|
|
20
|
+
if (tag === 0x1f) {
|
|
21
|
+
tag = 0;
|
|
22
|
+
let byte;
|
|
23
|
+
do {
|
|
24
|
+
if (position >= bytes.length || tag > 0xffffff) fail();
|
|
25
|
+
byte = bytes[position++];
|
|
26
|
+
tag = tag * 128 + (byte & 0x7f);
|
|
27
|
+
} while (byte & 0x80);
|
|
28
|
+
}
|
|
29
|
+
const result = { tagClass: first >> 6, constructed: (first & 0x20) !== 0, tag, start: 0, end: 0, next: 0 };
|
|
30
|
+
if (position >= bytes.length) fail();
|
|
31
|
+
const lengthByte = bytes[position++];
|
|
32
|
+
if (lengthByte === 0x80) {
|
|
33
|
+
if (!result.constructed) fail();
|
|
34
|
+
result.start = position;
|
|
35
|
+
let cursor = position;
|
|
36
|
+
for (;;) {
|
|
37
|
+
if (cursor + 2 > bytes.length) fail();
|
|
38
|
+
if (bytes[cursor] === 0 && bytes[cursor + 1] === 0) break;
|
|
39
|
+
cursor = readNode(bytes, cursor, depth + 1).next;
|
|
40
|
+
}
|
|
41
|
+
result.end = cursor;
|
|
42
|
+
result.next = cursor + 2;
|
|
43
|
+
return result;
|
|
44
|
+
}
|
|
45
|
+
let length = lengthByte;
|
|
46
|
+
if (lengthByte & 0x80) {
|
|
47
|
+
const count = lengthByte & 0x7f;
|
|
48
|
+
if (count > 4 || position + count > bytes.length) fail();
|
|
49
|
+
length = 0;
|
|
50
|
+
for (let index = 0; index < count; index++) length = length * 256 + bytes[position++];
|
|
51
|
+
}
|
|
52
|
+
if (position + length > bytes.length) fail();
|
|
53
|
+
Object.assign(result, { start: position, end: position + length, next: position + length });
|
|
54
|
+
return result;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function children(bytes, parent, depth) {
|
|
58
|
+
const list = [];
|
|
59
|
+
for (let cursor = parent.start; cursor < parent.end; ) {
|
|
60
|
+
const child = readNode(bytes, cursor, depth + 1);
|
|
61
|
+
list.push(child);
|
|
62
|
+
cursor = child.next;
|
|
63
|
+
}
|
|
64
|
+
return list;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function oid(bytes, { start, end }) {
|
|
68
|
+
const parts = [];
|
|
69
|
+
let value = 0;
|
|
70
|
+
for (let index = start; index < end; index++) {
|
|
71
|
+
value = value * 128 + (bytes[index] & 0x7f);
|
|
72
|
+
if (!(bytes[index] & 0x80)) {
|
|
73
|
+
parts.push(value);
|
|
74
|
+
value = 0;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (parts.length === 0) return '';
|
|
78
|
+
const head = parts[0] < 80 ? [Math.floor(parts[0] / 40), parts[0] % 40] : [2, parts[0] - 80];
|
|
79
|
+
return [...head, ...parts.slice(1)].join('.');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** The bytes of an OCTET STRING, joining the pieces of a constructed one. */
|
|
83
|
+
function octets(bytes, string, depth) {
|
|
84
|
+
if (string.tagClass !== 0 || string.tag !== OCTET_STRING) fail();
|
|
85
|
+
if (!string.constructed) return bytes.subarray(string.start, string.end);
|
|
86
|
+
return Buffer.concat(children(bytes, string, depth).map((piece) => octets(bytes, piece, depth + 1)));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** The encapsulated content of a CMS SignedData message (a provisioning profile gives its XML property list). */
|
|
90
|
+
export function signedContent(der) {
|
|
91
|
+
const bytes = Buffer.from(der);
|
|
92
|
+
const root = readNode(bytes, 0, 0);
|
|
93
|
+
if (root.tag !== 16 || !root.constructed) fail();
|
|
94
|
+
const [type, explicit] = children(bytes, root, 0);
|
|
95
|
+
if (!type || type.tag !== 6 || oid(bytes, type) !== SIGNED_DATA || !explicit || explicit.tagClass !== 2 || explicit.tag !== 0) fail();
|
|
96
|
+
const [signedData] = children(bytes, explicit, 1);
|
|
97
|
+
if (!signedData || signedData.tag !== 16) fail();
|
|
98
|
+
const encapsulated = children(bytes, signedData, 2)[2];
|
|
99
|
+
if (!encapsulated || encapsulated.tag !== 16) fail();
|
|
100
|
+
const [, content] = children(bytes, encapsulated, 3);
|
|
101
|
+
if (!content || content.tagClass !== 2 || content.tag !== 0) fail();
|
|
102
|
+
const [string] = children(bytes, content, 4);
|
|
103
|
+
if (!string) fail();
|
|
104
|
+
return Buffer.from(octets(bytes, string, 5));
|
|
105
|
+
}
|