feat: add prelaunch site controls and refresh branding
Validate and deploy production / validate (push) Successful in 23s
Validate and deploy staging / validate (push) Successful in 19s
Validate and deploy production / deploy (push) Successful in 1m0s
Validate and deploy staging / deploy (push) Successful in 36s

This commit is contained in:
JB
2026-07-27 02:24:13 -04:00
parent 7b3aff30d8
commit 5a67b252aa
37 changed files with 1738 additions and 100 deletions
+210 -19
View File
File diff suppressed because one or more lines are too long
+20 -1
View File
@@ -2,6 +2,7 @@ import { json, type Handle, type HandleServerError } from '@sveltejs/kit';
import { stringify } from 'devalue';
import { randomUUID } from 'node:crypto';
import { getSessionUser } from '$lib/server/auth/session';
import { getSiteAccessBlock, getSiteAccessSettings } from '$lib/server/site-access';
import { DATA_MODULES, proposalActionResult, queueDataProposal, shouldModerateDataMutation, type DataModule } from '$lib/server/data-moderation';
export const handle: Handle = async ({ event, resolve }) => {
@@ -9,6 +10,24 @@ export const handle: Handle = async ({ event, resolve }) => {
const location = `/isac${event.url.pathname.slice('/admin'.length)}${event.url.search}`;
return new Response(null, { status: 308, headers: { location } });
}
const siteSettings = await getSiteAccessSettings();
const sessionUser = await getSessionUser(event.cookies);
const accessBlock = getSiteAccessBlock(siteSettings, event.url.pathname, Boolean(sessionUser));
if (accessBlock) {
const target = new URL('/coming-soon', event.url);
if (accessBlock.kind === 'section') target.searchParams.set('unavailable', accessBlock.section);
if (event.request.method === 'GET' || event.request.method === 'HEAD') {
return new Response(null, { status: 303, headers: { location: `${target.pathname}${target.search}` } });
}
return json(
{
error: accessBlock.kind === 'section'
? 'This section is not available.'
: 'The public site is not available yet.'
},
{ status: 404 }
);
}
if (event.request.method === 'POST') {
const match = event.url.pathname.match(/^\/isac\/([^/]+)$/);
const module = match?.[1] as DataModule | undefined;
@@ -17,7 +36,7 @@ export const handle: Handle = async ({ event, resolve }) => {
// Publishing calculation rules is never a contributor proposal. The
// action has its own dedicated permission and confirmation contract.
if (module === 'calculations' && action === 'publishRevision') return resolve(event);
const user = await getSessionUser(event.cookies);
const user = sessionUser;
const access = await shouldModerateDataMutation(user);
if (!access.canManage && access.canPropose && user) {
if (!access.contributorEnabled) {
+22 -14
View File
@@ -1,23 +1,32 @@
<script lang="ts">
import { page } from '$app/state';
import BrandLockup from '$lib/components/BrandLockup.svelte';
import PendingForm from '$lib/components/PendingForm.svelte';
import { onMount, tick } from 'svelte';
import type { Snippet } from 'svelte';
let { user, isacAccess, notificationSummary, children }: {
let { user, isacAccess, notificationSummary, siteAccess, children }: {
user: { username: string; displayName: string; role: string; avatarUrl: string } | null;
isacAccess: boolean;
notificationSummary: { unread: number };
siteAccess: { comingSoonEnabled: boolean; sections: Record<string, boolean> };
children: Snippet;
} = $props();
const links = [
{ href: '/news', label: 'News' },
{ href: '/database', label: 'Database' },
{ href: '/builds', label: 'Builds' },
{ href: '/guides', label: 'Guides' },
{ href: '/builder', label: 'Builder' }
const allLinks = [
{ href: '/news', label: 'News', key: 'news' },
{ href: '/database', label: 'Database', key: 'database' },
{ href: '/builds', label: 'Builds', key: 'builds' },
{ href: '/guides', label: 'Guides', key: 'guides' },
{ href: '/builder', label: 'Builder', key: 'builder' }
];
const links = $derived(allLinks.filter((link) => siteAccess.sections[link.key]));
const launchSupportPages = ['/contact', '/dmca', '/terms', '/sign-in'];
const homeHref = $derived(
siteAccess.comingSoonEnabled && launchSupportPages.some((path) => page.url.pathname === path || page.url.pathname.startsWith(`${path}/`))
? '/coming-soon'
: '/'
);
let showCookieNotice = $state(false);
let mobileOpen = $state(false);
@@ -102,8 +111,8 @@
<svelte:window onkeydown={handleNavigationKey} onclick={closeAccountFromOutside}/>
<header class="site-header">
<a class="brand" href="/" aria-label="SHD.build home">
<span class="brand-mark"><i></i><b>SHD</b></span><span class="brand-name">SHD<span>.build</span><small>RESURGENCE COMMUNITY</small></span>
<a class="brand" href={homeHref} aria-label="SHD.build home">
<BrandLockup />
</a>
<nav aria-label="Main navigation">
{#each links as link}
@@ -162,8 +171,8 @@
<div class="mobile-nav-layer">
<button class="mobile-nav-backdrop" type="button" aria-label="Close navigation" onclick={closeMobileNavigation}></button>
<nav id="mobile-navigation" class="mobile-navigation" aria-label="Mobile navigation" bind:this={mobilePanel}>
<div class="mobile-nav-heading"><div><span>SHD COMMUNITY</span><strong>NAVIGATION</strong></div><b>ONLINE</b></div>
<a class:active={page.url.pathname === '/'} href="/"><small>00</small><span>Home</span><b>›</b></a>
<div class="mobile-nav-heading"><div><span>SHD NETWORK</span><strong>NAVIGATION</strong></div><b>ONLINE</b></div>
<a class:active={page.url.pathname === '/' || page.url.pathname === '/coming-soon'} href={homeHref}><small>00</small><span>Home</span><b>›</b></a>
{#each links as link, index}
<a class:active={page.url.pathname.startsWith(link.href)} href={link.href}><small>{String(index + 1).padStart(2, '0')}</small><span>{link.label}</span><b>›</b></a>
{/each}
@@ -186,9 +195,8 @@
<footer class="site-footer">
<div>
<a class="brand footer-brand" href="/" aria-label="SHD.build home">
<span class="brand-mark"><i></i><b>SHD</b></span>
<span class="brand-name">SHD<span>.build</span><small>RESURGENCE COMMUNITY</small></span>
<a class="brand footer-brand" href={homeHref} aria-label="SHD.build home">
<BrandLockup size="footer" />
</a>
<p>Independent builds, guides, game data, news, and community intelligence for The Division Resurgence.</p>
</div>
+21
View File
@@ -0,0 +1,21 @@
<script lang="ts">
let { size = '100%' }: { size?: string } = $props();
</script>
<svg class="brand-emblem" viewBox="0 0 100 100" aria-hidden="true" style:width={size} style:height={size}>
<g fill="none" stroke="#f6a821" stroke-width="12" stroke-linecap="butt">
<path d="M73 20A38 38 0 0 0 15 63"/>
<path d="M85 37A38 38 0 0 1 55 88"/>
<path d="M42 87A38 38 0 0 1 18 70"/>
</g>
<rect x="72" y="23" width="10" height="10" fill="#f6a821"/>
<path d="M62 35H35V65H62M68 43V57" fill="none" stroke="#ecebe5" stroke-width="9" stroke-linecap="butt" stroke-linejoin="miter"/>
</svg>
<style>
.brand-emblem {
display: block;
flex: 0 0 auto;
overflow: visible;
}
</style>
+79
View File
@@ -0,0 +1,79 @@
<script lang="ts">
import BrandEmblem from '$lib/components/BrandEmblem.svelte';
let {
size = 'header',
descriptor = 'RESURGENCE DATA & TOOLS'
}: {
size?: 'header' | 'footer' | 'launch';
descriptor?: string;
} = $props();
</script>
<span class="brand-lockup brand-lockup--{size}">
<span class="brand-lockup__emblem"><BrandEmblem /></span>
<span class="brand-lockup__copy">
<span class="brand-lockup__name"><span>SHD</span><b>.build</b></span>
<small>{descriptor}</small>
</span>
</span>
<style>
.brand-lockup {
--brand-emblem-size: 38px;
--brand-name-size: 20px;
display: inline-flex;
align-items: center;
gap: 11px;
min-width: max-content;
}
.brand-lockup__emblem {
display: block;
width: var(--brand-emblem-size);
height: var(--brand-emblem-size);
flex: 0 0 auto;
}
.brand-lockup__copy {
display: grid;
gap: 3px;
}
.brand-lockup__name {
display: flex;
align-items: baseline;
color: var(--shd-text);
font-size: var(--brand-name-size);
font-weight: 900;
letter-spacing: -.025em;
line-height: .82;
}
.brand-lockup__name b {
color: var(--shd-orange);
font-weight: inherit;
}
.brand-lockup small {
color: var(--shd-text-muted);
font-size: 9px;
font-weight: 800;
letter-spacing: .13em;
line-height: 1;
white-space: nowrap;
}
.brand-lockup--footer {
--brand-emblem-size: 36px;
--brand-name-size: 18px;
}
.brand-lockup--launch {
--brand-emblem-size: 42px;
--brand-name-size: 22px;
}
@media (max-width: 430px) {
.brand-lockup--header,
.brand-lockup--launch {
--brand-emblem-size: 36px;
}
.brand-lockup--header .brand-lockup__copy,
.brand-lockup--launch .brand-lockup__copy {
display: none;
}
}
</style>
+197
View File
@@ -0,0 +1,197 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
const databaseRow = vi.hoisted(() => ({ value: undefined as Record<string, unknown> | undefined }));
vi.mock('$lib/server/database', () => ({
getDatabase: vi.fn(async () => ({
prepare: vi.fn(async () => ({
get: vi.fn(async () => databaseRow.value)
}))
}))
}));
import {
getSiteAccessBlock,
getSiteAccessSettings,
invalidateSiteAccessSettings,
isLaunchGateExempt,
siteSectionForPath,
type SiteAccessSettings,
type SiteSectionKey
} from './site-access';
function accessSettings(
overrides: Partial<Pick<SiteAccessSettings, 'comingSoonEnabled' | 'accountCreationEnabled'>> & {
sections?: Partial<SiteAccessSettings['sections']>;
} = {}
): SiteAccessSettings {
return {
comingSoonEnabled: overrides.comingSoonEnabled ?? false,
accountCreationEnabled: overrides.accountCreationEnabled ?? true,
sections: {
news: true,
database: true,
builds: true,
guides: true,
builder: true,
...overrides.sections
},
updatedAt: '2026-07-27T00:00:00.000Z'
};
}
describe('siteSectionForPath', () => {
it.each([
['/news', 'news'],
['/news/latest', 'news'],
['/database', 'database'],
['/database/weapons/first-wave-vector', 'database'],
['/builds', 'builds'],
['/builds/shared-loadout', 'builds'],
['/guides', 'guides'],
['/guides/combat-medic', 'guides'],
['/builder', 'builder'],
['/builder/loadout-id', 'builder']
])('maps %s to the %s launch section', (pathname, section) => {
expect(siteSectionForPath(pathname)).toBe(section);
});
it.each(['/', '/newsroom', '/database-tools', '/build', '/guide', '/builder-tools', '/isac'])(
'does not treat %s as a gated section',
(pathname) => {
expect(siteSectionForPath(pathname)).toBeNull();
}
);
});
describe('isLaunchGateExempt', () => {
it.each([
'/coming-soon',
'/sign-in',
'/terms',
'/dmca',
'/contact',
'/auth/discord',
'/auth/discord/callback',
'/api/health/database',
'/api/internal/notifications',
'/_app/immutable/entry/start.js',
'/favicon.svg'
])('keeps %s reachable while the launch gate is active', (pathname) => {
expect(isLaunchGateExempt(pathname)).toBe(true);
});
it.each([
'/',
'/news',
'/isac',
'/coming-soon/preview',
'/sign-in/help',
'/auth',
'/api/health',
'/api/internal',
'/favicon.png'
])('does not exempt %s', (pathname) => {
expect(isLaunchGateExempt(pathname)).toBe(false);
});
});
describe('getSiteAccessBlock', () => {
it('gives the global launch gate priority over a disabled section for anonymous visitors', () => {
const settings = accessSettings({
comingSoonEnabled: true,
sections: { builder: false }
});
expect(getSiteAccessBlock(settings, '/builder', false)).toEqual({ kind: 'launch' });
});
it('lets authenticated users bypass the global launch gate when the requested section is enabled', () => {
const settings = accessSettings({ comingSoonEnabled: true });
expect(getSiteAccessBlock(settings, '/builder', true)).toBeNull();
});
it.each([
['/news', 'news'],
['/database/weapons', 'database'],
['/builds/agent-loadout', 'builds'],
['/guides/combat-medic', 'guides'],
['/builder', 'builder']
] satisfies Array<[string, SiteSectionKey]>)(
'blocks %s for authenticated and anonymous visitors when the %s section is disabled',
(pathname, section) => {
const settings = accessSettings({
sections: { [section]: false }
});
expect(getSiteAccessBlock(settings, pathname, false)).toEqual({ kind: 'section', section });
expect(getSiteAccessBlock(settings, pathname, true)).toEqual({ kind: 'section', section });
}
);
it.each(['/coming-soon', '/sign-in', '/terms', '/dmca', '/contact', '/auth/discord/callback'])(
'keeps exempt route %s available while the global gate is active',
(pathname) => {
const settings = accessSettings({ comingSoonEnabled: true });
expect(getSiteAccessBlock(settings, pathname, false)).toBeNull();
expect(getSiteAccessBlock(settings, pathname, true)).toBeNull();
}
);
it('allows an enabled section when the global launch gate is off', () => {
const settings = accessSettings();
expect(getSiteAccessBlock(settings, '/news', false)).toBeNull();
expect(getSiteAccessBlock(settings, '/news', true)).toBeNull();
});
});
describe('getSiteAccessSettings', () => {
beforeEach(() => {
databaseRow.value = undefined;
invalidateSiteAccessSettings();
});
it('fails safely to the launch page while leaving registration and sections staged on a new database', async () => {
await expect(getSiteAccessSettings()).resolves.toEqual({
comingSoonEnabled: true,
accountCreationEnabled: true,
sections: {
news: true,
database: true,
builds: true,
guides: true,
builder: true
},
updatedAt: '2026-07-27T00:00:00.000Z'
});
});
it('normalizes persisted integer flags into the public settings shape', async () => {
databaseRow.value = {
coming_soon_enabled: 0,
account_creation_enabled: 0,
news_enabled: 1,
database_enabled: 0,
builds_enabled: 1,
guides_enabled: 0,
builder_enabled: 1,
updated_at: '2026-08-01T12:30:00.000Z'
};
await expect(getSiteAccessSettings()).resolves.toEqual({
comingSoonEnabled: false,
accountCreationEnabled: false,
sections: {
news: true,
database: false,
builds: true,
guides: false,
builder: true
},
updatedAt: '2026-08-01T12:30:00.000Z'
});
});
});
+85
View File
@@ -0,0 +1,85 @@
import { getDatabase } from '$lib/server/database';
export const SITE_SECTION_KEYS = ['news', 'database', 'builds', 'guides', 'builder'] as const;
export type SiteSectionKey = (typeof SITE_SECTION_KEYS)[number];
export type SiteAccessSettings = {
comingSoonEnabled: boolean;
accountCreationEnabled: boolean;
sections: Record<SiteSectionKey, boolean>;
updatedAt: string;
};
export type SiteAccessBlock =
| { kind: 'launch' }
| { kind: 'section'; section: SiteSectionKey };
const DEFAULT_SETTINGS: SiteAccessSettings = {
comingSoonEnabled: true,
accountCreationEnabled: true,
sections: { news: true, database: true, builds: true, guides: true, builder: true },
updatedAt: '2026-07-27T00:00:00.000Z'
};
let cached: { expiresAt: number; value: SiteAccessSettings } | null = null;
function normalized(row: Record<string, unknown> | undefined): SiteAccessSettings {
if (!row) return structuredClone(DEFAULT_SETTINGS);
return {
comingSoonEnabled: Boolean(row.coming_soon_enabled),
accountCreationEnabled: Boolean(row.account_creation_enabled),
sections: {
news: Boolean(row.news_enabled),
database: Boolean(row.database_enabled),
builds: Boolean(row.builds_enabled),
guides: Boolean(row.guides_enabled),
builder: Boolean(row.builder_enabled)
},
updatedAt: String(row.updated_at ?? DEFAULT_SETTINGS.updatedAt)
};
}
export function invalidateSiteAccessSettings() {
cached = null;
}
export async function getSiteAccessSettings() {
if (cached && cached.expiresAt > Date.now()) return cached.value;
const database = await getDatabase();
const row = await (await database.prepare("SELECT * FROM site_launch_settings WHERE id='primary'")).get();
const value = normalized(row);
cached = { value, expiresAt: Date.now() + 5_000 };
return value;
}
export function siteSectionForPath(pathname: string): SiteSectionKey | null {
for (const section of SITE_SECTION_KEYS) {
if (pathname === `/${section}` || pathname.startsWith(`/${section}/`)) return section;
}
return null;
}
export function isLaunchGateExempt(pathname: string) {
return pathname === '/coming-soon' ||
pathname === '/sign-in' ||
pathname === '/terms' ||
pathname === '/dmca' ||
pathname === '/contact' ||
pathname.startsWith('/auth/') ||
pathname.startsWith('/api/health/') ||
pathname.startsWith('/api/internal/') ||
pathname.startsWith('/_app/') ||
pathname === '/favicon.svg';
}
export function getSiteAccessBlock(
settings: SiteAccessSettings,
pathname: string,
authenticated: boolean
): SiteAccessBlock | null {
if (isLaunchGateExempt(pathname)) return null;
if (!authenticated && settings.comingSoonEnabled) return { kind: 'launch' };
const section = siteSectionForPath(pathname);
if (section && !settings.sections[section]) return { kind: 'section', section };
return null;
}
+7 -1
View File
@@ -2,8 +2,14 @@ import { getSessionUser } from '$lib/server/auth/session';
import { hasPermission } from '$lib/server/auth/authorization';
import type { LayoutServerLoad } from './$types';
import { notificationSummary } from '$lib/server/notifications';
import { getSiteAccessSettings } from '$lib/server/site-access';
export const load: LayoutServerLoad = async ({ cookies }) => {
const user = await getSessionUser(cookies);
return { user, isacAccess: await hasPermission(user, 'isac.access'), notifications: user ? await notificationSummary(user.id) : { unread: 0 } };
return {
user,
isacAccess: await hasPermission(user, 'isac.access'),
notifications: user ? await notificationSummary(user.id) : { unread: 0 },
siteAccess: await getSiteAccessSettings()
};
};
+6 -1
View File
@@ -9,6 +9,7 @@
import '../app.css';
import AppShell from '$lib/components/AppShell.svelte';
import ToastViewport from '$lib/components/ToastViewport.svelte';
import { page } from '$app/state';
let { data, children } = $props();
</script>
@@ -16,5 +17,9 @@
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
</svelte:head>
<AppShell user={data.user} isacAccess={data.isacAccess} notificationSummary={data.notifications}>{@render children()}</AppShell>
{#if page.url.pathname === '/coming-soon'}
{@render children()}
{:else}
<AppShell user={data.user} isacAccess={data.isacAccess} notificationSummary={data.notifications} siteAccess={data.siteAccess}>{@render children()}</AppShell>
{/if}
<ToastViewport/>
+2 -2
View File
@@ -5,10 +5,10 @@
{ title: 'Status Protocol', author: 'SHD_Warden', mode: 'PVE', rating: 4.7, votes: 76, tags: ['Skill', 'Status', 'Support'], color: '#d36b5c' }
];
</script>
<svelte:head><title>SHD.build — The Division Resurgence Community</title><meta name="description" content="The Division Resurgence community for builds, guides, game data, news, and agent discussion." /></svelte:head>
<svelte:head><title>SHD.build — The Division Resurgence Data & Tools</title><meta name="description" content="Builds, guides, game data, news, and agent tools for The Division Resurgence." /></svelte:head>
<main>
<section class="hero">
<div class="hero-copy"><p class="eyebrow"><span></span> THE DIVISION RESURGENCE COMMUNITY</p><h1>CONNECT.<br /><em>ADAPT. RISE.</em></h1><p class="lede">Build stronger agents, learn from field-tested guides, explore the game database, follow Resurgence news, and share intelligence with the community.</p><div class="hero-actions"><a class="primary" href="/builder">CREATE A BUILD <span>+</span></a><a class="secondary" href="/news">ENTER THE NETWORK ↓</a></div><div class="metrics community-metrics"><div><strong>BUILD</strong><span>LOADOUT LAB</span></div><div><strong>LEARN</strong><span>FIELD GUIDES</span></div><div><strong>TRACK</strong><span>DATA & NEWS</span></div></div></div>
<div class="hero-copy"><p class="eyebrow"><span></span> THE DIVISION RESURGENCE DATA & TOOLS</p><h1>CONNECT.<br /><em>ADAPT. RISE.</em></h1><p class="lede">Build stronger agents, learn from field-tested guides, explore the game database, follow Resurgence news, and share intelligence with the community.</p><div class="hero-actions"><a class="primary" href="/builder">CREATE A BUILD <span>+</span></a><a class="secondary" href="/news">ENTER THE NETWORK ↓</a></div><div class="metrics community-metrics"><div><strong>BUILD</strong><span>LOADOUT LAB</span></div><div><strong>LEARN</strong><span>FIELD GUIDES</span></div><div><strong>TRACK</strong><span>DATA & NEWS</span></div></div></div>
<div class="loadout-preview community-preview"><div class="scan-lines"></div><div class="preview-top"><span>COMMUNITY INTELLIGENCE</span><b>SHD // LIVE</b></div><div class="agent"><div class="orbit orbit-one"></div><div class="orbit orbit-two"></div><div class="agent-shape"><span>SHD</span></div>{#each ['BUILDS','GUIDES','DATA','NEWS'] as label,i}<a href={['/builds','/guides','/database','/news'][i]} class="gear-node n-{i}"><small>{label}</small><b>{['OPTIMIZE','LEARN','REFERENCE','UPDATES'][i]}</b></a>{/each}</div><div class="preview-name"><small>COMMUNITY // ONLINE</small><strong>RESURGENCE INTELLIGENCE HUB</strong><span>BUILT FOR AGENTS</span></div></div>
</section>
<section class="community-channels">
@@ -7,6 +7,7 @@ import { redirect } from '@sveltejs/kit';
import { createHash, randomUUID, timingSafeEqual } from 'node:crypto';
import type { RequestHandler } from './$types';
import { triggerAutomatedSystemMessages } from '$lib/server/automated-messages';
import { getSiteAccessSettings } from '$lib/server/site-access';
const STATE_COOKIE = 'shd_discord_oauth_state';
@@ -112,6 +113,9 @@ export const GET: RequestHandler = async ({ cookies, url, fetch, request, getCli
let created = false;
if (!user) {
if (!(await getSiteAccessSettings()).accountCreationEnabled) {
redirect(303, '/sign-in?oauth_error=registration_closed');
}
const username = await uniqueUsername();
const id = randomUUID();
const now = new Date().toISOString();
+17
View File
@@ -0,0 +1,17 @@
import { getSessionUser } from '$lib/server/auth/session';
import { getSiteAccessSettings, SITE_SECTION_KEYS } from '$lib/server/site-access';
import { redirect } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async ({ cookies, url }) => {
const user = await getSessionUser(cookies);
const settings = await getSiteAccessSettings();
const unavailable = url.searchParams.get('unavailable');
const unavailableSection = SITE_SECTION_KEYS.find((section) => section === unavailable) ?? null;
if (!user && !settings.comingSoonEnabled && !unavailableSection) redirect(303, '/');
return {
settings,
authenticated: Boolean(user),
unavailable: unavailableSection
};
};
+78
View File
@@ -0,0 +1,78 @@
<script lang="ts">
import BrandEmblem from '$lib/components/BrandEmblem.svelte';
import BrandLockup from '$lib/components/BrandLockup.svelte';
let { data } = $props();
type ChannelKey = 'news' | 'database' | 'builds' | 'guides' | 'builder';
const channels: Array<{ key: ChannelKey; label: string; detail: string }> = [
{ key: 'news', label: 'FIELD REPORTS', detail: 'Resurgence news and community intelligence' },
{ key: 'database', label: 'ISAC ARCHIVE', detail: 'Gear, weapons, talents, mods, and game data' },
{ key: 'builds', label: 'AGENT LOADOUTS', detail: 'Published builds from the SHD community' },
{ key: 'guides', label: 'FIELD MANUALS', detail: 'Detailed tactics, play styles, and build guides' },
{ key: 'builder', label: 'LOADOUT LAB', detail: 'A precise character and equipment builder' }
];
</script>
<svelte:head>
<title>SHD.build — Network deployment in progress</title>
<meta name="description" content="SHD.build is preparing a data and tools network for The Division Resurgence."/>
<meta name="robots" content="index,follow"/>
</svelte:head>
<main class="launch-page">
<div class="launch-grid" aria-hidden="true"></div>
<header class="launch-header">
<a class="brand" href="/coming-soon" aria-label="SHD.build coming soon">
<BrandLockup size="launch" />
</a>
<div class="launch-signal"><i></i><span>NETWORK STATUS</span><b>DEPLOYMENT IN PROGRESS</b></div>
</header>
<section class="launch-hero">
<div class="launch-copy">
<p class="eyebrow"><span></span> THE DIVISION RESURGENCE DATA & TOOLS</p>
<h1>INTELLIGENCE<br/><em>IS COMING.</em></h1>
<p class="launch-lede">SHD.build is assembling a tactical home for Resurgence agents—loadout engineering, field guides, game data, community builds, and mission-ready intelligence in one network.</p>
{#if data.unavailable}
<p class="launch-notice" role="status"><b>{data.unavailable.toUpperCase()} CHANNEL OFFLINE</b><span>This section is offline for all agents while deployment continues. Use an available channel or return later.</span></p>
{/if}
<div class="launch-actions">
<a class="primary" href={data.authenticated ? '/' : '/sign-in'}>{data.authenticated ? 'ENTER FULL SITE' : 'AGENT SIGN IN'} <span>→</span></a>
<a class="secondary" href="https://discord.gg/dhpd33VwQK">JOIN THE DISCORD ↗</a>
</div>
<p class="launch-access-note">{data.settings.accountCreationEnabled ? 'NEW AGENT REGISTRATION IS CURRENTLY OPEN.' : 'SIGN-IN IS LIMITED TO EXISTING AGENT IDENTITIES.'}</p>
</div>
<div class="launch-core" aria-label="SHD network deployment status">
<div class="launch-radar">
<div class="radar-ring ring-a"></div>
<div class="radar-ring ring-b"></div>
<div class="radar-ring ring-c"></div>
<div class="radar-sweep"></div>
<div class="radar-core"><BrandEmblem size="76%" /></div>
{#each channels.slice(0, 4) as channel, index}
<div class="radar-node node-{index}"><i></i><span>{channel.label}</span></div>
{/each}
</div>
<div class="launch-core-status"><span>ISAC NODE // NYC-01</span><b>COMMUNITY UPLINK INITIALIZING</b><small>SECURE ACCESS AVAILABLE</small></div>
</div>
</section>
<section class="launch-channels" aria-labelledby="launch-channels-title">
<header><div><p class="eyebrow"><span></span> DEPLOYMENT MANIFEST</p><h2 id="launch-channels-title">WHAT’S ENTERING THE NETWORK</h2></div><b>{channels.length.toString().padStart(2, '0')} CHANNELS</b></header>
<div>
{#each channels as channel, index}
<article>
<small>{String(index + 1).padStart(2, '0')} // {channel.label}</small>
<h3>{channel.detail}</h3>
<span class:available={data.settings.sections[channel.key]}> {data.settings.sections[channel.key] ? 'STAGED' : 'IN DEVELOPMENT'} </span>
</article>
{/each}
</div>
</section>
<footer class="launch-footer">
<p>UNOFFICIAL FAN PROJECT FOR TOM CLANCY’S THE DIVISION RESURGENCE</p>
<nav aria-label="Prelaunch support and legal"><a href="/contact">CONTACT</a><a href="/terms">TERMS & PRIVACY</a><a href="/dmca">DMCA</a></nav>
</footer>
</main>
+8 -8
View File
@@ -1,21 +1,21 @@
<svelte:head>
<title>DMCA & Copyright Policy — SHD.build</title>
<title>DMCA & Copyright Policy | SHD.build</title>
<meta name="description" content="SHD.build copyright, DMCA notice, counter-notice, and repeat-infringer policy."/>
</svelte:head>
<main class="legal-page">
<header class="legal-hero"><p class="eyebrow"><span></span> SHD COPYRIGHT OPERATIONS</p><h1>DMCA & <em>COPYRIGHT.</em></h1><p>SHD.build respects intellectual-property rights and provides a documented channel for copyright owners, authorized agents, and affected users.</p></header>
<header class="legal-hero"><p class="eyebrow"><span></span> SHD COPYRIGHT OPERATIONS</p><h1>DMCA & <em>COPYRIGHT.</em></h1><p>SHD.build respects intellectual-property rights. Copyright owners, their authorized agents, and affected users can use this policy to submit claims and counter-notices.</p></header>
<div class="legal-layout">
<nav class="legal-index" aria-label="DMCA policy sections">
<a href="#notice">01 · TAKEDOWN NOTICE</a><a href="#processing">02 · PROCESSING</a><a href="#counter">03 · COUNTER-NOTICE</a><a href="#repeat">04 · REPEAT INFRINGERS</a><a href="#misrepresentation">05 · ACCURACY</a><a href="#contact">06 · SUBMIT</a>
</nav>
<article class="legal-document">
<section id="notice"><span>01</span><div><h2>NOTICE OF CLAIMED INFRINGEMENT</h2><p>If you own a copyright or are authorized to act for its owner and believe material on SHD.build infringes that copyright, submit a written notice containing: your physical or electronic signature; identification of the copyrighted work or a representative list; identification and exact location of the material to remove or disable; sufficient contact information, including mailing address, telephone number, and email address; a good-faith statement that the disputed use is not authorized by the owner, its agent, or law; and a statement under penalty of perjury that the notice is accurate and you are authorized to act for the owner.</p><p>Use exact SHD.build URLs whenever possible. A notice that omits required information may delay review while we request clarification.</p></div></section>
<section id="processing"><span>02</span><div><h2>REVIEW & REMOVAL</h2><p>We review sufficiently complete notices and may remove or disable access to identified material expeditiously where appropriate. We may provide the notice, including the claimant’s identity and allegations, to the user who supplied the material so that user can understand the claim and exercise applicable counter-notice rights.</p><p>Submitting a notice does not guarantee removal, constitute a legal ruling, or waive any party’s rights.</p></div></section>
<section id="counter"><span>03</span><div><h2>COUNTER-NOTIFICATION</h2><p>A user who believes material was removed through mistake or misidentification may submit a counter-notice identifying the removed material and its prior location; providing a physical or electronic signature and complete contact information; stating under penalty of perjury a good-faith belief that removal resulted from mistake or misidentification; consenting to the jurisdiction of the appropriate United States federal district court; and agreeing to accept service of process from the original claimant or that claimant’s agent.</p><p>Where applicable, SHD.build may restore material no sooner than 10 and no later than 14 business days after forwarding a compliant counter-notice, unless the original claimant informs us that a court action has been filed seeking to restrain the allegedly infringing activity.</p></div></section>
<section id="repeat"><span>04</span><div><h2>REPEAT-INFRINGER POLICY</h2><p>SHD.build may restrict or terminate accounts of repeat infringers in appropriate circumstances. We consider the nature, credibility, and outcome of notices and counter-notices rather than treating every allegation as a final determination. We may also remove content or restrict accounts when infringement is otherwise established or apparent.</p></div></section>
<section id="misrepresentation"><span>05</span><div><h2>ACCURACY & MISREPRESENTATION</h2><p>Knowingly making a material misrepresentation that content is infringing, or that removed content was removed through mistake or misidentification, may result in liability under 17 U.S.C. § 512(f). Consider fair use, licenses, and other lawful uses before submitting a notice. This policy is operational information and not legal advice.</p><p>For official guidance, consult the <a href="https://www.copyright.gov/512/" rel="external noreferrer">U.S. Copyright Office Section 512 resources</a>.</p></div></section>
<section id="contact"><span>06</span><div><h2>SUBMIT A REQUEST</h2><p>Use the DMCA / Copyright destination on the SHD.build contact form. The form collects the elements needed for an initial takedown notice and routes the complete record to administrators. Counter-notices and follow-up documents may use the same channel.</p><a class="primary legal-primary" href="/contact?team=dmca">OPEN COPYRIGHT REQUEST →</a></div></section>
<section id="notice"><span>01</span><div><h2>NOTICE OF CLAIMED INFRINGEMENT</h2><p>If you own a copyright, or the owner has authorized you to act, and you believe material on SHD.build infringes that copyright, submit a written notice containing: your physical or electronic signature; identification of the copyrighted work or a representative list; identification and exact location of the material to remove or disable; sufficient contact information, including mailing address, telephone number, and email address; a good-faith statement that the owner, the owner’s agent, and the law do not authorize the disputed use; and a statement under penalty of perjury that the notice is accurate and you are authorized to act for the owner.</p><p>Use exact SHD.build URLs when possible. Missing information may delay our review while we request clarification.</p></div></section>
<section id="processing"><span>02</span><div><h2>REVIEW & REMOVAL</h2><p>We review notices that contain enough information and may remove the identified material or disable access expeditiously when appropriate. We may provide the notice, including the claimant’s identity and allegations, to the user who supplied the material. The user can then understand the claim and exercise applicable counter-notice rights.</p><p>Submitting a notice does not guarantee removal, constitute a legal ruling, or waive any party’s rights.</p></div></section>
<section id="counter"><span>03</span><div><h2>COUNTER-NOTIFICATION</h2><p>If you believe we removed material because of mistake or misidentification, you may submit a counter-notice that identifies the removed material and its prior location; provides your physical or electronic signature and complete contact information; states under penalty of perjury your good-faith belief that mistake or misidentification caused the removal; consents to the jurisdiction of the appropriate United States federal district court; and agrees to accept service of process from the original claimant or that claimant’s agent.</p><p>Where applicable, SHD.build may restore material no sooner than 10 and no later than 14 business days after we forward a compliant counter-notice, unless the original claimant informs us that it filed a court action seeking to restrain the allegedly infringing activity.</p></div></section>
<section id="repeat"><span>04</span><div><h2>REPEAT-INFRINGER POLICY</h2><p>SHD.build may restrict or terminate accounts of repeat infringers in appropriate circumstances. We assess the nature, credibility, and outcome of notices and counter-notices without treating each allegation as a final determination. We may remove content or restrict accounts when other evidence establishes or indicates infringement.</p></div></section>
<section id="misrepresentation"><span>05</span><div><h2>ACCURACY & MISREPRESENTATION</h2><p>If you knowingly make a material misrepresentation that content infringes a copyright, or that mistake or misidentification caused us to remove content, you may incur liability under 17 U.S.C. § 512(f). Review fair use, licenses, and other lawful uses before submitting a notice. This policy explains our process and does not provide legal advice.</p><p>Consult the <a href="https://www.copyright.gov/512/" rel="external noreferrer">U.S. Copyright Office Section 512 resources</a> for official guidance.</p></div></section>
<section id="contact"><span>06</span><div><h2>SUBMIT A REQUEST</h2><p>Select DMCA / Copyright on the SHD.build contact form. We use the form to collect the information required for an initial takedown notice and send the complete record to administrators. You may send counter-notices and follow-up documents through the same channel.</p><a class="primary legal-primary" href="/contact?team=dmca">OPEN COPYRIGHT REQUEST →</a></div></section>
</article>
</div>
</main>
+1
View File
@@ -40,6 +40,7 @@
];
const visibleOversightSections = $derived(oversightSections.filter((section) => can(section.permission)));
const systemSections = [
{ href: '/isac/site-controls', label: 'Site controls', permission: 'site.settings.manage' },
{ href: '/isac/images', label: 'Image library', permission: 'images.manage' },
{ href: '/isac/system-messages', label: 'Automated messages', permission: 'system.notifications.manage' },
{ href: '/isac/metrics', label: 'System metrics', permission: 'metrics.view' }
+1
View File
@@ -9,6 +9,7 @@ export const load: PageServerLoad = async ({ cookies }) => {
if (permissions.includes('users.roles')) redirect(303, '/isac/roles');
if (permissions.includes('*') || ['data.view','data.manage','data.propose','data.approve'].some((permission) => permissions.includes(permission))) redirect(303, '/isac/configuration');
if (permissions.includes('audit.view')) redirect(303, '/isac/audit');
if (permissions.includes('site.settings.manage')) redirect(303, '/isac/site-controls');
if (permissions.includes('images.manage')) redirect(303, '/isac/images');
if (permissions.includes('system.notifications.manage')) redirect(303, '/isac/system-messages');
if (permissions.includes('metrics.view')) redirect(303, '/isac/metrics');
+7 -2
View File
@@ -30,10 +30,15 @@ export const load: PageServerLoad = async ({ cookies, url }) => {
}
}, 'name');
const permissionPrefix = filter === 'isac' ? 'isac.%' : filter === 'data' ? 'data.%' : filter === 'users' ? 'users.%' : filter === 'audit' ? 'audit.%' : filter === 'system' ? 'system.%' : '';
const relatedPermissionFilter = filter === 'data'
? " OR p.name LIKE 'calculation.%'"
: filter === 'system'
? " OR p.name LIKE 'site.%' OR p.name LIKE 'metrics.%' OR p.name LIKE 'images.%'"
: '';
const conditions = [
...(permissionPrefix ? [`EXISTS (
SELECT 1 FROM role_permissions rp JOIN permissions p ON p.id=rp.permission_id
WHERE rp.role_id=roles.id AND (p.name LIKE ?${filter === 'data' ? " OR p.name LIKE 'calculation.%'" : ''})
WHERE rp.role_id=roles.id AND (p.name LIKE ?${relatedPermissionFilter})
)`] : []),
...(search ? ["roles.name LIKE ? ESCAPE '\\' COLLATE NOCASE"] : [])
];
@@ -45,7 +50,7 @@ export const load: PageServerLoad = async ({ cookies, url }) => {
SUM(CASE WHEN EXISTS (SELECT 1 FROM role_permissions rp JOIN permissions p ON p.id=rp.permission_id WHERE rp.role_id=roles.id AND (p.name LIKE 'data.%' OR p.name LIKE 'calculation.%')) THEN 1 ELSE 0 END) AS data,
SUM(CASE WHEN EXISTS (SELECT 1 FROM role_permissions rp JOIN permissions p ON p.id=rp.permission_id WHERE rp.role_id=roles.id AND p.name LIKE 'users.%') THEN 1 ELSE 0 END) AS users,
SUM(CASE WHEN EXISTS (SELECT 1 FROM role_permissions rp JOIN permissions p ON p.id=rp.permission_id WHERE rp.role_id=roles.id AND p.name LIKE 'audit.%') THEN 1 ELSE 0 END) AS audit
,SUM(CASE WHEN EXISTS (SELECT 1 FROM role_permissions rp JOIN permissions p ON p.id=rp.permission_id WHERE rp.role_id=roles.id AND p.name LIKE 'system.%') THEN 1 ELSE 0 END) AS system
,SUM(CASE WHEN EXISTS (SELECT 1 FROM role_permissions rp JOIN permissions p ON p.id=rp.permission_id WHERE rp.role_id=roles.id AND (p.name LIKE 'system.%' OR p.name LIKE 'site.%' OR p.name LIKE 'metrics.%' OR p.name LIKE 'images.%')) THEN 1 ELSE 0 END) AS system
FROM roles`)).get(),
(await database.prepare(`SELECT COUNT(*) AS total FROM roles ${where}`)).get(...filterArgs),
(await database.prepare(`SELECT id,name,description FROM permissions
@@ -0,0 +1,83 @@
import { randomUUID } from 'node:crypto';
import { fail, error } from '@sveltejs/kit';
import { requirePermission } from '$lib/server/auth/authorization';
import { getSessionUser } from '$lib/server/auth/session';
import { getDatabase } from '$lib/server/database';
import { recordIsacAudit } from '$lib/server/audit';
import { invalidateSiteAccessSettings } from '$lib/server/site-access';
import type { Actions, PageServerLoad } from './$types';
const SETTINGS_ID = 'primary';
const CONFIRMATION = 'apply-site-access-settings';
const settingSnapshot = async () => {
const database = await getDatabase();
return await (await database.prepare(
'SELECT * FROM site_launch_settings WHERE id=?'
)).get(SETTINGS_ID) ?? null;
};
const flagNames = [
'comingSoonEnabled', 'accountCreationEnabled', 'newsEnabled', 'databaseEnabled',
'buildsEnabled', 'guidesEnabled', 'builderEnabled'
] as const;
const enabled = (form: FormData, name: (typeof flagNames)[number]) =>
String(form.get(name)) === '1' ? 1 : 0;
export const load: PageServerLoad = async ({ cookies }) => {
await requirePermission(await getSessionUser(cookies), 'site.settings.manage');
const database = await getDatabase();
const settings = await (await database.prepare(`SELECT settings.*,
COALESCE(profile.display_name,user.username) AS updated_by_name
FROM site_launch_settings settings
LEFT JOIN users user ON user.id=settings.updated_by
LEFT JOIN user_profiles profile ON profile.user_id=user.id
WHERE settings.id=?`)).get(SETTINGS_ID);
if (!settings) error(500, 'Site launch settings are not initialized.');
return { settings };
};
export const actions: Actions = {
save: async ({ cookies, request }) => {
const operator = await requirePermission(await getSessionUser(cookies), 'site.settings.manage');
const form = await request.formData();
if (String(form.get('confirmation') ?? '') !== CONFIRMATION) {
return fail(400, { siteSettingsError: 'Review and confirm the complete site access profile before applying it.' });
}
if (flagNames.some((name) => !['0', '1'].includes(String(form.get(name))))) {
return fail(400, { siteSettingsError: 'Every site access control must have an explicit enabled or disabled state.' });
}
const before = await settingSnapshot();
if (!before) return fail(404, { siteSettingsError: 'Site launch settings are not initialized.' });
const next = {
comingSoonEnabled: enabled(form, 'comingSoonEnabled'),
accountCreationEnabled: enabled(form, 'accountCreationEnabled'),
newsEnabled: enabled(form, 'newsEnabled'),
databaseEnabled: enabled(form, 'databaseEnabled'),
buildsEnabled: enabled(form, 'buildsEnabled'),
guidesEnabled: enabled(form, 'guidesEnabled'),
builderEnabled: enabled(form, 'builderEnabled')
};
const database = await getDatabase();
const updatedAt = new Date().toISOString();
await (await database.prepare(`UPDATE site_launch_settings SET
coming_soon_enabled=?,account_creation_enabled=?,news_enabled=?,database_enabled=?,
builds_enabled=?,guides_enabled=?,builder_enabled=?,updated_at=?,updated_by=?
WHERE id=?`)).run(
next.comingSoonEnabled, next.accountCreationEnabled, next.newsEnabled, next.databaseEnabled,
next.buildsEnabled, next.guidesEnabled, next.builderEnabled, updatedAt, operator.id, SETTINGS_ID
);
invalidateSiteAccessSettings();
const after = await settingSnapshot();
await recordIsacAudit({
actorId: operator.id,
category: 'system_configuration',
action: 'site_access.updated',
entityId: SETTINGS_ID,
before,
after
});
return { siteSettingsSaved: true, resultToken: randomUUID() };
}
};
+163
View File
@@ -0,0 +1,163 @@
<script lang="ts">
import PendingForm from '$lib/components/PendingForm.svelte';
import SegmentedChoice from '$lib/components/SegmentedChoice.svelte';
let { data, form } = $props();
const binaryOptions = [
{ value: '1', label: 'ENABLED' },
{ value: '0', label: 'DISABLED' }
];
const launchOptions = [
{ value: '1', label: 'COMING SOON' },
{ value: '0', label: 'PUBLIC LAUNCH' }
];
const sectionDefinitions = [
{ key: 'news', label: 'NEWS', description: 'Field reports, updates, and community reporting.' },
{ key: 'database', label: 'DATABASE', description: 'Public game-data registries and tactical lookup.' },
{ key: 'builds', label: 'BUILDS', description: 'Published loadout discovery and shared build views.' },
{ key: 'guides', label: 'GUIDES', description: 'Long-form field manuals and build walkthroughs.' },
{ key: 'builder', label: 'BUILDER', description: 'Loadout creation, calculation, and account saves.' }
] as const;
const settingValue = (key: string) => Number(data.settings[key]) ? '1' : '0';
let comingSoonEnabled = $state(settingValue('coming_soon_enabled'));
let accountCreationEnabled = $state(settingValue('account_creation_enabled'));
let sectionValues = $state<Record<string, string>>(Object.fromEntries(
sectionDefinitions.map((section) => [section.key, settingValue(`${section.key}_enabled`)])
));
let reviewing = $state(false);
let synchronizedAt = $state('');
let handledResultToken = $state('');
const synchronize = () => {
comingSoonEnabled = settingValue('coming_soon_enabled');
accountCreationEnabled = settingValue('account_creation_enabled');
sectionValues = Object.fromEntries(sectionDefinitions.map((section) => [
section.key,
settingValue(`${section.key}_enabled`)
]));
};
$effect(() => {
const updatedAt = String(data.settings.updated_at);
if (updatedAt === synchronizedAt) return;
synchronizedAt = updatedAt;
synchronize();
});
$effect(() => {
if (!form?.siteSettingsSaved || !form.resultToken || form.resultToken === handledResultToken) return;
handledResultToken = form.resultToken;
reviewing = false;
});
const currentValue = (key: string) => Number(data.settings[key]) ? 'ENABLED' : 'DISABLED';
const proposedValue = (value: string) => value === '1' ? 'ENABLED' : 'DISABLED';
const changes = $derived.by(() => {
const rows = [
{
label: 'LAUNCH GATE',
before: Number(data.settings.coming_soon_enabled) ? 'COMING SOON' : 'PUBLIC LAUNCH',
after: comingSoonEnabled === '1' ? 'COMING SOON' : 'PUBLIC LAUNCH'
},
{
label: 'ACCOUNT CREATION',
before: currentValue('account_creation_enabled'),
after: proposedValue(accountCreationEnabled)
},
...sectionDefinitions.map((section) => ({
label: section.label,
before: currentValue(`${section.key}_enabled`),
after: proposedValue(sectionValues[section.key] ?? '0')
}))
];
return rows.filter((row) => row.before !== row.after);
});
const date = (value: unknown) => value
? new Intl.DateTimeFormat(undefined, { dateStyle: 'medium', timeStyle: 'short' }).format(new Date(String(value)))
: 'INITIAL CONFIGURATION';
</script>
<svelte:head><title>Site controls — ISAC</title></svelte:head>
<div class="module-toolbar site-control-toolbar">
<div>
<p class="eyebrow">SYSTEM CONFIGURATION / RELEASE CONTROL</p>
<h2>SITE CONTROLS</h2>
<p>Coordinate public launch state, account registration, and access to major SHD.build sections.</p>
</div>
<div class="site-control-state">
<span>NETWORK STATE</span>
<strong class:restricted={comingSoonEnabled === '1'}>{comingSoonEnabled === '1' ? 'COMING SOON' : 'PUBLIC'}</strong>
<small>LAST UPDATED {date(data.settings.updated_at)}</small>
</div>
</div>
<div class="site-control-grid">
<section class="site-control-panel launch-control">
<header><div><span>01 / LAUNCH GATE</span><h3>PUBLIC NETWORK STATE</h3></div><b class:restricted={comingSoonEnabled === '1'}>{comingSoonEnabled === '1' ? 'GATED' : 'LIVE'}</b></header>
<p>The Coming Soon gate takes priority over individual sections for signed-out visitors. Signed-in agents bypass the launch gate, but section availability still applies.</p>
<SegmentedChoice legend="LAUNCH STATE" name="launchStateDraft" options={launchOptions} bind:value={comingSoonEnabled}/>
</section>
<section class="site-control-panel account-control">
<header><div><span>02 / IDENTITY INTAKE</span><h3>ACCOUNT CREATION</h3></div><b class:restricted={accountCreationEnabled === '0'}>{accountCreationEnabled === '1' ? 'OPEN' : 'CLOSED'}</b></header>
<p>Controls creation of new accounts across supported registration methods. Existing agents retain their configured sign-in methods.</p>
<SegmentedChoice legend="REGISTRATION STATE" name="accountCreationDraft" options={binaryOptions} bind:value={accountCreationEnabled}/>
</section>
<section class="site-control-panel public-sections">
<header><div><span>03 / PUBLIC SECTIONS</span><h3>SECTION AVAILABILITY</h3></div><b>{sectionDefinitions.filter((section) => sectionValues[section.key] === '1').length} / {sectionDefinitions.length} ONLINE</b></header>
<p>Section controls apply after the Launch Gate. Disabled sections disappear from navigation and reject direct access and mutations for every agent.</p>
<div class="section-control-list">
{#each sectionDefinitions as section}
<article>
<div><strong>{section.label}</strong><small>{section.description}</small></div>
<SegmentedChoice legend={`${section.label} STATE`} name={`${section.key}StateDraft`} options={binaryOptions} bind:value={sectionValues[section.key]}/>
</article>
{/each}
</div>
</section>
</div>
{#if form?.siteSettingsError}<p class="error">{form.siteSettingsError}</p>{/if}
<div class="site-control-actions">
<div><span>CHANGE SET</span><b>{changes.length ? `${changes.length} PENDING` : 'SYNCHRONIZED'}</b><small>UPDATED BY {data.settings.updated_by_name ?? 'SYSTEM BOOTSTRAP'}</small></div>
<div class="site-control-action-buttons">
<a class="secondary" href="/coming-soon">PREVIEW PAGE ↗</a>
<button class="primary" disabled={!changes.length} onclick={() => reviewing = true}>REVIEW CHANGES →</button>
</div>
</div>
{#if reviewing}
<div class="isac-modal-backdrop" role="presentation">
<div class="isac-confirm-modal site-control-review" role="dialog" aria-modal="true" aria-labelledby="site-control-review-title">
<div class="editor-title">
<div><p class="eyebrow">CONFIRM SITE ACCESS PROFILE</p><h3 id="site-control-review-title">{changes.length} OPERATIONAL CHANGE{changes.length === 1 ? '' : 'S'}</h3></div>
<button type="button" aria-label="Close site control review" onclick={() => reviewing = false}>×</button>
</div>
<p>These settings change which public SHD.build services agents can reach. Review the full change set before deployment.</p>
<div class="site-control-change-list">
{#each changes as change}
<div><strong>{change.label}</strong><span>{change.before}</span><b>→</b><span class:restricted={change.after === 'DISABLED' || change.after === 'COMING SOON'}>{change.after}</span></div>
{/each}
</div>
<PendingForm action="?/save" successMessage="Site access settings updated.">
{#snippet children(pending)}
<input type="hidden" name="confirmation" value="apply-site-access-settings"/>
<input type="hidden" name="comingSoonEnabled" value={comingSoonEnabled}/>
<input type="hidden" name="accountCreationEnabled" value={accountCreationEnabled}/>
{#each sectionDefinitions as section}<input type="hidden" name={`${section.key}Enabled`} value={sectionValues[section.key]}/>{/each}
<div class="form-actions">
<button type="button" class="secondary" onclick={() => reviewing = false} disabled={pending}>CANCEL</button>
<button class="primary" disabled={pending || !changes.length} aria-busy={pending}>{pending ? 'APPLYING PROFILE…' : 'CONFIRM SITE CONTROLS →'}</button>
</div>
{/snippet}
</PendingForm>
</div>
</div>
{/if}
<style>
.site-control-toolbar{align-items:stretch}.site-control-state{display:grid;align-content:center;min-width:230px;padding:10px 14px;border:1px solid var(--shd-border);border-left:2px solid var(--shd-orange);background:var(--shd-surface)}.site-control-state span,.site-control-state small,.site-control-panel header span,.site-control-actions span,.site-control-actions small{color:var(--shd-text-muted);font-size:var(--shd-type-telemetry);font-weight:900;letter-spacing:.8px}.site-control-state strong{color:var(--shd-success);font-size:21px}.site-control-state strong.restricted,.site-control-panel b.restricted,.site-control-change-list span.restricted{color:var(--shd-danger)}.site-control-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}.site-control-panel{display:grid;align-content:start;gap:10px;padding:12px;border:1px solid var(--shd-border);background:var(--shd-surface)}.site-control-panel>header{display:flex;justify-content:space-between;gap:12px;align-items:start;padding-bottom:9px;border-bottom:1px solid var(--shd-border)}.site-control-panel h3,.site-control-panel p{margin:0}.site-control-panel>p{color:var(--shd-text-muted);font-size:var(--shd-type-supporting)}.site-control-panel header b{color:var(--shd-success);font-size:var(--shd-type-supporting);letter-spacing:.8px}.public-sections{grid-column:1/-1}.section-control-list{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:7px}.section-control-list article{display:grid;grid-template-columns:minmax(0,1fr) minmax(220px,.8fr);gap:10px;align-items:center;padding:9px;border:1px solid var(--shd-border);background:#0d1011}.section-control-list article>div{display:grid;gap:2px}.section-control-list small{color:var(--shd-text-muted)}.site-control-actions{display:flex;justify-content:space-between;align-items:center;gap:12px;margin-top:9px;padding:10px;border:1px solid var(--shd-border);background:var(--shd-surface)}.site-control-actions>div:first-child{display:grid}.site-control-action-buttons{display:flex;gap:7px}.site-control-action-buttons>a{display:flex;align-items:center}.site-control-actions b{color:var(--shd-orange)}.site-control-review>p{margin:0;padding:12px;color:var(--shd-text-muted)}.site-control-change-list{display:grid;gap:4px;padding:0 12px 12px}.site-control-change-list>div{display:grid;grid-template-columns:minmax(140px,1fr) minmax(110px,.7fr) 28px minmax(110px,.7fr);gap:7px;align-items:center;padding:8px;border:1px solid var(--shd-border);background:#0d1011}.site-control-change-list>div>b{text-align:center;color:var(--shd-orange)}.site-control-change-list span:last-child{font-weight:900}.site-control-review .form-actions{padding:12px;border-top:1px solid var(--shd-border)}
@media(max-width:900px){.site-control-grid{grid-template-columns:1fr}.public-sections{grid-column:auto}.section-control-list{grid-template-columns:1fr}}
@media(max-width:650px){.site-control-state{min-width:0}.section-control-list article{grid-template-columns:1fr}.site-control-actions{display:grid}.site-control-action-buttons{display:grid;grid-template-columns:1fr}.site-control-action-buttons>*{width:100%;justify-content:center}.site-control-change-list>div{grid-template-columns:1fr auto 1fr}.site-control-change-list strong{grid-column:1/-1}.site-control-change-list span:last-child{text-align:right}}
</style>
+29 -8
View File
@@ -8,15 +8,23 @@ import { fail, redirect } from '@sveltejs/kit';
import { randomUUID } from 'node:crypto';
import type { Actions, PageServerLoad } from './$types';
import { triggerAutomatedSystemMessages } from '$lib/server/automated-messages';
import { getSiteAccessSettings } from '$lib/server/site-access';
export const load: PageServerLoad = async ({ url }) => ({
emailDeliveryConfigured: emailDeliveryConfigured(),
magicLinkSent: url.searchParams.has('magic_sent'),
magicError: url.searchParams.get('magic_error') === 'unavailable'
? 'This account is not permitted to sign in.'
: url.searchParams.has('magic_error') ? 'That sign-in link is invalid or expired.' : null,
oauthError: url.searchParams.has('oauth_error') ? 'Discord sign-in could not be completed.' : null
});
export const load: PageServerLoad = async ({ url }) => {
const { accountCreationEnabled } = await getSiteAccessSettings();
const oauthError = url.searchParams.get('oauth_error');
return {
accountCreationEnabled,
emailDeliveryConfigured: emailDeliveryConfigured(),
magicLinkSent: url.searchParams.has('magic_sent'),
magicError: url.searchParams.get('magic_error') === 'unavailable'
? 'This account is not permitted to sign in.'
: url.searchParams.has('magic_error') ? 'That sign-in link is invalid or expired.' : null,
oauthError: oauthError === 'registration_closed'
? 'New account registration is currently closed. Existing Discord-linked agents can still sign in.'
: oauthError ? 'Discord sign-in could not be completed.' : null
};
};
export const actions: Actions = {
magicLink: async ({ request }) => {
@@ -38,6 +46,12 @@ export const actions: Actions = {
purpose = 'verify_email';
}
if (!identity?.user_id) {
if (!(await getSiteAccessSettings()).accountCreationEnabled) {
return fail(403, {
magicLinkError: 'New account registration is currently closed. Enter an email already linked to an agent account.',
email
});
}
const id = randomUUID(), now = new Date().toISOString();
await (await database.prepare('INSERT INTO users(id,username,role,created_at) VALUES(?,?,?,?)'))
.run(id, `agent_${id.replaceAll('-', '').slice(0, 16)}`, 'user', now);
@@ -56,6 +70,13 @@ export const actions: Actions = {
return { magicLinkSent: true };
},
register: async ({ request, cookies, getClientAddress }) => {
if (!(await getSiteAccessSettings()).accountCreationEnabled) {
return fail(403, {
registerError: 'New account registration is currently closed.',
registerDisplayName: '',
registerUsername: ''
});
}
const form = await request.formData();
const displayName = String(form.get('displayName') ?? '').normalize('NFKC').trim();
const username = String(form.get('username') ?? '').normalize('NFKC').trim();
+10 -6
View File
@@ -10,9 +10,12 @@
let passwordMode = $state<'signin' | 'create'>('signin');
let passwordPanelOpen = $state(false);
$effect(() => {
if (form?.registerError) {
if (form?.registerError && data.accountCreationEnabled) {
passwordMode = 'create';
passwordPanelOpen = true;
} else if (form?.registerError) {
passwordMode = 'signin';
passwordPanelOpen = true;
} else if (form?.loginError) {
passwordMode = 'signin';
passwordPanelOpen = true;
@@ -50,12 +53,13 @@
</script>
<svelte:head><title>Sign in — SHD.build</title></svelte:head>
<main class="auth-page"><div class="auth-frame"><section class="auth-intel"><p class="eyebrow"><span></span> SHD COMMUNITY IDENTITY</p><h1>AGENT<br/><em>ACCESS.</em></h1><p>Authenticate to publish builds and guides, contribute game data, join discussions, and keep your SHD intelligence synchronized.</p><div class="auth-status"><span><i></i> NETWORK ONLINE</span><span>NODE NYC-01</span></div></section><section class="auth-console"><div class="console-head"><span>SECURE SIGN-IN</span><b>SHD // AUTH</b></div>
<p class="auth-mode-note">Sign in to an existing account or create one with email, Discord, or local credentials.</p>
<p class="auth-mode-note">{data.accountCreationEnabled ? 'Sign in to an existing account or create one with email, Discord, or local credentials.' : 'Sign in to an existing agent account with a configured authentication method.'}</p>
{#if !data.accountCreationEnabled}<p class="account-alert"><b>REGISTRATION PAUSED</b><span>New agent accounts cannot be created right now. Existing accounts retain full sign-in access.</span></p>{/if}
<button class="primary full passkey-signin" disabled={!passkeySupported || passkeyPending} aria-busy={passkeyPending} onclick={signInWithPasskey}>{passkeyPending ? 'VERIFYING PASSKEY…' : 'SIGN IN WITH A PASSKEY →'}</button>
{#if !passkeySupported}<p class="auth-note">This browser does not support passkeys.</p>{/if}
{#if passkeyError}<p class="error">{passkeyError}</p>{/if}
<div class="divider"><span>OR EMAIL A ONE-TIME LINK</span></div>
<PendingForm action="?/magicLink"><label class="field">VERIFIED EMAIL<input name="email" type="email" value={form?.email ?? ''} autocomplete="email" placeholder="agent@example.com" required /></label>{#if form?.magicLinkError}<p class="error">{form.magicLinkError}</p>{/if}{#if form?.magicLinkSent || data.magicLinkSent}<p class="success">Your one-time access link is on its way.</p>{/if}{#if data.magicError}<p class="error">{data.magicError}</p>{/if}<button class="secondary full" disabled={!data.emailDeliveryConfigured}>EMAIL ACCESS LINK →</button><small class="identity-note">A verified link signs you in, or creates your SHD.build account if this email is new.</small></PendingForm>
<PendingForm action="?/magicLink"><label class="field">VERIFIED EMAIL<input name="email" type="email" value={form?.email ?? ''} autocomplete="email" placeholder="agent@example.com" required /></label>{#if form?.magicLinkError}<p class="error">{form.magicLinkError}</p>{/if}{#if form?.magicLinkSent || data.magicLinkSent}<p class="success">Your one-time access link is on its way.</p>{/if}{#if data.magicError}<p class="error">{data.magicError}</p>{/if}<button class="secondary full" disabled={!data.emailDeliveryConfigured}>EMAIL ACCESS LINK →</button><small class="identity-note">{data.accountCreationEnabled ? 'A verified link signs you in, or creates your SHD.build account if this email is new.' : 'Email access is available only to addresses already linked to an agent account.'}</small></PendingForm>
<div class="divider"><span>OR CONTINUE WITH</span></div>
<div class="signin-provider-rail" aria-label="External sign-in providers">
<a class="signin-provider discord-provider" class:interaction-pending={discordPending} href="/auth/discord" aria-busy={discordPending} onclick={beginDiscordSignIn}>
@@ -82,13 +86,13 @@
<details class="password-fallback" bind:open={passwordPanelOpen}><summary>USE USERNAME AND PASSWORD</summary>
<div class="auth-mode-switch" role="group" aria-label="Password access mode">
<button type="button" class:active={passwordMode === 'signin'} onclick={() => passwordMode = 'signin'}>SIGN IN</button>
<button type="button" class:active={passwordMode === 'create'} onclick={() => passwordMode = 'create'}>CREATE ACCOUNT</button>
{#if data.accountCreationEnabled}<button type="button" class:active={passwordMode === 'create'} onclick={() => passwordMode = 'create'}>CREATE ACCOUNT</button>{/if}
</div>
{#if passwordMode === 'signin'}
<PendingForm action="?/login"><label class="field">USERNAME<input name="username" value={form?.username ?? ''} autocomplete="username webauthn" placeholder="Private login username" required /></label><label class="field">PASSWORD<input name="password" type="password" autocomplete="current-password" placeholder="••••••••••••" required /></label>{#if form?.loginError}<p class="error">{form.loginError}</p>{/if}<button class="secondary full">SIGN IN WITH PASSWORD →</button></PendingForm>
{:else}
{:else if data.accountCreationEnabled}
<PendingForm action="?/register"><label class="field">DISPLAY NAME<input name="displayName" value={form?.registerDisplayName ?? ''} autocomplete="nickname" placeholder="Public agent name" required /></label><label class="field">PRIVATE LOGIN USERNAME<input name="username" value={form?.registerUsername ?? ''} autocomplete="username" placeholder="ASCII letters, numbers, _ or -" required /></label><label class="field">PASSWORD<input name="password" type="password" autocomplete="new-password" placeholder="12–128 characters" minlength="12" maxlength="128" required /></label><label class="field">CONFIRM PASSWORD<input name="confirmPassword" type="password" autocomplete="new-password" placeholder="Repeat password" minlength="12" maxlength="128" required /></label>{#if form?.registerError}<p class="error">{form.registerError}</p>{/if}<button class="secondary full">CREATE ACCOUNT →</button></PendingForm>
{/if}
</details>
<small>Passkeys are preferred for returning agents and can be added after account creation. Password sign-in remains optional.</small>
<small>{data.accountCreationEnabled ? 'Passkeys are preferred for returning agents and can be added after account creation. Password sign-in remains optional.' : 'Existing agents can continue with a passkey, verified email, Discord, or configured username and password.'}</small>
</section></div></main>
+246 -12
View File
@@ -1,13 +1,19 @@
<svelte:head>
<title>Terms & Privacy — SHD.build</title>
<meta name="description" content="Terms of service, privacy practices, and fan-site disclaimer for SHD.build."/>
<title>Terms & Privacy | SHD.build</title>
<meta
name="description"
content="Terms of service, privacy practices, and fan-site disclaimer for SHD.build."
/>
</svelte:head>
<main class="legal-page">
<header class="legal-hero">
<p class="eyebrow"><span></span> SHD NETWORK POLICY</p>
<h1>TERMS & <em>PRIVACY.</em></h1>
<p>Effective July 25, 2026. These terms describe the rules for using SHD.build and the information processed to operate and secure the service.</p>
<p>
Effective July 25, 2026. These terms set the rules for using SHD.build and explain what
information we process to operate and secure the service.
</p>
</header>
<div class="legal-layout">
@@ -19,15 +25,243 @@
<a href="#disclaimers">09 · DISCLAIMERS</a>
</nav>
<article class="legal-document">
<section id="service"><span>01</span><div><h2>THE SERVICE</h2><p>SHD.build is an independent community service for The Division Resurgence, including loadouts, guides, community-maintained game data, news, and discussion. By accessing or using it, you agree to these terms. If you do not agree, do not use the service. Features may change, be suspended, or be discontinued as development continues.</p></div></section>
<section id="accounts"><span>02</span><div><h2>ACCOUNTS & SECURITY</h2><p>You are responsible for activity performed through your account and for maintaining control of your authentication methods. You must provide accurate information, may not impersonate another person, and may not attempt to access another account. We may restrict accounts to protect users, enforce these terms, or comply with law.</p><p>Available authentication methods may include passkeys, verified email magic links, local username and password credentials, Discord, and other providers added later. Third-party authentication remains subject to that provider’s terms and privacy practices.</p></div></section>
<section id="content"><span>03</span><div><h2>USER CONTENT</h2><p>You retain ownership of original content you submit. You grant SHD.build a non-exclusive, worldwide, royalty-free license to host, reproduce, format, display, and distribute that content as necessary to operate the service. You represent that you have the rights required to post it.</p><p>Builds, guides, and comments may remain available after account deletion under a “Deleted Agent” attribution so community references are not broken. You may not upload unlawful, infringing, malicious, or privacy-invasive content.</p></div></section>
<section id="conduct"><span>04</span><div><h2>COMMUNITY CONDUCT</h2><p>Do not harass others; publish illegal or infringing material; distribute malware; evade access restrictions; scrape or overload the service; manipulate ratings; spam; or exploit security vulnerabilities. Community Oversight may remove content, restrict access, or preserve relevant records when reasonably necessary for safety, support, abuse prevention, or legal obligations.</p></div></section>
<section id="privacy"><span>05</span><div><h2>INFORMATION WE PROCESS</h2><p>Account data may include internal account identifiers, private login username, public display name, password hash, verified email, linked-provider identifiers and profile details, passkey public credentials, avatar, roles, permissions, builds, guides, favorites, ratings, comments, notification messages, read state, and delivery preferences. Passwords are stored only as salted Argon2id hashes. Private passkey material remains on your authenticator.</p><p>For login security and support, events may include the time, success status, authentication provider, IP address, user-agent string, normalized browser, operating system and device type, approximate country/region/city when supplied by the hosting proxy, failure reason, automated risk flags, provider or passkey credential reference, associated session identifier, and session end time or reason. We do not sell this information. It is used for authentication, abuse prevention, troubleshooting, moderation, and account support.</p><p>Contact submissions include the sender’s name, reply email, destination, subject, message, account identifier when signed in, and submission time. Copyright requests may additionally include claimant contact details, identified works and URLs, attestations, and electronic signature. These records are shared only with authorized recipients and retained as reasonably necessary to respond, document action, resolve disputes, enforce policy, and meet legal obligations.</p><p>Browser and email notification delivery is optional except that operational ISAC notices remain available in the in-app inbox. Email frequency is configurable by notification category and defaults to off. OAuth and email providers process information under their own policies.</p></div></section>
<section id="cookies"><span>06</span><div><h2>ESSENTIAL COOKIES</h2><p>SHD.build currently uses only first-party cookies that are strictly necessary to provide authentication and protect sign-in flows. We do not currently use analytics, advertising, behavioral-tracking, or personalization cookies. These essential cookies cannot be disabled through a preference control. You may prevent cookies through your browser, but account sign-in will not function correctly.</p><div class="legal-table-wrap"><table class="legal-table"><thead><tr><th>COOKIE</th><th>PURPOSE</th><th>DURATION</th><th>PROTECTION</th></tr></thead><tbody><tr><td><code>shd_session</code></td><td>Keeps you securely signed in and associates requests with your server-side session.</td><td>Up to 30 days, or until sign-out or session invalidation.</td><td>HTTP-only, SameSite=Lax, Secure in production.</td></tr><tr><td><code>shd_discord_oauth_state</code></td><td>Validates a Discord sign-in or linking response and helps prevent request forgery.</td><td>Up to 10 minutes; removed when the callback is processed.</td><td>HTTP-only, SameSite=Lax, Secure in production; limited to the Discord authentication path.</td></tr></tbody></table></div><p>The informational notice remembers its dismissal in browser local storage under <code>shd-essential-cookie-notice</code>. It is not sent with web requests and can be removed through your browser’s site-data controls. OAuth state remains in a short-lived HTTP-only cookie because it must be securely validated by the server and unavailable to browser scripts.</p><p>If non-essential technologies are introduced later, this policy and the site’s consent controls will be updated before those technologies are enabled where consent is required.</p></div></section>
<section id="retention"><span>07</span><div><h2>RETENTION & DELETION</h2><p>IP addresses, user-agent strings, normalized device information, and credential references in login history are retained for no more than 30 days, then automatically removed from the event. Non-sensitive facts such as event time, provider, outcome, failure category, and whether a session was created may remain for security and support history.</p><p>Deleting your account removes active sessions, credentials, verified email, provider profiles, passkeys, permissions, and uploaded avatar. You may also select immediate purging of retained login-event history during self-deletion. Public contributions may remain attributed to “Deleted Agent.” Backups and records required for fraud prevention, dispute resolution, or law may persist for a limited period where applicable.</p></div></section>
<section id="third-parties"><span>08</span><div><h2>THIRD-PARTY SERVICES</h2><p>SHD.build may connect to Discord, email delivery services, and future identity providers. Their services are governed by their own terms. Links to other sites do not imply endorsement. You authorize the minimum provider data requested when choosing a linked sign-in method.</p></div></section>
<section id="disclaimers"><span>09</span><div><h2>FAN-SITE & LEGAL DISCLAIMER</h2><p>SHD.build is an unofficial fan-created service and is not affiliated with, endorsed by, sponsored by, or approved by Ubisoft Entertainment or its affiliates.</p><p>Tom Clancy’s The Division, Tom Clancy’s The Division Resurgence, Ubisoft, and related names, trademarks, logos, characters, artwork, game data, and other intellectual property are owned by Ubisoft and/or its licensors. Their use is for identification, commentary, and community reference. No ownership is claimed.</p><p>The service and community information are provided “as is” and “as available,” without warranties of accuracy, availability, fitness for a particular purpose, or non-infringement to the extent permitted by law. Build data may become outdated after game updates. SHD.build is not responsible for third-party services or decisions made using community content.</p><p>Copyright owners and authorized agents may use our <a href="/dmca">DMCA & Copyright Policy</a> and <a href="/contact?team=dmca">copyright request form</a>. Other questions may be sent through <a href="/contact">Contact Us</a>.</p><p>These terms may be updated as the service changes. Material revisions will be identified by a new effective date. Nothing here limits rights that cannot legally be waived.</p><p>For authoritative information about Ubisoft intellectual property, consult Ubisoft’s official <a href="https://www.ubisoft.com/legal/documents/termsofuse/en-US" rel="external">Terms of Use</a> and the official <a href="https://www.ubisoft.com/game/the-division/resurgence" rel="external">The Division Resurgence site</a>.</p></div></section>
<section id="service">
<span>01</span>
<div>
<h2>THE SERVICE</h2>
<p>
SHD.build is an independent community service for The Division Resurgence. It
includes loadouts, guides, community-maintained game data, news, and discussion.
By accessing or using SHD.build, you agree to these terms. If you do not agree, do
not use the service. We may change, suspend, or discontinue features as development
continues.
</p>
</div>
</section>
<section id="accounts">
<span>02</span>
<div>
<h2>ACCOUNTS & SECURITY</h2>
<p>
You are responsible for activity through your account and for keeping control of
your authentication methods. You must provide accurate information. You may not
impersonate another person or attempt to access another account. We may restrict
accounts to protect users, enforce these terms, or comply with law.
</p>
<p>
SHD.build may offer passkeys, verified email magic links, local username and
password credentials, Discord, and other authentication providers added later.
Each third-party provider applies its own terms and privacy practices.
</p>
</div>
</section>
<section id="content">
<span>03</span>
<div>
<h2>USER CONTENT</h2>
<p>
You retain ownership of original content you submit. You grant SHD.build a
non-exclusive, worldwide, royalty-free license to host, reproduce, format, display,
and distribute that content as necessary to operate the service. You represent that
you have the rights required to post it.
</p>
<p>
SHD.build may keep builds, guides, and comments available after account deletion
under a “Deleted Agent” attribution to preserve community references. You may not
upload unlawful, infringing, malicious, or privacy-invasive content.
</p>
</div>
</section>
<section id="conduct">
<span>04</span>
<div>
<h2>COMMUNITY CONDUCT</h2>
<p>
Do not harass others; publish illegal or infringing material; distribute malware;
evade access restrictions; scrape or overload the service; manipulate ratings; spam;
or exploit security vulnerabilities. Community Oversight may remove content,
restrict access, or preserve relevant records when reasonably necessary for safety,
support, abuse prevention, or legal obligations.
</p>
</div>
</section>
<section id="privacy">
<span>05</span>
<div>
<h2>INFORMATION WE PROCESS</h2>
<p>
We may process internal account identifiers, private login username, public display
name, password hash, verified email, linked-provider identifiers and profile
details, passkey public credentials, avatar, roles, permissions, builds, guides,
favorites, ratings, comments, notification messages, read state, and delivery
preferences. SHD.build stores passwords only as salted Argon2id hashes. Private
passkey material remains on your authenticator.
</p>
<p>
For login security and support, we may record the time, success status,
authentication provider, IP address, user-agent string, normalized browser,
operating system and device type, approximate country, region, and city when the
hosting proxy supplies them, failure reason, automated risk flags, provider or
passkey credential reference, associated session identifier, and session end time
or reason. We do not sell this information. We use it for authentication, abuse
prevention, troubleshooting, moderation, and account support.
</p>
<p>
When you contact us, we collect the sender’s name, reply email, destination,
subject, message, account identifier when signed in, and submission time. Copyright
requests may also include claimant contact details, identified works and URLs,
attestations, and electronic signature. We share these records only with authorized
recipients and keep them as long as reasonably necessary to respond, document
action, resolve disputes, enforce policy, and meet legal obligations.
</p>
<p>
Browser and email notification delivery is optional. Operational ISAC notices
remain available in the in-app inbox. You can configure email frequency by
notification category, and email delivery defaults to off. OAuth and email
providers process information under their own policies.
</p>
</div>
</section>
<section id="cookies">
<span>06</span>
<div>
<h2>ESSENTIAL COOKIES</h2>
<p>
SHD.build currently uses only first-party cookies that are strictly necessary for
authentication and sign-in protection. We do not currently use analytics,
advertising, behavioral-tracking, or personalization cookies. Because these cookies
are essential, SHD.build does not offer a preference control for disabling them.
You may block cookies through your browser, but account sign-in will not function
without them.
</p>
<div class="legal-table-wrap">
<table class="legal-table">
<thead>
<tr>
<th>COOKIE</th>
<th>PURPOSE</th>
<th>DURATION</th>
<th>PROTECTION</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>shd_session</code></td>
<td>
Maintains your sign-in and associates requests with your server-side
session.
</td>
<td>Up to 30 days, or until sign-out or session invalidation.</td>
<td>HTTP-only, SameSite=Lax, Secure in production.</td>
</tr>
<tr>
<td><code>shd_discord_oauth_state</code></td>
<td>
Validates a Discord sign-in or linking response and helps prevent
request forgery.
</td>
<td>Up to 10 minutes; SHD.build removes it after processing the callback.</td>
<td>
HTTP-only, SameSite=Lax, Secure in production; limited to the Discord
authentication path.
</td>
</tr>
</tbody>
</table>
</div>
<p>
Your browser stores dismissal of the informational notice in local storage under
<code>shd-essential-cookie-notice</code>. Your browser does not send this value with
web requests. You can remove it through your browser’s site-data controls. We keep
OAuth state in a short-lived HTTP-only cookie so the server can validate it without
exposing it to browser scripts.
</p>
<p>
If we introduce non-essential technologies, we will update this policy and the
site’s consent controls before enabling those technologies where consent is
required.
</p>
</div>
</section>
<section id="retention">
<span>07</span>
<div>
<h2>RETENTION & DELETION</h2>
<p>
We retain IP addresses, user-agent strings, normalized device information, and
credential references in login history for no more than 30 days. After 30 days, we
remove those fields from the event. We may retain non-sensitive facts such as event
time, provider, outcome, failure category, and whether the event created a session
for security and support history.
</p>
<p>
When you delete your account, SHD.build removes active sessions, credentials,
verified email, provider profiles, passkeys, permissions, and uploaded avatar. You
may also select an immediate purge of retained login-event history during
self-deletion. SHD.build may keep public contributions under a “Deleted Agent”
attribution. SHD.build may retain backups and records for a limited period where
applicable when fraud prevention, dispute resolution, or law requires them.
</p>
</div>
</section>
<section id="third-parties">
<span>08</span>
<div>
<h2>THIRD-PARTY SERVICES</h2>
<p>
SHD.build may connect to Discord, email delivery services, and future identity
providers. Each provider applies its own terms. Links to other sites do not imply
endorsement. When you choose a linked sign-in method, you authorize SHD.build to
request the minimum provider data needed for that method.
</p>
</div>
</section>
<section id="disclaimers">
<span>09</span>
<div>
<h2>FAN-SITE & LEGAL DISCLAIMER</h2>
<p>
SHD.build is an unofficial fan-created service with no affiliation to Ubisoft
Entertainment or its affiliates. Ubisoft does not endorse, sponsor, or approve
SHD.build.
</p>
<p>
Ubisoft and/or its licensors own Tom Clancy’s The Division, Tom Clancy’s The
Division Resurgence, Ubisoft, and related names, trademarks, logos, characters,
artwork, game data, and other intellectual property. SHD.build uses them for
identification, commentary, and community reference and does not claim ownership.
</p>
<p>
SHD.build provides the service and community information “as is” and “as
available,” without warranties of accuracy, availability, fitness for a particular
purpose, or non-infringement to the extent permitted by law. Game updates may make
build data outdated. SHD.build is not responsible for third-party services or for
decisions that users make based on community content.
</p>
<p>
Copyright owners and authorized agents may use our
<a href="/dmca">DMCA & Copyright Policy</a> and
<a href="/contact?team=dmca">copyright request form</a>. Send other questions
through <a href="/contact">Contact Us</a>.
</p>
<p>
SHD.build may update these terms as the service changes. We will identify material
revisions with a new effective date. Nothing here limits rights that cannot legally
be waived.
</p>
<p>
For authoritative information about Ubisoft intellectual property, consult
Ubisoft’s official
<a href="https://www.ubisoft.com/legal/documents/termsofuse/en-US" rel="external"
>Terms of Use</a
>
and the official
<a href="https://www.ubisoft.com/game/the-division/resurgence" rel="external"
>The Division Resurgence site</a
>.
</p>
</div>
</section>
</article>
</div>
</main>