Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 95 additions & 11 deletions docs/store-listing-draft.md
Original file line number Diff line number Diff line change
@@ -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.
Binary file added store/listing/01-title-card-1280x800.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added store/listing/02-panel-sign-in-1280x800.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
57 changes: 57 additions & 0 deletions store/listing/README.md
Original file line number Diff line number Diff line change
@@ -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 |
66 changes: 66 additions & 0 deletions store/listing/capture-screenshots.mjs
Original file line number Diff line number Diff line change
@@ -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 <build-dir> [<out-dir>]
// What it does: loads <build-dir> 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 <build-dir> [<out-dir>]"); 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); }
50 changes: 50 additions & 0 deletions store/listing/demo-page.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<!doctype html>
<!-- Synthetic host page for the store screenshots (store/listing/capture-screenshots.mjs).
Made up for the purpose: "Example Co." is a placeholder, nothing here is a real site,
customer or person. The page loads nothing from the network. After UDASdkReady it asks
for the PROD environment (documented host-page call) so the panel shows no TEST badge. -->
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Example Co. - Help Center (demo page)</title>
<style>
* { box-sizing: border-box; }
body { margin: 0; min-height: 100vh; font-family: "DejaVu Sans", Arial, sans-serif; color: #1f2937; background: #f8fafc; }
header { background: #1e3a8a; color: #fff; padding: 18px 40px; display: flex; justify-content: space-between; align-items: center; }
header strong { font-size: 22px; letter-spacing: .3px; }
nav a { color: #dbeafe; margin-left: 24px; font-size: 15px; }
main { padding: 40px; max-width: 620px; }
h1 { margin: 0 0 8px; font-size: 30px; }
p.lead { margin: 0 0 28px; color: #4b5563; font-size: 17px; line-height: 1.5; }
form { background: #fff; border: 1px solid #e5e7eb; border-radius: 10px; padding: 24px; }
label { display: block; font-size: 14px; font-weight: bold; margin: 0 0 6px; }
input, select { width: 100%; padding: 10px 12px; margin: 0 0 18px; border: 1px solid #cbd5e1; border-radius: 6px; font-size: 15px; background: #fff; }
button { background: #2563eb; color: #fff; border: 0; padding: 11px 22px; border-radius: 6px; font-size: 15px; }
</style>
<script>
window.addEventListener("UDASdkReady", function () {
window.UDAPluginSDK({ environment: "PROD" });
});
</script>
</head>
<body>
<header>
<strong>Example Co.</strong>
<nav><a href="#">Products</a><a href="#">Pricing</a><a href="#">Help</a></nav>
</header>
<main>
<h1>Request a service visit</h1>
<p class="lead">A made-up page for illustration. Fill in the form below to book a visit.</p>
<form onsubmit="return false">
<label for="n">Full name</label>
<input id="n" placeholder="Jane Example">
<label for="t">Service type</label>
<select id="t"><option>Installation</option><option>Repair</option><option>Inspection</option></select>
<label for="d">Preferred date</label>
<input id="d" placeholder="2030-01-01">
<button type="button">Submit request</button>
</form>
</main>
</body>
</html>
Loading
Loading