The client was calling GET ${baseUrl}/flags/kill-switch which does
not exist on platform-service. The actual kill-switch endpoint lives
under /settings/kill-switch in the settings module (public, no auth
required). The bug was silently masked by the client's fail-open
behavior on non-OK responses, but it produced a 404 on every page
load for every consumer (NoteLett, MindLyst, ChronoMind, FlowMonk,
NomGap, PeakPulse, JarvisJr, LysnrAI, ActionTrail, EffoRise, Local
Memory GPT).
Discovery: running the deployed NoteLett docker stack against the
sibling platform-service, every page load triggered:
GET http://localhost:4003/api/flags/kill-switch?platform=web → 404
Confirmed by curl-ing both endpoints directly:
/api/flags/kill-switch → {"message":"Route GET:/api/flags/kill-switch not found"}
/api/settings/kill-switch → {"enabled":true,"disabled":false,"message":""}
Also adds the productId as a query param. The server route accepts
productId from the query string OR an x-product-id header — sending
both is harmless and improves debuggability when grepping logs.
Updated JSDoc and the corresponding test assertion. Test count
unchanged (6 passed).
Verified:
pnpm --filter @bytelyst/kill-switch-client test → 6/6 passed
pnpm --filter @bytelyst/kill-switch-client build → ok
curl /api/settings/kill-switch?productId=notelett → 200 with payload
81 lines
2.6 KiB
TypeScript
81 lines
2.6 KiB
TypeScript
/**
|
|
* Browser/React Native-safe kill switch client for platform-service.
|
|
*
|
|
* Checks GET /api/settings/kill-switch to determine if the app is disabled.
|
|
* Fail-open: returns { disabled: false } on any network error.
|
|
*
|
|
* @example
|
|
* ```ts
|
|
* import { createKillSwitchClient } from '@bytelyst/kill-switch-client';
|
|
*
|
|
* const ks = createKillSwitchClient({
|
|
* baseUrl: 'http://localhost:4003/api',
|
|
* productId: 'nomgap',
|
|
* });
|
|
*
|
|
* const result = await ks.check();
|
|
* if (result.disabled) showBlockScreen(result.message);
|
|
* ```
|
|
*/
|
|
|
|
export interface KillSwitchClientConfig {
|
|
/** Platform-service base URL (e.g. "http://localhost:4003/api"). */
|
|
baseUrl: string;
|
|
|
|
/** Product identifier sent as x-product-id header. */
|
|
productId: string;
|
|
|
|
/** Platform string for the query (e.g. "mobile", "web"). Default: "mobile". */
|
|
platform?: string;
|
|
}
|
|
|
|
export interface KillSwitchResult {
|
|
disabled: boolean;
|
|
message: string | null;
|
|
}
|
|
|
|
export interface KillSwitchClient {
|
|
/** Check if the app is disabled. Fail-open on any error. */
|
|
check(): Promise<KillSwitchResult>;
|
|
}
|
|
|
|
export function createKillSwitchClient(config: KillSwitchClientConfig): KillSwitchClient {
|
|
const { baseUrl, productId, platform = 'mobile' } = config;
|
|
|
|
async function check(): Promise<KillSwitchResult> {
|
|
try {
|
|
const requestId =
|
|
typeof globalThis.crypto?.randomUUID === 'function'
|
|
? globalThis.crypto.randomUUID()
|
|
: `${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
|
|
|
|
// Endpoint lives under /settings/kill-switch in platform-service's
|
|
// settings module (no auth required). The route reads the
|
|
// `kill_switch` feature flag from the flags container and returns
|
|
// { enabled, disabled, message }. Older versions of this client
|
|
// pointed at /flags/kill-switch which does not exist; that bug
|
|
// was masked by fail-open behavior on 404 but produced noisy
|
|
// 404s on every page load.
|
|
const res = await globalThis.fetch(
|
|
`${baseUrl}/settings/kill-switch?platform=${encodeURIComponent(platform)}&productId=${encodeURIComponent(productId)}`,
|
|
{
|
|
headers: { 'x-product-id': productId, 'x-request-id': requestId },
|
|
}
|
|
);
|
|
|
|
if (!res.ok) return { disabled: false, message: null };
|
|
|
|
const data = (await res.json()) as KillSwitchResult;
|
|
return {
|
|
disabled: data.disabled ?? false,
|
|
message: data.message ?? null,
|
|
};
|
|
} catch {
|
|
// Fail-open: network errors should NOT block the user
|
|
return { disabled: false, message: null };
|
|
}
|
|
}
|
|
|
|
return { check };
|
|
}
|