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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,4 @@ bun.lock
.env.local
.env.*
!.env.example
supabase/.temp/
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# Changelog

## [0.11.16.1] - 2026-03-24 — Installation ID Privacy Fix

### Fixed

- **Installation IDs are now random UUIDs instead of hostname hashes.** The old `SHA-256(hostname+username)` approach meant anyone who knew your machine identity could compute your installation ID. Now uses a random UUID stored in `~/.gstack/installation-id` — not derivable from any public input, rotatable by deleting the file.
- **RLS verification script handles edge cases.** `verify-rls.sh` now correctly treats INSERT success as expected (kept for old client compat), handles 409 conflicts and 204 no-ops.

## [0.11.16.0] - 2026-03-24 — Telemetry Security Hardening

### Fixed
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.11.16.0
0.11.16.1
29 changes: 20 additions & 9 deletions bin/gstack-telemetry-log
Original file line number Diff line number Diff line change
Expand Up @@ -106,18 +106,29 @@ if [ -d "$STATE_DIR/sessions" ]; then
fi

# Generate installation_id for community tier
# Uses a random UUID stored locally — not derived from hostname/user so it
# can't be guessed or correlated by someone who knows your machine identity.
INSTALL_ID=""
if [ "$TIER" = "community" ]; then
HOST="$(hostname 2>/dev/null || echo "unknown")"
USER="$(whoami 2>/dev/null || echo "unknown")"
if command -v shasum >/dev/null 2>&1; then
INSTALL_ID="$(printf '%s-%s' "$HOST" "$USER" | shasum -a 256 | awk '{print $1}')"
elif command -v sha256sum >/dev/null 2>&1; then
INSTALL_ID="$(printf '%s-%s' "$HOST" "$USER" | sha256sum | awk '{print $1}')"
elif command -v openssl >/dev/null 2>&1; then
INSTALL_ID="$(printf '%s-%s' "$HOST" "$USER" | openssl dgst -sha256 | awk '{print $NF}')"
ID_FILE="$HOME/.gstack/installation-id"
if [ -f "$ID_FILE" ]; then
INSTALL_ID="$(cat "$ID_FILE" 2>/dev/null)"
fi
if [ -z "$INSTALL_ID" ]; then
# Generate a random UUID v4
if command -v uuidgen >/dev/null 2>&1; then
INSTALL_ID="$(uuidgen | tr '[:upper:]' '[:lower:]')"
elif [ -r /proc/sys/kernel/random/uuid ]; then
INSTALL_ID="$(cat /proc/sys/kernel/random/uuid)"
else
# Fallback: random hex from /dev/urandom
INSTALL_ID="$(od -An -tx1 -N16 /dev/urandom 2>/dev/null | tr -d ' \n')"
fi
if [ -n "$INSTALL_ID" ]; then
mkdir -p "$(dirname "$ID_FILE")" 2>/dev/null
printf '%s' "$INSTALL_ID" > "$ID_FILE" 2>/dev/null
fi
fi
# If no SHA-256 command available, install_id stays empty
fi

# Local-only fields (never sent remotely)
Expand Down
4 changes: 2 additions & 2 deletions test/telemetry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,8 @@ describe('gstack-telemetry-log', () => {

const events = parseJsonl();
expect(events).toHaveLength(1);
// installation_id should be a SHA-256 hash (64 hex chars)
expect(events[0].installation_id).toMatch(/^[a-f0-9]{64}$/);
// installation_id should be a UUID v4 (or hex fallback)
expect(events[0].installation_id).toMatch(/^[a-f0-9-]{32,36}$/);
});

test('installation_id is null for anonymous tier', () => {
Expand Down
Loading