1 | "use strict";
|
2 | Object.defineProperty(exports, "__esModule", { value: true });
|
3 | exports.validate = exports.validateStorageGroup = exports.validateDefaultLanguage = exports.validateAppUUID = exports.validateSupportedLocales = exports.validateLocaleDisplayNames = exports.validateLocaleDisplayName = exports.validateBuildTarget = exports.validateRequestedPermissions = exports.validateWipeColor = exports.validateProjectDisplayName = exports.validateDisplayName = exports.validateAppType = exports.normalizeProjectConfig = exports.normalizeLocales = exports.getAllPermissionTypes = exports.Permission = exports.Locales = exports.MAX_DISPLAY_NAME_LENGTH = exports.MAX_LENGTH_APP_CLUSTER_ID = exports.VALID_APP_TYPES = exports.AppType = void 0;
|
4 | const tslib_1 = require("tslib");
|
5 | const humanize_list_1 = tslib_1.__importDefault(require("humanize-list"));
|
6 | const validator_1 = tslib_1.__importDefault(require("validator"));
|
7 | const lodash_1 = tslib_1.__importDefault(require("lodash"));
|
8 | const semver_1 = tslib_1.__importDefault(require("semver"));
|
9 | const buildTargets_1 = tslib_1.__importDefault(require("./buildTargets"));
|
10 | const DiagnosticList_1 = tslib_1.__importDefault(require("./DiagnosticList"));
|
11 | const languageTag_1 = require("./languageTag");
|
12 | const sdkVersion_1 = tslib_1.__importDefault(require("./sdkVersion"));
|
13 | const knownBuildTargets = Object.keys(buildTargets_1.default);
|
14 | var AppType;
|
15 | (function (AppType) {
|
16 | AppType["APP"] = "app";
|
17 | AppType["CLOCKFACE"] = "clockface";
|
18 | })(AppType = exports.AppType || (exports.AppType = {}));
|
19 | exports.VALID_APP_TYPES = Object.values(AppType);
|
20 | exports.MAX_LENGTH_APP_CLUSTER_ID = 64;
|
21 | exports.MAX_DISPLAY_NAME_LENGTH = 30;
|
22 | var Locales;
|
23 | (function (Locales) {
|
24 | Locales["en-US"] = "English (US)";
|
25 | Locales["de-DE"] = "German";
|
26 | Locales["es-ES"] = "Spanish";
|
27 | Locales["fr-FR"] = "French";
|
28 | Locales["it-IT"] = "Italian";
|
29 | Locales["ja-JP"] = "Japanese";
|
30 | Locales["ko-KR"] = "Korean";
|
31 | Locales["nl-NL"] = "Dutch";
|
32 | Locales["sv-SE"] = "Swedish";
|
33 | Locales["zh-CN"] = "Chinese (Simplified)";
|
34 | Locales["zh-TW"] = "Chinese (Traditional)";
|
35 | Locales["pt-BR"] = "Portuguese (Brazillian)";
|
36 | Locales["id-ID"] = "Indonesian (Bahasa)";
|
37 | Locales["ro-RO"] = "Romanian";
|
38 | Locales["ru-RU"] = "Russian";
|
39 | Locales["pl-PL"] = "Polish";
|
40 | Locales["cs-CZ"] = "Czech";
|
41 | })(Locales = exports.Locales || (exports.Locales = {}));
|
42 | const languageTags = Object.keys(Locales);
|
43 | var Permission;
|
44 | (function (Permission) {
|
45 | Permission["ACCESS_ACTIVITY"] = "access_activity";
|
46 | Permission["ACCESS_AOD"] = "access_aod";
|
47 | Permission["ACCESS_APP_CLUSTER_STORAGE"] = "access_app_cluster_storage";
|
48 | Permission["ACCESS_CALENDAR"] = "access_calendar";
|
49 | Permission["ACCESS_EXERCISE"] = "access_exercise";
|
50 | Permission["ACCESS_HEART_RATE"] = "access_heart_rate";
|
51 | Permission["ACCESS_INTERNET"] = "access_internet";
|
52 | Permission["ACCESS_LOCATION"] = "access_location";
|
53 | Permission["ACCESS_SECURE_EXCHANGE"] = "access_secure_exchange";
|
54 | Permission["ACCESS_USER_PROFILE"] = "access_user_profile";
|
55 | Permission["FITBIT_TOKEN"] = "fitbit_token";
|
56 | Permission["RUN_BACKGROUND"] = "run_background";
|
57 | Permission["EXTERNAL_APP_COMMUNICATION"] = "external_app_communication";
|
58 | Permission["MOBILE_NOTIFICATIONS"] = "mobile_notifications";
|
59 | })(Permission = exports.Permission || (exports.Permission = {}));
|
60 | const permissionTypes = [
|
61 | {
|
62 | key: Permission.ACCESS_ACTIVITY,
|
63 | name: 'Activity',
|
64 | description: 'Read user activities for today (distance, calories, steps, elevation and active minutes), and daily goals',
|
65 | },
|
66 | {
|
67 | key: Permission.ACCESS_USER_PROFILE,
|
68 | name: 'User Profile',
|
69 | description: 'Read non-identifiable personal information (gender, age, height, weight, resting HR, basal metabolic rate, stride, HR zones)',
|
70 | },
|
71 | {
|
72 | key: Permission.ACCESS_HEART_RATE,
|
73 | name: 'Heart Rate',
|
74 | description: 'Application may read the heart-rate sensor in real-time',
|
75 | },
|
76 | {
|
77 | key: Permission.ACCESS_LOCATION,
|
78 | name: 'Location',
|
79 | description: 'Application and companion may use GPS',
|
80 | },
|
81 | {
|
82 | key: Permission.ACCESS_INTERNET,
|
83 | name: 'Internet',
|
84 | description: 'Companion may communicate with the Internet using your phone data connection',
|
85 | },
|
86 | {
|
87 | key: Permission.RUN_BACKGROUND,
|
88 | name: 'Run in background',
|
89 | description: 'Companion may run even when the application is not actively in use',
|
90 | },
|
91 | {
|
92 | key: Permission.ACCESS_EXERCISE,
|
93 | name: 'Exercise Tracking',
|
94 | description: 'Application may track an exercise',
|
95 | sdkVersion: '>=3.0.0',
|
96 | },
|
97 | {
|
98 | key: Permission.ACCESS_APP_CLUSTER_STORAGE,
|
99 | name: 'App Cluster Storage',
|
100 | description: 'Application may access storage shared by other applications from the same developer',
|
101 | sdkVersion: '>=4.0.0',
|
102 | },
|
103 | {
|
104 | key: Permission.ACCESS_CALENDAR,
|
105 | name: 'Calendars',
|
106 | description: 'Application may access calendar data stored on the mobile device',
|
107 | sdkVersion: '>=4.1.0',
|
108 | },
|
109 | ];
|
110 | const restrictedPermissionTypes = [
|
111 | {
|
112 | key: Permission.FITBIT_TOKEN,
|
113 | name: '[Restricted] Fitbit Token',
|
114 | description: 'Access Fitbit API token',
|
115 | },
|
116 | {
|
117 | key: Permission.EXTERNAL_APP_COMMUNICATION,
|
118 | name: '[Restricted] External Application Communication',
|
119 | description: 'Allows communication between external mobile applications and companion',
|
120 | },
|
121 | {
|
122 | key: Permission.ACCESS_SECURE_EXCHANGE,
|
123 | name: '[Restricted] Secure Exchange',
|
124 | description: 'Allows securing any data and verifying that data was secured',
|
125 | },
|
126 | {
|
127 | key: Permission.ACCESS_AOD,
|
128 | name: '[Restricted] Always-on Display',
|
129 | description: 'Application may stay active whilst always-on display mode is active',
|
130 | sdkVersion: '>=4.1.0',
|
131 | },
|
132 | {
|
133 | key: Permission.MOBILE_NOTIFICATIONS,
|
134 | name: '[Restricted] Mobile Notifications',
|
135 | description: 'Application may display notifications on the mobile device',
|
136 | sdkVersion: '>=4.1.0',
|
137 | },
|
138 | ];
|
139 | function getAllPermissionTypes(options) {
|
140 | const { enableProposedAPI, includeRestrictedPermissions } = Object.assign({ enableProposedAPI: false, includeRestrictedPermissions: true }, options);
|
141 | return [
|
142 | ...permissionTypes,
|
143 | ...(includeRestrictedPermissions ? restrictedPermissionTypes : []),
|
144 | ].filter((permission) => !permission.sdkVersion ||
|
145 | semver_1.default.satisfies(sdkVersion_1.default(), permission.sdkVersion) ||
|
146 | enableProposedAPI);
|
147 | }
|
148 | exports.getAllPermissionTypes = getAllPermissionTypes;
|
149 | function constrainedSetDiagnostics({ actualValues, knownValues, valueTypeNoun, notFoundIsFatal = false, }) {
|
150 | const unknownValues = lodash_1.default.without(actualValues, ...knownValues);
|
151 | const diagnostics = new DiagnosticList_1.default();
|
152 | if (unknownValues.length > 0) {
|
153 | const unknownValueStrings = unknownValues.filter(lodash_1.default.isString);
|
154 | const unknownValueOther = lodash_1.default
|
155 | .without(unknownValues, ...unknownValueStrings)
|
156 | .map(String);
|
157 | if (unknownValueStrings.length) {
|
158 | const errStr = `One or more ${valueTypeNoun} was invalid: ${unknownValueStrings.join(', ')}`;
|
159 | if (notFoundIsFatal)
|
160 | diagnostics.pushFatalError(errStr);
|
161 | else
|
162 | diagnostics.pushWarning(errStr);
|
163 | }
|
164 | if (unknownValueOther.length) {
|
165 | diagnostics.pushFatalError(`One or more ${valueTypeNoun} was not a string: ${unknownValueOther.join(', ')}`);
|
166 | }
|
167 | }
|
168 | const duplicatedValues = lodash_1.default
|
169 | .uniq(actualValues)
|
170 | .filter((value) => actualValues.indexOf(value) !== actualValues.lastIndexOf(value));
|
171 | if (duplicatedValues.length > 0) {
|
172 | diagnostics.pushWarning(`One or more ${valueTypeNoun} was specified multiple times: ${duplicatedValues.join(', ')}`);
|
173 | }
|
174 | return diagnostics;
|
175 | }
|
176 | function normalizeLanguageTag(languageTag) {
|
177 | const match = /^([a-z]{2})(-[a-z]{2})?$/i.exec(languageTag);
|
178 | if (match === null)
|
179 | return languageTag;
|
180 | const [, language, region] = match;
|
181 | return language.toLowerCase() + (region || '').toUpperCase();
|
182 | }
|
183 | function normalizeLocales(locales) {
|
184 | const localeMapping = lodash_1.default.mapKeys(Object.keys(Locales), (tag) => tag.split('-')[0]);
|
185 | const normalizedLocales = {};
|
186 | for (const [locale, localeConfig] of Object.entries(locales)) {
|
187 | const mappedLocale = localeMapping[locale];
|
188 | if (mappedLocale === undefined) {
|
189 | normalizedLocales[normalizeLanguageTag(locale)] = localeConfig;
|
190 | continue;
|
191 | }
|
192 | if (locales[mappedLocale] !== undefined) {
|
193 | continue;
|
194 | }
|
195 | normalizedLocales[mappedLocale] = localeConfig;
|
196 | }
|
197 | return normalizedLocales;
|
198 | }
|
199 | exports.normalizeLocales = normalizeLocales;
|
200 | function normalizeProjectConfig(config, defaults) {
|
201 | if (!lodash_1.default.isPlainObject(config)) {
|
202 | throw new TypeError('Project configuration root must be an object');
|
203 | }
|
204 | const mergedConfig = Object.assign(Object.assign({ appUUID: '', appType: AppType.APP, appDisplayName: '', iconFile: 'resources/icon.png', wipeColor: '', requestedPermissions: [], buildTargets: [], i18n: {}, defaultLanguage: 'en-US' }, defaults), config.fitbit);
|
205 | const { requestedPermissions } = mergedConfig;
|
206 | if (!Array.isArray(requestedPermissions)) {
|
207 | throw new TypeError(`fitbit.requestedPermissions must be an array, not ${typeof requestedPermissions}`);
|
208 | }
|
209 | mergedConfig.i18n = normalizeLocales(mergedConfig.i18n);
|
210 | return mergedConfig;
|
211 | }
|
212 | exports.normalizeProjectConfig = normalizeProjectConfig;
|
213 | function validateAppType(config) {
|
214 | const diagnostics = new DiagnosticList_1.default();
|
215 | if (exports.VALID_APP_TYPES.indexOf(config.appType) === -1) {
|
216 | const appTypeNames = humanize_list_1.default(exports.VALID_APP_TYPES, { conjunction: 'or' });
|
217 | diagnostics.pushFatalError(`App type '${config.appType}' is invalid, expected ${appTypeNames}`);
|
218 | }
|
219 | return diagnostics;
|
220 | }
|
221 | exports.validateAppType = validateAppType;
|
222 | function validateDisplayName(name) {
|
223 | if (name.length === 0) {
|
224 | return 'Display name must not be blank';
|
225 | }
|
226 | if (name.length > exports.MAX_DISPLAY_NAME_LENGTH) {
|
227 | return `Display name must not exceed ${exports.MAX_DISPLAY_NAME_LENGTH} characters`;
|
228 | }
|
229 | return true;
|
230 | }
|
231 | exports.validateDisplayName = validateDisplayName;
|
232 | function validateProjectDisplayName(config) {
|
233 | const diagnostics = new DiagnosticList_1.default();
|
234 | const result = validateDisplayName(config.appDisplayName);
|
235 | if (result !== true) {
|
236 | diagnostics.pushFatalError(result);
|
237 | }
|
238 | return diagnostics;
|
239 | }
|
240 | exports.validateProjectDisplayName = validateProjectDisplayName;
|
241 | function validateWipeColor(config) {
|
242 | const diagnostics = new DiagnosticList_1.default();
|
243 | if (config.appType !== AppType.CLOCKFACE &&
|
244 | !validator_1.default.isHexColor(config.wipeColor)) {
|
245 | diagnostics.pushFatalError('Wipe color must be a valid hex color');
|
246 | }
|
247 | return diagnostics;
|
248 | }
|
249 | exports.validateWipeColor = validateWipeColor;
|
250 | function validateRequestedPermissions({ enableProposedAPI, requestedPermissions, }) {
|
251 | return constrainedSetDiagnostics({
|
252 | actualValues: requestedPermissions,
|
253 | knownValues: getAllPermissionTypes({
|
254 | enableProposedAPI: !!enableProposedAPI,
|
255 | }).map((permission) => permission.key),
|
256 | valueTypeNoun: 'requested permissions',
|
257 | notFoundIsFatal: false,
|
258 | });
|
259 | }
|
260 | exports.validateRequestedPermissions = validateRequestedPermissions;
|
261 | function validateBuildTarget({ buildTargets }, { hasNativeComponents }) {
|
262 | const diagnostics = constrainedSetDiagnostics({
|
263 | actualValues: buildTargets,
|
264 | knownValues: knownBuildTargets,
|
265 | valueTypeNoun: 'build targets',
|
266 | notFoundIsFatal: true,
|
267 | });
|
268 | if ((buildTargets === undefined || buildTargets.length === 0) &&
|
269 | !hasNativeComponents) {
|
270 | diagnostics.pushFatalError('At least one build target must be enabled');
|
271 | }
|
272 | return diagnostics;
|
273 | }
|
274 | exports.validateBuildTarget = validateBuildTarget;
|
275 | function validateLocaleDisplayName({ i18n }, localeKey) {
|
276 | const diagnostics = new DiagnosticList_1.default();
|
277 | const locale = i18n[localeKey];
|
278 | if (!locale)
|
279 | return diagnostics;
|
280 | if (!locale.name || locale.name.length === 0) {
|
281 | diagnostics.pushFatalError(`Localized display name for ${Locales[localeKey]} must not be blank`);
|
282 | }
|
283 | if (locale.name.length > exports.MAX_DISPLAY_NAME_LENGTH) {
|
284 | diagnostics.pushFatalError(`Localized display name for ${Locales[localeKey]} must not exceed ${exports.MAX_DISPLAY_NAME_LENGTH} characters`);
|
285 | }
|
286 | return diagnostics;
|
287 | }
|
288 | exports.validateLocaleDisplayName = validateLocaleDisplayName;
|
289 | function validateLocaleDisplayNames(config) {
|
290 | const diagnostics = new DiagnosticList_1.default();
|
291 | for (const localeKey of Object.keys(Locales)) {
|
292 | diagnostics.extend(validateLocaleDisplayName(config, localeKey));
|
293 | }
|
294 | return diagnostics;
|
295 | }
|
296 | exports.validateLocaleDisplayNames = validateLocaleDisplayNames;
|
297 | function validateSupportedLocales({ i18n }) {
|
298 | const diagnostics = new DiagnosticList_1.default();
|
299 | const unknownLocales = lodash_1.default.without(Object.keys(i18n), ...Object.keys(Locales));
|
300 | if (unknownLocales.length > 0) {
|
301 | diagnostics.pushWarning(`Invalid locales: ${unknownLocales.join(', ')}`);
|
302 | }
|
303 | return diagnostics;
|
304 | }
|
305 | exports.validateSupportedLocales = validateSupportedLocales;
|
306 | function validateAppUUID({ appUUID }) {
|
307 | const diagnostics = new DiagnosticList_1.default();
|
308 | if (!validator_1.default.isUUID(String(appUUID))) {
|
309 | diagnostics.pushFatalError('appUUID must be a valid UUID, run "npx fitbit-build generate-appid" to fix');
|
310 | }
|
311 | return diagnostics;
|
312 | }
|
313 | exports.validateAppUUID = validateAppUUID;
|
314 | function validateDefaultLanguage(config) {
|
315 | const diagnostics = new DiagnosticList_1.default();
|
316 | if (!languageTag_1.validateLanguageTag(config.defaultLanguage)) {
|
317 | diagnostics.pushFatalError(`Default language is an invalid language tag: ${config.defaultLanguage}. Must be ${humanize_list_1.default(languageTags, { conjunction: 'or' })}.`);
|
318 | }
|
319 | return diagnostics;
|
320 | }
|
321 | exports.validateDefaultLanguage = validateDefaultLanguage;
|
322 | function validateStorageGroup(config) {
|
323 | const diagnostics = new DiagnosticList_1.default();
|
324 | const hasRequestedPermission = getAllPermissionTypes({
|
325 | enableProposedAPI: !!config.enableProposedAPI,
|
326 | })
|
327 | .map((permission) => permission.key)
|
328 | .filter((permission) => (config.requestedPermissions || []).includes(permission))
|
329 | .includes(Permission.ACCESS_APP_CLUSTER_STORAGE);
|
330 | if (hasRequestedPermission) {
|
331 | if (config.appClusterID === undefined) {
|
332 | diagnostics.pushFatalError('App Cluster ID must be set when the App Cluster Storage permission is requested');
|
333 | }
|
334 | else if (config.appClusterID.length < 1 ||
|
335 | config.appClusterID.length > exports.MAX_LENGTH_APP_CLUSTER_ID) {
|
336 | diagnostics.pushFatalError('App Cluster ID must be between 1-64 characters');
|
337 | }
|
338 | else if (!/^([a-z0-9]+)(\.[a-z0-9]+)*$/.test(config.appClusterID)) {
|
339 | diagnostics.pushFatalError('App Cluster ID may only contain alphanumeric characters separated by periods, eg: my.app.123');
|
340 | }
|
341 | if (config.developerID === undefined) {
|
342 | diagnostics.pushFatalError('Developer ID must be set when the App Cluster Storage permission is requested');
|
343 | }
|
344 | else if (!validator_1.default.isUUID(String(config.developerID))) {
|
345 | diagnostics.pushFatalError('Developer ID must be a valid UUID');
|
346 | }
|
347 | }
|
348 | else if (config.appClusterID !== undefined ||
|
349 | config.developerID !== undefined) {
|
350 | diagnostics.pushFatalError('App Cluster Storage permission must be requested to set App Cluster ID and Developer ID fields');
|
351 | }
|
352 | return diagnostics;
|
353 | }
|
354 | exports.validateStorageGroup = validateStorageGroup;
|
355 | function validate(config, options) {
|
356 | const { hasNativeComponents } = Object.assign({ hasNativeComponents: false }, options);
|
357 | const diagnostics = new DiagnosticList_1.default();
|
358 | [
|
359 | validateAppUUID,
|
360 | validateProjectDisplayName,
|
361 | validateAppType,
|
362 | validateWipeColor,
|
363 | validateRequestedPermissions,
|
364 | validateSupportedLocales,
|
365 | validateLocaleDisplayNames,
|
366 | validateDefaultLanguage,
|
367 | validateStorageGroup,
|
368 | ].forEach((validator) => diagnostics.extend(validator(config)));
|
369 | diagnostics.extend(validateBuildTarget(config, { hasNativeComponents }));
|
370 | return diagnostics;
|
371 | }
|
372 | exports.validate = validate;
|