Files
OpenFlare/frontend/app/(main)/responses/components/shared.ts
T
ryan ca21ff3a5b feat(option): add sw offline
fix(openresty): scope sw injection per cert partition

fix(lint): satisfy revive and biome format for sw offline feature

docs: sw offline scope changelog

fix(frontend): use scoped query key for sw scope zones

fix(frontend): hide preview link in sw contact page editor

feat(frontend): add sw scope domain picker and contact page fields

refactor(frontend): generalize html editor workspace for reuse

feat(openresty): scope sw offline injection by route domains

feat(openresty): add sw offline domains snapshot field

feat(option): add sw offline domains scope option

docs: fill html editor workspace generalization detail

docs: sw offline scope implementation plan

docs: sw offline scope design

test(openresty): assert single merged access block in sw enabled servers

fix(openresty): restrict sw intercept to https server blocks

fix(openresty): version sw offline cache by html content

fix(agent): escape redir in sw challenge page to prevent xss

fix(agent): return sw.runtime module table and add lua spec

docs: sw offline fallback changelog

fix(frontend): memoize option map to preserve unsaved contact page edits

feat(frontend): add response pages module with contact page tab

feat(agent): ship sw offline lua assets and placeholder substitution

feat(config): wire sw offline options into config snapshot

feat(openresty): render sw offline assets and challenge intercept

feat(openresty): add sw offline ConfigSnapshot fields and placeholder

feat(db): seed sw offline options

feat(option): add sw offline config keys and validation

docs: add service worker offline fallback implementation plan

docs: adopt global-option pattern for SW offline fallback (matches origin error page)

docs: unify offline contact page with error pages as response pages

docs: service worker offline fallback design (issue #23)
2026-08-08 20:14:28 +08:00

63 lines
1.6 KiB
TypeScript

export const OPTIONS_QUERY_KEY = ['openflare', 'options'] as const;
export const KEY_SW_ENABLED = 'sw_offline_enabled';
export const KEY_SW_HTML = 'sw_offline_html';
export const KEY_SW_DOMAINS = 'sw_offline_domains';
export type ContactPageFields = {
enabled: boolean;
html: string;
domains: string[];
};
export const defaultContactPageFields: ContactPageFields = {
enabled: false,
html: '',
domains: [],
};
export function optionsToMap(options: Array<{ key: string; value: string }>) {
return options.reduce<Record<string, string>>((acc, option) => {
acc[option.key] = option.value;
return acc;
}, {});
}
function parseDomains(raw: string | undefined): string[] {
if (!raw) return [];
try {
const parsed: unknown = JSON.parse(raw);
return Array.isArray(parsed)
? parsed.filter((item): item is string => typeof item === 'string')
: [];
} catch {
return [];
}
}
export function mapOptionsToContactFields(
optionMap: Record<string, string>,
): ContactPageFields {
return {
enabled: optionMap[KEY_SW_ENABLED] === 'true',
html: optionMap[KEY_SW_HTML] ?? '',
domains: parseDomains(optionMap[KEY_SW_DOMAINS]),
};
}
export async function invalidateResponseQueries(queryClient: {
invalidateQueries: (opts: {
queryKey: readonly unknown[];
}) => Promise<unknown>;
}) {
await Promise.all([
queryClient.invalidateQueries({ queryKey: OPTIONS_QUERY_KEY }),
queryClient.invalidateQueries({
queryKey: ['openflare', 'config-preview'],
}),
queryClient.invalidateQueries({
queryKey: ['openflare', 'config-versions'],
}),
]);
}