NASA API fix

This commit is contained in:
Guy Sandler 2026-10-27 17:34:40 -07:00
parent 1c140c7153
commit e248a3aad6
2 changed files with 388 additions and 334 deletions

View File

@ -1,333 +1,387 @@
chrome.runtime.onInstalled.addListener(function () { chrome.runtime.onInstalled.addListener(function () {
let default_options = { let default_options = {
"local": { "local": {
"previous_colors": null, "previous_colors": null,
"previous_theme": null, "previous_theme": null,
"errors": [], "errors": [],
"saved_themes": {}, "saved_themes": {},
"liked_themes": [], "liked_themes": [],
}, },
"sync": { "sync": {
"dark_preset": { "dark_preset": {
"background-0": "#161616", "background-0": "#161616",
"background-1": "#1e1e1e", "background-1": "#1e1e1e",
"background-2": "#262626", "background-2": "#262626",
"borders": "#3c3c3c", "borders": "#3c3c3c",
"text-0": "#f5f5f5", "text-0": "#f5f5f5",
"text-1": "#e2e2e2", "text-1": "#e2e2e2",
"text-2": "#ababab", "text-2": "#ababab",
"links": "#56Caf0", "links": "#56Caf0",
"sidebar": "#1e1e1e", "sidebar": "#1e1e1e",
"sidebar-text": "#f5f5f5" "sidebar-text": "#f5f5f5"
}, },
"new_install": true, "new_install": true,
"assignments_due": true, "assignments_due": true,
"gpa_calc": true, "gpa_calc": true,
"dark_mode": true, "dark_mode": true,
"gradent_cards": false, "gradent_cards": false,
"disable_color_overlay": false, "disable_color_overlay": false,
"auto_dark": false, "auto_dark": false,
"auto_dark_start": { "hour": "20", "minute": "00" }, "auto_dark_start": { "hour": "20", "minute": "00" },
"auto_dark_end": { "hour": "08", "minute": "00" }, "auto_dark_end": { "hour": "08", "minute": "00" },
"num_assignments": 4, "num_assignments": 4,
"custom_domain": [""], "custom_domain": [""],
"custom_domain_denied": [], "custom_domain_denied": [],
"auto_detect_disabled": false, "auto_detect_disabled": false,
"assignments_done": [], "assignments_done": [],
"dashboard_grades": true, "dashboard_grades": true,
"assignment_date_format": false, "assignment_date_format": false,
"dashboard_notes": false, "dashboard_notes": false,
"dashboard_notes_text": "", "dashboard_notes_text": "",
"dashboard_notes_mode": "edit", "dashboard_notes_mode": "edit",
"dashboard_notes_position": "top", "dashboard_notes_position": "top",
"widget_order": {}, "widget_order": {},
"focus_timer": false, "focus_timer": false,
"focus_timer_position": "cards_end", "focus_timer_position": "cards_end",
"focus_timer_preset_1": { "h": 0, "m": 15, "s": 0 }, "focus_timer_preset_1": { "h": 0, "m": 15, "s": 0 },
"focus_timer_preset_2": { "h": 0, "m": 25, "s": 0 }, "focus_timer_preset_2": { "h": 0, "m": 25, "s": 0 },
"focus_timer_preset_3": { "h": 0, "m": 45, "s": 0 }, "focus_timer_preset_3": { "h": 0, "m": 45, "s": 0 },
"breathing": false, "breathing": false,
"breathing_position": "cards_end", "breathing_position": "cards_end",
"breathing_preset_1": { "h": 0, "m": 3, "s": 0 }, "breathing_preset_1": { "h": 0, "m": 3, "s": 0 },
"breathing_preset_2": { "h": 0, "m": 5, "s": 0 }, "breathing_preset_2": { "h": 0, "m": 5, "s": 0 },
"breathing_preset_3": { "h": 0, "m": 10, "s": 0 }, "breathing_preset_3": { "h": 0, "m": 10, "s": 0 },
"daily_quotes": false, "daily_quotes": false,
"daily_quotes_position": "top", "daily_quotes_position": "top",
"quick_links": false, "quick_links": false,
"quick_links_position": "cards_end", "quick_links_position": "cards_end",
"heatmap": false, "heatmap": false,
"heatmap_position": "below", "heatmap_position": "below",
"heatmap_excluded_courses": [], "heatmap_excluded_courses": [],
"widget_top_height": 0, "widget_top_height": 0,
"widget_bottom_height": 0, "widget_bottom_height": 0,
"widget_row_height": 0, "widget_row_height": 0,
"dashboard_notes_height": 0, "dashboard_notes_height": 0,
"gpa_calc_height": 0, "gpa_calc_height": 0,
"focus_timer_height": 0, "focus_timer_height": 0,
"breathing_height": 0, "breathing_height": 0,
"daily_quotes_height": 0, "daily_quotes_height": 0,
"quick_links_height": 0, "quick_links_height": 0,
"heatmap_height": 0, "heatmap_height": 0,
"quick_links_font_size": 12, "quick_links_font_size": 12,
"quick_links_icon_size": 36, "quick_links_icon_size": 36,
"better_todo": true, "better_todo": true,
"todo_hr24": false, "todo_hr24": false,
"todo_separate_scrollbar": false, "todo_separate_scrollbar": false,
"better_sidebar": false, "better_sidebar": false,
"condensed_cards": false, "condensed_cards": false,
"custom_cards": {}, "custom_cards": {},
"custom_cards_2": {}, "custom_cards_2": {},
"custom_cards_3": {}, "custom_cards_3": {},
"custom_assignments": [], "custom_assignments": [],
"custom_assignments_overflow": ["custom_assignments"], "custom_assignments_overflow": ["custom_assignments"],
"grade_hover": false, "grade_hover": false,
"card_letter": true, "card_letter": true,
"hide_completed_cards": false, "hide_completed_cards": false,
"num_todo_items": 10, "num_todo_items": 10,
"custom_font": { "link": "", "family": "" }, "custom_font": { "link": "", "family": "" },
"hover_preview": true, "hover_preview": true,
"full_width": null, "full_width": null,
"remlogo": null, "remlogo": null,
"gpa_calc_bounds": { "gpa_calc_bounds": {
"A+": { "cutoff": 97, "gpa": 4.0 }, "A+": { "cutoff": 97, "gpa": 4.0 },
"A": { "cutoff": 93, "gpa": 4 }, "A": { "cutoff": 93, "gpa": 4 },
"A-": { "cutoff": 90, "gpa": 3.7 }, "A-": { "cutoff": 90, "gpa": 3.7 },
"B+": { "cutoff": 87, "gpa": 3.3 }, "B+": { "cutoff": 87, "gpa": 3.3 },
"B": { "cutoff": 83, "gpa": 3 }, "B": { "cutoff": 83, "gpa": 3 },
"B-": { "cutoff": 80, "gpa": 2.7 }, "B-": { "cutoff": 80, "gpa": 2.7 },
"C+": { "cutoff": 77, "gpa": 2.3 }, "C+": { "cutoff": 77, "gpa": 2.3 },
"C": { "cutoff": 73, "gpa": 2 }, "C": { "cutoff": 73, "gpa": 2 },
"C-": { "cutoff": 70, "gpa": 1.7 }, "C-": { "cutoff": 70, "gpa": 1.7 },
"D+": { "cutoff": 67, "gpa": 1.3 }, "D+": { "cutoff": 67, "gpa": 1.3 },
"D": { "cutoff": 63, "gpa": 1 }, "D": { "cutoff": 63, "gpa": 1 },
"D-": { "cutoff": 60, "gpa": .7 }, "D-": { "cutoff": 60, "gpa": .7 },
"F": { "cutoff": 0, "gpa": 0 } "F": { "cutoff": 0, "gpa": 0 }
}, },
"card_overdues": false, "card_overdues": false,
"relative_dues": false, "relative_dues": false,
"equal_height_cards": false, "equal_height_cards": false,
"hide_new_canvas": true, "hide_new_canvas": true,
"hide_sequence_footer": false, "hide_sequence_footer": false,
"hide_navbar": false, "hide_navbar": false,
"hide_sidebar": false, "hide_sidebar": false,
"quiz_safe_mode": false, "quiz_safe_mode": false,
"dark_mode_fix": [], "dark_mode_fix": [],
"assignment_states": {}, "assignment_states": {},
"tab_icons": false, "tab_icons": false,
"todo_hide_feedback": false, "todo_hide_feedback": false,
"todo_full_height": true, "todo_full_height": true,
"todo_progress_rings": "rings", "todo_progress_rings": "rings",
"todo_timeframe": "all", "todo_timeframe": "all",
"todo_timeframe_custom_past": 7, "todo_timeframe_custom_past": 7,
"todo_timeframe_custom_future": 14, "todo_timeframe_custom_future": 14,
"todo_celebrations_enabled": true, "todo_celebrations_enabled": true,
"todo_celebrations": ["confetti"], "todo_celebrations": ["confetti"],
"todo_celebration_amount": "normal", "todo_celebration_amount": "normal",
"todo_show_scores": false, "todo_show_scores": false,
"custom_font_skip_p": false, "custom_font_skip_p": false,
"device_dark": false, "device_dark": false,
"cumulative_gpa": { "name": "Cumulative GPA", "hidden": false, "weight": "dnc", "credits": 999, "gr": 3.21 }, "cumulative_gpa": { "name": "Cumulative GPA", "hidden": false, "weight": "dnc", "credits": 999, "gr": 3.21 },
"card_method_date": false, "card_method_date": false,
"card_method_dashboard": true, "card_method_dashboard": true,
"card_limit": 25, "card_limit": 25,
"remind": false, "remind": false,
"reminders": [], "reminders": [],
"reminder_count": 1, "reminder_count": 1,
"multi_remind": false, "multi_remind": false,
"id": "", "id": "",
"new_browser": null, "new_browser": null,
"gpa_calc_cumulative": false, "gpa_calc_cumulative": false,
"gpa_calc_weighted": true, "gpa_calc_weighted": true,
"gpa_calc_show_on_hover": false, "gpa_calc_show_on_hover": false,
"browser_show_likes": false, "browser_show_likes": false,
"custom_styles": "", "custom_styles": "",
"imageSize": 100, "imageSize": 100,
"cardRoundness": 5, "cardRoundness": 5,
"imageRoundness": 0, "imageRoundness": 0,
'cardSpacing': 0, 'cardSpacing': 0,
"cardWidth": 262, "cardWidth": 262,
"cardHeight": 146, "cardHeight": 146,
"customCardStyles": false, "customCardStyles": false,
"customBackgroundLink": "", "customBackgroundLink": "",
"customBackgroundColor": "", "customBackgroundColor": "",
"customBackgroundColorTransparent": false, "customBackgroundColorTransparent": false,
"customBackgroundScale": 100, "customBackgroundScale": 100,
"customBackgroundDaily": false, "customBackgroundDaily": false,
"customBackgroundNasaDaily": false, "customBackgroundNasaDaily": false,
"nasaInfoOverlay": false, "nasaInfoOverlay": false,
"fitImageToScreen": false, "fitImageToScreen": false,
"bg_opacity": 65, "bg_opacity": 65,
"sidebar_opacity": 100, "sidebar_opacity": 100,
"bg_blur": 8, "bg_blur": 8,
"sidebar_blur": 0, "sidebar_blur": 0,
"global_search": false, "global_search": false,
"grade_analytics": true, "grade_analytics": true,
"grade_analytics_zones": false, "grade_analytics_zones": false,
} }
}; };
const updateMsg = "Canvas Refined was just updated!\nThis version added new themes, cumulative GPA into the GPA calculator, and a new sorting function for themes."; const updateMsg = "Canvas Refined was just updated!\nThis version added new themes, cumulative GPA into the GPA calculator, and a new sorting function for themes.";
chrome.storage.local.get(null, local => { chrome.storage.local.get(null, local => {
chrome.storage.sync.get(null, async sync => { chrome.storage.sync.get(null, async sync => {
let newSyncOptions = {"update_msg": updateMsg}; let newSyncOptions = {"update_msg": updateMsg};
let newLocalOptions = {}; let newLocalOptions = {};
Object.keys(default_options["sync"]).forEach(option => { Object.keys(default_options["sync"]).forEach(option => {
if (sync[option] !== undefined) return; if (sync[option] !== undefined) return;
newSyncOptions[option] = default_options["sync"][option]; newSyncOptions[option] = default_options["sync"][option];
}); });
Object.keys(default_options["local"]).forEach(option => { Object.keys(default_options["local"]).forEach(option => {
if (local[option] !== undefined) return; if (local[option] !== undefined) return;
newLocalOptions[option] = default_options["local"][option]; newLocalOptions[option] = default_options["local"][option];
}) })
if (sync["nasaFitToScreen"] !== undefined && sync["fitImageToScreen"] === undefined) { if (sync["nasaFitToScreen"] !== undefined && sync["fitImageToScreen"] === undefined) {
newSyncOptions["fitImageToScreen"] = sync["nasaFitToScreen"]; newSyncOptions["fitImageToScreen"] = sync["nasaFitToScreen"];
} }
// The Top/Bottom widget rows used to share one height: give both rows the // The Top/Bottom widget rows used to share one height: give both rows the
// user's old value instead of dropping it to "auto". // user's old value instead of dropping it to "auto".
if (sync["widget_top_height"] === undefined && sync["widget_bottom_height"] === undefined) { if (sync["widget_top_height"] === undefined && sync["widget_bottom_height"] === undefined) {
const legacyRowHeight = Number(sync["widget_row_height"]); const legacyRowHeight = Number(sync["widget_row_height"]);
if (Number.isFinite(legacyRowHeight) && legacyRowHeight > 0) { if (Number.isFinite(legacyRowHeight) && legacyRowHeight > 0) {
newSyncOptions["widget_top_height"] = legacyRowHeight; newSyncOptions["widget_top_height"] = legacyRowHeight;
newSyncOptions["widget_bottom_height"] = legacyRowHeight; newSyncOptions["widget_bottom_height"] = legacyRowHeight;
} }
} }
if (Object.keys(newLocalOptions).length > 0) { if (Object.keys(newLocalOptions).length > 0) {
chrome.storage.local.set(newLocalOptions); chrome.storage.local.set(newLocalOptions);
} }
if (Object.keys(newSyncOptions).length > 0) { if (Object.keys(newSyncOptions).length > 0) {
chrome.storage.sync.set(newSyncOptions).then(() => { chrome.storage.sync.set(newSyncOptions).then(() => {
console.log(newSyncOptions); console.log(newSyncOptions);
if (newSyncOptions.new_install === true) { if (newSyncOptions.new_install === true) {
chrome.runtime.openOptionsPage(); chrome.runtime.openOptionsPage();
chrome.storage.sync.set({ new_install: false }); chrome.storage.sync.set({ new_install: false });
} }
}); });
} }
}); });
}); });
}); });
// APOD demo key allows 30 req/hour, 50/day; the API's own 429s do the limiting. // NASA serves APOD from its own site now: /wp-json/wp/v2/apod-basic/<YYMMDD>.
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { // No API key and no per-key quota, but the response carries no usable CORS
if (message?.type === "getNasaBackground") { // header, so science.nasa.gov needs a host permission (see manifest.json).
getNasaBackground().then(sendResponse); chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
return true; if (message?.type === "getNasaBackground") {
} getNasaBackground().then(sendResponse);
if (message?.type === "getDailyQuote") { return true;
getDailyQuote().then(sendResponse); }
return true; if (message?.type === "getDailyQuote") {
} getDailyQuote().then(sendResponse);
}); return true;
}
let nasaRequestQueue = Promise.resolve(); });
function queuedNasaTask(task) {
const run = nasaRequestQueue.then(task, task); let nasaRequestQueue = Promise.resolve();
nasaRequestQueue = run.catch(() => {}); function queuedNasaTask(task) {
return run; const run = nasaRequestQueue.then(task, task);
} nasaRequestQueue = run.catch(() => {});
return run;
async function getNasaBackground() { }
return queuedNasaTask(async () => {
const date = new Date(); async function getNasaBackground() {
for (let i = 0; i < 7; i++) { return queuedNasaTask(async () => {
const dateStr = date.toISOString().slice(0, 10); const date = new Date();
const cacheKey = `nasa_apod_${dateStr}`; const todayStr = date.toISOString().slice(0, 10);
const metadataKey = `nasa_apod_meta_${dateStr}`; for (let i = 0; i < 7; i++) {
const cached = await chrome.storage.local.get([cacheKey, metadataKey]); const dateStr = date.toISOString().slice(0, 10);
if (cached[cacheKey]) return cached[cacheKey]; const cacheKey = `nasa_apod_${dateStr}`;
const metadataKey = `nasa_apod_meta_${dateStr}`;
// never re-probe dates the API reported as missing const cached = await chrome.storage.local.get([cacheKey, metadataKey]);
const missingKey = `nasa_apod_missing_${dateStr}`; if (cached[cacheKey]) return cached[cacheKey];
const missing = await chrome.storage.local.get(missingKey);
if (missing[missingKey]) { // never re-probe dates the API reported as missing
date.setDate(date.getDate() - 1); const missingKey = `nasa_apod_missing_${dateStr}`;
continue; const missing = await chrome.storage.local.get(missingKey);
} if (missing[missingKey]) {
date.setDate(date.getDate() - 1);
const data = await callNasaApi(dateStr); continue;
if (data === "ratelimited" || data === null) return null; }
if (data === "missing") {
await chrome.storage.local.set({ [missingKey]: true }); const data = await callNasaApi(dateStr);
date.setDate(date.getDate() - 1); if (data === "ratelimited" || data === null) return null;
continue; const url = nasaApodImageUrl(data);
} if (data === "missing" || !url) {
// Video days carry only a generic NASA card in hdurl, so they make a
const url = data.thumbnail_url || data.hdurl || data.url; // poor wallpaper; skip them like a missing date. Only persist the
if (!url) return null; // marker for past dates: today's APOD may simply not be posted yet,
const result = { url, scale: 100, date: dateStr }; // and a permanent marker would hide it for good.
const meta = { title: data.title || "", date: data.date || dateStr, copyright: data.copyright || "", explanation: data.explanation || "" }; if (dateStr < todayStr || data !== "missing") {
// one write, so the overlay never sees an image without its metadata await chrome.storage.local.set({ [missingKey]: true });
await chrome.storage.local.set({ [cacheKey]: result, [metadataKey]: meta }); }
return result; date.setDate(date.getDate() - 1);
} continue;
return null; }
});
} const result = { url, scale: 100, date: dateStr };
// The feed ships HTML (links, <strong>, entities) where the old API
async function callNasaApi(dateStr) { // sent plain text; the overlay renders textContent, so strip it.
let response; const meta = {
try { title: nasaPlainText(data.title),
response = await fetch(`https://api.nasa.gov/planetary/apod?api_key=DEMO_KEY&thumbs=true&date=${dateStr}`); date: data.date || dateStr,
} catch (error) { copyright: nasaPlainText(data.copyright || data.credit),
console.error("[CanvasRefined] Failed to fetch NASA APOD:", error); explanation: nasaPlainText(data.explanation)
return null; };
} // one write, so the overlay never sees an image without its metadata
await chrome.storage.local.set({ [cacheKey]: result, [metadataKey]: meta });
if (response.status === 429) return "ratelimited"; return result;
if (!response.ok) { }
const errorData = await response.json().catch(() => ({})); return null;
if (errorData.code === 404 && (errorData.msg || "").toLowerCase().includes("no data available")) { });
return "missing"; }
}
return null; // "2026-10-27" -> "261027", the date path this endpoint expects.
} function nasaApodPathKey(dateStr) {
return String(dateStr).replace(/-/g, "").slice(2);
return await response.json().catch(() => null); }
}
// Video entries point hdurl at a generic placeholder, not a still frame.
// Cached in storage.local for one day; fetched from the worker (no CORS headers). const NASA_APOD_PLACEHOLDER = /news-thumbnail\.png/i;
function dailyQuoteTodayKey() {
const d = new Date(); function nasaApodImageUrl(data) {
return d.getFullYear() + "-" + String(d.getMonth() + 1).padStart(2, "0") + "-" + String(d.getDate()).padStart(2, "0"); if (!data || typeof data !== "object") return null;
} if (data.media_type && data.media_type !== "image") return null;
// hdurl is the asset itself; url is the article page, so only trust it when
let dailyQuoteInFlight = null; // it actually points at a picture.
async function getDailyQuote() { const url = data.hdurl || (/\/\S*\.(jpe?g|png|webp|gif)(\?|$)/i.test(data.url || "") ? data.url : null);
const today = dailyQuoteTodayKey(); if (typeof url !== "string" || !/^https?:\/\//i.test(url)) return null;
let cached = null; if (NASA_APOD_PLACEHOLDER.test(url)) return null;
try { return url;
const st = await chrome.storage.local.get("daily_quote"); }
cached = st && st.daily_quote;
} catch (e) { /* ignore storage issues */ } function nasaPlainText(html) {
if (!html) return "";
if (cached && cached.date === today && cached.quote) return cached; return String(html)
if (dailyQuoteInFlight) return dailyQuoteInFlight; .replace(/<br\s*\/?>/gi, " ")
.replace(/<\/(p|div|li)>/gi, " ")
const request = (async () => { .replace(/<[^>]*>/g, "")
try { .replace(/&#x([0-9a-f]+);/gi, (_, hex) => String.fromCodePoint(parseInt(hex, 16)))
const response = await fetch("https://stoic.tekloon.net/stoic-quote", { cache: "no-store" }); .replace(/&#(\d+);/g, (_, dec) => String.fromCodePoint(Number(dec)))
if (!response.ok) throw new Error(`HTTP ${response.status}`); .replace(/&nbsp;/gi, " ")
const json = await response.json(); .replace(/&quot;/gi, '"')
const data = (json && json.data) || {}; .replace(/&apos;/gi, "'")
const quote = String(data.quote || "").replace(/\s+/g, " ").trim(); .replace(/&mdash;/gi, "\u2014")
const author = String(data.author || "").trim(); .replace(/&ndash;/gi, "\u2013")
if (!quote) throw new Error("empty quote"); .replace(/&amp;/gi, "&")
const entry = { date: today, quote, author }; .replace(/\s+/g, " ")
await chrome.storage.local.set({ daily_quote: entry }); .trim();
return entry; }
} catch (e) {
console.warn("[CanvasRefined] Daily quote fetch failed:", e); async function callNasaApi(dateStr) {
return cached || null; // _fields trims the payload: the default also inlines the whole article HTML.
} finally { const fields = "date,title,media_type,hdurl,url,permalink,credit,copyright,explanation,alt";
dailyQuoteInFlight = null; let response;
} try {
})(); response = await fetch(`https://science.nasa.gov/wp-json/wp/v2/apod-basic/${nasaApodPathKey(dateStr)}?_fields=${fields}`);
dailyQuoteInFlight = request; } catch (error) {
return request; console.error("[CanvasRefined] Failed to fetch NASA APOD:", error);
} return null;
}
if (response.status === 429) return "ratelimited";
if (!response.ok) {
if (response.status === 404) {
const errorData = await response.json().catch(() => null);
if (errorData?.code === "apod_basic_not_found") return "missing";
}
return null;
}
return await response.json().catch(() => null);
}
// Cached in storage.local for one day; fetched from the worker (no CORS headers).
function dailyQuoteTodayKey() {
const d = new Date();
return d.getFullYear() + "-" + String(d.getMonth() + 1).padStart(2, "0") + "-" + String(d.getDate()).padStart(2, "0");
}
let dailyQuoteInFlight = null;
async function getDailyQuote() {
const today = dailyQuoteTodayKey();
let cached = null;
try {
const st = await chrome.storage.local.get("daily_quote");
cached = st && st.daily_quote;
} catch (e) { /* ignore storage issues */ }
if (cached && cached.date === today && cached.quote) return cached;
if (dailyQuoteInFlight) return dailyQuoteInFlight;
const request = (async () => {
try {
const response = await fetch("https://stoic.tekloon.net/stoic-quote", { cache: "no-store" });
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const json = await response.json();
const data = (json && json.data) || {};
const quote = String(data.quote || "").replace(/\s+/g, " ").trim();
const author = String(data.author || "").trim();
if (!quote) throw new Error("empty quote");
const entry = { date: today, quote, author };
await chrome.storage.local.set({ daily_quote: entry });
return entry;
} catch (e) {
console.warn("[CanvasRefined] Daily quote fetch failed:", e);
return cached || null;
} finally {
dailyQuoteInFlight = null;
}
})();
dailyQuoteInFlight = request;
return request;
}

View File

@ -31,7 +31,7 @@
} }
], ],
"permissions": ["storage", "unlimitedStorage"], "permissions": ["storage", "unlimitedStorage"],
"host_permissions": ["https://stoic.tekloon.net/*"], "host_permissions": ["https://stoic.tekloon.net/*", "https://science.nasa.gov/*"],
"default_locale": "en", "default_locale": "en",
"browser_specific_settings": { "browser_specific_settings": {
"gecko": { "gecko": {