Files
eden/src/lib/server/release-gate.ts
T

103 lines
3.4 KiB
TypeScript

/**
* Deployment-time rollout controls. These are deliberately separate from the
* domain capability contracts: a capability may have a complete contract and
* still remain disabled in a particular release.
*
* The allowlist is evaluated on the server for every entry point. Unknown
* names are rejected instead of becoming an accidental opt-in, and newly
* added known capabilities are absent from the baseline allowlist until an
* operator explicitly enables them.
*/
export const RELEASE_CAPABILITIES = [
'asset_search',
'asset_valuation',
'asset_sync',
'assistant_dispatch',
'session_token',
'subscriptions',
'wallet_balance',
'inventory_export',
'manufacturing_opportunities',
] as const;
export type ReleaseCapability = (typeof RELEASE_CAPABILITIES)[number];
/** Existing first-slice behavior remains enabled unless an operator narrows it. */
export const BASELINE_RELEASE_CAPABILITIES: readonly ReleaseCapability[] = [
'asset_search',
'asset_valuation',
'asset_sync',
'assistant_dispatch',
'session_token',
];
const known = new Set<string>(RELEASE_CAPABILITIES);
export class CapabilityConfigurationError extends Error {
constructor(message: string) {
super(message);
this.name = 'CapabilityConfigurationError';
}
}
/**
* Parse APP_CAPABILITY_ALLOWLIST without accepting typos or duplicate policy
* entries. An explicitly empty value disables every capability; an omitted
* value preserves the existing private asset/sign-in baseline.
*/
export function parseCapabilityAllowlist(raw: string | undefined): readonly ReleaseCapability[] {
if (raw === undefined) return [...BASELINE_RELEASE_CAPABILITIES];
const value = raw.trim();
if (value === '') return [];
const result: ReleaseCapability[] = [];
const seen = new Set<string>();
for (const token of value.split(',')) {
const name = token.trim();
if (!name || !known.has(name)) {
throw new CapabilityConfigurationError(`Unknown deployment capability: ${name || '<empty>'}`);
}
if (!seen.has(name)) {
seen.add(name);
result.push(name as ReleaseCapability);
}
}
return result;
}
export function capabilityEnabled(
capability: ReleaseCapability,
allowlist: readonly ReleaseCapability[],
): boolean {
return allowlist.includes(capability);
}
export function capabilityDisabledReason(capability: ReleaseCapability): string {
return `The ${capability} capability is disabled by deployment configuration.`;
}
/** A non-throwing check is useful at HTTP boundaries that should fail closed. */
export function requireCapability(
capability: ReleaseCapability,
allowlist: readonly ReleaseCapability[],
): void {
if (!capabilityEnabled(capability, allowlist)) {
throw new CapabilityConfigurationError(capabilityDisabledReason(capability));
}
}
/** Release identifiers are metadata, not a source of authorization. */
export function validReleaseId(value: string): boolean {
return /^(?!latest$)[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(value);
}
/**
* Production images must carry a non-placeholder immutable identifier. Local
* development and fixture runs intentionally use `local`.
*/
export function releaseIdIsImmutable(value: string, environment = process.env.NODE_ENV): boolean {
if (!validReleaseId(value)) return false;
if (environment !== 'production') return true;
return value !== 'local' && value !== 'dev' && value !== 'development' && value !== 'unreleased';
}