diff --git a/docs/store-listing-draft.md b/docs/store-listing-draft.md index 2edf7a7d3..004df63a4 100644 --- a/docs/store-listing-draft.md +++ b/docs/store-listing-draft.md @@ -1,17 +1,101 @@ -# Store listing draft: privacy (owner submits) +# Store listing: copy-paste text for the Chrome Web Store dashboard -Draft only. The Chrome Web Store text is the owner's to submit; check it against the README section "Privacy: what the extension stores" first. +The owner submits this text. It is checked against the README section "Privacy: what the extension stores"; change the README and this file together. Images and the dashboard field list are in `store/listing/README.md`. The two items (PROD and the unlisted TEST item "UDA TEST") use the same text; the item name comes from the manifest in each store zip, not from this file. The PROD name is `Universal Digital Assistant by Nistapp (UDAN)`, exactly 45 characters, the Chrome Web Store limit; keep any copy consistent with it. -**Single purpose.** Record, save and replay step-by-step guides on a web page (Universal Digital Assistant Network). +## Store listing tab -**Data the extension handles** -- Account: when you sign in, UDAN gets a Keycloak account id, your email and your name from your Google account. The extension keeps sign-in tokens in its own browser storage so you stay signed in; Sign Out deletes them and ends the session. -- Content you create: the guides you record and save (the clicks and labels on a page), and search terms you type, are sent to the UDAN service. -- Local: the extension stores its settings and your sign-in tokens in the browser only. +**Summary** (from the package; 112 of 132 characters) + +``` +Record, save and replay step-by-step guides on any web page. Sign in with Google to keep and search your guides. +``` + +**Description** (plain text, no headings; paste as is) + +``` +Universal Digital Assistant by Nistapp (UDAN) lets you record a task on a web page as a step-by-step guide, save it, and replay it later so that you or someone else can follow the same steps. + +What you can do +- Record: start a recording on a page and click through the task. The extension notes each step (the clicked element and a label you can edit). +- Save: give the guide a name and save it to your UDAN account. +- Replay: open the panel on the page, search for a guide, and follow it step by step. + +How it works +The extension adds a small UDAN button to web pages. Open it to get the panel. Signing in uses Google through UDAN's sign-in page. + +Your data, in plain words +- When you sign in, UDAN receives a Keycloak account id, your email and your name from your Google account. Your Google password is never seen by UDAN: you enter it on Google's or UDAN's own sign-in page. +- The guides you record and save (the clicks and labels on a page, and the page's domain) and the search terms you type are sent to the UDAN service. +- Your sign-in tokens and settings are kept in the extension's own browser storage so that you stay signed in. Sign Out ends the session and deletes the tokens. +- The web page where the panel runs receives a short-lived access token (about 5 minutes). A script on that page can ask the extension for a fresh access token while you are signed in. The long-lived refresh token never leaves the extension. +- Your data is not sold and not used for advertising. + +Privacy policy: https://udan.nistapp.com/legal/privacy +``` + +**Category:** Productivity, subcategory "Workflow & Planning". Closest alternatives: Productivity > "Tools" and Productivity > "Education" (from the dashboard list as I know it; I did not open the dashboard, so if a name has changed, pick the nearest). + +**Language:** English. + +**Privacy policy URL:** `https://udan.nistapp.com/legal/privacy` (renders the policy in a browser, verified 2026-11-01). Do not use `https://udan.nistapp.com/privacy`: the site is a single-page app and that route renders an empty page. + +**Images:** see `store/listing/README.md` (three 1280x800 screenshots, small promo tile 440x280). + +## Privacy practices tab + +**Single purpose** (paste into "Single purpose description") -**Things to say plainly** -- The web page where the panel runs receives a short-lived access token (about 5 minutes). A script on that page can ask the extension for a new one while you are signed in. The long-lived refresh token never leaves the extension. +``` +Record, save and replay step-by-step guides on a web page (Universal Digital Assistant Network). +``` + +**Permission justifications** (one box per permission; unchanged from the earlier submission, `1.0.8.8`) + +| Permission | Text for the dashboard | +|---|---| +| `identity` | Opens the sign-in window (launchWebAuthFlow) for the user's Google sign-in through UDAN's sign-in page, and reads the Chrome profile hint so the sign-in page can suggest the signed-in Chrome account. | +| `identity.email` | Suggests the Chrome account that is already signed in on the sign-in page, so the user does not have to type it. | +| `storage` | Keeps the extension's settings and the user's sign-in tokens in the extension's own storage so the user stays signed in. Sign Out deletes the tokens. | +| `tabs` | Finds the page the user is on, to show the panel on it. | +| `activeTab` | Gives the extension access to the page the user is on when they open the panel or start a recording. | +| Host access (`*://*/*`, content scripts) | The panel and the recorder run on the pages where the user records or replays a guide. The guides are for customer sites that can be on any domain, so the extension cannot name them in advance. | + +**Remote code:** No. All code is in the package; the page script is loaded from the extension's own files. + +**Data usage: what the extension collects** (tick these; leave the others empty) + +| Dashboard category | Tick | Why | +|---|---|---| +| Personally identifiable information | yes | Name and email from the user's Google account, and the Keycloak account id. | +| Authentication information | yes | Access, refresh and ID tokens from the UDAN sign-in (kept in the extension's storage). | +| User activity | yes | The clicks the user records as steps in a guide. | +| Website content | yes | The labels and element data of the steps of a recorded guide. | +| Web history | yes | A saved guide stores the page it was recorded on (`fetchDomain()` / the page URL is saved with the recording), so page addresses are collected. Verified. | +| Health, Financial and payment, Personal communications, Location | no | Not collected. | + +**Certifications** (the three statements; tick all three) + +- I do not sell or transfer user data to third parties, outside of the approved use cases. Data goes to the UDAN service that provides the item's function; the Google sign-in is the approved sign-in use. Not sold. +- I do not use or transfer user data for purposes that are unrelated to my item's single purpose. Not used for advertising or anything else. +- I do not use or transfer user data to determine creditworthiness or for lending purposes. + +**Answers to the questions behind them** + +| Question | Answer | +|---|---| +| Is data sold? | No. | +| Used for purposes unrelated to the single purpose (for example advertising)? | No. | +| Is the data required for the single purpose? | Yes: the account is needed to save and find guides; the guides and search terms are the function itself. | +| Encrypted in transit? | Yes. Evidence: the PROD store zip `1.1.0.204` was unzipped and the bundle grepped; every backend host is `https://` (`udan.nistapp.com`, `authenticate.nistapp.com`, `iam.nistapp.com`, the `*test` equivalents, third-party docs links). The only `http://` strings are `http://localhost` (dev only) and `http://www.w3.org` (a namespace string, not a request). Checkpoint: no PROD request uses plain HTTP. | +| Can the user delete it? | Sign Out deletes the tokens in the extension. For data saved on the service, point to the privacy policy (the policy page is the place to name a deletion route; see the privacy-policy placeholder issue in the PR output). | + +## Facts behind the text (from the README, keep in step) + +- Account: when you sign in, UDAN gets a Keycloak account id, your email and your name from your Google account. The extension keeps sign-in tokens in its own browser storage (`chrome.storage.local`, not readable by websites) so you stay signed in; Sign Out ends the session and deletes them. +- Content you create: the guides you record and save (the clicks and labels on a page), and search terms you type, are sent to the UDAN service. The email is sent as a hash. +- Local: the extension stores its settings and your sign-in tokens in the browser only. +- The web page where the panel runs receives a short-lived access token (about 5 minutes). **A script on that page can ask the extension for a new one while you are signed in** (`UDARequestAccessToken`). The long-lived refresh token never leaves the extension. - Your Google password is never seen by UDAN: you enter it on Google's or Keycloak's own page. - Data is not sold and not used for advertising. - -**Permission reasons** (unchanged by this change): `identity` (sign-in window and the Chrome profile hint), `identity.email` (suggest the signed-in Chrome account on the sign-in page), `storage` (settings and tokens), `tabs` and `activeTab` (find the page to show the panel on), host access (the panel runs on the pages of customer sites). +- The package file `tier.json` (`TEST` or `PROD`) is read from the extension's own package; it stores nothing and sends nothing. +- Not stored or sent: the Google password, browsing history outside the pages where the user records. diff --git a/store/listing/01-title-card-1280x800.png b/store/listing/01-title-card-1280x800.png new file mode 100644 index 000000000..84f84a815 Binary files /dev/null and b/store/listing/01-title-card-1280x800.png differ diff --git a/store/listing/02-panel-sign-in-1280x800.png b/store/listing/02-panel-sign-in-1280x800.png new file mode 100644 index 000000000..254d887f1 Binary files /dev/null and b/store/listing/02-panel-sign-in-1280x800.png differ diff --git a/store/listing/03-page-with-udan-button-1280x800.png b/store/listing/03-page-with-udan-button-1280x800.png new file mode 100644 index 000000000..5f7854ac4 Binary files /dev/null and b/store/listing/03-page-with-udan-button-1280x800.png differ diff --git a/store/listing/README.md b/store/listing/README.md new file mode 100644 index 000000000..4c02f2aa0 --- /dev/null +++ b/store/listing/README.md @@ -0,0 +1,57 @@ +# Store listing assets + +Everything the Chrome Web Store dashboard asks you to supply for the listing. The text lives in `docs/store-listing-draft.md`; this folder holds the images and how to make them again. Nothing here is uploaded by a script: the owner pastes and uploads by hand. + +## Files + +| File | Pixels | What | +|---|---|---| +| `01-title-card-1280x800.png` | 1280x800 | **Generated** title card: logo, product name, one line of value text, white background. Not a UI screenshot. | +| `02-panel-sign-in-1280x800.png` | 1280x800 | **Real UI**: the signed-out panel ("Sign in with Google") open on a synthetic page. | +| `03-page-with-udan-button-1280x800.png` | 1280x800 | **Real UI**: the same synthetic page with the extension's floating UDAN button, panel closed. | +| `promo-tile-440x280.png` | 440x280 | **Generated** small promo tile: logo and name. | +| `demo-page.html` | | The synthetic page ("Example Co.", placeholder text, no real site or person). | +| `make-cards.sh` | | Makes the title card and the tile (ImageMagick, DejaVu Sans). | +| `capture-screenshots.mjs` | | Makes screenshots 02 and 03 from a built extension (Playwright). | + +No screenshot shows a signed-in state: that needs a backend, and nothing here may touch one. The panel shows no TEST badge because the demo page asks for the PROD look (`UDAPluginSDK({ environment: "PROD" })` after `UDASdkReady`, the documented host-page call); the build behind it has only unroutable placeholder URLs. + +## Make them again + +From the repo root, Node 22, ImageMagick, and the Playwright Chromium (`npx playwright-core install chromium` if missing): + +``` +store/listing/make-cards.sh + +printf '%s\n' baseProdURL=http://127.0.0.1:9/api baseTestURL=http://127.0.0.1:9/api \ + tokenProdUrl=http://127.0.0.1:9 tokenTestUrl=http://127.0.0.1:9 \ + llmProdUrl=http://127.0.0.1:9 llmTestUrl=http://127.0.0.1:9 \ + keycloakProdUrl=http://127.0.0.1:9 keycloakTestUrl=http://127.0.0.1:9 \ + keycloakRealm=placeholder-realm keycloakClientId=placeholder-client \ + keycloakPublicClientId=placeholder-public-client keycloakIdpHint= \ + googleAnalyticsMeasurementId= enableGoogleAnalytics=false > /tmp/shot.env +UDAN_ENV_FILE=/tmp/shot.env npm run build +node store/listing/capture-screenshots.mjs build +``` + +The capture serves the page from `127.0.0.1` (content scripts only run on `http(s)` pages), aborts every other request, prints the hosts it aborted and fails on anything but the placeholder `127.0.0.1:9`. The cards are deterministic (same bytes on every run with the same fonts); screenshots 02 and 03 depend on the Chromium build and fonts, so re-run `identify` after regenerating. + +## Dashboard fields, copy-paste + +Text for each field is in `docs/store-listing-draft.md`. + +| Dashboard tab / field | Value | +|---|---| +| Store listing, Summary (max 132) | the "Summary" block (112 characters) | +| Store listing, Description | the "Description" block | +| Store listing, Category | Productivity (alternatives: Tools, Education) | +| Store listing, Language | English | +| Store listing, Screenshots | `01`, `02`, `03` (1280x800) | +| Store listing, Small promo tile | `promo-tile-440x280.png` | +| Store listing, Privacy policy URL | `https://udan.nistapp.com/legal/privacy` (never `/privacy`: it renders an empty page) | +| Privacy practices, Single purpose | the "Single purpose" block | +| Privacy practices, Permission justifications | the table: `identity`, `identity.email`, `storage`, `tabs`, `activeTab`, host access | +| Privacy practices, Remote code | No | +| Privacy practices, Data usage | Personally identifiable information, Authentication information, User activity, Website content, Web history | +| Privacy practices, Certifications | tick all three (not sold, no unrelated use, no creditworthiness use) | +| Distribution, Visibility (TEST item) | Unlisted | diff --git a/store/listing/capture-screenshots.mjs b/store/listing/capture-screenshots.mjs new file mode 100644 index 000000000..5a7b9acc9 --- /dev/null +++ b/store/listing/capture-screenshots.mjs @@ -0,0 +1,66 @@ +// Capture the real-UI store screenshots from a built extension (1280x800). +// npm run build (any build; the capture only needs the unpacked folder) +// node store/listing/capture-screenshots.mjs [] +// What it does: loads unpacked into Chromium (Playwright), serves the synthetic +// page demo-page.html from 127.0.0.1 (the content scripts only run on http(s) pages, so file:// +// would show no panel) and screenshots the page and the signed-out panel. Every request other than +// that page is aborted and listed; the run fails if the extension tried anything else than the +// build's own configured backend URLs, so nothing can reach a real backend, Keycloak, TEST or PROD. +// Build it with placeholder URLs (see README.md) so the aborted requests go nowhere anyway. +import { mkdtempSync, readFileSync, rmSync } from "node:fs"; +import { createServer } from "node:http"; +import { tmpdir } from "node:os"; +import { dirname, join, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import { chromium } from "playwright-core"; + +const here = dirname(fileURLToPath(import.meta.url)); +if (!process.argv[2]) { console.error("usage: capture-screenshots.mjs []"); process.exit(2); } +const buildDir = resolve(process.argv[2]); +const outDir = resolve(process.argv[3] || here); + +const page_html = readFileSync(join(here, "demo-page.html")); +const server = createServer((_req, res) => { res.setHeader("content-type", "text/html; charset=utf-8"); res.end(page_html); }); +await new Promise((r) => server.listen(0, "127.0.0.1", r)); +const pageOrigin = `http://127.0.0.1:${server.address().port}`; + +const profile = mkdtempSync(join(tmpdir(), "udan-shot-")); +const context = await chromium.launchPersistentContext(profile, { + channel: "chromium", + headless: true, + viewport: { width: 1280, height: 800 }, + deviceScaleFactor: 1, + args: [`--disable-extensions-except=${buildDir}`, `--load-extension=${buildDir}`], +}); + +// Allow only the local page itself and the browser's internal schemes; abort and record the rest. +const aborted = []; +await context.route( + (url) => !(url.origin === pageOrigin || /^(chrome-extension|about|blob|data):$/.test(url.protocol)), + (route) => { aborted.push(route.request().url()); route.abort(); }, +); + +try { + const page = await context.newPage(); + await page.goto(`${pageOrigin}/`, { waitUntil: "load" }); + await page.waitForSelector("#udan-react-root", { state: "attached", timeout: 20_000 }); + const button = page.locator("#udan-react-root >> text=UDAN").first(); + await button.waitFor({ state: "visible", timeout: 20_000 }); + await page.waitForTimeout(1500); + await page.screenshot({ path: join(outDir, "03-page-with-udan-button-1280x800.png") }); + + await button.click(); + await page.getByText("Sign in with Google").first().waitFor({ state: "visible", timeout: 20_000 }); + await page.waitForTimeout(1500); + await page.screenshot({ path: join(outDir, "02-panel-sign-in-1280x800.png") }); +} finally { + await context.close().catch(() => {}); + server.close(); + rmSync(profile, { recursive: true, force: true }); +} + +// Report only hosts of aborted requests (placeholders in a placeholder build). +const hosts = [...new Set(aborted.map((u) => new URL(u).host))]; +console.log(`aborted ${aborted.length} non-local request(s); hosts: ${hosts.join(", ") || "none"}`); +const unexpected = hosts.filter((h) => h !== "127.0.0.1:9"); +if (unexpected.length) { console.error(`unexpected host(s): ${unexpected.join(", ")}`); process.exit(1); } diff --git a/store/listing/demo-page.html b/store/listing/demo-page.html new file mode 100644 index 000000000..e685eb22d --- /dev/null +++ b/store/listing/demo-page.html @@ -0,0 +1,50 @@ + + + + + + + Example Co. - Help Center (demo page) + + + + +
+ Example Co. + +
+
+

Request a service visit

+

A made-up page for illustration. Fill in the form below to book a visit.

+
+ + + + + + + +
+
+ + diff --git a/store/listing/make-cards.sh b/store/listing/make-cards.sh new file mode 100755 index 000000000..0cb8044b0 --- /dev/null +++ b/store/listing/make-cards.sh @@ -0,0 +1,33 @@ +#!/usr/bin/env bash +# Generate the title card (1280x800) and the small promo tile (440x280) with ImageMagick. +# Deterministic: fixed fonts, fixed colours, no timestamps (png:exclude-chunk drops date chunks). +# Usage: store/listing/make-cards.sh (run from anywhere) +# Needs: ImageMagick 6 or 7 (magick or convert) and the DejaVu Sans fonts (override with FONT_BOLD / FONT_REG). +set -euo pipefail + +here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +logo="$here/../../public/logos/nist-logo-128x128.png" +FONT_BOLD="${FONT_BOLD:-/usr/share/fonts/TTF/DejaVuSans-Bold.ttf}" +FONT_REG="${FONT_REG:-/usr/share/fonts/TTF/DejaVuSans.ttf}" +for f in "$logo" "$FONT_BOLD" "$FONT_REG"; do [ -f "$f" ] || { echo "missing: $f" >&2; exit 2; }; done + +if command -v magick >/dev/null 2>&1; then im=(magick); else im=(convert); fi +bg="#ffffff"; ink="#14213d"; mute="#4a5568" +png=(-define png:exclude-chunk=date,time -strip) + +# Title card, 1280x800: logo, product name on two lines, one line of value text. +"${im[@]}" -size 1280x800 "xc:$bg" \ + "$logo" -gravity north -geometry +0+210 -composite \ + -gravity north -font "$FONT_BOLD" -fill "$ink" -pointsize 58 -annotate +0+370 "Universal Digital Assistant" \ + -gravity north -font "$FONT_BOLD" -fill "$ink" -pointsize 58 -annotate +0+440 "by Nistapp (UDAN)" \ + -gravity north -font "$FONT_REG" -fill "$mute" -pointsize 30 -annotate +0+560 "Record, save and replay step-by-step guides on any web page." \ + "${png[@]}" "PNG24:$here/01-title-card-1280x800.png" + +# Small promo tile, 440x280: logo and name only. +"${im[@]}" -size 440x280 "xc:$bg" \ + "$logo" -gravity north -geometry +0+36 -composite \ + -gravity north -font "$FONT_BOLD" -fill "$ink" -pointsize 22 -annotate +0+184 "Universal Digital Assistant" \ + -gravity north -font "$FONT_BOLD" -fill "$ink" -pointsize 22 -annotate +0+214 "by Nistapp (UDAN)" \ + "${png[@]}" "PNG24:$here/promo-tile-440x280.png" + +identify "$here/01-title-card-1280x800.png" "$here/promo-tile-440x280.png" diff --git a/store/listing/promo-tile-440x280.png b/store/listing/promo-tile-440x280.png new file mode 100644 index 000000000..3fdc4d8a0 Binary files /dev/null and b/store/listing/promo-tile-440x280.png differ