hotcodepush 0.0.1 → 0.2.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 (165) hide show
  1. package/README.md +3 -3
  2. package/dist/commands/app/create.js +49 -0
  3. package/dist/commands/app/delete.js +34 -0
  4. package/dist/commands/app/get.js +33 -0
  5. package/dist/commands/app/list.js +38 -0
  6. package/dist/commands/app/transfer.js +58 -0
  7. package/dist/commands/app/update.js +34 -0
  8. package/dist/commands/audience/get.js +69 -0
  9. package/dist/commands/audit-log/list.js +82 -0
  10. package/dist/commands/binary/create.js +191 -0
  11. package/dist/commands/binary/get.js +56 -0
  12. package/dist/commands/binary/list.js +58 -0
  13. package/dist/commands/bundle/delete.js +35 -0
  14. package/dist/commands/bundle/get.js +63 -0
  15. package/dist/commands/bundle/list.js +66 -0
  16. package/dist/commands/bundle/upload.js +177 -0
  17. package/dist/commands/channel/create.js +59 -0
  18. package/dist/commands/channel/delete.js +68 -0
  19. package/dist/commands/channel/get.js +45 -0
  20. package/dist/commands/channel/list.js +39 -0
  21. package/dist/commands/channel/pause.js +32 -0
  22. package/dist/commands/channel/resume.js +32 -0
  23. package/dist/commands/channel/update.js +40 -0
  24. package/dist/commands/checkout/create.js +25 -0
  25. package/dist/commands/completion.js +37 -0
  26. package/dist/commands/customer-portal-session/create.js +25 -0
  27. package/dist/commands/device/delete.js +29 -0
  28. package/dist/commands/device/get.js +60 -0
  29. package/dist/commands/device/list.js +97 -0
  30. package/dist/commands/device/probe.js +253 -0
  31. package/dist/commands/doctor.js +364 -0
  32. package/dist/commands/fingerprint/diff.js +134 -0
  33. package/dist/commands/fingerprint.js +42 -0
  34. package/dist/commands/init.js +493 -0
  35. package/dist/commands/invitation/accept.js +53 -0
  36. package/dist/commands/invitation/create.js +52 -0
  37. package/dist/commands/invitation/delete.js +60 -0
  38. package/dist/commands/invitation/list.js +40 -0
  39. package/dist/commands/limit/list.js +34 -0
  40. package/dist/commands/login.js +36 -0
  41. package/dist/commands/logout.js +81 -0
  42. package/dist/commands/member/delete.js +35 -0
  43. package/dist/commands/member/get.js +37 -0
  44. package/dist/commands/member/list.js +41 -0
  45. package/dist/commands/member/update.js +53 -0
  46. package/dist/commands/open.js +37 -0
  47. package/dist/commands/organization/create.js +30 -0
  48. package/dist/commands/organization/delete.js +39 -0
  49. package/dist/commands/organization/get.js +38 -0
  50. package/dist/commands/organization/list.js +41 -0
  51. package/dist/commands/organization/update.js +64 -0
  52. package/dist/commands/release/create.js +237 -0
  53. package/dist/commands/release/get.js +81 -0
  54. package/dist/commands/release/list.js +69 -0
  55. package/dist/commands/release/pause.js +36 -0
  56. package/dist/commands/release/resume.js +36 -0
  57. package/dist/commands/release/revoke.js +254 -0
  58. package/dist/commands/release/rollback.js +73 -0
  59. package/dist/commands/release/rollout.js +63 -0
  60. package/dist/commands/release/update.js +82 -0
  61. package/dist/commands/resource-file/write.js +78 -0
  62. package/dist/commands/signing-key/add.js +45 -0
  63. package/dist/commands/signing-key/create.js +84 -0
  64. package/dist/commands/signing-key/delete.js +67 -0
  65. package/dist/commands/signing-key/list.js +38 -0
  66. package/dist/commands/sso-provider/delete.js +32 -0
  67. package/dist/commands/sso-provider/get.js +63 -0
  68. package/dist/commands/sso-provider/set.js +161 -0
  69. package/dist/commands/sso-provider/verify.js +31 -0
  70. package/dist/commands/statistics/get.js +189 -0
  71. package/dist/commands/usage/get.js +31 -0
  72. package/dist/commands/usage/list.js +41 -0
  73. package/dist/commands/webhook/create.js +72 -0
  74. package/dist/commands/webhook/delete.js +32 -0
  75. package/dist/commands/webhook/get.js +34 -0
  76. package/dist/commands/webhook/list.js +40 -0
  77. package/dist/commands/webhook/test.js +32 -0
  78. package/dist/commands/webhook/update.js +56 -0
  79. package/dist/commands/webhook-delivery/list.js +63 -0
  80. package/dist/commands/webhook-delivery/redeliver.js +48 -0
  81. package/dist/commands/whoami.js +24 -0
  82. package/dist/completion/hotcodepush.bash +536 -0
  83. package/dist/completion/hotcodepush.fish +979 -0
  84. package/dist/completion/hotcodepush.zsh +1372 -0
  85. package/dist/config/consts.js +32 -0
  86. package/dist/index.js +5 -0
  87. package/dist/registry.js +83 -0
  88. package/dist/utils/api-client.js +17 -0
  89. package/dist/utils/audience.js +9 -0
  90. package/dist/utils/auth-client.js +35 -0
  91. package/dist/utils/authentication.js +60 -0
  92. package/dist/utils/boolean-flag.js +3 -0
  93. package/dist/utils/browser.js +6 -0
  94. package/dist/utils/build-step.js +104 -0
  95. package/dist/utils/bundle-files.js +80 -0
  96. package/dist/utils/bundle-resolution.js +46 -0
  97. package/dist/utils/channel-fields.js +130 -0
  98. package/dist/utils/cli.js +80 -0
  99. package/dist/utils/command-registry.js +79 -0
  100. package/dist/utils/command.js +4 -0
  101. package/dist/utils/completion/bash.js +48 -0
  102. package/dist/utils/completion/command-tree.js +72 -0
  103. package/dist/utils/completion/fish.js +44 -0
  104. package/dist/utils/completion/posix-case-arms.js +9 -0
  105. package/dist/utils/completion/posix-quote.js +4 -0
  106. package/dist/utils/completion/script-files.js +26 -0
  107. package/dist/utils/completion/zsh.js +72 -0
  108. package/dist/utils/compressed-files.js +31 -0
  109. package/dist/utils/credential.js +15 -0
  110. package/dist/utils/device-resolution.js +17 -0
  111. package/dist/utils/diagnostics.js +20 -0
  112. package/dist/utils/duration.js +16 -0
  113. package/dist/utils/environment.js +22 -0
  114. package/dist/utils/error-mapping.js +75 -0
  115. package/dist/utils/errors.js +266 -0
  116. package/dist/utils/fingerprint.js +76 -0
  117. package/dist/utils/flag-name.js +4 -0
  118. package/dist/utils/fleet.js +9 -0
  119. package/dist/utils/framework.js +74 -0
  120. package/dist/utils/frameworks/binary-create-step.js +70 -0
  121. package/dist/utils/frameworks/capacitor.js +150 -0
  122. package/dist/utils/frameworks/cordova.js +110 -0
  123. package/dist/utils/frameworks/expo.js +152 -0
  124. package/dist/utils/frameworks/index.js +13 -0
  125. package/dist/utils/frameworks/native-glue.js +10 -0
  126. package/dist/utils/frameworks/native-projects.js +27 -0
  127. package/dist/utils/frameworks/react-native-build.js +182 -0
  128. package/dist/utils/frameworks/react-native.js +127 -0
  129. package/dist/utils/frameworks/sdk-package.js +63 -0
  130. package/dist/utils/git-provenance.js +71 -0
  131. package/dist/utils/global-options.js +34 -0
  132. package/dist/utils/hosts.js +53 -0
  133. package/dist/utils/init-steps.js +51 -0
  134. package/dist/utils/json-file.js +28 -0
  135. package/dist/utils/login.js +153 -0
  136. package/dist/utils/member-resolution.js +49 -0
  137. package/dist/utils/native-project-edit.js +41 -0
  138. package/dist/utils/nullable-field.js +10 -0
  139. package/dist/utils/outcome.js +24 -0
  140. package/dist/utils/output.js +38 -0
  141. package/dist/utils/pack.js +20 -0
  142. package/dist/utils/package-json.js +5 -0
  143. package/dist/utils/package-manager.js +50 -0
  144. package/dist/utils/pagination.js +29 -0
  145. package/dist/utils/progress.js +23 -0
  146. package/dist/utils/progression.js +119 -0
  147. package/dist/utils/project-config.js +61 -0
  148. package/dist/utils/prompts.js +53 -0
  149. package/dist/utils/react-native-project.js +119 -0
  150. package/dist/utils/release-conditions.js +79 -0
  151. package/dist/utils/release-output.js +13 -0
  152. package/dist/utils/release-resolution.js +83 -0
  153. package/dist/utils/resource-file.js +81 -0
  154. package/dist/utils/resource-resolution.js +107 -0
  155. package/dist/utils/signing-private-key.js +105 -0
  156. package/dist/utils/spending-cap.js +16 -0
  157. package/dist/utils/telemetry.js +172 -0
  158. package/dist/utils/token-store.js +63 -0
  159. package/dist/utils/update-notice.js +68 -0
  160. package/dist/utils/upload.js +148 -0
  161. package/dist/utils/usage-month.js +14 -0
  162. package/dist/utils/user-config.js +54 -0
  163. package/dist/utils/webhook-resolution.js +47 -0
  164. package/dist/utils/xcode-project.js +181 -0
  165. package/package.json +11 -9
@@ -0,0 +1,84 @@
1
+ import { existsSync, rmSync, statSync } from 'node:fs';
2
+ import { dirname, resolve } from 'node:path';
3
+ import { generateSigningKeyPair } from '@hotcodepush/protocol';
4
+ import { z } from 'zod';
5
+ import { PROJECT_CONFIG_FILE_NAME, SIGNING_PRIVATE_KEY_FILE_NAME, } from '../../config/consts.js';
6
+ import { createApiClient } from '../../utils/api-client.js';
7
+ import { defineCommand } from '../../utils/command.js';
8
+ import { InvalidParameterError } from '../../utils/errors.js';
9
+ import { defineCommandOptions } from '../../utils/global-options.js';
10
+ import { printJson } from '../../utils/output.js';
11
+ import { locateProjectConfig, writeProjectConfig, } from '../../utils/project-config.js';
12
+ import { fetchAppId } from '../../utils/resource-resolution.js';
13
+ import { writeSigningPrivateKeyFile } from '../../utils/signing-private-key.js';
14
+ export const UPLOAD_SIGNING_TEXT = "Uploads sign with it through --private-key-path. In CI, set HOTCODEPUSH_SIGNING_KEY to the file's content.";
15
+ export default defineCommand({
16
+ description: 'Generate a key pair and add its public key to the app, the private key written to a new file on this machine: from then on the app releases only signed bundles.',
17
+ examples: [
18
+ 'hotcodepush signing-key create',
19
+ 'hotcodepush signing-key create --app "My App" --private-key-path my-app-private-key.pem --json',
20
+ ],
21
+ options: defineCommandOptions({
22
+ privateKeyPath: z
23
+ .string()
24
+ .optional()
25
+ .describe(`The file to write the private key to, which must not exist; ${SIGNING_PRIVATE_KEY_FILE_NAME} in the current directory by default.`),
26
+ }),
27
+ action: async (options) => {
28
+ const privateKeyPath = resolve(options.privateKeyPath ?? SIGNING_PRIVATE_KEY_FILE_NAME);
29
+ assertNewFilePath(privateKeyPath);
30
+ const hotCodePush = createApiClient();
31
+ const projectConfigLocation = locateProjectConfig(options.config);
32
+ const appId = await fetchAppId(hotCodePush, options, projectConfigLocation.projectConfig);
33
+ const { privateKey, publicKey } = await generateSigningKeyPair();
34
+ // Written before the public key is registered, since a registered key without its private key could sign nothing.
35
+ writeSigningPrivateKeyFile(privateKeyPath, privateKey);
36
+ const createdSigningKey = await hotCodePush.apps.signingKeys
37
+ .create({ appId, publicKey })
38
+ .catch((error) => {
39
+ // Removed when the registration fails, since the app would refuse what it signs.
40
+ rmSync(privateKeyPath);
41
+ throw error;
42
+ });
43
+ const isConfigured = addPublicKeyToProjectConfig(projectConfigLocation, appId, publicKey);
44
+ if (options.json) {
45
+ printJson({ ...createdSigningKey, privateKeyPath });
46
+ return;
47
+ }
48
+ console.log(`Created signing key ${createdSigningKey.keyId} (${createdSigningKey.id}).`);
49
+ console.log(resolvePublicKeyListingText(isConfigured, publicKey));
50
+ console.log(`Wrote the private key to ${privateKeyPath}.`);
51
+ console.log("Keep the file out of version control: whoever holds it can sign the app's bundles.");
52
+ console.log(UPLOAD_SIGNING_TEXT);
53
+ },
54
+ });
55
+ // Keys already listed stay, so the next builds keep trusting them.
56
+ // A file of another app, or no file, is left alone.
57
+ export function addPublicKeyToProjectConfig({ filePath, projectConfig }, appId, publicKey) {
58
+ if (filePath === undefined || projectConfig?.appId !== appId) {
59
+ return false;
60
+ }
61
+ if (projectConfig.publicKeys?.includes(publicKey)) {
62
+ return true;
63
+ }
64
+ writeProjectConfig(filePath, {
65
+ ...projectConfig,
66
+ publicKeys: [...(projectConfig.publicKeys ?? []), publicKey],
67
+ });
68
+ return true;
69
+ }
70
+ // An existing file may hold a key that a store build trusts, and no folder is made on the person's behalf.
71
+ function assertNewFilePath(filePath) {
72
+ if (existsSync(filePath)) {
73
+ throw new InvalidParameterError(`The file ${filePath} from --private-key-path already exists.`, 'Pass a path where no file is, since signing-key create never overwrites a private key.');
74
+ }
75
+ const directoryPath = dirname(filePath);
76
+ if (!statSync(directoryPath, { throwIfNoEntry: false })?.isDirectory()) {
77
+ throw new InvalidParameterError(`The folder ${directoryPath} from --private-key-path does not exist.`, 'Create the folder first, or pass a path in an existing one.');
78
+ }
79
+ }
80
+ export function resolvePublicKeyListingText(isConfigured, publicKey) {
81
+ return isConfigured
82
+ ? `Added to publicKeys in ${PROJECT_CONFIG_FILE_NAME}.`
83
+ : `Add it to publicKeys in the app's ${PROJECT_CONFIG_FILE_NAME}: ${publicKey}`;
84
+ }
@@ -0,0 +1,67 @@
1
+ import { z } from 'zod';
2
+ import { createApiClient } from '../../utils/api-client.js';
3
+ import { defineCommand } from '../../utils/command.js';
4
+ import { InvalidParameterError } from '../../utils/errors.js';
5
+ import { defineCommandOptions } from '../../utils/global-options.js';
6
+ import { printJson } from '../../utils/output.js';
7
+ import { locateProjectConfig, writeProjectConfig, } from '../../utils/project-config.js';
8
+ import { confirmConsequence, promptSelect } from '../../utils/prompts.js';
9
+ import { fetchAppId, fetchSigningKeys, } from '../../utils/resource-resolution.js';
10
+ export default defineCommand({
11
+ description: 'Unregister a signing key after the store release that stopped trusting it.',
12
+ examples: [
13
+ 'hotcodepush signing-key delete --signing-key sha256:9f2c4e1ab07d',
14
+ 'hotcodepush signing-key delete --signing-key 3b1f8e7a-5c2d-4f6b-9a0e-7d4c1b2a3f5e --yes --json',
15
+ ],
16
+ options: defineCommandOptions({
17
+ signingKey: z
18
+ .string()
19
+ .optional()
20
+ .describe('The signing key, by id or key id.'),
21
+ }),
22
+ action: async (options) => {
23
+ const hotCodePush = createApiClient();
24
+ const projectConfigLocation = locateProjectConfig(options.config);
25
+ const appId = await fetchAppId(hotCodePush, options, projectConfigLocation.projectConfig);
26
+ const signingKeys = await fetchSigningKeys(hotCodePush, appId);
27
+ const signingKey = resolveSigningKey(signingKeys, options.signingKey ??
28
+ (await promptSelect('--signing-key', 'Which signing key?', signingKeys.map(({ id, keyId }) => ({
29
+ label: keyId,
30
+ value: id,
31
+ })), options)));
32
+ const isConfirmed = await confirmConsequence(`unregisters signing key ${signingKey.keyId}: an upload signed with it is refused from then on`, options);
33
+ if (!isConfirmed) {
34
+ return;
35
+ }
36
+ await hotCodePush.apps.signingKeys.delete({
37
+ appId,
38
+ signingKeyId: signingKey.id,
39
+ });
40
+ removePublicKeyFromProjectConfig(projectConfigLocation, appId, signingKey.publicKey);
41
+ if (options.json) {
42
+ printJson({ id: signingKey.id, name: signingKey.keyId });
43
+ }
44
+ else {
45
+ console.log(`Deleted signing key ${signingKey.keyId} (${signingKey.id}).`);
46
+ }
47
+ },
48
+ });
49
+ // Removing the key from `publicKeys` makes the file and the app agree again.
50
+ function removePublicKeyFromProjectConfig({ filePath, projectConfig }, appId, publicKey) {
51
+ if (filePath === undefined ||
52
+ projectConfig?.appId !== appId ||
53
+ !projectConfig.publicKeys?.includes(publicKey)) {
54
+ return;
55
+ }
56
+ writeProjectConfig(filePath, {
57
+ ...projectConfig,
58
+ publicKeys: projectConfig.publicKeys.filter(listedPublicKey => listedPublicKey !== publicKey),
59
+ });
60
+ }
61
+ function resolveSigningKey(signingKeys, reference) {
62
+ const signingKey = signingKeys.find(({ id, keyId }) => id === reference || keyId === reference);
63
+ if (signingKey === undefined) {
64
+ throw new InvalidParameterError(`The app has no signing key ${reference} from --signing-key.`, 'Run "hotcodepush signing-key list" for the ids and key ids.');
65
+ }
66
+ return signingKey;
67
+ }
@@ -0,0 +1,38 @@
1
+ import { createApiClient } from '../../utils/api-client.js';
2
+ import { defineCommand } from '../../utils/command.js';
3
+ import { defineCommandOptions } from '../../utils/global-options.js';
4
+ import { printJson, printTable, resolveDate } from '../../utils/output.js';
5
+ import { paginationShape, resolveNextOffset } from '../../utils/pagination.js';
6
+ import { readProjectConfig } from '../../utils/project-config.js';
7
+ import { fetchAppId } from '../../utils/resource-resolution.js';
8
+ export default defineCommand({
9
+ description: "List the app's signing keys, newest first, with the key id a signature names.",
10
+ examples: [
11
+ 'hotcodepush signing-key list',
12
+ 'hotcodepush signing-key list --app "My App" --json',
13
+ ],
14
+ options: defineCommandOptions(paginationShape),
15
+ action: async (options) => {
16
+ const hotCodePush = createApiClient();
17
+ const listedSigningKeys = await hotCodePush.apps.signingKeys.list({
18
+ appId: await fetchAppId(hotCodePush, options, readProjectConfig(options.config)),
19
+ limit: options.limit,
20
+ offset: options.offset,
21
+ });
22
+ const nextOffset = resolveNextOffset(listedSigningKeys.length, options);
23
+ if (options.json) {
24
+ printJson({ nextOffset, signingKeys: listedSigningKeys });
25
+ return;
26
+ }
27
+ printTable({
28
+ emptyText: 'No signing keys. Run signing-key create to enable code signing.',
29
+ headers: ['ID', 'KEY ID', 'CREATED'],
30
+ nextOffset,
31
+ rows: listedSigningKeys.map(({ createdAt, id, keyId }) => [
32
+ id,
33
+ keyId,
34
+ resolveDate(createdAt),
35
+ ]),
36
+ });
37
+ },
38
+ });
@@ -0,0 +1,32 @@
1
+ import { createApiClient } from '../../utils/api-client.js';
2
+ import { defineCommand } from '../../utils/command.js';
3
+ import { defineCommandOptions } from '../../utils/global-options.js';
4
+ import { printJson } from '../../utils/output.js';
5
+ import { confirmConsequence } from '../../utils/prompts.js';
6
+ import { fetchOrganizationId } from '../../utils/resource-resolution.js';
7
+ export default defineCommand({
8
+ description: "Delete the organization's SSO provider: password sign-in works again for its members, and the domain is forgotten.",
9
+ examples: [
10
+ 'hotcodepush sso-provider delete',
11
+ 'hotcodepush sso-provider delete --organization Acme --yes --json',
12
+ ],
13
+ options: defineCommandOptions({}),
14
+ action: async (options) => {
15
+ const hotCodePush = createApiClient();
16
+ const organizationId = await fetchOrganizationId(hotCodePush, options);
17
+ const fetchedProvider = await hotCodePush.organizations.ssoProvider.get({
18
+ organizationId,
19
+ });
20
+ const isConfirmed = await confirmConsequence(`deletes the SSO provider for ${fetchedProvider.domain}: password sign-in works again for its members and the domain is forgotten`, options);
21
+ if (!isConfirmed) {
22
+ return;
23
+ }
24
+ await hotCodePush.organizations.ssoProvider.delete({ organizationId });
25
+ if (options.json) {
26
+ printJson({ id: fetchedProvider.id, name: fetchedProvider.domain });
27
+ }
28
+ else {
29
+ console.log(`Deleted the SSO provider for ${fetchedProvider.domain} (${fetchedProvider.id}).`);
30
+ }
31
+ },
32
+ });
@@ -0,0 +1,63 @@
1
+ import { createApiClient } from '../../utils/api-client.js';
2
+ import { defineCommand } from '../../utils/command.js';
3
+ import { defineCommandOptions } from '../../utils/global-options.js';
4
+ import { resolveConsoleBaseUrl } from '../../utils/hosts.js';
5
+ import { printDetails, printJson } from '../../utils/output.js';
6
+ import { fetchOrganizationId } from '../../utils/resource-resolution.js';
7
+ import { readApiUrl } from '../../utils/user-config.js';
8
+ export default defineCommand({
9
+ description: "Print the organization's SSO provider with its sign-in link and, until the domain is verified, the TXT record to set.",
10
+ examples: [
11
+ 'hotcodepush sso-provider get',
12
+ 'hotcodepush sso-provider get --organization Acme --json',
13
+ ],
14
+ options: defineCommandOptions({}),
15
+ action: async (options) => {
16
+ const hotCodePush = createApiClient();
17
+ const fetchedProvider = await hotCodePush.organizations.ssoProvider.get({
18
+ organizationId: await fetchOrganizationId(hotCodePush, options),
19
+ });
20
+ if (options.json) {
21
+ printJson(fetchedProvider);
22
+ return;
23
+ }
24
+ printProviderDetails(fetchedProvider);
25
+ },
26
+ });
27
+ export function printProviderDetails(provider) {
28
+ const apiUrl = readApiUrl();
29
+ printDetails([
30
+ ['ID', provider.id],
31
+ ['Type', provider.type],
32
+ ['Domain', provider.domain],
33
+ ['Verified', provider.domainVerified ? 'yes' : 'no'],
34
+ [
35
+ 'Sign-in URL',
36
+ `${resolveConsoleBaseUrl(apiUrl)}/login?sso=${provider.providerId}`,
37
+ ],
38
+ ...resolveServiceProviderDetails(provider, apiUrl),
39
+ ...resolveVerificationDetails(provider),
40
+ ]);
41
+ }
42
+ // The identity provider calls these URLs on the API origin.
43
+ function resolveServiceProviderDetails({ providerId, type }, apiUrl) {
44
+ const ssoUrl = `${apiUrl}/v1/auth/sso`;
45
+ return type === 'oidc'
46
+ ? [['Redirect URI', `${ssoUrl}/callback/${providerId}`]]
47
+ : [
48
+ ['ACS URL', `${ssoUrl}/saml2/sp/acs/${providerId}`],
49
+ [
50
+ 'SP metadata URL',
51
+ `${ssoUrl}/saml2/sp/metadata?providerId=${providerId}`,
52
+ ],
53
+ ];
54
+ }
55
+ // The record name is the provider name under the domain.
56
+ function resolveVerificationDetails({ domain, domainVerificationToken, domainVerified, providerId, }) {
57
+ return domainVerified || domainVerificationToken === null
58
+ ? []
59
+ : [
60
+ ['TXT name', `_hotcodepush-sso-${providerId}.${domain}`],
61
+ ['TXT value', domainVerificationToken],
62
+ ];
63
+ }
@@ -0,0 +1,161 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { z } from 'zod';
3
+ import { createApiClient } from '../../utils/api-client.js';
4
+ import { defineCommand } from '../../utils/command.js';
5
+ import { InvalidParameterError } from '../../utils/errors.js';
6
+ import { defineCommandOptions } from '../../utils/global-options.js';
7
+ import { printJson } from '../../utils/output.js';
8
+ import { promptSecret, promptSelect, promptText } from '../../utils/prompts.js';
9
+ import { fetchOrganizationId } from '../../utils/resource-resolution.js';
10
+ import { printProviderDetails } from './get.js';
11
+ // A missing protocol names the first flag of each protocol.
12
+ const PROVIDER_TYPE_FLAG = '--oidc-issuer or --saml-entry-point';
13
+ export default defineCommand({
14
+ description: "Set the organization's OIDC or SAML provider and its email domain, replacing the one before. A new domain starts unverified.",
15
+ examples: [
16
+ 'hotcodepush sso-provider set --domain example.com --oidc-issuer https://login.example.com --oidc-client-id hotcodepush --oidc-client-secret "$OIDC_CLIENT_SECRET"',
17
+ 'hotcodepush sso-provider set --domain example.com --saml-entry-point https://idp.example.com/sso --saml-issuer https://idp.example.com --saml-metadata-path idp-metadata.xml --json',
18
+ ],
19
+ options: defineCommandOptions({
20
+ domain: z
21
+ .string()
22
+ .optional()
23
+ .describe('The email domain whose addresses sign in through the provider, such as example.com.'),
24
+ oidcClientId: z
25
+ .string()
26
+ .optional()
27
+ .describe('OIDC: the client ID the identity provider issued.'),
28
+ oidcClientSecret: z
29
+ .string()
30
+ .optional()
31
+ .describe('OIDC: the client secret the identity provider issued.'),
32
+ oidcDiscoveryEndpoint: z
33
+ .string()
34
+ .optional()
35
+ .describe("OIDC: the discovery document's URL, the issuer's /.well-known/openid-configuration by default."),
36
+ oidcIssuer: z
37
+ .string()
38
+ .optional()
39
+ .describe("OIDC: the identity provider's issuer URL."),
40
+ oidcScopes: z
41
+ .string()
42
+ .transform(value => value.split(',').map(scope => scope.trim()))
43
+ .optional()
44
+ .describe('OIDC: the scopes to request, comma-separated.'),
45
+ samlCertPath: z
46
+ .string()
47
+ .optional()
48
+ .describe("SAML: the identity provider's signing certificate file, sent with --saml-entity-id when no metadata is."),
49
+ samlEntityId: z
50
+ .string()
51
+ .optional()
52
+ .describe("SAML: the identity provider's entity ID."),
53
+ samlEntryPoint: z
54
+ .string()
55
+ .optional()
56
+ .describe("SAML: the identity provider's sign-in URL."),
57
+ samlIssuer: z
58
+ .string()
59
+ .optional()
60
+ .describe("SAML: the identity provider's issuer."),
61
+ samlMetadataPath: z
62
+ .string()
63
+ .optional()
64
+ .describe("SAML: the identity provider's metadata XML file."),
65
+ }),
66
+ action: async (options) => {
67
+ const flaggedProviderType = resolveFlaggedProviderType(options);
68
+ const hotCodePush = createApiClient();
69
+ const organizationId = await fetchOrganizationId(hotCodePush, options);
70
+ const domain = options.domain ??
71
+ (await promptText('--domain', 'Which email domain signs in through the provider?', options));
72
+ const providerType = flaggedProviderType ??
73
+ (await promptSelect(PROVIDER_TYPE_FLAG, 'Which protocol does the identity provider speak?', [
74
+ { label: 'OIDC', value: 'oidc' },
75
+ { label: 'SAML', value: 'saml' },
76
+ ], options));
77
+ const updatedProvider = await hotCodePush.organizations.ssoProvider.set({
78
+ domain,
79
+ organizationId,
80
+ ...(providerType === 'oidc'
81
+ ? { oidc: await promptOidcBlock(options) }
82
+ : { saml: await promptSamlBlock(options) }),
83
+ });
84
+ if (options.json) {
85
+ printJson(updatedProvider);
86
+ return;
87
+ }
88
+ console.log(`Set the ${providerType} provider for ${updatedProvider.domain}.`);
89
+ printProviderDetails(updatedProvider);
90
+ },
91
+ });
92
+ async function promptOidcBlock(options) {
93
+ const issuer = options.oidcIssuer ??
94
+ (await promptText('--oidc-issuer', "What is the identity provider's issuer URL?", options));
95
+ const clientId = options.oidcClientId ??
96
+ (await promptText('--oidc-client-id', 'What is the client ID?', options));
97
+ const clientSecret = options.oidcClientSecret ??
98
+ (await promptSecret('--oidc-client-secret', 'What is the client secret?', options));
99
+ return {
100
+ clientId,
101
+ clientSecret,
102
+ discoveryEndpoint: options.oidcDiscoveryEndpoint,
103
+ issuer,
104
+ scopes: options.oidcScopes,
105
+ };
106
+ }
107
+ // The metadata and the certificate are sent as text and never parsed, since the API reads them.
108
+ async function promptSamlBlock(options) {
109
+ const entryPoint = options.samlEntryPoint ??
110
+ (await promptText('--saml-entry-point', "What is the identity provider's sign-in URL?", options));
111
+ const issuer = options.samlIssuer ??
112
+ (await promptText('--saml-issuer', "What is the identity provider's issuer?", options));
113
+ const hasCertificateOrEntityId = options.samlCertPath !== undefined || options.samlEntityId !== undefined;
114
+ const metadataPath = options.samlMetadataPath ??
115
+ (hasCertificateOrEntityId
116
+ ? undefined
117
+ : await promptText('--saml-metadata-path', "Which file holds the identity provider's SAML metadata?", options));
118
+ return {
119
+ cert: options.samlCertPath === undefined
120
+ ? undefined
121
+ : readDocumentFile('--saml-cert-path', options.samlCertPath),
122
+ entityId: options.samlEntityId,
123
+ entryPoint,
124
+ issuer,
125
+ metadata: metadataPath === undefined
126
+ ? undefined
127
+ : readDocumentFile('--saml-metadata-path', metadataPath),
128
+ };
129
+ }
130
+ function readDocumentFile(flag, filePath) {
131
+ try {
132
+ return readFileSync(filePath, 'utf8');
133
+ }
134
+ catch (error) {
135
+ throw new InvalidParameterError(`The file ${filePath} from ${flag} cannot be read.`, 'Pass the path of a file you can read.', error);
136
+ }
137
+ }
138
+ // A provider speaks one protocol, so flags of both are refused.
139
+ function resolveFlaggedProviderType(options) {
140
+ const hasOidcFlag = [
141
+ options.oidcClientId,
142
+ options.oidcClientSecret,
143
+ options.oidcDiscoveryEndpoint,
144
+ options.oidcIssuer,
145
+ options.oidcScopes,
146
+ ].some(value => value !== undefined);
147
+ const hasSamlFlag = [
148
+ options.samlCertPath,
149
+ options.samlEntityId,
150
+ options.samlEntryPoint,
151
+ options.samlIssuer,
152
+ options.samlMetadataPath,
153
+ ].some(value => value !== undefined);
154
+ if (hasOidcFlag && hasSamlFlag) {
155
+ throw new InvalidParameterError('The --oidc- and --saml- flags were passed together.', 'Pass the --oidc- flags for an OIDC provider or the --saml- flags for a SAML one, since a provider speaks one protocol.');
156
+ }
157
+ if (hasOidcFlag) {
158
+ return 'oidc';
159
+ }
160
+ return hasSamlFlag ? 'saml' : undefined;
161
+ }
@@ -0,0 +1,31 @@
1
+ import { createApiClient } from '../../utils/api-client.js';
2
+ import { defineCommand } from '../../utils/command.js';
3
+ import { defineCommandOptions } from '../../utils/global-options.js';
4
+ import { printJson } from '../../utils/output.js';
5
+ import { fetchOrganizationId } from '../../utils/resource-resolution.js';
6
+ import { printProviderDetails } from './get.js';
7
+ export default defineCommand({
8
+ description: "Look up the domain's TXT record and mark the domain verified when it is there.",
9
+ examples: [
10
+ 'hotcodepush sso-provider verify',
11
+ 'hotcodepush sso-provider verify --organization Acme --json',
12
+ ],
13
+ options: defineCommandOptions({}),
14
+ action: async (options) => {
15
+ const hotCodePush = createApiClient();
16
+ const checkedProvider = await hotCodePush.organizations.ssoProvider.verifications.create({
17
+ organizationId: await fetchOrganizationId(hotCodePush, options),
18
+ });
19
+ if (options.json) {
20
+ printJson(checkedProvider);
21
+ return;
22
+ }
23
+ console.log(resolveVerificationText(checkedProvider));
24
+ printProviderDetails(checkedProvider);
25
+ },
26
+ });
27
+ function resolveVerificationText({ domain, domainVerified, }) {
28
+ return domainVerified
29
+ ? `Verified ${domain}.`
30
+ : `No TXT record verifies ${domain} yet. A new record can take a while to appear.`;
31
+ }
@@ -0,0 +1,189 @@
1
+ import { z } from 'zod';
2
+ import { createApiClient } from '../../utils/api-client.js';
3
+ import { defineCommand } from '../../utils/command.js';
4
+ import { InvalidParameterError } from '../../utils/errors.js';
5
+ import { defineCommandOptions } from '../../utils/global-options.js';
6
+ import { printDetails, printJson, printTable } from '../../utils/output.js';
7
+ import { resolveByteText } from '../../utils/progress.js';
8
+ import { readProjectConfig } from '../../utils/project-config.js';
9
+ import { promptSelect } from '../../utils/prompts.js';
10
+ import { fetchAppId, fetchChannels, fetchResourceId, } from '../../utils/resource-resolution.js';
11
+ const STATISTICS_TYPES = ['fleet', 'updates', 'usage'];
12
+ // Buckets of these dimensions hold only a value and its devices. Release and binary buckets carry more.
13
+ const FLEET_DIMENSIONS = [
14
+ ['BINARY VERSION', 'binaryVersion'],
15
+ ['SDK VERSION', 'sdkVersion'],
16
+ ['PLATFORM', 'platform'],
17
+ ['OS VERSION', 'osVersion'],
18
+ ['COUNTRY', 'country'],
19
+ ];
20
+ export default defineCommand({
21
+ description: "Print one of the app's statistics: the fleet of devices seen in 30 days, the updates per day, or the usage the bill counts.",
22
+ examples: [
23
+ 'hotcodepush statistics get --type fleet --channel production',
24
+ 'hotcodepush statistics get --type updates --period-since 2026-09-01 --period-until 2026-09-30 --json',
25
+ ],
26
+ options: defineCommandOptions({
27
+ channel: z
28
+ .string()
29
+ .optional()
30
+ .describe('The channel, by id or name, to narrow the fleet or the updates to. Defaults to the whole app.'),
31
+ periodSince: z
32
+ .string()
33
+ .optional()
34
+ .describe('The first UTC day of the updates or the usage, YYYY-MM-DD, inclusive; 30 days before --period-until by default.'),
35
+ periodUntil: z
36
+ .string()
37
+ .optional()
38
+ .describe('The last UTC day of the updates or the usage, YYYY-MM-DD, inclusive. Defaults to today.'),
39
+ type: z
40
+ .enum(STATISTICS_TYPES)
41
+ .optional()
42
+ .describe('The statistics to print. fleet is the devices seen in 30 days, updates is the updates per day, and usage is the monthly active devices and the traffic.'),
43
+ }),
44
+ action: async (options) => {
45
+ const hotCodePush = createApiClient();
46
+ const type = options.type ??
47
+ (await promptSelect('--type', 'Which statistics?', STATISTICS_TYPES.map(value => ({ label: value, value })), options));
48
+ assertFlagsOfType(type, options);
49
+ const appId = await fetchAppId(hotCodePush, options, readProjectConfig(options.config));
50
+ const query = {
51
+ appId,
52
+ channelId: options.channel === undefined
53
+ ? undefined
54
+ : await fetchResourceId('channel', options.channel, () => fetchChannels(hotCodePush, appId)),
55
+ periodSince: options.periodSince,
56
+ periodUntil: options.periodUntil,
57
+ };
58
+ switch (type) {
59
+ case 'fleet':
60
+ return printFleetStatistics(await hotCodePush.apps.statistics.fleet.get({
61
+ appId,
62
+ channelId: query.channelId,
63
+ }), options.json);
64
+ case 'updates':
65
+ return printUpdateStatistics(await hotCodePush.apps.statistics.updates.get(query), options.json);
66
+ case 'usage':
67
+ return printUsageStatistics(await hotCodePush.apps.statistics.usage.get({
68
+ appId,
69
+ periodSince: query.periodSince,
70
+ periodUntil: query.periodUntil,
71
+ }), options.json);
72
+ }
73
+ },
74
+ });
75
+ // The fleet is a snapshot without a time axis, and the usage is the whole bill.
76
+ // So each refuses the flag it has no field for.
77
+ function assertFlagsOfType(type, options) {
78
+ if (type === 'fleet' &&
79
+ (options.periodSince !== undefined || options.periodUntil !== undefined)) {
80
+ throw new InvalidParameterError('The fleet is a snapshot of the last 30 days without a period, so --period-since and --period-until do not apply.');
81
+ }
82
+ if (type === 'usage' && options.channel !== undefined) {
83
+ throw new InvalidParameterError('The usage counts the whole app, never one channel, so --channel does not apply.');
84
+ }
85
+ }
86
+ function printFleetStatistics(statistics, isJson) {
87
+ if (isJson) {
88
+ printJson(statistics);
89
+ return;
90
+ }
91
+ printDetails([
92
+ ['Active devices', String(statistics.deviceCount)],
93
+ ['Current devices', String(statistics.currentDeviceCount)],
94
+ ]);
95
+ printFleetTable(['RELEASE', 'DEVICES'], statistics.release.map(({ deviceCount, number }) => [
96
+ number === null ? 'embedded' : `#${number}`,
97
+ String(deviceCount),
98
+ ]));
99
+ printFleetTable(['BINARY', 'VERSION', 'PLATFORM', 'DEVICES'], statistics.binary.map(({ deviceCount, platform, value, version }) => [
100
+ value ?? 'none',
101
+ version ?? 'none',
102
+ platform ?? 'none',
103
+ String(deviceCount),
104
+ ]));
105
+ for (const [header, dimension] of FLEET_DIMENSIONS) {
106
+ printFleetTable([header, 'DEVICES'], statistics[dimension].map(({ deviceCount, value }) => [
107
+ value ?? 'none',
108
+ String(deviceCount),
109
+ ]));
110
+ }
111
+ }
112
+ function printFleetTable(headers, rows) {
113
+ printTable({
114
+ emptyText: `${headers[0]}: no device seen in the last 30 days.`,
115
+ headers,
116
+ nextOffset: null,
117
+ rows,
118
+ });
119
+ }
120
+ function printUpdateStatistics(statistics, isJson) {
121
+ if (isJson) {
122
+ printJson(statistics);
123
+ return;
124
+ }
125
+ printTable({
126
+ emptyText: 'No days in the period.',
127
+ headers: ['DAY', 'CONFIRMED', 'FAILED', 'ROLLED BACK'],
128
+ nextOffset: null,
129
+ rows: statistics.days.map(({ confirmedUpdateCount, day, failedUpdateCount, rolledBackUpdateCount, }) => [
130
+ day,
131
+ String(confirmedUpdateCount),
132
+ String(failedUpdateCount),
133
+ String(rolledBackUpdateCount),
134
+ ]),
135
+ });
136
+ printTable({
137
+ emptyText: 'No release went live recently.',
138
+ headers: ['RELEASE', 'LIVE', 'CONFIRMED', '50% AT', '90% AT'],
139
+ nextOffset: null,
140
+ rows: statistics.releases.map(({ adoption, liveAt, number, timeToAdoption }) => [
141
+ `#${number}`,
142
+ liveAt,
143
+ String(adoption.at(-1)?.confirmedUpdateCount ?? 0),
144
+ timeToAdoption.percent50At ?? 'not yet',
145
+ timeToAdoption.percent90At ?? 'not yet',
146
+ ]),
147
+ });
148
+ for (const [header, reasons] of [
149
+ ['FAILURE REASON', statistics.failureReasons],
150
+ ['SKIP REASON', statistics.skippedReasons],
151
+ ]) {
152
+ printTable({
153
+ emptyText: `${header}: none in the period.`,
154
+ headers: [header, 'EVENTS'],
155
+ nextOffset: null,
156
+ rows: reasons.map(({ eventCount, reason }) => [
157
+ reason,
158
+ String(eventCount),
159
+ ]),
160
+ });
161
+ }
162
+ }
163
+ function printUsageStatistics(statistics, isJson) {
164
+ if (isJson) {
165
+ printJson(statistics);
166
+ return;
167
+ }
168
+ printTable({
169
+ emptyText: 'No months in the period.',
170
+ headers: ['MONTH', 'MAU', 'DATA TRANSFER'],
171
+ nextOffset: null,
172
+ rows: statistics.months.map(({ dataTransferBytes, month, monthlyActiveDeviceCount }) => [
173
+ month.slice(0, 7),
174
+ String(monthlyActiveDeviceCount),
175
+ resolveByteText(dataTransferBytes),
176
+ ]),
177
+ });
178
+ printTable({
179
+ emptyText: 'No days in the period.',
180
+ headers: ['DAY', 'CHECKS', 'DATA TRANSFER', 'DEVICE DOWNLOADS'],
181
+ nextOffset: null,
182
+ rows: statistics.days.map(({ checkCount, dataTransferBytes, day, deviceDownloadBytes }) => [
183
+ day,
184
+ String(checkCount),
185
+ resolveByteText(dataTransferBytes),
186
+ resolveByteText(deviceDownloadBytes),
187
+ ]),
188
+ });
189
+ }