Merge pull request #14547 from mozilla/FXA-298-shared-js-to-ts

chore(typescript): convert more of fxa-shared to TS
This commit is contained in:
Barry Chen 2022-12-01 11:38:23 -06:00 коммит произвёл GitHub
Родитель 1329ab7c72 95cded6e96
Коммит d81e6cb196
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
45 изменённых файлов: 491 добавлений и 276 удалений

Просмотреть файл

@ -52,7 +52,7 @@
} }
}, },
"lint-staged": { "lint-staged": {
"*.js": [ "packages/!(fxa-payments-server)/**/*.js": [
"prettier --config _dev/.prettierrc --write", "prettier --config _dev/.prettierrc --write",
"eslint" "eslint"
], ],

Просмотреть файл

@ -8,7 +8,7 @@ import path from 'path';
import mysql from 'mysql'; import mysql from 'mysql';
import patcher from 'mysql-patcher'; import patcher from 'mysql-patcher';
import convict from 'convict'; import convict from 'convict';
import { makeMySQLConfig } from 'fxa-shared/db/config.js'; import { makeMySQLConfig } from 'fxa-shared/db/config';
const patch = promisify(patcher.patch); const patch = promisify(patcher.patch);
const conf = convict({ const conf = convict({

Просмотреть файл

@ -15,7 +15,8 @@
const { Container } = require('typedi'); const { Container } = require('typedi');
const { StatsD } = require('hot-shots'); const { StatsD } = require('hot-shots');
const { GROUPS, initialize } = require('fxa-shared/metrics/amplitude'); const { GROUPS, initialize } =
require('fxa-shared/metrics/amplitude').amplitude;
const { version: VERSION } = require('../../package.json'); const { version: VERSION } = require('../../package.json');
// Maps template name to email type // Maps template name to email type

Просмотреть файл

@ -1175,7 +1175,7 @@ export class AccountHandler {
scope = { contains: () => true }; scope = { contains: () => true };
} else { } else {
uid = auth.credentials.user; uid = auth.credentials.user;
scope = ScopeSet.fromArray(auth.credentials.scope); scope = ScopeSet.fromArray(auth.credentials.scope!);
} }
const res: Record<string, any> = {}; const res: Record<string, any> = {};

Просмотреть файл

@ -13,7 +13,7 @@ const { OAUTH_SCOPE_OLD_SYNC } = require('fxa-shared/oauth/constants');
const encrypt = require('fxa-shared/auth/encrypt'); const encrypt = require('fxa-shared/auth/encrypt');
const oauthDB = require('../../oauth/db'); const oauthDB = require('../../oauth/db');
const client = require('../../oauth/client'); const client = require('../../oauth/client');
const ScopeSet = require('fxa-shared/oauth/scopes'); const ScopeSet = require('fxa-shared/oauth/scopes').scopeSetHelpers;
// the refresh token scheme is currently used by things connected to sync, // the refresh token scheme is currently used by things connected to sync,
// and we're at a transitionary stage of its evolution into something more generic, // and we're at a transitionary stage of its evolution into something more generic,

Просмотреть файл

@ -6,7 +6,7 @@
const MISC_DOCS = require('../../docs/swagger/misc-api').default; const MISC_DOCS = require('../../docs/swagger/misc-api').default;
const validators = require('./validators'); const validators = require('./validators');
const ScopeSet = require('fxa-shared/oauth/scopes'); const ScopeSet = require('fxa-shared/oauth/scopes').scopeSetHelpers;
const AppError = require('../../lib/error'); const AppError = require('../../lib/error');
const Joi = require('joi'); const Joi = require('joi');

Просмотреть файл

@ -101,7 +101,7 @@ export class AppleIapHandler {
// once VPN migration is complete (FXA-5848). // once VPN migration is complete (FXA-5848).
'profile:subscriptions', 'profile:subscriptions',
]; ];
const scope = ScopeSet.fromArray(auth.credentials.scope); const scope = ScopeSet.fromArray(auth.credentials.scope!);
if (!scopes.some((requiredScope) => scope.contains(requiredScope))) { if (!scopes.some((requiredScope) => scope.contains(requiredScope))) {
throw error.invalidScopes(); throw error.invalidScopes();
} }

Просмотреть файл

@ -15,7 +15,7 @@ export async function handleAuth(
auth: AuthRequest['auth'], auth: AuthRequest['auth'],
fetchEmail = false fetchEmail = false
) { ) {
const scope = ScopeSet.fromArray(auth.credentials.scope); const scope = ScopeSet.fromArray(auth.credentials.scope!);
if (!scope.contains(OAUTH_SCOPE_SUBSCRIPTIONS)) { if (!scope.contains(OAUTH_SCOPE_SUBSCRIPTIONS)) {
throw error.invalidScopes(); throw error.invalidScopes();
} }
@ -36,7 +36,7 @@ export async function handleAuth(
} }
export function handleUidAuth(auth: AuthRequest['auth']): string { export function handleUidAuth(auth: AuthRequest['auth']): string {
const scope = ScopeSet.fromArray(auth.credentials.scope); const scope = ScopeSet.fromArray(auth.credentials.scope!);
if (!scope.contains(OAUTH_SCOPE_SUBSCRIPTIONS)) { if (!scope.contains(OAUTH_SCOPE_SUBSCRIPTIONS)) {
throw error.invalidScopes(); throw error.invalidScopes();
} }
@ -44,7 +44,7 @@ export function handleUidAuth(auth: AuthRequest['auth']): string {
} }
export function handleAuthScoped(auth: AuthRequest['auth'], scopes: string[]) { export function handleAuthScoped(auth: AuthRequest['auth'], scopes: string[]) {
const scope = ScopeSet.fromArray(auth.credentials.scope); const scope = ScopeSet.fromArray(auth.credentials.scope!);
for (const requiredScope of scopes) { for (const requiredScope of scopes) {
if (!scope.contains(requiredScope)) { if (!scope.contains(requiredScope)) {
throw error.invalidScopes(); throw error.invalidScopes();

Просмотреть файл

@ -8,7 +8,7 @@ const sinon = require('sinon');
const assert = { ...sinon.assert, ...require('chai').assert }; const assert = { ...sinon.assert, ...require('chai').assert };
const getRoute = require('../../routes_helpers').getRoute; const getRoute = require('../../routes_helpers').getRoute;
const mocks = require('../../mocks'); const mocks = require('../../mocks');
const ScopeSet = require('fxa-shared/oauth/scopes'); const ScopeSet = require('fxa-shared/oauth/scopes').scopeSetHelpers;
const error = require('../../../lib/error'); const error = require('../../../lib/error');
const { INVALID_PARAMETER, MISSING_PARAMETER } = error.ERRNO; const { INVALID_PARAMETER, MISSING_PARAMETER } = error.ERRNO;

Просмотреть файл

@ -34,13 +34,13 @@ const serveStatic = require('serve-static');
const sentry = require('../lib/sentry'); const sentry = require('../lib/sentry');
const statsd = require('../lib/statsd'); const statsd = require('../lib/statsd');
const { cors, routing } = require('fxa-shared/express')(); const { cors, routing } = require('fxa-shared/express').express();
const { const {
useSettingsProxy, useSettingsProxy,
modifySettingsStatic, modifySettingsStatic,
} = require('../lib/beta-settings'); } = require('../lib/beta-settings');
const userAgent = require('fxa-shared/metrics/user-agent'); const userAgent = require('fxa-shared/metrics/user-agent').default;
if (!userAgent.isToVersionStringSupported()) { if (!userAgent.isToVersionStringSupported()) {
// npm@3 installs the incorrect version of node-uap, one without `toVersionString`. // npm@3 installs the incorrect version of node-uap, one without `toVersionString`.
// To ensure the correct version is installed, check toVersionString is available. // To ensure the correct version is installed, check toVersionString is available.

Просмотреть файл

@ -23,9 +23,9 @@ const {
mapLocation, mapLocation,
mapOs, mapOs,
validate, validate,
} = require('fxa-shared/metrics/amplitude'); } = require('fxa-shared/metrics/amplitude').amplitude;
const logger = require('./logging/log')(); const logger = require('./logging/log')();
const ua = require('fxa-shared/metrics/user-agent'); const ua = require('fxa-shared/metrics/user-agent').default;
const config = require('./configuration'); const config = require('./configuration');
const { version: VERSION } = require('../../package.json'); const { version: VERSION } = require('../../package.json');

Просмотреть файл

@ -10,20 +10,17 @@ const flowMetrics = require('./flow-metrics');
const log = require('./logging/log')('server.flow-event'); const log = require('./logging/log')('server.flow-event');
const geodbConfig = config.get('geodb'); const geodbConfig = config.get('geodb');
const geodb = require('fxa-geodb')(geodbConfig); const geodb = require('fxa-geodb')(geodbConfig);
const remoteAddress = require('fxa-shared/express/remote-address')( const remoteAddress =
config.get('clientAddressDepth') require('fxa-shared/express/remote-address').remoteAddress(
); config.get('clientAddressDepth')
const geolocate = require('fxa-shared/express/geo-locate')(geodb)( );
const geolocate = require('fxa-shared/express/geo-locate').geolocate(geodb)(
remoteAddress remoteAddress
)(log); )(log);
const os = require('os'); const os = require('os');
const statsd = require('./statsd'); const statsd = require('./statsd');
const { const { VERSION, PERFORMANCE_TIMINGS, limitLength, isValidTime } =
VERSION, require('fxa-shared').metrics.flowPerformance;
PERFORMANCE_TIMINGS,
limitLength,
isValidTime,
} = require('fxa-shared').metrics.flowPerformance;
const VALIDATION_PATTERNS = require('./validation').PATTERNS; const VALIDATION_PATTERNS = require('./validation').PATTERNS;
const DNT_ALLOWED_DATA = ['context', 'entrypoint', 'service']; const DNT_ALLOWED_DATA = ['context', 'entrypoint', 'service'];

Просмотреть файл

@ -10,9 +10,10 @@
const logger = require('./log')('server.requests'); const logger = require('./log')('server.requests');
const morgan = require('morgan'); const morgan = require('morgan');
const config = require('../configuration').getProperties(); const config = require('../configuration').getProperties();
const remoteAddress = require('fxa-shared/express/remote-address')( const remoteAddress =
config.clientAddressDepth require('fxa-shared/express/remote-address').remoteAddress(
); config.clientAddressDepth
);
/** /**
* Enhances connect logger middleware - custom formats. * Enhances connect logger middleware - custom formats.

Просмотреть файл

@ -12,10 +12,11 @@ const logFlowEvent = require('../flow-event').logFlowEvent;
const logger = require('../logging/log')('server.get-metrics-flow'); const logger = require('../logging/log')('server.get-metrics-flow');
const geodbConfig = config.get('geodb'); const geodbConfig = config.get('geodb');
const geodb = require('fxa-geodb')(geodbConfig); const geodb = require('fxa-geodb')(geodbConfig);
const remoteAddress = require('fxa-shared/express/remote-address')( const remoteAddress =
config.get('clientAddressDepth') require('fxa-shared/express/remote-address').remoteAddress(
); config.get('clientAddressDepth')
const geolocate = require('fxa-shared/express/geo-locate')(geodb)( );
const geolocate = require('fxa-shared/express/geo-locate').geolocate(geodb)(
remoteAddress remoteAddress
)(logger); )(logger);
const uuid = require('node-uuid'); const uuid = require('node-uuid');

Просмотреть файл

@ -38,7 +38,12 @@ const amplitude = proxyquire(path.resolve('server/lib/amplitude'), {
}, },
}, },
'./logging/log': () => logger, './logging/log': () => logger,
'fxa-shared/metrics/amplitude': { validate: schemaValidatorStub }, 'fxa-shared/metrics/amplitude': {
amplitude: {
...require('fxa-shared/metrics/amplitude').amplitude,
validate: schemaValidatorStub,
},
},
'@sentry/node': mockSentry, '@sentry/node': mockSentry,
}); });
const Sentry = require('@sentry/node'); const Sentry = require('@sentry/node');

Просмотреть файл

@ -50,7 +50,7 @@ registerSuite('flow-event', {
'./amplitude': mocks.amplitude, './amplitude': mocks.amplitude,
'./configuration': mocks.config, './configuration': mocks.config,
'./flow-metrics': mocks.flowMetrics, './flow-metrics': mocks.flowMetrics,
'fxa-shared/express/geo-locate': mocks.geolocate, 'fxa-shared/express/geo-locate': { geolocate: mocks.geolocate },
'./statsd': mocks.statsd, './statsd': mocks.statsd,
}).metricsRequest; }).metricsRequest;
}, },

Просмотреть файл

@ -53,7 +53,7 @@ registerSuite('routes/get-metrics-flow', {
route = proxyquire('../../../server/lib/routes/get-metrics-flow', { route = proxyquire('../../../server/lib/routes/get-metrics-flow', {
'../amplitude': mocks.amplitude, '../amplitude': mocks.amplitude,
'../flow-event': mocks.flowEvent, '../flow-event': mocks.flowEvent,
'fxa-shared/express/geo-locate': mocks.geolocate, 'fxa-shared/express/geo-locate': { geolocate: mocks.geolocate },
'../logging/log': () => mocks.log, '../logging/log': () => mocks.log,
}); });
instance = route(mocks.config); instance = route(mocks.config);

Просмотреть файл

@ -11,7 +11,7 @@ const {
mapOs, mapOs,
toSnakeCase, toSnakeCase,
validate, validate,
} = require('fxa-shared/metrics/amplitude'); } = require('fxa-shared/metrics/amplitude').amplitude;
const config = require('../config'); const config = require('../config');
const amplitude = config.get('amplitude'); const amplitude = config.get('amplitude');
const log = require('./logging/log')(); const log = require('./logging/log')();

Просмотреть файл

@ -9,9 +9,11 @@ const mockAmplitudeConfig = {
schemaValidation: true, schemaValidation: true,
rawEvents: false, rawEvents: false,
}; };
jest.mock('fxa-shared/metrics/amplitude.js', () => ({ jest.mock('fxa-shared/metrics/amplitude', () => ({
...jest.requireActual('fxa-shared/metrics/amplitude.js'), amplitude: {
validate: mockSchemaValidatorFn, ...jest.requireActual('fxa-shared/metrics/amplitude').amplitude,
validate: mockSchemaValidatorFn,
},
})); }));
let scope; let scope;
const mockSentry = { const mockSentry = {

Просмотреть файл

@ -7,9 +7,10 @@
const logger = require('./log')('server.requests'); const logger = require('./log')('server.requests');
const morgan = require('morgan'); const morgan = require('morgan');
const config = require('../../config'); const config = require('../../config');
const remoteAddress = require('fxa-shared/express/remote-address')( const remoteAddress =
config.get('clientAddressDepth') require('fxa-shared/express/remote-address').remoteAddress(
); config.get('clientAddressDepth')
);
const { enabled, format } = config.get('logging.routes'); const { enabled, format } = config.get('logging.routes');

Просмотреть файл

@ -31,7 +31,7 @@ module.exports = () => {
const csp = require('../lib/csp'); const csp = require('../lib/csp');
const cspRulesBlocking = require('../lib/csp/blocking')(config); const cspRulesBlocking = require('../lib/csp/blocking')(config);
const cspRulesReportOnly = require('../lib/csp/report-only')(config); const cspRulesReportOnly = require('../lib/csp/report-only')(config);
const { cors, routing } = require('fxa-shared/express')(); const { cors, routing } = require('fxa-shared/express').express();
const { v4: uuid } = require('uuid'); const { v4: uuid } = require('uuid');
const { const {

Просмотреть файл

@ -1,7 +1,7 @@
/* This Source Code Form is subject to the terms of the Mozilla Public /* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this * License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import * as ScopeSet from '../oauth/scopes'; import ScopeSet from '../oauth/scopes';
import { IClientFormatter } from './formatters'; import { IClientFormatter } from './formatters';
import { import {
AttachedClient, AttachedClient,

Просмотреть файл

@ -7,7 +7,7 @@ import mysql from 'mysql';
import { AccessToken as AccessToken } from '../db/models/auth/access-token'; import { AccessToken as AccessToken } from '../db/models/auth/access-token';
import { ILogger } from '../log'; import { ILogger } from '../log';
import * as ScopeSet from '../oauth/scopes'; import ScopeSet from '../oauth/scopes';
// TODO: Improve types. Ported form javascript... // TODO: Improve types. Ported form javascript...
const buf = require('buf').hex; const buf = require('buf').hex;

Просмотреть файл

@ -2,4 +2,6 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this * License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
module.exports = require('cors'); import cors from 'cors';
export default cors;

Просмотреть файл

@ -1,16 +0,0 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
// IP address geolocation
'use strict';
module.exports = (geodb) => (remoteAddress) => (log) => (request) => {
try {
return geodb(remoteAddress(request).clientAddress);
} catch (err) {
log.error('geodb.error', err);
return {};
}
};

Просмотреть файл

@ -0,0 +1,23 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
// IP address geolocation
import express from 'express';
import { Logger } from 'mozlog';
export const geolocate =
(geodb: Function) =>
(remoteAddress: Function) =>
(log: Logger) =>
(request: express.Request) => {
try {
return geodb(remoteAddress(request).clientAddress);
} catch (err) {
log.error('geodb.error', err);
return {};
}
};
export default geolocate;

Просмотреть файл

@ -2,12 +2,12 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this * License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
module.exports = () => { import cors from './cors';
const cors = require('./cors'); import routing from './routing';
const routing = require('./routing');
return { export const express = () => ({
cors, cors,
routing, routing,
}; });
};
export default express;

Просмотреть файл

@ -1,31 +0,0 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
// Utility function to parse the client IP address from request headers.
'use strict';
const joi = require('joi');
const IP_ADDRESS = joi.string().ip().required();
module.exports = (clientIpAddressDepth) => (request) => {
let ipAddresses = (request.headers['x-forwarded-for'] || '')
.split(',')
.map((address) => address.trim());
ipAddresses.push(request.ip || request.connection.remoteAddress);
ipAddresses = ipAddresses.filter(
(ipAddress) => !IP_ADDRESS.validate(ipAddress).error
);
let clientAddressIndex = ipAddresses.length - clientIpAddressDepth;
if (clientAddressIndex < 0) {
clientAddressIndex = 0;
}
return {
addresses: ipAddresses,
clientAddress: ipAddresses[clientAddressIndex],
};
};

Просмотреть файл

@ -0,0 +1,35 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
// Utility function to parse the client IP address from request headers.
import express from 'express';
import joi from 'joi';
const IP_ADDRESS = joi.string().ip().required();
export const remoteAddress =
(clientIpAddressDepth: number) => (request: express.Request) => {
let ipAddresses = ((request.headers['x-forwarded-for'] as string) || '')
.split(',')
.map((address) => address.trim());
ipAddresses.push(
request.ip || (request.connection.remoteAddress as string)
);
ipAddresses = ipAddresses.filter(
(ipAddress) => !IP_ADDRESS.validate(ipAddress).error
);
let clientAddressIndex = ipAddresses.length - clientIpAddressDepth;
if (clientAddressIndex < 0) {
clientAddressIndex = 0;
}
return {
addresses: ipAddresses,
clientAddress: ipAddresses[clientAddressIndex],
};
};
export default remoteAddress;

Просмотреть файл

@ -2,8 +2,29 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this * License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
module.exports = (app, logger) => { import express from 'express';
const cors = require('./cors'); import Logger from '../lib/logger';
import cors from './cors';
type RouteMethod =
| 'all'
| 'get'
| 'post'
| 'put'
| 'delete'
| 'patch'
| 'options'
| 'head';
type RouteDefinition = {
method: RouteMethod;
path: string | RegExp;
process: Function;
cors?: any;
preProcess?: Function;
validate?: Object;
};
export const routing = (app: express.Express, logger: Logger) => {
const { const {
celebrate, celebrate,
isCelebrateError: isValidationError, isCelebrateError: isValidationError,
@ -26,7 +47,7 @@ module.exports = (app, logger) => {
* @param {Object} [routeDefinition.validate] declare JOI validation. * @param {Object} [routeDefinition.validate] declare JOI validation.
* Follows [celebrate](https://www.npmjs.com/package/celebrate) conventions. * Follows [celebrate](https://www.npmjs.com/package/celebrate) conventions.
*/ */
addRoute(routeDefinition) { addRoute(routeDefinition: RouteDefinition) {
if (!isValidRouteDefinition(routeDefinition)) { if (!isValidRouteDefinition(routeDefinition)) {
logger.error('route definition invalid: ', routeDefinition); logger.error('route definition invalid: ', routeDefinition);
throw new Error('Invalid route definition'); throw new Error('Invalid route definition');
@ -45,7 +66,10 @@ module.exports = (app, logger) => {
: undefined; : undefined;
// Enable the pre-flight OPTIONS request // Enable the pre-flight OPTIONS request
const corsHandler = cors(corsConfig); const corsHandler = cors(corsConfig);
app.options(routeDefinition.path, corsHandler); app.options(
routeDefinition.path as string,
corsHandler as express.RequestHandler
);
routeHandlers.push(corsHandler); routeHandlers.push(corsHandler);
} }
@ -63,10 +87,18 @@ module.exports = (app, logger) => {
} }
routeHandlers.push(routeDefinition.process); routeHandlers.push(routeDefinition.process);
app[routeDefinition.method](routeDefinition.path, ...routeHandlers); app[routeDefinition.method as RouteMethod](
routeDefinition.path as string,
...routeHandlers
);
}, },
validationErrorHandler(err, req, res, next) { validationErrorHandler(
err: Error,
req: express.Request,
res: express.Response,
next: express.NextFunction
) {
if (err && isValidationError(err)) { if (err && isValidationError(err)) {
logger.error('validation.failed', { logger.error('validation.failed', {
err, err,
@ -81,7 +113,9 @@ module.exports = (app, logger) => {
}, },
}; };
function isValidRouteDefinition(route) { function isValidRouteDefinition(route: { [key: string]: unknown }) {
return !!route.method && route.path && route.process; return !!route.method && route.path && route.process;
} }
}; };
export default routing;

Просмотреть файл

@ -6,6 +6,7 @@ import * as invoice from './dto/auth/payments/invoice';
import * as emailHelpers from './email/helpers'; import * as emailHelpers from './email/helpers';
import popularDomains from './email/popularDomains.json'; import popularDomains from './email/popularDomains.json';
import BaseGroupingRule from './experiments/base'; import BaseGroupingRule from './experiments/base';
import express from './express';
import featureFlags from './feature-flags'; import featureFlags from './feature-flags';
import { localizeTimestamp } from './l10n/localizeTimestamp'; import { localizeTimestamp } from './l10n/localizeTimestamp';
import supportedLanguages from './l10n/supportedLanguages.json'; import supportedLanguages from './l10n/supportedLanguages.json';
@ -29,6 +30,7 @@ module.exports = {
experiments: { experiments: {
BaseGroupingRule, BaseGroupingRule,
}, },
express,
featureFlags, featureFlags,
l10n: { l10n: {
localizeTimestamp, localizeTimestamp,

Просмотреть файл

@ -2,9 +2,19 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this * License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
'use strict'; import Ajv from 'ajv';
import { ParsedUserAgentProperties, ParsedUa, ParsedOs } from './user-agent';
import { Location } from '../connected-services/models/Location';
type AmplitudeEventGroup = typeof GROUPS;
type AmplitudeEventGroupKey = keyof AmplitudeEventGroup;
type AmplitudeEventFuzzyEventGroupMapFn = (category: string) => string;
type AmplitudeEventFuzzyEventNameMapFn = (
category: string,
target: string
) => string;
type EventData = { [key: string]: any };
const Ajv = require('ajv');
const ajv = new Ajv(); const ajv = new Ajv();
const amplitudeSchema = require('./amplitude-event.1.schema.json'); const amplitudeSchema = require('./amplitude-event.1.schema.json');
const validateAmplitudeEvent = ajv.compile(amplitudeSchema); const validateAmplitudeEvent = ajv.compile(amplitudeSchema);
@ -72,11 +82,17 @@ const EVENT_PROPERTIES = {
function NOP() {} function NOP() {}
function mapConnectDeviceFlow(eventType, eventCategory, eventTarget) { function mapConnectDeviceFlow(
eventType: string,
eventCategory: string,
eventTarget: string
) {
// @ts-ignore
const connect_device_flow = CONNECT_DEVICE_FLOWS[eventCategory]; const connect_device_flow = CONNECT_DEVICE_FLOWS[eventCategory];
if (connect_device_flow) { if (connect_device_flow) {
const result = { connect_device_flow }; const result: { connect_device_flow: string; connect_device_os?: string } =
{ connect_device_flow };
if (eventTarget) { if (eventTarget) {
result.connect_device_os = eventTarget; result.connect_device_os = eventTarget;
@ -84,13 +100,20 @@ function mapConnectDeviceFlow(eventType, eventCategory, eventTarget) {
return result; return result;
} }
return;
} }
function mapEmailType(eventType, eventCategory, eventTarget, data) { function mapEmailType(
eventType: string,
eventCategory: string,
eventTarget: string,
data: EventData
) {
const email_type = data.emailTypes[eventCategory]; const email_type = data.emailTypes[eventCategory];
if (email_type) { if (email_type) {
const result = { const result: { [key: string]: string } = {
email_type, email_type,
email_provider: data.emailDomain, email_provider: data.emailDomain,
}; };
@ -103,41 +126,47 @@ function mapEmailType(eventType, eventCategory, eventTarget, data) {
return result; return result;
} }
return;
} }
function mapSettingsEventProperties(...args) { function mapSettingsEventProperties(...args: [string, string]) {
return { return {
...mapDisconnectReason(...args), ...mapDisconnectReason(...args),
}; };
} }
function mapDisconnectReason(eventType, eventCategory) { function mapDisconnectReason(eventType: string, eventCategory: string) {
if (eventType === 'disconnect_device' && eventCategory) { if (eventType === 'disconnect_device' && eventCategory) {
return { reason: eventCategory }; return { reason: eventCategory };
} }
return;
} }
function mapDomainValidationResult( function mapDomainValidationResult(
eventType, eventType: string,
eventCategory, eventCategory: string,
eventTarget, eventTarget: string,
data data: EventData
) { ) {
// This function is called for all fxa_reg event types, only add the event // This function is called for all fxa_reg event types, only add the event
// properties for the results pertaining to domain_validation_result. // properties for the results pertaining to domain_validation_result.
if (eventType === 'domain_validation_result' && eventCategory) { if (eventType === 'domain_validation_result' && eventCategory) {
return { validation_result: eventCategory }; return { validation_result: eventCategory };
} }
return;
} }
function mapSubscriptionUpgradeEventProperties( function mapSubscriptionUpgradeEventProperties(
eventType, eventType: string,
eventCategory, eventCategory: string,
eventTarget, eventTarget: string,
data data: EventData
) { ) {
if (data) { if (data) {
const properties = {}; const properties: { [key: string]: string } = {};
if (data.previousPlanId) { if (data.previousPlanId) {
properties['previous_plan_id'] = data.previousPlanId; properties['previous_plan_id'] = data.previousPlanId;
@ -149,16 +178,18 @@ function mapSubscriptionUpgradeEventProperties(
return properties; return properties;
} }
return;
} }
function mapSubscriptionPaymentEventProperties( function mapSubscriptionPaymentEventProperties(
eventType, eventType: string,
eventCategory, eventCategory: string,
eventTarget, eventTarget: string,
data data: EventData
) { ) {
if (data) { if (data) {
const properties = {}; const properties: { [key: string]: string } = {};
if (data.sourceCountry) { if (data.sourceCountry) {
properties['source_country'] = data.sourceCountry; properties['source_country'] = data.sourceCountry;
@ -174,9 +205,11 @@ function mapSubscriptionPaymentEventProperties(
return properties; return properties;
} }
return undefined;
} }
function validate(event) { function validate(event: { [key: string]: any }) {
if (!validateAmplitudeEvent(event)) { if (!validateAmplitudeEvent(event)) {
throw new Error( throw new Error(
`Invalid data: ${ajv.errorsText(validateAmplitudeEvent.errors, { `Invalid data: ${ajv.errorsText(validateAmplitudeEvent.errors, {
@ -187,7 +220,7 @@ function validate(event) {
return true; return true;
} }
module.exports = { export const amplitude = {
EVENT_PROPERTIES, EVENT_PROPERTIES,
GROUPS, GROUPS,
mapBrowser, mapBrowser,
@ -227,7 +260,23 @@ module.exports = {
* *
* @returns {Function} The mapper function. * @returns {Function} The mapper function.
*/ */
initialize(services, events, fuzzyEvents) { initialize(
services: { [key: string]: string },
events: {
[key: string]: {
group: AmplitudeEventGroupKey | Function;
event: string | AmplitudeEventFuzzyEventNameMapFn;
minimal?: boolean;
};
},
fuzzyEvents: Map<
RegExp,
{
group: AmplitudeEventGroupKey | AmplitudeEventFuzzyEventGroupMapFn;
event: string | AmplitudeEventFuzzyEventNameMapFn;
}
>
) {
/** /**
* Map from a source event and it's associated data to an amplitude event. * Map from a source event and it's associated data to an amplitude event.
* *
@ -242,7 +291,7 @@ module.exports = {
* numerous to list here, but may be discerned with * numerous to list here, but may be discerned with
* ease by perusing the code. * ease by perusing the code.
*/ */
return (event, data) => { return (event: { [key: string]: any }, data: EventData) => {
if (!event || !data) { if (!event || !data) {
return; return;
} }
@ -288,6 +337,7 @@ module.exports = {
let version; let version;
try { try {
// @ts-ignore
version = /([0-9]+)\.([0-9]+)$/.exec(data.version)[0]; version = /([0-9]+)\.([0-9]+)$/.exec(data.version)[0];
} catch (err) {} } catch (err) {}
@ -317,22 +367,28 @@ module.exports = {
device_model: data.formFactor, device_model: data.formFactor,
event_properties: mapEventProperties( event_properties: mapEventProperties(
eventType, eventType,
eventGroup, eventGroup as string,
eventCategory, eventCategory as string,
eventTarget, eventTarget as string,
data
),
user_properties: mapUserProperties(
eventGroup as string,
eventCategory as string,
data data
), ),
user_properties: mapUserProperties(eventGroup, eventCategory, data),
}); });
} }
return;
}; };
function mapEventProperties( function mapEventProperties(
eventType, eventType: string,
eventGroup, eventGroup: string,
eventCategory, eventCategory: string,
eventTarget, eventTarget: string,
data data: EventData
) { ) {
const { serviceName, clientId } = getServiceNameAndClientId(data); const { serviceName, clientId } = getServiceNameAndClientId(data);
@ -356,7 +412,7 @@ module.exports = {
); );
} }
function getServiceNameAndClientId(data) { function getServiceNameAndClientId(data: EventData) {
let serviceName, clientId; let serviceName, clientId;
const { service } = data; const { service } = data;
@ -372,7 +428,11 @@ module.exports = {
return { serviceName, clientId }; return { serviceName, clientId };
} }
function mapUserProperties(eventGroup, eventCategory, data) { function mapUserProperties(
eventGroup: string,
eventCategory: string,
data: EventData
) {
return Object.assign( return Object.assign(
pruneUnsetValues({ pruneUnsetValues({
entrypoint: data.entrypoint, entrypoint: data.entrypoint,
@ -395,7 +455,7 @@ module.exports = {
); );
} }
function mapAppendProperties(data) { function mapAppendProperties(data: EventData) {
const servicesUsed = mapServicesUsed(data); const servicesUsed = mapServicesUsed(data);
const experiments = mapExperiments(data); const experiments = mapExperiments(data);
const userPreferences = mapUserPreferences(data); const userPreferences = mapUserPreferences(data);
@ -410,9 +470,11 @@ module.exports = {
), ),
}; };
} }
return;
} }
function mapServicesUsed(data) { function mapServicesUsed(data: EventData) {
const { serviceName } = getServiceNameAndClientId(data); const { serviceName } = getServiceNameAndClientId(data);
if (serviceName) { if (serviceName) {
@ -420,12 +482,14 @@ module.exports = {
fxa_services_used: serviceName, fxa_services_used: serviceName,
}; };
} }
return;
} }
}, },
}; };
function pruneUnsetValues(data) { function pruneUnsetValues(data: EventData) {
const result = {}; const result: Partial<EventData> = {};
Object.keys(data).forEach((key) => { Object.keys(data).forEach((key) => {
const value = data[key]; const value = data[key];
@ -438,7 +502,7 @@ function pruneUnsetValues(data) {
return result; return result;
} }
function mapExperiments(data) { function mapExperiments(data: EventData) {
const { experiments } = data; const { experiments } = data;
if (Array.isArray(experiments) && experiments.length > 0) { if (Array.isArray(experiments) && experiments.length > 0) {
@ -448,9 +512,11 @@ function mapExperiments(data) {
), ),
}; };
} }
return;
} }
function mapUserPreferences(data) { function mapUserPreferences(data: EventData) {
const { userPreferences } = data; const { userPreferences } = data;
// Don't send user preferences metric if there are none! // Don't send user preferences metric if there are none!
@ -458,7 +524,7 @@ function mapUserPreferences(data) {
return; return;
} }
const formattedUserPreferences = {}; const formattedUserPreferences: { [key: string]: any } = {};
for (const pref in userPreferences) { for (const pref in userPreferences) {
formattedUserPreferences[toSnakeCase(pref)] = userPreferences[pref]; formattedUserPreferences[toSnakeCase(pref)] = userPreferences[pref];
} }
@ -466,7 +532,7 @@ function mapUserPreferences(data) {
return formattedUserPreferences; return formattedUserPreferences;
} }
function toSnakeCase(string) { function toSnakeCase(string: string) {
return string return string
.replace(/([a-z])([A-Z])/g, (s, c1, c2) => `${c1}_${c2.toLowerCase()}`) .replace(/([a-z])([A-Z])/g, (s, c1, c2) => `${c1}_${c2.toLowerCase()}`)
.replace(/([A-Z])/g, (c) => c.toLowerCase()) .replace(/([A-Z])/g, (c) => c.toLowerCase())
@ -474,79 +540,102 @@ function toSnakeCase(string) {
.replace(/-/g, '_'); .replace(/-/g, '_');
} }
function mapSyncDevices(data) { function mapSyncDevices(data: EventData) {
const { devices } = data; const { devices } = data;
if (Array.isArray(devices)) { if (Array.isArray(devices)) {
return { return {
sync_device_count: devices.length, sync_device_count: devices.length,
sync_active_devices_day: countDevices(devices, DAY), sync_active_devices_day: countDevices(
sync_active_devices_week: countDevices(devices, WEEK), devices as [{ [key: string]: any }],
sync_active_devices_month: countDevices(devices, FOUR_WEEKS), DAY
),
sync_active_devices_week: countDevices(
devices as [{ [key: string]: any }],
WEEK
),
sync_active_devices_month: countDevices(
devices as [{ [key: string]: any }],
FOUR_WEEKS
),
}; };
} }
return;
} }
function countDevices(devices, period) { function countDevices(devices: [{ [key: string]: any }], period: number) {
return devices.filter( return devices.filter(
(device) => device.lastAccessTime >= Date.now() - period (device) => device.lastAccessTime >= Date.now() - period
).length; ).length;
} }
function mapSyncEngines(data) { function mapSyncEngines(data: EventData) {
const { syncEngines: sync_engines } = data; const { syncEngines: sync_engines } = data;
if (Array.isArray(sync_engines) && sync_engines.length > 0) { if (Array.isArray(sync_engines) && sync_engines.length > 0) {
return { sync_engines }; return { sync_engines };
} }
return;
} }
function mapNewsletters(data) { function mapNewsletters(data: EventData) {
let { newsletters } = data; let { newsletters } = data;
if (newsletters) { if (newsletters) {
newsletters = newsletters.map((newsletter) => { newsletters = newsletters.map((newsletter: string) => {
return toSnakeCase(newsletter); return toSnakeCase(newsletter);
}); });
return { newsletters, newsletter_state: 'subscribed' }; return { newsletters, newsletter_state: 'subscribed' };
} }
return;
} }
function mapBrowser(userAgent) { function mapBrowser(userAgent: ParsedUserAgentProperties) {
return mapUserAgentProperties(userAgent, 'ua', 'browser', 'browserVersion'); return mapUserAgentProperties(userAgent, 'ua', 'browser', 'browserVersion');
} }
function mapOs(userAgent) { function mapOs(userAgent: ParsedUserAgentProperties) {
return mapUserAgentProperties(userAgent, 'os', 'os', 'osVersion'); return mapUserAgentProperties(userAgent, 'os', 'os', 'osVersion');
} }
function mapUserAgentProperties( function mapUserAgentProperties(
userAgent, userAgent: ParsedUserAgentProperties,
key, key: keyof ParsedUserAgentProperties,
familyProperty, familyProperty: string,
versionProperty versionProperty: string
) { ) {
const group = userAgent[key]; const group = userAgent[key];
const { family } = group; const { family } = group;
if (family && family !== 'Other') { if (family && family !== 'Other') {
return { return {
[familyProperty]: family, [familyProperty]: family,
[versionProperty]: group.toVersionString(), [versionProperty]: (group as ParsedUa | ParsedOs).toVersionString(),
}; };
} }
return;
} }
function mapFormFactor(userAgent) { function mapFormFactor(userAgent: ParsedUserAgentProperties) {
const { brand, family: formFactor } = userAgent.device; const { brand, family: formFactor } = userAgent.device;
if (brand && formFactor && brand !== 'Generic') { if (brand && formFactor && brand !== 'Generic') {
return { formFactor }; return { formFactor };
} }
return;
} }
function mapLocation(location) { function mapLocation(location: Location) {
if (location && (location.country || location.state)) { if (location && (location.country || location.state)) {
return { return {
country: location.country, country: location.country,
region: location.state, region: location.state,
}; };
} }
return;
} }
export default amplitude;

Просмотреть файл

@ -2,11 +2,9 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this * License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
'use strict';
const MAX_DATA_LENGTH = 100; const MAX_DATA_LENGTH = 100;
const VERSION = 1; export const VERSION = 1;
const PERFORMANCE_TIMINGS = [ export const PERFORMANCE_TIMINGS = [
// These timings are only an approximation, to be used as extra signals // These timings are only an approximation, to be used as extra signals
// when looking for correlations in the flow data. They're not perfect // when looking for correlations in the flow data. They're not perfect
// representations, for instance: // representations, for instance:
@ -37,7 +35,7 @@ const PERFORMANCE_TIMINGS = [
}, },
]; ];
function limitLength(data) { export function limitLength(data: string) {
if (data && data.length > MAX_DATA_LENGTH) { if (data && data.length > MAX_DATA_LENGTH) {
return data.substr(0, MAX_DATA_LENGTH); return data.substr(0, MAX_DATA_LENGTH);
} }
@ -45,7 +43,11 @@ function limitLength(data) {
return data; return data;
} }
function isValidTime(time, requestReceivedTime, expiry) { export function isValidTime(
time: any,
requestReceivedTime: number,
expiry: number
) {
if (typeof time !== 'number') { if (typeof time !== 'number') {
return false; return false;
} }
@ -58,9 +60,4 @@ function isValidTime(time, requestReceivedTime, expiry) {
return true; return true;
} }
module.exports = { export default { VERSION, PERFORMANCE_TIMINGS, limitLength, isValidTime };
VERSION,
PERFORMANCE_TIMINGS,
limitLength,
isValidTime,
};

Просмотреть файл

@ -1,9 +1,13 @@
const joi = require('joi'); /* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import joi from 'joi';
const TIMESTAMP_MS = joi.number().required(); const TIMESTAMP_MS = joi.number().required();
// Validate the subset of PerformanceNavgationTiming properties used here. // Validate the subset of PerformanceNavgationTiming properties used here.
// (Ref: https://www.w3.org/TR/navigation-timing-2/#dom-performancenavigationtiming) // (Ref: https://www.w3.org/TR/navigation-timing-2/#dom-performancenavigationtiming)
module.exports.navigationTimingSchema = joi.object().keys({ export const navigationTimingSchema = joi.object().keys({
domainLookupStart: TIMESTAMP_MS.min(0), domainLookupStart: TIMESTAMP_MS.min(0),
domComplete: TIMESTAMP_MS.min(joi.ref('domInteractive')), domComplete: TIMESTAMP_MS.min(joi.ref('domInteractive')),
domInteractive: TIMESTAMP_MS.min(joi.ref('responseEnd')), domInteractive: TIMESTAMP_MS.min(joi.ref('responseEnd')),
@ -15,3 +19,5 @@ module.exports.navigationTimingSchema = joi.object().keys({
responseEnd: TIMESTAMP_MS.min(joi.ref('responseStart')), responseEnd: TIMESTAMP_MS.min(joi.ref('responseStart')),
responseStart: TIMESTAMP_MS.min(joi.ref('requestStart')), responseStart: TIMESTAMP_MS.min(joi.ref('requestStart')),
}); });
export default { navigationTimingSchema };

25
packages/fxa-shared/metrics/user-agent.d.ts поставляемый
Просмотреть файл

@ -1,25 +0,0 @@
export type ParsedUa = {
family: string | null;
major: string | null;
minor: string | null;
patch: string | null;
toVersionString: () => string;
};
export type ParsedOs = ParsedUa & {
patchMinor: string | null;
};
export type ParsedDevice = {
family: string | null;
brand: string | null;
model: string | null;
};
export type ParsedUserAgentProperties = {
ua: ParsedUa;
os: ParsedOs;
device: ParsedDevice;
};
export function parse(uaString: string | undefined): ParsedUserAgentProperties;
export function isToVersionStringSupported(ua: ParsedUa): boolean;

Просмотреть файл

@ -2,12 +2,32 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this * License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
export type ParsedUa = {
family: string | null;
major: string | null;
minor: string | null;
patch: string | null;
toVersionString: () => string;
};
export type ParsedOs = ParsedUa & {
patchMinor: string | null;
};
export type ParsedDevice = {
family: string | null;
brand: string | null;
model: string | null;
};
export type ParsedUserAgentProperties = {
ua: ParsedUa;
os: ParsedOs;
device: ParsedDevice;
};
// Safe wrapper around node-uap, which prevents unsafe input from // Safe wrapper around node-uap, which prevents unsafe input from
// leaking back to the result data. // leaking back to the result data.
'use strict'; // @ts-ignore
import * as ua from 'node-uap';
const ua = require('node-uap');
// We know this won't match "Symbian^3", "UI/WKWebView" or "Mail.ru" but // We know this won't match "Symbian^3", "UI/WKWebView" or "Mail.ru" but
// it's simpler and safer to limit to alphanumerics, underscore and space. // it's simpler and safer to limit to alphanumerics, underscore and space.
@ -15,7 +35,9 @@ const VALID_FAMILY = /^[\w ]{1,32}$/;
const VALID_VERSION = /^[\w.]{1,16}$/; const VALID_VERSION = /^[\w.]{1,16}$/;
exports.parse = (userAgentString) => { export const parse = (
userAgentString: string | undefined
): ParsedUserAgentProperties => {
const result = ua.parse(userAgentString); const result = ua.parse(userAgentString);
safeFamily(result.ua); safeFamily(result.ua);
@ -27,7 +49,9 @@ exports.parse = (userAgentString) => {
return result; return result;
}; };
exports.isToVersionStringSupported = (result) => { export const isToVersionStringSupported = (
result: ParsedUserAgentProperties
): boolean => {
if (!result) { if (!result) {
result = exports.parse( result = exports.parse(
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.13; rv:65.0) Gecko/20100101 Firefox/65.0' 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.13; rv:65.0) Gecko/20100101 Firefox/65.0'
@ -45,13 +69,13 @@ exports.isToVersionStringSupported = (result) => {
return true; return true;
}; };
function safeFamily(parent) { function safeFamily(parent: ParsedUa) {
if (!VALID_FAMILY.test(parent.family)) { if (!VALID_FAMILY.test(parent.family as string)) {
parent.family = null; parent.family = null;
} }
} }
function safeVersion(parent) { function safeVersion(parent: ParsedOs) {
if ( if (
parent && parent &&
parent.toVersionString && parent.toVersionString &&
@ -60,3 +84,5 @@ function safeVersion(parent) {
parent.major = parent.minor = parent.patch = parent.patchMinor = null; parent.major = parent.minor = parent.patch = parent.patchMinor = null;
} }
} }
export default { parse, isToVersionStringSupported };

Просмотреть файл

@ -1,18 +0,0 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
const OAUTH_SCOPE_SUBSCRIPTIONS =
'https://identity.mozilla.com/account/subscriptions';
module.exports = {
OAUTH_SCOPE_OLD_SYNC: 'https://identity.mozilla.com/apps/oldsync',
OAUTH_SCOPE_SESSION_TOKEN: 'https://identity.mozilla.com/tokens/session',
OAUTH_SCOPE_NEWSLETTERS: 'https://identity.mozilla.com/account/newsletters',
OAUTH_SCOPE_SUBSCRIPTIONS,
OAUTH_SCOPE_SUBSCRIPTIONS_IAP: `${OAUTH_SCOPE_SUBSCRIPTIONS}/iap`,
SHORT_ACCESS_TOKEN_TTL_IN_MS: 1000 * 60 * 60 * 6,
// Maximum age an account is considered "new", useful when sending
// notification emails
MAX_NEW_ACCOUNT_AGE: 1000 * 60 * 60 * 24,
};

Просмотреть файл

@ -0,0 +1,29 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
export const OAUTH_SCOPE_SUBSCRIPTIONS =
'https://identity.mozilla.com/account/subscriptions';
export const OAUTH_SCOPE_OLD_SYNC = 'https://identity.mozilla.com/apps/oldsync';
export const OAUTH_SCOPE_SESSION_TOKEN =
'https://identity.mozilla.com/tokens/session';
export const OAUTH_SCOPE_NEWSLETTERS =
'https://identity.mozilla.com/account/newsletters';
export const OAUTH_SCOPE_SUBSCRIPTIONS_IAP = `${OAUTH_SCOPE_SUBSCRIPTIONS}/iap`;
export const SHORT_ACCESS_TOKEN_TTL_IN_MS = 1000 * 60 * 60 * 6;
// Maximum age an account is considered "new"; useful when sending
// notification emails
export const MAX_NEW_ACCOUNT_AGE = 1000 * 60 * 60 * 24;
export const OauthConsts = {
OAUTH_SCOPE_OLD_SYNC,
OAUTH_SCOPE_SESSION_TOKEN,
OAUTH_SCOPE_NEWSLETTERS,
OAUTH_SCOPE_SUBSCRIPTIONS,
OAUTH_SCOPE_SUBSCRIPTIONS_IAP,
SHORT_ACCESS_TOKEN_TTL_IN_MS,
MAX_NEW_ACCOUNT_AGE,
};
export default OauthConsts;

Просмотреть файл

@ -2,9 +2,9 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this * License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
'use strict'; import { URL } from 'url';
const { URL } = require('url'); type Coerceable = ScopeSet | string[] | string;
// These character ranges are from the OAuth RFC, // These character ranges are from the OAuth RFC,
// https://tools.ietf.org/html/rfc6749#section-3.3 // https://tools.ietf.org/html/rfc6749#section-3.3
@ -75,7 +75,10 @@ const VALID_FRAGMENT_VALUE = /^#[a-zA-Z0-9_]+$/;
* *
*/ */
class ScopeSet { class ScopeSet {
constructor(scopes = []) { private _scopesToImplicants: { [key: string]: Set<string> };
private _implicantsToScopes: { [key: string]: Set<string> };
constructor(scopes: string[] = []) {
// To support efficient lookups, we store the set of scopes and // To support efficient lookups, we store the set of scopes and
// their fully-expanded set of implicants in a bi-directional mapping. // their fully-expanded set of implicants in a bi-directional mapping.
// //
@ -103,7 +106,7 @@ class ScopeSet {
* Check whether this `ScopeSet` contains the given scope. * Check whether this `ScopeSet` contains the given scope.
* *
*/ */
_hasScope(scope) { _hasScope(scope: string) {
return scope in this._scopesToImplicants; return scope in this._scopesToImplicants;
} }
@ -111,7 +114,7 @@ class ScopeSet {
* Check whether this `ScopeSet` contains one of the given scopes. * Check whether this `ScopeSet` contains one of the given scopes.
* *
*/ */
_hasSomeScope(scopes) { _hasSomeScope(scopes: Set<string>) {
for (const scope of scopes) { for (const scope of scopes) {
if (this._hasScope(scope)) { if (this._hasScope(scope)) {
return true; return true;
@ -125,7 +128,7 @@ class ScopeSet {
* the given scope. * the given scope.
* *
*/ */
_hasImplicant(scope) { _hasImplicant(scope: string) {
return scope in this._implicantsToScopes; return scope in this._implicantsToScopes;
} }
@ -134,7 +137,7 @@ class ScopeSet {
* one of the given scopes. * one of the given scopes.
* *
*/ */
_hasSomeImplicant(scopes) { _hasSomeImplicant(scopes: string[]) {
for (const scope of scopes) { for (const scope of scopes) {
if (scope in this._implicantsToScopes) { if (scope in this._implicantsToScopes) {
return true; return true;
@ -148,7 +151,7 @@ class ScopeSet {
* corresponding sets of implicants. * corresponding sets of implicants.
* *
*/ */
_iterScopes(cb) { _iterScopes(cb: (scope: string, implicants: Set<string>) => void) {
for (const scope in this._scopesToImplicants) { for (const scope in this._scopesToImplicants) {
cb(scope, this._scopesToImplicants[scope]); cb(scope, this._scopesToImplicants[scope]);
} }
@ -159,7 +162,7 @@ class ScopeSet {
* corresponding sets of implied scopes. * corresponding sets of implied scopes.
* *
*/ */
_iterImplicants(cb) { _iterImplicants(cb: (implicant: string, scopes: Set<string>) => void) {
for (const implicant in this._implicantsToScopes) { for (const implicant in this._implicantsToScopes) {
cb(implicant, this._implicantsToScopes[implicant]); cb(implicant, this._implicantsToScopes[implicant]);
} }
@ -170,7 +173,7 @@ class ScopeSet {
* the given scope. * the given scope.
* *
*/ */
_iterImpliedScopes(implicant, cb) { _iterImpliedScopes(implicant: string, cb: (s: string) => void) {
const impliedScopes = this._implicantsToScopes[implicant]; const impliedScopes = this._implicantsToScopes[implicant];
if (impliedScopes) { if (impliedScopes) {
for (const impliedScope of impliedScopes) { for (const impliedScope of impliedScopes) {
@ -185,7 +188,7 @@ class ScopeSet {
* at the first successful match. * at the first successful match.
* *
*/ */
_searchScopes(cb) { _searchScopes(cb: (scope: string, implicants: Set<string>) => boolean) {
for (const scope in this._scopesToImplicants) { for (const scope in this._scopesToImplicants) {
if (cb(scope, this._scopesToImplicants[scope])) { if (cb(scope, this._scopesToImplicants[scope])) {
return true; return true;
@ -200,7 +203,7 @@ class ScopeSet {
* at the first successful match. * at the first successful match.
* *
*/ */
_searchImplicants(cb) { _searchImplicants(cb: (implicant: string, scopes: Set<string>) => boolean) {
for (const implicant in this._implicantsToScopes) { for (const implicant in this._implicantsToScopes) {
if (cb(implicant, this._implicantsToScopes[implicant])) { if (cb(implicant, this._implicantsToScopes[implicant])) {
return true; return true;
@ -216,7 +219,7 @@ class ScopeSet {
* order to keep memory usage down and simplify further handling. * order to keep memory usage down and simplify further handling.
* *
*/ */
_addScope(scope, implicants) { _addScope(scope: string, implicants: Set<string>) {
// If the scope is already implied by something in this `ScopeSet`, // If the scope is already implied by something in this `ScopeSet`,
// then we can safely ignore it. // then we can safely ignore it.
if (this._hasSomeScope(implicants)) { if (this._hasSomeScope(implicants)) {
@ -244,7 +247,7 @@ class ScopeSet {
* scopes and their implicants. * scopes and their implicants.
* *
*/ */
_removeScope(scope) { _removeScope(scope: string) {
const implicants = this._scopesToImplicants[scope]; const implicants = this._scopesToImplicants[scope];
for (const implicant of implicants) { for (const implicant of implicants) {
const impliedScopes = this._implicantsToScopes[implicant]; const impliedScopes = this._implicantsToScopes[implicant];
@ -307,8 +310,8 @@ class ScopeSet {
* quadratic" performance traps. * quadratic" performance traps.
* *
*/ */
add(other) { add(other: Coerceable) {
other = coerce(other)._iterScopes((scope, implicants) => { coerce(other)._iterScopes((scope, implicants) => {
this._addScope(scope, implicants); this._addScope(scope, implicants);
}); });
} }
@ -322,7 +325,7 @@ class ScopeSet {
* than `B`. * than `B`.
* *
*/ */
contains(other) { contains(other: Coerceable) {
return !coerce(other)._searchScopes((scope, implicants) => { return !coerce(other)._searchScopes((scope, implicants) => {
return !this._hasSomeScope(implicants); return !this._hasSomeScope(implicants);
}); });
@ -336,14 +339,14 @@ class ScopeSet {
* some scope value in `A` that is implied by a scope value in `B`. * some scope value in `A` that is implied by a scope value in `B`.
* *
*/ */
intersects(other) { intersects(other: Parameters<typeof coerce>[number]) {
other = coerce(other); other = coerce(other);
return ( return (
other._searchImplicants((implicant) => { other._searchImplicants((implicant) => {
return this._hasScope(implicant); return this._hasScope(implicant);
}) || }) ||
this._searchImplicants((implicant) => { this._searchImplicants((implicant) => {
return other._hasScope(implicant); return (other as ScopeSet)._hasScope(implicant);
}) })
); );
} }
@ -375,11 +378,11 @@ class ScopeSet {
* directly a member of `A`, it will not appear in the result. * directly a member of `A`, it will not appear in the result.
* *
*/ */
filtered(other) { filtered(other: Coerceable) {
other = coerce(other); other = coerce(other);
const result = new ScopeSet(); const result = new ScopeSet();
this._iterScopes((scope, implicants) => { this._iterScopes((scope, implicants) => {
if (other._hasSomeScope(implicants)) { if ((other as ScopeSet)._hasSomeScope(implicants)) {
result._addScope(scope, implicants); result._addScope(scope, implicants);
} }
}); });
@ -396,11 +399,11 @@ class ScopeSet {
* `A.filtered(B)`. It returns a new `ScopeSet` object. * `A.filtered(B)`. It returns a new `ScopeSet` object.
* *
*/ */
difference(other) { difference(other: Coerceable) {
other = coerce(other); other = coerce(other);
const result = new ScopeSet(); const result = new ScopeSet();
this._iterScopes((scope, implicants) => { this._iterScopes((scope, implicants) => {
if (!other._hasSomeScope(implicants)) { if (!(other as ScopeSet)._hasSomeScope(implicants)) {
result._addScope(scope, implicants); result._addScope(scope, implicants);
} }
}); });
@ -415,7 +418,7 @@ class ScopeSet {
* either `A` or `B` (or both). It returns a new `ScopeSet` object. * either `A` or `B` (or both). It returns a new `ScopeSet` object.
* *
*/ */
union(other) { union(other: Coerceable) {
other = coerce(other); other = coerce(other);
const result = new ScopeSet(); const result = new ScopeSet();
this._iterScopes((scope, implicants) => { this._iterScopes((scope, implicants) => {
@ -433,7 +436,7 @@ class ScopeSet {
* kinds of input data into a `ScopeSet` instance. * kinds of input data into a `ScopeSet` instance.
* *
*/ */
function coerce(scopes) { function coerce(scopes: Coerceable) {
if (scopes instanceof ScopeSet) { if (scopes instanceof ScopeSet) {
return scopes; return scopes;
} }
@ -452,7 +455,7 @@ function coerce(scopes) {
* An iterator yielding all implicants of the given scope value. * An iterator yielding all implicants of the given scope value.
* *
*/ */
function getImplicantValues(value) { function getImplicantValues(value: string) {
if (value.startsWith('https:')) { if (value.startsWith('https:')) {
return getImplicantValuesForURLScope(value); return getImplicantValuesForURLScope(value);
} else { } else {
@ -478,7 +481,7 @@ function getImplicantValues(value) {
* only "write" scopes can imply other "write" scopes. * only "write" scopes can imply other "write" scopes.
* *
*/ */
function* getImplicantValuesForShortScope(value) { function* getImplicantValuesForShortScope(value: string) {
if (!VALID_SCOPE_VALUE.test(value)) { if (!VALID_SCOPE_VALUE.test(value)) {
throw new Error('Invalid scope value: ' + value); throw new Error('Invalid scope value: ' + value);
} }
@ -522,7 +525,7 @@ function* getImplicantValuesForShortScope(value) {
* URL is a child of another. * URL is a child of another.
* *
*/ */
function* getImplicantValuesForURLScope(value) { function* getImplicantValuesForURLScope(value: string) {
if (!VALID_SCOPE_VALUE.test(value)) { if (!VALID_SCOPE_VALUE.test(value)) {
throw new Error('Invalid scope value: ' + value); throw new Error('Invalid scope value: ' + value);
} }
@ -532,7 +535,7 @@ function* getImplicantValuesForURLScope(value) {
throw new Error('Invalid scope value: ' + value); throw new Error('Invalid scope value: ' + value);
} }
// No credentials or query params are allowed. // No credentials or query params are allowed.
if (url.username || url.password || url.query) { if (url.username || url.password || url.search) {
throw new Error('Invalid scope value: ' + value); throw new Error('Invalid scope value: ' + value);
} }
// The pathname must be non-empty and not end in a slash. // The pathname must be non-empty and not end in a slash.
@ -561,12 +564,12 @@ function* getImplicantValuesForURLScope(value) {
} }
} }
module.exports = { export const scopeSetHelpers = {
/** /**
* Parse a list of strings into a Scope object. * Parse a list of strings into a Scope object.
* *
*/ */
fromArray(scopesArray) { fromArray(scopesArray: string[]) {
return new ScopeSet(scopesArray); return new ScopeSet(scopesArray);
}, },
@ -578,7 +581,7 @@ module.exports = {
* case-sensitive strings identifying individual scope values. * case-sensitive strings identifying individual scope values.
* *
*/ */
fromString(scopesString) { fromString(scopesString: string) {
// Split the string by one or more space characters. // Split the string by one or more space characters.
return new ScopeSet( return new ScopeSet(
scopesString.split(/ +/).filter((scopeString) => { scopesString.split(/ +/).filter((scopeString) => {
@ -597,7 +600,9 @@ module.exports = {
* characters in the scopes will be expanded. * characters in the scopes will be expanded.
* *
*/ */
fromURLEncodedString(encodedScopesString) { fromURLEncodedString(
encodedScopesString: ReturnType<typeof encodeURIComponent>
) {
// Split the string by a literal plus character. // Split the string by a literal plus character.
return new ScopeSet( return new ScopeSet(
encodedScopesString encodedScopesString
@ -611,3 +616,5 @@ module.exports = {
); );
}, },
}; };
export default scopeSetHelpers;

Просмотреть файл

@ -5,7 +5,52 @@
"main": "dist/index.js", "main": "dist/index.js",
"exports": { "exports": {
".": "./dist/index.js", ".": "./dist/index.js",
"./": "./dist/" "./*": "./dist/*",
"./auth": "./dist/auth/index.js",
"./auth/*": "./dist/auth/*.js",
"./cache/*": "./dist/cache/*.js",
"./configuration/*": "./dist/configuration/*.js",
"./connected-services": "./dist/connected-services/index.js",
"./connected-services/*": "./dist/connected-services/*.js",
"./coverage/*": "./dist/coverage/*.js",
"./db": "./dist/db/index.js",
"./db/*": "./dist/db/*.js",
"./db/config": "./dist/db/config.js",
"./db/models": "./dist/db/models/index.js",
"./db/models/auth": "./dist/db/models/auth/index.js",
"./db/models/auth/*": "./dist/db/models/auth/*.js",
"./db/models/profile": "./dist/db/models/profile/index.js",
"./db/mysql": "./dist/db/mysql.js",
"./db/redis": "./dist/db/redis.js",
"./dto/*": "./dist/dto/*.js",
"./email/*": "./dist/email/*.js",
"./email/popularDomains.json": "./dist/email/popularDomains.json",
"./experiments/*": "./dist/experiments/*.js",
"./express": "./dist/express/index.js",
"./express/*": "./dist/express/*.js",
"./feature-flags": "./dist/feature-flags/index.js",
"./feature-flags/*": "./dist/feature-flags/*.js",
"./guards": "./dist/guards/index.js",
"./guards/*": "./dist/guards/*.js",
"./l10n/*": "./dist/l10n/*.js",
"./lib/*": "./dist/lib/*.js",
"./log": "./dist/log/index.js",
"./log/*": "./dist/log/*.js",
"./metrics/*": "./dist/metrics/*.js",
"./nestjs/*": "./dist/nestjs/*.js",
"./oauth/*": "./dist/oauth/*.js",
"./payments/*": "./dist/payments/*.js",
"./payments/configuration/*": "./dist/payments/configuration/*.js",
"./payments/iap/*": "./dist/payments/iap/*.js",
"./payments/iap/apple-app-store/types": "./dist/payments/iap/apple-app-store/types/index.js",
"./payments/iap/google-play/types": "./dist/payments/iap/google-play/types/index.js",
"./payments/stripe": "./dist/payments/stripe.js",
"./payments/stripe-firestore": "./dist/payments/stripe-firestore.js",
"./scripts/*": "./dist/scripts/*.js",
"./sentry": "./dist/sentry/index.js",
"./sentry/*": "./dist/sentry/*.js",
"./subscriptions/*": "./dist/subscriptions/*.js",
"./tracing/*": "./dist/tracing/*.js"
}, },
"scripts": { "scripts": {
"postinstall": "yarn build || true", "postinstall": "yarn build || true",
@ -43,6 +88,7 @@
"@types/accept-language-parser": "^1.5.3", "@types/accept-language-parser": "^1.5.3",
"@types/chai": "^4.2.18", "@types/chai": "^4.2.18",
"@types/chance": "^1.1.2", "@types/chance": "^1.1.2",
"@types/express": "^4.17.12",
"@types/generic-pool": "^3.1.9", "@types/generic-pool": "^3.1.9",
"@types/i18n-abide": "^0", "@types/i18n-abide": "^0",
"@types/ioredis": "^4.26.4", "@types/ioredis": "^4.26.4",
@ -106,8 +152,6 @@
"@sentry/browser": "^6.19.7", "@sentry/browser": "^6.19.7",
"@sentry/integrations": "^6.19.1", "@sentry/integrations": "^6.19.1",
"@sentry/node": "^6.19.1", "@sentry/node": "^6.19.1",
"@types/js-md5": "^0.4.2",
"@types/uuid": "^8.3.1",
"accept-language-parser": "^1.5.0", "accept-language-parser": "^1.5.0",
"ajv": "^8.11.0", "ajv": "^8.11.0",
"apollo-server": "^2.26.0", "apollo-server": "^2.26.0",
@ -120,6 +164,7 @@
"cldr-localenames-full": "42.0.0", "cldr-localenames-full": "42.0.0",
"cors": "^2.8.5", "cors": "^2.8.5",
"db-migrations": "workspace:*", "db-migrations": "workspace:*",
"express": "^4.17.2",
"find-up": "^5.0.0", "find-up": "^5.0.0",
"generic-pool": "^3.8.2", "generic-pool": "^3.8.2",
"graphql": "^15.6.1", "graphql": "^15.6.1",

Просмотреть файл

@ -5,7 +5,7 @@
'use strict'; 'use strict';
const { assert } = require('chai'); const { assert } = require('chai');
const remoteAddress = require('../../express/remote-address')(3); const remoteAddress = require('../../express/remote-address').remoteAddress(3);
describe('remote-address', () => { describe('remote-address', () => {
it('has the correct interface', () => { it('has the correct interface', () => {

Просмотреть файл

@ -34,7 +34,7 @@ describe('express/routing:', () => {
routingFactory = proxyquire('../../express/routing', { routingFactory = proxyquire('../../express/routing', {
celebrate: celebrateMock, celebrate: celebrateMock,
'./cors': () => corsHandler, './cors': { default: () => corsHandler },
}); });
appMock = { appMock = {
@ -49,7 +49,7 @@ describe('express/routing:', () => {
error: sinon.spy(), error: sinon.spy(),
}; };
routing = routingFactory(appMock, loggerMock); routing = routingFactory.default(appMock, loggerMock);
}); });
it('exposes the correct interface', () => { it('exposes the correct interface', () => {

Просмотреть файл

@ -15,7 +15,7 @@ describe('metrics/amplitude:', () => {
let amplitude; let amplitude;
before(() => { before(() => {
amplitude = require('../../metrics/amplitude'); amplitude = require('../../metrics/amplitude').amplitude;
}); });
it('exports the event groups', () => { it('exports the event groups', () => {

Просмотреть файл

@ -10,7 +10,7 @@ describe('oauth/scopes:', () => {
let scopes; let scopes;
before(() => { before(() => {
scopes = require('../../oauth/scopes'); scopes = require('../../oauth/scopes').default;
}); });
describe('valid implications', () => { describe('valid implications', () => {

Просмотреть файл

@ -25723,6 +25723,7 @@ fsevents@~2.1.1:
"@types/accept-language-parser": ^1.5.3 "@types/accept-language-parser": ^1.5.3
"@types/chai": ^4.2.18 "@types/chai": ^4.2.18
"@types/chance": ^1.1.2 "@types/chance": ^1.1.2
"@types/express": ^4.17.12
"@types/generic-pool": ^3.1.9 "@types/generic-pool": ^3.1.9
"@types/i18n-abide": ^0 "@types/i18n-abide": ^0
"@types/ioredis": ^4.26.4 "@types/ioredis": ^4.26.4
@ -25757,6 +25758,7 @@ fsevents@~2.1.1:
esbuild-register: ^3.2.0 esbuild-register: ^3.2.0
eslint: ^7.32.0 eslint: ^7.32.0
eslint-plugin-fxa: ^2.0.2 eslint-plugin-fxa: ^2.0.2
express: ^4.17.2
find-up: ^5.0.0 find-up: ^5.0.0
generic-pool: ^3.8.2 generic-pool: ^3.8.2
graphql: ^15.6.1 graphql: ^15.6.1