@reearth/sentinel
v0.1.2
Published
Service worker library for secure asset access with transparent authentication
Readme
Sentinel
A generic, framework-agnostic service worker library for securing asset access with authentication. Sentinel intercepts network requests to protected domains and automatically adds Bearer token authentication headers.
Features
- 🔐 Transparent Authentication: Automatically adds auth headers to protected asset requests
- 🎯 Customizable Patterns: Define your own URL patterns for different asset types
- 💾 Smart Caching: Configurable cache strategies (cache-first, network-first, etc.)
- 🔄 Token Management: Hierarchical token storage (Memory → IndexedDB → Fresh)
- ⚡ Zero Configuration Defaults: Works out of the box with sensible defaults
- 🔧 Fully Configurable: Customize every aspect for your use case
- 📦 Framework Agnostic: Works with React, Vue, Angular, Svelte, or vanilla JS
- 🎨 TypeScript First: Full TypeScript support with comprehensive types
Use Cases
- Securing cloud storage assets (GCS, S3, Azure Blob)
- Private CDN content
- Protected map tiles (MVT, raster)
- Authenticated API resources
- Secure media delivery
Installation
npm install @reearth/sentinel
# or
yarn add @reearth/sentinel
# or
pnpm add @reearth/sentinelQuick Start
1. Copy Service Worker
Copy the service worker file to your public directory:
cp node_modules/@reearth/sentinel/dist/sw.js public/2. Basic Setup
import { registerAssetSecurity, updateToken } from '@reearth/sentinel';
// Initialize with minimal config
await registerAssetSecurity({
proxyUrl: 'https://your-auth-proxy.com',
protectedDomains: [
'storage.googleapis.com',
'your-cdn.example.com'
],
// Optional: Handle token expiration
onTokenExpired: async () => {
const newToken = await yourAuthSystem.getToken();
await updateToken({
accessToken: newToken,
expiresAt: Date.now() + 3600000
});
}
});
// Update token when user logs in
await updateToken({
accessToken: 'your-jwt-token',
expiresAt: Date.now() + 3600000 // 1 hour
});registerAssetSecurity() resolves after the service worker is active and has been asked to control the current page. Always await it before starting protected asset or tile requests so hard reloads do not bypass the service worker.
3. Clear Token on Logout
import { clearToken } from '@reearth/sentinel';
// When user logs out
await clearToken();Advanced Configuration
Custom Asset Patterns
Define regex patterns for your specific asset types:
await registerAssetSecurity({
proxyUrl: 'https://proxy.example.com',
protectedDomains: ['assets.example.com'],
assetPatterns: {
// Images
customAssets: /\.(jpg|jpeg|png|gif|webp)$/i,
// Map tiles: /tiles/{source}/{z}/{x}/{y}
mvtTiles: /\/tiles\/[^/]+\/\d+\/\d+\/\d+\.mvt/,
rasterTiles: /\/tiles\/[^/]+\/\d+\/\d+\/\d+\.(png|jpg)/,
// Other assets
generalAssets: /\.(pdf|json|geojson|svg)$/i
}
});Custom Asset ID Extraction
If your URLs have a specific structure, provide a custom extractor:
await registerAssetSecurity({
proxyUrl: 'https://proxy.example.com',
protectedDomains: ['cdn.example.com'],
// Extract asset ID from: /content/assets/{uuid}/file.jpg
extractAssetId: (url) => {
const match = url.pathname.match(/\/assets\/([^/]+)\//);
return match ? match[1] : null;
}
});Cache Strategies
Configure caching behavior per asset type:
await registerAssetSecurity({
proxyUrl: 'https://proxy.example.com',
protectedDomains: ['assets.example.com'],
cacheStrategies: {
images: 'cache-first', // Try cache first, fallback to network
tiles: 'network-first', // Try network first, fallback to cache
documents: 'network-only' // Always fetch from network
}
});Token Configuration
Customize token management:
await registerAssetSecurity({
proxyUrl: 'https://proxy.example.com',
protectedDomains: ['assets.example.com'],
tokenConfig: {
memoryCacheTTL: 10 * 60 * 1000, // 10 minutes in memory
refreshThreshold: 2 * 60 * 1000 // Refresh if < 2 min remaining
}
});Namespace Isolation
Use namespaces to isolate storage for multiple apps:
await registerAssetSecurity({
namespace: 'my-app', // Creates 'my-app-auth' DB and 'my-app-v1' cache
proxyUrl: 'https://proxy.example.com',
protectedDomains: ['assets.example.com']
});Service Worker Control Timeout
Sentinel waits up to 5 seconds for an active service worker to control the current page. You can tune this startup wait if your app needs a shorter or longer guardrail:
await registerAssetSecurity({
proxyUrl: 'https://proxy.example.com',
protectedDomains: ['assets.example.com'],
serviceWorkerControlTimeout: 3000
});API Reference
registerAssetSecurity(config)
Initialize the service worker with your configuration.
The promise resolves once the service worker is registered, configured, and controlling the current page when the browser allows it. Sentinel sends an internal CLAIM_CLIENTS message to handle hard reloads where navigator.serviceWorker.controller can be null; if control is not gained within serviceWorkerControlTimeout, registration continues and logs a warning in debug mode.
Parameters:
config.proxyUrl(string, required): URL of your authentication proxyconfig.protectedDomains(string[], required): Domains requiring authconfig.assetPatterns(object, optional): Custom URL patternsconfig.extractAssetId(function, optional): Custom ID extractorconfig.namespace(string, optional): Storage namespace (default: 'asset-security')config.tokenConfig(object, optional): Token management settingsconfig.cacheStrategies(object, optional): Cache behavior per asset typeconfig.serviceWorkerControlTimeout(number, optional): Max wait in ms for the service worker to control the current page (default: 5000)config.debug(boolean, optional): Enable debug logging for registration, service worker, token manager, and request interceptor logs (default:false)config.onTokenExpired(function, optional): Token expiration callbackconfig.onTokenRefreshed(function, optional): Token refresh callbackconfig.onSecurityEvent(function, optional): Generic event callback
Returns: Promise<RegistrationResult>
updateToken(options)
Update the authentication token.
Parameters:
options.accessToken(string, required): The JWT or bearer tokenoptions.expiresAt(number, optional): Expiry timestamp in msoptions.refreshToken(string, optional): Refresh tokenoptions.scope(string, optional): Token scope
Returns: Promise<void>
clearToken()
Remove stored authentication token.
Returns: Promise<void>
getSecurityStatus()
Get current security system status.
Returns: Promise<AssetSecurityStatus>
{
isRegistered: boolean;
isAuthenticated: boolean;
version?: string;
cachedRequests?: number;
caches?: string[];
tokenExpiresAt?: number;
proxyStatus?: 'connected' | 'disconnected' | 'unknown';
}clearCache()
Clear all cached assets.
Returns: Promise<void>
unregisterAssetSecurity()
Unregister the service worker and clean up.
Returns: Promise<void>
Framework Examples
React
import { useEffect } from 'react';
import { registerAssetSecurity, updateToken, clearToken } from '@reearth/sentinel';
function App() {
const { token, isAuthenticated } = useAuth(); // Your auth hook
useEffect(() => {
let cancelled = false;
async function setupSecurity() {
const result = await registerAssetSecurity({
proxyUrl: import.meta.env.VITE_PROXY_URL,
protectedDomains: ['storage.googleapis.com'],
onTokenExpired: async () => {
// Handle token refresh
}
});
if (!result.success || cancelled) return;
if (isAuthenticated && token) {
await updateToken({
accessToken: token,
expiresAt: Date.now() + 3600000
});
} else {
await clearToken();
}
}
setupSecurity();
return () => {
cancelled = true;
};
}, [isAuthenticated, token]);
return <YourApp />;
}Vue 3
import { onMounted, watch } from 'vue';
import { registerAssetSecurity, updateToken, clearToken } from '@reearth/sentinel';
export default {
setup() {
const auth = useAuth(); // Your auth composable
onMounted(async () => {
const result = await registerAssetSecurity({
proxyUrl: import.meta.env.VITE_PROXY_URL,
protectedDomains: ['storage.googleapis.com']
});
if (result.success && auth.token) {
await updateToken({ accessToken: auth.token });
}
});
watch(() => auth.token, async (newToken) => {
if (newToken) {
await updateToken({ accessToken: newToken });
} else {
await clearToken();
}
});
}
};Vanilla JavaScript
import { registerAssetSecurity, updateToken } from '@reearth/sentinel';
// Initialize before starting protected asset requests
await registerAssetSecurity({
proxyUrl: 'https://proxy.example.com',
protectedDomains: ['assets.example.com']
});
// On login
document.getElementById('login-btn').addEventListener('click', async () => {
const token = await yourAuthFunction();
await updateToken({ accessToken: token });
});Architecture
┌─────────────────┐
│ Your App │
│ (any framework)│
└────────┬────────┘
│ registerAssetSecurity()
│ updateToken()
▼
┌─────────────────┐
│ Service Worker │◄──── Intercepts fetch events
│ (this library) │
└────────┬────────┘
│ Adds auth headers
▼
┌─────────────────┐
│ Auth Proxy │◄──── Validates tokens
│ (your server) │
└────────┬────────┘
│ Proxies request
▼
┌─────────────────┐
│ Protected Assets│
│ (GCS, S3, etc) │
└─────────────────┘Token Storage Hierarchy
Memory Cache (fastest, 5 min TTL)
- In-memory storage
- Cleared on page refresh
IndexedDB (persistent)
- Survives page refreshes
- Per-origin storage
Fresh Request (fallback)
- Calls
onTokenExpiredcallback - Requests new token from your auth system
- Calls
Browser Support
- Chrome 67+
- Firefox 63+
- Safari 11.3+
- Edge 79+
Service Workers require HTTPS (except on localhost).
Security Considerations
- Tokens are stored in IndexedDB (encrypted by browser)
- Tokens never appear in URLs
- Service Worker scope should match your app's scope
- Use short-lived tokens with refresh capability
- Implement proper CORS on your proxy server
Troubleshooting
Service Worker Not Registering
// Check registration result
const result = await registerAssetSecurity({...});
if (!result.success) {
console.error('Registration failed:', result.error);
}Requests Not Being Intercepted
- Verify
protectedDomainsincludes the asset domain - Check browser DevTools → Application → Service Workers
- Ensure assets match
assetPatternsregex - Await
registerAssetSecurity()before rendering views that start protected requests - If requests fail only after a hard reload, deploy the latest
sw.jsso the worker can handle the internalCLAIM_CLIENTSmessage and callclients.claim()
Token Not Being Applied
// Check status
const status = await getSecurityStatus();
console.log('Authenticated:', status.isAuthenticated);
console.log('Token expires:', new Date(status.tokenExpiresAt));Enable Debug Logs
await registerAssetSecurity({
proxyUrl: 'https://proxy.example.com',
protectedDomains: ['assets.example.com'],
debug: true,
});Clear Everything and Start Fresh
import { unregisterAssetSecurity } from '@reearth/sentinel';
await unregisterAssetSecurity();
// Then re-registerDevelopment
# Install dependencies
npm install
# Build library
npm run build
# Build service worker
npm run build:sw
# Type check
npm run type-check
# Run tests
npm testLicense
Apache-2.0
Contributing
Contributions welcome! Please open an issue or PR.
