Files
gh-jezweb-claude-skills-ski…/templates/location-hints.ts
2025-11-30 08:24:13 +08:00

239 lines
6.8 KiB
TypeScript

/**
* Location Hints and Geographic Routing
*
* Demonstrates:
* - Location hints for geographic routing
* - Jurisdiction restrictions (EU, FedRAMP)
* - When to use each approach
* - Limitations and best practices
*/
import { DurableObject, DurableObjectState } from 'cloudflare:workers';
interface Env {
USER_DATA: DurableObjectNamespace<UserDataDO>;
}
export class UserDataDO extends DurableObject<Env> {
async storeUserData(data: any): Promise<void> {
await this.ctx.storage.put('userData', data);
}
async getUserData(): Promise<any> {
return await this.ctx.storage.get('userData');
}
}
// CRITICAL: Export the class
export default UserDataDO;
/**
* Worker demonstrating location hints and jurisdiction
*/
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url);
const userId = url.searchParams.get('userId');
if (!userId) {
return new Response('Missing userId', { status: 400 });
}
// Pattern 1: Location Hints (Best-Effort)
// Use when you want to create DO near user's location for lower latency
// Get user's location from request
const userRegion = request.cf?.continent as string || 'NA';
// Map continent to location hint
const locationHint = getLocationHint(userRegion);
// Create DO with location hint
const id = env.USER_DATA.idFromName(userId);
const stub = env.USER_DATA.get(id, { locationHint });
// ⚠️ Location hint only affects FIRST creation
// Subsequent access uses existing DO location
await stub.storeUserData({ userId, region: userRegion });
return new Response(JSON.stringify({ success: true, locationHint }), {
headers: { 'content-type': 'application/json' },
});
},
};
/**
* Map user region to Cloudflare location hint
*/
function getLocationHint(continent: string): string {
switch (continent) {
case 'NA':
return 'enam'; // Eastern North America
case 'EU':
return 'weur'; // Western Europe
case 'AS':
return 'apac'; // Asia-Pacific
case 'SA':
return 'sam'; // South America
case 'AF':
return 'afr'; // Africa
case 'OC':
return 'oc'; // Oceania
default:
return 'enam'; // Default
}
}
/**
* Available location hints:
*
* - 'wnam' - Western North America
* - 'enam' - Eastern North America
* - 'sam' - South America
* - 'weur' - Western Europe
* - 'eeur' - Eastern Europe
* - 'apac' - Asia-Pacific
* - 'oc' - Oceania
* - 'afr' - Africa
* - 'me' - Middle East
*/
/**
* Pattern 2: Jurisdiction Restriction (Strictly Enforced)
* Use for regulatory compliance (GDPR, FedRAMP)
*/
export const jurisdictionWorker = {
async fetch(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url);
const userId = url.searchParams.get('userId');
const requireEU = url.searchParams.get('requireEU') === 'true';
if (!userId) {
return new Response('Missing userId', { status: 400 });
}
if (requireEU) {
// STRICT: DO MUST stay in EU
// Cannot combine jurisdiction with location hints
const euId = env.USER_DATA.newUniqueId({ jurisdiction: 'eu' });
const stub = env.USER_DATA.get(euId);
// Store ID for future access
// await env.KV.put(`user:${userId}`, euId.toString());
await stub.storeUserData({ userId, jurisdiction: 'eu' });
return new Response(JSON.stringify({
success: true,
jurisdiction: 'eu',
id: euId.toString(),
}), {
headers: { 'content-type': 'application/json' },
});
}
// Non-EU user: use location hint
const id = env.USER_DATA.idFromName(userId);
const stub = env.USER_DATA.get(id, { locationHint: 'enam' });
await stub.storeUserData({ userId });
return new Response(JSON.stringify({ success: true }), {
headers: { 'content-type': 'application/json' },
});
},
};
/**
* Available jurisdictions:
*
* - 'eu' - European Union (GDPR compliance)
* - 'fedramp' - FedRAMP (US government)
*/
/**
* Location Hints vs Jurisdiction: Decision Matrix
*
* | Requirement | Use |
* |-------------|-----|
* | Lower latency (nice-to-have) | Location hints |
* | Data residency (MUST) | Jurisdiction |
* | Regulatory compliance (GDPR, FedRAMP) | Jurisdiction |
* | Optimize for user location | Location hints |
* | Strict data sovereignty | Jurisdiction |
* | Performance optimization | Location hints |
*/
/**
* CRITICAL Limitations:
*
* ❌ Location hints are BEST-EFFORT (not guaranteed)
* ❌ Location hints only affect FIRST creation
* ❌ Cannot move existing DOs to new location
* ❌ Cannot combine jurisdiction with location hints
*
* ✅ Jurisdiction is STRICTLY ENFORCED
* ✅ Jurisdiction guarantees data never leaves region
*/
/**
* Example: Multi-region routing based on user IP
*/
export const multiRegionWorker = {
async fetch(request: Request, env: Env): Promise<Response> {
const userId = new URL(request.url).searchParams.get('userId');
if (!userId) {
return new Response('Missing userId', { status: 400 });
}
// Get user's country from request
const country = request.cf?.country as string;
// Determine if user is in EU
const euCountries = ['DE', 'FR', 'IT', 'ES', 'NL', 'BE', 'AT', 'PL', 'PT', 'GR', /* ... */];
const isEU = euCountries.includes(country);
if (isEU) {
// EU user: MUST use jurisdiction
const euId = env.USER_DATA.newUniqueId({ jurisdiction: 'eu' });
const stub = env.USER_DATA.get(euId);
// Store ID for future access
// await env.DB.prepare('INSERT INTO user_do_ids (user_id, do_id) VALUES (?, ?)')
// .bind(userId, euId.toString())
// .run();
await stub.storeUserData({ userId, jurisdiction: 'eu' });
return new Response(JSON.stringify({ region: 'EU', id: euId.toString() }), {
headers: { 'content-type': 'application/json' },
});
}
// Non-EU user: use location hint for optimization
const locationHint = getLocationHintFromCountry(country);
const id = env.USER_DATA.idFromName(userId);
const stub = env.USER_DATA.get(id, { locationHint });
await stub.storeUserData({ userId });
return new Response(JSON.stringify({ region: locationHint }), {
headers: { 'content-type': 'application/json' },
});
},
};
function getLocationHintFromCountry(country: string): string {
// Simplified mapping - expand as needed
if (['US', 'CA', 'MX'].includes(country)) return 'enam';
if (['GB', 'FR', 'DE', 'ES', 'IT'].includes(country)) return 'weur';
if (['CN', 'JP', 'KR', 'SG', 'IN'].includes(country)) return 'apac';
if (['BR', 'AR', 'CL'].includes(country)) return 'sam';
if (['AU', 'NZ'].includes(country)) return 'oc';
if (['ZA', 'EG', 'KE'].includes(country)) return 'afr';
return 'enam'; // Default
}