Skip to content
Merged
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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,7 @@ If you want the technical breakdown instead of the public pitch, read [docs/ARCH
| Runtime verification | [docs/RUNTIME-VERIFICATION.md](docs/RUNTIME-VERIFICATION.md) |
| Security audit | [docs/SECURITY-AUDIT.md](docs/SECURITY-AUDIT.md) |
| WebView harness | [docs/WEBVIEW-HARNESS.md](docs/WEBVIEW-HARNESS.md) |
| Proxy deployment | [docs/PROXY-DEPLOYMENT.md](docs/PROXY-DEPLOYMENT.md) |
| Release runbook | [docs/RELEASE-RUNBOOK.md](docs/RELEASE-RUNBOOK.md) |
| Publishing checklist | [docs/PUBLISHING-CHECKLIST.md](docs/PUBLISHING-CHECKLIST.md) |
| Showcase | [docs/SHOWCASE.md](docs/SHOWCASE.md) |
Expand All @@ -227,6 +228,7 @@ If you want the technical breakdown instead of the public pitch, read [docs/ARCH
- HTTPS is preferred when you can issue a trusted certificate.
- Cleartext HTTP remains supported for Tailscale or another trusted private network because that is a core self-hosted use case, but the app now warns before every HTTP session.
- The app intentionally stays scoped to one configured server host inside the WebView; other destinations open outside the app.
- The optional proxy is self-hosted infrastructure for trusted networks. Check [docs/PROXY-DEPLOYMENT.md](docs/PROXY-DEPLOYMENT.md) before exposing it through any reverse proxy.

See [docs/SECURITY-AUDIT.md](docs/SECURITY-AUDIT.md) for the current audit notes, fixes, and remaining tradeoffs.

Expand Down
2 changes: 2 additions & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,5 @@ Target response windows:
- Use trusted certificates for HTTPS where possible.
- Do not commit TLS keys, certificates, `.env` files, or signed build artifacts to the repository.
- Review WebView and proxy settings carefully before broader deployment.

Proxy deployment boundaries are documented in [docs/PROXY-DEPLOYMENT.md](docs/PROXY-DEPLOYMENT.md).
44 changes: 44 additions & 0 deletions docs/PROXY-DEPLOYMENT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Proxy Deployment Checklist

Use the optional proxy as self-hosted infrastructure for a T3 Code session you already control. It is not a hardened public edge service.

## Appropriate Use

- Use it on Tailscale, a trusted LAN, or behind a reverse proxy you already operate.
- Prefer HTTPS with a certificate trusted by the client device.
- Use `PROXY_HTTP=true` only behind Tailscale Serve or another HTTPS-terminating private reverse proxy.
- Keep `T3_TARGET` pointed at a T3 Code instance on a private host or loopback address.
- Keep `GET /__t3mobile/health` available for smoke tests and support checks.

## Do Not Use It This Way

- Do not expose the proxy directly to the public internet as an unauthenticated gateway.
- Do not use self-signed certificates on networks you do not control.
- Do not point `T3_TARGET` at an untrusted upstream host.
- Do not publish `.env` files, TLS keys, certificates, signed APKs, or local diagnostic output.
- Do not describe the proxy as production-hardened public infrastructure.

## Minimum Setup

1. Set `T3_TARGET` to your local T3 Code URL, for example `http://127.0.0.1:3773`.
2. Set `PUBLIC_URL` to the URL your phone will open.
3. For built-in HTTPS mode, set `SSL_KEY_PATH` and `SSL_CERT_PATH` to trusted certificate files.
4. For HTTP mode behind another HTTPS layer, set `PROXY_HTTP=true` and keep the HTTP listener private.
5. Run `npm test` before sharing setup instructions or publishing a release.

## Verification

Run the automated proxy smoke test:

```bash
npm run test:proxy
```

Then verify the deployed proxy from the phone network path:

- `GET /__t3mobile/health` returns JSON and does not expose filesystem paths.
- The reported upstream status is healthy.
- The root page loads through the expected HTTPS or trusted private-network path.
- Browser dev tools or proxy logs do not show TLS key paths, tokens, or private filesystem paths.

If any of those checks fail, treat the deployment as not verified.
23 changes: 23 additions & 0 deletions docs/SECURITY-AUDIT.md
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,29 @@ Current behavior:

- health results are cached for 5 seconds to prevent probe amplification

## Maintenance Updates (May 5, 2026)

### 15. Proxy deployment boundaries documented

Previous behavior:

- proxy deployment warnings were spread across README, SECURITY, and audit notes

Current behavior:

- `docs/PROXY-DEPLOYMENT.md` now provides a concrete checklist for trusted-network use, unsafe public exposure patterns, minimum setup, and verification

### 16. Upload-injection path has automated assertions

Previous behavior:

- upload-button behavior was stronger visually than mechanically asserted

Current behavior:

- `npm test` now runs DOM-level assertions for proxy upload button placement, hidden file input creation, paste-event dispatch, and fallback placement
- Android WebView upload injection has source-marker checks for the critical IDs, composer selectors, paste flow, observer, and fallback path

## Recommended Next Security Steps

- add optional host allowlisting for more than one trusted origin if the product needs it
Expand Down
45 changes: 23 additions & 22 deletions docs/STARTER-ISSUES.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,40 +20,41 @@ Labels:
- `docs`
- `help wanted`

## 2. Add automated assertion coverage for the upload-button path
## 2. Harden DOM targeting for composer upload injection

Why it matters:

- the upload flow is one of the core product claims
- it is still stronger visually than it is mechanically asserted
- upstream UI drift is a realistic long-term risk
- this is a good contributor-sized reliability issue

Suggested scope:

- expand the local harness or verification flow so the injected control can be asserted more directly
- document what is and is not guaranteed by the automated check
- review the current selector strategy in `MainActivity.java`
- reduce brittle assumptions where possible
- keep fallback placement behavior documented

Labels:

- `android`
- `help wanted`
- `good first issue`

## 3. Harden DOM targeting for composer upload injection
## 3. Add iPhone PWA runtime evidence

Why it matters:

- upstream UI drift is a realistic long-term risk
- this is a good contributor-sized reliability issue
- the PWA path has automated proxy coverage but still needs real-device proof
- iOS-specific install and safe-area behavior should be checked on hardware

Suggested scope:

- review the current selector strategy in `MainActivity.java`
- reduce brittle assumptions where possible
- keep fallback placement behavior documented
- install the PWA on a real iPhone or iPad
- capture connection, launch-from-home-screen, and proxy health evidence
- update `IPHONE-GUIDE.md` if device behavior differs from the current notes

Labels:

- `android`
- `good first issue`
- `docs`
- `help wanted`

## 4. Add a release demo clip for the README and release page

Expand All @@ -72,20 +73,20 @@ Labels:
- `community`
- `docs`

## 5. Improve proxy deployment guidance for self-hosted users
## 5. Add release signing verification notes

Why it matters:

- the proxy is useful but easy to misunderstand
- better deployment guidance lowers support overhead
- Android update trust depends on stable signing identity
- signing continuity is the highest-risk release operations gap

Suggested scope:

- expand docs around trusted-network assumptions
- add a short “when not to expose this” checklist
- keep the guidance aligned with `SECURITY.md`
- document the intended public release signing fingerprint
- add a release-time manual verification step for signer continuity
- keep private signing material out of the repo

Labels:

- `proxy`
- `docs`
- `android`
- `security`
3 changes: 2 additions & 1 deletion docs/UPSTREAM-FIT.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,15 @@ This project is most credible to `pingdotgg/t3code` when it stays narrow and rel
## Current Validation

Last checked: 2026-05-05 on `main` after the ESLint 10 and Node runtime update in
[`JSvandijk/t3code-mobile#19`](https://github.com/JSvandijk/t3code-mobile/pull/19).
[`JSvandijk/t3code-mobile#19`](https://github.com/JSvandijk/t3code-mobile/pull/19), plus the follow-up proxy deployment and upload-assertion maintenance.

- `npm test` passes from a clean local checkout after `npm ci`.
- JavaScript syntax checks pass.
- ESLint 10 flat-config linting passes with no warnings.
- `manifest.json` validation passes.
- Release checks pass for version `1.1.0`.
- HTML unit tests pass: 18/18.
- Upload-injection tests pass: 5/5.
- Proxy smoke test passes, including HTML injection, static assets, and `GET /__t3mobile/health`.
- `cmd /c build-apk.bat` builds a dev-signed APK successfully.

Expand Down
1 change: 1 addition & 0 deletions docs/WEBVIEW-HARNESS.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ Use `npm run harness:redirect` to confirm:
- If those files are absent, the harness generates a temporary self-signed certificate automatically.
- Override paths with `SSL_KEY_PATH` and `SSL_CERT_PATH` if needed.
- The harness also exposes `/status` for quick local checks.
- `npm test` runs DOM-level upload injection assertions for the proxy path and source-marker checks for the Android WebView path.
- Pair this guide with [RUNTIME-VERIFICATION.md](RUNTIME-VERIFICATION.md) when you are collecting release evidence.

## Recommended Evidence To Capture
Expand Down
6 changes: 3 additions & 3 deletions docs/evidence/v1.1.0-emulator/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ This evidence set does not prove:
- behavior on a physical Android device
- microphone permission flow
- real T3 Code upstream behavior beyond the local harness and existing repo smoke tests
- upload-button behavior by machine-readable assertion
- Android upload-button behavior by current runtime evidence

## Environment

Expand Down Expand Up @@ -150,5 +150,5 @@ This evidence set was captured against commit `7b7680807bd97f06802fd98783096dd37
## Status Summary

- Proven by emulator runtime evidence: connect screen, diagnostics path, pairing-link input acceptance, cleartext warning, base URL normalization, invalid HTTPS blocking
- Proven by automated checks: manifest integrity, release gates, proxy smoke path, proxy header expectations
- Still not proven here: physical-device behavior, microphone permission path, and machine-verifiable upload-button assertion
- Proven by current automated checks: manifest integrity, release gates, proxy smoke path, proxy header expectations, proxy upload-injection DOM assertions
- Still not proven here: physical-device behavior, microphone permission path, and Android upload-button behavior on the latest runtime
147 changes: 147 additions & 0 deletions lib/upload-injection.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
const uploadInjectionScript = `
(function() {
if (window.__t3MobileObserverAttached) return;
window.__t3MobileObserverAttached = true;

function getComposerTarget() {
var selectors = [
'[data-chat-composer-form] textarea',
'[data-chat-composer-form] [contenteditable="true"]',
'[role="textbox"]',
'textarea',
'[contenteditable="true"]'
];
for (var i = 0; i < selectors.length; i++) {
var node = document.querySelector(selectors[i]);
if (node) return node;
}
return null;
}

function getFileInput() {
var fi = document.getElementById('t3-file-input');
if (fi && fi.parentNode) return fi;
if (fi) fi.remove();
fi = document.createElement('input');
fi.type = 'file';
fi.accept = 'image/*';
fi.style.display = 'none';
fi.id = 't3-file-input';
fi.addEventListener('change', handleFile);
document.body.appendChild(fi);
return fi;
}

function handleFile(e) {
var file = e.target.files[0];
if (!file) return;
var dt = new DataTransfer();
dt.items.add(file);
var target = getComposerTarget();
if (target) {
target.focus();
target.dispatchEvent(new ClipboardEvent('paste', {
bubbles: true, cancelable: true, clipboardData: dt
}));
}
e.target.value = '';
}

function findAnchorButton() {
var footer = document.querySelector('[data-chat-composer-footer]');
if (footer) {
var buttons = footer.querySelectorAll('button');
for (var i = buttons.length - 1; i >= 0; i--) {
if (buttons[i].querySelectorAll('circle').length === 3) return buttons[i];
}
if (buttons.length) return buttons[buttons.length - 1];
}
var composer = document.querySelector('[data-chat-composer-form]');
if (composer) {
var composerButtons = composer.querySelectorAll('button');
if (composerButtons.length) return composerButtons[composerButtons.length - 1];
}
return null;
}

function getFallbackContainer() {
var composer = document.querySelector('[data-chat-composer-form]');
if (!composer) return null;
var container = document.getElementById('t3-mobile-actions');
if (container && container.parentNode === composer) return container;
if (container) container.remove();
container = document.createElement('div');
container.id = 't3-mobile-actions';
container.style.display = 'flex';
container.style.justifyContent = 'flex-end';
container.style.marginTop = '8px';
composer.appendChild(container);
return container;
}

function ensureButton() {
var anchor = findAnchorButton();
var existing = document.getElementById('t3-img-btn');
var fallback = getFallbackContainer();
if (anchor && anchor.parentNode) {
if (existing && existing.parentNode === anchor.parentNode) return true;
if (existing) existing.remove();
} else if (fallback) {
if (existing && existing.parentNode === fallback) return true;
if (existing) existing.remove();
} else {
return false;
}
var btn = document.createElement('button');
btn.id = 't3-img-btn';
btn.type = 'button';
btn.title = 'Add image';
btn.setAttribute('aria-label', 'Add image');
btn.className = anchor ? anchor.className : '';
if (!anchor) {
btn.style.minWidth = '40px';
btn.style.minHeight = '40px';
btn.style.borderRadius = '12px';
btn.style.border = '1px solid rgba(148, 163, 184, 0.25)';
btn.style.background = 'transparent';
btn.style.color = 'inherit';
btn.style.display = 'inline-flex';
btn.style.alignItems = 'center';
btn.style.justifyContent = 'center';
}
btn.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg>';
btn.addEventListener('click', function(ev) {
ev.preventDefault();
ev.stopPropagation();
getFileInput().click();
});
if (anchor && anchor.parentNode) {
anchor.parentNode.insertBefore(btn, anchor.nextSibling);
} else {
fallback.appendChild(btn);
}
return true;
}

function init() {
ensureButton();
var observerTicking = false;
new MutationObserver(function() {
if (observerTicking) return;
observerTicking = true;
requestAnimationFrame(function() {
observerTicking = false;
ensureButton();
});
}).observe(document.body, { childList: true, subtree: true });
}

if (document.body) {
init();
} else {
document.addEventListener('DOMContentLoaded', init);
}
})();
`;

module.exports = { uploadInjectionScript };
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading