@copilotkit/web-inspector 1.73.2 → 1.74.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +363 -271
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +29 -6
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +29 -6
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +364 -272
- package/dist/index.mjs.map +1 -1
- package/dist/index.umd.js +1274 -628
- package/dist/index.umd.js.map +1 -1
- package/dist/lib/notification-loader.cjs +38 -0
- package/dist/lib/notification-loader.cjs.map +1 -0
- package/dist/lib/notification-loader.mjs +38 -0
- package/dist/lib/notification-loader.mjs.map +1 -0
- package/dist/lib/notifications.cjs +173 -0
- package/dist/lib/notifications.cjs.map +1 -0
- package/dist/lib/notifications.d.cts +17 -0
- package/dist/lib/notifications.d.cts.map +1 -0
- package/dist/lib/notifications.d.mts +17 -0
- package/dist/lib/notifications.d.mts.map +1 -0
- package/dist/lib/notifications.mjs +168 -0
- package/dist/lib/notifications.mjs.map +1 -0
- package/dist/lib/onboarding-prompt.cjs +11 -17
- package/dist/lib/onboarding-prompt.cjs.map +1 -1
- package/dist/lib/onboarding-prompt.mjs +11 -17
- package/dist/lib/onboarding-prompt.mjs.map +1 -1
- package/dist/lib/persistence.cjs +124 -48
- package/dist/lib/persistence.cjs.map +1 -1
- package/dist/lib/persistence.mjs +121 -45
- package/dist/lib/persistence.mjs.map +1 -1
- package/dist/lib/telemetry.cjs +42 -0
- package/dist/lib/telemetry.cjs.map +1 -1
- package/dist/lib/telemetry.d.cts.map +1 -1
- package/dist/lib/telemetry.d.mts.map +1 -1
- package/dist/lib/telemetry.mjs +35 -1
- package/dist/lib/telemetry.mjs.map +1 -1
- package/dist/package.cjs +1 -1
- package/dist/package.mjs +1 -1
- package/dist/styles/generated.cjs +1 -1
- package/dist/styles/generated.cjs.map +1 -1
- package/dist/styles/generated.mjs +1 -1
- package/dist/styles/generated.mjs.map +1 -1
- package/dist/styles/notification-article.cjs +244 -0
- package/dist/styles/notification-article.cjs.map +1 -0
- package/dist/styles/notification-article.mjs +244 -0
- package/dist/styles/notification-article.mjs.map +1 -0
- package/package.json +4 -3
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"onboarding-prompt.cjs","names":[],"sources":["../../src/lib/onboarding-prompt.ts"],"sourcesContent":["import type { HomeServiceId } from \"./home-briefing.js\";\n\n/**\n * The Intelligence install prompt the Inspector hands to a coding agent.\n *\n * Every other Intelligence call to action in the Inspector opens a page in a\n * new tab, which is where developers drop out: they leave the editor, meet a\n * signup form, and never come back. This prompt lets the install happen where\n * they already are.\n *\n * The wording is kept byte-identical to `INTELLIGENCE_ONBOARDING_PROMPT` in the\n * Intelligence app (`apps/app-frontend/react-shell/src/home/intelligence-home.tsx`).\n * The CLI resolves `onboard start` against a prompt graph, so the two surfaces\n * must not drift — if the CLI's entry point changes, both change together.\n *\n *
|
|
1
|
+
{"version":3,"file":"onboarding-prompt.cjs","names":[],"sources":["../../src/lib/onboarding-prompt.ts"],"sourcesContent":["import type { HomeServiceId } from \"./home-briefing.js\";\n\n/**\n * The Intelligence install prompt the Inspector hands to a coding agent.\n *\n * Every other Intelligence call to action in the Inspector opens a page in a\n * new tab, which is where developers drop out: they leave the editor, meet a\n * signup form, and never come back. This prompt lets the install happen where\n * they already are.\n *\n * The wording is kept byte-identical to `INTELLIGENCE_ONBOARDING_PROMPT` in the\n * Intelligence app (`apps/app-frontend/react-shell/src/home/intelligence-home.tsx`).\n * The CLI resolves `onboard start` against a prompt graph, so the two surfaces\n * must not drift — if the CLI's entry point changes, both change together.\n *\n * One sentence, and deliberately nothing else. It used to carry the `npx`\n * command itself, which put a command line and a telemetry id in the one piece\n * of text a human reads, decides on and pastes. The command now lives in the\n * document the URL serves, with the run id already written into it\n * (PE-216/PE-217).\n *\n * This one stays generic, and so do the docs and Intelligence web-app copies\n * of it (OSS-1150). Those three are entry points for a developer with no\n * CopilotKit app yet, and every `--intent` route requires an existing app: it\n * inspects for one first and reads `feature/stop` when it is missing. The\n * feature buttons on Home are the surface that knows which feature the\n * developer is looking at, so they are the surface that names an intent — see\n * `FEATURE_ONBOARDING_PROMPT_TEMPLATE` below.\n */\nconst RUN_ID_PLACEHOLDER = \"<run-id>\";\n\nexport const ONBOARDING_PROMPT_ORIGIN = \"https://copilotkit.ai\";\n\nexport const ONBOARDING_PROMPT_TEMPLATE = `Read ${ONBOARDING_PROMPT_ORIGIN}/onboarding-prompts/${RUN_ID_PLACEHOLDER} and help me get set up.`;\n\n/** Length and alphabet are the CLI's, so a run id copied here resolves there. */\nconst RUN_ID_LENGTH = 12;\n\n/**\n * Mint the identifier that joins this copy to the CLI run it starts.\n *\n * The Intelligence app derives it from `crypto.randomUUID()`. The Inspector\n * runs inside whatever page embeds it, so neither `randomUUID` nor a secure\n * context is guaranteed; the two fallbacks keep the id present rather than\n * letting the button fail. Collision risk is irrelevant — this correlates\n * telemetry, it does not authorise anything.\n */\nexport function createOnboardingRunId(): string {\n const webCrypto = globalThis.crypto;\n\n if (typeof webCrypto?.randomUUID === \"function\") {\n return webCrypto.randomUUID().replaceAll(\"-\", \"\").slice(0, RUN_ID_LENGTH);\n }\n\n if (typeof webCrypto?.getRandomValues === \"function\") {\n const bytes = webCrypto.getRandomValues(new Uint8Array(RUN_ID_LENGTH / 2));\n return Array.from(bytes, (byte) => byte.toString(16).padStart(2, \"0\")).join(\n \"\",\n );\n }\n\n let id = \"\";\n while (id.length < RUN_ID_LENGTH) {\n id += Math.floor(Math.random() * 16).toString(16);\n }\n return id.slice(0, RUN_ID_LENGTH);\n}\n\n/** Bind one run id into the prompt text that gets copied. */\nexport function createOnboardingPrompt(runId: string): string {\n return ONBOARDING_PROMPT_TEMPLATE.replace(RUN_ID_PLACEHOLDER, runId);\n}\n\n/**\n * The closed set of feature outcomes the CLI prompt graph can start at.\n *\n * Kept equal to `ONBOARDING_INTENT_ROOTS` in Intelligence's\n * `apps/cli/onboarding-intents.cjs`, which is the build-time source of truth.\n * The two repositories cannot import each other, so the agreement is held by\n * this list plus the CLI's own `onboarding-intents.spec.ts`. Drift is loud\n * rather than silent: `onboard start` refuses an unknown `--intent` before it\n * persists a run, so a stale slug fails on the first command instead of\n * quietly onboarding the wrong feature.\n */\nexport const ONBOARDING_INTENTS = [\n \"add-a2ui\",\n \"add-chat-suggestions\",\n \"add-learning\",\n \"add-open-generative-ui\",\n \"add-realtime-sync\",\n \"add-rich-threads\",\n \"add-voice\",\n] as const;\n\nexport type OnboardingIntent = (typeof ONBOARDING_INTENTS)[number];\n\n/**\n * Which feature outcome each Home tile asks the CLI for.\n *\n * The two vocabularies stay separate on purpose. A `HomeServiceId` names a\n * tile and the pane behind it, so `websocket` and `audio` are the right words\n * for it; an intent names the work to be done, so `add-realtime-sync` and\n * `add-voice` are the right words for that. One table maps between them, and\n * `Record<HomeServiceId, OnboardingIntent>` makes a new tile or a renamed\n * slug a type error rather than a missing button.\n *\n * `memory` maps to `add-learning` because the tile is the Learning tile: it is\n * enabled by a *configured Learning container* (see `learningOn` in\n * `index.ts`), which is exactly what `feature/learning` sets up. The tile id\n * and the `memories` menu key are older names for that same pane.\n */\nexport const FEATURE_ONBOARDING_INTENT: Record<\n HomeServiceId,\n OnboardingIntent\n> = {\n threads: \"add-rich-threads\",\n memory: \"add-learning\",\n a2ui: \"add-a2ui\",\n \"open-gen-ui\": \"add-open-generative-ui\",\n suggestions: \"add-chat-suggestions\",\n audio: \"add-voice\",\n websocket: \"add-realtime-sync\",\n};\n\nconst INTENT_PLACEHOLDER = \"<intent>\";\n\n/**\n * The prompt a Home feature button copies.\n *\n * It carries no feature-specific instruction, and it must not gain any. The\n * `--intent` route owns the plan, the guide links, the per-phase check-ins,\n * the refusal when a prerequisite is missing, and the proof step; prose copied\n * here would duplicate all of that and then drift from it the next time the\n * underlying API changes.\n *\n * The session-check permission the developer granted by copying this, and the\n * credentials warning beside it, moved with the command into the document the\n * URL serves. They are not dropped.\n */\nexport const FEATURE_ONBOARDING_PROMPT_TEMPLATE = `Read ${ONBOARDING_PROMPT_ORIGIN}/onboarding-prompts/${RUN_ID_PLACEHOLDER}?intent=${INTENT_PLACEHOLDER} and help me set this up.`;\n\n/** Bind one run id and one tile's feature outcome into the copied prompt. */\nexport function createFeatureOnboardingPrompt(\n serviceId: HomeServiceId,\n runId: string,\n): string {\n return FEATURE_ONBOARDING_PROMPT_TEMPLATE.replace(\n RUN_ID_PLACEHOLDER,\n runId,\n ).replace(INTENT_PLACEHOLDER, FEATURE_ONBOARDING_INTENT[serviceId]);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6BA,MAAM,qBAAqB;AAE3B,MAAa,2BAA2B;AAExC,MAAa,6BAA6B,QAAQ,yBAAyB,sBAAsB,mBAAmB;;AAGpH,MAAM,gBAAgB;;;;;;;;;;AAWtB,SAAgB,wBAAgC;CAC9C,MAAM,YAAY,WAAW;AAE7B,KAAI,OAAO,WAAW,eAAe,WACnC,QAAO,UAAU,YAAY,CAAC,WAAW,KAAK,GAAG,CAAC,MAAM,GAAG,cAAc;AAG3E,KAAI,OAAO,WAAW,oBAAoB,YAAY;EACpD,MAAM,QAAQ,UAAU,gBAAgB,IAAI,WAAW,gBAAgB,EAAE,CAAC;AAC1E,SAAO,MAAM,KAAK,QAAQ,SAAS,KAAK,SAAS,GAAG,CAAC,SAAS,GAAG,IAAI,CAAC,CAAC,KACrE,GACD;;CAGH,IAAI,KAAK;AACT,QAAO,GAAG,SAAS,cACjB,OAAM,KAAK,MAAM,KAAK,QAAQ,GAAG,GAAG,CAAC,SAAS,GAAG;AAEnD,QAAO,GAAG,MAAM,GAAG,cAAc;;;AAInC,SAAgB,uBAAuB,OAAuB;AAC5D,QAAO,2BAA2B,QAAQ,oBAAoB,MAAM;;;;;;;;;;;;;;;;;AAyCtE,MAAa,4BAGT;CACF,SAAS;CACT,QAAQ;CACR,MAAM;CACN,eAAe;CACf,aAAa;CACb,OAAO;CACP,WAAW;CACZ;AAED,MAAM,qBAAqB;;;;;;;;;;;;;;AAe3B,MAAa,qCAAqC,QAAQ,yBAAyB,sBAAsB,mBAAmB,UAAU,mBAAmB;;AAGzJ,SAAgB,8BACd,WACA,OACQ;AACR,QAAO,mCAAmC,QACxC,oBACA,MACD,CAAC,QAAQ,oBAAoB,0BAA0B,WAAW"}
|
|
@@ -12,12 +12,11 @@
|
|
|
12
12
|
* The CLI resolves `onboard start` against a prompt graph, so the two surfaces
|
|
13
13
|
* must not drift — if the CLI's entry point changes, both change together.
|
|
14
14
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
* (Intelligence OSS-1157).
|
|
15
|
+
* One sentence, and deliberately nothing else. It used to carry the `npx`
|
|
16
|
+
* command itself, which put a command line and a telemetry id in the one piece
|
|
17
|
+
* of text a human reads, decides on and pastes. The command now lives in the
|
|
18
|
+
* document the URL serves, with the run id already written into it
|
|
19
|
+
* (PE-216/PE-217).
|
|
21
20
|
*
|
|
22
21
|
* This one stays generic, and so do the docs and Intelligence web-app copies
|
|
23
22
|
* of it (OSS-1150). Those three are entry points for a developer with no
|
|
@@ -28,9 +27,8 @@
|
|
|
28
27
|
* `FEATURE_ONBOARDING_PROMPT_TEMPLATE` below.
|
|
29
28
|
*/
|
|
30
29
|
const RUN_ID_PLACEHOLDER = "<run-id>";
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
npx --yes copilotkit@latest onboard start --run ${RUN_ID_PLACEHOLDER}`;
|
|
30
|
+
const ONBOARDING_PROMPT_ORIGIN = "https://copilotkit.ai";
|
|
31
|
+
const ONBOARDING_PROMPT_TEMPLATE = `Read ${ONBOARDING_PROMPT_ORIGIN}/onboarding-prompts/${RUN_ID_PLACEHOLDER} and help me get set up.`;
|
|
34
32
|
/** Length and alphabet are the CLI's, so a run id copied here resolves there. */
|
|
35
33
|
const RUN_ID_LENGTH = 12;
|
|
36
34
|
/**
|
|
@@ -91,15 +89,11 @@ const INTENT_PLACEHOLDER = "<intent>";
|
|
|
91
89
|
* here would duplicate all of that and then drift from it the next time the
|
|
92
90
|
* underlying API changes.
|
|
93
91
|
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
92
|
+
* The session-check permission the developer granted by copying this, and the
|
|
93
|
+
* credentials warning beside it, moved with the command into the document the
|
|
94
|
+
* URL serves. They are not dropped.
|
|
97
95
|
*/
|
|
98
|
-
const FEATURE_ONBOARDING_PROMPT_TEMPLATE = `
|
|
99
|
-
|
|
100
|
-
npx --yes copilotkit@latest onboard start --run ${RUN_ID_PLACEHOLDER} --intent ${INTENT_PLACEHOLDER}
|
|
101
|
-
|
|
102
|
-
If it requires a CopilotKit CLI session check, you have permission to run it. Never reveal credentials.`;
|
|
96
|
+
const FEATURE_ONBOARDING_PROMPT_TEMPLATE = `Read ${ONBOARDING_PROMPT_ORIGIN}/onboarding-prompts/${RUN_ID_PLACEHOLDER}?intent=${INTENT_PLACEHOLDER} and help me set this up.`;
|
|
103
97
|
/** Bind one run id and one tile's feature outcome into the copied prompt. */
|
|
104
98
|
function createFeatureOnboardingPrompt(serviceId, runId) {
|
|
105
99
|
return FEATURE_ONBOARDING_PROMPT_TEMPLATE.replace(RUN_ID_PLACEHOLDER, runId).replace(INTENT_PLACEHOLDER, FEATURE_ONBOARDING_INTENT[serviceId]);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"onboarding-prompt.mjs","names":[],"sources":["../../src/lib/onboarding-prompt.ts"],"sourcesContent":["import type { HomeServiceId } from \"./home-briefing.js\";\n\n/**\n * The Intelligence install prompt the Inspector hands to a coding agent.\n *\n * Every other Intelligence call to action in the Inspector opens a page in a\n * new tab, which is where developers drop out: they leave the editor, meet a\n * signup form, and never come back. This prompt lets the install happen where\n * they already are.\n *\n * The wording is kept byte-identical to `INTELLIGENCE_ONBOARDING_PROMPT` in the\n * Intelligence app (`apps/app-frontend/react-shell/src/home/intelligence-home.tsx`).\n * The CLI resolves `onboard start` against a prompt graph, so the two surfaces\n * must not drift — if the CLI's entry point changes, both change together.\n *\n *
|
|
1
|
+
{"version":3,"file":"onboarding-prompt.mjs","names":[],"sources":["../../src/lib/onboarding-prompt.ts"],"sourcesContent":["import type { HomeServiceId } from \"./home-briefing.js\";\n\n/**\n * The Intelligence install prompt the Inspector hands to a coding agent.\n *\n * Every other Intelligence call to action in the Inspector opens a page in a\n * new tab, which is where developers drop out: they leave the editor, meet a\n * signup form, and never come back. This prompt lets the install happen where\n * they already are.\n *\n * The wording is kept byte-identical to `INTELLIGENCE_ONBOARDING_PROMPT` in the\n * Intelligence app (`apps/app-frontend/react-shell/src/home/intelligence-home.tsx`).\n * The CLI resolves `onboard start` against a prompt graph, so the two surfaces\n * must not drift — if the CLI's entry point changes, both change together.\n *\n * One sentence, and deliberately nothing else. It used to carry the `npx`\n * command itself, which put a command line and a telemetry id in the one piece\n * of text a human reads, decides on and pastes. The command now lives in the\n * document the URL serves, with the run id already written into it\n * (PE-216/PE-217).\n *\n * This one stays generic, and so do the docs and Intelligence web-app copies\n * of it (OSS-1150). Those three are entry points for a developer with no\n * CopilotKit app yet, and every `--intent` route requires an existing app: it\n * inspects for one first and reads `feature/stop` when it is missing. The\n * feature buttons on Home are the surface that knows which feature the\n * developer is looking at, so they are the surface that names an intent — see\n * `FEATURE_ONBOARDING_PROMPT_TEMPLATE` below.\n */\nconst RUN_ID_PLACEHOLDER = \"<run-id>\";\n\nexport const ONBOARDING_PROMPT_ORIGIN = \"https://copilotkit.ai\";\n\nexport const ONBOARDING_PROMPT_TEMPLATE = `Read ${ONBOARDING_PROMPT_ORIGIN}/onboarding-prompts/${RUN_ID_PLACEHOLDER} and help me get set up.`;\n\n/** Length and alphabet are the CLI's, so a run id copied here resolves there. */\nconst RUN_ID_LENGTH = 12;\n\n/**\n * Mint the identifier that joins this copy to the CLI run it starts.\n *\n * The Intelligence app derives it from `crypto.randomUUID()`. The Inspector\n * runs inside whatever page embeds it, so neither `randomUUID` nor a secure\n * context is guaranteed; the two fallbacks keep the id present rather than\n * letting the button fail. Collision risk is irrelevant — this correlates\n * telemetry, it does not authorise anything.\n */\nexport function createOnboardingRunId(): string {\n const webCrypto = globalThis.crypto;\n\n if (typeof webCrypto?.randomUUID === \"function\") {\n return webCrypto.randomUUID().replaceAll(\"-\", \"\").slice(0, RUN_ID_LENGTH);\n }\n\n if (typeof webCrypto?.getRandomValues === \"function\") {\n const bytes = webCrypto.getRandomValues(new Uint8Array(RUN_ID_LENGTH / 2));\n return Array.from(bytes, (byte) => byte.toString(16).padStart(2, \"0\")).join(\n \"\",\n );\n }\n\n let id = \"\";\n while (id.length < RUN_ID_LENGTH) {\n id += Math.floor(Math.random() * 16).toString(16);\n }\n return id.slice(0, RUN_ID_LENGTH);\n}\n\n/** Bind one run id into the prompt text that gets copied. */\nexport function createOnboardingPrompt(runId: string): string {\n return ONBOARDING_PROMPT_TEMPLATE.replace(RUN_ID_PLACEHOLDER, runId);\n}\n\n/**\n * The closed set of feature outcomes the CLI prompt graph can start at.\n *\n * Kept equal to `ONBOARDING_INTENT_ROOTS` in Intelligence's\n * `apps/cli/onboarding-intents.cjs`, which is the build-time source of truth.\n * The two repositories cannot import each other, so the agreement is held by\n * this list plus the CLI's own `onboarding-intents.spec.ts`. Drift is loud\n * rather than silent: `onboard start` refuses an unknown `--intent` before it\n * persists a run, so a stale slug fails on the first command instead of\n * quietly onboarding the wrong feature.\n */\nexport const ONBOARDING_INTENTS = [\n \"add-a2ui\",\n \"add-chat-suggestions\",\n \"add-learning\",\n \"add-open-generative-ui\",\n \"add-realtime-sync\",\n \"add-rich-threads\",\n \"add-voice\",\n] as const;\n\nexport type OnboardingIntent = (typeof ONBOARDING_INTENTS)[number];\n\n/**\n * Which feature outcome each Home tile asks the CLI for.\n *\n * The two vocabularies stay separate on purpose. A `HomeServiceId` names a\n * tile and the pane behind it, so `websocket` and `audio` are the right words\n * for it; an intent names the work to be done, so `add-realtime-sync` and\n * `add-voice` are the right words for that. One table maps between them, and\n * `Record<HomeServiceId, OnboardingIntent>` makes a new tile or a renamed\n * slug a type error rather than a missing button.\n *\n * `memory` maps to `add-learning` because the tile is the Learning tile: it is\n * enabled by a *configured Learning container* (see `learningOn` in\n * `index.ts`), which is exactly what `feature/learning` sets up. The tile id\n * and the `memories` menu key are older names for that same pane.\n */\nexport const FEATURE_ONBOARDING_INTENT: Record<\n HomeServiceId,\n OnboardingIntent\n> = {\n threads: \"add-rich-threads\",\n memory: \"add-learning\",\n a2ui: \"add-a2ui\",\n \"open-gen-ui\": \"add-open-generative-ui\",\n suggestions: \"add-chat-suggestions\",\n audio: \"add-voice\",\n websocket: \"add-realtime-sync\",\n};\n\nconst INTENT_PLACEHOLDER = \"<intent>\";\n\n/**\n * The prompt a Home feature button copies.\n *\n * It carries no feature-specific instruction, and it must not gain any. The\n * `--intent` route owns the plan, the guide links, the per-phase check-ins,\n * the refusal when a prerequisite is missing, and the proof step; prose copied\n * here would duplicate all of that and then drift from it the next time the\n * underlying API changes.\n *\n * The session-check permission the developer granted by copying this, and the\n * credentials warning beside it, moved with the command into the document the\n * URL serves. They are not dropped.\n */\nexport const FEATURE_ONBOARDING_PROMPT_TEMPLATE = `Read ${ONBOARDING_PROMPT_ORIGIN}/onboarding-prompts/${RUN_ID_PLACEHOLDER}?intent=${INTENT_PLACEHOLDER} and help me set this up.`;\n\n/** Bind one run id and one tile's feature outcome into the copied prompt. */\nexport function createFeatureOnboardingPrompt(\n serviceId: HomeServiceId,\n runId: string,\n): string {\n return FEATURE_ONBOARDING_PROMPT_TEMPLATE.replace(\n RUN_ID_PLACEHOLDER,\n runId,\n ).replace(INTENT_PLACEHOLDER, FEATURE_ONBOARDING_INTENT[serviceId]);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6BA,MAAM,qBAAqB;AAE3B,MAAa,2BAA2B;AAExC,MAAa,6BAA6B,QAAQ,yBAAyB,sBAAsB,mBAAmB;;AAGpH,MAAM,gBAAgB;;;;;;;;;;AAWtB,SAAgB,wBAAgC;CAC9C,MAAM,YAAY,WAAW;AAE7B,KAAI,OAAO,WAAW,eAAe,WACnC,QAAO,UAAU,YAAY,CAAC,WAAW,KAAK,GAAG,CAAC,MAAM,GAAG,cAAc;AAG3E,KAAI,OAAO,WAAW,oBAAoB,YAAY;EACpD,MAAM,QAAQ,UAAU,gBAAgB,IAAI,WAAW,gBAAgB,EAAE,CAAC;AAC1E,SAAO,MAAM,KAAK,QAAQ,SAAS,KAAK,SAAS,GAAG,CAAC,SAAS,GAAG,IAAI,CAAC,CAAC,KACrE,GACD;;CAGH,IAAI,KAAK;AACT,QAAO,GAAG,SAAS,cACjB,OAAM,KAAK,MAAM,KAAK,QAAQ,GAAG,GAAG,CAAC,SAAS,GAAG;AAEnD,QAAO,GAAG,MAAM,GAAG,cAAc;;;AAInC,SAAgB,uBAAuB,OAAuB;AAC5D,QAAO,2BAA2B,QAAQ,oBAAoB,MAAM;;;;;;;;;;;;;;;;;AAyCtE,MAAa,4BAGT;CACF,SAAS;CACT,QAAQ;CACR,MAAM;CACN,eAAe;CACf,aAAa;CACb,OAAO;CACP,WAAW;CACZ;AAED,MAAM,qBAAqB;;;;;;;;;;;;;;AAe3B,MAAa,qCAAqC,QAAQ,yBAAyB,sBAAsB,mBAAmB,UAAU,mBAAmB;;AAGzJ,SAAgB,8BACd,WACA,OACQ;AACR,QAAO,mCAAmC,QACxC,oBACA,MACD,CAAC,QAAQ,oBAAoB,0BAA0B,WAAW"}
|
package/dist/lib/persistence.cjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
const require_notifications = require('./notifications.cjs');
|
|
1
2
|
|
|
2
3
|
//#region src/lib/persistence.ts
|
|
3
4
|
function loadInspectorState(storageKey) {
|
|
@@ -95,30 +96,8 @@ function parseInspectorDismissalPayload(raw) {
|
|
|
95
96
|
return null;
|
|
96
97
|
}
|
|
97
98
|
}
|
|
98
|
-
const ANNOUNCEMENT_READ_COOKIE_NAME = "cpk_inspector_announcements";
|
|
99
|
-
const ANNOUNCEMENT_READ_MIRROR_KEY = "cpk:inspector:announcement_read";
|
|
100
99
|
const LEGACY_ANNOUNCEMENT_READ_KEY = "cpk:inspector:announcements";
|
|
101
100
|
const ANNOUNCEMENT_PULSED_SESSION_KEY = "cpk:inspector:pulsed";
|
|
102
|
-
const ANNOUNCEMENT_READ_COOKIE_MAX_AGE_SECONDS = 3600 * 24 * 365;
|
|
103
|
-
/**
|
|
104
|
-
* The announcement timestamp the user has already read, or `null` when
|
|
105
|
-
* nothing has been read yet. Prefers the host-scoped cookie and falls back to
|
|
106
|
-
* the origin-scoped mirror, so cookie-blocking browsers still remember the
|
|
107
|
-
* read within the port they are on.
|
|
108
|
-
*/
|
|
109
|
-
function loadAnnouncementReadTimestamp() {
|
|
110
|
-
return parseTimestampPayload(readAnnouncementCookie()) ?? parseTimestampPayload(readLocalStorageItem(ANNOUNCEMENT_READ_MIRROR_KEY));
|
|
111
|
-
}
|
|
112
|
-
/**
|
|
113
|
-
* Records an announcement as read in both the host-scoped cookie and the
|
|
114
|
-
* origin-scoped mirror. The stored value is `{"timestamp":"…"}` — the same
|
|
115
|
-
* shape the announcement state has always used.
|
|
116
|
-
*/
|
|
117
|
-
function saveAnnouncementReadTimestamp(timestamp) {
|
|
118
|
-
const payload = JSON.stringify({ timestamp });
|
|
119
|
-
writeAnnouncementCookie(payload);
|
|
120
|
-
writeLocalStorageItem(ANNOUNCEMENT_READ_MIRROR_KEY, payload);
|
|
121
|
-
}
|
|
122
101
|
/**
|
|
123
102
|
* Deletes the superseded origin-scoped read state. Safe to call on every
|
|
124
103
|
* startup: nothing writes that key any more, so the value cannot come back.
|
|
@@ -138,28 +117,6 @@ function loadAnnouncementPulsedTimestamp() {
|
|
|
138
117
|
return null;
|
|
139
118
|
}
|
|
140
119
|
}
|
|
141
|
-
/** Suppresses further pulses for this announcement in this browser tab. */
|
|
142
|
-
function saveAnnouncementPulsedTimestamp(timestamp) {
|
|
143
|
-
if (typeof window === "undefined") return;
|
|
144
|
-
try {
|
|
145
|
-
window.sessionStorage.setItem(ANNOUNCEMENT_PULSED_SESSION_KEY, timestamp);
|
|
146
|
-
} catch {}
|
|
147
|
-
}
|
|
148
|
-
function parseTimestampPayload(raw) {
|
|
149
|
-
if (!raw) return null;
|
|
150
|
-
try {
|
|
151
|
-
const parsed = JSON.parse(raw);
|
|
152
|
-
return typeof parsed?.timestamp === "string" ? parsed.timestamp : null;
|
|
153
|
-
} catch {
|
|
154
|
-
return null;
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
function readAnnouncementCookie() {
|
|
158
|
-
return readCookie(ANNOUNCEMENT_READ_COOKIE_NAME);
|
|
159
|
-
}
|
|
160
|
-
function writeAnnouncementCookie(value) {
|
|
161
|
-
writeCookie(ANNOUNCEMENT_READ_COOKIE_NAME, value, `Max-Age=${ANNOUNCEMENT_READ_COOKIE_MAX_AGE_SECONDS}`);
|
|
162
|
-
}
|
|
163
120
|
function readCookie(name) {
|
|
164
121
|
if (typeof document === "undefined") return null;
|
|
165
122
|
try {
|
|
@@ -243,24 +200,143 @@ function generateUuidV4() {
|
|
|
243
200
|
return (c === "x" ? r : r & 3 | 8).toString(16);
|
|
244
201
|
});
|
|
245
202
|
}
|
|
203
|
+
const NOTIFICATION_COOKIE = "cpk_inspector_notifications_v1";
|
|
204
|
+
const NOTIFICATION_STORAGE = "cpk:inspector:notifications:v1";
|
|
205
|
+
const NOTIFICATION_COOKIE_MAX_LENGTH = 1024;
|
|
206
|
+
const LEGACY_ANNOUNCEMENT_ID = "16f7d877-49e3-41c3-9ca6-f951d3d8ba80";
|
|
207
|
+
/** Read host-wide acknowledgements from either the new compact cookie or an old full-state cookie. */
|
|
208
|
+
function readNotificationAcknowledgements() {
|
|
209
|
+
try {
|
|
210
|
+
const raw = readCookie(NOTIFICATION_COOKIE);
|
|
211
|
+
if (raw) {
|
|
212
|
+
const value = JSON.parse(raw);
|
|
213
|
+
if (value && typeof value === "object") {
|
|
214
|
+
const state = require_notifications.parseNotificationState({
|
|
215
|
+
...require_notifications.emptyNotificationState(),
|
|
216
|
+
...value
|
|
217
|
+
});
|
|
218
|
+
if (state) return {
|
|
219
|
+
readIds: state.readIds,
|
|
220
|
+
suppressedIds: state.suppressedIds
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
} catch {}
|
|
225
|
+
return {
|
|
226
|
+
readIds: [],
|
|
227
|
+
suppressedIds: []
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
/** Load per-origin selection and merge host-wide read and suppressed notices. */
|
|
231
|
+
function loadNotificationState() {
|
|
232
|
+
const raw = readLocalStorageItem(NOTIFICATION_STORAGE);
|
|
233
|
+
let localState = require_notifications.emptyNotificationState();
|
|
234
|
+
if (raw) try {
|
|
235
|
+
localState = require_notifications.parseNotificationState(JSON.parse(raw)) ?? localState;
|
|
236
|
+
} catch {}
|
|
237
|
+
const host = readNotificationAcknowledgements();
|
|
238
|
+
return {
|
|
239
|
+
...localState,
|
|
240
|
+
readIds: [...new Set([...localState.readIds, ...host.readIds])],
|
|
241
|
+
suppressedIds: [...new Set([...localState.suppressedIds, ...host.suppressedIds])]
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
/** Preserve a legacy acknowledgement only for the known announcement entering the new feed. */
|
|
245
|
+
function migrateAnnouncementReadState(state, feed) {
|
|
246
|
+
for (const raw of [readCookie("cpk_inspector_announcements"), readLocalStorageItem("cpk:inspector:announcement_read")]) {
|
|
247
|
+
if (!raw) continue;
|
|
248
|
+
try {
|
|
249
|
+
const value = JSON.parse(raw);
|
|
250
|
+
if (!value || typeof value !== "object" || !("timestamp" in value)) continue;
|
|
251
|
+
const legacyNotice = feed.notifications.find((notice) => notice.id === LEGACY_ANNOUNCEMENT_ID && notice.publishedAt === value.timestamp);
|
|
252
|
+
if (!legacyNotice) continue;
|
|
253
|
+
const migrated = {
|
|
254
|
+
...state,
|
|
255
|
+
readIds: [...new Set([...state.readIds, legacyNotice.id])],
|
|
256
|
+
suppressedIds: [...new Set([...state.suppressedIds, legacyNotice.id])]
|
|
257
|
+
};
|
|
258
|
+
saveNotificationState(migrated);
|
|
259
|
+
writeCookie("cpk_inspector_announcements", "", "Max-Age=0");
|
|
260
|
+
removeLocalStorageItem("cpk:inspector:announcement_read");
|
|
261
|
+
return migrated;
|
|
262
|
+
} catch {}
|
|
263
|
+
}
|
|
264
|
+
return state;
|
|
265
|
+
}
|
|
266
|
+
/** Save full state per origin and a bounded host-wide acknowledgement cookie. */
|
|
267
|
+
function saveNotificationState(state) {
|
|
268
|
+
const host = readNotificationAcknowledgements();
|
|
269
|
+
const merged = {
|
|
270
|
+
...state,
|
|
271
|
+
readIds: [...new Set([...state.readIds, ...host.readIds])],
|
|
272
|
+
suppressedIds: [...new Set([...state.suppressedIds, ...host.suppressedIds])]
|
|
273
|
+
};
|
|
274
|
+
writeLocalStorageItem(NOTIFICATION_STORAGE, JSON.stringify(merged));
|
|
275
|
+
const cookieState = {
|
|
276
|
+
schemaVersion: 1,
|
|
277
|
+
readIds: [...merged.readIds],
|
|
278
|
+
suppressedIds: [...merged.suppressedIds]
|
|
279
|
+
};
|
|
280
|
+
let raw = JSON.stringify(cookieState);
|
|
281
|
+
while (encodeURIComponent(raw).length >= NOTIFICATION_COOKIE_MAX_LENGTH) {
|
|
282
|
+
if (cookieState.readIds.length >= cookieState.suppressedIds.length) cookieState.readIds.shift();
|
|
283
|
+
else cookieState.suppressedIds.shift();
|
|
284
|
+
raw = JSON.stringify(cookieState);
|
|
285
|
+
}
|
|
286
|
+
writeCookie(NOTIFICATION_COOKIE, raw, "Max-Age=31536000");
|
|
287
|
+
}
|
|
288
|
+
/** ID-based pulse state is separate from the legacy announcement timestamp. */
|
|
289
|
+
const NOTIFICATION_PULSED_SESSION_KEY = "cpk:inspector:notification-pulsed-id";
|
|
290
|
+
const MAX_PULSED_NOTIFICATION_IDS = 100;
|
|
291
|
+
function pulsedNotificationIds(raw) {
|
|
292
|
+
if (!raw) return [];
|
|
293
|
+
if (!raw.startsWith("[")) return [raw];
|
|
294
|
+
try {
|
|
295
|
+
const ids = JSON.parse(raw);
|
|
296
|
+
return Array.isArray(ids) ? ids.filter((id) => typeof id === "string") : [];
|
|
297
|
+
} catch {
|
|
298
|
+
return [];
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
function saveNotificationPulsedId(id) {
|
|
302
|
+
if (typeof window === "undefined") return;
|
|
303
|
+
try {
|
|
304
|
+
const ids = pulsedNotificationIds(window.sessionStorage.getItem(NOTIFICATION_PULSED_SESSION_KEY));
|
|
305
|
+
if (!ids.includes(id)) ids.push(id);
|
|
306
|
+
window.sessionStorage.setItem(NOTIFICATION_PULSED_SESSION_KEY, JSON.stringify(ids.slice(-MAX_PULSED_NOTIFICATION_IDS)));
|
|
307
|
+
} catch {}
|
|
308
|
+
}
|
|
309
|
+
function hasNotificationPulsed(id, publishedAt) {
|
|
310
|
+
if (typeof window === "undefined") return false;
|
|
311
|
+
try {
|
|
312
|
+
const current = window.sessionStorage.getItem(NOTIFICATION_PULSED_SESSION_KEY);
|
|
313
|
+
if (current !== null) return pulsedNotificationIds(current).includes(id);
|
|
314
|
+
if (loadAnnouncementPulsedTimestamp() === publishedAt) {
|
|
315
|
+
saveNotificationPulsedId(id);
|
|
316
|
+
return true;
|
|
317
|
+
}
|
|
318
|
+
} catch {}
|
|
319
|
+
return false;
|
|
320
|
+
}
|
|
246
321
|
|
|
247
322
|
//#endregion
|
|
248
323
|
exports.INSPECTOR_DISMISSAL_MAX_DURATION_MS = INSPECTOR_DISMISSAL_MAX_DURATION_MS;
|
|
249
324
|
exports.clearLegacyAnnouncementReadState = clearLegacyAnnouncementReadState;
|
|
250
325
|
exports.getOrCreateTelemetryDistinctId = getOrCreateTelemetryDistinctId;
|
|
326
|
+
exports.hasNotificationPulsed = hasNotificationPulsed;
|
|
251
327
|
exports.hasTelemetryDisclosureBeenShown = hasTelemetryDisclosureBeenShown;
|
|
252
328
|
exports.isTelemetryOptedOut = isTelemetryOptedOut;
|
|
253
329
|
exports.isValidAnchor = isValidAnchor;
|
|
254
330
|
exports.isValidDockMode = isValidDockMode;
|
|
255
331
|
exports.isValidPosition = isValidPosition;
|
|
256
332
|
exports.isValidSize = isValidSize;
|
|
257
|
-
exports.loadAnnouncementPulsedTimestamp = loadAnnouncementPulsedTimestamp;
|
|
258
|
-
exports.loadAnnouncementReadTimestamp = loadAnnouncementReadTimestamp;
|
|
259
333
|
exports.loadInspectorDismissedUntil = loadInspectorDismissedUntil;
|
|
260
334
|
exports.loadInspectorState = loadInspectorState;
|
|
335
|
+
exports.loadNotificationState = loadNotificationState;
|
|
261
336
|
exports.markTelemetryDisclosureShown = markTelemetryDisclosureShown;
|
|
262
|
-
exports.
|
|
263
|
-
exports.saveAnnouncementReadTimestamp = saveAnnouncementReadTimestamp;
|
|
337
|
+
exports.migrateAnnouncementReadState = migrateAnnouncementReadState;
|
|
264
338
|
exports.saveInspectorDismissedUntil = saveInspectorDismissedUntil;
|
|
265
339
|
exports.saveInspectorState = saveInspectorState;
|
|
340
|
+
exports.saveNotificationPulsedId = saveNotificationPulsedId;
|
|
341
|
+
exports.saveNotificationState = saveNotificationState;
|
|
266
342
|
//# sourceMappingURL=persistence.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"persistence.cjs","names":[],"sources":["../../src/lib/persistence.ts"],"sourcesContent":["import type { Anchor, DockMode, Position, Size } from \"./types.js\";\n\nexport type PersistedContextState = {\n anchor?: Anchor;\n anchorOffset?: Position;\n size?: Size;\n hasCustomPosition?: boolean;\n};\n\nexport type PersistedState = {\n button?: Omit<PersistedContextState, \"size\">;\n window?: PersistedContextState;\n isOpen?: boolean;\n dockMode?: DockMode;\n selectedMenu?: string;\n selectedContext?: string;\n hasOpenedInspector?: boolean;\n sidebarCollapsed?: boolean;\n /** @deprecated Replaced by colorSchemePreference to distinguish a user choice from the old light default. */\n colorScheme?: \"light\" | \"dark\";\n colorSchemePreference?: \"light\" | \"dark\";\n};\n\nexport const HOME_NEWS_READ_STORAGE_KEY = \"cpk:inspector:home-news-read\";\n\n/** Return story ids the user has already opened or hovered on Home. */\nexport function loadHomeNewsReadIds(storageKey: string): string[] {\n if (typeof window === \"undefined\") {\n return [];\n }\n\n try {\n const raw = window.localStorage.getItem(storageKey);\n if (!raw) {\n return [];\n }\n const parsed = JSON.parse(raw);\n if (!Array.isArray(parsed)) {\n return [];\n }\n return parsed.filter((id): id is string => typeof id === \"string\");\n } catch {\n return [];\n }\n}\n\n/** Persist Home news story ids that the user has already seen. */\nexport function saveHomeNewsReadIds(storageKey: string, ids: string[]): void {\n if (typeof window === \"undefined\") {\n return;\n }\n\n try {\n window.localStorage.setItem(storageKey, JSON.stringify(ids));\n } catch (error) {\n console.warn(\"Failed to persist Home news read state\", error);\n }\n}\n\nexport function loadInspectorState(storageKey: string): PersistedState | null {\n if (typeof window === \"undefined\") {\n return null;\n }\n\n const raw = window.localStorage.getItem(storageKey);\n if (raw) {\n try {\n const parsed = JSON.parse(raw);\n if (parsed && typeof parsed === \"object\") {\n return parsed as PersistedState;\n }\n } catch {\n // Fall through to cookie migration path\n }\n }\n\n // Backwards compatibility: try to read the legacy cookie and migrate it\n if (typeof document !== \"undefined\") {\n const prefix = `${storageKey}=`;\n const entry = document.cookie\n .split(\"; \")\n .find((cookie) => cookie.startsWith(prefix));\n if (entry) {\n const legacyRaw = entry.substring(prefix.length);\n try {\n const parsed = JSON.parse(decodeURIComponent(legacyRaw));\n if (parsed && typeof parsed === \"object\") {\n return parsed as PersistedState;\n }\n } catch {\n return null;\n }\n }\n }\n\n return null;\n}\n\nexport function saveInspectorState(\n storageKey: string,\n state: PersistedState,\n): void {\n if (typeof window === \"undefined\") {\n return;\n }\n\n try {\n window.localStorage.setItem(storageKey, JSON.stringify(state));\n } catch (error) {\n console.warn(\"Failed to persist inspector state\", error);\n }\n}\n\nexport function isValidAnchor(value: unknown): value is Anchor {\n if (!value || typeof value !== \"object\") {\n return false;\n }\n\n const candidate = value as Anchor;\n return (\n (candidate.horizontal === \"left\" || candidate.horizontal === \"right\") &&\n (candidate.vertical === \"top\" || candidate.vertical === \"bottom\")\n );\n}\n\nexport function isValidPosition(value: unknown): value is Position {\n if (!value || typeof value !== \"object\") {\n return false;\n }\n\n const candidate = value as Position;\n return isFiniteNumber(candidate.x) && isFiniteNumber(candidate.y);\n}\n\nexport function isValidSize(value: unknown): value is Size {\n if (!value || typeof value !== \"object\") {\n return false;\n }\n\n const candidate = value as Size;\n return isFiniteNumber(candidate.width) && isFiniteNumber(candidate.height);\n}\n\nexport function isFiniteNumber(value: unknown): value is number {\n return typeof value === \"number\" && Number.isFinite(value);\n}\n\nexport function isValidDockMode(value: unknown): value is DockMode {\n return value === \"floating\" || value === \"docked-left\";\n}\n\n// Inspector dismissal is intentionally host-scoped rather than origin-scoped.\n// A developer who closes the Inspector on localhost:3000 should not see it\n// again on localhost:5173 during the same dismissal window. Cookies are shared\n// across ports on one host; localStorage is not.\nexport const INSPECTOR_DISMISSAL_MIRROR_KEY = \"cpk:inspector:dismissed_until\";\nexport const INSPECTOR_DISMISSAL_COOKIE_NAME = \"cpk_inspector_dismissed_until\";\nexport const INSPECTOR_DISMISSAL_MAX_DURATION_MS = 7 * 24 * 60 * 60 * 1000;\n\ntype InspectorDismissalPayload = Readonly<{ until: number }>;\n\n/** Return the active host-scoped Inspector dismissal deadline, if any. */\nexport function loadInspectorDismissedUntil(\n now: number = Date.now(),\n): number | null {\n const until =\n parseInspectorDismissalPayload(\n readCookie(INSPECTOR_DISMISSAL_COOKIE_NAME),\n ) ??\n parseInspectorDismissalPayload(\n readLocalStorageItem(INSPECTOR_DISMISSAL_MIRROR_KEY),\n );\n\n if (until === null) return null;\n if (until <= now) {\n clearInspectorDismissal();\n return null;\n }\n const maximumUntil = now + INSPECTOR_DISMISSAL_MAX_DURATION_MS;\n if (until > maximumUntil) {\n saveInspectorDismissedUntil(maximumUntil, now);\n return maximumUntil;\n }\n return until;\n}\n\n/** Persist a dismissal across browser sessions and localhost ports. */\nexport function saveInspectorDismissedUntil(\n until: number,\n now: number = Date.now(),\n): void {\n if (!Number.isFinite(until) || until <= now) {\n clearInspectorDismissal();\n return;\n }\n\n const boundedUntil = Math.min(\n until,\n now + INSPECTOR_DISMISSAL_MAX_DURATION_MS,\n );\n const payload = JSON.stringify({\n until: boundedUntil,\n } satisfies InspectorDismissalPayload);\n const maxAgeSeconds = Math.max(1, Math.ceil((boundedUntil - now) / 1000));\n writeCookie(\n INSPECTOR_DISMISSAL_COOKIE_NAME,\n payload,\n `Max-Age=${maxAgeSeconds}`,\n );\n writeLocalStorageItem(INSPECTOR_DISMISSAL_MIRROR_KEY, payload);\n}\n\n/** Clear both persistence layers, including an expired host cookie. */\nexport function clearInspectorDismissal(): void {\n writeCookie(INSPECTOR_DISMISSAL_COOKIE_NAME, \"\", \"Max-Age=0\");\n removeLocalStorageItem(INSPECTOR_DISMISSAL_MIRROR_KEY);\n}\n\n/** Parse a finite deadline from either persistence layer. */\nfunction parseInspectorDismissalPayload(raw: string | null): number | null {\n if (!raw) return null;\n try {\n const parsed = JSON.parse(raw) as Partial<InspectorDismissalPayload>;\n return typeof parsed.until === \"number\" && Number.isFinite(parsed.until)\n ? parsed.until\n : null;\n } catch {\n return null;\n }\n}\n\n// Announcement read state — \"have I read this announcement\" is a property of\n// the person, not of the project, so it must survive a change of localhost\n// port. localStorage cannot express that: it is partitioned by origin and\n// origin includes the port, so :3000 and :5173 are separate stores. Cookies\n// are partitioned by host, so a cookie set on `localhost` without a `domain`\n// attribute is shared by every port on that host.\n//\n// Underscores, not colons: `:` is a separator in RFC 6265 and is not valid in\n// a cookie name. Browsers are lenient about it; we don't rely on that.\nconst ANNOUNCEMENT_READ_COOKIE_NAME = \"cpk_inspector_announcements\";\n\n// localStorage mirror of the cookie, so a browser that blocks cookies\n// degrades to per-port behaviour instead of losing the read state entirely.\n// A NEW key on purpose — the legacy one is abandoned, not migrated.\nconst ANNOUNCEMENT_READ_MIRROR_KEY = \"cpk:inspector:announcement_read\";\n\n// The superseded key. Every existing user is re-armed exactly once so they\n// discover the surface that replaced the announcement bubble, and the key is\n// deleted rather than left in place so nothing can fall back to it later.\nconst LEGACY_ANNOUNCEMENT_READ_KEY = \"cpk:inspector:announcements\";\n\n// Pulse suppression is per browser tab, and stores the announcement timestamp\n// rather than a boolean: a boolean would swallow a newly published\n// announcement for the rest of that tab's life, and the feed is fetched once\n// per mount with no polling.\nconst ANNOUNCEMENT_PULSED_SESSION_KEY = \"cpk:inspector:pulsed\";\n\n// Roughly one year. No `Secure` — local development is served over plain\n// HTTP — and no `HttpOnly`, because the component reads the value from\n// script. `Path=/` and `SameSite=Lax` keep it host-wide and same-site only.\nconst ANNOUNCEMENT_READ_COOKIE_MAX_AGE_SECONDS = 60 * 60 * 24 * 365;\n\n/**\n * The announcement timestamp the user has already read, or `null` when\n * nothing has been read yet. Prefers the host-scoped cookie and falls back to\n * the origin-scoped mirror, so cookie-blocking browsers still remember the\n * read within the port they are on.\n */\nexport function loadAnnouncementReadTimestamp(): string | null {\n return (\n parseTimestampPayload(readAnnouncementCookie()) ??\n parseTimestampPayload(readLocalStorageItem(ANNOUNCEMENT_READ_MIRROR_KEY))\n );\n}\n\n/**\n * Records an announcement as read in both the host-scoped cookie and the\n * origin-scoped mirror. The stored value is `{\"timestamp\":\"…\"}` — the same\n * shape the announcement state has always used.\n */\nexport function saveAnnouncementReadTimestamp(timestamp: string): void {\n const payload = JSON.stringify({ timestamp });\n writeAnnouncementCookie(payload);\n writeLocalStorageItem(ANNOUNCEMENT_READ_MIRROR_KEY, payload);\n}\n\n/**\n * Deletes the superseded origin-scoped read state. Safe to call on every\n * startup: nothing writes that key any more, so the value cannot come back.\n */\nexport function clearLegacyAnnouncementReadState(): void {\n removeLocalStorageItem(LEGACY_ANNOUNCEMENT_READ_KEY);\n}\n\n/**\n * The announcement timestamp this browser tab has already pulsed for, or\n * `null` when it hasn't pulsed yet.\n */\nexport function loadAnnouncementPulsedTimestamp(): string | null {\n if (typeof window === \"undefined\") return null;\n try {\n return window.sessionStorage.getItem(ANNOUNCEMENT_PULSED_SESSION_KEY);\n } catch {\n return null;\n }\n}\n\n/** Suppresses further pulses for this announcement in this browser tab. */\nexport function saveAnnouncementPulsedTimestamp(timestamp: string): void {\n if (typeof window === \"undefined\") return;\n try {\n window.sessionStorage.setItem(ANNOUNCEMENT_PULSED_SESSION_KEY, timestamp);\n } catch {\n // No-op — a lost suppression costs one extra pulse, never correctness.\n }\n}\n\nfunction parseTimestampPayload(raw: string | null): string | null {\n if (!raw) return null;\n try {\n const parsed = JSON.parse(raw) as { timestamp?: unknown };\n return typeof parsed?.timestamp === \"string\" ? parsed.timestamp : null;\n } catch {\n return null;\n }\n}\n\nfunction readAnnouncementCookie(): string | null {\n return readCookie(ANNOUNCEMENT_READ_COOKIE_NAME);\n}\n\nfunction writeAnnouncementCookie(value: string): void {\n writeCookie(\n ANNOUNCEMENT_READ_COOKIE_NAME,\n value,\n `Max-Age=${ANNOUNCEMENT_READ_COOKIE_MAX_AGE_SECONDS}`,\n );\n}\n\nfunction readCookie(name: string): string | null {\n if (typeof document === \"undefined\") return null;\n try {\n for (const entry of document.cookie.split(\";\")) {\n const separator = entry.indexOf(\"=\");\n if (separator === -1) continue;\n if (entry.slice(0, separator).trim() !== name) continue;\n return decodeURIComponent(entry.slice(separator + 1).trim());\n }\n } catch {\n // Cookie access throws in sandboxed documents. Callers fall back to their\n // origin-scoped mirror rather than breaking the host app.\n }\n return null;\n}\n\nfunction writeCookie(name: string, value: string, lifetime: string): void {\n if (typeof document === \"undefined\") return;\n try {\n // No Domain attribute: the cookie belongs to exactly this host, while\n // remaining shared by every port on that host.\n document.cookie = `${name}=${encodeURIComponent(\n value,\n )}; Path=/; ${lifetime}; SameSite=Lax`;\n } catch {\n // The localStorage mirror gives cookie-blocking browsers per-origin\n // persistence, and neither storage layer may break the host app.\n }\n}\n\nfunction readLocalStorageItem(key: string): string | null {\n if (typeof window === \"undefined\") return null;\n try {\n return window.localStorage.getItem(key);\n } catch {\n return null;\n }\n}\n\nfunction writeLocalStorageItem(key: string, value: string): void {\n if (typeof window === \"undefined\") return;\n try {\n window.localStorage.setItem(key, value);\n } catch {\n // No-op — see getOrCreateTelemetryDistinctId.\n }\n}\n\nfunction removeLocalStorageItem(key: string): void {\n if (typeof window === \"undefined\") return;\n try {\n window.localStorage.removeItem(key);\n } catch {\n // No-op.\n }\n}\n\n// Telemetry persistence — flat per-key localStorage rather than the\n// JSON-blob shape used for window/dock state, because each value is\n// independent and we want to read/write them without round-tripping\n// the whole inspector state object.\nconst TELEMETRY_DISTINCT_ID_KEY = \"cpk:inspector:telemetry:distinct_id\";\nconst TELEMETRY_OPT_OUT_KEY = \"cpk:inspector:telemetry:opt_out\";\nconst TELEMETRY_DISCLOSURE_SHOWN_KEY =\n \"cpk:inspector:telemetry:disclosure_shown\";\n\n// Module-level fallback for when localStorage is unavailable (private mode,\n// quota exceeded, etc.). Cached so that whats_new_viewed and\n// whats_new_clicked from the same page-load share one distinct_id even\n// without persistent storage —\n// funnel coherence within a session is preserved even when storage fails.\nlet inMemoryFallbackId: string | null = null;\n\nexport function getOrCreateTelemetryDistinctId(): string {\n if (typeof window === \"undefined\") {\n // SSR / test fallback. A non-persistent ID is preferable to throwing\n // because telemetry must never break the host application.\n return generateUuidV4();\n }\n\n try {\n const existing = window.localStorage.getItem(TELEMETRY_DISTINCT_ID_KEY);\n if (existing && existing.length > 0) return existing;\n const fresh = generateUuidV4();\n window.localStorage.setItem(TELEMETRY_DISTINCT_ID_KEY, fresh);\n return fresh;\n } catch {\n return (inMemoryFallbackId ??= generateUuidV4());\n }\n}\n\n// Test-only reset so the in-memory fallback doesn't leak between test cases.\nexport function _resetTelemetryPersistenceForTesting(): void {\n inMemoryFallbackId = null;\n}\n\nexport function isTelemetryOptedOut(): boolean {\n if (typeof window === \"undefined\") return false;\n try {\n return window.localStorage.getItem(TELEMETRY_OPT_OUT_KEY) === \"true\";\n } catch {\n return false;\n }\n}\n\nexport function setTelemetryOptOut(optedOut: boolean): void {\n if (typeof window === \"undefined\") return;\n try {\n if (optedOut) {\n window.localStorage.setItem(TELEMETRY_OPT_OUT_KEY, \"true\");\n } else {\n window.localStorage.removeItem(TELEMETRY_OPT_OUT_KEY);\n }\n } catch {\n // No-op — see getOrCreateTelemetryDistinctId.\n }\n}\n\nexport function hasTelemetryDisclosureBeenShown(): boolean {\n if (typeof window === \"undefined\") return false;\n try {\n return (\n window.localStorage.getItem(TELEMETRY_DISCLOSURE_SHOWN_KEY) === \"true\"\n );\n } catch {\n return false;\n }\n}\n\nexport function markTelemetryDisclosureShown(): void {\n if (typeof window === \"undefined\") return;\n try {\n window.localStorage.setItem(TELEMETRY_DISCLOSURE_SHOWN_KEY, \"true\");\n } catch {\n // No-op.\n }\n}\n\nfunction generateUuidV4(): string {\n if (\n typeof globalThis.crypto !== \"undefined\" &&\n typeof globalThis.crypto.randomUUID === \"function\"\n ) {\n return globalThis.crypto.randomUUID();\n }\n // Fallback for environments without crypto.randomUUID (older browsers,\n // some test runners). Not cryptographically strong; acceptable because\n // the value is just an anonymous correlation ID.\n return \"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx\".replace(/[xy]/g, (c) => {\n const r = (Math.random() * 16) | 0;\n const v = c === \"x\" ? r : (r & 0x3) | 0x8;\n return v.toString(16);\n });\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"persistence.cjs","names":["parseNotificationState","emptyNotificationState"],"sources":["../../src/lib/persistence.ts"],"sourcesContent":["import {\n emptyNotificationState,\n parseNotificationState,\n} from \"./notifications.js\";\nimport type { NotificationState, NotificationFeed } from \"./notifications.js\";\nimport type { Anchor, DockMode, Position, Size } from \"./types.js\";\n\nexport type PersistedContextState = {\n anchor?: Anchor;\n anchorOffset?: Position;\n size?: Size;\n hasCustomPosition?: boolean;\n};\n\nexport type PersistedState = {\n button?: Omit<PersistedContextState, \"size\">;\n window?: PersistedContextState;\n isOpen?: boolean;\n dockMode?: DockMode;\n selectedMenu?: string;\n selectedContext?: string;\n hasOpenedInspector?: boolean;\n sidebarCollapsed?: boolean;\n /** @deprecated Replaced by colorSchemePreference to distinguish a user choice from the old light default. */\n colorScheme?: \"light\" | \"dark\";\n colorSchemePreference?: \"light\" | \"dark\";\n};\n\nexport const HOME_NEWS_READ_STORAGE_KEY = \"cpk:inspector:home-news-read\";\n\n/** Return story ids the user has already opened or hovered on Home. */\nexport function loadHomeNewsReadIds(storageKey: string): string[] {\n if (typeof window === \"undefined\") {\n return [];\n }\n\n try {\n const raw = window.localStorage.getItem(storageKey);\n if (!raw) {\n return [];\n }\n const parsed = JSON.parse(raw);\n if (!Array.isArray(parsed)) {\n return [];\n }\n return parsed.filter((id): id is string => typeof id === \"string\");\n } catch {\n return [];\n }\n}\n\n/** Persist Home news story ids that the user has already seen. */\nexport function saveHomeNewsReadIds(storageKey: string, ids: string[]): void {\n if (typeof window === \"undefined\") {\n return;\n }\n\n try {\n window.localStorage.setItem(storageKey, JSON.stringify(ids));\n } catch (error) {\n console.warn(\"Failed to persist Home news read state\", error);\n }\n}\n\nexport function loadInspectorState(storageKey: string): PersistedState | null {\n if (typeof window === \"undefined\") {\n return null;\n }\n\n const raw = window.localStorage.getItem(storageKey);\n if (raw) {\n try {\n const parsed = JSON.parse(raw);\n if (parsed && typeof parsed === \"object\") {\n return parsed as PersistedState;\n }\n } catch {\n // Fall through to cookie migration path\n }\n }\n\n // Backwards compatibility: try to read the legacy cookie and migrate it\n if (typeof document !== \"undefined\") {\n const prefix = `${storageKey}=`;\n const entry = document.cookie\n .split(\"; \")\n .find((cookie) => cookie.startsWith(prefix));\n if (entry) {\n const legacyRaw = entry.substring(prefix.length);\n try {\n const parsed = JSON.parse(decodeURIComponent(legacyRaw));\n if (parsed && typeof parsed === \"object\") {\n return parsed as PersistedState;\n }\n } catch {\n return null;\n }\n }\n }\n\n return null;\n}\n\nexport function saveInspectorState(\n storageKey: string,\n state: PersistedState,\n): void {\n if (typeof window === \"undefined\") {\n return;\n }\n\n try {\n window.localStorage.setItem(storageKey, JSON.stringify(state));\n } catch (error) {\n console.warn(\"Failed to persist inspector state\", error);\n }\n}\n\nexport function isValidAnchor(value: unknown): value is Anchor {\n if (!value || typeof value !== \"object\") {\n return false;\n }\n\n const candidate = value as Anchor;\n return (\n (candidate.horizontal === \"left\" || candidate.horizontal === \"right\") &&\n (candidate.vertical === \"top\" || candidate.vertical === \"bottom\")\n );\n}\n\nexport function isValidPosition(value: unknown): value is Position {\n if (!value || typeof value !== \"object\") {\n return false;\n }\n\n const candidate = value as Position;\n return isFiniteNumber(candidate.x) && isFiniteNumber(candidate.y);\n}\n\nexport function isValidSize(value: unknown): value is Size {\n if (!value || typeof value !== \"object\") {\n return false;\n }\n\n const candidate = value as Size;\n return isFiniteNumber(candidate.width) && isFiniteNumber(candidate.height);\n}\n\nexport function isFiniteNumber(value: unknown): value is number {\n return typeof value === \"number\" && Number.isFinite(value);\n}\n\nexport function isValidDockMode(value: unknown): value is DockMode {\n return value === \"floating\" || value === \"docked-left\";\n}\n\n// Inspector dismissal is intentionally host-scoped rather than origin-scoped.\n// A developer who closes the Inspector on localhost:3000 should not see it\n// again on localhost:5173 during the same dismissal window. Cookies are shared\n// across ports on one host; localStorage is not.\nexport const INSPECTOR_DISMISSAL_MIRROR_KEY = \"cpk:inspector:dismissed_until\";\nexport const INSPECTOR_DISMISSAL_COOKIE_NAME = \"cpk_inspector_dismissed_until\";\nexport const INSPECTOR_DISMISSAL_MAX_DURATION_MS = 7 * 24 * 60 * 60 * 1000;\n\ntype InspectorDismissalPayload = Readonly<{ until: number }>;\n\n/** Return the active host-scoped Inspector dismissal deadline, if any. */\nexport function loadInspectorDismissedUntil(\n now: number = Date.now(),\n): number | null {\n const until =\n parseInspectorDismissalPayload(\n readCookie(INSPECTOR_DISMISSAL_COOKIE_NAME),\n ) ??\n parseInspectorDismissalPayload(\n readLocalStorageItem(INSPECTOR_DISMISSAL_MIRROR_KEY),\n );\n\n if (until === null) return null;\n if (until <= now) {\n clearInspectorDismissal();\n return null;\n }\n const maximumUntil = now + INSPECTOR_DISMISSAL_MAX_DURATION_MS;\n if (until > maximumUntil) {\n saveInspectorDismissedUntil(maximumUntil, now);\n return maximumUntil;\n }\n return until;\n}\n\n/** Persist a dismissal across browser sessions and localhost ports. */\nexport function saveInspectorDismissedUntil(\n until: number,\n now: number = Date.now(),\n): void {\n if (!Number.isFinite(until) || until <= now) {\n clearInspectorDismissal();\n return;\n }\n\n const boundedUntil = Math.min(\n until,\n now + INSPECTOR_DISMISSAL_MAX_DURATION_MS,\n );\n const payload = JSON.stringify({\n until: boundedUntil,\n } satisfies InspectorDismissalPayload);\n const maxAgeSeconds = Math.max(1, Math.ceil((boundedUntil - now) / 1000));\n writeCookie(\n INSPECTOR_DISMISSAL_COOKIE_NAME,\n payload,\n `Max-Age=${maxAgeSeconds}`,\n );\n writeLocalStorageItem(INSPECTOR_DISMISSAL_MIRROR_KEY, payload);\n}\n\n/** Clear both persistence layers, including an expired host cookie. */\nexport function clearInspectorDismissal(): void {\n writeCookie(INSPECTOR_DISMISSAL_COOKIE_NAME, \"\", \"Max-Age=0\");\n removeLocalStorageItem(INSPECTOR_DISMISSAL_MIRROR_KEY);\n}\n\n/** Parse a finite deadline from either persistence layer. */\nfunction parseInspectorDismissalPayload(raw: string | null): number | null {\n if (!raw) return null;\n try {\n const parsed = JSON.parse(raw) as Partial<InspectorDismissalPayload>;\n return typeof parsed.until === \"number\" && Number.isFinite(parsed.until)\n ? parsed.until\n : null;\n } catch {\n return null;\n }\n}\n\n// Obsolete pre-cookie state; the cookie and its mirror are migrated below.\nconst LEGACY_ANNOUNCEMENT_READ_KEY = \"cpk:inspector:announcements\";\n\n// Pulse suppression is per browser tab, and stores the announcement timestamp\n// rather than a boolean: a boolean would swallow a newly published\n// announcement for the rest of that tab's life, and the feed is fetched once\n// per mount with no polling.\nconst ANNOUNCEMENT_PULSED_SESSION_KEY = \"cpk:inspector:pulsed\";\n\n/**\n * Deletes the superseded origin-scoped read state. Safe to call on every\n * startup: nothing writes that key any more, so the value cannot come back.\n */\nexport function clearLegacyAnnouncementReadState(): void {\n removeLocalStorageItem(LEGACY_ANNOUNCEMENT_READ_KEY);\n}\n\n/**\n * The announcement timestamp this browser tab has already pulsed for, or\n * `null` when it hasn't pulsed yet.\n */\nexport function loadAnnouncementPulsedTimestamp(): string | null {\n if (typeof window === \"undefined\") return null;\n try {\n return window.sessionStorage.getItem(ANNOUNCEMENT_PULSED_SESSION_KEY);\n } catch {\n return null;\n }\n}\n\nfunction readCookie(name: string): string | null {\n if (typeof document === \"undefined\") return null;\n try {\n for (const entry of document.cookie.split(\";\")) {\n const separator = entry.indexOf(\"=\");\n if (separator === -1) continue;\n if (entry.slice(0, separator).trim() !== name) continue;\n return decodeURIComponent(entry.slice(separator + 1).trim());\n }\n } catch {\n // Cookie access throws in sandboxed documents. Callers fall back to their\n // origin-scoped mirror rather than breaking the host app.\n }\n return null;\n}\n\nfunction writeCookie(name: string, value: string, lifetime: string): void {\n if (typeof document === \"undefined\") return;\n try {\n // No Domain attribute: the cookie belongs to exactly this host, while\n // remaining shared by every port on that host.\n document.cookie = `${name}=${encodeURIComponent(\n value,\n )}; Path=/; ${lifetime}; SameSite=Lax`;\n } catch {\n // The localStorage mirror gives cookie-blocking browsers per-origin\n // persistence, and neither storage layer may break the host app.\n }\n}\n\nfunction readLocalStorageItem(key: string): string | null {\n if (typeof window === \"undefined\") return null;\n try {\n return window.localStorage.getItem(key);\n } catch {\n return null;\n }\n}\n\nfunction writeLocalStorageItem(key: string, value: string): void {\n if (typeof window === \"undefined\") return;\n try {\n window.localStorage.setItem(key, value);\n } catch {\n // No-op — see getOrCreateTelemetryDistinctId.\n }\n}\n\nfunction removeLocalStorageItem(key: string): void {\n if (typeof window === \"undefined\") return;\n try {\n window.localStorage.removeItem(key);\n } catch {\n // No-op.\n }\n}\n\n// Telemetry persistence — flat per-key localStorage rather than the\n// JSON-blob shape used for window/dock state, because each value is\n// independent and we want to read/write them without round-tripping\n// the whole inspector state object.\nconst TELEMETRY_DISTINCT_ID_KEY = \"cpk:inspector:telemetry:distinct_id\";\nconst TELEMETRY_OPT_OUT_KEY = \"cpk:inspector:telemetry:opt_out\";\nconst TELEMETRY_DISCLOSURE_SHOWN_KEY =\n \"cpk:inspector:telemetry:disclosure_shown\";\n\n// Module-level fallback for when localStorage is unavailable (private mode,\n// quota exceeded, etc.). Cached so that whats_new_viewed and\n// whats_new_clicked from the same page-load share one distinct_id even\n// without persistent storage —\n// funnel coherence within a session is preserved even when storage fails.\nlet inMemoryFallbackId: string | null = null;\n\nexport function getOrCreateTelemetryDistinctId(): string {\n if (typeof window === \"undefined\") {\n // SSR / test fallback. A non-persistent ID is preferable to throwing\n // because telemetry must never break the host application.\n return generateUuidV4();\n }\n\n try {\n const existing = window.localStorage.getItem(TELEMETRY_DISTINCT_ID_KEY);\n if (existing && existing.length > 0) return existing;\n const fresh = generateUuidV4();\n window.localStorage.setItem(TELEMETRY_DISTINCT_ID_KEY, fresh);\n return fresh;\n } catch {\n return (inMemoryFallbackId ??= generateUuidV4());\n }\n}\n\n// Test-only reset so the in-memory fallback doesn't leak between test cases.\nexport function _resetTelemetryPersistenceForTesting(): void {\n inMemoryFallbackId = null;\n}\n\nexport function isTelemetryOptedOut(): boolean {\n if (typeof window === \"undefined\") return false;\n try {\n return window.localStorage.getItem(TELEMETRY_OPT_OUT_KEY) === \"true\";\n } catch {\n return false;\n }\n}\n\nexport function setTelemetryOptOut(optedOut: boolean): void {\n if (typeof window === \"undefined\") return;\n try {\n if (optedOut) {\n window.localStorage.setItem(TELEMETRY_OPT_OUT_KEY, \"true\");\n } else {\n window.localStorage.removeItem(TELEMETRY_OPT_OUT_KEY);\n }\n } catch {\n // No-op — see getOrCreateTelemetryDistinctId.\n }\n}\n\nexport function hasTelemetryDisclosureBeenShown(): boolean {\n if (typeof window === \"undefined\") return false;\n try {\n return (\n window.localStorage.getItem(TELEMETRY_DISCLOSURE_SHOWN_KEY) === \"true\"\n );\n } catch {\n return false;\n }\n}\n\nexport function markTelemetryDisclosureShown(): void {\n if (typeof window === \"undefined\") return;\n try {\n window.localStorage.setItem(TELEMETRY_DISCLOSURE_SHOWN_KEY, \"true\");\n } catch {\n // No-op.\n }\n}\n\nfunction generateUuidV4(): string {\n if (\n typeof globalThis.crypto !== \"undefined\" &&\n typeof globalThis.crypto.randomUUID === \"function\"\n ) {\n return globalThis.crypto.randomUUID();\n }\n // Fallback for environments without crypto.randomUUID (older browsers,\n // some test runners). Not cryptographically strong; acceptable because\n // the value is just an anonymous correlation ID.\n return \"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx\".replace(/[xy]/g, (c) => {\n const r = (Math.random() * 16) | 0;\n const v = c === \"x\" ? r : (r & 0x3) | 0x8;\n return v.toString(16);\n });\n}\n\nconst NOTIFICATION_COOKIE = \"cpk_inspector_notifications_v1\";\nconst NOTIFICATION_STORAGE = \"cpk:inspector:notifications:v1\";\nconst NOTIFICATION_COOKIE_MAX_LENGTH = 1024;\nconst LEGACY_ANNOUNCEMENT_ID = \"16f7d877-49e3-41c3-9ca6-f951d3d8ba80\";\n\n/** Read host-wide acknowledgements from either the new compact cookie or an old full-state cookie. */\nfunction readNotificationAcknowledgements(): Pick<\n NotificationState,\n \"readIds\" | \"suppressedIds\"\n> {\n try {\n const raw = readCookie(NOTIFICATION_COOKIE);\n if (raw) {\n const value: unknown = JSON.parse(raw);\n if (value && typeof value === \"object\") {\n const state = parseNotificationState({\n ...emptyNotificationState(),\n ...value,\n });\n if (state)\n return { readIds: state.readIds, suppressedIds: state.suppressedIds };\n }\n }\n } catch {\n // A blocked or malformed cookie must not disrupt the Inspector.\n }\n return { readIds: [], suppressedIds: [] };\n}\n\n/** Load per-origin selection and merge host-wide read and suppressed notices. */\nexport function loadNotificationState(): NotificationState {\n const raw = readLocalStorageItem(NOTIFICATION_STORAGE);\n let localState = emptyNotificationState();\n if (raw) {\n try {\n localState = parseNotificationState(JSON.parse(raw)) ?? localState;\n } catch {\n // Keep the host acknowledgements when local storage is malformed.\n }\n }\n const host = readNotificationAcknowledgements();\n return {\n ...localState,\n readIds: [...new Set([...localState.readIds, ...host.readIds])],\n suppressedIds: [\n ...new Set([...localState.suppressedIds, ...host.suppressedIds]),\n ],\n };\n}\n\n/** Preserve a legacy acknowledgement only for the known announcement entering the new feed. */\nexport function migrateAnnouncementReadState(\n state: NotificationState,\n feed: NotificationFeed,\n): NotificationState {\n for (const raw of [\n readCookie(\"cpk_inspector_announcements\"),\n readLocalStorageItem(\"cpk:inspector:announcement_read\"),\n ]) {\n if (!raw) continue;\n try {\n const value: unknown = JSON.parse(raw);\n if (!value || typeof value !== \"object\" || !(\"timestamp\" in value))\n continue;\n const legacyNotice = feed.notifications.find(\n (notice) =>\n notice.id === LEGACY_ANNOUNCEMENT_ID &&\n notice.publishedAt === value.timestamp,\n );\n if (!legacyNotice) continue;\n const migrated = {\n ...state,\n readIds: [...new Set([...state.readIds, legacyNotice.id])],\n suppressedIds: [...new Set([...state.suppressedIds, legacyNotice.id])],\n };\n saveNotificationState(migrated);\n writeCookie(\"cpk_inspector_announcements\", \"\", \"Max-Age=0\");\n removeLocalStorageItem(\"cpk:inspector:announcement_read\");\n return migrated;\n } catch {\n // Malformed legacy data must not disrupt the host app.\n }\n }\n return state;\n}\n\n/** Save full state per origin and a bounded host-wide acknowledgement cookie. */\nexport function saveNotificationState(state: NotificationState): void {\n const host = readNotificationAcknowledgements();\n const merged = {\n ...state,\n readIds: [...new Set([...state.readIds, ...host.readIds])],\n suppressedIds: [\n ...new Set([...state.suppressedIds, ...host.suppressedIds]),\n ],\n };\n writeLocalStorageItem(NOTIFICATION_STORAGE, JSON.stringify(merged));\n const cookieState = {\n schemaVersion: 1,\n readIds: [...merged.readIds],\n suppressedIds: [...merged.suppressedIds],\n };\n let raw = JSON.stringify(cookieState);\n // Limit bytes sent with every localhost request. Full history stays per\n // origin when the host cookie keeps only the newest acknowledgements.\n while (encodeURIComponent(raw).length >= NOTIFICATION_COOKIE_MAX_LENGTH) {\n if (cookieState.readIds.length >= cookieState.suppressedIds.length)\n cookieState.readIds.shift();\n else cookieState.suppressedIds.shift();\n raw = JSON.stringify(cookieState);\n }\n writeCookie(NOTIFICATION_COOKIE, raw, \"Max-Age=31536000\");\n}\n\n/** ID-based pulse state is separate from the legacy announcement timestamp. */\nconst NOTIFICATION_PULSED_SESSION_KEY = \"cpk:inspector:notification-pulsed-id\";\nconst MAX_PULSED_NOTIFICATION_IDS = 100;\n\nfunction pulsedNotificationIds(raw: string | null): string[] {\n if (!raw) return [];\n // Earlier previews stored one ID directly under this key.\n if (!raw.startsWith(\"[\")) return [raw];\n try {\n const ids: unknown = JSON.parse(raw);\n return Array.isArray(ids)\n ? ids.filter((id): id is string => typeof id === \"string\")\n : [];\n } catch {\n return [];\n }\n}\n\nexport function saveNotificationPulsedId(id: string): void {\n if (typeof window === \"undefined\") return;\n try {\n const ids = pulsedNotificationIds(\n window.sessionStorage.getItem(NOTIFICATION_PULSED_SESSION_KEY),\n );\n if (!ids.includes(id)) ids.push(id);\n window.sessionStorage.setItem(\n NOTIFICATION_PULSED_SESSION_KEY,\n JSON.stringify(ids.slice(-MAX_PULSED_NOTIFICATION_IDS)),\n );\n } catch {\n /* A lost suppression must not disrupt the host. */\n }\n}\nexport function hasNotificationPulsed(\n id: string,\n publishedAt: string,\n): boolean {\n if (typeof window === \"undefined\") return false;\n try {\n const current = window.sessionStorage.getItem(\n NOTIFICATION_PULSED_SESSION_KEY,\n );\n if (current !== null) return pulsedNotificationIds(current).includes(id);\n if (loadAnnouncementPulsedTimestamp() === publishedAt) {\n saveNotificationPulsedId(id);\n return true;\n }\n } catch {\n /* Treat unavailable storage as a fresh tab. */\n }\n return false;\n}\n"],"mappings":"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"}
|