| name | architecture |
| description | Use when changing module structure, auth internals, conditional exports, or the HTTP and storage layers. |
| trigger | Working on internals, auth strategies, HTTP layer, module structure, platform-specific code |
Architecture
Dual-Platform Design
safeguard.js serves Node.js AND Browser from a single codebase:
- Node entry:
src/index.ts — exports everything including Node-only modules
- Browser entry:
src/browser.ts — excludes CertificateAuth, PkceNonInteractive, NodeHttpClient, fs/tls
Resolution is automatic via package.json conditional exports:
{
"exports": {
".": {
"import": {
"types": "./dist/index.d.ts",
"browser": "./dist/browser.js",
"default": "./dist/index.js"
},
"require": {
"types": "./dist/index.d.cts",
"default": "./dist/index.cjs"
}
},
"./browser": {
"types": "./dist/browser.d.ts",
"default": "./dist/browser.js"
},
"./events": {
"import": { "types": "./dist/events.d.ts", "default": "./dist/events.js" },
"require": { "types": "./dist/events.d.cts", "default": "./dist/events.cjs" }
}
}
}
The ./events subpath is opt-in — consumers who don't use SignalR never install @microsoft/signalr.
Module Layers (Dependency Graph)
Layer 0 (no internal deps): types.ts, errors.ts, secret.ts, utils.ts
Layer 1 (depends on L0): storage/, http/
Layer 2 (depends on L0-1): auth/
Layer 3 (depends on all): client.ts, a2a/, events/
Layer 4 (barrel exports): index.ts, browser.ts
Build from leaves to root. Never introduce upward dependencies.
Key Design Rules
- No global state — every client is independent (own Agent, own tokens)
- No side effects at import — constructors do no I/O
- Auth as strategy objects — implement
Auth interface, plug into client
- HttpClient is an interface — Node uses undici (TLS control), Browser uses native fetch
- StorageProvider is an interface — MemoryStorage (Node), BrowserSessionStorage (Browser)
- sideEffects: false — tree-shakeable, bundlers can eliminate dead code
HTTP Layer
Node (undici)
const agent = new Agent({
connect: {
ca: options.ca,
cert: options.cert,
key: options.key,
rejectUnauthorized: options.verify !== false,
},
});
Per-instance Agent — no global HTTPS agent mutation.
Browser (native fetch)
const response = await fetch(url, {
method, headers, body,
signal,
credentials: 'include',
});
SignalR (v10.x)
- Use
withAutomaticReconnect() — built into SignalR 10
accessTokenFactory: () => Promise<string> wired to auth token
- For Node custom CA: pass custom
httpClient option using undici
- PersistentSafeguardEventListener adds token refresh on reconnect + state tracking
Error Hierarchy
SafeguardError (base)
├── ApiError (HTTP errors from Safeguard API)
│ ├── AuthenticationError (401)
│ ├── AuthorizationError (403)
│ └── NotFoundError (404)
├── TransportError (network/connection failures)
└── ConfigurationError (invalid client options)
ApiError.fromResponse(status, body) factory parses Safeguard error JSON.