diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 37adaa6..24ec702 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -43,6 +43,6 @@ jobs:
- run: pnpm screenshot
- uses: actions/upload-artifact@v7
with:
- name: emdash-openanalytics-screenshots
+ name: emdash-plugin-openanalytics-screenshots
path: docs/screenshots/*.png
if-no-files-found: error
diff --git a/README.md b/README.md
index b97eabc..1b3e2cc 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-# @blackswampai/emdash-openanalytics
+# @blackswampai/emdash-plugin-openanalytics
Native EmDash CMS integration for OpenAnalytics, by Black Swamp AI.
@@ -7,20 +7,30 @@ overview inside EmDash. The admin page uses EmDash Block Kit controls, metric
cards, notices, a timeseries chart, Top Pages, and Traffic Sources. Private
credentials stay on the server.
-## Installation
+## Quick start
Requires EmDash 1.0.1 or later in the 1.x series and Node.js 22.16 or later.
-This package has not been published to npm. For local testing, run `pnpm install`
-and `pnpm build` in this checkout, then install it from your EmDash site:
-```sh
-pnpm add /path/to/emdash-openanalytics
-```
+1. Run `pnpm add @blackswampai/emdash-plugin-openanalytics` in your EmDash site.
+2. Register `openAnalytics()` in the EmDash `plugins` array in `astro.config.mjs`.
+3. Ensure your public layout renders `` inside `
`.
+4. Deploy the site and set `EMDASH_ENCRYPTION_KEY` on its server.
+5. Create an OpenAnalytics private read key and save it in this plugin's settings.
+6. Open **OpenAnalytics** in plugin navigation; it validates once automatically,
+ then loads the overview. Revisit or choose a date range to see reports.
+
+## Install
-After an npm release is published, the installation command will be:
+Install the package after the first public npm release is available:
+
+> **No OpenAnalytics credential is needed to install or register this plugin.**
+> **Do not put your `oa_sk_...` private read key in the npm install command,
+> `astro.config.mjs`, or source code.** Add it after installation through
+> OpenAnalytics plugin settings in EmDash. EmDash encrypts secret settings using
+> `EMDASH_ENCRYPTION_KEY`. Keep it out of browser code and public environment variables.
```sh
-pnpm add @blackswampai/emdash-openanalytics
+pnpm add @blackswampai/emdash-plugin-openanalytics
```
Register the native plugin in `astro.config.mjs`:
@@ -28,7 +38,7 @@ Register the native plugin in `astro.config.mjs`:
```js
import { defineConfig } from "astro/config";
import emdash from "emdash/astro";
-import { openAnalytics } from "@blackswampai/emdash-openanalytics";
+import { openAnalytics } from "@blackswampai/emdash-plugin-openanalytics";
export default defineConfig({
integrations: [
@@ -48,37 +58,29 @@ a context created with `createPublicPageContext` from `emdash/page`.
Import `EmDashHead` from `emdash/ui`. Layouts without that insertion point cannot
render the tracker. See [EmDash's page fragment guide](https://docs.emdashcms.com/plugins/creating-native-plugins/page-fragments/).
-## OpenAnalytics setup
-
-1. Create a **private read key** in OpenAnalytics for the site you want to track.
- Request `site:read` and `analytics:read`. Connection validation uses `site:read`;
- the admin overview uses `analytics:read`.
- Older keys may only have `site:read`; site validation cannot verify the extra scope.
-2. Configure `EMDASH_ENCRYPTION_KEY` on the EmDash server **before saving a key**.
- Follow [EmDash's secrets and key management guide](https://docs.emdashcms.com/deployment/secrets/).
-3. Open **Plugins**, then the settings control for this plugin. Save the API URL,
- private read key, tracking switch, and analytics timezone. Set the timezone
- to your site's IANA timezone, such as `America/New_York`. It defaults to UTC;
- EmDash's native plugin context does not expose the host site's timezone.
- Tracking defaults to enabled, but no tracker appears before successful validation.
-4. Open **OpenAnalytics** in EmDash's plugin navigation and click **Validate connection**.
- The page shows the connected site, tracking readiness, API URL, and last
- validation time. Use **Revalidate connection** after rotating tracker settings.
-
-The existing protected validation route also remains available to administrators:
+## Configure
-```js
-const response = await fetch("/_emdash/api/plugins/emdash-openanalytics/validate-connection", {
- method: "POST",
- headers: { "X-EmDash-Request": "1" },
-});
-console.log(await response.json());
-```
+1. Set `EMDASH_ENCRYPTION_KEY` on the EmDash server before saving a secret. See
+ [EmDash's secrets and key management guide](https://docs.emdashcms.com/deployment/secrets/).
+2. In OpenAnalytics, create a private read key with `site:read` and `analytics:read`.
+ The first scope validates the site; the second allows the overview and reports.
+3. In EmDash, open **Plugins** and the OpenAnalytics settings. Set the API URL,
+ private read key, tracking switch, and IANA analytics timezone (for example,
+ `America/New_York`; the default is UTC). Save settings.
+4. Open **OpenAnalytics** in plugin navigation. With no matching saved snapshot,
+ the page validates once and then loads the overview. It shows the connected
+ site, tracking readiness, API URL, and last validation time.
+5. If automatic validation fails, use **Retry connection**. The same failed
+ configuration will not trigger another automatic attempt. Changing settings
+ permits a fresh attempt. For an established connection, **Refresh connection**
+ validates separately from the date range control.
+
+## Screenshots
+
+These captures use real EmDash with synthetic analytics data.
-The route requires `plugins:manage`. It reads the stored credential on the server;
-you do not pass the key in this request. EmDash wraps the plugin's result in its
-standard API response envelope. Connection failures contain safe error details.
-Successful validation reports site identity, status, and tracker readiness.
+
+
## Analytics overview
@@ -136,19 +138,25 @@ API URL or credential.
## Security
-`oa_sk_…` is private and server-only. EmDash's `secret` setting encrypts it at
+`oa_sk_…` is private and server-only. Never put it in `astro.config.mjs`, source
+code, browser code, or public environment variables. Enter it only in the
+OpenAnalytics plugin settings after configuring the host's `EMDASH_ENCRYPTION_KEY`.
+EmDash's `secret` setting encrypts it at
rest and presents a write-only admin input; the plugin never puts it in public
HTML, validation results, or logs. Keep the EmDash encryption key available and
back it up according to the host's secret management practices.
-`oa_pk_…` is the public browser tracking key. It belongs in page HTML. The plugin
+`oa_pk_…` is the public browser tracking key. The plugin retrieves it
+automatically from OpenAnalytics and puts it in page HTML. The plugin
uses EmDash's structured external-script fragment, which escapes attributes and
deduplicates the tracker by a stable fragment key.
The API client rejects redirects and uses a five-second timeout. API response
bodies and transport exceptions are not echoed into errors. The administrator
controls the API and tracker origins: configure endpoints you trust. HTTP works
-for local self-hosted deployments; use HTTPS for production credentials.
+for local self-hosted deployments. With a remote HTTP endpoint, the private key
+travels without encryption; use HTTPS in production. The admin page warns when
+the configured API URL uses remote HTTP.
## Self-hosting
@@ -159,7 +167,7 @@ it has no hard-coded hosted tracker or collector URL.
If any installation field is `null`, the connection can still validate, but no
script is emitted. Configure your OpenAnalytics deployment's `COLLECTOR_BASE_URL`
-and a live public tracking key, then validate again. This scaffold does not add
+and a live public tracking key, then validate again. The plugin does not add
manual tracker URL overrides.
## Current limitations
@@ -168,8 +176,10 @@ manual tracker URL overrides.
devices, sessions browser, individual visitors, custom-event reports, funnels,
revenue, web vitals, editor analytics, or realtime polling. OAuth and account
or site creation are also outside this plugin's current scope.
-- Revalidate after tracker rotation or a collector URL change. Saved installation
+- Refresh the connection after tracker rotation or a collector URL change. Saved installation
metadata has no automatic expiry; private-key revocation is detected on validation.
+- Pre-release installations using the old `emdash-openanalytics` plugin ID must
+ re-enter their OpenAnalytics settings once after updating.
- Static pages receive the snapshot available when they are rendered. Rebuild
those pages after changing the connection or tracking switch.
- The tracker independently fetches OpenAnalytics's browser configuration. A
diff --git a/demo/README.md b/demo/README.md
index e2a5ef8..1b79885 100644
--- a/demo/README.md
+++ b/demo/README.md
@@ -1,6 +1,18 @@
# EmDash OpenAnalytics screenshot demo
-Run `pnpm screenshot` from the repository root. It builds `dist/`, starts a local OpenAnalytics fixture and an actual EmDash Astro admin using the packaged plugin descriptor, authenticates through EmDash's development-only bypass, configures the fixture connection through the protected settings API, validates it, and captures three screenshots under `docs/screenshots/`.
+Run `pnpm screenshot` from the repository root. It builds `dist/`, starts a local
+OpenAnalytics fixture and an actual EmDash Astro admin using the packaged plugin
+descriptor, authenticates through EmDash's development-only bypass, and opens
+the plugin's blank analytics page. The harness fills and saves EmDash's native
+settings form with the fixture API URL, synthetic private key, tracking switch,
+and timezone. Saving makes no OpenAnalytics request. It then opens the analytics
+page again, where the first site validation happens automatically, and captures overview, reports, and
+narrow-layout screenshots under `docs/screenshots/`.
+
+The demo asserts that the blank-settings page has no connection controls, the
+first configured page visit makes exactly one site read before the four analytics
+reads, and revisiting the page with unchanged settings makes no second site read.
+There is no manual validation step in the setup.
The fixture and EmDash app bind to `127.0.0.1` on reserved ephemeral ports. Their readiness routes require a per-run random token, and EmDash also refuses to start if its configured port is occupied. The fixture accepts only its synthetic `oa_sk_demo_fixture_only_…` key. The EmDash demo database lives in a newly created temporary directory and is removed on exit. Captures are staged in another temporary directory; before they are copied into the repository, the harness scans rendered DOM text, page HTML, text/JSON browser responses, process output, and PNG bytes for private-key-shaped values and the fixture key. This environment has no OCR tool, so the screenshot check supplements the scan of the exact DOM used to take each image.
diff --git a/docs/implementation-footprint.md b/docs/implementation-footprint.md
index eac3446..8ddcd5f 100644
--- a/docs/implementation-footprint.md
+++ b/docs/implementation-footprint.md
@@ -12,6 +12,7 @@ excluded. These are size comparisons, not runtime performance measurements.
| EmDash scaffold, PR #1 | 495 | 721 | 9 | 1 |
| EmDash native overview, PR #2 | 1458 | 1923 | 10 | 3 |
| EmDash pages and sources, PR #3 | 1770 | 2423 | 14 | 5 |
+| EmDash 0.1.0 release candidate | 1903 | 2711 | 14 | 5 |
| n8n OpenAnalytics 0.1.1 | 607 | 613 | 5 | 11 |
PR #2 adds **963 production lines** and **1202 test lines**
@@ -26,6 +27,15 @@ The admin coordinator delegates range handling, connection blocks, overview/char
rendering, and report tables to four small modules. Successful sections remain
visible when another read fails.
+The release candidate is `@blackswampai/emdash-plugin-openanalytics`, from
+`BlackSwampAI/emdash-plugin-openanalytics`, with native plugin ID `openanalytics`.
+Release hardening adds automatic first-load validation, connection controls apart
+from the date range, bounded upstream responses and route bodies, package checks,
+and a form-driven screenshot setup. It adds no analytics reports or endpoints.
+A fresh connection adds one site read before the existing four analytics reads;
+matching snapshots skip that site read on revisits and range changes. Failed
+configuration fingerprints require an explicit connection retry.
+
The EmDash plugin now owns a native admin page, public tracker insertion,
credential-bound installation snapshots, secure read transport, response
validation/projection, and useful connection/error/freshness states. The n8n
@@ -76,3 +86,4 @@ size. EmDash PR #3 packs to **about 23.7 kB** (decimal), compared with
Its package includes compiled workflow operations, source maps, and icons; the
EmDash package includes the compiled plugin, declarations, README, and two
contract/footprint documents. Package size reflects those different contents.
+The current 0.1.0 release candidate packs to approximately **26.5 kB** (decimal).
diff --git a/docs/screenshots/openanalytics-narrow.png b/docs/screenshots/openanalytics-narrow.png
index 77fcee4..f8c306a 100644
Binary files a/docs/screenshots/openanalytics-narrow.png and b/docs/screenshots/openanalytics-narrow.png differ
diff --git a/docs/screenshots/openanalytics-overview.png b/docs/screenshots/openanalytics-overview.png
index 190d250..bcb0949 100644
Binary files a/docs/screenshots/openanalytics-overview.png and b/docs/screenshots/openanalytics-overview.png differ
diff --git a/docs/screenshots/openanalytics-reports.png b/docs/screenshots/openanalytics-reports.png
index 5adb040..05ce7a7 100644
Binary files a/docs/screenshots/openanalytics-reports.png and b/docs/screenshots/openanalytics-reports.png differ
diff --git a/docs/upstream-contracts.md b/docs/upstream-contracts.md
index a99e2fd..4a87974 100644
--- a/docs/upstream-contracts.md
+++ b/docs/upstream-contracts.md
@@ -1,5 +1,64 @@
# Verified upstream contracts
+## Release naming and current native-plugin contract (2026-09-29)
+
+Before the release naming change, I rechecked the current EmDash upstream
+`main` ref with `git ls-remote`: `54209bc9bd0b48e12bdefa8ac971da01ced7990f`.
+This is the pinned upstream revision for the release-convention check. A direct
+source clone was unavailable, so I used GitHub's public API to read the relevant
+source files at this exact commit.
+
+The current [native plugin distribution guide](https://docs.emdashcms.com/plugins/creating-native-plugins/distributing/)
+explicitly says `definePlugin()` accepts a simple unscoped ID of lowercase
+letters, digits, and hyphens, and recommends that form because an ID occupies
+one route path segment. It separately shows a scoped npm entrypoint. Therefore
+`@blackswampai/emdash-plugin-openanalytics` as the npm package and `openanalytics`
+as the native plugin ID follow the upstream recommendation; the package scope
+does not need to be repeated in the plugin ID. Current EmDash packages such as
+`@emdash-cms/plugin-audit-log` also use `plugin-` in their package name, while
+the official distribution guide documents that package identity and plugin ID
+are distinct values. No current EmDash technical constraint argues against the
+requested Black Swamp AI package family. At the pinned revision,
+[`packages/plugins/audit-log/package.json`](https://github.com/emdash-cms/emdash/blob/54209bc9bd0b48e12bdefa8ac971da01ced7990f/packages/plugins/audit-log/package.json)
+is named `@emdash-cms/plugin-audit-log`; the native Color example uses
+`id: "color"` in
+[`packages/plugins/color/src/index.ts`](https://github.com/emdash-cms/emdash/blob/54209bc9bd0b48e12bdefa8ac971da01ced7990f/packages/plugins/color/src/index.ts).
+The route-segment recommendation appears in the
+[current native distribution guide](https://docs.emdashcms.com/plugins/creating-native-plugins/distributing/).
+
+The current [native generated-settings documentation](https://docs.emdashcms.com/plugins/creating-native-plugins/react-admin/)
+describes `admin.settingsSchema` as the generated form contract. The [official
+hook reference](https://docs.emdashcms.com/reference/hooks/) documents plugin
+lifecycle, content, media, and public-page hooks; it does not define a settings
+save callback. Likewise, the current [hook guide](https://docs.emdashcms.com/plugins/creating-plugins/hooks/)
+says hooks are declared at plugin definition time, with `content:afterSave`
+being explicitly a content-save hook. There is no documented native settings
+`afterSave`/`onSave` extension point to validate credentials after a generated
+settings form save. The pinned handler source,
+[`packages/core/src/api/handlers/plugin-settings.ts`](https://github.com/emdash-cms/emdash/blob/54209bc9bd0b48e12bdefa8ac971da01ced7990f/packages/core/src/api/handlers/plugin-settings.ts),
+shows `handlePluginSettingsUpdate` validating, encrypting, transactionally
+writing, and reading back declared keys; it takes no plugin callback and
+dispatches no settings lifecycle event. Settings are namespaced as
+`plugin:{pluginId}:settings:{key}` in
+[`packages/core/src/plugins/settings.ts`](https://github.com/emdash-cms/emdash/blob/54209bc9bd0b48e12bdefa8ac971da01ced7990f/packages/core/src/plugins/settings.ts).
+Connection establishment therefore belongs in the authenticated plugin admin
+page flow, rather than relying on undocumented host internals.
+
+GitHub's current [repository rename documentation](https://docs.github.com/en/repositories/creating-and-managing-repositories/renaming-a-repository)
+confirms that ordinary web URLs and `git clone`, `git fetch`, and `git push`
+requests to the old repository location redirect after a rename. Project Pages
+URLs are the exception; GitHub Actions hosted from a renamed repository also do
+not redirect. The documented conditions include having organization-owner or
+repository-admin permission and not recreating a repository under the old name.
+Thus a normal rename from `BlackSwampAI/emdash-openanalytics` to
+`BlackSwampAI/emdash-plugin-openanalytics` preserves ordinary repository and
+git traffic, subject to those stated exceptions.
+
+The repository rename was performed after the release branch was committed and
+pushed. Both the old and new GitHub API paths resolve to repository ID
+`1396756742` and the canonical name `BlackSwampAI/emdash-plugin-openanalytics`;
+the old repository path redirects to the renamed repository.
+
Inspected on 2026-09-29 before implementation:
- EmDash [`54209bc9bd0b48e12bdefa8ac971da01ced7990f`](https://github.com/emdash-cms/emdash/tree/54209bc9bd0b48e12bdefa8ac971da01ced7990f),
diff --git a/package.json b/package.json
index 4fc37b3..d9196f9 100644
--- a/package.json
+++ b/package.json
@@ -1,5 +1,5 @@
{
- "name": "@blackswampai/emdash-openanalytics",
+ "name": "@blackswampai/emdash-plugin-openanalytics",
"version": "0.1.0",
"description": "Native EmDash plugin for OpenAnalytics",
"keywords": [
@@ -8,15 +8,15 @@
"emdash-plugin",
"openanalytics"
],
- "homepage": "https://github.com/BlackSwampAI/emdash-openanalytics#readme",
+ "homepage": "https://github.com/BlackSwampAI/emdash-plugin-openanalytics#readme",
"bugs": {
- "url": "https://github.com/BlackSwampAI/emdash-openanalytics/issues"
+ "url": "https://github.com/BlackSwampAI/emdash-plugin-openanalytics/issues"
},
"license": "MIT",
"author": "Black Swamp AI",
"repository": {
"type": "git",
- "url": "git+https://github.com/BlackSwampAI/emdash-openanalytics.git"
+ "url": "git+https://github.com/BlackSwampAI/emdash-plugin-openanalytics.git"
},
"files": [
"dist",
@@ -32,6 +32,9 @@
"import": "./dist/index.mjs"
}
},
+ "publishConfig": {
+ "access": "public"
+ },
"scripts": {
"build": "tsdown src/index.ts --format esm --dts --clean",
"dev": "tsdown src/index.ts --format esm --dts --watch",
@@ -60,7 +63,7 @@
"react-dom": "19.3.0",
"tsdown": "0.20.3",
"typescript": "5.9.3",
- "vitest": "4.0.15"
+ "vitest": "4.1.11"
},
"peerDependencies": {
"emdash": ">=1.0.1 <2"
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 21f633a..0c95663 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -48,8 +48,8 @@ importers:
specifier: 5.9.3
version: 5.9.3
vitest:
- specifier: 4.0.15
- version: 4.0.15(@types/node@24.10.1)(lightningcss@1.33.0)
+ specifier: 4.1.11
+ version: 4.1.11(@types/node@24.10.1)(vite@8.3.1(@types/node@24.10.1)(esbuild@0.28.2))
packages:
@@ -931,13 +931,6 @@ packages:
'@cfworker/json-schema':
optional: true
- '@napi-rs/lzma-linux-x64-gnu@1.5.1':
- resolution: {integrity: sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==}
- engines: {node: ^22.20 || ^24.12 || >=25}
- cpu: [x64]
- os: [linux]
- libc: [glibc]
-
'@napi-rs/wasm-runtime@1.2.4':
resolution: {integrity: sha512-AJxoUD2/15ESHbvpcyjU274nsAPLuOtPHCk0vKJM5pj//Fg/B1FXNWjPnXTT9PymCYYiHo4zPj0ZomXBKhoy7g==}
engines: {node: ^20.19.0 || ^22.13.0 || >=23.5.0}
@@ -1551,144 +1544,6 @@ packages:
'@rolldown/pluginutils@1.0.1':
resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==}
- '@rollup/rollup-android-arm-eabi@4.63.5':
- resolution: {integrity: sha512-J25QJU+B78T4FhhBsNpLJyVWOi31mwtpcMwywHmOKH65Q9IWGA81gPj+dnwlhU8wktVriYE+tFAaQgrnJRzAZg==}
- cpu: [arm]
- os: [android]
-
- '@rollup/rollup-android-arm64@4.63.5':
- resolution: {integrity: sha512-LDopB3zuZM5Ux9TT2luNEBJW/tYbGU2g1d+VpKk6I+gSKDb+/7sYE6M225gRQt4RbMX6MSwMsVR/phdjVUgRLg==}
- cpu: [arm64]
- os: [android]
-
- '@rollup/rollup-darwin-arm64@4.63.5':
- resolution: {integrity: sha512-wlJEERGfeuHeBavCL2qVnNacOK43NDoZM4sjkeRPymd04OAE9T1zBqDJgmZ+CIsPTYKwdzpUC8vmOw84dwY4Tg==}
- cpu: [arm64]
- os: [darwin]
-
- '@rollup/rollup-darwin-x64@4.63.5':
- resolution: {integrity: sha512-4nJJGg5jbo2wwPP4JP+LfEBA3bvP8rU9CLuhp7jWvq9sxEyhjQFTFdrqi+/dHEin/pd8jpT0vcehIpnZtmEdcQ==}
- cpu: [x64]
- os: [darwin]
-
- '@rollup/rollup-freebsd-arm64@4.63.5':
- resolution: {integrity: sha512-DrZbyCDF1hneuO6jRbvZ2D7+PIBM6yIwYnJpg2vIk58T+wuFpiaGZrfUr59lDWw45bg+IrpTGLPiNi/Fk4w3Cg==}
- cpu: [arm64]
- os: [freebsd]
-
- '@rollup/rollup-freebsd-x64@4.63.5':
- resolution: {integrity: sha512-gqfUVMJMB3mehqywxp6hTBFfgtMQykZY19+cfiaYP0toIJLb/1DZRJHVkQQGP13W4TAwfZDWeg1qBcheTRioXQ==}
- cpu: [x64]
- os: [freebsd]
-
- '@rollup/rollup-linux-arm-gnueabihf@4.63.5':
- resolution: {integrity: sha512-CFmhpvAwzSaWMlN3VN7UtmoTihlZNzoP0juQib5TQRnYUyDV8dXeWOp29sobWAT6gXl/hQgAClLlEiYozQG3OQ==}
- cpu: [arm]
- os: [linux]
- libc: [glibc]
-
- '@rollup/rollup-linux-arm-musleabihf@4.63.5':
- resolution: {integrity: sha512-Uc9H8eXCOayV6JLTH5bXKMId6qbhNHa818/BgYjm4jrlq3vZquC9cqyvHBw17xy5Mnj5f+I3gFK5JcEf3hSqrw==}
- cpu: [arm]
- os: [linux]
- libc: [musl]
-
- '@rollup/rollup-linux-arm64-gnu@4.63.5':
- resolution: {integrity: sha512-VcPr/szv/1BFw112Kt//fxulXt/JPqzzidU84iW68L2DdjnOO8QFUv2zTSYBEPHD6movBD4z+bbr5y60GYM7Jw==}
- cpu: [arm64]
- os: [linux]
- libc: [glibc]
-
- '@rollup/rollup-linux-arm64-musl@4.63.5':
- resolution: {integrity: sha512-BnxtJ5/91BrIHYIkGrmjz/lbMhqEHt1dPFqIxIFR+jPn0xVc/oUSCtIT089zfp5ufwGDlYz2UC+Fe1SRBpYFbQ==}
- cpu: [arm64]
- os: [linux]
- libc: [musl]
-
- '@rollup/rollup-linux-loong64-gnu@4.63.5':
- resolution: {integrity: sha512-LrYcHZwF+fAMNKHYTOQ5osWM4AZF7YF6D+XtsjDyEvljtt11twc+zHVXBLNEjxVSUnKYsOhvVz4Z213eW02COQ==}
- cpu: [loong64]
- os: [linux]
- libc: [glibc]
-
- '@rollup/rollup-linux-loong64-musl@4.63.5':
- resolution: {integrity: sha512-nj7QKQePAAUpCpJHtg0pR0W/b92A9NO17JS3BAQmHDn/yhmkir2p8llrKY9TOhleKIaSzy1JhxS3T9FVld6coA==}
- cpu: [loong64]
- os: [linux]
- libc: [musl]
-
- '@rollup/rollup-linux-ppc64-gnu@4.63.5':
- resolution: {integrity: sha512-5ylkX6dWMeBKge9nTU+Rxfb+ZfaCIJ9lRqIFaK0eAMcWp7OJbYnLveLgXmm0VrvuLKb8qIK+mHyH0qu88RM+iA==}
- cpu: [ppc64]
- os: [linux]
- libc: [glibc]
-
- '@rollup/rollup-linux-ppc64-musl@4.63.5':
- resolution: {integrity: sha512-oHK4ZHYFDKjZviK34I+NwgfbGxgI7ztrNxj2hPTSSNFgeq1a/lEd7dHV2fdGAuTH4Iym3RHJg+vAbWaWG4B7Zg==}
- cpu: [ppc64]
- os: [linux]
- libc: [musl]
-
- '@rollup/rollup-linux-riscv64-gnu@4.63.5':
- resolution: {integrity: sha512-UcetmHZ6XOXuUByiKZyQmb55ZPr0LABr3Ec/HB9wKZn6CEAFWZkE+hsJErJ9hbPBC7nI0dKuELx7CoV6IM7TMg==}
- cpu: [riscv64]
- os: [linux]
- libc: [glibc]
-
- '@rollup/rollup-linux-riscv64-musl@4.63.5':
- resolution: {integrity: sha512-C5CmDPQBtvjVo8cgQsBs+w6WB0JLkiixhgi6hVLV11hERWdn/p0XcPU2OUcZzac9BPOFq7SbaHFa8r3SWEysCQ==}
- cpu: [riscv64]
- os: [linux]
- libc: [musl]
-
- '@rollup/rollup-linux-s390x-gnu@4.63.5':
- resolution: {integrity: sha512-lHVQHJFKsuuxLMi3MQO9XVL8Tje3JR82CzB+QDKC5NWBcsIWuwsn9uIM5e3lBhI+fF1/s63qnyYqsg65+8rV/w==}
- cpu: [s390x]
- os: [linux]
- libc: [glibc]
-
- '@rollup/rollup-linux-x64-gnu@4.63.5':
- resolution: {integrity: sha512-3W9bTFcQNJn71cSJVM9RKIiZOy8DO/XLDii8Uv/Pm6WKqDRj7JV3ZfuXIEfyuy5LXpIzAbB/1M4Ukp9GKNa7nA==}
- cpu: [x64]
- os: [linux]
- libc: [glibc]
-
- '@rollup/rollup-linux-x64-musl@4.63.5':
- resolution: {integrity: sha512-VDC7rRJlee/scpki96GZ27Omf6yU87s1YXwVTpjE5841faVlDYYT565rgfmoR1U0sqL7z5ivQSDjcsF6VRXyBA==}
- cpu: [x64]
- os: [linux]
- libc: [musl]
-
- '@rollup/rollup-openbsd-x64@4.63.5':
- resolution: {integrity: sha512-z86Ok2p4pTdv5xqCKZsTooO7yBEiaJR/HzU3Wx8RmWsPoLppnMKROhJusQob8B3IE1ghC343kUW9rC2r+Wf3ig==}
- cpu: [x64]
- os: [openbsd]
-
- '@rollup/rollup-openharmony-arm64@4.63.5':
- resolution: {integrity: sha512-IzQmj+xXwQFGhMAMKMQVXkMwMZN3TqkJgAE0nSsqvVwWWciP4AIPMmWRqOQ2GfX7TUDZr+xqGFcBS36CRPGw0g==}
- cpu: [arm64]
- os: [openharmony]
-
- '@rollup/rollup-win32-arm64-msvc@4.63.5':
- resolution: {integrity: sha512-F6qpTaPc9bwBH85kjy0/BLmLSW1uv7AoOXCoRIkg2arlgCYlWYcAbiMkvZuAcaWk9TpCRG//okznLAqLGshkMw==}
- cpu: [arm64]
- os: [win32]
-
- '@rollup/rollup-win32-ia32-msvc@4.63.5':
- resolution: {integrity: sha512-igoDsTFhhwECBeGbUuLeIk7t8Y1apa+cs6mDWpx2EZ0ch7oEQgzHbFUXN9euoHekCAQzXdXApAGkV6jznS7tWw==}
- cpu: [ia32]
- os: [win32]
-
- '@rollup/rollup-win32-x64-gnu@4.63.5':
- resolution: {integrity: sha512-U3teMeMbXFmaM5D+OTJpsOXd+wV/qftIeYF9kBKL4v73641qyJmoXFtA28DQLsnmlyayEsTe72xpLHrArq6vHw==}
- cpu: [x64]
- os: [win32]
-
- '@rollup/rollup-win32-x64-msvc@4.63.5':
- resolution: {integrity: sha512-ypfC34F3RKXvCXBglGqGMsUSMKlgwd1HX9AOAlx9RoZZ6GaI42YHVeKpzg3JG+wpBUJYTG+NNZhqbDWL8tBZkw==}
- cpu: [x64]
- os: [win32]
-
'@shikijs/core@4.4.3':
resolution: {integrity: sha512-QCR4q2ZO/ILJEuwiBMel4wdcTDb1JGwfjKTxPDF6x8ixOaluPrVqIn06C99AcRPhmYlBR56d/Fb+GN58GzExpg==}
engines: {node: '>=20'}
@@ -2102,34 +1957,34 @@ packages:
oxc-transform-react:
optional: true
- '@vitest/expect@4.0.15':
- resolution: {integrity: sha512-Gfyva9/GxPAWXIWjyGDli9O+waHDC0Q0jaLdFP1qPAUUfo1FEXPXUfUkp3eZA0sSq340vPycSyOlYUeM15Ft1w==}
+ '@vitest/expect@4.1.11':
+ resolution: {integrity: sha512-VX2x5vNJXET47KAFzwERI+KRMtTTCSWTfSMKsW7JsUsXV4psq++e3DvZpuTDOpHcxytiDs6p2nhVb2tVDiiUYw==}
- '@vitest/mocker@4.0.15':
- resolution: {integrity: sha512-CZ28GLfOEIFkvCFngN8Sfx5h+Se0zN+h4B7yOsPVCcgtiO7t5jt9xQh2E1UkFep+eb9fjyMfuC5gBypwb07fvQ==}
+ '@vitest/mocker@4.1.11':
+ resolution: {integrity: sha512-2XJVD55d1o5AZous5CCGKS74g/riOj9odEt2bQpCVZeblHyHdnMeFl4jl0XjU21stf4mbjUkew2eXQZt65g5CQ==}
peerDependencies:
msw: ^2.4.9
- vite: ^6.0.0 || ^7.0.0-0
+ vite: ^6.0.0 || ^7.0.0 || ^8.0.0
peerDependenciesMeta:
msw:
optional: true
vite:
optional: true
- '@vitest/pretty-format@4.0.15':
- resolution: {integrity: sha512-SWdqR8vEv83WtZcrfLNqlqeQXlQLh2iilO1Wk1gv4eiHKjEzvgHb2OVc3mIPyhZE6F+CtfYjNlDJwP5MN6Km7A==}
+ '@vitest/pretty-format@4.1.11':
+ resolution: {integrity: sha512-yiZzPbGTS9Sr/JpFl8zHrcIkAofNbFV6k21vIgQN/cY/oxZeXhJv5sc/MBJ5jFKWmWs+oJHw0UXLZjmf931+Vw==}
- '@vitest/runner@4.0.15':
- resolution: {integrity: sha512-+A+yMY8dGixUhHmNdPUxOh0la6uVzun86vAbuMT3hIDxMrAOmn5ILBHm8ajrqHE0t8R9T1dGnde1A5DTnmi3qw==}
+ '@vitest/runner@4.1.11':
+ resolution: {integrity: sha512-LztvUgdwMNJMIkj3hQnnxiC2Xy1zNxq928W/xhjCLaNCzqTZOudjwbQf6v9IntZGPw132i2Lq2rgTRZHD3JHNw==}
- '@vitest/snapshot@4.0.15':
- resolution: {integrity: sha512-A7Ob8EdFZJIBjLjeO0DZF4lqR6U7Ydi5/5LIZ0xcI+23lYlsYJAfGn8PrIWTYdZQRNnSRlzhg0zyGu37mVdy5g==}
+ '@vitest/snapshot@4.1.11':
+ resolution: {integrity: sha512-pN7ikn1ON7h8ee4gIAp4AzyK+zBtJPzVbqOgu5LCEh4VaJVbPQcgYQYJIMGQPXVeJJq1fnfazis7a5pFNPahog==}
- '@vitest/spy@4.0.15':
- resolution: {integrity: sha512-+EIjOJmnY6mIfdXtE/bnozKEvTC4Uczg19yeZ2vtCz5Yyb0QQ31QWVQ8hswJ3Ysx/K2EqaNsVanjr//2+P3FHw==}
+ '@vitest/spy@4.1.11':
+ resolution: {integrity: sha512-apNa/prQy2qCeywhnixOHPRCgGNhvg7T4Dapfl1GahLp/R+uhBm5cPyFoNVyqsNd2h1nJxL6BqqdIjiABL60YA==}
- '@vitest/utils@4.0.15':
- resolution: {integrity: sha512-HXjPW2w5dxhTD0dLwtYHDnelK3j8sR8cWIaLxr22evTyY6q8pRCjZSmhRWVjBaOVXChQd6AwMzi9pucorXCPZA==}
+ '@vitest/utils@4.1.11':
+ resolution: {integrity: sha512-zTCVGpyFsGWBhllOyKlTw/vnr6D9qxsfSDyfbyZmTyjHw5N/VuvzHpHoQjm2ZJzn4RJgx5w4r7V0er69CmLgPQ==}
'@wordpress/block-serialization-default-parser@5.56.0':
resolution: {integrity: sha512-ZS2DpbXSFke+Hqld0l2oB8SxNj49AC6MidE0CKA0MYk5Xd/eBeDUNt/JqjpVJlWcdiO+7kVCe8VP7YPOkON64A==}
@@ -2290,6 +2145,9 @@ packages:
resolution: {integrity: sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==}
engines: {node: '>=18'}
+ convert-source-map@2.0.0:
+ resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
+
cookie-es@1.2.3:
resolution: {integrity: sha512-lXVyvUvrNXblMqzIRrxHb57UUVmqsSWlxqt3XIjCkUP0wDAf6uicO6KMbEgYrMNtEvWgWHwe42CKxPu9MYAnWw==}
@@ -2500,9 +2358,6 @@ packages:
resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==}
engines: {node: '>= 0.4'}
- es-module-lexer@1.7.0:
- resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==}
-
es-module-lexer@2.3.2:
resolution: {integrity: sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==}
@@ -3379,11 +3234,6 @@ packages:
engines: {node: ^20.19.0 || >=22.12.0}
hasBin: true
- rollup@4.63.5:
- resolution: {integrity: sha512-KRWwmNLlPw5M7HcdYfm15oBv9n9LPtjzpzCIxS/phwqvPyxHSoKX6Y2YU3pxSPfy0CLquVgsx/j/hBi6OvH1Nw==}
- engines: {node: '>=18.0.0', npm: '>=8.0.0'}
- hasBin: true
-
rope-sequence@1.3.4:
resolution: {integrity: sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==}
@@ -3502,8 +3352,8 @@ packages:
resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==}
engines: {node: '>= 0.8'}
- std-env@3.10.0:
- resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==}
+ std-env@4.3.0:
+ resolution: {integrity: sha512-OtU/EgQ1kIm5KwqQpBC6ZEMXrZRui11w8zgfTWp8cdO9B8OaPsbA8bTHO2P+HNo1VlUTGMVBwPhydu6poeXiag==}
stringify-entities@4.0.4:
resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==}
@@ -3758,46 +3608,6 @@ packages:
vfile@6.0.3:
resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==}
- vite@7.3.6:
- resolution: {integrity: sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==}
- engines: {node: ^20.19.0 || >=22.12.0}
- hasBin: true
- peerDependencies:
- '@types/node': ^20.19.0 || >=22.12.0
- jiti: '>=1.21.0'
- less: ^4.0.0
- lightningcss: ^1.21.0
- sass: ^1.70.0
- sass-embedded: ^1.70.0
- stylus: '>=0.54.8'
- sugarss: ^5.0.0
- terser: ^5.16.0
- tsx: ^4.8.1
- yaml: ^2.4.2
- peerDependenciesMeta:
- '@types/node':
- optional: true
- jiti:
- optional: true
- less:
- optional: true
- lightningcss:
- optional: true
- sass:
- optional: true
- sass-embedded:
- optional: true
- stylus:
- optional: true
- sugarss:
- optional: true
- terser:
- optional: true
- tsx:
- optional: true
- yaml:
- optional: true
-
vite@8.3.1:
resolution: {integrity: sha512-/bvH9E9tmCXRGp2uXY3WbOldqpTwFkbha/8ANaEQ6VkxhH60KyqLwgZq6lG2y+4uT55x9+9eUHMpQ7uGnOCKjA==}
engines: {node: ^20.19.0 || >=22.12.0}
@@ -3849,20 +3659,23 @@ packages:
vite:
optional: true
- vitest@4.0.15:
- resolution: {integrity: sha512-n1RxDp8UJm6N0IbJLQo+yzLZ2sQCDyl1o0LeugbPWf8+8Fttp29GghsQBjYJVmWq3gBFfe9Hs1spR44vovn2wA==}
+ vitest@4.1.11:
+ resolution: {integrity: sha512-fhACrNXUidIbGSBr5FlbuBkO7VWC1ZyLl0DO4CU2DrQoAPxX84Ysxs+HeGQpii5lZWV1Q4gBZTTu49mF+A6Edw==}
engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0}
hasBin: true
peerDependencies:
'@edge-runtime/vm': '*'
'@opentelemetry/api': ^1.9.0
'@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0
- '@vitest/browser-playwright': 4.0.15
- '@vitest/browser-preview': 4.0.15
- '@vitest/browser-webdriverio': 4.0.15
- '@vitest/ui': 4.0.15
+ '@vitest/browser-playwright': 4.1.11
+ '@vitest/browser-preview': 4.1.11
+ '@vitest/browser-webdriverio': 4.1.11
+ '@vitest/coverage-istanbul': 4.1.11
+ '@vitest/coverage-v8': 4.1.11
+ '@vitest/ui': 4.1.11
happy-dom: '*'
jsdom: '*'
+ vite: ^6.0.0 || ^7.0.0 || ^8.0.0
peerDependenciesMeta:
'@edge-runtime/vm':
optional: true
@@ -3876,6 +3689,10 @@ packages:
optional: true
'@vitest/browser-webdriverio':
optional: true
+ '@vitest/coverage-istanbul':
+ optional: true
+ '@vitest/coverage-v8':
+ optional: true
'@vitest/ui':
optional: true
happy-dom:
@@ -4874,9 +4691,6 @@ snapshots:
transitivePeerDependencies:
- supports-color
- '@napi-rs/lzma-linux-x64-gnu@1.5.1':
- optional: true
-
'@napi-rs/wasm-runtime@1.2.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)':
dependencies:
'@emnapi/core': 1.10.0
@@ -5228,81 +5042,6 @@ snapshots:
'@rolldown/pluginutils@1.0.1': {}
- '@rollup/rollup-android-arm-eabi@4.63.5':
- optional: true
-
- '@rollup/rollup-android-arm64@4.63.5':
- optional: true
-
- '@rollup/rollup-darwin-arm64@4.63.5':
- optional: true
-
- '@rollup/rollup-darwin-x64@4.63.5':
- optional: true
-
- '@rollup/rollup-freebsd-arm64@4.63.5':
- optional: true
-
- '@rollup/rollup-freebsd-x64@4.63.5':
- optional: true
-
- '@rollup/rollup-linux-arm-gnueabihf@4.63.5':
- optional: true
-
- '@rollup/rollup-linux-arm-musleabihf@4.63.5':
- optional: true
-
- '@rollup/rollup-linux-arm64-gnu@4.63.5':
- optional: true
-
- '@rollup/rollup-linux-arm64-musl@4.63.5':
- optional: true
-
- '@rollup/rollup-linux-loong64-gnu@4.63.5':
- optional: true
-
- '@rollup/rollup-linux-loong64-musl@4.63.5':
- optional: true
-
- '@rollup/rollup-linux-ppc64-gnu@4.63.5':
- optional: true
-
- '@rollup/rollup-linux-ppc64-musl@4.63.5':
- optional: true
-
- '@rollup/rollup-linux-riscv64-gnu@4.63.5':
- optional: true
-
- '@rollup/rollup-linux-riscv64-musl@4.63.5':
- optional: true
-
- '@rollup/rollup-linux-s390x-gnu@4.63.5':
- optional: true
-
- '@rollup/rollup-linux-x64-gnu@4.63.5':
- optional: true
-
- '@rollup/rollup-linux-x64-musl@4.63.5':
- optional: true
-
- '@rollup/rollup-openbsd-x64@4.63.5':
- optional: true
-
- '@rollup/rollup-openharmony-arm64@4.63.5':
- optional: true
-
- '@rollup/rollup-win32-arm64-msvc@4.63.5':
- optional: true
-
- '@rollup/rollup-win32-ia32-msvc@4.63.5':
- optional: true
-
- '@rollup/rollup-win32-x64-gnu@4.63.5':
- optional: true
-
- '@rollup/rollup-win32-x64-msvc@4.63.5':
- optional: true
-
'@shikijs/core@4.4.3':
dependencies:
'@shikijs/primitive': 4.4.3
@@ -5729,43 +5468,45 @@ snapshots:
'@rolldown/pluginutils': 1.0.1
vite: 8.3.1(@types/node@24.10.1)(esbuild@0.28.2)
- '@vitest/expect@4.0.15':
+ '@vitest/expect@4.1.11':
dependencies:
'@standard-schema/spec': 1.1.0
'@types/chai': 5.2.3
- '@vitest/spy': 4.0.15
- '@vitest/utils': 4.0.15
+ '@vitest/spy': 4.1.11
+ '@vitest/utils': 4.1.11
chai: 6.2.2
tinyrainbow: 3.1.1
- '@vitest/mocker@4.0.15(vite@7.3.6(@types/node@24.10.1)(lightningcss@1.33.0))':
+ '@vitest/mocker@4.1.11(vite@8.3.1(@types/node@24.10.1)(esbuild@0.28.2))':
dependencies:
- '@vitest/spy': 4.0.15
+ '@vitest/spy': 4.1.11
estree-walker: 3.0.3
magic-string: 0.30.21
optionalDependencies:
- vite: 7.3.6(@types/node@24.10.1)(lightningcss@1.33.0)
+ vite: 8.3.1(@types/node@24.10.1)(esbuild@0.28.2)
- '@vitest/pretty-format@4.0.15':
+ '@vitest/pretty-format@4.1.11':
dependencies:
tinyrainbow: 3.1.1
- '@vitest/runner@4.0.15':
+ '@vitest/runner@4.1.11':
dependencies:
- '@vitest/utils': 4.0.15
+ '@vitest/utils': 4.1.11
pathe: 2.0.3
- '@vitest/snapshot@4.0.15':
+ '@vitest/snapshot@4.1.11':
dependencies:
- '@vitest/pretty-format': 4.0.15
+ '@vitest/pretty-format': 4.1.11
+ '@vitest/utils': 4.1.11
magic-string: 0.30.21
pathe: 2.0.3
- '@vitest/spy@4.0.15': {}
+ '@vitest/spy@4.1.11': {}
- '@vitest/utils@4.0.15':
+ '@vitest/utils@4.1.11':
dependencies:
- '@vitest/pretty-format': 4.0.15
+ '@vitest/pretty-format': 4.1.11
+ convert-source-map: 2.0.0
tinyrainbow: 3.1.1
'@wordpress/block-serialization-default-parser@5.56.0': {}
@@ -5984,6 +5725,8 @@ snapshots:
content-type@2.1.0: {}
+ convert-source-map@2.0.0: {}
+
cookie-es@1.2.3: {}
cookie-es@2.0.1: {}
@@ -6232,8 +5975,6 @@ snapshots:
es-errors@1.3.0: {}
- es-module-lexer@1.7.0: {}
-
es-module-lexer@2.3.2: {}
es-object-atoms@1.1.2:
@@ -7177,38 +6918,6 @@ snapshots:
'@rolldown/binding-win32-arm64-msvc': 1.2.11
'@rolldown/binding-win32-x64-msvc': 1.2.11
- rollup@4.63.5:
- dependencies:
- '@types/estree': 1.0.9
- optionalDependencies:
- '@napi-rs/lzma-linux-x64-gnu': 1.5.1
- '@rollup/rollup-android-arm-eabi': 4.63.5
- '@rollup/rollup-android-arm64': 4.63.5
- '@rollup/rollup-darwin-arm64': 4.63.5
- '@rollup/rollup-darwin-x64': 4.63.5
- '@rollup/rollup-freebsd-arm64': 4.63.5
- '@rollup/rollup-freebsd-x64': 4.63.5
- '@rollup/rollup-linux-arm-gnueabihf': 4.63.5
- '@rollup/rollup-linux-arm-musleabihf': 4.63.5
- '@rollup/rollup-linux-arm64-gnu': 4.63.5
- '@rollup/rollup-linux-arm64-musl': 4.63.5
- '@rollup/rollup-linux-loong64-gnu': 4.63.5
- '@rollup/rollup-linux-loong64-musl': 4.63.5
- '@rollup/rollup-linux-ppc64-gnu': 4.63.5
- '@rollup/rollup-linux-ppc64-musl': 4.63.5
- '@rollup/rollup-linux-riscv64-gnu': 4.63.5
- '@rollup/rollup-linux-riscv64-musl': 4.63.5
- '@rollup/rollup-linux-s390x-gnu': 4.63.5
- '@rollup/rollup-linux-x64-gnu': 4.63.5
- '@rollup/rollup-linux-x64-musl': 4.63.5
- '@rollup/rollup-openbsd-x64': 4.63.5
- '@rollup/rollup-openharmony-arm64': 4.63.5
- '@rollup/rollup-win32-arm64-msvc': 4.63.5
- '@rollup/rollup-win32-ia32-msvc': 4.63.5
- '@rollup/rollup-win32-x64-gnu': 4.63.5
- '@rollup/rollup-win32-x64-msvc': 4.63.5
- fsevents: 2.3.3
-
rope-sequence@1.3.4: {}
router@2.2.0:
@@ -7387,7 +7096,7 @@ snapshots:
statuses@2.0.2: {}
- std-env@3.10.0: {}
+ std-env@4.3.0: {}
stringify-entities@4.0.4:
dependencies:
@@ -7581,19 +7290,6 @@ snapshots:
'@types/unist': 3.0.3
vfile-message: 4.0.3
- vite@7.3.6(@types/node@24.10.1)(lightningcss@1.33.0):
- dependencies:
- esbuild: 0.28.2
- fdir: 6.5.0(picomatch@4.0.7)
- picomatch: 4.0.7
- postcss: 8.5.28
- rollup: 4.63.5
- tinyglobby: 0.2.17
- optionalDependencies:
- '@types/node': 24.10.1
- fsevents: 2.3.3
- lightningcss: 1.33.0
-
vite@8.3.1(@types/node@24.10.1)(esbuild@0.28.2):
dependencies:
lightningcss: 1.33.0
@@ -7610,42 +7306,32 @@ snapshots:
optionalDependencies:
vite: 8.3.1(@types/node@24.10.1)(esbuild@0.28.2)
- vitest@4.0.15(@types/node@24.10.1)(lightningcss@1.33.0):
+ vitest@4.1.11(@types/node@24.10.1)(vite@8.3.1(@types/node@24.10.1)(esbuild@0.28.2)):
dependencies:
- '@vitest/expect': 4.0.15
- '@vitest/mocker': 4.0.15(vite@7.3.6(@types/node@24.10.1)(lightningcss@1.33.0))
- '@vitest/pretty-format': 4.0.15
- '@vitest/runner': 4.0.15
- '@vitest/snapshot': 4.0.15
- '@vitest/spy': 4.0.15
- '@vitest/utils': 4.0.15
- es-module-lexer: 1.7.0
+ '@vitest/expect': 4.1.11
+ '@vitest/mocker': 4.1.11(vite@8.3.1(@types/node@24.10.1)(esbuild@0.28.2))
+ '@vitest/pretty-format': 4.1.11
+ '@vitest/runner': 4.1.11
+ '@vitest/snapshot': 4.1.11
+ '@vitest/spy': 4.1.11
+ '@vitest/utils': 4.1.11
+ es-module-lexer: 2.3.2
expect-type: 1.4.0
magic-string: 0.30.21
obug: 2.2.1
pathe: 2.0.3
picomatch: 4.0.7
- std-env: 3.10.0
+ std-env: 4.3.0
tinybench: 2.9.0
tinyexec: 1.3.1
tinyglobby: 0.2.17
tinyrainbow: 3.1.1
- vite: 7.3.6(@types/node@24.10.1)(lightningcss@1.33.0)
+ vite: 8.3.1(@types/node@24.10.1)(esbuild@0.28.2)
why-is-node-running: 2.3.0
optionalDependencies:
'@types/node': 24.10.1
transitivePeerDependencies:
- - jiti
- - less
- - lightningcss
- msw
- - sass
- - sass-embedded
- - stylus
- - sugarss
- - terser
- - tsx
- - yaml
w3c-keyname@2.2.8: {}
diff --git a/scripts/check-package.mjs b/scripts/check-package.mjs
index 48f7b11..c4c8f9d 100644
--- a/scripts/check-package.mjs
+++ b/scripts/check-package.mjs
@@ -7,6 +7,19 @@ import { join, resolve } from "node:path";
const root = resolve(import.meta.dirname, "..");
const pkg = JSON.parse(await readFile(resolve(root, "package.json"), "utf8"));
+assert.equal(pkg.name, "@blackswampai/emdash-plugin-openanalytics");
+assert.equal(pkg.version, "0.1.0");
+assert.equal(pkg.private, undefined);
+assert.deepEqual(pkg.publishConfig, { access: "public" });
+assert.equal(pkg.engines.node, ">=22.16");
+assert.equal(pkg.peerDependencies.emdash, ">=1.0.1 <2");
+assert.deepEqual(pkg.files, [
+ "dist",
+ "docs/implementation-footprint.md",
+ "docs/upstream-contracts.md",
+]);
+assert.equal(pkg.main, pkg.exports["."].import);
+assert.equal(pkg.types, pkg.exports["."].types);
await readFile(resolve(root, pkg.exports["."].import));
await readFile(resolve(root, pkg.exports["."].types));
const { openAnalytics, createPlugin } = await import(pkg.name);
@@ -17,6 +30,7 @@ const plugin = createPlugin();
assert.equal(descriptor.entrypoint, pkg.name);
assert.equal(descriptor.format, "native");
assert.match(descriptor.id, /^[a-z0-9-]+$/);
+assert.equal(descriptor.id, "openanalytics");
assert.equal(descriptor.id, plugin.id);
assert.equal(descriptor.version, pkg.version);
assert.equal(plugin.version, pkg.version);
@@ -25,6 +39,8 @@ assert(plugin.capabilities.includes("hooks.page-fragments:register"));
assert.equal(typeof plugin.hooks["page:fragments"].handler, "function");
assert.equal(typeof plugin.routes["validate-connection"].handler, "function");
assert.equal(plugin.routes["validate-connection"].permission, "plugins:manage");
+assert.deepEqual(plugin.routes["validate-connection"].methods, ["POST"]);
+assert.deepEqual(plugin.routes["validate-connection"].request, { body: "bytes", maxBytes: 4_096 });
assert.equal(typeof plugin.routes.admin.handler, "function");
assert.equal(plugin.routes.admin.permission, "plugins:manage");
assert.deepEqual(plugin.routes.admin.methods, ["POST"]);
@@ -49,10 +65,29 @@ try {
} finally {
rmSync(cache, { recursive: true, force: true });
}
-for (const { path } of packed.files) {
- assert(!/^(demo|scripts|tests)\//.test(path), `Development fixture packed: ${path}`);
- assert(!path.startsWith("docs/screenshots/"), `Screenshot packed: ${path}`);
+const allowedFiles = new Set([
+ "package.json",
+ "README.md",
+ "LICENSE",
+ "dist/index.mjs",
+ "dist/index.d.mts",
+ "docs/implementation-footprint.md",
+ "docs/upstream-contracts.md",
+]);
+const packedFiles = packed.files.map(({ path }) => path).sort();
+assert.deepEqual(packedFiles, [...allowedFiles].sort(), "Unexpected npm package file list");
+for (const path of packedFiles) {
const contents = await readFile(resolve(root, path), "utf8");
assert(!/oa_sk_[A-Za-z0-9_-]{8,}/.test(contents), `Private credential packed: ${path}`);
+ assert(!/(?:^|\/)\.env(?:\.|$)/m.test(contents), `Environment file reference packed: ${path}`);
+ assert(!/\.(?:sqlite|sqlite3|db)(?:\W|$)/i.test(path), `Database artifact packed: ${path}`);
+ assert(
+ !/(?:\/home\/|\/tmp\/|\/Users\/|[A-Z]:\\Users\\)/.test(contents),
+ `Local path packed: ${path}`,
+ );
+ assert(
+ !/(?:from|import)\s+["'][^"']*(?:\.\/)?tests?\//.test(contents),
+ `Test import packed: ${path}`,
+ );
}
-console.log("Package exports, native metadata, packed files, and artifact credential scan passed.");
+console.log("Package metadata, exports, exact file allowlist, and artifact safety checks passed.");
diff --git a/scripts/demo-openanalytics.mjs b/scripts/demo-openanalytics.mjs
index 384f745..dde5387 100644
--- a/scripts/demo-openanalytics.mjs
+++ b/scripts/demo-openanalytics.mjs
@@ -52,7 +52,7 @@ const server = createServer((req, res) => {
return send(res, 401, { error: { code: "UNAUTHORIZED", message: "fixture auth required" } });
if (req.method !== "GET") return send(res, 405, { error: { code: "METHOD_NOT_ALLOWED" } });
const path = new URL(req.url ?? "/", `http://127.0.0.1:${PORT}`).pathname;
- if (path.startsWith("/v1/read/analytics/")) {
+ if (path === "/v1/read/site" || path.startsWith("/v1/read/analytics/")) {
const url = new URL(req.url ?? "/", `http://127.0.0.1:${PORT}`);
observedReads.push({
path,
diff --git a/scripts/screenshot-demo.mjs b/scripts/screenshot-demo.mjs
index 757b2d4..1283649 100644
--- a/scripts/screenshot-demo.mjs
+++ b/scripts/screenshot-demo.mjs
@@ -221,44 +221,48 @@ try {
);
});
await getSession(page);
- const headers = { "X-EmDash-Request": "1", "Content-Type": "application/json", Origin: BASE };
- const settings = await page.request.put(
- `${BASE}/_emdash/api/admin/plugins/emdash-openanalytics/settings`,
- {
- headers,
- data: {
- values: {
- apiUrl: FIXTURE_BASE,
- privateReadKey: SYNTHETIC_KEY,
- trackingEnabled: true,
- timezone: "America/New_York",
- },
- },
- },
+ await page.goto(`${BASE}/_emdash/admin/plugins/openanalytics/analytics`);
+ await page.getByText("Not configured", { exact: true }).waitFor({ state: "visible" });
+ if (await page.getByRole("button", { name: /(?:Refresh|Retry) connection/ }).count())
+ throw new Error("Blank settings must not offer connection maintenance.");
+ assertNoSecret(await page.content(), "blank-settings admin HTML");
+ await page.goto(`${BASE}/_emdash/admin/plugins-manager/openanalytics/settings`);
+ await page.getByLabel("OpenAnalytics API URL", { exact: true }).fill(FIXTURE_BASE);
+ await page.getByLabel("Private read key", { exact: true }).fill(SYNTHETIC_KEY);
+ await page.getByLabel("Analytics timezone", { exact: true }).fill("America/New_York");
+ const trackingSwitch = page.getByRole("switch", { name: "Enable tracking", exact: true });
+ if ((await trackingSwitch.getAttribute("aria-checked")) !== "true") await trackingSwitch.click();
+ const settingsSaved = page.waitForResponse(
+ (response) =>
+ response.url() === `${BASE}/_emdash/api/admin/plugins/openanalytics/settings` &&
+ response.request().method() === "PUT",
);
- await requireOk(settings, "seed synthetic plugin settings");
- const validation = await page.request.post(
- `${BASE}/_emdash/api/plugins/emdash-openanalytics/validate-connection`,
- {
- headers,
- data: {},
- },
- );
- const validationJson = await requireOk(validation, "validate fixture connection");
- const validationResult = validationJson.data ?? validationJson;
- if (validationResult.success !== true)
- throw new Error("The fixture connection did not validate successfully.");
+ await page.getByRole("button", { name: "Save Settings", exact: true }).first().click();
+ await requireOk(await settingsSaved, "save synthetic plugin settings through the native form");
+ await page
+ .getByText("Settings saved successfully", { exact: true })
+ .waitFor({ state: "visible" });
+ const beforeNavigation = await (await fetch(`${FIXTURE_BASE}/__requests`)).json();
+ if (beforeNavigation.length !== 0)
+ throw new Error("Saving settings unexpectedly called OpenAnalytics.");
const requested = [];
page.on("request", (request) => {
- if (request.url().includes("/_emdash/api/plugins/emdash-openanalytics"))
- requested.push(request.url());
+ if (request.url().includes("/_emdash/api/plugins/openanalytics")) requested.push(request.url());
});
- await page.goto(`${BASE}/_emdash/admin/plugins/emdash-openanalytics/analytics`);
+ await page.goto(`${BASE}/_emdash/admin/plugins/openanalytics/analytics`);
await page.locator("text=Top Pages").waitFor({ state: "visible", timeout: 30_000 });
await page.locator("text=Traffic Sources").waitFor({ state: "visible", timeout: 30_000 });
await page.getByText("1284", { exact: true }).waitFor({ state: "visible" });
await page.getByText("EmDash Demo", { exact: true }).waitFor({ state: "visible" });
+ await page.getByText("Connected", { exact: true }).waitFor({ state: "visible" });
+ const refresh = page.getByRole("button", { name: "Refresh connection", exact: true });
+ await refresh.waitFor({ state: "visible" });
+ const range = page.getByText("Date range", { exact: true });
+ const refreshBounds = await refresh.boundingBox();
+ const rangeBounds = await range.boundingBox();
+ if (!refreshBounds || !rangeBounds || refreshBounds.y + refreshBounds.height >= rangeBounds.y)
+ throw new Error("Connection maintenance must appear above the independent date range control.");
await page.locator("canvas").first().waitFor({ state: "visible", timeout: 30_000 });
await waitForStableChart(page);
await Promise.all(responseScans);
@@ -272,6 +276,10 @@ try {
for (const line of childLogs) assertNoSecret(line, "demo process logs");
const readLogResponse = await fetch(`${FIXTURE_BASE}/__requests`);
const readLog = await readLogResponse.json();
+ if (readLog.filter(({ path }) => path === "/v1/read/site").length !== 1)
+ throw new Error("The first admin load must validate the connection exactly once.");
+ if (requested.some((url) => url.includes("/validate-connection")))
+ throw new Error("The screenshot setup must not explicitly validate the connection.");
const analyticsReads = readLog.filter((item) => item.path.startsWith("/v1/read/analytics/"));
const relevant = [
"/v1/read/analytics/overview",
@@ -289,6 +297,14 @@ try {
}
if (new Set(analyticsReads.map(({ from, to }) => `${from}|${to}`)).size !== 1)
throw new Error("Overview and report calls did not share one selected range.");
+ // A matching snapshot must survive an ordinary revisit without another site read.
+ await page.reload();
+ await page.getByText("Connected", { exact: true }).waitFor({ state: "visible" });
+ await page.getByText("1284", { exact: true }).waitFor({ state: "visible" });
+ await waitForStableChart(page);
+ const revisitReads = await (await fetch(`${FIXTURE_BASE}/__requests`)).json();
+ if (revisitReads.filter(({ path }) => path === "/v1/read/site").length !== 1)
+ throw new Error("An ordinary page revisit revalidated a matching snapshot.");
await mkdir(staging, { recursive: true });
await waitForStableChart(page);
await page.screenshot({ path: join(staging, "openanalytics-overview.png"), fullPage: false });
diff --git a/src/admin/connection.ts b/src/admin/connection.ts
index edc4c82..b983621 100644
--- a/src/admin/connection.ts
+++ b/src/admin/connection.ts
@@ -34,6 +34,7 @@ export function connectionBlocks(args: {
needsValidation?: boolean;
notConfigured?: boolean;
error?: string;
+ connectionAction?: { id: "revalidate" | "retry"; label: string; value?: unknown };
}): Block[] {
let title = args.notConfigured
? "Not configured"
@@ -48,7 +49,7 @@ export function connectionBlocks(args: {
let variant: "default" | "alert" | "error" =
args.notConfigured || args.needsValidation ? "alert" : "default";
if (args.error) {
- title = "Connection needs attention";
+ title = args.site ? "Connection needs attention" : "Connection failed";
description = args.error;
variant = "error";
} else if (args.site && args.site.status !== "active") {
@@ -57,6 +58,15 @@ export function connectionBlocks(args: {
variant = "alert";
}
const install = args.site?.install;
+ let insecureRemoteHttp = false;
+ try {
+ const parsed = new URL(args.apiUrl);
+ const host = parsed.hostname.toLowerCase().replace(/^\[|\]$/g, "");
+ const loopback = host === "localhost" || host === "::1" || /^127(?:\.\d{1,3}){3}$/.test(host);
+ insecureRemoteHttp = parsed.protocol === "http:" && !loopback;
+ } catch {
+ /* Invalid URLs are already represented safely in the API field. */
+ }
const tracking = !install?.hasTrackingKey
? "No tracking key"
: !install.trackerReady
@@ -84,13 +94,36 @@ export function connectionBlocks(args: {
},
],
},
+ ...(args.connectionAction
+ ? [
+ {
+ type: "actions" as const,
+ elements: [
+ {
+ type: "button" as const,
+ action_id: args.connectionAction.id,
+ label: args.connectionAction.label,
+ style: "secondary" as const,
+ ...(args.connectionAction.value === undefined
+ ? {}
+ : { value: args.connectionAction.value }),
+ },
+ ],
+ },
+ ]
+ : []),
+ ...(insecureRemoteHttp
+ ? [
+ {
+ type: "context" as const,
+ text: "HTTP sends your private read key without encryption. Use HTTPS for production.",
+ },
+ ]
+ : []),
];
}
-export function controls(
- selected: DateRange,
- options: { validate?: boolean; retry?: boolean } = {},
-): Block {
+export function controls(selected: DateRange, options: { retry?: boolean } = {}): Block {
const labels: Record = {
"24h": "Last 24 hours",
"7d": "Last 7 days",
@@ -117,12 +150,5 @@ export function controls(
style: "primary",
value: { range: selected },
});
- elements.push({
- type: "button",
- action_id: "revalidate",
- label: options.validate ? "Validate connection" : "Revalidate connection",
- style: "secondary",
- value: { range: selected },
- });
return { type: "actions", elements };
}
diff --git a/src/admin/page.ts b/src/admin/page.ts
index 18e1cfd..1744d24 100644
--- a/src/admin/page.ts
+++ b/src/admin/page.ts
@@ -23,6 +23,8 @@ type Input =
| { type: "page_load"; page: string }
| { type: "block_action"; action_id: string; value?: unknown; page?: string };
+const VALIDATION_FAILURE_KEY = "state:admin-validation-failure";
+
function record(value: unknown): value is Record {
return !!value && typeof value === "object" && !Array.isArray(value);
}
@@ -42,12 +44,18 @@ function parseInput(value: unknown): Input | null {
function safeError(kind: unknown, retryAfterSeconds?: number): string {
switch (kind) {
+ case "configuration":
+ return "The OpenAnalytics API URL or private read key is invalid. Review plugin settings.";
case "analytics_forbidden":
return "This private key does not have analytics:read permission.";
case "suspended":
return "This OpenAnalytics site is suspended, so analytics data is unavailable.";
case "unauthorized":
- return "OpenAnalytics rejected this credential. Revalidate with a current private read key.";
+ return "OpenAnalytics rejected this credential. Update the private read key in plugin settings, then retry the connection.";
+ case "not_found":
+ return "No OpenAnalytics site was found for this private read key. Check the key and site configuration.";
+ case "invalid_response":
+ return "OpenAnalytics returned an invalid connection response. Retry the connection shortly.";
case "forbidden":
return "This private key does not have permission to read OpenAnalytics data.";
case "billing":
@@ -65,7 +73,7 @@ function safeError(kind: unknown, retryAfterSeconds?: number): string {
case "server":
return "OpenAnalytics is temporarily unavailable. Try again shortly.";
default:
- return "OpenAnalytics could not load analytics. Revalidate the connection or try again shortly.";
+ return "OpenAnalytics could not load analytics. Refresh the connection or try again shortly.";
}
}
@@ -82,6 +90,7 @@ function waitingPage(
message: string,
needsValidation = true,
notConfigured = false,
+ connectionAction?: { id: "revalidate" | "retry"; label: string },
): BlockResponse {
return {
blocks: [
@@ -92,11 +101,12 @@ function waitingPage(
needsValidation,
notConfigured,
error: needsValidation || notConfigured ? undefined : message,
+ connectionAction: connectionAction
+ ? { ...connectionAction, value: { range: selected } }
+ : undefined,
}),
- controls(selected, {
- validate: notConfigured || (needsValidation && message.startsWith("Validate")),
- }),
- ...(needsValidation ? [{ type: "context" as const, text: message }] : []),
+ controls(selected),
+ ...(needsValidation || notConfigured ? [{ type: "context" as const, text: message }] : []),
],
};
}
@@ -150,9 +160,14 @@ async function loadAnalytics(
site,
trackingEnabled,
validatedAt: snapshot.validatedAt,
+ connectionAction: {
+ id: "revalidate",
+ label: "Refresh connection",
+ value: { range: selected },
+ },
}),
- controls(selected, { retry: failures }),
{ type: "header", text: rangeLabel(selected) },
+ controls(selected, { retry: failures }),
...renderOverview({
timezone,
overview,
@@ -229,17 +244,21 @@ export async function renderAdminPage(ctx: RouteContext): Promise
}
const { timezone, error: timezoneError } = timezoneFromSetting(timezoneValue);
let validationError: string | null = null;
+ const fingerprint = await configurationFingerprint(config.apiUrl, config.readKey);
+ let snapshot = await ctx.kv.get(SITE_SNAPSHOT_KEY);
+ const matchingSnapshot = () => isSiteSnapshot(snapshot) && snapshot.fingerprint === fingerprint;
if (interaction.type === "block_action" && interaction.action_id === "revalidate") {
+ await ctx.kv.delete(VALIDATION_FAILURE_KEY).catch(() => undefined);
const validation = await validateConnection(ctx);
if (!validation.success) {
validationError = safeError(
validation.error.kind,
"retryAfterSeconds" in validation.error ? validation.error.retryAfterSeconds : undefined,
);
+ await ctx.kv.set(VALIDATION_FAILURE_KEY, { fingerprint, message: validationError });
}
+ snapshot = await ctx.kv.get(SITE_SNAPSHOT_KEY);
}
- const fingerprint = await configurationFingerprint(config.apiUrl, config.readKey);
- const snapshot = await ctx.kv.get(SITE_SNAPSHOT_KEY);
if (validationError) {
if (isSiteSnapshot(snapshot) && snapshot.fingerprint === fingerprint) {
return {
@@ -250,6 +269,11 @@ export async function renderAdminPage(ctx: RouteContext): Promise
site: safeConnectionSummary(snapshot.site),
trackingEnabled,
validatedAt: snapshot.validatedAt,
+ connectionAction: {
+ id: "revalidate",
+ label: "Retry connection",
+ value: { range: selected },
+ },
}),
controls(selected),
{
@@ -261,7 +285,33 @@ export async function renderAdminPage(ctx: RouteContext): Promise
],
};
}
- return waitingPage(selected, config.apiUrl, trackingEnabled, validationError, false);
+ return waitingPage(selected, config.apiUrl, trackingEnabled, validationError, false, false, {
+ id: "revalidate",
+ label: "Retry connection",
+ });
+ }
+ const previousFailure = await ctx.kv.get(VALIDATION_FAILURE_KEY);
+ const failedState =
+ record(previousFailure) && previousFailure.fingerprint === fingerprint
+ ? typeof previousFailure.message === "string"
+ ? previousFailure.message
+ : "Connection failed. Try again."
+ : null;
+ if (!matchingSnapshot() && interaction.type === "page_load" && !failedState) {
+ const validation = await validateConnection(ctx);
+ if (validation.success) {
+ snapshot = await ctx.kv.get(SITE_SNAPSHOT_KEY);
+ } else {
+ validationError = safeError(
+ validation.error.kind,
+ "retryAfterSeconds" in validation.error ? validation.error.retryAfterSeconds : undefined,
+ );
+ await ctx.kv.set(VALIDATION_FAILURE_KEY, { fingerprint, message: validationError });
+ return waitingPage(selected, config.apiUrl, trackingEnabled, validationError, false, false, {
+ id: "revalidate",
+ label: "Retry connection",
+ });
+ }
}
if (timezoneError) {
return {
@@ -273,6 +323,11 @@ export async function renderAdminPage(ctx: RouteContext): Promise
site: safeConnectionSummary(snapshot.site),
trackingEnabled,
validatedAt: snapshot.validatedAt,
+ connectionAction: {
+ id: "revalidate",
+ label: "Refresh connection",
+ value: { range: selected },
+ },
})
: connectionBlocks({ apiUrl: config.apiUrl, trackingEnabled, needsValidation: true })),
controls(selected),
@@ -286,13 +341,18 @@ export async function renderAdminPage(ctx: RouteContext): Promise
};
}
if (!isSiteSnapshot(snapshot) || snapshot.fingerprint !== fingerprint) {
+ if (failedState)
+ return waitingPage(selected, config.apiUrl, trackingEnabled, failedState, false, false, {
+ id: "revalidate",
+ label: "Retry connection",
+ });
return waitingPage(
selected,
config.apiUrl,
trackingEnabled,
isSiteSnapshot(snapshot)
- ? "Configuration changed. Revalidate the connection to resume tracking and load analytics."
- : "Validate the connection to load analytics.",
+ ? "Configuration changed. Open this page to validate the connection and resume tracking."
+ : "Open this page to validate the connection and load analytics.",
);
}
return loadAnalytics(config, snapshot, selected, timezone, trackingEnabled);
diff --git a/src/index.ts b/src/index.ts
index 29774be..3dbc7f7 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -5,9 +5,9 @@ export { createPlugin } from "./plugin";
/** Native EmDash plugin entry descriptor. */
export function openAnalytics(): PluginDescriptor {
return {
- id: "emdash-openanalytics",
+ id: "openanalytics",
version: "0.1.0",
- entrypoint: "@blackswampai/emdash-openanalytics",
+ entrypoint: "@blackswampai/emdash-plugin-openanalytics",
format: "native" as const,
};
}
diff --git a/src/openanalytics/client.ts b/src/openanalytics/client.ts
index 2ea3787..ffd3549 100644
--- a/src/openanalytics/client.ts
+++ b/src/openanalytics/client.ts
@@ -22,6 +22,7 @@ const SITE_STATUSES = new Set(["active", "suspended", "deleting", "deleted"]);
const RESOLUTIONS = new Set(["minute", "hour", "day", "week"]);
const FRESHNESS_STATES = new Set(["ok", "no_data", "stale", "degraded"]);
const ISO_UTC_INSTANT = /^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d(?:\.\d+)?Z$/;
+const MAX_RESPONSE_BYTES = 1_048_576;
export function containsPrivateKey(value: unknown, seen = new Set