@imagina-build/ias-cli 0.0.0 → 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 (78) hide show
  1. package/README.md +406 -2
  2. package/bin/ias.mjs +19 -0
  3. package/package.json +31 -12
  4. package/schema/ias.schema.json +535 -0
  5. package/shared/credentials/cms.mjs +105 -0
  6. package/shared/credentials/index.mjs +354 -0
  7. package/shared/credentials/plist.mjs +93 -0
  8. package/shared/vault/approvers-vectors.mjs +188 -0
  9. package/shared/vault/approvers.mjs +234 -0
  10. package/shared/vault/index.mjs +583 -0
  11. package/shared/vault/plaintext.mjs +115 -0
  12. package/shared/vault/test-vectors/ias-vault-2/approvers.json +2122 -0
  13. package/shared/vault/test-vectors/ias-vault-2/keys.json +43 -0
  14. package/shared/vault/test-vectors/ias-vault-2/malformed.json +1739 -0
  15. package/shared/vault/test-vectors/ias-vault-2/plaintexts.json +149 -0
  16. package/shared/vault/test-vectors/ias-vault-2/runners.json +580 -0
  17. package/shared/vault/test-vectors/ias-vault-2/sealed.json +567 -0
  18. package/shared/vault/test-vectors/ias-vault-2/tampered.json +989 -0
  19. package/shared/vault/test-vectors/keys.json +31 -0
  20. package/shared/vault/test-vectors/malformed.json +1341 -0
  21. package/shared/vault/test-vectors/sealed.json +384 -0
  22. package/shared/vault/test-vectors/tampered.json +610 -0
  23. package/shared/vault/vectors.mjs +720 -0
  24. package/src/cli.mjs +231 -0
  25. package/src/commands/apps/index.mjs +36 -0
  26. package/src/commands/auth/index.mjs +141 -0
  27. package/src/commands/build/follow.mjs +168 -0
  28. package/src/commands/build/index.mjs +110 -0
  29. package/src/commands/build/local.mjs +69 -0
  30. package/src/commands/build/profile.mjs +50 -0
  31. package/src/commands/build/source.mjs +71 -0
  32. package/src/commands/builds/index.mjs +148 -0
  33. package/src/commands/credentials/index.mjs +256 -0
  34. package/src/commands/credentials/material.mjs +159 -0
  35. package/src/commands/devices/asc.mjs +116 -0
  36. package/src/commands/devices/index.mjs +65 -0
  37. package/src/commands/devices/sync.mjs +223 -0
  38. package/src/commands/dist/index.mjs +108 -0
  39. package/src/commands/doctor/index.mjs +158 -0
  40. package/src/commands/env/index.mjs +163 -0
  41. package/src/commands/github/index.mjs +53 -0
  42. package/src/commands/init/index.mjs +224 -0
  43. package/src/commands/migrate/credentials.mjs +213 -0
  44. package/src/commands/migrate/eas-cli.mjs +153 -0
  45. package/src/commands/migrate/index.mjs +73 -0
  46. package/src/commands/migrate/numbers.mjs +85 -0
  47. package/src/commands/migrate/profiles.mjs +174 -0
  48. package/src/commands/migrate/project.mjs +249 -0
  49. package/src/commands/migrate/survey.mjs +205 -0
  50. package/src/commands/migrate/variables.mjs +183 -0
  51. package/src/commands/push/index.mjs +334 -0
  52. package/src/commands/sdk/index.mjs +81 -0
  53. package/src/commands/submit/index.mjs +85 -0
  54. package/src/commands/update/index.mjs +526 -0
  55. package/src/commands/usage/index.mjs +57 -0
  56. package/src/commands/vault/approvers.mjs +129 -0
  57. package/src/commands/vault/index.mjs +159 -0
  58. package/src/commands/vault/runners.mjs +115 -0
  59. package/src/commands/vault/team.mjs +111 -0
  60. package/src/commands/version/index.mjs +71 -0
  61. package/src/commands/webhooks/index.mjs +104 -0
  62. package/src/commands/workflows/index.mjs +88 -0
  63. package/src/config.mjs +261 -0
  64. package/src/dist.mjs +99 -0
  65. package/src/errors.mjs +158 -0
  66. package/src/http.mjs +45 -0
  67. package/src/legacy.mjs +73 -0
  68. package/src/manage.mjs +120 -0
  69. package/src/ota.mjs +122 -0
  70. package/src/output.mjs +36 -0
  71. package/src/process.mjs +45 -0
  72. package/src/schema.mjs +131 -0
  73. package/src/sdk.mjs +120 -0
  74. package/src/secret-input.mjs +27 -0
  75. package/src/secrets.mjs +37 -0
  76. package/src/shared.mjs +5 -0
  77. package/src/team-vault.mjs +152 -0
  78. package/src/token-store.mjs +93 -0
@@ -0,0 +1,115 @@
1
+ // What an ias-vault-2 envelope holds for each type of credential (docs/technical/vault.md §3.5): one UTF-8 JSON object with the
2
+ // type, the files (base64url of their exact bytes) and the fields the runner needs to sign with them. The CLI writes
3
+ // it with encodePlaintext before sealing and the runner reads it with decodePlaintext after opening; the server never
4
+ // sees it. The type inside must be the credential.type of the envelope, so a plaintext of one type is never read as
5
+ // another.
6
+ import { Buffer } from 'node:buffer';
7
+
8
+ const APPLE_ID = /^[A-Z0-9]{10}$/;
9
+ const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
10
+ const APPLICATION = /^[a-z0-9][a-z0-9-]{0,31}$/;
11
+ const VARIABLE = /^[A-Z][A-Z0-9_]{0,63}$/;
12
+ const ENVIRONMENTS = ['development', 'preview', 'production'];
13
+
14
+ /** The kinds of field: what each one holds and how it is checked. `file` is base64url, the rest JSON strings. */
15
+ const KINDS = {
16
+ file: { rule: 'the base64url (no padding) of a file of at least one byte', check: (value) => decodeFile(value) !== null },
17
+ password: { rule: 'a string of at most 1024 characters, which may be empty', check: (value) => typeof value === 'string' && value.length <= 1024 },
18
+ secret: { rule: 'a string of 1 to 1024 characters', check: (value) => typeof value === 'string' && value.length >= 1 && value.length <= 1024 },
19
+ alias: { rule: 'a string of 1 to 255 characters', check: (value) => typeof value === 'string' && value.length >= 1 && value.length <= 255 },
20
+ appleId: { rule: '10 characters of A-Z and 0-9', check: (value) => typeof value === 'string' && APPLE_ID.test(value) },
21
+ uuid: { rule: 'a UUID', check: (value) => typeof value === 'string' && UUID.test(value) },
22
+ application: { rule: 'the code of an application: 1 to 32 characters of a-z 0-9 -, not starting with -', check: (value) => typeof value === 'string' && APPLICATION.test(value) },
23
+ environment: { rule: `one of ${ENVIRONMENTS.join(', ')}`, check: (value) => ENVIRONMENTS.includes(value) },
24
+ variable: { rule: 'a name of A-Z 0-9 _ that starts with a letter, up to 64, and not EXPO_PUBLIC_ (those are always plain)', check: (value) => typeof value === 'string' && VARIABLE.test(value) && !value.startsWith('EXPO_PUBLIC_') },
25
+ value: { rule: 'a string', check: (value) => typeof value === 'string' },
26
+ };
27
+
28
+ /**
29
+ * The plaintext of each type, field by field and in this order after `type`. `scope` says whether the credential is
30
+ * the team's (shared by several applications) or one application's, which then goes inside as `application`.
31
+ */
32
+ export const PLAINTEXT_TYPES = Object.freeze({
33
+ apple_distribution_certificate: { scope: 'team', fields: { p12: 'file', password: 'password' } },
34
+ apple_provisioning_profile: { scope: 'application', fields: { application: 'application', mobileprovision: 'file' } },
35
+ android_upload_keystore: { scope: 'application', fields: { application: 'application', keystore: 'file', store_password: 'secret', key_alias: 'alias', key_password: 'secret' } },
36
+ apns_key: { scope: 'team', fields: { p8: 'file', key_id: 'appleId', team_id: 'appleId' } },
37
+ asc_api_key: { scope: 'team', fields: { p8: 'file', key_id: 'appleId', issuer_id: 'uuid' } },
38
+ google_service_account: { scope: 'application', fields: { application: 'application', json: 'file' } },
39
+ environment_variable: { scope: 'application', fields: { application: 'application', environment: 'environment', name: 'variable', value: 'value' } },
40
+ });
41
+
42
+ /** A plaintext that is not what its type says. `code` is stable (docs/technical/vault.md §3.5); the message holds no value. */
43
+ export class PlaintextError extends Error {
44
+ constructor(code, message) {
45
+ super(message);
46
+ this.name = 'PlaintextError';
47
+ this.code = code;
48
+ }
49
+ }
50
+
51
+ function decodeFile(text) {
52
+ if (typeof text !== 'string' || !/^[A-Za-z0-9_-]+$/.test(text) || text.length % 4 === 1) return null;
53
+ const bytes = Buffer.from(text, 'base64url');
54
+ return bytes.length > 0 && bytes.toString('base64url') === text ? bytes : null;
55
+ }
56
+
57
+ function schemaOf(type) {
58
+ const schema = PLAINTEXT_TYPES[type];
59
+ if (!schema) throw new PlaintextError('plaintext.type', `there is no plaintext for the type ${type}: it is one of ${Object.keys(PLAINTEXT_TYPES).join(', ')}`);
60
+ return schema;
61
+ }
62
+
63
+ function problemOf(type, value) {
64
+ const schema = PLAINTEXT_TYPES[type];
65
+ if (!schema) return ['plaintext.type', `there is no plaintext for the type ${type}`];
66
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) return ['plaintext.json', 'the plaintext must be a JSON object'];
67
+ const names = ['type', ...Object.keys(schema.fields)];
68
+ const own = Object.keys(value);
69
+ if (own.length !== names.length || !names.every((name) => own.includes(name))) return ['plaintext.fields', `the plaintext of ${type} has exactly ${names.join(', ')}`];
70
+ if (value.type !== type) return ['plaintext.type', `the plaintext is of type ${typeof value.type === 'string' ? value.type : 'nothing'}, not ${type}`];
71
+ for (const [name, kind] of Object.entries(schema.fields)) {
72
+ if (!KINDS[kind].check(value[name])) return [`plaintext.${name}`, `${name} must be ${KINDS[kind].rule}`];
73
+ }
74
+ return null;
75
+ }
76
+
77
+ /** Checks the plaintext bytes of `type`: { valid: true } or { valid: false, code, message }. */
78
+ export function validatePlaintext(type, bytes) {
79
+ let value;
80
+ try {
81
+ value = JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(bytes));
82
+ } catch {
83
+ return { valid: false, code: 'plaintext.json', message: 'the plaintext is not UTF-8 JSON' };
84
+ }
85
+ const problem = problemOf(type, value);
86
+ return problem ? { valid: false, code: problem[0], message: problem[1] } : { valid: true };
87
+ }
88
+
89
+ /**
90
+ * The plaintext of `type` from its fields: files as bytes (or their base64url), the rest as strings. Returns the bytes
91
+ * to seal: compact JSON with `type` first and the fields in the order of PLAINTEXT_TYPES.
92
+ */
93
+ export function encodePlaintext(type, fields) {
94
+ const schema = schemaOf(type);
95
+ const value = { type };
96
+ for (const [name, kind] of Object.entries(schema.fields)) {
97
+ const given = fields?.[name];
98
+ value[name] = kind === 'file' && given instanceof Uint8Array ? Buffer.from(given).toString('base64url') : given;
99
+ }
100
+ const extra = Object.keys(fields ?? {}).filter((name) => !(name in schema.fields));
101
+ if (extra.length > 0) throw new PlaintextError('plaintext.fields', `the plaintext of ${type} has no ${extra.join(', ')}`);
102
+ const problem = problemOf(type, value);
103
+ if (problem) throw new PlaintextError(...problem);
104
+ return Buffer.from(JSON.stringify(value), 'utf8');
105
+ }
106
+
107
+ /** The fields of a plaintext of `type`, checked, with each file as bytes. */
108
+ export function decodePlaintext(type, bytes) {
109
+ schemaOf(type);
110
+ const result = validatePlaintext(type, bytes);
111
+ if (!result.valid) throw new PlaintextError(result.code, result.message);
112
+ const value = JSON.parse(Buffer.from(bytes).toString('utf8'));
113
+ for (const [name, kind] of Object.entries(PLAINTEXT_TYPES[type].fields)) if (kind === 'file') value[name] = decodeFile(value[name]);
114
+ return value;
115
+ }