packages/fxa-auth-server/lib/routes/validators.js (755 lines of code) (raw):
/* eslint-disable no-useless-escape,no-control-regex */
/* 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/. */
'use strict';
const { URL } = require('url');
const punycode = require('punycode.js');
const isA = require('joi');
const { MozillaSubscriptionTypes } = require('fxa-shared/subscriptions/types');
const {
minimalConfigSchema,
} = require('fxa-shared/subscriptions/configuration/base');
const {
productConfigJoiKeys,
} = require('fxa-shared/subscriptions/configuration/product');
const {
planConfigJoiKeys,
} = require('fxa-shared/subscriptions/configuration/plan');
const {
appStoreSubscriptionSchema,
playStoreSubscriptionSchema,
} = require('fxa-shared/dto/auth/payments/iap-subscription');
const {
latestInvoiceItemsSchema,
} = require('fxa-shared/dto/auth/payments/invoice');
const {
default: DESCRIPTIONS,
} = require('../../docs/swagger/shared/descriptions');
const {
subscriptionProductMetadataBaseValidator,
capabilitiesClientIdPattern,
} = require('fxa-shared/subscriptions/validation');
const {
VX_REGEX: CLIENT_SALT_STRING,
} = require('../../lib/routes/utils/client-key-stretch');
const { ReasonForDeletion } = require('./cloud-tasks');
// Match any non-empty hex-encoded string.
const HEX_STRING = /^(?:[a-fA-F0-9]{2})+$/;
module.exports.HEX_STRING = HEX_STRING;
module.exports.BASE_36 = /^[a-zA-Z0-9]*$/;
module.exports.BASE_10 = /^[0-9]*$/;
// RFC 4648, section 5
module.exports.URL_SAFE_BASE_64 = /^[A-Za-z0-9_-]+$/;
// RFC 7636, section 4.1
module.exports.PKCE_CODE_VERIFIER = /^[A-Za-z0-9-\._~]{43,128}$/;
// Crude phone number validation. The handler code does it more thoroughly.
exports.E164_NUMBER = /^\+[1-9]\d{1,14}$/;
exports.DIGITS = /^[0-9]+$/;
exports.DEVICE_COMMAND_NAME = /^[a-zA-Z0-9._\/\-:]{1,100}$/;
exports.IP_ADDRESS = isA.string().ip();
// Match display-safe unicode characters.
// We're pretty liberal with what's allowed in a unicode string,
// but we exclude the following classes of characters:
//
// \u0000-\u001F - C0 (ascii) control characters
// \u007F - ascii DEL character
// \u0080-\u009F - C1 (ansi escape) control characters
// \u2028-\u2029 - unicode line/paragraph separator
// \uD800-\uDFFF - non-BMP surrogate pairs
// \uE000-\uF8FF - BMP private use area
// \uFFF9-\uFFFC - unicode specials prior to the replacement character
// \uFFFE-\uFFFF - unicode this-is-not-a-character specials
//
// Note that the unicode replacement character \uFFFD is explicitly allowed,
// and clients may use it to replace other disallowed characters.
//
// We might tweak this list in future.
const DISPLAY_SAFE_UNICODE =
/^(?:[^\u0000-\u001F\u007F\u0080-\u009F\u2028-\u2029\uD800-\uDFFF\uE000-\uF8FF\uFFF9-\uFFFC\uFFFE-\uFFFF])*$/;
module.exports.DISPLAY_SAFE_UNICODE = DISPLAY_SAFE_UNICODE;
// Similar display-safe match but includes non-BMP characters
const DISPLAY_SAFE_UNICODE_WITH_NON_BMP =
/^(?:[^\u0000-\u001F\u007F\u0080-\u009F\u2028-\u2029\uE000-\uF8FF\uFFF9-\uFFFC\uFFFE-\uFFFF])*$/;
module.exports.DISPLAY_SAFE_UNICODE_WITH_NON_BMP =
DISPLAY_SAFE_UNICODE_WITH_NON_BMP;
// Bearer auth header regex
const BEARER_AUTH_REGEX = /^Bearer\s+([a-z0-9+\/]+)$/i;
module.exports.BEARER_AUTH_REGEX = BEARER_AUTH_REGEX;
// Joi validator to match any valid email address.
// This is different to Joi's builtin email validator, and
// requires a custom validation function.
module.exports.email = function () {
const email = isA
.string()
.max(255)
.regex(DISPLAY_SAFE_UNICODE)
.custom((value) => {
// Do custom validation
const isValid = module.exports.isValidEmailAddress(value);
if (!isValid) {
throw new Error('Not a valid email address');
}
return value;
});
return email;
};
module.exports.service = isA
.string()
.max(16)
.regex(/^[a-zA-Z0-9\-]*$/);
module.exports.hexString = isA.string().regex(HEX_STRING);
module.exports.uid = module.exports.hexString.length(32);
module.exports.clientId = module.exports.hexString.length(16);
module.exports.clientSecret = module.exports.hexString;
module.exports.idToken = module.exports.jwt;
module.exports.reasonForAccountDeletion = isA
.string()
.valid(...Object.values(ReasonForDeletion));
module.exports.refreshToken = module.exports.hexString.length(64);
module.exports.sessionToken = module.exports.hexString.length(64);
module.exports.sessionTokenId = module.exports.hexString.length(64);
module.exports.authorizationCode = module.exports.hexString.length(64);
// Note that the empty string is a valid scope value (meaning "no permissions").
const scope = isA
.string()
.max(256)
.regex(/^[a-zA-Z0-9 _\/.:-]*$/)
.allow('');
module.exports.scope = scope;
module.exports.assertion = isA
.string()
.min(50)
.max(10240)
.regex(/^[a-zA-Z0-9_\-\.~=]+$/);
module.exports.pkceCodeChallengeMethod = isA.string().valid('S256');
module.exports.pkceCodeChallenge = isA
.string()
.length(43)
.regex(module.exports.URL_SAFE_BASE_64);
module.exports.pkceCodeVerifier = isA
.string()
.min(43)
.max(128)
.regex(module.exports.PKCE_CODE_VERIFIER);
module.exports.jwe = isA
.string()
.max(1024)
// JWE token format: 'protectedheader.encryptedkey.iv.cyphertext.authenticationtag'
.regex(
/^[A-Za-z0-9-_]+\.[A-Za-z0-9-_]*\.[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+$/
);
module.exports.jwt = isA
.string()
.max(1024)
// JWT format: 'header.payload.signature'
.regex(/^([a-zA-Z0-9\-_]+)\.([a-zA-Z0-9\-_]+)\.([a-zA-Z0-9\-_]+)$/);
module.exports.accessToken = isA
.alternatives()
.try(module.exports.hexString.length(64), module.exports.jwt);
// Function to validate an email address.
//
// Uses regexes based on the ones in fxa-email-service, tweaked slightly
// because Node's support for unicode regexes is hidden behind a harmony
// flag. As soon as we have default support for unicode regexes, we should
// be able to just use the regex from there directly (and ditch the punycode
// transformation).
//
// https://github.com/mozilla/fxa-email-service/blob/6fc6c31043598b246102cd1fdd27fc325f4514fb/src/validate/mod.rs#L28-L30
const EMAIL_USER = /^[A-Z0-9.!#$%&'*+\/=?^_`{|}~-]{1,64}$/i;
const EMAIL_DOMAIN =
/^[A-Z0-9](?:[A-Z0-9-]{0,253}[A-Z0-9])?(?:\.[A-Z0-9](?:[A-Z0-9-]{0,253}[A-Z0-9])?)+$/i;
module.exports.isValidEmailAddress = function (value) {
if (!value) {
return false;
}
const parts = value.split('@');
if (parts.length !== 2 || parts[1].length > 255) {
return false;
}
if (!EMAIL_USER.test(punycode.toASCII(parts[0]))) {
return false;
}
if (!EMAIL_DOMAIN.test(punycode.toASCII(parts[1]))) {
return false;
}
return true;
};
module.exports.redirectTo = function redirectTo(base) {
const validator = isA
.string()
.max(2048)
.custom((value) => {
let hostnameRegex = '';
if (base) {
hostnameRegex = new RegExp(`(?:\\.|^)${base.replaceAll('.', '\\.')}$`);
}
// Do your validation
const isValid = isValidUrl(value, hostnameRegex);
if (!isValid) {
throw new Error('Not a valid URL');
}
return value;
});
return validator;
};
module.exports.url = function url(options) {
const validator = isA
.string()
.uri(options)
.custom((value) => {
const isValid = isValidUrl(value);
if (!isValid) {
throw new Error('Not a valid URL');
}
return value;
});
return validator;
};
// resourceUrls must *not* contain a hash fragment.
// See https://tools.ietf.org/html/draft-ietf-oauth-resource-indicators-02#section-2
module.exports.resourceUrl = module.exports.url().regex(/#/, { invert: true });
module.exports.pushCallbackUrl = function pushUrl(options) {
const validator = isA
.string()
.uri(options)
.custom((value) => {
let normalizedValue = value;
// Fx Desktop registers https push urls with a :443 which causes `isValidUrl`
// to fail because the :443 is expected to have been normalized away.
if (/^https:\/\/[a-zA-Z0-9._-]+(:443)($|\/)/.test(value)) {
normalizedValue = value.replace(':443', '');
}
const isValid = isValidUrl(normalizedValue);
if (!isValid) {
throw new Error('Not a valid URL');
}
return value;
});
return validator;
};
function isValidUrl(url, hostnameRegex) {
let parsed;
try {
parsed = new URL(url);
} catch (err) {
return false;
}
if (hostnameRegex && !hostnameRegex.test(parsed.hostname)) {
return false;
}
if (!/^https?:$/.test(parsed.protocol)) {
return false;
}
// Reject anything that won't round-trip unambiguously
// through a parse. This puts the onus on the requestor
// to e.g. escape special characters, normalize ports, etc.
// The only trick here is that `new URL()` will add a trailing
// slash if there's no path component, which is why we also
// compare to `origin` below.
if (parsed.href !== url && parsed.origin !== url) {
return false;
}
return parsed.href;
}
module.exports.verificationMethod = isA.string().valid(
'email', // Verification by email link
'email-otp', // Verification by email otp code using account long code (`emailCode`) as secret
'email-2fa', // Verification by email code using randomly generated code (used in login flow)
'email-captcha', // Verification by email code using randomly generated code (used in unblock flow)
'totp-2fa' // Verification by TOTP authenticator device code, secret is randomly generated
);
module.exports.authPW = isA.string().length(64).regex(HEX_STRING).required();
module.exports.wrapKb = isA.string().length(64).regex(HEX_STRING);
module.exports.authPWVersion2 = isA.string().length(64).regex(HEX_STRING);
module.exports.clientSalt = isA.string().regex(CLIENT_SALT_STRING);
module.exports.recoveryKeyId = isA.string().regex(HEX_STRING).max(32);
module.exports.recoveryData = isA
.string()
.regex(/[a-zA-Z0-9.]/)
.max(1024)
.required();
module.exports.recoveryKeyHint = isA
.string()
.max(255)
.regex(DISPLAY_SAFE_UNICODE);
module.exports.recoveryCode = function (len, base) {
const regex = base || module.exports.BASE_36;
return isA.string().regex(regex).min(8).max(len);
};
module.exports.recoveryCodes = function (codeCount, codeLen, base) {
return isA.object({
recoveryCodes: isA
.array()
.min(1)
.max(codeCount)
.unique()
.items(module.exports.recoveryCode(codeLen, base))
.required(),
});
};
module.exports.stripePaymentMethodId = isA.string().max(30);
module.exports.paypalPaymentToken = isA.string().max(30);
module.exports.subscriptionsSubscriptionId = isA.string().max(255);
module.exports.subscriptionsPlanId = isA.string().max(255);
module.exports.subscriptionsProductId = isA.string().max(255);
module.exports.subscriptionsProductName = isA.string().max(255);
module.exports.subscriptionsPaymentToken = isA.string().max(255);
module.exports.subscriptionPaymentCountryCode = isA
.string()
.length(2)
.allow(null);
// This is fxa-auth-db-mysql's perspective on an active subscription
module.exports.activeSubscriptionValidator = isA.object({
uid: isA.string().required().description(DESCRIPTIONS.uid),
subscriptionId: module.exports.subscriptionsSubscriptionId
.required()
.description(DESCRIPTIONS.subscriptionId),
productId: module.exports.subscriptionsProductId
.required()
.description(DESCRIPTIONS.productId),
createdAt: isA.number().required().description(DESCRIPTIONS.createdAt),
cancelledAt: isA
.alternatives(isA.number(), isA.any().allow(null))
.description(DESCRIPTIONS.cancelledAt),
});
module.exports.subscriptionsSetupIntent = isA
.object({
client_secret: isA
.string()
.required()
.description(DESCRIPTIONS.clientSecret),
})
.unknown(true);
// This is a Stripe subscription object with latest_invoice.payment_intent expanded
module.exports.subscriptionsSubscriptionExpandedValidator = isA
.object({
id: isA.string().required(),
object: isA.string().allow('subscription').required(),
latest_invoice: isA
.object({
id: isA.string().required(),
object: isA.string().allow('invoice').required(),
payment_intent: isA
.object({
id: isA.string().required(),
object: isA.string().allow('payment_intent').required(),
client_secret: isA.string().required(),
})
.unknown(true)
.required(),
})
.unknown(true)
.required()
.description(DESCRIPTIONS.latestInvoice),
})
.unknown(true);
module.exports.subscriptionsInvoicePIExpandedValidator = isA
.object({
id: isA.string().required(),
object: isA.string().allow('invoice').required(),
payment_intent: isA
.object({
id: isA.string().required(),
object: isA.string().allow('payment_intent').required(),
client_secret: isA.string().required(),
})
.unknown(true)
.required(),
})
.unknown(true);
module.exports.subscriptionsSubscriptionValidator = isA.object({
_subscription_type: MozillaSubscriptionTypes.WEB,
created: isA.number().required().description(DESCRIPTIONS.createdAt),
current_period_end: isA
.number()
.required()
.description(DESCRIPTIONS.currentPeriodEnd),
current_period_start: isA
.number()
.required()
.description(DESCRIPTIONS.currentPeriodStart),
cancel_at_period_end: isA
.boolean()
.required()
.description(DESCRIPTIONS.cancelAtPeriodEnd),
end_at: isA.alternatives(isA.number(), isA.any().allow(null)),
failure_code: isA.string().optional().description(DESCRIPTIONS.failureCode),
failure_message: isA
.string()
.optional()
.description(DESCRIPTIONS.failureMessage),
latest_invoice: isA
.string()
.required()
.description(DESCRIPTIONS.latestInvoice),
latest_invoice_items: latestInvoiceItemsSchema.required(),
plan_id: module.exports.subscriptionsPlanId
.required()
.description(DESCRIPTIONS.planId),
product_id: module.exports.subscriptionsProductId
.required()
.description(DESCRIPTIONS.productId),
product_name: isA.string().required().description(DESCRIPTIONS.productName),
status: isA.string().required().description(DESCRIPTIONS.status),
subscription_id: module.exports.subscriptionsSubscriptionId
.required()
.description(DESCRIPTIONS.subscriptionId),
promotion_amount_off: isA
.number()
.integer()
.min(0)
.optional()
.allow(null)
.description(DESCRIPTIONS.promotionAmountOff),
promotion_code: isA
.string()
.optional()
.allow(null)
.description(DESCRIPTIONS.promotionCode),
promotion_duration: isA
.string()
.optional()
.allow(null)
.description(DESCRIPTIONS.promotionDuration),
promotion_end: isA
.number()
.optional()
.allow(null)
.description(DESCRIPTIONS.promotionEnd),
promotion_name: isA
.string()
.optional()
.allow(null)
.description(DESCRIPTIONS.promotionName),
promotion_percent_off: isA
.number()
.min(0)
.max(100)
.optional()
.allow(null)
.description(DESCRIPTIONS.promotionPercentOff),
});
// This is support-panel's perspective on a subscription
module.exports.subscriptionsWebSubscriptionSupportValidator = isA
.object({
created: isA.number().required().description(DESCRIPTIONS.createdAt),
current_period_end: isA
.number()
.required()
.description(DESCRIPTIONS.currentPeriodEnd),
current_period_start: isA
.number()
.required()
.description(DESCRIPTIONS.currentPeriodStart),
plan_changed: isA.alternatives(isA.number(), isA.any().allow(null)),
previous_product: isA
.alternatives(isA.string(), isA.any().allow(null))
.description(DESCRIPTIONS.previousProduct),
product_name: isA.string().required().description(DESCRIPTIONS.productName),
status: isA.string().required().description(DESCRIPTIONS.status),
subscription_id: module.exports.subscriptionsSubscriptionId
.required()
.description(DESCRIPTIONS.subscriptionId),
})
.unknown(true);
module.exports.subscriptionsPlaySubscriptionSupportValidator = isA
.object({
_subscription_type: MozillaSubscriptionTypes.IAP_GOOGLE,
auto_renewing: isA.bool().required(),
cancel_reason: isA.number().optional(),
expiry_time_millis: isA.number().required(),
package_name: isA.string().optional(),
price_id: isA.string().optional(),
product_id: isA.string().optional(),
product_name: isA.string().required(),
sku: isA.string().optional(),
})
.unknown(true);
module.exports.subscriptionsAppStoreSubscriptionSupportValidator = isA
.object({
_subscription_type: MozillaSubscriptionTypes.IAP_APPLE,
app_store_product_id: isA.string().required(),
auto_renewing: isA.bool().required(),
bundle_id: isA.string().required(),
expiry_time_millis: isA.number().optional(),
is_in_billing_retry_period: isA.boolean().optional(),
price_id: isA.string().optional(),
product_id: isA.string().optional(),
product_name: isA.string().required(),
})
.unknown(true);
module.exports.subscriptionsSubscriptionSupportValidator = isA.object({
[MozillaSubscriptionTypes.WEB]: isA
.array()
.items(module.exports.subscriptionsWebSubscriptionSupportValidator),
[MozillaSubscriptionTypes.IAP_GOOGLE]: isA
.array()
.items(module.exports.subscriptionsPlaySubscriptionSupportValidator),
[MozillaSubscriptionTypes.IAP_APPLE]: isA
.array()
.items(module.exports.subscriptionsAppStoreSubscriptionSupportValidator),
});
module.exports.subscriptionsSubscriptionListValidator = isA.object({
subscriptions: isA
.array()
.items(module.exports.subscriptionsSubscriptionValidator),
});
// https://mana.mozilla.org/wiki/pages/viewpage.action?spaceKey=COPS&title=SP+Tiered+Product+Support#SPTieredProductSupport-MetadataAgreements
// Trying to be a bit flexible in validation here:
// - subhub may not yet be including product / plan metadata in responses
// - metadata can contain arbitrary keys that we don't expect (e.g. used by other systems)
// - but we can make a good effort at validating what we expect to see when we see it
module.exports.subscriptionPlanMetadataValidator = isA.object().unknown(true);
module.exports.subscriptionProductMetadataValidator = {
validate: function (metadata) {
const hasCapability = Object.keys(metadata).some((k) =>
capabilitiesClientIdPattern.test(k)
);
if (!hasCapability) {
return {
error: 'Capability missing from metadata',
};
}
const { value: result, error } =
subscriptionProductMetadataBaseValidator.validate(metadata, {
abortEarly: false,
});
if (error) {
return { error };
}
return { result };
},
async validateAsync(metadata) {
const hasCapability = Object.keys(metadata).some((k) =>
capabilitiesClientIdPattern.test(k)
);
if (!hasCapability) {
return {
error: 'Capability missing from metadata',
};
}
try {
const validationSchema = subscriptionProductMetadataBaseValidator;
const value = await validationSchema.validateAsync(metadata, {
abortEarly: false,
});
return { value };
} catch (error) {
return { error };
}
},
};
module.exports.subscriptionsPlanWithMetaDataValidator = isA.object({
plan_id: module.exports.subscriptionsPlanId
.required()
.description(DESCRIPTIONS.planId),
plan_metadata: module.exports.subscriptionPlanMetadataValidator
.optional()
.description(DESCRIPTIONS.planMetadata),
product_id: module.exports.subscriptionsProductId
.required()
.description(DESCRIPTIONS.productId),
product_name: isA.string().required().description(DESCRIPTIONS.productName),
plan_name: isA
.string()
.allow('')
.optional()
.description(DESCRIPTIONS.planName),
product_metadata: subscriptionProductMetadataBaseValidator
.optional()
.description(DESCRIPTIONS.productMetadata),
interval: isA.string().required().description(DESCRIPTIONS.interval),
interval_count: isA
.number()
.required()
.description(DESCRIPTIONS.intervalCount),
amount: isA.number().required().description(DESCRIPTIONS.amount),
currency: isA.string().required().description(DESCRIPTIONS.currency),
active: isA.boolean().required().description(DESCRIPTIONS.activePrice),
configuration: minimalConfigSchema
.keys(productConfigJoiKeys)
.keys(planConfigJoiKeys)
.optional()
.allow(null),
});
module.exports.subscriptionsPlanWithProductConfigValidator = isA.object({
plan_id: module.exports.subscriptionsPlanId
.required()
.description(DESCRIPTIONS.planId),
plan_metadata: isA.object().optional().description(DESCRIPTIONS.planMetadata),
product_id: module.exports.subscriptionsProductId
.required()
.description(DESCRIPTIONS.productId),
product_name: isA.string().required().description(DESCRIPTIONS.productName),
plan_name: isA
.string()
.allow('')
.optional()
.description(DESCRIPTIONS.planName),
product_metadata: isA
.object()
.optional()
.description(DESCRIPTIONS.productMetadata),
interval: isA.string().required().description(DESCRIPTIONS.interval),
interval_count: isA
.number()
.required()
.description(DESCRIPTIONS.intervalCount),
amount: isA.number().required().description(DESCRIPTIONS.amount),
currency: isA.string().required().description(DESCRIPTIONS.currency),
active: isA.boolean().required().description(DESCRIPTIONS.activePrice),
configuration: minimalConfigSchema
.keys(productConfigJoiKeys)
.keys(planConfigJoiKeys)
.required(),
});
module.exports.customerId = isA
.string()
.optional()
.description(DESCRIPTIONS.customerId);
module.exports.subscriptionsCustomerValidator = isA.object({
customerId: module.exports.customerId,
billing_name: isA
.alternatives(isA.string(), isA.any().allow(null))
.optional()
.description(DESCRIPTIONS.billingName),
exp_month: isA.number().optional().description(DESCRIPTIONS.expMonth),
exp_year: isA.number().optional().description(DESCRIPTIONS.expYear),
last4: isA.string().optional().description(DESCRIPTIONS.last4),
payment_provider: isA
.string()
.optional()
.description(DESCRIPTIONS.paymentProvider),
payment_type: isA.string().optional().description(DESCRIPTIONS.paymentType),
paypal_payment_error: isA
.string()
.optional()
.description(DESCRIPTIONS.paypalPaymentError),
brand: isA.string().optional().description(DESCRIPTIONS.brand),
billing_agreement_id: isA
.alternatives(isA.string(), isA.any().allow(null))
.optional()
.description(DESCRIPTIONS.billingAgreementId),
subscriptions: isA
.array()
.items(module.exports.subscriptionsSubscriptionValidator)
.optional()
.description(DESCRIPTIONS.subscriptions),
});
module.exports.subscriptionsStripeIntentValidator = isA
.object({
client_secret: isA
.string()
.optional()
.description(DESCRIPTIONS.clientSecret),
created: isA.number().required().description(DESCRIPTIONS.createdAt),
payment_method: isA
.alternatives(isA.string(), isA.object({}).unknown(true))
.optional()
.allow(null),
source: isA.alternatives().conditional('payment_method', {
// cannot be that strict here since this validator is used in two routes
is: null,
then: isA.string().optional(),
otherwise: isA.any().optional().allow(null),
}),
status: isA.string().required().description(DESCRIPTIONS.status),
})
.unknown(true);
module.exports.subscriptionsStripeSourceValidator = isA
.object({
id: isA.string().required(),
object: isA.string().required(),
brand: isA.string().optional().description(DESCRIPTIONS.brand),
exp_month: isA.string().optional().description(DESCRIPTIONS.expMonth),
exp_year: isA.string().optional().description(DESCRIPTIONS.expYear),
last4: isA.string().optional().description(DESCRIPTIONS.last4),
})
.unknown(true);
module.exports.subscriptionsStripeInvoiceValidator = isA
.object({
id: isA.string().required(),
payment_intent: isA
.alternatives(
isA.string().allow(null),
module.exports.subscriptionsStripeIntentValidator
)
.optional(),
})
.unknown(true);
module.exports.subscriptionsStripePriceValidator = isA
.object({
id: isA.string().required(),
})
.unknown(true);
module.exports.subscriptionsStripeSubscriptionItemValidator = isA
.object({
id: isA.string().required(),
created: isA.number().required(),
price: module.exports.subscriptionsStripePriceValidator.required(),
})
.unknown(true);
module.exports.subscriptionsStripeSubscriptionValidator = isA
.object({
id: isA.string().required(),
cancel_at: isA.alternatives(isA.number(), isA.any().valid(null)),
canceled_at: isA
.alternatives(isA.number(), isA.any().valid(null))
.description(DESCRIPTIONS.cancelledAt),
cancel_at_period_end: isA
.bool()
.required()
.description(DESCRIPTIONS.cancelAtPeriodEnd),
created: isA.number().required().description(DESCRIPTIONS.createdAt),
current_period_end: isA
.number()
.required()
.description(DESCRIPTIONS.currentPeriodEnd),
current_period_start: isA
.number()
.required()
.description(DESCRIPTIONS.currentPeriodStart),
ended_at: isA.alternatives(isA.number(), isA.any().valid(null)),
items: isA
.object({
data: isA
.array()
.items(module.exports.subscriptionsStripeSubscriptionItemValidator)
.required(),
})
.unknown(true)
.optional(),
latest_invoice: isA
.alternatives(
isA.string(),
module.exports.subscriptionsStripeInvoiceValidator
)
.optional(),
status: isA.string().required().description(DESCRIPTIONS.status),
})
.unknown(true);
module.exports.subscriptionsGooglePlaySubscriptionValidator =
playStoreSubscriptionSchema;
module.exports.subscriptionsAppStoreSubscriptionValidator =
appStoreSubscriptionSchema;
module.exports.subscriptionsStripeCustomerValidator = isA
.object({
invoices_settings: isA
.object({
default_payment_method: isA.string().optional(),
})
.unknown(true)
.optional(),
subscriptions: isA
.object({
data: isA
.array()
.items(module.exports.subscriptionsStripeSubscriptionValidator)
.required(),
})
.unknown(true)
.optional(),
})
.unknown(true);
module.exports.subscriptionsMozillaSubscriptionsValidator = isA
.object({
customerId: module.exports.customerId,
billing_name: isA
.alternatives(isA.string(), isA.any().allow(null))
.optional()
.description(DESCRIPTIONS.billingName),
exp_month: isA.number().optional().description(DESCRIPTIONS.expMonth),
exp_year: isA.number().optional().description(DESCRIPTIONS.expYear),
last4: isA.string().optional().description(DESCRIPTIONS.last4),
payment_provider: isA
.string()
.optional()
.description(DESCRIPTIONS.paymentProvider),
payment_type: isA.string().optional().description(DESCRIPTIONS.paymentType),
paypal_payment_error: isA
.string()
.optional()
.description(DESCRIPTIONS.paypalPaymentError),
brand: isA.string().optional().description(DESCRIPTIONS.brand),
billing_agreement_id: isA
.alternatives(isA.string(), isA.any().allow(null))
.optional()
.description(DESCRIPTIONS.billingAgreementId),
subscriptions: isA
.array()
.items(
module.exports.subscriptionsSubscriptionValidator,
module.exports.subscriptionsGooglePlaySubscriptionValidator,
module.exports.subscriptionsAppStoreSubscriptionValidator
)
.required()
.description(DESCRIPTIONS.subscriptions),
})
.unknown(true);
module.exports.ppidSeed = isA.number().integer().min(0).max(1024);
module.exports.scopes = isA.array().items(scope).default([]).optional();
module.exports.newsletters = isA
.array()
.items(
isA
.string()
.valid(
'firefox-accounts-journey',
'knowledge-is-power',
'mozilla-foundation',
'take-action-for-the-internet',
'test-pilot',
'mozilla-and-you',
'security-privacy-news',
'mozilla-accounts',
'hubs',
'mdnplus'
)
)
.default([])
.optional();
module.exports.thirdPartyProvider = isA
.string()
.max(256)
.allow('google', 'apple')
.required();
module.exports.thirdPartyIdToken = module.exports.jwt.optional();
module.exports.thirdPartyOAuthCode = isA.string().optional();