Deep links и мобильные SDK
Web deep link
Используйте ссылку вида:
https://watch.shopstory.live/stream/<translationId>?t=<seconds>&utm_source=<source>
translationId— ID трансляции из/v3/streams.t— опциональный таймкод в секундах, чтобы открыть запись с нужного момента.- Добавляйте
utm_*параметры для аналитики.
Мобильные deep links
shopstory://stream/<translationId>?t=<seconds>&product=<feedProductId>
- Поддерживается ShopStory SDK для iOS и Android.
- Передавайте
product, чтобы открывать запись с нужным товаром внутри приложен ия.
WebView bridge и SDK
- SDK пробрасывает события воспроизведения и кликов в нативный код — используйте их для аналитики и CRM.
- При открытии deep link из push-уведомления передавайте
utm_campaign, чтобы можно было корректно атрибутировать переходы. - Если вы используете собственный WebView, убедитесь, что он разрешает
postMessageмежду страницей и контейнером.
Встраивание в кастомные UI
- Используйте параметр
embed=trueв мини-плеере, чтобы получить iFrame-friendly версию. - Для SSR-проектов не забывайте обрабатывать CORS и задавать
referrerpolicy="strict-origin".
🔒 Безопасность Deep Links
Валидация translationId
Всегда валидируйте ID перед использованием:
Безопасная валидация translationId
/**
* Валидатор для translationId
* Разрешает только буквы, цифры, дефисы и подчеркивания
*/
function isValidTranslationId(id) {
if (!id || typeof id !== 'string') {
return false;
}
// Только без опасные символы
const safePattern = /^[a-zA-Z0-9_-]+$/;
// Разумная длина (1-100 символов)
if (id.length === 0 || id.length > 100) {
return false;
}
return safePattern.test(id);
}
/**
* Создать безопасный deep link
*/
function createSafeDeepLink(translationId, options = {}) {
// Валидация ID
if (!isValidTranslationId(translationId)) {
throw new Error('Invalid translation ID format');
}
// Валидация таймкода
const startTime = parseInt(options.startTime) || 0;
if (startTime < 0 || startTime > 86400) { // Макс 24 часа
throw new Error('Invalid start time');
}
// Валидация UTM параметров
const allowedUtmSources = ['app', 'website', 'email', 'push', 'banner'];
const utmSource = allowedUtmSources.includes(options.utmSource)
? options.utmSource
: 'app';
// Безопасное построение URL
const url = new URL('https://watch.shopstory.live/stream/' + encodeURIComponent(translationId));
if (startTime > 0) {
url.searchParams.set('t', startTime.toString());
}
url.searchParams.set('utm_source', utmSource);
return url.toString();
}
// Использование
try {
const link = createSafeDeepLink('2017', {
startTime: 120,
utmSource: 'app'
});
console.log('✅ Deep link:', link);
// https://watch.shopstory.live/stream/2017?t=120&utm_source=app
} catch (error) {
console.error('❌ Ошибка создания deep link:', error.message);
}
Защита от Open Redirect
Безопасная обработка редиректов
/**
* ❌ УЯЗВИМЫЙ КОД - не используйте!
*/
function createUnsafeLink(translationId, redirect) {
// Опасно! Пользовательский ввод может содержать http://evil.com
return `https://watch.shopstory.live/stream/${translationId}?redirect=${redirect}`;
}
/**
* ✅ БЕЗОПАСНЫЙ КОД
*/
function createSafeLinkWithReturn(translationId, returnPath) {
if (!isValidTranslationId(translationId)) {
throw new Error('Invalid translation ID');
}
// Whitelist разрешенных путей для возврата
const allowedPaths = [
'/catalog',
'/product',
'/profile',
'/cart'
];
// Проверяем, что returnPath начинается с разрешенного префикса
const isAllowed = allowedPaths.some(path => returnPath?.startsWith(path));
if (!isAllowed) {
console.warn('Invalid return path, using default');
returnPath = '/catalog';
}
// Убеждаемся, что путь не содержит протокол
if (returnPath.includes('://')) {
throw new Error('Return path cannot contain protocol');
}
const url = new URL('https://watch.shopstory.live/stream/' + encodeURIComponent(translationId));
url.searchParams.set('return', returnPath);
return url.toString();
}
// Безопасное использование
const link = createSafeLinkWithReturn('2017', '/catalog');
// ✅ https://watch.shopstory.live/stream/2017?return=/catalog
// Попытка атаки отклонена
try {
createSafeLinkWithReturn('2017', 'http://evil.com');
} catch (error) {
console.log('✅ Атака заблокирована:', error.message);
}
Безопасная обработка UTM параметров
Валидация UTM параметров
/**
* Безопасное добавление UTM меток
*/
function addSafeUTMParams(baseUrl, utmParams = {}) {
const url = new URL(baseUrl);
// Whitelist разрешенных источников
const allowedSources = ['app', 'website', 'email', 'push', 'banner', 'social'];
const allowedMediums = ['organic', 'cpc', 'email', 'push', 'banner', 'social'];
const allowedCampaigns = /^[a-zA-Z0-9_-]+$/;
// utm_source
if (utmParams.source && allowedSources.includes(utmParams.source)) {
url.searchParams.set('utm_source', utmParams.source);
}
// utm_medium
if (utmParams.medium && allowedMediums.includes(utmParams.medium)) {
url.searchParams.set('utm_medium', utmParams.medium);
}
// utm_campaign (только безопасные символы)
if (utmParams.campaign && allowedCampaigns.test(utmParams.campaign)) {
url.searchParams.set('utm_campaign', utmParams.campaign.slice(0, 50)); // Ограничение длины
}
return url.toString();
}
// Использование
const link = addSafeUTMParams(
'https://watch.shopstory.live/stream/2017',
{
source: 'email',
medium: 'email',
campaign: 'spring-sale-2025'
}
);
console.log(link);
// https://watch.shopstory.live/stream/2017?utm_source=email&utm_medium=email&utm_campaign=spring-sale-2025
React компонент с валидацией
SafeStreamLink.jsx
import React from 'react';
import PropTypes from 'prop-types';
/**
* Безопасный компонент ссылки на стрим
*/
export function SafeStreamLink({ translationId, startTime, utmSource, children, className }) {
const isValidId = (id) => {
return typeof id === 'string' &&
id.length > 0 &&
id.length <= 100 &&
/^[a-zA-Z0-9_-]+$/.test(id);
};
const createLink = () => {
if (!isValidId(translationId)) {
console.error('Invalid translationId:', translationId);
return '#invalid-id';
}
const url = new URL('https://watch.shopstory.live/stream/' + encodeURIComponent(translationId));
// Таймкод
const time = parseInt(startTime) || 0;
if (time > 0 && time <= 86400) {
url.searchParams.set('t', time.toString());
}
// UTM source
const allowedSources = ['app', 'website', 'email', 'push'];
if (allowedSources.includes(utmSource)) {
url.searchParams.set('utm_source', utmSource);
}
return url.toString();
};
const handleClick = (e) => {
const link = createLink();
if (link === '#invalid-id') {
e.preventDefault();
console.error('Cannot navigate to invalid stream');
return;
}
// Логирование для аналитики
console.log('Stream link clicked:', {
translationId,
startTime,
utmSource
});
};
return (
<a
href={createLink()}
className={className}
onClick={handleClick}
rel="noopener noreferrer"
target="_blank"
>
{children}
</a>
);
}
SafeStreamLink.propTypes = {
translationId: PropTypes.string.isRequired,
startTime: PropTypes.number,
utmSource: PropTypes.oneOf(['app', 'website', 'email', 'push']),
children: PropTypes.node.isRequired,
className: PropTypes.string
};
SafeStreamLink.defaultProps = {
startTime: 0,
utmSource: 'app',
className: ''
};
// Использование
<SafeStreamLink
translationId="2017"
startTime={120}
utmSource="app"
className="stream-link"
>
Смотреть стрим
</SafeStreamLink>
Мобильные deep links (iOS/Android)
Безопасная обработка мобильных deep links
/**
* Генерация мобильного deep link с валидацией
*/
function createMobileDeepLink(translationId, options = {}) {
if (!isValidTranslationId(translationId)) {
throw new Error('Invalid translationId');
}
const params = new URLSearchParams();
// Таймкод
const startTime = parseInt(options.startTime) || 0;
if (startTime > 0 && startTime <= 86400) {
params.set('t', startTime.toString());
}
// Product ID (валидация)
if (options.productId && /^[a-zA-Z0-9_-]+$/.test(options.productId)) {
params.set('product', options.productId.slice(0, 100));
}
const queryString = params.toString();
const link = `shopstory://stream/${encodeURIComponent(translationId)}${queryString ? '?' + queryString : ''}`;
return link;
}
/**
* Универсальная функция для web и mobile
*/
function createUniversalLink(translationId, options = {}) {
const isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent);
if (isMobile && options.preferApp) {
return createMobileDeepLink(translationId, options);
}
return createSafeDeepLink(translationId, options);
}
// Использование
const link = createUniversalLink('2017', {
startTime: 120,
productId: 'p-12345',
preferApp: true
});
console.log('Universal link:', link);
Sanitization пользовательского ввода
Очистка пользовательского ввода
/**
* Очистить translationId от потенциально опасных символов
*/
function sanitizeTranslationId(input) {
if (!input || typeof input !== 'string') {
return null;
}
// Удаляем все кроме безопасных символов
const sanitized = input.replace(/[^a-zA-Z0-9_-]/g, '');
// Проверяем длину
if (sanitized.length === 0 || sanitized.length > 100) {
return null;
}
return sanitized;
}
/**
* Безопасная обработка ввода пользователя
*/
function handleUserInput(userInput) {
const translationId = sanitizeTranslationId(userInput);
if (!translationId) {
console.error('Invalid input from user');
// Показываем дружелюбное сообщение пользователю
showError('Некорректный формат идентификатора стрима');
return;
}
try {
const link = createSafeDeepLink(translationId, {
utmSource: 'search'
});
window.location.href = link;
} catch (error) {
console.error('Failed to create deep link:', error);
showError('Не удалось создать ссылку на стрим');
}
}
// Пример использования с формой поиска
document.getElementById('searchForm').addEventListener('submit', (e) => {
e.preventDefault();
const userInput = document.getElementById('streamId').value;
handleUserInput(userInput);
});
📊 Контрольный список безопасности
Перед использованием deep links в production:
- ✅ Валидируете translationId перед использованием
- ✅ Используете
encodeURIComponent()для всех параметров - ✅ Проверяете длину строк (защита от DoS)
- ✅ Используете whitelist для UTM параметров
- ✅ Не принимаете URL редиректов от пользователей
- ✅ Логируете подозрительные попытки создания ссылок
- ✅ Используете Content Security Policy
- ✅ Тестируете на XSS и open redirect уязвимости
Готово! Вернитесь к операционному разделу, чтобы учесть лимиты и кэширование.