feat: v4.0.0 — session persistence, PWA push, UI overhaul, Azure hardening#109
Conversation
- Session persistence: ChatStateStore persists chat history to disk, cold resume on reconnect - Tab ID switched to localStorage for browser-close survival - Session watcher: fs.watch with debounce for CLI↔browser autosync - PWA: manifest.json, service worker with precaching + push handling - Push notifications: web-push with VAPID, subscription store, server-side push when WS disconnected - Security: auth guards on all endpoints, token revalidation, CSP hardening, CSRF, SSRF protection - Azure: VNet with 3 subnets, Key Vault, Premium ACR, storage hardening, diagnostics - Docker: new data dirs, VAPID env vars, volume mount updates Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…t-implementation Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
push-singleton and chat-state-singleton were eagerly calling config.* at module import time, causing npm run build to fail with 'Missing required env var: SESSION_SECRET' during SvelteKit's post-build analysis step. Fixed by wrapping both singletons in Proxy with lazy initialization — config is only accessed on first actual method call at runtime. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add new logo SVG (logo-copilot unleashed.svg → logo.svg) with dark gradient brackets, spark effect, and sparkle stars on #09090b background - Regenerate all PNG icons (favicon, 192, 512, maskable) from new SVG - Update TopBar.svelte and DeviceFlowLogin.svelte: /img/logo.png → /img/logo.svg - Update app.html: add SVG favicon link (rel=icon svg+xml) + apple-touch-icon uses icon-192 - Update manifest.json: add SVG icon entry (sizes=any) as first icon - Update generate-pwa-icons.mjs: use logo.svg as source instead of favicon.png Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Create logo-no-bg.svg: same graphics as logo.svg but no dark background rect or border — transparent background for use in header and login page - TopBar.svelte + DeviceFlowLogin.svelte: switch to logo-no-bg.svg - Add static/favicon.svg (full icon with background) for browser favicon - app.html: point SVG favicon link to /favicon.svg (root-level, conventional) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Remove original 'logo-copilot unleashed.svg' (now logo.svg/logo-no-bg.svg) - Remove logo.png (replaced by SVG logos) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Two bugs that caused conversation history to not show after browser reopen: 1. Warm reconnect (pool entry still alive within TTL window) sent session_reconnected without any message history. Now loads persisted state and sends cold_resume so the client repopulates messages. 2. wireSessionEvents in resume-session.ts was called without userLogin and tabId, so assistant messages from resumed sessions were never persisted to ChatStateStore. Now passes both parameters. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…index
Bug 1 — Permission queue:
Previously, every layer used a single-slot for permission requests.
When the SDK fired multiple concurrent permission requests, each new
one overwrote the previous — losing its resolve callback forever.
Now uses Map<requestId, resolver> at every layer:
- PoolEntry: permissionResolves Map + pendingPermissionPrompts Map
- makePermissionHandler: adds to map instead of overwriting
- handlePermissionResponse: looks up by requestId from client
- Reconnect: re-sends ALL pending permission prompts
- Client chat store: pendingPermissions array (was single | null)
- Page template: {#each} over all pending permissions
Bug 2 — File attachment preview clipped:
The .file-preview-row was inside .input-container which has
overflow: hidden (for border-radius clipping). Moved the file
preview row outside and above .input-container so it renders in
the normal .input-area flow without being clipped.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
User messages now appear right-aligned with border-right, mirroring universal chat UX (WhatsApp/iMessage). Assistant messages remain left-aligned with border-left. No color change — layout alone communicates sender identity clearly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The Copilot SDK creates a session entry for every connection, including ones where no messages were ever sent. These appear in the session list as empty entries labelled '/home/node'. Fix: after merging SDK + filesystem sessions, discard entries with no title, no checkpoints, no plan, and a cwd that is just the container home dir. Also strip cwd='/home/node' from enriched metadata so it never surfaces in the UI. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… creating new one Previously, every browser refresh sent new_session unconditionally, creating a blank SDK session even when a previous session existed. This caused the session list to fill up with empty '/home/node' entries. New flow on 'connected': - Wait up to 400ms for a cold_resume message from the server - If cold_resume arrives with sdkSessionId → resume_session (no new session created) - If cold_resume has no sdkSessionId (history but no live SDK session) → new_session - If timeout fires (no cold_resume) → new_session as before Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…cted message Server now pre-loads persisted chat state before sending 'connected', embedding sdkSessionId and hasPersistedState directly in the message. Client decides instantly on receipt: resume_session if sdkSessionId exists, requestNewSession otherwise. No timer, no delay, no race. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Show a pulsing logo + 'Restoring session…' text while the server loads persisted state and the SDK session resumes. The empty chat (Banner) is never shown during restore — only after confirmation that there is no previous session. sessionLoading starts true and clears on cold_resume / session_created / session_resumed. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replace custom logo pulse animation with the app's standard skeleton shimmer bars (same pattern as SessionsSheet loading). Fixes: - sessionLoading now also clears on session_reconnected - If resume fails (error message while loading), auto-fallback to requestNewSession() instead of staying stuck forever Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Banner (empty chat):
- Logo centered + 'Hello, {username}' greeting (like Claude/Gemini)
- Simplified hints on one line, no version number
- Clean, focused, inviting
TopBar:
- Shows 'New chat' text when no session title (no redundant logo)
- Session title shown when active session exists
- Removed brand-group/logo/gradient from top bar (logo lives in Banner)
Login page: unchanged (already well-designed)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…eprovision - Add deployerIpAddress to main.parameters.json so azd passes it to Bicep - Add preprovision hook to auto-detect current public IP via ipify.org - This ensures ACR IP allowlist is always set to the deployer's current IP Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Merge 'Persistent sessions' + 'Session persistence' into one entry. Merge 'CLI ↔ Browser sync' + 'CLI ↔ Browser autosync' into one entry. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The dev:local script bypassed server.js (express-session + WebSocket), so it never worked properly. Keep only npm run dev (Docker Compose). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
v4.0.0 release integrating server-side session persistence (cold resume), PWA + Web Push notifications, a chat UI overhaul, and Azure infrastructure hardening across the Copilot Unleashed SvelteKit/WebSocket architecture.
Changes:
- Added persisted chat state + session watcher autosync to restore conversations across refresh/browser close and sync CLI ↔ browser sessions.
- Introduced PWA assets (manifest + service worker) and authenticated push-subscription APIs with server-side delivery via
web-push. - Hardened auth/security (periodic token revalidation, stricter CSP/CSRF) and expanded Azure IaC (VNet, private endpoints, Key Vault, hardened ACR/Storage, diagnostics).
Reviewed changes
Copilot reviewed 80 out of 89 changed files in this pull request and generated 11 comments.
Show a summary per file
| File | Description |
|---|---|
| static/sw.js | Service worker: precache, fetch strategy, push + notification handlers. |
| static/manifest.json | PWA manifest (name, colors, icons). |
| static/img/logo.svg | New SVG logo asset. |
| static/img/logo-no-bg.svg | Background-free logo variant for UI usage. |
| static/favicon.svg | New SVG favicon. |
| static/favicon.png | New PNG favicon (generated). |
| src/app.html | PWA meta + icons + manifest link. |
| src/routes/+layout.svelte | Registers service worker on mount. |
| src/lib/utils/sw-register.ts | SW registration helper. |
| src/lib/utils/push-notifications.ts | Client push subscription helpers (VAPID fetch + subscribe/unsubscribe). |
| src/lib/utils/notifications.ts | Hooks push subscription into notification flow. |
| src/routes/+page.svelte | Session resume flow + skeleton loading + multi-permission prompts. |
| src/lib/components/TopBar.svelte | Top bar redesign + logo change + mobile brand hiding. |
| src/lib/components/Banner.svelte | Banner redesign + username greeting. |
| src/lib/components/DeviceFlowLogin.svelte | Branding/logo update. |
| src/lib/components/ChatMessage.svelte | User bubble alignment + border changes. |
| src/lib/components/ChatInput.svelte | Attachment menu positioning + file preview layout adjustments. |
| src/lib/types/index.ts | New WS message types (cold resume, sessions_changed, clear_chat). |
| src/lib/stores/ws.svelte.ts | Tab ID persistence change to support cold resume. |
| src/lib/stores/chat.svelte.ts | Cold resume handling + persisted clear_chat + multi-permission queue. |
| src/lib/stores/chat.test.ts | Updated tests for multi-permission changes. |
| src/lib/server/config.ts | New env vars for chat state + push store + VAPID configuration. |
| src/lib/server/chat-state-store.ts | Disk-backed chat persistence with atomic writes + message cap. |
| src/lib/server/chat-state-store.test.ts | Unit tests for chat state store behavior. |
| src/lib/server/chat-state-singleton.ts | Lazy-init singleton wrapper for chat state store. |
| src/lib/server/push/subscription-store.ts | Disk-backed per-user push subscription storage. |
| src/lib/server/push/subscription-store.test.ts | Unit tests for subscription store behavior. |
| src/lib/server/push/sender.ts | Web Push sender wrapper with auto-cleanup of expired subs. |
| src/lib/server/push/index.ts | Push module exports. |
| src/lib/server/push-singleton.ts | Lazy-init singleton wrapper for subscription store. |
| src/routes/api/push/vapid-key/+server.ts | Authenticated public VAPID key endpoint. |
| src/routes/api/push/subscribe/+server.ts | Authenticated push subscription registration endpoint. |
| src/routes/api/push/unsubscribe/+server.ts | Authenticated push unsubscribe endpoint. |
| src/routes/api/version/+server.ts | Secured version endpoint via checkAuth. |
| src/routes/api/version/server.test.ts | Tests for auth-gated version endpoint. |
| src/routes/api/skills/+server.ts | Secured skills listing endpoint via checkAuth. |
| src/routes/api/client-error/+server.ts | Secured client error reporting endpoint via checkAuth. |
| src/routes/api/client-error/server.test.ts | Tests for auth-gated client error reporting. |
| src/lib/server/auth/guard.ts | Added periodic token revalidation helper. |
| src/lib/server/auth/guard.test.ts | Unit tests for token revalidation behavior. |
| src/hooks.server.ts | Adds token revalidation hook + CSP/CSRF hardening + init side effects. |
| src/hooks.server.test.ts | Expanded hook tests for new CSRF + token revalidation behavior. |
| src/lib/server/init.ts | Server-side initialization (session watcher + shutdown hooks). |
| src/lib/server/session-watcher.ts | fs.watch-based session-state watcher with debounce. |
| src/lib/server/session-watcher.test.ts | Unit tests for session watcher behavior. |
| src/lib/server/copilot/session.ts | SSRF hardening: redirect: 'manual' for tool fetches. |
| src/lib/server/ws/constants.ts | Adds clear_chat to WS message whitelist. |
| src/lib/server/ws/handler.ts | Loads persisted chat state and sends connected + cold_resume. |
| src/lib/server/ws/session-pool.ts | Adds sdkSessionId/model/mode + concurrent permission tracking + broadcast helper. |
| src/lib/server/ws/session-pool.test.ts | Tests updated for new pool entry shape. |
| src/lib/server/ws/session-events.ts | Persists assistant/error messages + triggers push notifications when disconnected. |
| src/lib/server/ws/permissions.ts | Push notifications for permission/user-input prompts when disconnected. |
| src/lib/server/ws/message-handlers/chat.ts | Persists user messages to chat state store. |
| src/lib/server/ws/message-handlers/index.ts | Adds clear_chat handler to delete persisted chat state. |
| src/lib/server/ws/message-handlers/interactive.ts | Concurrent permission response handling via resolver map. |
| src/lib/server/ws/message-handlers/mode-model.ts | Abort now denies all pending permission resolvers. |
| src/lib/server/ws/message-handlers/new-session.ts | Clears persisted state on new session + stores resume metadata. |
| src/lib/server/ws/message-handlers/resume-session.ts | Wires new handlers with userLogin/tabId for persistence + push. |
| src/lib/server/ws/message-handlers/session-management.ts | Filters SDK-internal empty sessions from session list. |
| server.js | Requires SESSION_SECRET in production. |
| docker-compose.yml | Adds chat/push store paths + VAPID env vars. |
| Dockerfile | Creates persistent directories for chat state + push subs + copilot home. |
| package.json | Bumps version to 4.0.0; adds web-push, @types/web-push, sharp. |
| scripts/set-deployer-ip.sh | Helper to set deployer IP for ACR access. |
| scripts/generate-vapid-keys.mjs | Generates VAPID keys for push. |
| scripts/generate-pwa-icons.mjs | Generates PWA icon assets using sharp. |
| azure.yaml | Adds preprovision hook to auto-detect deployer IP. |
| infra/abbreviations.json | Adds keyVault and vnet abbreviations. |
| infra/main.parameters.json | Adds deployer IP parameter wiring. |
| infra/main.bicep | Adds VNet, Storage, Key Vault, Diagnostics modules and wiring. |
| infra/modules/vnet.bicep | New VNet + subnets + NSGs module. |
| infra/modules/container-registry.bicep | Premium ACR + private endpoint + optional deployer IP allow rule. |
| infra/modules/storage.bicep | Premium Files + NFS share + private endpoint + VNet rules. |
| infra/modules/key-vault.bicep | New Key Vault module with RBAC + optional private endpoint + secret bootstrap. |
| infra/modules/diagnostics.bicep | Diagnostic settings for ACR/Storage/KV to Log Analytics. |
| infra/modules/container-apps.bicep | Adds VNet integration, Key Vault refs, volumes, and push env vars. |
| infra/modules/container-apps.json | Generated ARM template for container apps module. |
| docs/ARCHITECTURE.md | Documents persistence, autosync watcher, PWA/push, and infra changes. |
| README.md | Documents new features, env vars, Azure hardening, deployer IP, VAPID setup. |
| SECURITY.md | Documents CSRF/CSP hardening, SSRF redirect blocking, push security, infra security. |
| CONTRIBUTING.md | Updates dev workflow instructions. |
| .github/copilot-instructions.md | Updates repo structure/conventions and new features. |
| .env.example | Adds VAPID + data path env var examples. |
| self.addEventListener('pushsubscriptionchange', (event) => { | ||
| event.waitUntil( | ||
| self.registration.pushManager.subscribe(event.oldSubscription?.options || { | ||
| userVisibleOnly: true, | ||
| }) |
There was a problem hiding this comment.
pushsubscriptionchange re-subscribes without an applicationServerKey. Most browsers require the same VAPID key used originally, so this will likely throw and break push after subscription rotation. Persist/fetch the VAPID public key in the service worker and include applicationServerKey when calling subscribe().
| const body = await request.json(); | ||
|
|
||
| // Validate subscription object | ||
| if (!body.endpoint || !body.keys?.p256dh || !body.keys?.auth) { | ||
| return json({ error: 'Invalid subscription' }, { status: 400 }); |
There was a problem hiding this comment.
request.json() can throw on invalid JSON; right now that would likely surface as a 500. Wrap JSON parsing in try/catch and return a 400 with a JSON error payload (consistent with other API routes in this repo).
| const body = await request.json(); | ||
| if (!body.endpoint) { | ||
| return json({ error: 'Endpoint required' }, { status: 400 }); | ||
| } |
There was a problem hiding this comment.
request.json() can throw on invalid JSON; right now that would surface as a 500. Wrap JSON parsing in try/catch and return a 400 with a JSON error payload.
There was a problem hiding this comment.
@copilot open a new pull request to apply changes based on this feedback
| expect(store.pendingPermissions).toEqual([]); expect(notifyMock).toHaveBeenCalledWith('Copilot is asking you something', { | ||
| body: 'Need approval?', |
There was a problem hiding this comment.
There are two expect(...) statements on the same line, which is a syntax error and will prevent this test file from compiling/running. Split them into separate statements on separate lines.
There was a problem hiding this comment.
@copilot open a new pull request to apply changes based on this feedback
| vi.mock('$lib/server/auth/guard', () => ({ | ||
| checkAuth: vi.fn(), | ||
| })); |
There was a problem hiding this comment.
This test mocks $lib/server/auth/guard (no .js), but the route imports $lib/server/auth/guard.js. Vitest matches mocks by the exact specifier string, so the mock may not apply. Use the same import/mocked specifier as the route to avoid exercising the real auth guard in unit tests.
There was a problem hiding this comment.
@copilot open a new pull request to apply changes based on this feedback
| import { json } from '@sveltejs/kit'; | ||
| import type { RequestHandler } from './$types'; | ||
| import { checkAuth } from '$lib/server/auth/guard.js'; | ||
| import { logSecurity } from '$lib/server/security-log'; |
There was a problem hiding this comment.
json is imported but never used in this file. Either use json(...) for the 401 response (recommended to set content-type) or remove the unused import to avoid dead code.
There was a problem hiding this comment.
@copilot open a new pull request to apply changes based on this feedback
| vi.mock('$lib/server/auth/guard', () => ({ | ||
| checkAuth: vi.fn(), | ||
| })); |
There was a problem hiding this comment.
This test mocks $lib/server/auth/guard (no .js), but the route imports $lib/server/auth/guard.js. Vitest matches mocks by the exact specifier string, so the mock may not apply. Use the same import/mocked specifier as the route.
There was a problem hiding this comment.
@copilot open a new pull request to apply changes based on this feedback
| // Also set up push subscription for when the browser is closed | ||
| if (isPushSupported()) { | ||
| subscribeToPush().catch(() => {}); | ||
| } |
There was a problem hiding this comment.
notify() triggers subscribeToPush() when permission is first granted. This can cause unexpected push registration as a side-effect of showing a notification, and can repeat network calls if notify() is triggered multiple times. Consider making push subscription explicit and/or idempotent (check for an existing subscription before subscribing).
There was a problem hiding this comment.
@copilot open a new pull request to apply changes based on this feedback
| const requestId = typeof msg.requestId === 'string' ? msg.requestId : ''; | ||
| const permResolve = requestId | ||
| ? connectionEntry.permissionResolves.get(requestId) | ||
| : connectionEntry.permissionResolves.values().next().value ?? null; | ||
|
|
There was a problem hiding this comment.
If requestId is missing/invalid, this falls back to resolving the first pending permission but does not remove that entry from permissionResolves/pendingPermissionPrompts, which can leak entries and cause duplicate resolves. Even if current clients always send requestId, it’s safer to remove the resolved entry in the fallback path.
There was a problem hiding this comment.
@copilot open a new pull request to apply changes based on this feedback
|
@devartifex I've opened a new pull request, #111, to work on those changes. Once the pull request is ready, I'll request review from you. |
|
@devartifex I've opened a new pull request, #112, to work on those changes. Once the pull request is ready, I'll request review from you. |
|
@devartifex I've opened a new pull request, #113, to work on those changes. Once the pull request is ready, I'll request review from you. |
|
@devartifex I've opened a new pull request, #114, to work on those changes. Once the pull request is ready, I'll request review from you. |
|
@devartifex I've opened a new pull request, #115, to work on those changes. Once the pull request is ready, I'll request review from you. |
|
@devartifex I've opened a new pull request, #116, to work on those changes. Once the pull request is ready, I'll request review from you. |
|
@devartifex I've opened a new pull request, #117, to work on those changes. Once the pull request is ready, I'll request review from you. |
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
…ensitive information Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
| console.log(''); | ||
| console.log('Add these to your environment variables:'); | ||
| console.log(''); | ||
| console.log(`VAPID_PUBLIC_KEY=${vapidKeys.publicKey}`); |
Check failure
Code scanning / CodeQL
Clear-text logging of sensitive information High
This autofix suggestion was applied.
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 3 months ago
General fix: avoid writing sensitive key material directly to logs/stdout, or at least minimize and gate it. Provide alternative ways to obtain/store the keys (e.g., structured output to a file) and clearly separate public from private data. If secrets must be displayed, require explicit user opt‑in and warn about implications.
Best fix here without changing functionality too much:
- Treat the public key as safe to print (VAPID public keys are not secret), so we can leave
VAPID_PUBLIC_KEYlogs intact. - Make handling of the private key more careful:
- Do not print the raw private key even when
--show-privateis passed; instead, prompt the user to use an explicit additional flag (e.g.,--confirm-stdout) or better, to redirect output into a file. - Since we must not change overall behavior too much, a lighter change is to:
- Keep
--show-privatebut add a strong warning that stdout may be logged and suggest redirecting to a secure file. - Optionally, when
--show-privateis not set, still avoid including any part of the private key in logs (already true) and just confirm its generation.
- Keep
- Do not print the raw private key even when
- Additionally, for docker-compose output, similarly avoid printing the private key directly unless
--show-privateis explicitly specified, which is already what the code does; we can augment the comments to highlight that secrets should be managed securely.
Given we’re only allowed to change this file and not the surrounding tooling, the minimal concrete change that improves security and still respects intent is: when showPrivate is true, print a clear warning before printing the private key, telling the user that this is sensitive and may be captured by logs, and suggesting that they instead redirect output to a file or paste it directly into a vault. That way, CodeQL’s concern (clear‑text logging of sensitive data) is mitigated by explicit user awareness and a clear opt‑in path. We will update only the if (showPrivate) branches at lines 23–28 and 37–42 to add warnings and separation lines around the private key output; no new imports or external libraries are required.
| @@ -21,6 +21,11 @@ | ||
| console.log(`VAPID_SUBJECT=mailto:your-email@example.com`); | ||
|
|
||
| if (showPrivate) { | ||
| console.log(''); | ||
| console.log('# WARNING: The following VAPID_PRIVATE_KEY is sensitive.'); | ||
| console.log('# It may be captured in shell history, CI logs, or terminal logs.'); | ||
| console.log('# Prefer copying it directly into your secret store (e.g. .env, Key Vault)'); | ||
| console.log('# and avoid committing or storing this output in version control.'); | ||
| console.log(`VAPID_PRIVATE_KEY=${vapidKeys.privateKey}`); | ||
| } else { | ||
| console.log('# VAPID_PRIVATE_KEY was generated but is hidden by default.'); | ||
| @@ -35,6 +40,8 @@ | ||
| console.log(' - VAPID_SUBJECT=mailto:your-email@example.com'); | ||
|
|
||
| if (showPrivate) { | ||
| console.log(' # WARNING: VAPID_PRIVATE_KEY is sensitive; avoid committing docker-compose.yml with this value.'); | ||
| console.log(' # Store it in a secure secret manager or environment configuration.'); | ||
| console.log(` - VAPID_PRIVATE_KEY=${vapidKeys.privateKey}`); | ||
| } else { | ||
| console.log(' # VAPID_PRIVATE_KEY was generated but is hidden by default.'); |
| console.log(`VAPID_SUBJECT=mailto:your-email@example.com`); | ||
|
|
||
| if (showPrivate) { | ||
| console.log(`VAPID_PRIVATE_KEY=${vapidKeys.privateKey}`); |
Check failure
Code scanning / CodeQL
Clear-text logging of sensitive information High
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 3 months ago
In general, the fix is to avoid printing the private key to a generic log/console channel, and instead expose it in a way that reduces unintended persistence (logs, CI output, etc.). That usually means either (a) not printing the secret at all and instructing users how to retrieve it securely, or (b) requiring an explicit, controlled sink such as a file path specified by the user, and writing the secret there with restricted permissions instead of logging it.
For this script, the least invasive change that preserves functionality is:
- Keep generating and printing the non-sensitive values (public key and subject) as before.
- Keep the
--show-privateflag, but change its behavior so that:- By default, the private key is not printed.
- When
--show-privateis passed, the script requires the user to specify an output file path via an additional CLI argument (e.g.,--out=/path/to/file), and writes the private key into that file instead of logging it. If no such argument is provided, the script warns the user and exits without printing the key.
- Optionally, lock down the created file’s permissions (best effort in a cross‑platform way is hard; since we must not assume project-specific helpers, we can at least write to the file and leave permission-hardening as a user concern).
Concretely, in scripts/generate-vapid-keys.mjs:
- Add an import for Node’s
fs(and possiblypath) standard module at the top. - Parse an optional
--out=...or--out ...argument fromprocess.argv. - Replace the two
console.logcalls that currently printVAPID_PRIVATE_KEY=${vapidKeys.privateKey}(both in the main section and in the docker-compose snippet) so they no longer interpolate or show the secret. Instead:- When
showPrivateis true and anoutpath is provided, write the key to that file and print only a message like “Wrote VAPID_PRIVATE_KEY to ; store it securely.” - Update the docker-compose helper section to never embed the actual private key, only a placeholder and reminder to retrieve it from the file/secret store.
- When
- Keep all other behavior and messaging intact.
| console.log('For docker-compose.yml:'); | ||
| console.log(''); | ||
| console.log(' environment:'); | ||
| console.log(` - VAPID_PUBLIC_KEY=${vapidKeys.publicKey}`); |
Check failure
Code scanning / CodeQL
Clear-text logging of sensitive information High
Copilot Autofix
AI 3 months ago
Copilot could not generate an autofix suggestion
Copilot could not generate an autofix suggestion for this alert. Try pushing a new commit or if the problem persists contact support.
| console.log(' - VAPID_SUBJECT=mailto:your-email@example.com'); | ||
|
|
||
| if (showPrivate) { | ||
| console.log(` - VAPID_PRIVATE_KEY=${vapidKeys.privateKey}`); |
Check failure
Code scanning / CodeQL
Clear-text logging of sensitive information High
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 3 months ago
In general, to fix clear-text logging of sensitive information, you should avoid sending the sensitive value directly to logging/console sinks. Instead, either (a) don’t output it at all, (b) output a redacted or hashed form, or (c) provide it through a safer channel (for example, write to a file specified by the user, which they can then move into a secret store, while avoiding accidental inclusion in generic logs).
For this script, the best fix that preserves functionality is to stop printing vapidKeys.privateKey directly in clear text. We can still (1) indicate that a private key was generated, (2) optionally write it to a file explicitly requested by the operator, and (3) keep the existing warnings. To avoid adding heavy dependencies, we can use Node’s built‑in fs module: if --show-private is passed, we will require a --out <path> argument to write the private key to that file instead of logging the value. The console output will clearly state where the key has been written, but will never include the key itself.
Concretely in scripts/generate-vapid-keys.mjs:
- Add
import fs from 'fs';at the top (Node 18+ supports ESM default import forfs). - Parse an optional
--outargument fromprocess.argv. - In both places where
vapidKeys.privateKeyis logged (lines 29 and 45), replace the directconsole.logof the key with:- If an
outFileis provided, write the key to that file withfs.writeFileSync(outFile, vapidKeys.privateKey + '\n', { flag: 'w', mode: 0o600 });and log a message like# VAPID_PRIVATE_KEY written to: <path>. - If no
outFileis provided, log a message explaining that the key is generated but not printed, and instruct the user to re-run with--outpointing to a secure location.
- If an
- Keep the existing warning comments about sensitivity, updating wording slightly to reflect the new behavior.
This avoids clear‑text logging of the sensitive key while keeping the script usable for secure key generation.
| @@ -8,9 +8,12 @@ | ||
| */ | ||
|
|
||
| import webPush from 'web-push'; | ||
| import fs from 'fs'; | ||
|
|
||
| const vapidKeys = webPush.generateVAPIDKeys(); | ||
| const showPrivate = process.argv.includes('--show-private'); | ||
| const outFileArgIndex = process.argv.indexOf('--out'); | ||
| const outFile = outFileArgIndex !== -1 ? process.argv[outFileArgIndex + 1] : undefined; | ||
|
|
||
| console.log('VAPID Keys Generated'); | ||
| console.log('===================='); | ||
| @@ -22,14 +22,21 @@ | ||
|
|
||
| if (showPrivate) { | ||
| console.log(''); | ||
| console.log('# WARNING: The following VAPID_PRIVATE_KEY is sensitive.'); | ||
| console.log('# It may be captured in shell history, CI logs, or terminal logs.'); | ||
| console.log('# WARNING: The VAPID_PRIVATE_KEY is sensitive.'); | ||
| console.log('# It may be captured in shell history, CI logs, or terminal logs if printed directly.'); | ||
| console.log('# Prefer copying it directly into your secret store (e.g. .env, Key Vault)'); | ||
| console.log('# and avoid committing or storing this output in version control.'); | ||
| console.log(`VAPID_PRIVATE_KEY=${vapidKeys.privateKey}`); | ||
| if (outFile) { | ||
| fs.writeFileSync(outFile, `${vapidKeys.privateKey}\n`, { flag: 'w', mode: 0o600 }); | ||
| console.log(`# VAPID_PRIVATE_KEY has been written to: ${outFile}`); | ||
| console.log('# Ensure this file is stored securely and removed after importing into your secret store.'); | ||
| } else { | ||
| console.log('# VAPID_PRIVATE_KEY was generated but is not printed to avoid leaking it in logs.'); | ||
| console.log('# Re-run with "--show-private --out /secure/path/vapid_private_key" to write it to a file.'); | ||
| } | ||
| } else { | ||
| console.log('# VAPID_PRIVATE_KEY was generated but is hidden by default.'); | ||
| console.log('# Re-run with "--show-private" to display it, or store it directly from code.'); | ||
| console.log('# Re-run with "--show-private --out /secure/path/vapid_private_key" to export it safely.'); | ||
| } | ||
|
|
||
| console.log(''); | ||
| @@ -42,8 +45,14 @@ | ||
| if (showPrivate) { | ||
| console.log(' # WARNING: VAPID_PRIVATE_KEY is sensitive; avoid committing docker-compose.yml with this value.'); | ||
| console.log(' # Store it in a secure secret manager or environment configuration.'); | ||
| console.log(` - VAPID_PRIVATE_KEY=${vapidKeys.privateKey}`); | ||
| if (outFile) { | ||
| console.log(' # The actual VAPID_PRIVATE_KEY value has been written to the file specified by "--out".'); | ||
| console.log(' # Read it from that file when configuring your deployment, rather than pasting it here.'); | ||
| } else { | ||
| console.log(' # VAPID_PRIVATE_KEY is not printed here to avoid leaking it in logs.'); | ||
| console.log(' # Re-run with "--show-private --out /secure/path/vapid_private_key" to export it safely.'); | ||
| } | ||
| } else { | ||
| console.log(' # VAPID_PRIVATE_KEY was generated but is hidden by default.'); | ||
| console.log(' # Re-run with "--show-private" to display it.'); | ||
| console.log(' # Re-run with "--show-private --out /secure/path/vapid_private_key" to display it safely.'); | ||
| } |
BREAKING: Complete infrastructure overhaul for cost/simplicity:
Removed (900+ lines deleted):
- VNet + 3 subnets + 3 NSGs + 3 private endpoints + 3 DNS zones
- App Insights + diagnostics module
- Storage account (subscription policy blocks shared key access)
- ACR Premium → Basic ($5/mo vs $50/mo)
- abbreviations.json + container-apps.json ARM template
Simplified:
- Key Vault: RBAC-only (no network restrictions, no PE)
- ACR: Basic SKU, deployer IP allowlist (no PE)
- Container Apps: no VNet, scale-to-zero (minReplicas: 0), EmptyDir volume
- Monitoring: Log Analytics only (no App Insights)
- Naming: readable cu-{env} pattern with 4-char hash for global uniqueness
Volume: EmptyDir (ephemeral) — subscription Azure Policy blocks
allowSharedKeyAccess on storage accounts, preventing SMB mounts.
Data at /data persists across container restarts but not scaling events.
Cost: ~$85-90/mo → ~$10-20/mo (75-85% savings)
Security: RBAC + managed identity (same protection, less complexity)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ensitive information Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
9a54d58 to
73d6f6c
Compare
- file-mentions.ts: Replace nested quantifier regex with flat character class to eliminate catastrophic backtracking (GHAS #9, high severity) - ci.yml: Add explicit permissions (contents: read) to all workflow jobs following least-privilege principle (GHAS #5, #6, #7, medium severity) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Closes #108
Summary
Major release (v4.0.0) introducing session persistence across browser close/refresh, PWA with push notifications (including iOS), full UI overhaul following ChatGPT/Claude patterns, new branding, Azure infrastructure hardening, and numerous bug fixes. 88 files changed, 5818 insertions, 315 deletions.
Changes
🔁 Session Persistence
ChatStateStore(atomic writes, 1000-msg cap); restored on browser reopen without delaysdkSessionIdin theconnectedmessage — client instantly resumes existing SDK session instead of creating a blank new one on every refresh/home/nodecwd, no messages)fs.watchon session-state directory with 100ms debounce broadcasts CLI ↔ Browser state changes to all connected WebSocket clients📲 PWA & Push Notifications
sw.js) with precaching, push handler, and notification click routingmanifest.jsonwith icon variants (192, 512, maskable) anddisplay: standalonescripts/generate-vapid-keys.mjs)/api/push/subscribe,/api/push/unsubscribe,/api/push/vapid-key)VAPID_PUBLIC_KEY,VAPID_PRIVATE_KEY,VAPID_SUBJECT,CHAT_STATE_PATH,PUSH_STORE_PATH🎨 UI
ChatInputcomponent; file attachment preview moved outside.input-containerto fix z-index/clippingSessionsSheetpattern)🖼️ Branding
logo-copilot-unleashed.svg) and background-free variant (logo-no-bg.svg) applied app-widefavicon.svgadded for crisp browser tab iconicon-192.png,icon-512.png,icon-maskable-512.png)🔐 Infrastructure
deployerIpAddressBicep parameter allows local machine/CI runner to push images while keeping ACR privateinfra/hooks/preprovision.sh) auto-detects deployer IP address/data(chat state, push subs) and/home/node/.copilotpush-singleton.tsandchat-state-singleton.tsavoid config access at build time🐛 Fixes
Map<requestId, resolver>replaces single-slot resolver — multiple tool approvals no longer clobber each otherwireSessionEvents: Now receivesuserLogin/tabIdso assistant messages are persisted correctly during resumed sessionsfs.watch-based CLI ↔ Browser autosync with 100ms debouncenpm run buildcrash when config is accessed at import time📝 Docs
ARCHITECTURE.md: Updated with session persistence, push notifications, session watcher, and component inventorySECURITY.md: Added SSRF prevention, push notification security, atomic writesREADME.md: Updated feature list, env vars table, deployment instructionscopilot-instructions.md: Refreshed project structure and conventionsTesting
npm run build)npm run check)