feat: add isolated release gates and CI checks
This commit is contained in:
@@ -0,0 +1,102 @@
|
||||
/**
|
||||
* 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';
|
||||
}
|
||||
Reference in New Issue
Block a user