@chendesheng/comm100-auto-login 0.0.0-stage → 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +690 -2
- package/bin/comm100-accounts-host.mjs +4 -0
- package/bin/comm100-accounts.mjs +9 -0
- package/extension/manifest.json +70 -0
- package/extension/src/background/cli-bridge.js +50 -0
- package/extension/src/background/service-worker.js +1338 -0
- package/extension/src/content/content.js +418 -0
- package/extension/src/content/request-hook.js +204 -0
- package/extension/src/icons/icon128.png +0 -0
- package/extension/src/icons/icon16.png +0 -0
- package/extension/src/icons/icon48.png +0 -0
- package/extension/src/popup/popup.html +65 -0
- package/extension/src/popup/popup.js +652 -0
- package/extension/src/popup/tailwind.css +2 -0
- package/extension/src/popup/tailwind.input.css +68 -0
- package/extension/src/shared/api.js +252 -0
- package/extension/src/shared/cookie-budget.js +116 -0
- package/extension/src/shared/cookies.js +209 -0
- package/extension/src/shared/platforms.js +119 -0
- package/extension/src/shared/storage.js +612 -0
- package/extension/src/shared/tokens.js +18 -0
- package/package.json +39 -4
- package/scripts/postinstall.mjs +14 -0
- package/src/cli/accounts.mjs +265 -0
- package/src/cli/bridge.mjs +103 -0
- package/src/cli/extension-path.mjs +11 -0
- package/src/cli/native-host.mjs +73 -0
- package/src/cli/setup.mjs +35 -0
- package/src/shared/api.js +252 -0
- package/src/shared/cookie-budget.js +116 -0
- package/src/shared/cookies.js +209 -0
- package/src/shared/platforms.js +119 -0
- package/src/shared/storage.js +612 -0
- package/src/shared/tokens.js +18 -0
package/README.md
CHANGED
|
@@ -1,3 +1,691 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Comm100 Auto Login
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
An unpacked Chrome (MV3) extension that signs in to Comm100 test dashboards **in the background**,
|
|
4
|
+
using the login HTTP API rather than the sign-in form. You get the dashboard, not a form.
|
|
5
|
+
|
|
6
|
+
It remembers multiple accounts — **one per dashboard** — and always uses the **most recently used**
|
|
7
|
+
one for the site you are on.
|
|
8
|
+
|
|
9
|
+
## Terminal account tool
|
|
10
|
+
|
|
11
|
+
Requires Node.js 22 or later. Install the CLI and bundled Chrome extension from npm:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install --global --foreground-scripts @chendesheng/comm100-auto-login
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
The install message prints the absolute extension folder inside the installed package:
|
|
18
|
+
`node_modules/@chendesheng/comm100-auto-login/extension`. Open `chrome://extensions`, enable
|
|
19
|
+
**Developer mode**, click **Load unpacked**, and select that folder. You can print its path again:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
comm100-accounts extension-path
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
`--foreground-scripts` makes npm show the install message. If install scripts are disabled or their
|
|
26
|
+
output is hidden, the extension files are still included and `extension-path` works normally.
|
|
27
|
+
After updating the npm package, click **Reload** for the extension in Chrome to load the new code.
|
|
28
|
+
|
|
29
|
+
For development from a source checkout, install dependencies and link the executable instead:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
npm install
|
|
33
|
+
npm link
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Reload the updated extension at `chrome://extensions`, then run the one-time bridge setup:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
comm100-accounts setup
|
|
40
|
+
# Reload the Comm100 Auto Login extension once more to connect the installed native host.
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Setup registers a [Chrome native messaging host](https://developer.chrome.com/docs/extensions/develop/concepts/native-messaging)
|
|
44
|
+
for this extension and profile. It uses one temporary LevelDB snapshot to discover the profile's
|
|
45
|
+
connection ID; it never opens Chrome's original database. The launcher uses this installation's
|
|
46
|
+
absolute Node.js path, so Chrome does not need Node on its PATH. Setup currently supports Chrome on
|
|
47
|
+
macOS and Linux. Chrome must be running, with the extension enabled, for subsequent CLI commands.
|
|
48
|
+
|
|
49
|
+
All normal reads and writes go through the extension service worker over a private local socket.
|
|
50
|
+
Accounts and JWTs share the extension's `chrome.storage.local` state; no database snapshots are
|
|
51
|
+
made for `list`, `login`, or `tokens`, and there is no separate JWT cache file. Bridge configuration
|
|
52
|
+
contains only profile/connection metadata, not credentials or tokens.
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
comm100-accounts list
|
|
56
|
+
comm100-accounts list --domain=livechat6dash.testing.comm100dev.io --siteId=10010
|
|
57
|
+
comm100-accounts list --siteId=10010
|
|
58
|
+
comm100-accounts list --show-password
|
|
59
|
+
|
|
60
|
+
comm100-accounts login agent@example.com
|
|
61
|
+
comm100-accounts login agent@example.com --siteId=10010
|
|
62
|
+
comm100-accounts login agent@example.com --domain=livechat6dash.testing.comm100dev.io
|
|
63
|
+
comm100-accounts login agent@example.com --cookie
|
|
64
|
+
|
|
65
|
+
comm100-accounts tokens
|
|
66
|
+
comm100-accounts tokens --domain=livechat6dash.testing.comm100dev.io --siteId=10010
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
`list` prints unique rows ordered by dashboard, then site id, then email, in `siteId`, email, domain
|
|
70
|
+
columns. Site IDs are padded to 12 characters and emails to 30 characters, separated by a space;
|
|
71
|
+
longer values are preserved. Site ids compare as numbers (site 9999 before site 10002). Unknown sites
|
|
72
|
+
or domains appear as `-` and sort last. Domain and site filters are optional. `login` prints only
|
|
73
|
+
the JWT, so it can be captured with `token="$(comm100-accounts login agent@example.com)"`. Errors go
|
|
74
|
+
to stderr with a nonzero exit status.
|
|
75
|
+
|
|
76
|
+
`login --cookie` prints a single cookie string that can be passed directly to curl's `-b` option:
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
curl -b "$(comm100-accounts login agent@example.com --siteId=10010 --cookie)" \
|
|
80
|
+
'https://livechat6dash.testing.comm100dev.io/api/global/siteActivity:active'
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
The output is `token_<siteId>=<jwt>; tokenExpire_<siteId>=<encoded-expiry>; siteList=<siteId>`.
|
|
84
|
+
The two token cookies hold the JWT and its URL-encoded expiry; `siteList` identifies the selected
|
|
85
|
+
site, matching the extension's session cookies. The expiry comes from the login response, or the
|
|
86
|
+
JWT's `exp` claim when no server expiry was supplied. No `Cookie:` prefix or cookie attributes are
|
|
87
|
+
printed. This option reuses the same JWT cache and leaves browser cookies untouched.
|
|
88
|
+
|
|
89
|
+
`list --show-password` inserts the saved password as the third column, after email, padded to
|
|
90
|
+
20 characters. It uses the same password as `login`: verified password first, otherwise the pending
|
|
91
|
+
password. Missing passwords appear as `-`; longer passwords are printed in full.
|
|
92
|
+
|
|
93
|
+
An email or explicit site ID is usually enough to select an account. If the email matches multiple
|
|
94
|
+
site IDs across different dashboards, the default domain is `${NAMESPACE}dash.testing.comm100dev.io`.
|
|
95
|
+
For example, `NAMESPACE=livechat6 comm100-accounts login agent@example.com` selects the livechat6
|
|
96
|
+
dashboard. A unique match does not require `NAMESPACE`. Explicit `--domain` always takes precedence;
|
|
97
|
+
multiple sites remaining on the same dashboard require `--siteId`. Among records for the same site,
|
|
98
|
+
the most recently used account is preferred.
|
|
99
|
+
|
|
100
|
+
Setup discovers this extension in Chrome's `Default` profile. Use
|
|
101
|
+
`comm100-accounts setup --profile="/path/to/Profile 1"` to configure another profile, or
|
|
102
|
+
`--db="/path/to/Local Extension Settings/EXTENSION_ID"` to select its LevelDB directory for setup.
|
|
103
|
+
Use the same `--profile` or `--db` on subsequent commands to select that configured connection.
|
|
104
|
+
The service worker owns all storage writes and serializes them with account updates, avoiding
|
|
105
|
+
Chrome's LevelDB lock and stale in-memory data.
|
|
106
|
+
|
|
107
|
+
CLI login reuses a fresh JWT from the shared cache. Otherwise it attempts credential login once and
|
|
108
|
+
asks the service worker to save the result. Browser login can reuse that same JWT to restore session
|
|
109
|
+
cookies; successful browser sign-ins also update the shared cache. Both the JWT's `exp` and the
|
|
110
|
+
server's expiry time are checked, tokens too close to expiry are renewed, and a server-rejected JWT
|
|
111
|
+
is removed from the cache. Two-factor accounts require browser sign-in. CLI login does not change
|
|
112
|
+
browser cookies, and failed credentials are never automatically retried.
|
|
113
|
+
|
|
114
|
+
`tokens` prints all shared cached JWTs, including expired tokens, as a formatted JSON array ordered
|
|
115
|
+
by dashboard, site id, and email. Each entry includes the full JWT, account scope, cache save time,
|
|
116
|
+
server expiry, `fresh` status, decoded `header` and `payload`, and `claimTimes` with UTC dates for
|
|
117
|
+
numeric `iat`, `nbf`, and `exp` claims. Nested payload data is preserved. Decoding does not verify the
|
|
118
|
+
signature; `fresh` uses the same local expiry checks as `login`. A malformed JWT is listed with a
|
|
119
|
+
`decodeError` so other entries can still be inspected. Use `--domain` and/or `--siteId` to filter.
|
|
120
|
+
An empty cache prints `[]`. The command only reads from the worker and never signs in or renews tokens.
|
|
121
|
+
|
|
122
|
+
### Publishing npm releases
|
|
123
|
+
|
|
124
|
+
`npm pack` and `npm publish` build `extension/` from the current browser source files. The npm
|
|
125
|
+
package includes that ready-to-load extension, CLI runtime modules and the install message.
|
|
126
|
+
Tests, development tools, local account data and `node_modules` are excluded from the archive.
|
|
127
|
+
|
|
128
|
+
The first release is published with `npm login --auth-type=web` followed by `npm publish`.
|
|
129
|
+
Then configure **Trusted publishing** in the npm package settings with:
|
|
130
|
+
|
|
131
|
+
- Provider: GitHub Actions
|
|
132
|
+
- Repository owner: `chendesheng`
|
|
133
|
+
- Repository: `Comm100Login`
|
|
134
|
+
- Workflow filename: `publish-npm.yml`
|
|
135
|
+
- Allowed action: publish
|
|
136
|
+
|
|
137
|
+
The workflow uses GitHub's OIDC identity and requires no npm token secret. Future releases run
|
|
138
|
+
automatically when a version tag is pushed:
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
npm version patch
|
|
142
|
+
git push origin main --follow-tags
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
The tag must match the package version, such as `v0.0.2`. CI installs dependencies, runs the test
|
|
146
|
+
suite, builds the extension during packing, and publishes the package with public access.
|
|
147
|
+
|
|
148
|
+
## How it works
|
|
149
|
+
|
|
150
|
+
Sign-in happens in the service worker. Three requests, verified live against
|
|
151
|
+
`livechat6dash.testing.comm100dev.io`:
|
|
152
|
+
|
|
153
|
+
| # | Request | Purpose |
|
|
154
|
+
|---|---------|---------|
|
|
155
|
+
| 1 | `GET https://{branch}route.testing.comm100dev.io/api/v1/router/agents/getByEmail?email=…` | which `siteId` / `portalUrl` this email belongs to |
|
|
156
|
+
| 2 | `POST {portalUrl}/api/login/jwtToken/login` — `{ email, password, siteId }` | the JWT |
|
|
157
|
+
| 3 | `POST {portalUrl}/api/login/accounts/CrossPlatformLogin` — `{ token, siteId }` | turns the JWT into the session cookie |
|
|
158
|
+
|
|
159
|
+
Step 3 responds with `Set-Cookie`, and the extension **then re-reads the cookie and plants it
|
|
160
|
+
explicitly** (`chrome.cookies.set`) if it is missing — extension-initiated `fetch` is not guaranteed
|
|
161
|
+
to persist `Set-Cookie`, so the outcome is verified rather than assumed.
|
|
162
|
+
|
|
163
|
+
Once the cookie exists, the normal navigation you asked for simply renders authenticated. The sign-in
|
|
164
|
+
page is never opened, filled, or clicked.
|
|
165
|
+
|
|
166
|
+
### Intercepting API requests
|
|
167
|
+
|
|
168
|
+
At `document_start`, a script in the page's main JavaScript world wraps `fetch` and asynchronous
|
|
169
|
+
`XMLHttpRequest`. For same-origin `/api/*` requests, it waits for the service worker to obtain a
|
|
170
|
+
usable JWT before sending, then replaces (or adds) `Authorization: Bearer …`. This also covers a
|
|
171
|
+
page holding an expired token in memory, or a cookie deleted immediately after the last login.
|
|
172
|
+
Concurrent requests for the same dashboard and site share a login attempt.
|
|
173
|
+
|
|
174
|
+
If the server returns HTTP 401 despite a locally valid token, the hook renews it and retries once
|
|
175
|
+
before delivering the response to the app. It preserves the method, body and other request headers;
|
|
176
|
+
403 responses are passed through. Login endpoints, other origins and synchronous XHR are excluded.
|
|
177
|
+
Without a usable saved credential, the original request proceeds so the app can handle sign-in.
|
|
178
|
+
Explicit sign-out suppression, temporary retry cooldowns and renewal limits still apply.
|
|
179
|
+
|
|
180
|
+
The content script relays these requests to the worker; passwords are never sent to the page hook.
|
|
181
|
+
Background cookie monitoring and header rules remain as backstops, but cannot themselves pause a
|
|
182
|
+
page request while signing in. After changing the extension, reload it in `chrome://extensions`
|
|
183
|
+
and refresh existing dashboard tabs so they load the request hook.
|
|
184
|
+
|
|
185
|
+
Run `npm test` for the offline background and request-hook checks. These checks stub Chrome and
|
|
186
|
+
network boundaries; live dashboard behavior still needs verification after reloading the extension.
|
|
187
|
+
|
|
188
|
+
### Static files rejected because the Cookie header is too large
|
|
189
|
+
|
|
190
|
+
HTML pages, scripts, stylesheets, images, fonts and media load outside the app's API hooks. The
|
|
191
|
+
worker watches their GET responses. An HTML 400 response with a Cookie header over a conservative
|
|
192
|
+
4 KiB budget, or HTTP 431, triggers cleanup: expired JWT sessions first, then the oldest JWTs, along
|
|
193
|
+
with their `tokenExpire_*` cookies. The current page's site is kept; without a known site, the newest
|
|
194
|
+
token is kept. Saved accounts and unrelated cookies are preserved.
|
|
195
|
+
|
|
196
|
+
After successful cleanup, the extension reloads the affected tab so Chrome fetches the static files
|
|
197
|
+
again. A persistent guard allows at most one automatic reload per tab and dashboard per minute,
|
|
198
|
+
including when several assets fail together or the worker restarts. Watchdog and keep-alive tasks
|
|
199
|
+
do not restore evicted sessions; opening an evicted site can sign it in again normally.
|
|
200
|
+
SPA route and fragment changes within the same document do not prevent recovery. Navigating to
|
|
201
|
+
another document skips the reload without consuming the retry, and a rejected reload releases its
|
|
202
|
+
retry guard. The activity log distinguishes reload requests, skipped reloads and reload failures.
|
|
203
|
+
|
|
204
|
+
The worker cannot read response bodies, so it uses status, content type and measured cookie size
|
|
205
|
+
instead of matching Nginx's error text. The actual ingress limit is server-specific. If the current
|
|
206
|
+
JWT or unrelated cookies alone exceed that limit, removing older JWTs cannot solve it. Cleanup and
|
|
207
|
+
byte counts appear as `cookie-budget` entries in the popup's activity log. Reloading the page can
|
|
208
|
+
discard unsaved state.
|
|
209
|
+
|
|
210
|
+
### Credentials belong to a dashboard
|
|
211
|
+
|
|
212
|
+
Every Comm100 test branch shares the `comm100dev.io` cookie domain, but they are **separate
|
|
213
|
+
deployments with separate databases**. An account that exists on `livechat3dash` usually does not
|
|
214
|
+
exist on `livechat6dash` (and when the same address does exist, it is a different `siteId`). Firing
|
|
215
|
+
one dashboard's password at another therefore does nothing except spend attempts against the
|
|
216
|
+
**lockout threshold**.
|
|
217
|
+
|
|
218
|
+
So an account is identified by **email *and* dashboard**, and `pickAccount()` never crosses one:
|
|
219
|
+
|
|
220
|
+
- `src/shared/platforms.js` holds the dashboard list (`wanTestPlatformsPrefixes`,
|
|
221
|
+
`lanTestPlatformsPrefixes`, `productionPlatformsPrefixes` — the same names the web client uses).
|
|
222
|
+
The "domain" of an account is that entry's host, **including the port** where there is one, so
|
|
223
|
+
`http://localhost:34500` and `https://localhost:3000` are different dashboards.
|
|
224
|
+
- The popup has no dashboard picker. A credential is recorded on the dashboard it was typed into (the
|
|
225
|
+
capture path), and the account list's dashboard filter is built from the dashboards that actually
|
|
226
|
+
have credentials — so `livechat3dash.testing.comm100dev.io` and `livechat3dash.comm100dev.io` list as
|
|
227
|
+
separate entries there.
|
|
228
|
+
- `livechat3dash.testing.comm100dev.io` (WAN) and `livechat3dash.comm100dev.io` (LAN) are *different*
|
|
229
|
+
entries on purpose: same name, different deployments.
|
|
230
|
+
- Signing in is a property of the account, not of the tab you are looking at, so a row's **Login
|
|
231
|
+
now** works from any page — it signs in to *that account's* dashboard and opens it in a new tab.
|
|
232
|
+
- Accounts saved before this existed are pinned to the first host they were ever used on
|
|
233
|
+
(`hosts[0]`) — that is where the credential was introduced, and anything appended later was a
|
|
234
|
+
successful sign-in. An account with no host history stays deliberately unscoped (a wildcard), which
|
|
235
|
+
is what an imported file expects; the picker asks for a dashboard on the next edit.
|
|
236
|
+
|
|
237
|
+
### Which site? (`getByEmail`, never the URL)
|
|
238
|
+
|
|
239
|
+
A dashboard can serve **several sites** — `livechat6dash.testing.comm100dev.io` serves site 10010 to
|
|
240
|
+
one agent and 10012 to another — and a URL can name any of them. The panel's URL is therefore a
|
|
241
|
+
*claim*, not a fact, and it is never trusted:
|
|
242
|
+
|
|
243
|
+
- Which site an email uses on a dashboard comes from the route service (`getByEmail`) — the same call
|
|
244
|
+
that resolves `portalUrl`. Nobody types a site id anywhere; the popup has no field for it.
|
|
245
|
+
- A site learned from a successful sign-in is cached on the account and shown in the list as
|
|
246
|
+
`site 10010`. That cache makes the common case free: no route call when the URL agrees with it.
|
|
247
|
+
- When the URL names a site the credential disagrees with, the route service is asked. Owns it → sign
|
|
248
|
+
in for it. Does not own it → **nothing is attempted**; the dashboard shows its own sign-in page and
|
|
249
|
+
you sign in by hand (that credential is remembered, and its site comes from the same call).
|
|
250
|
+
- A captured credential records **no** site. A claim from the address bar is not evidence, and a
|
|
251
|
+
wrong site remembered as fact would skip exactly the check that keeps a rejected session from being
|
|
252
|
+
planted.
|
|
253
|
+
|
|
254
|
+
That distinction is the whole fix for "it keeps jumping to login": the sign-in API will happily issue
|
|
255
|
+
a token for a site the account does not belong to, and the panel then rejects it on every call and
|
|
256
|
+
reloads itself. Refusing to guess is what makes it stop.
|
|
257
|
+
|
|
258
|
+
A dashboard that has no route service of its own — `dash11.comm100staging.com`, and the local
|
|
259
|
+
dashboard — is routed to itself (`getByEmail` on its own origin). That is a best-effort fallback,
|
|
260
|
+
not a verified convention like `{branch}dash` → `{branch}route`. **Neither has been verified live.**
|
|
261
|
+
|
|
262
|
+
### The session cookie
|
|
263
|
+
|
|
264
|
+
Discovered from the `Set-Cookie` headers of step 3:
|
|
265
|
+
|
|
266
|
+
```
|
|
267
|
+
siteList=10012; Domain=comm100dev.io; Path=/; Secure; SameSite=Strict; HttpOnly
|
|
268
|
+
token_10012=<jwt>; Domain=comm100dev.io; Path=/; Secure; SameSite=Strict; HttpOnly
|
|
269
|
+
tokenExpire_10012=<url-encoded ISO>; Domain=comm100dev.io; Path=/; Secure; SameSite=Strict
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
`token_{siteId}` **is the JWT cookie**. `tokenExpire_{siteId}` is a session cookie and is not HttpOnly.
|
|
273
|
+
|
|
274
|
+
### Which site is this tab? (important)
|
|
275
|
+
|
|
276
|
+
Every Comm100 test branch shares the **`comm100dev.io`** cookie domain, so a `token_*` cookie for
|
|
277
|
+
one branch says *nothing* about another. A leftover `token_10012` from `livechat6dash` must never be
|
|
278
|
+
read as "livechat3dash is signed in".
|
|
279
|
+
|
|
280
|
+
So the extension takes the site from the URL instead:
|
|
281
|
+
|
|
282
|
+
```
|
|
283
|
+
/ui/10008/livechat/dashboard/ -> site 10008, and only token_10008 counts
|
|
284
|
+
/login/?retUrl=…%2Fui%2F10008%2F… -> site 10008, read out of retUrl
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
With the site known, the route service is not consulted at all — one fewer round trip, and no chance
|
|
288
|
+
of confusing branches. The route lookup only runs when the URL does not name a site.
|
|
289
|
+
|
|
290
|
+
### Never showing the sign-in page
|
|
291
|
+
|
|
292
|
+
The control panel is a SPA that decides whether it is signed in only *after* loading its bundle.
|
|
293
|
+
Three things keep the form off the screen:
|
|
294
|
+
|
|
295
|
+
1. **Keep-alive.** An alarm every 15 minutes renews any session with less than 30 minutes left
|
|
296
|
+
(the JWT lives ~2 h). In normal browsing the cookie is therefore always valid and the app never
|
|
297
|
+
considers routing to `/login` at all.
|
|
298
|
+
2. **Replenish on deletion.** A `cookies.onChanged` listener puts `token_{siteId}` straight back
|
|
299
|
+
when it disappears — so deleting the cookie by hand is a non-event rather than a countdown to
|
|
300
|
+
the next bounce. A deliberate sign-out is respected: the worker ignores removals that follow a
|
|
301
|
+
trip to `/login`, and a `logout` message from the popup (no longer wired to a button, but still
|
|
302
|
+
handled) suppresses it entirely.
|
|
303
|
+
3. **Start before the document.** `webNavigation.onBeforeNavigate` kicks off the sign-in as soon as
|
|
304
|
+
the navigation is known, and the cookie is planted the moment `jwtToken/login` returns —
|
|
305
|
+
`CrossPlatformLogin` is fire-and-forget, off the critical path. The control panel has to fetch and
|
|
306
|
+
parse its bundle before it checks, so this usually wins outright.
|
|
307
|
+
4. **Watch the SPA's own routing.** The control panel can move to `/login` without loading a
|
|
308
|
+
document, so `pushState`/`replaceState`/`popstate` are watched too; otherwise a session that
|
|
309
|
+
vanishes mid-session leaves the sign-in page sitting there.
|
|
310
|
+
5. **Cover the form** as a last resort. If the tab does land on `/login`, the content script first
|
|
311
|
+
asks a local-only pre-flight whether a sign-in is genuinely about to happen, and only then hides
|
|
312
|
+
`#login` behind a short "Signing in…" notice — removed the instant the attempt resolves, so a
|
|
313
|
+
refusal reveals the form and a success navigates away.
|
|
314
|
+
|
|
315
|
+
The pre-flight deliberately mirrors *every* gate the real attempt applies (no account, suppressed
|
|
316
|
+
after sign-out, still cooling down, site unresolvable). If it disagrees with
|
|
317
|
+
the attempt, the form would be hidden for nothing — a worse flicker than the one being prevented.
|
|
318
|
+
|
|
319
|
+
### Acting as middleware on a rejected request
|
|
320
|
+
|
|
321
|
+
The wanted behaviour is "catch the 401, sign in again, update the cookie and resend". In MV3 the
|
|
322
|
+
first three are possible and the fourth is not:
|
|
323
|
+
|
|
324
|
+
| Step | Possible? |
|
|
325
|
+
|---|---|
|
|
326
|
+
| Read the token out of the request **before it is sent** | ✅ `webRequest.onBeforeSendHeaders` |
|
|
327
|
+
| Decide whether that token is still valid | ✅ decode the JWT's own `exp` claim |
|
|
328
|
+
| Sign in again and plant a fresh cookie | ✅ |
|
|
329
|
+
| Rewrite the outgoing `Cookie` header, or resend the request | ❌ a worker cannot modify or replay a page's request — blocking `webRequest` is enterprise-force-install only, and `declarativeNetRequest` has no cookie action |
|
|
330
|
+
|
|
331
|
+
Two hooks cover the gap:
|
|
332
|
+
|
|
333
|
+
**Pre-flight (before the request).** `onBeforeSendHeaders` sees the actual outgoing `Cookie` header
|
|
334
|
+
for every `/api/` call. The token is decoded and checked against its own `exp` claim — the JWT is the
|
|
335
|
+
authority, not the cookie expiry, because the cookie's copy can outlive the token it carries.
|
|
336
|
+
`hasSession` applies the same rule, so a live cookie holding an expired JWT no longer counts as a
|
|
337
|
+
session. If the token is missing or within 60 s of expiry, the sign-in starts **there** — a full
|
|
338
|
+
round trip before the 401 could tell us. The sign-in endpoints (`/api/login/…`) are excluded, since
|
|
339
|
+
they carry no session and must not be read as evidence of a bad one.
|
|
340
|
+
|
|
341
|
+
**Response (after the request).** The offending request cannot be replayed, and the obvious next
|
|
342
|
+
move — reloading the page — is deliberately **not** taken: it throws away whatever the user was
|
|
343
|
+
doing. Instead the worker re-authenticates and then does nothing to the tab. The fresh cookie is in
|
|
344
|
+
the jar, so the panel's *next* request simply succeeds, and the user retries when they are ready
|
|
345
|
+
rather than when we decide.
|
|
346
|
+
|
|
347
|
+
Bounds on that re-authentication:
|
|
348
|
+
|
|
349
|
+
- a budget of 3 per host per minute, so an endpoint rejecting for a reason signing in cannot fix
|
|
350
|
+
gives up rather than looping;
|
|
351
|
+
- 2 s coalescing, so a burst of failing calls causes one sign-in;
|
|
352
|
+
- nothing at all for a keep-alive renewal, where the page was never broken.
|
|
353
|
+
|
|
354
|
+
**Nothing in this extension reloads a working page.** The *only* remaining way anything moves a tab
|
|
355
|
+
is the content script leaving `/login` for `retUrl` — the recovery from a bounce, not a reload. An
|
|
356
|
+
earlier revision reloaded the tab that produced a 401; that was removed, because yanking the page
|
|
357
|
+
away throws out whatever the user was doing, and with the CDP path the token is attached before the
|
|
358
|
+
request leaves so a 401 should not arise in the first place.
|
|
359
|
+
|
|
360
|
+
This is a backstop, not the main defence — the keep-alive renews long before the JWT lapses, so a
|
|
361
|
+
401 should mean the server rejected a cookie that had not yet expired (revocation, a password
|
|
362
|
+
change, or the JWT's `thumbprint` binding) rather than plain expiry.
|
|
363
|
+
|
|
364
|
+
### Dead end: force-installing for webRequestBlocking
|
|
365
|
+
|
|
366
|
+
Tried, and closed by the platform. Force-installing was meant to grant `webRequestBlocking` so the
|
|
367
|
+
worker could hold a navigation while signing in. **Chrome refuses this on a Mac that is not
|
|
368
|
+
MDM-enrolled:**
|
|
369
|
+
|
|
370
|
+
```
|
|
371
|
+
ExtensionSettings [BLOCKED]gpfpnldaoihmfpjcefhbfmkbdccngebd
|
|
372
|
+
Warning: This computer is not detected as enterprise managed so policy can only
|
|
373
|
+
automatically install extensions hosted on the Chrome Webstore.
|
|
374
|
+
Warning: Schema validation error: Unknown property: [BLOCKED]<id>
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
That is a built-in Chromium string, not a misconfiguration — off-store automatic installs require a
|
|
378
|
+
genuinely managed device, because otherwise anything that can write that policy file could silently
|
|
379
|
+
install an extension with full network interception. The artifacts were correct (the derived
|
|
380
|
+
extension ID matched the key inside the CRX3 header and the policy passed `plutil -lint`); the
|
|
381
|
+
platform simply would not accept them. The packaging script and the `webRequestBlocking` code path
|
|
382
|
+
have since been deleted rather than left behind, since neither could ever run.
|
|
383
|
+
|
|
384
|
+
A `chrome.debugger` + CDP `Fetch` implementation was written next, on the reasoning that a paused
|
|
385
|
+
request can be resumed with rewritten headers without any policy. It was also removed: it cannot
|
|
386
|
+
attach while **DevTools is open** — which is exactly how a cookie gets cleared by hand — and when it
|
|
387
|
+
failed it said so only on a console nobody was reading. The `declarativeNetRequest` approach below
|
|
388
|
+
needs no attach, no permission prompt worth worrying about, and no policy.
|
|
389
|
+
|
|
390
|
+
### Attaching the session to the panel's API calls
|
|
391
|
+
|
|
392
|
+
The control panel authenticates with **`Authorization: Bearer <token>`** — its repositories call
|
|
393
|
+
`sendHttpRequest({ auth: this.token })`. That is why re-planting the cookie alone does nothing: the
|
|
394
|
+
panel goes on sending whatever token it already had, every call 401s, and *that* is what makes it
|
|
395
|
+
reload itself. Cookie state is irrelevant to those calls.
|
|
396
|
+
|
|
397
|
+
So a `declarativeNetRequest` rule substitutes the header on the way out:
|
|
398
|
+
|
|
399
|
+
```js
|
|
400
|
+
{ id, priority: 1, action: { type: 'modifyHeaders', requestHeaders: [
|
|
401
|
+
{ header: 'authorization', operation: 'set', value: `Bearer ${jwt}` } ] },
|
|
402
|
+
condition: { urlFilter: `||${host}/api/`, resourceTypes: ['xmlhttprequest'] } }
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
Plus a higher-priority `allow` rule for `||{host}/api/login/`, so the sign-in calls are never handed
|
|
406
|
+
a stale bearer. The rule is republished whenever a session is established or renewed, and dropped on
|
|
407
|
+
install/startup (dynamic rules outlive the service worker, the bookkeeping that owns them does not).
|
|
408
|
+
|
|
409
|
+
Only `/api/` XHRs are touched. Navigations are never paused, held, or rewritten — that was tried and
|
|
410
|
+
removed, because it was the one change capable of making Chrome or the server respond unexpectedly.
|
|
411
|
+
|
|
412
|
+
**Two earlier attempts, both dead ends worth recording:**
|
|
413
|
+
|
|
414
|
+
- *Policy-installed extension for `webRequestBlocking`* — Chrome refuses off-store force-install on
|
|
415
|
+
a machine that is not MDM-enrolled.
|
|
416
|
+
- *`chrome.debugger` + CDP `Fetch`* — cannot attach while **DevTools is open**, which is exactly how
|
|
417
|
+
a cookie gets cleared by hand, and it failed silently. DNR needs no attach, shows no infobar, and
|
|
418
|
+
works whether or not DevTools is running.
|
|
419
|
+
|
|
420
|
+
### Activity log
|
|
421
|
+
|
|
422
|
+
The worker keeps a rolling log of what it decided and why — `cookie-removed`, `sign-in ->
|
|
423
|
+
ok`/`-> bad-credentials`/`-> api-error`, `header-rule -> Authorization attached to …`, `401`, and `redirect: login page -> …`
|
|
424
|
+
when the content script leaves `/login`.
|
|
425
|
+
|
|
426
|
+
It exists because "it still reloads" was being diagnosed by guesswork across several rounds. That log
|
|
427
|
+
is what finally showed the truth: dozens of `/api/` calls returning 401 with **no interception
|
|
428
|
+
happening at all** — the panel reloading itself in reaction to them, not the extension reloading
|
|
429
|
+
anything. Capped at 40 entries in `chrome.storage.local`; it never influences behaviour.
|
|
430
|
+
|
|
431
|
+
The popup no longer renders the log — **Export activity** in the popup footer writes it to
|
|
432
|
+
`comm100-auto-login-activity.json` instead.
|
|
433
|
+
|
|
434
|
+
### Route host mapping
|
|
435
|
+
|
|
436
|
+
```
|
|
437
|
+
livechat6dash.testing.comm100dev.io -> livechat6route.testing.comm100dev.io
|
|
438
|
+
*.comm100.io -> route.comm100.io
|
|
439
|
+
any other listed dashboard -> itself (staging, http://localhost:34500)
|
|
440
|
+
anything else -> not supported, nothing happens
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
## Install
|
|
444
|
+
|
|
445
|
+
1. Open `chrome://extensions`.
|
|
446
|
+
2. Turn on **Developer mode**.
|
|
447
|
+
3. **Load unpacked** → select this folder.
|
|
448
|
+
4. Reload any already-open Comm100 tab once so the content script is injected.
|
|
449
|
+
5. After pulling a change to `host_permissions` or `content_scripts`, hit **Reload** on the extension
|
|
450
|
+
card there as well — the service worker keeps the old manifest until you do.
|
|
451
|
+
|
|
452
|
+
The extension itself has no build step and no runtime dependencies: load the folder unpacked and it
|
|
453
|
+
works. The one generated file is the popup's stylesheet — see **Styling** below.
|
|
454
|
+
|
|
455
|
+
### Styling
|
|
456
|
+
|
|
457
|
+
The popup is styled with Tailwind. Tailwind's browser build (the `cdn.jsdelivr.net` `<script>` from
|
|
458
|
+
the Tailwind docs) **cannot** be used here: an MV3 extension page is served with `script-src 'self'`
|
|
459
|
+
and Chrome rejects remote script origins, so the CDN script would be blocked and the popup would
|
|
460
|
+
render unstyled. The stylesheet is therefore compiled at development time and committed:
|
|
461
|
+
|
|
462
|
+
```
|
|
463
|
+
npm install
|
|
464
|
+
npm run build:css # src/popup/tailwind.input.css -> src/popup/tailwind.css
|
|
465
|
+
npm run watch:css # same, rebuilding on change
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
`tailwind.css` is a static file the popup links directly, so nothing about this is needed to *use* the
|
|
469
|
+
extension — only to change its styling. Class names are read out of `src/popup/popup.html` and
|
|
470
|
+
`src/popup/popup.js` (see the `@source` lines), so a class the script builds at runtime must appear
|
|
471
|
+
literally in one of those files, or Tailwind will never generate it.
|
|
472
|
+
|
|
473
|
+
### Icons
|
|
474
|
+
|
|
475
|
+
`src/icons/icon.png` is the source artwork; the three sizes the manifest references are rendered from
|
|
476
|
+
it and committed:
|
|
477
|
+
|
|
478
|
+
```
|
|
479
|
+
pip install pillow
|
|
480
|
+
python3 tools/make-icons.py # -> src/icons/icon16.png, icon48.png, icon128.png
|
|
481
|
+
python3 tools/make-icons.py --keep-background
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
The render is a single LANCZOS pass over the square source — no cropping, no staged downscale, because
|
|
485
|
+
LANCZOS already scales its kernel to the reduction ratio.
|
|
486
|
+
|
|
487
|
+
A toolbar icon needs a transparent surround, and exports sometimes come out flattened onto an opaque
|
|
488
|
+
near-white page instead. So when the source is fully opaque the script locates the rounded tile drawn
|
|
489
|
+
in the artwork and knocks out everything outside it; when the source already has alpha it is used
|
|
490
|
+
exactly as drawn and nothing is inferred. `--keep-background` opts out for artwork that is genuinely a
|
|
491
|
+
full square.
|
|
492
|
+
|
|
493
|
+
Two caveats on that knockout, since it is a reconstruction rather than something read from the file:
|
|
494
|
+
|
|
495
|
+
- it clips the tile's soft drop shadow along with the page, and assumes all four corners share one
|
|
496
|
+
radius, inferred from where the outline stops curving;
|
|
497
|
+
- the edge threshold (`EDGE_THRESHOLD` in the script) is tuned to this artwork — the faintest run of
|
|
498
|
+
the tile outline reads ~22 against the page while the shadow just outside reaches ~16, so there is
|
|
499
|
+
not much room. If a future export has a softer edge, check the result on a dark background before
|
|
500
|
+
trusting it.
|
|
501
|
+
|
|
502
|
+
At 16 px the three elements (C, figure, padlock) are near the limit of what that size can carry
|
|
503
|
+
regardless; a simplified small variant would be a change to the artwork, not to this script.
|
|
504
|
+
|
|
505
|
+
## Use
|
|
506
|
+
|
|
507
|
+
There is no "sign in automatically" switch. Signing in *is* the extension, so the off switch is
|
|
508
|
+
disabling the extension in `chrome://extensions`. A deliberate sign-out suppresses sign-in for
|
|
509
|
+
5 minutes and shows a **Re-enable auto-login** button. Failed sign-ins leave the login form available
|
|
510
|
+
and retry automatically after the cooldown; they never require manual re-enabling. Permanent pause
|
|
511
|
+
flags saved by older versions are ignored and removed by the next normal storage write.
|
|
512
|
+
|
|
513
|
+
- **Login now** (per account) — signs in with that credential and opens
|
|
514
|
+
`https://{dashboard}/ui/{siteId}/entry/?fromlogin=1` — the panel's own post-sign-in path, which then
|
|
515
|
+
hands off to the site's landing page — in a **new tab**. The current tab is left alone: it is
|
|
516
|
+
usually the `/login` page (or another site's dashboard) that prompted the click, and taking it away
|
|
517
|
+
loses that context. The site is the one the route service gives for that email, so the button is
|
|
518
|
+
unaffected by which site the current URL happens to name. If the sign-in fails, no tab is opened
|
|
519
|
+
and the reason is shown in the popup's status line instead.
|
|
520
|
+
- **Open a Comm100 dashboard** — you are signed in before the page needs the session.
|
|
521
|
+
- **Show / Edit / Delete** per account. The account list is ordered by dashboard, then site id, then
|
|
522
|
+
email — the same order `comm100-accounts list` prints, so the newest sign-in no longer reshuffles
|
|
523
|
+
it — and each row shows the dashboard it belongs to and the site id it resolved to.
|
|
524
|
+
- **Edit** turns the row into its address and password fields, and *Save & verify* calls the login API
|
|
525
|
+
so a replacement password is only trusted once it is proven — a typo is caught immediately rather
|
|
526
|
+
than at the next sign-in. There is no dashboard or site field: an account is identified by
|
|
527
|
+
(address, dashboard), so signing in on another dashboard records that credential on its own, and the
|
|
528
|
+
site is always the route service's answer.
|
|
529
|
+
- **Dashboard / site filters** above the account list. Once more than a handful of credentials are
|
|
530
|
+
saved, the question is usually "which of these is the one for site 10010 on livechat6dash?", and
|
|
531
|
+
the two selects answer it without reading every row. The site choices follow the dashboard choice
|
|
532
|
+
(a site id only means something inside one dashboard), `any dashboard` / `any site` are filterable
|
|
533
|
+
values for credentials that have not been scoped yet, and anything the filters hide is stated as
|
|
534
|
+
`showing N of M` rather than left to look like an empty list. The filters live in the popup's
|
|
535
|
+
memory only: reopening the popup always shows every account again.
|
|
536
|
+
- **Export / Import** — the whole saved state, or **Export activity** for just the worker's rolling
|
|
537
|
+
log (`comm100-auto-login-activity.json`), which the popup itself no longer renders.
|
|
538
|
+
|
|
539
|
+
There is deliberately no **Add account** form. A credential is recorded by *using* it: sign in through
|
|
540
|
+
the panel's own form (for example when a second factor is required) and the typed credentials are
|
|
541
|
+
recorded **against the dashboard you are on**. They are stored as *pending* and promoted to a trusted
|
|
542
|
+
password as soon as something proves they work — a live session, or a later successful sign-in.
|
|
543
|
+
**Capture never calls the login API**, precisely so it cannot burn sign-in attempts (see *Lockout
|
|
544
|
+
safety*). *Edit* is the way back to a credential that was recorded wrong; *Import* is the way to
|
|
545
|
+
restore a saved set.
|
|
546
|
+
|
|
547
|
+
## How logout really works
|
|
548
|
+
|
|
549
|
+
Removing the cookies **is** the logout. The control panel decides whether it is signed in purely by
|
|
550
|
+
looking for `token_{siteId}`, so once the cookies are gone the next navigation lands on `/login`.
|
|
551
|
+
|
|
552
|
+
`POST /api/login/accounts/logout` exists, but it belongs to the login service and rejects a
|
|
553
|
+
JWT-cookie session with `401 AuthenticationFailed` — verified against a live session, with and
|
|
554
|
+
without `Origin`/`Referer` headers. The extension therefore attempts it best-effort and **never
|
|
555
|
+
depends on it**; cookie removal is what does the work.
|
|
556
|
+
|
|
557
|
+
One consequence worth knowing: the JWT itself stays valid until its `exp` claim (roughly 2 hours)
|
|
558
|
+
even after sign-out, so this is a client-side sign-out, not server-side revocation.
|
|
559
|
+
|
|
560
|
+
## Lockout safety
|
|
561
|
+
|
|
562
|
+
Comm100 locks an account once failed sign-ins exceed the site's password policy, and per
|
|
563
|
+
[Comm100's own troubleshooting article](https://help.comm100.com/why-am-i-receiving-account-locked-error-message-when-i-login)
|
|
564
|
+
the lock clears after **30 minutes** or when a site administrator unlocks it. The lockout arrives as
|
|
565
|
+
`errorCode 114`, not a wrong-password error.
|
|
566
|
+
|
|
567
|
+
Because of that the extension is deliberately conservative:
|
|
568
|
+
|
|
569
|
+
- a **60 s** cooldown after a failed attempt, including attempts triggered by outgoing requests;
|
|
570
|
+
- **form capture does not verify** — the user has just spent one sign-in attempt, and duplicating
|
|
571
|
+
every attempt would halve the budget;
|
|
572
|
+
- `errorCode 114` is reported as-is rather than retried.
|
|
573
|
+
|
|
574
|
+
There is no failure threshold that disables auto-login. The cooldown expires automatically. An
|
|
575
|
+
explicit popup action can retry sooner; deliberate sign-out suppression remains independent.
|
|
576
|
+
|
|
577
|
+
## Testing
|
|
578
|
+
|
|
579
|
+
`test/harness.mjs` stubs the `chrome.*` APIs the extension uses and exercises the real modules.
|
|
580
|
+
Failure paths (bad password, MFA) are answered by a fetch stub so they can never lock a real
|
|
581
|
+
account; the happy path talks to the real API when credentials are supplied.
|
|
582
|
+
|
|
583
|
+
```bash
|
|
584
|
+
# offline: pure logic, no credentials, no network
|
|
585
|
+
node test/harness.mjs
|
|
586
|
+
|
|
587
|
+
# full run including a live background sign-in
|
|
588
|
+
C100_EMAIL='you@example.com' C100_PASSWORD='…' node test/harness.mjs
|
|
589
|
+
```
|
|
590
|
+
|
|
591
|
+
It asserts the planted cookie's exact shape (name, domain, `HttpOnly`, `Secure`, `SameSite`,
|
|
592
|
+
session-vs-persistent), the already-signed-in short-circuit, failure accounting, the MFA handoff,
|
|
593
|
+
cooldown/logout suppression, recency selection, the dashboard list itself (every entry parses to a
|
|
594
|
+
unique scope, WAN and LAN variants of one name stay distinct), that credentials never cross
|
|
595
|
+
dashboards, that a legacy record is pinned to the right one, that a site a credential does not own is
|
|
596
|
+
refused without spending a sign-in attempt, that a live session is recognised *before* any such
|
|
597
|
+
check, and that "nothing to do" outcomes stay out of the activity log. No credentials are stored in
|
|
598
|
+
the file.
|
|
599
|
+
|
|
600
|
+
## Behaviour details
|
|
601
|
+
|
|
602
|
+
- Auto-login backs off for 60 s after a failed attempt per host, then becomes eligible automatically.
|
|
603
|
+
There is no permanent pause after repeated failures. An explicit popup action may retry sooner.
|
|
604
|
+
- A dashboard with no saved account, or an account that has none for the site the URL names, is
|
|
605
|
+
reported as such — the popup says which dashboard and site it *does* have, and the sign-in page is
|
|
606
|
+
left alone for you. Suspicious silences are shown in the popup, not just logged.
|
|
607
|
+
- Repeated 401s cannot re-sign-in without bound: at most 3 in a rolling minute per dashboard, and the
|
|
608
|
+
cap is not reset by its own successes (a loop where every sign-in "works" is the one that needs
|
|
609
|
+
bounding). Leaving the sign-in page is separately capped at 3 bounces per tab per minute, counted in
|
|
610
|
+
`sessionStorage` so it survives the navigation it is counting.
|
|
611
|
+
- Two-factor / forced-2FA accounts cannot be completed headlessly. The extension detects this,
|
|
612
|
+
does **not** count it as a credential failure, and pre-fills the form so you only type the code.
|
|
613
|
+
- Auto-login stays out of the way on `/login/forgotpassword`, `/login/sso`, and reset/2FA links.
|
|
614
|
+
- When a `/login` bounce sends you on, it follows the app's own `retUrl` if there is one; otherwise it
|
|
615
|
+
lands on the same `/ui/{siteId}/entry/?fromlogin=1` path the panel uses after a manual sign-in.
|
|
616
|
+
|
|
617
|
+
## Security
|
|
618
|
+
|
|
619
|
+
Passwords are stored **unencrypted** in `chrome.storage.local` — local to this browser profile,
|
|
620
|
+
never synced, and erasable with **Clear all data**. This is the same trust model as Chrome's own
|
|
621
|
+
password manager storing them on disk. The only network requests the extension makes are the three
|
|
622
|
+
endpoints above plus the logout call, all over HTTPS to the Comm100 host you are already visiting.
|
|
623
|
+
|
|
624
|
+
## Troubleshooting
|
|
625
|
+
|
|
626
|
+
| Symptom | Check |
|
|
627
|
+
|---|---|
|
|
628
|
+
| Nothing happens | The host must map to a route host (see above). Check the service worker console. |
|
|
629
|
+
| "no-route" | The email is unknown to the route service for that branch. |
|
|
630
|
+
| "No saved account for <dashboard>" | The credential is saved under a different dashboard, or its picker entry was never set. Edit the account and pick the right dashboard. |
|
|
631
|
+
| "No saved credential for site N on …" | Something is saved for that dashboard, but for a different site. The route service says which site this address uses here; sign in by hand for the other one and it will be remembered. |
|
|
632
|
+
| "… has X on site N, not site M" | The URL asks for a site this credential does not belong to — typically a site id copied from another branch. Open the site's own URL, or sign in by hand with the account that owns it. |
|
|
633
|
+
| Account exists but is never used | It is scoped to another dashboard on purpose. A dashboard never borrows another's credential. |
|
|
634
|
+
| Session not stored | `chrome://settings/cookies` → the `comm100dev.io` cookies `token_*`, `tokenExpire_*`, `siteList`. |
|
|
635
|
+
| `errorCode 114` | Account locked by password policy — wait 30 minutes or ask a site admin. |
|
|
636
|
+
| Signed out unexpectedly | Another tab or the app itself called logout; auto-login is suppressed for 5 minutes. |
|
|
637
|
+
| Service worker logs | `chrome://extensions` → Details → *Inspect views: service worker*. Logs are prefixed `[Comm100 Auto Login]`. |
|
|
638
|
+
|
|
639
|
+
## Layout
|
|
640
|
+
|
|
641
|
+
```
|
|
642
|
+
manifest.json
|
|
643
|
+
package.json dev-only: the Tailwind build script
|
|
644
|
+
src/shared/api.js login API + host / route mapping
|
|
645
|
+
src/shared/platforms.js the dashboard list credentials are scoped to
|
|
646
|
+
src/shared/cookies.js session cookie read / plant / clear
|
|
647
|
+
src/shared/storage.js chrome.storage.local schema + account lifecycle
|
|
648
|
+
src/background/service-worker.js
|
|
649
|
+
src/content/content.js thin: trigger, redirect, capture, prefill fallback
|
|
650
|
+
src/popup/popup.html account manager UI
|
|
651
|
+
src/popup/popup.js
|
|
652
|
+
src/popup/tailwind.input.css Tailwind source (not loaded by the extension)
|
|
653
|
+
src/popup/tailwind.css built stylesheet (the one the popup links)
|
|
654
|
+
src/icons/icon.png source artwork for the toolbar icons
|
|
655
|
+
src/icons/icon{16,48,128}.png rendered from it by tools/make-icons.py
|
|
656
|
+
tools/make-icons.py renders the icon sizes (dev-only, needs Pillow)
|
|
657
|
+
test/harness.mjs headless verification
|
|
658
|
+
```
|
|
659
|
+
|
|
660
|
+
## Verification status
|
|
661
|
+
|
|
662
|
+
Steps 1–3, every cookie flag, and the already-signed-in short-circuit were executed against the
|
|
663
|
+
live test environment, as were the `errorCode 100/101/114` failure shapes and the `logout` 401. The
|
|
664
|
+
harness additionally proved the planted cookies are genuinely accepted: an authenticated endpoint
|
|
665
|
+
(`/api/global/siteActivity:active`) returns `401` without them and gets past authentication with
|
|
666
|
+
them.
|
|
667
|
+
|
|
668
|
+
End-to-end in Chrome was confirmed too: with `voiceagenttest@comm100.com` saved for
|
|
669
|
+
`livechat3dash.testing.comm100dev.io`, deleting the session cookie and reloading a
|
|
670
|
+
`/login/?retUrl=…%2Fui%2F10008%2Fentry%2F` page landed on the Live Chat dashboard for site 10008
|
|
671
|
+
with no manual interaction, the popup reported `Signed in (token_10008, …)`, and the account's use
|
|
672
|
+
count incremented — a real background sign-in, not a lucky existing session.
|
|
673
|
+
|
|
674
|
+
The `*.comm100.io` production route host is inferred from the same login bundle and was checked for
|
|
675
|
+
reachability only, so if it behaves differently the extension falls back to doing nothing rather
|
|
676
|
+
than guessing.
|
|
677
|
+
|
|
678
|
+
Per-dashboard credential scoping was verified against the real browser, not just the harness: the two
|
|
679
|
+
accounts already saved in Chrome (`voiceagenttest@comm100.com`, `jackapi@livechat6.com`) were written
|
|
680
|
+
before accounts had a dashboard, and both were pinned to the right one by the `hosts[0]` rule and
|
|
681
|
+
picked up their site ids (`10008`, `10010`) from the route cache — visible in the popup with no
|
|
682
|
+
re-entry.
|
|
683
|
+
|
|
684
|
+
The loop itself was diagnosed from the activity log of a real occurrence, not from theory:
|
|
685
|
+
`/ui/10012/ai/dashboard/` on `livechat6dash` (site 10012 belongs to a different agent there) produced
|
|
686
|
+
`401` → `sign-in -> ok` → `header-rule` → `401` on repeat, because the extension was signing in for
|
|
687
|
+
the site *in the URL* and the panel rejected every call made with that token. `getByEmail` confirms
|
|
688
|
+
the account has exactly one site there (`10010`), which is what the fixed code checks before it
|
|
689
|
+
attempts anything. The `dash11.comm100staging.com` and `http://localhost:34500` entries are the local
|
|
690
|
+
full-stack dashboards; their self-routing has **not** been exercised, only their appearance in the
|
|
691
|
+
picker.
|