зеркало из https://github.com/mozilla/fxa.git
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:
Коммит
d81e6cb196
|
@ -52,7 +52,7 @@
|
|||
}
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.js": [
|
||||
"packages/!(fxa-payments-server)/**/*.js": [
|
||||
"prettier --config _dev/.prettierrc --write",
|
||||
"eslint"
|
||||
],
|
||||
|
|
|
@ -8,7 +8,7 @@ import path from 'path';
|
|||
import mysql from 'mysql';
|
||||
import patcher from 'mysql-patcher';
|
||||
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 conf = convict({
|
||||
|
|
|
@ -15,7 +15,8 @@
|
|||
const { Container } = require('typedi');
|
||||
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');
|
||||
|
||||
// Maps template name to email type
|
||||
|
|
|
@ -1175,7 +1175,7 @@ export class AccountHandler {
|
|||
scope = { contains: () => true };
|
||||
} else {
|
||||
uid = auth.credentials.user;
|
||||
scope = ScopeSet.fromArray(auth.credentials.scope);
|
||||
scope = ScopeSet.fromArray(auth.credentials.scope!);
|
||||
}
|
||||
|
||||
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 oauthDB = require('../../oauth/db');
|
||||
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,
|
||||
// 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 validators = require('./validators');
|
||||
const ScopeSet = require('fxa-shared/oauth/scopes');
|
||||
const ScopeSet = require('fxa-shared/oauth/scopes').scopeSetHelpers;
|
||||
const AppError = require('../../lib/error');
|
||||
const Joi = require('joi');
|
||||
|
||||
|
|
|
@ -101,7 +101,7 @@ export class AppleIapHandler {
|
|||
// once VPN migration is complete (FXA-5848).
|
||||
'profile:subscriptions',
|
||||
];
|
||||
const scope = ScopeSet.fromArray(auth.credentials.scope);
|
||||
const scope = ScopeSet.fromArray(auth.credentials.scope!);
|
||||
if (!scopes.some((requiredScope) => scope.contains(requiredScope))) {
|
||||
throw error.invalidScopes();
|
||||
}
|
||||
|
|
|
@ -15,7 +15,7 @@ export async function handleAuth(
|
|||
auth: AuthRequest['auth'],
|
||||
fetchEmail = false
|
||||
) {
|
||||
const scope = ScopeSet.fromArray(auth.credentials.scope);
|
||||
const scope = ScopeSet.fromArray(auth.credentials.scope!);
|
||||
if (!scope.contains(OAUTH_SCOPE_SUBSCRIPTIONS)) {
|
||||
throw error.invalidScopes();
|
||||
}
|
||||
|
@ -36,7 +36,7 @@ export async function handleAuth(
|
|||
}
|
||||
|
||||
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)) {
|
||||
throw error.invalidScopes();
|
||||
}
|
||||
|
@ -44,7 +44,7 @@ export function handleUidAuth(auth: AuthRequest['auth']): 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) {
|
||||
if (!scope.contains(requiredScope)) {
|
||||
throw error.invalidScopes();
|
||||
|
|
|
@ -8,7 +8,7 @@ const sinon = require('sinon');
|
|||
const assert = { ...sinon.assert, ...require('chai').assert };
|
||||
const getRoute = require('../../routes_helpers').getRoute;
|
||||
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 { INVALID_PARAMETER, MISSING_PARAMETER } = error.ERRNO;
|
||||
|
|
|
@ -34,13 +34,13 @@ const serveStatic = require('serve-static');
|
|||
|
||||
const sentry = require('../lib/sentry');
|
||||
const statsd = require('../lib/statsd');
|
||||
const { cors, routing } = require('fxa-shared/express')();
|
||||
const { cors, routing } = require('fxa-shared/express').express();
|
||||
const {
|
||||
useSettingsProxy,
|
||||
modifySettingsStatic,
|
||||
} = require('../lib/beta-settings');
|
||||
|
||||
const userAgent = require('fxa-shared/metrics/user-agent');
|
||||
const userAgent = require('fxa-shared/metrics/user-agent').default;
|
||||
if (!userAgent.isToVersionStringSupported()) {
|
||||
// npm@3 installs the incorrect version of node-uap, one without `toVersionString`.
|
||||
// To ensure the correct version is installed, check toVersionString is available.
|
||||
|
|
|
@ -23,9 +23,9 @@ const {
|
|||
mapLocation,
|
||||
mapOs,
|
||||
validate,
|
||||
} = require('fxa-shared/metrics/amplitude');
|
||||
} = require('fxa-shared/metrics/amplitude').amplitude;
|
||||
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 { version: VERSION } = require('../../package.json');
|
||||
|
||||
|
|
|
@ -10,20 +10,17 @@ const flowMetrics = require('./flow-metrics');
|
|||
const log = require('./logging/log')('server.flow-event');
|
||||
const geodbConfig = config.get('geodb');
|
||||
const geodb = require('fxa-geodb')(geodbConfig);
|
||||
const remoteAddress = require('fxa-shared/express/remote-address')(
|
||||
config.get('clientAddressDepth')
|
||||
);
|
||||
const geolocate = require('fxa-shared/express/geo-locate')(geodb)(
|
||||
const remoteAddress =
|
||||
require('fxa-shared/express/remote-address').remoteAddress(
|
||||
config.get('clientAddressDepth')
|
||||
);
|
||||
const geolocate = require('fxa-shared/express/geo-locate').geolocate(geodb)(
|
||||
remoteAddress
|
||||
)(log);
|
||||
const os = require('os');
|
||||
const statsd = require('./statsd');
|
||||
const {
|
||||
VERSION,
|
||||
PERFORMANCE_TIMINGS,
|
||||
limitLength,
|
||||
isValidTime,
|
||||
} = require('fxa-shared').metrics.flowPerformance;
|
||||
const { VERSION, PERFORMANCE_TIMINGS, limitLength, isValidTime } =
|
||||
require('fxa-shared').metrics.flowPerformance;
|
||||
|
||||
const VALIDATION_PATTERNS = require('./validation').PATTERNS;
|
||||
const DNT_ALLOWED_DATA = ['context', 'entrypoint', 'service'];
|
||||
|
|
|
@ -10,9 +10,10 @@
|
|||
const logger = require('./log')('server.requests');
|
||||
const morgan = require('morgan');
|
||||
const config = require('../configuration').getProperties();
|
||||
const remoteAddress = require('fxa-shared/express/remote-address')(
|
||||
config.clientAddressDepth
|
||||
);
|
||||
const remoteAddress =
|
||||
require('fxa-shared/express/remote-address').remoteAddress(
|
||||
config.clientAddressDepth
|
||||
);
|
||||
|
||||
/**
|
||||
* 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 geodbConfig = config.get('geodb');
|
||||
const geodb = require('fxa-geodb')(geodbConfig);
|
||||
const remoteAddress = require('fxa-shared/express/remote-address')(
|
||||
config.get('clientAddressDepth')
|
||||
);
|
||||
const geolocate = require('fxa-shared/express/geo-locate')(geodb)(
|
||||
const remoteAddress =
|
||||
require('fxa-shared/express/remote-address').remoteAddress(
|
||||
config.get('clientAddressDepth')
|
||||
);
|
||||
const geolocate = require('fxa-shared/express/geo-locate').geolocate(geodb)(
|
||||
remoteAddress
|
||||
)(logger);
|
||||
const uuid = require('node-uuid');
|
||||
|
|
|
@ -38,7 +38,12 @@ const amplitude = proxyquire(path.resolve('server/lib/amplitude'), {
|
|||
},
|
||||
},
|
||||
'./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,
|
||||
});
|
||||
const Sentry = require('@sentry/node');
|
||||
|
|
|
@ -50,7 +50,7 @@ registerSuite('flow-event', {
|
|||
'./amplitude': mocks.amplitude,
|
||||
'./configuration': mocks.config,
|
||||
'./flow-metrics': mocks.flowMetrics,
|
||||
'fxa-shared/express/geo-locate': mocks.geolocate,
|
||||
'fxa-shared/express/geo-locate': { geolocate: mocks.geolocate },
|
||||
'./statsd': mocks.statsd,
|
||||
}).metricsRequest;
|
||||
},
|
||||
|
|
|
@ -53,7 +53,7 @@ registerSuite('routes/get-metrics-flow', {
|
|||
route = proxyquire('../../../server/lib/routes/get-metrics-flow', {
|
||||
'../amplitude': mocks.amplitude,
|
||||
'../flow-event': mocks.flowEvent,
|
||||
'fxa-shared/express/geo-locate': mocks.geolocate,
|
||||
'fxa-shared/express/geo-locate': { geolocate: mocks.geolocate },
|
||||
'../logging/log': () => mocks.log,
|
||||
});
|
||||
instance = route(mocks.config);
|
||||
|
|
|
@ -11,7 +11,7 @@ const {
|
|||
mapOs,
|
||||
toSnakeCase,
|
||||
validate,
|
||||
} = require('fxa-shared/metrics/amplitude');
|
||||
} = require('fxa-shared/metrics/amplitude').amplitude;
|
||||
const config = require('../config');
|
||||
const amplitude = config.get('amplitude');
|
||||
const log = require('./logging/log')();
|
||||
|
|
|
@ -9,9 +9,11 @@ const mockAmplitudeConfig = {
|
|||
schemaValidation: true,
|
||||
rawEvents: false,
|
||||
};
|
||||
jest.mock('fxa-shared/metrics/amplitude.js', () => ({
|
||||
...jest.requireActual('fxa-shared/metrics/amplitude.js'),
|
||||
validate: mockSchemaValidatorFn,
|
||||
jest.mock('fxa-shared/metrics/amplitude', () => ({
|
||||
amplitude: {
|
||||
...jest.requireActual('fxa-shared/metrics/amplitude').amplitude,
|
||||
validate: mockSchemaValidatorFn,
|
||||
},
|
||||
}));
|
||||
let scope;
|
||||
const mockSentry = {
|
||||
|
|
|
@ -7,9 +7,10 @@
|
|||
const logger = require('./log')('server.requests');
|
||||
const morgan = require('morgan');
|
||||
const config = require('../../config');
|
||||
const remoteAddress = require('fxa-shared/express/remote-address')(
|
||||
config.get('clientAddressDepth')
|
||||
);
|
||||
const remoteAddress =
|
||||
require('fxa-shared/express/remote-address').remoteAddress(
|
||||
config.get('clientAddressDepth')
|
||||
);
|
||||
|
||||
const { enabled, format } = config.get('logging.routes');
|
||||
|
||||
|
|
|
@ -31,7 +31,7 @@ module.exports = () => {
|
|||
const csp = require('../lib/csp');
|
||||
const cspRulesBlocking = require('../lib/csp/blocking')(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 {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
/* 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 * as ScopeSet from '../oauth/scopes';
|
||||
import ScopeSet from '../oauth/scopes';
|
||||
import { IClientFormatter } from './formatters';
|
||||
import {
|
||||
AttachedClient,
|
||||
|
|
|
@ -7,7 +7,7 @@ import mysql from 'mysql';
|
|||
|
||||
import { AccessToken as AccessToken } from '../db/models/auth/access-token';
|
||||
import { ILogger } from '../log';
|
||||
import * as ScopeSet from '../oauth/scopes';
|
||||
import ScopeSet from '../oauth/scopes';
|
||||
|
||||
// TODO: Improve types. Ported form javascript...
|
||||
const buf = require('buf').hex;
|
||||
|
|
|
@ -2,4 +2,6 @@
|
|||
* 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/. */
|
||||
|
||||
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
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
module.exports = () => {
|
||||
const cors = require('./cors');
|
||||
const routing = require('./routing');
|
||||
import cors from './cors';
|
||||
import routing from './routing';
|
||||
|
||||
return {
|
||||
cors,
|
||||
routing,
|
||||
};
|
||||
};
|
||||
export const express = () => ({
|
||||
cors,
|
||||
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
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
module.exports = (app, logger) => {
|
||||
const cors = require('./cors');
|
||||
import express from 'express';
|
||||
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 {
|
||||
celebrate,
|
||||
isCelebrateError: isValidationError,
|
||||
|
@ -26,7 +47,7 @@ module.exports = (app, logger) => {
|
|||
* @param {Object} [routeDefinition.validate] declare JOI validation.
|
||||
* Follows [celebrate](https://www.npmjs.com/package/celebrate) conventions.
|
||||
*/
|
||||
addRoute(routeDefinition) {
|
||||
addRoute(routeDefinition: RouteDefinition) {
|
||||
if (!isValidRouteDefinition(routeDefinition)) {
|
||||
logger.error('route definition invalid: ', routeDefinition);
|
||||
throw new Error('Invalid route definition');
|
||||
|
@ -45,7 +66,10 @@ module.exports = (app, logger) => {
|
|||
: undefined;
|
||||
// Enable the pre-flight OPTIONS request
|
||||
const corsHandler = cors(corsConfig);
|
||||
app.options(routeDefinition.path, corsHandler);
|
||||
app.options(
|
||||
routeDefinition.path as string,
|
||||
corsHandler as express.RequestHandler
|
||||
);
|
||||
routeHandlers.push(corsHandler);
|
||||
}
|
||||
|
||||
|
@ -63,10 +87,18 @@ module.exports = (app, logger) => {
|
|||
}
|
||||
|
||||
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)) {
|
||||
logger.error('validation.failed', {
|
||||
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;
|
||||
}
|
||||
};
|
||||
|
||||
export default routing;
|
|
@ -6,6 +6,7 @@ import * as invoice from './dto/auth/payments/invoice';
|
|||
import * as emailHelpers from './email/helpers';
|
||||
import popularDomains from './email/popularDomains.json';
|
||||
import BaseGroupingRule from './experiments/base';
|
||||
import express from './express';
|
||||
import featureFlags from './feature-flags';
|
||||
import { localizeTimestamp } from './l10n/localizeTimestamp';
|
||||
import supportedLanguages from './l10n/supportedLanguages.json';
|
||||
|
@ -29,6 +30,7 @@ module.exports = {
|
|||
experiments: {
|
||||
BaseGroupingRule,
|
||||
},
|
||||
express,
|
||||
featureFlags,
|
||||
l10n: {
|
||||
localizeTimestamp,
|
||||
|
|
|
@ -2,9 +2,19 @@
|
|||
* 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/. */
|
||||
|
||||
'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 amplitudeSchema = require('./amplitude-event.1.schema.json');
|
||||
const validateAmplitudeEvent = ajv.compile(amplitudeSchema);
|
||||
|
@ -72,11 +82,17 @@ const EVENT_PROPERTIES = {
|
|||
|
||||
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];
|
||||
|
||||
if (connect_device_flow) {
|
||||
const result = { connect_device_flow };
|
||||
const result: { connect_device_flow: string; connect_device_os?: string } =
|
||||
{ connect_device_flow };
|
||||
|
||||
if (eventTarget) {
|
||||
result.connect_device_os = eventTarget;
|
||||
|
@ -84,13 +100,20 @@ function mapConnectDeviceFlow(eventType, eventCategory, eventTarget) {
|
|||
|
||||
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];
|
||||
|
||||
if (email_type) {
|
||||
const result = {
|
||||
const result: { [key: string]: string } = {
|
||||
email_type,
|
||||
email_provider: data.emailDomain,
|
||||
};
|
||||
|
@ -103,41 +126,47 @@ function mapEmailType(eventType, eventCategory, eventTarget, data) {
|
|||
|
||||
return result;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
function mapSettingsEventProperties(...args) {
|
||||
function mapSettingsEventProperties(...args: [string, string]) {
|
||||
return {
|
||||
...mapDisconnectReason(...args),
|
||||
};
|
||||
}
|
||||
|
||||
function mapDisconnectReason(eventType, eventCategory) {
|
||||
function mapDisconnectReason(eventType: string, eventCategory: string) {
|
||||
if (eventType === 'disconnect_device' && eventCategory) {
|
||||
return { reason: eventCategory };
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
function mapDomainValidationResult(
|
||||
eventType,
|
||||
eventCategory,
|
||||
eventTarget,
|
||||
data
|
||||
eventType: string,
|
||||
eventCategory: string,
|
||||
eventTarget: string,
|
||||
data: EventData
|
||||
) {
|
||||
// This function is called for all fxa_reg event types, only add the event
|
||||
// properties for the results pertaining to domain_validation_result.
|
||||
if (eventType === 'domain_validation_result' && eventCategory) {
|
||||
return { validation_result: eventCategory };
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
function mapSubscriptionUpgradeEventProperties(
|
||||
eventType,
|
||||
eventCategory,
|
||||
eventTarget,
|
||||
data
|
||||
eventType: string,
|
||||
eventCategory: string,
|
||||
eventTarget: string,
|
||||
data: EventData
|
||||
) {
|
||||
if (data) {
|
||||
const properties = {};
|
||||
const properties: { [key: string]: string } = {};
|
||||
|
||||
if (data.previousPlanId) {
|
||||
properties['previous_plan_id'] = data.previousPlanId;
|
||||
|
@ -149,16 +178,18 @@ function mapSubscriptionUpgradeEventProperties(
|
|||
|
||||
return properties;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
function mapSubscriptionPaymentEventProperties(
|
||||
eventType,
|
||||
eventCategory,
|
||||
eventTarget,
|
||||
data
|
||||
eventType: string,
|
||||
eventCategory: string,
|
||||
eventTarget: string,
|
||||
data: EventData
|
||||
) {
|
||||
if (data) {
|
||||
const properties = {};
|
||||
const properties: { [key: string]: string } = {};
|
||||
|
||||
if (data.sourceCountry) {
|
||||
properties['source_country'] = data.sourceCountry;
|
||||
|
@ -174,9 +205,11 @@ function mapSubscriptionPaymentEventProperties(
|
|||
|
||||
return properties;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function validate(event) {
|
||||
function validate(event: { [key: string]: any }) {
|
||||
if (!validateAmplitudeEvent(event)) {
|
||||
throw new Error(
|
||||
`Invalid data: ${ajv.errorsText(validateAmplitudeEvent.errors, {
|
||||
|
@ -187,7 +220,7 @@ function validate(event) {
|
|||
return true;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
export const amplitude = {
|
||||
EVENT_PROPERTIES,
|
||||
GROUPS,
|
||||
mapBrowser,
|
||||
|
@ -227,7 +260,23 @@ module.exports = {
|
|||
*
|
||||
* @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.
|
||||
*
|
||||
|
@ -242,7 +291,7 @@ module.exports = {
|
|||
* numerous to list here, but may be discerned with
|
||||
* ease by perusing the code.
|
||||
*/
|
||||
return (event, data) => {
|
||||
return (event: { [key: string]: any }, data: EventData) => {
|
||||
if (!event || !data) {
|
||||
return;
|
||||
}
|
||||
|
@ -288,6 +337,7 @@ module.exports = {
|
|||
|
||||
let version;
|
||||
try {
|
||||
// @ts-ignore
|
||||
version = /([0-9]+)\.([0-9]+)$/.exec(data.version)[0];
|
||||
} catch (err) {}
|
||||
|
||||
|
@ -317,22 +367,28 @@ module.exports = {
|
|||
device_model: data.formFactor,
|
||||
event_properties: mapEventProperties(
|
||||
eventType,
|
||||
eventGroup,
|
||||
eventCategory,
|
||||
eventTarget,
|
||||
eventGroup as string,
|
||||
eventCategory as string,
|
||||
eventTarget as string,
|
||||
data
|
||||
),
|
||||
user_properties: mapUserProperties(
|
||||
eventGroup as string,
|
||||
eventCategory as string,
|
||||
data
|
||||
),
|
||||
user_properties: mapUserProperties(eventGroup, eventCategory, data),
|
||||
});
|
||||
}
|
||||
|
||||
return;
|
||||
};
|
||||
|
||||
function mapEventProperties(
|
||||
eventType,
|
||||
eventGroup,
|
||||
eventCategory,
|
||||
eventTarget,
|
||||
data
|
||||
eventType: string,
|
||||
eventGroup: string,
|
||||
eventCategory: string,
|
||||
eventTarget: string,
|
||||
data: EventData
|
||||
) {
|
||||
const { serviceName, clientId } = getServiceNameAndClientId(data);
|
||||
|
||||
|
@ -356,7 +412,7 @@ module.exports = {
|
|||
);
|
||||
}
|
||||
|
||||
function getServiceNameAndClientId(data) {
|
||||
function getServiceNameAndClientId(data: EventData) {
|
||||
let serviceName, clientId;
|
||||
|
||||
const { service } = data;
|
||||
|
@ -372,7 +428,11 @@ module.exports = {
|
|||
return { serviceName, clientId };
|
||||
}
|
||||
|
||||
function mapUserProperties(eventGroup, eventCategory, data) {
|
||||
function mapUserProperties(
|
||||
eventGroup: string,
|
||||
eventCategory: string,
|
||||
data: EventData
|
||||
) {
|
||||
return Object.assign(
|
||||
pruneUnsetValues({
|
||||
entrypoint: data.entrypoint,
|
||||
|
@ -395,7 +455,7 @@ module.exports = {
|
|||
);
|
||||
}
|
||||
|
||||
function mapAppendProperties(data) {
|
||||
function mapAppendProperties(data: EventData) {
|
||||
const servicesUsed = mapServicesUsed(data);
|
||||
const experiments = mapExperiments(data);
|
||||
const userPreferences = mapUserPreferences(data);
|
||||
|
@ -410,9 +470,11 @@ module.exports = {
|
|||
),
|
||||
};
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
function mapServicesUsed(data) {
|
||||
function mapServicesUsed(data: EventData) {
|
||||
const { serviceName } = getServiceNameAndClientId(data);
|
||||
|
||||
if (serviceName) {
|
||||
|
@ -420,12 +482,14 @@ module.exports = {
|
|||
fxa_services_used: serviceName,
|
||||
};
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
function pruneUnsetValues(data) {
|
||||
const result = {};
|
||||
function pruneUnsetValues(data: EventData) {
|
||||
const result: Partial<EventData> = {};
|
||||
|
||||
Object.keys(data).forEach((key) => {
|
||||
const value = data[key];
|
||||
|
@ -438,7 +502,7 @@ function pruneUnsetValues(data) {
|
|||
return result;
|
||||
}
|
||||
|
||||
function mapExperiments(data) {
|
||||
function mapExperiments(data: EventData) {
|
||||
const { experiments } = data;
|
||||
|
||||
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;
|
||||
|
||||
// Don't send user preferences metric if there are none!
|
||||
|
@ -458,7 +524,7 @@ function mapUserPreferences(data) {
|
|||
return;
|
||||
}
|
||||
|
||||
const formattedUserPreferences = {};
|
||||
const formattedUserPreferences: { [key: string]: any } = {};
|
||||
for (const pref in userPreferences) {
|
||||
formattedUserPreferences[toSnakeCase(pref)] = userPreferences[pref];
|
||||
}
|
||||
|
@ -466,7 +532,7 @@ function mapUserPreferences(data) {
|
|||
return formattedUserPreferences;
|
||||
}
|
||||
|
||||
function toSnakeCase(string) {
|
||||
function toSnakeCase(string: string) {
|
||||
return string
|
||||
.replace(/([a-z])([A-Z])/g, (s, c1, c2) => `${c1}_${c2.toLowerCase()}`)
|
||||
.replace(/([A-Z])/g, (c) => c.toLowerCase())
|
||||
|
@ -474,79 +540,102 @@ function toSnakeCase(string) {
|
|||
.replace(/-/g, '_');
|
||||
}
|
||||
|
||||
function mapSyncDevices(data) {
|
||||
function mapSyncDevices(data: EventData) {
|
||||
const { devices } = data;
|
||||
|
||||
if (Array.isArray(devices)) {
|
||||
return {
|
||||
sync_device_count: devices.length,
|
||||
sync_active_devices_day: countDevices(devices, DAY),
|
||||
sync_active_devices_week: countDevices(devices, WEEK),
|
||||
sync_active_devices_month: countDevices(devices, FOUR_WEEKS),
|
||||
sync_active_devices_day: countDevices(
|
||||
devices as [{ [key: string]: any }],
|
||||
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(
|
||||
(device) => device.lastAccessTime >= Date.now() - period
|
||||
).length;
|
||||
}
|
||||
|
||||
function mapSyncEngines(data) {
|
||||
function mapSyncEngines(data: EventData) {
|
||||
const { syncEngines: sync_engines } = data;
|
||||
|
||||
if (Array.isArray(sync_engines) && sync_engines.length > 0) {
|
||||
return { sync_engines };
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
function mapNewsletters(data) {
|
||||
function mapNewsletters(data: EventData) {
|
||||
let { newsletters } = data;
|
||||
if (newsletters) {
|
||||
newsletters = newsletters.map((newsletter) => {
|
||||
newsletters = newsletters.map((newsletter: string) => {
|
||||
return toSnakeCase(newsletter);
|
||||
});
|
||||
return { newsletters, newsletter_state: 'subscribed' };
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
function mapBrowser(userAgent) {
|
||||
function mapBrowser(userAgent: ParsedUserAgentProperties) {
|
||||
return mapUserAgentProperties(userAgent, 'ua', 'browser', 'browserVersion');
|
||||
}
|
||||
|
||||
function mapOs(userAgent) {
|
||||
function mapOs(userAgent: ParsedUserAgentProperties) {
|
||||
return mapUserAgentProperties(userAgent, 'os', 'os', 'osVersion');
|
||||
}
|
||||
|
||||
function mapUserAgentProperties(
|
||||
userAgent,
|
||||
key,
|
||||
familyProperty,
|
||||
versionProperty
|
||||
userAgent: ParsedUserAgentProperties,
|
||||
key: keyof ParsedUserAgentProperties,
|
||||
familyProperty: string,
|
||||
versionProperty: string
|
||||
) {
|
||||
const group = userAgent[key];
|
||||
const { family } = group;
|
||||
if (family && family !== 'Other') {
|
||||
return {
|
||||
[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;
|
||||
if (brand && formFactor && brand !== 'Generic') {
|
||||
return { formFactor };
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
function mapLocation(location) {
|
||||
function mapLocation(location: Location) {
|
||||
if (location && (location.country || location.state)) {
|
||||
return {
|
||||
country: location.country,
|
||||
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
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
'use strict';
|
||||
|
||||
const MAX_DATA_LENGTH = 100;
|
||||
const VERSION = 1;
|
||||
const PERFORMANCE_TIMINGS = [
|
||||
export const VERSION = 1;
|
||||
export const PERFORMANCE_TIMINGS = [
|
||||
// These timings are only an approximation, to be used as extra signals
|
||||
// when looking for correlations in the flow data. They're not perfect
|
||||
// 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) {
|
||||
return data.substr(0, MAX_DATA_LENGTH);
|
||||
}
|
||||
|
@ -45,7 +43,11 @@ function limitLength(data) {
|
|||
return data;
|
||||
}
|
||||
|
||||
function isValidTime(time, requestReceivedTime, expiry) {
|
||||
export function isValidTime(
|
||||
time: any,
|
||||
requestReceivedTime: number,
|
||||
expiry: number
|
||||
) {
|
||||
if (typeof time !== 'number') {
|
||||
return false;
|
||||
}
|
||||
|
@ -58,9 +60,4 @@ function isValidTime(time, requestReceivedTime, expiry) {
|
|||
return true;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
VERSION,
|
||||
PERFORMANCE_TIMINGS,
|
||||
limitLength,
|
||||
isValidTime,
|
||||
};
|
||||
export default { 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();
|
||||
// Validate the subset of PerformanceNavgationTiming properties used here.
|
||||
// (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),
|
||||
domComplete: TIMESTAMP_MS.min(joi.ref('domInteractive')),
|
||||
domInteractive: TIMESTAMP_MS.min(joi.ref('responseEnd')),
|
||||
|
@ -15,3 +19,5 @@ module.exports.navigationTimingSchema = joi.object().keys({
|
|||
responseEnd: TIMESTAMP_MS.min(joi.ref('responseStart')),
|
||||
responseStart: TIMESTAMP_MS.min(joi.ref('requestStart')),
|
||||
});
|
||||
|
||||
export default { navigationTimingSchema };
|
|
@ -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
|
||||
* 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
|
||||
// leaking back to the result data.
|
||||
|
||||
'use strict';
|
||||
|
||||
const ua = require('node-uap');
|
||||
// @ts-ignore
|
||||
import * as ua from 'node-uap';
|
||||
|
||||
// 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.
|
||||
|
@ -15,7 +35,9 @@ const VALID_FAMILY = /^[\w ]{1,32}$/;
|
|||
|
||||
const VALID_VERSION = /^[\w.]{1,16}$/;
|
||||
|
||||
exports.parse = (userAgentString) => {
|
||||
export const parse = (
|
||||
userAgentString: string | undefined
|
||||
): ParsedUserAgentProperties => {
|
||||
const result = ua.parse(userAgentString);
|
||||
|
||||
safeFamily(result.ua);
|
||||
|
@ -27,7 +49,9 @@ exports.parse = (userAgentString) => {
|
|||
return result;
|
||||
};
|
||||
|
||||
exports.isToVersionStringSupported = (result) => {
|
||||
export const isToVersionStringSupported = (
|
||||
result: ParsedUserAgentProperties
|
||||
): boolean => {
|
||||
if (!result) {
|
||||
result = exports.parse(
|
||||
'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;
|
||||
};
|
||||
|
||||
function safeFamily(parent) {
|
||||
if (!VALID_FAMILY.test(parent.family)) {
|
||||
function safeFamily(parent: ParsedUa) {
|
||||
if (!VALID_FAMILY.test(parent.family as string)) {
|
||||
parent.family = null;
|
||||
}
|
||||
}
|
||||
|
||||
function safeVersion(parent) {
|
||||
function safeVersion(parent: ParsedOs) {
|
||||
if (
|
||||
parent &&
|
||||
parent.toVersionString &&
|
||||
|
@ -60,3 +84,5 @@ function safeVersion(parent) {
|
|||
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
|
||||
* 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,
|
||||
// https://tools.ietf.org/html/rfc6749#section-3.3
|
||||
|
@ -75,7 +75,10 @@ const VALID_FRAGMENT_VALUE = /^#[a-zA-Z0-9_]+$/;
|
|||
*
|
||||
*/
|
||||
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
|
||||
// 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.
|
||||
*
|
||||
*/
|
||||
_hasScope(scope) {
|
||||
_hasScope(scope: string) {
|
||||
return scope in this._scopesToImplicants;
|
||||
}
|
||||
|
||||
|
@ -111,7 +114,7 @@ class ScopeSet {
|
|||
* Check whether this `ScopeSet` contains one of the given scopes.
|
||||
*
|
||||
*/
|
||||
_hasSomeScope(scopes) {
|
||||
_hasSomeScope(scopes: Set<string>) {
|
||||
for (const scope of scopes) {
|
||||
if (this._hasScope(scope)) {
|
||||
return true;
|
||||
|
@ -125,7 +128,7 @@ class ScopeSet {
|
|||
* the given scope.
|
||||
*
|
||||
*/
|
||||
_hasImplicant(scope) {
|
||||
_hasImplicant(scope: string) {
|
||||
return scope in this._implicantsToScopes;
|
||||
}
|
||||
|
||||
|
@ -134,7 +137,7 @@ class ScopeSet {
|
|||
* one of the given scopes.
|
||||
*
|
||||
*/
|
||||
_hasSomeImplicant(scopes) {
|
||||
_hasSomeImplicant(scopes: string[]) {
|
||||
for (const scope of scopes) {
|
||||
if (scope in this._implicantsToScopes) {
|
||||
return true;
|
||||
|
@ -148,7 +151,7 @@ class ScopeSet {
|
|||
* corresponding sets of implicants.
|
||||
*
|
||||
*/
|
||||
_iterScopes(cb) {
|
||||
_iterScopes(cb: (scope: string, implicants: Set<string>) => void) {
|
||||
for (const scope in this._scopesToImplicants) {
|
||||
cb(scope, this._scopesToImplicants[scope]);
|
||||
}
|
||||
|
@ -159,7 +162,7 @@ class ScopeSet {
|
|||
* corresponding sets of implied scopes.
|
||||
*
|
||||
*/
|
||||
_iterImplicants(cb) {
|
||||
_iterImplicants(cb: (implicant: string, scopes: Set<string>) => void) {
|
||||
for (const implicant in this._implicantsToScopes) {
|
||||
cb(implicant, this._implicantsToScopes[implicant]);
|
||||
}
|
||||
|
@ -170,7 +173,7 @@ class ScopeSet {
|
|||
* the given scope.
|
||||
*
|
||||
*/
|
||||
_iterImpliedScopes(implicant, cb) {
|
||||
_iterImpliedScopes(implicant: string, cb: (s: string) => void) {
|
||||
const impliedScopes = this._implicantsToScopes[implicant];
|
||||
if (impliedScopes) {
|
||||
for (const impliedScope of impliedScopes) {
|
||||
|
@ -185,7 +188,7 @@ class ScopeSet {
|
|||
* at the first successful match.
|
||||
*
|
||||
*/
|
||||
_searchScopes(cb) {
|
||||
_searchScopes(cb: (scope: string, implicants: Set<string>) => boolean) {
|
||||
for (const scope in this._scopesToImplicants) {
|
||||
if (cb(scope, this._scopesToImplicants[scope])) {
|
||||
return true;
|
||||
|
@ -200,7 +203,7 @@ class ScopeSet {
|
|||
* at the first successful match.
|
||||
*
|
||||
*/
|
||||
_searchImplicants(cb) {
|
||||
_searchImplicants(cb: (implicant: string, scopes: Set<string>) => boolean) {
|
||||
for (const implicant in this._implicantsToScopes) {
|
||||
if (cb(implicant, this._implicantsToScopes[implicant])) {
|
||||
return true;
|
||||
|
@ -216,7 +219,7 @@ class ScopeSet {
|
|||
* 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`,
|
||||
// then we can safely ignore it.
|
||||
if (this._hasSomeScope(implicants)) {
|
||||
|
@ -244,7 +247,7 @@ class ScopeSet {
|
|||
* scopes and their implicants.
|
||||
*
|
||||
*/
|
||||
_removeScope(scope) {
|
||||
_removeScope(scope: string) {
|
||||
const implicants = this._scopesToImplicants[scope];
|
||||
for (const implicant of implicants) {
|
||||
const impliedScopes = this._implicantsToScopes[implicant];
|
||||
|
@ -307,8 +310,8 @@ class ScopeSet {
|
|||
* quadratic" performance traps.
|
||||
*
|
||||
*/
|
||||
add(other) {
|
||||
other = coerce(other)._iterScopes((scope, implicants) => {
|
||||
add(other: Coerceable) {
|
||||
coerce(other)._iterScopes((scope, implicants) => {
|
||||
this._addScope(scope, implicants);
|
||||
});
|
||||
}
|
||||
|
@ -322,7 +325,7 @@ class ScopeSet {
|
|||
* than `B`.
|
||||
*
|
||||
*/
|
||||
contains(other) {
|
||||
contains(other: Coerceable) {
|
||||
return !coerce(other)._searchScopes((scope, 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`.
|
||||
*
|
||||
*/
|
||||
intersects(other) {
|
||||
intersects(other: Parameters<typeof coerce>[number]) {
|
||||
other = coerce(other);
|
||||
return (
|
||||
other._searchImplicants((implicant) => {
|
||||
return this._hasScope(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.
|
||||
*
|
||||
*/
|
||||
filtered(other) {
|
||||
filtered(other: Coerceable) {
|
||||
other = coerce(other);
|
||||
const result = new ScopeSet();
|
||||
this._iterScopes((scope, implicants) => {
|
||||
if (other._hasSomeScope(implicants)) {
|
||||
if ((other as ScopeSet)._hasSomeScope(implicants)) {
|
||||
result._addScope(scope, implicants);
|
||||
}
|
||||
});
|
||||
|
@ -396,11 +399,11 @@ class ScopeSet {
|
|||
* `A.filtered(B)`. It returns a new `ScopeSet` object.
|
||||
*
|
||||
*/
|
||||
difference(other) {
|
||||
difference(other: Coerceable) {
|
||||
other = coerce(other);
|
||||
const result = new ScopeSet();
|
||||
this._iterScopes((scope, implicants) => {
|
||||
if (!other._hasSomeScope(implicants)) {
|
||||
if (!(other as ScopeSet)._hasSomeScope(implicants)) {
|
||||
result._addScope(scope, implicants);
|
||||
}
|
||||
});
|
||||
|
@ -415,7 +418,7 @@ class ScopeSet {
|
|||
* either `A` or `B` (or both). It returns a new `ScopeSet` object.
|
||||
*
|
||||
*/
|
||||
union(other) {
|
||||
union(other: Coerceable) {
|
||||
other = coerce(other);
|
||||
const result = new ScopeSet();
|
||||
this._iterScopes((scope, implicants) => {
|
||||
|
@ -433,7 +436,7 @@ class ScopeSet {
|
|||
* kinds of input data into a `ScopeSet` instance.
|
||||
*
|
||||
*/
|
||||
function coerce(scopes) {
|
||||
function coerce(scopes: Coerceable) {
|
||||
if (scopes instanceof ScopeSet) {
|
||||
return scopes;
|
||||
}
|
||||
|
@ -452,7 +455,7 @@ function coerce(scopes) {
|
|||
* An iterator yielding all implicants of the given scope value.
|
||||
*
|
||||
*/
|
||||
function getImplicantValues(value) {
|
||||
function getImplicantValues(value: string) {
|
||||
if (value.startsWith('https:')) {
|
||||
return getImplicantValuesForURLScope(value);
|
||||
} else {
|
||||
|
@ -478,7 +481,7 @@ function getImplicantValues(value) {
|
|||
* only "write" scopes can imply other "write" scopes.
|
||||
*
|
||||
*/
|
||||
function* getImplicantValuesForShortScope(value) {
|
||||
function* getImplicantValuesForShortScope(value: string) {
|
||||
if (!VALID_SCOPE_VALUE.test(value)) {
|
||||
throw new Error('Invalid scope value: ' + value);
|
||||
}
|
||||
|
@ -522,7 +525,7 @@ function* getImplicantValuesForShortScope(value) {
|
|||
* URL is a child of another.
|
||||
*
|
||||
*/
|
||||
function* getImplicantValuesForURLScope(value) {
|
||||
function* getImplicantValuesForURLScope(value: string) {
|
||||
if (!VALID_SCOPE_VALUE.test(value)) {
|
||||
throw new Error('Invalid scope value: ' + value);
|
||||
}
|
||||
|
@ -532,7 +535,7 @@ function* getImplicantValuesForURLScope(value) {
|
|||
throw new Error('Invalid scope value: ' + value);
|
||||
}
|
||||
// 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);
|
||||
}
|
||||
// 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.
|
||||
*
|
||||
*/
|
||||
fromArray(scopesArray) {
|
||||
fromArray(scopesArray: string[]) {
|
||||
return new ScopeSet(scopesArray);
|
||||
},
|
||||
|
||||
|
@ -578,7 +581,7 @@ module.exports = {
|
|||
* case-sensitive strings identifying individual scope values.
|
||||
*
|
||||
*/
|
||||
fromString(scopesString) {
|
||||
fromString(scopesString: string) {
|
||||
// Split the string by one or more space characters.
|
||||
return new ScopeSet(
|
||||
scopesString.split(/ +/).filter((scopeString) => {
|
||||
|
@ -597,7 +600,9 @@ module.exports = {
|
|||
* characters in the scopes will be expanded.
|
||||
*
|
||||
*/
|
||||
fromURLEncodedString(encodedScopesString) {
|
||||
fromURLEncodedString(
|
||||
encodedScopesString: ReturnType<typeof encodeURIComponent>
|
||||
) {
|
||||
// Split the string by a literal plus character.
|
||||
return new ScopeSet(
|
||||
encodedScopesString
|
||||
|
@ -611,3 +616,5 @@ module.exports = {
|
|||
);
|
||||
},
|
||||
};
|
||||
|
||||
export default scopeSetHelpers;
|
|
@ -5,7 +5,52 @@
|
|||
"main": "dist/index.js",
|
||||
"exports": {
|
||||
".": "./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": {
|
||||
"postinstall": "yarn build || true",
|
||||
|
@ -43,6 +88,7 @@
|
|||
"@types/accept-language-parser": "^1.5.3",
|
||||
"@types/chai": "^4.2.18",
|
||||
"@types/chance": "^1.1.2",
|
||||
"@types/express": "^4.17.12",
|
||||
"@types/generic-pool": "^3.1.9",
|
||||
"@types/i18n-abide": "^0",
|
||||
"@types/ioredis": "^4.26.4",
|
||||
|
@ -106,8 +152,6 @@
|
|||
"@sentry/browser": "^6.19.7",
|
||||
"@sentry/integrations": "^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",
|
||||
"ajv": "^8.11.0",
|
||||
"apollo-server": "^2.26.0",
|
||||
|
@ -120,6 +164,7 @@
|
|||
"cldr-localenames-full": "42.0.0",
|
||||
"cors": "^2.8.5",
|
||||
"db-migrations": "workspace:*",
|
||||
"express": "^4.17.2",
|
||||
"find-up": "^5.0.0",
|
||||
"generic-pool": "^3.8.2",
|
||||
"graphql": "^15.6.1",
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
'use strict';
|
||||
|
||||
const { assert } = require('chai');
|
||||
const remoteAddress = require('../../express/remote-address')(3);
|
||||
const remoteAddress = require('../../express/remote-address').remoteAddress(3);
|
||||
|
||||
describe('remote-address', () => {
|
||||
it('has the correct interface', () => {
|
||||
|
|
|
@ -34,7 +34,7 @@ describe('express/routing:', () => {
|
|||
|
||||
routingFactory = proxyquire('../../express/routing', {
|
||||
celebrate: celebrateMock,
|
||||
'./cors': () => corsHandler,
|
||||
'./cors': { default: () => corsHandler },
|
||||
});
|
||||
|
||||
appMock = {
|
||||
|
@ -49,7 +49,7 @@ describe('express/routing:', () => {
|
|||
error: sinon.spy(),
|
||||
};
|
||||
|
||||
routing = routingFactory(appMock, loggerMock);
|
||||
routing = routingFactory.default(appMock, loggerMock);
|
||||
});
|
||||
|
||||
it('exposes the correct interface', () => {
|
||||
|
|
|
@ -15,7 +15,7 @@ describe('metrics/amplitude:', () => {
|
|||
let amplitude;
|
||||
|
||||
before(() => {
|
||||
amplitude = require('../../metrics/amplitude');
|
||||
amplitude = require('../../metrics/amplitude').amplitude;
|
||||
});
|
||||
|
||||
it('exports the event groups', () => {
|
||||
|
|
|
@ -10,7 +10,7 @@ describe('oauth/scopes:', () => {
|
|||
let scopes;
|
||||
|
||||
before(() => {
|
||||
scopes = require('../../oauth/scopes');
|
||||
scopes = require('../../oauth/scopes').default;
|
||||
});
|
||||
|
||||
describe('valid implications', () => {
|
||||
|
|
|
@ -25723,6 +25723,7 @@ fsevents@~2.1.1:
|
|||
"@types/accept-language-parser": ^1.5.3
|
||||
"@types/chai": ^4.2.18
|
||||
"@types/chance": ^1.1.2
|
||||
"@types/express": ^4.17.12
|
||||
"@types/generic-pool": ^3.1.9
|
||||
"@types/i18n-abide": ^0
|
||||
"@types/ioredis": ^4.26.4
|
||||
|
@ -25757,6 +25758,7 @@ fsevents@~2.1.1:
|
|||
esbuild-register: ^3.2.0
|
||||
eslint: ^7.32.0
|
||||
eslint-plugin-fxa: ^2.0.2
|
||||
express: ^4.17.2
|
||||
find-up: ^5.0.0
|
||||
generic-pool: ^3.8.2
|
||||
graphql: ^15.6.1
|
||||
|
|
Загрузка…
Ссылка в новой задаче