Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
967463b
refactor(auth): remove invite-code onboarding
dcadenas Sep 5, 2026
3aa8d81
fix(auth): preserve verification dependencies after invite removal
dcadenas Sep 5, 2026
a9a5f92
docs(e2e): remove retired invite service note
dcadenas Sep 5, 2026
7408b6c
chore(ratchets): lock invite removal reductions
dcadenas Sep 10, 2026
9b4c24f
fix(auth): reconcile invite removal with current main
dcadenas Sep 12, 2026
716a515
chore(ratchets): lock reductions after main reconciliation
dcadenas Sep 12, 2026
fe4b29f
refactor(auth): remove dead key-container anonymous import API
dcadenas Sep 12, 2026
436af23
chore(ratchets): relock service god-file sizes
dcadenas Sep 12, 2026
106cd46
fix(logging): keep legacy invite codes out of diagnostics
NotThatKindOfDrLiz Sep 13, 2026
3cc5774
fix(auth): clear retired invite data during startup
NotThatKindOfDrLiz Sep 13, 2026
b6e7a7c
refactor(analytics): remove unused user-property API
NotThatKindOfDrLiz Sep 13, 2026
ee101bb
docs(auth): repair invite-removal cleanup
NotThatKindOfDrLiz Sep 13, 2026
713691d
fix(logging): keep legacy invite codes out of the unknown-path log
mbradley Sep 14, 2026
009b4d6
test(auth): run retired invite flow in service CI
NotThatKindOfDrLiz Sep 14, 2026
db9eaaf
test(auth): mock unavailable desktop platform services
NotThatKindOfDrLiz Sep 14, 2026
e223e23
fix(router): preserve async cleanup import after rebase
NotThatKindOfDrLiz Sep 15, 2026
dd0a7d4
chore(ratchets): reconcile async baseline with current main
NotThatKindOfDrLiz Sep 15, 2026
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
51 changes: 2 additions & 49 deletions .agents/skills/e2e-test/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,18 +77,15 @@ the upgrade was actually for.
| FunnelCake API | 47777 | REST API, under `/api/` on the same proxy |
| Blossom | 43003 | Media server |
| Postgres | 15432 | Keycast DB |
| Invite | 43004 | divine-invite-darshan (Viceroy) |

The app reaches these at `10.0.2.2` from the emulator. Cleartext to
loopback hosts is permitted in every build type on both platforms.

### Only start what your flow needs

Most services are irrelevant to any given test, and `local_up`
failing on one does not mean you are blocked. An invite-only flow
needs `invite` alone: a locally-generated nsec signs on-device, so
no Keycast, and `onboarding_mode=open` means no invite gate. Check
what is actually healthy before debugging a service you never call:
failing on one does not mean you are blocked. Check what is actually
healthy before debugging a service your flow never calls:

```bash
docker compose -f local_stack/docker-compose.yml ps
Expand Down Expand Up @@ -161,40 +158,6 @@ whole `up` (idempotent — it restarts whatever died) up to 3 attempts,
compose output or in the failed containers' logs. A port clash or a
bad image fails straight through rather than retrying pointlessly.

### Running a locally built backend

Compose pulls `ghcr.io/divinevideo/divine-invite-darshan:e2e`. To
test an unmerged backend branch, build it and tag it as that name so
compose uses the local image without pulling:

```bash
docker build -f Dockerfile.local -t divine-invite-darshan:local .
docker tag divine-invite-darshan:local ghcr.io/divinevideo/divine-invite-darshan:e2e
```

The published image is amd64-only, and Compose selects `linux/amd64`
explicitly so Apple Silicon can run it through emulation. A local build
on Apple Silicon is arm64-native, so select that platform before starting
the stack:

```bash
export INVITE_PLATFORM=linux/arm64
```

`invite` is one of the few services without `pull_policy: always`, so
a plain `mise run local_up` keeps your tag. Use `local_up_cached` if
you have overridden a service that *does* pull on every start.

### Cross-repo gotcha: `kv-store-data.json`

The invite service (`divine-invite-darshan`) reads a gitignored
`kv-store-data.json`. A **fresh worktree of that repo does not have
it**, and without it the entire invite-service suite fails. Seed it:

```bash
printf '{}' > kv-store-data.json
```

## Emulator

```bash
Expand Down Expand Up @@ -312,16 +275,6 @@ in the file when the exit code disagrees with the summary.

Write `input and output`, not `input/output`.

### NIP-98 URL binding

The invite service rejects a signed request whose `u` tag does not
match the URL the server saw: `auth_invalid_binding`, HTTP 401. The
client must sign the **same base URL it calls**. Signing
`http://10.0.2.2:43004` while calling `http://localhost:43004` fails;
signing and calling the same host works. If you switch the emulator
between `10.0.2.2` and a forwarded `localhost`, switch the signing
base URL with it.

### Provider error caching

Providers using `requireIdentity` (or similar non-nullable getters)
Expand Down
2 changes: 1 addition & 1 deletion .claude/rules/ui_theming.md
Original file line number Diff line number Diff line change
Expand Up @@ -406,7 +406,7 @@ Pick both from what the field holds:
| Email | `.emailAddress` | `.none` |
| Username, handle, npub, hex key, NIP-05 | `.text` | `.none` |
| URL, relay address | `.url` | `.none` |
| Password, invite code | `.text` (+ `obscureText`) | `.none` |
| Password | `.text` (+ `obscureText`) | `.none` |
| One-time code, integer amount, sat count | `.number` | `.none` |
| Decimal amount | `.numberWithOptions(decimal: true)` | `.none` |
| Display name, list title, badge name | `.text` | `.words` |
Expand Down
51 changes: 2 additions & 49 deletions .claude/skills/e2e-test/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,18 +77,15 @@ the upgrade was actually for.
| FunnelCake API | 47777 | REST API, under `/api/` on the same proxy |
| Blossom | 43003 | Media server |
| Postgres | 15432 | Keycast DB |
| Invite | 43004 | divine-invite-darshan (Viceroy) |

The app reaches these at `10.0.2.2` from the emulator. Cleartext to
loopback hosts is permitted in every build type on both platforms.

### Only start what your flow needs

Most services are irrelevant to any given test, and `local_up`
failing on one does not mean you are blocked. An invite-only flow
needs `invite` alone: a locally-generated nsec signs on-device, so
no Keycast, and `onboarding_mode=open` means no invite gate. Check
what is actually healthy before debugging a service you never call:
failing on one does not mean you are blocked. Check what is actually
healthy before debugging a service your flow never calls:

```bash
docker compose -f local_stack/docker-compose.yml ps
Expand Down Expand Up @@ -161,40 +158,6 @@ whole `up` (idempotent — it restarts whatever died) up to 3 attempts,
compose output or in the failed containers' logs. A port clash or a
bad image fails straight through rather than retrying pointlessly.

### Running a locally built backend

Compose pulls `ghcr.io/divinevideo/divine-invite-darshan:e2e`. To
test an unmerged backend branch, build it and tag it as that name so
compose uses the local image without pulling:

```bash
docker build -f Dockerfile.local -t divine-invite-darshan:local .
docker tag divine-invite-darshan:local ghcr.io/divinevideo/divine-invite-darshan:e2e
```

The published image is amd64-only, and Compose selects `linux/amd64`
explicitly so Apple Silicon can run it through emulation. A local build
on Apple Silicon is arm64-native, so select that platform before starting
the stack:

```bash
export INVITE_PLATFORM=linux/arm64
```

`invite` is one of the few services without `pull_policy: always`, so
a plain `mise run local_up` keeps your tag. Use `local_up_cached` if
you have overridden a service that *does* pull on every start.

### Cross-repo gotcha: `kv-store-data.json`

The invite service (`divine-invite-darshan`) reads a gitignored
`kv-store-data.json`. A **fresh worktree of that repo does not have
it**, and without it the entire invite-service suite fails. Seed it:

```bash
printf '{}' > kv-store-data.json
```

## Emulator

```bash
Expand Down Expand Up @@ -312,16 +275,6 @@ in the file when the exit code disagrees with the summary.

Write `input and output`, not `input/output`.

### NIP-98 URL binding

The invite service rejects a signed request whose `u` tag does not
match the URL the server saw: `auth_invalid_binding`, HTTP 401. The
client must sign the **same base URL it calls**. Signing
`http://10.0.2.2:43004` while calling `http://localhost:43004` fails;
signing and calling the same host works. If you switch the emulator
between `10.0.2.2` and a forwarded `localhost`, switch the signing
base URL with it.

### Provider error caching

Providers using `requireIdentity` (or similar non-nullable getters)
Expand Down
28 changes: 0 additions & 28 deletions .github/workflows/invite_api_client.yaml

This file was deleted.

1 change: 0 additions & 1 deletion .github/workflows/mobile_pr_preview_build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,6 @@ jobs:
--pwa-strategy=none \
--dart-define=BACKEND_URL=https://api.divine.video \
--dart-define=ENVIRONMENT=production \
--dart-define=GH_ACTIONS_PR_PREVIEW=true \
--no-source-maps

- name: Bundle preview artifact
Expand Down
10 changes: 4 additions & 6 deletions .github/workflows/mobile_service_integration_tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ on:
pull_request:
branches: [main]

# The 13 Linux-runnable suites have two dependency shapes. Eleven have a
# focused closure across ten packages and five app files. Two widget-level
# The 14 Linux-runnable suites have two dependency shapes. Eleven have a
# focused closure across ten packages and five app files. Three widget-level
# suites import the provider/router graph, which reaches nearly all mobile/lib
# and mobile/packages production code. A 60-PR sample found those scopes on
# 37% and 77% of changes respectively; running every suite costs about 14
Expand Down Expand Up @@ -136,10 +136,7 @@ jobs:
# axes. The generated-files guard accounts for every suite against these
# lists or service_suite_exclusions.txt.
#
# local_stack cannot start on a GitHub runner at all:
# divine-invite-darshan:e2e is a private GHCR package, compose resolves
# every image before starting any, so zero containers come up. The
# suites below need none of it — they stand up their own in-process relay
# The suites below need no local stack — they stand up their own in-process relay
# (integration_test/helpers/fake_relay.dart).
- name: 🚀 Run service integration tests
env:
Expand All @@ -163,6 +160,7 @@ jobs:
app_suites=(
integration_test/e2e/reel_reply_retry_double_send_test.dart
integration_test/e2e/block_leaves_kind3_follow_test.dart
integration_test/e2e/video_creation_flow_test.dart
)
# SERVICE_SUITE_LIST_END
suites=()
Expand Down
1 change: 0 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -277,7 +277,6 @@ These issues stay report-only regardless of assignee; see `<context-dir>/AGENT_T
- The local Docker stack (`local_stack/`) speaks cleartext on `10.0.2.2`, `localhost`, and `127.0.0.1`. Cleartext to those loopback hosts is permitted in every build type on all three native platforms — Android via the `<domain-config>` block in `mobile/android/app/src/main/res/xml/network_security_config.xml`, iOS via `NSAllowsLocalNetworking=true` in `mobile/ios/Runner/Info.plist`, macOS via the same key in `mobile/macos/Runner/Info.plist`. Remote cleartext is rejected on all three in every build type.
- Host resolution for local app endpoints is platform-specific: Android emulator uses `10.0.2.2`, while iOS Simulator and macOS use `localhost`. `localHost` in `mobile/lib/models/environment_config.dart` resolves the right one per platform — do not hardcode either alias in app code.
- `BLOSSOM_PUBLIC_URL` is baked into the media URLs the seeder mints, so it cannot be resolved per client. It defaults to the Android emulator alias; export `BLOSSOM_PUBLIC_URL=http://localhost:43003` before `local_stack/up.sh` when running against the iOS Simulator or macOS.
- The invite server resolves through `localHost` like every other LOCAL endpoint, so a LOCAL run reaches `http://<host>:43004` without a define. An explicit `--dart-define=INVITE_SERVER_URL` still wins — `local_stack/run_android_local.sh` passes one, which is now redundant but harmless.
- Web resolves local endpoints to `localhost`, but nothing wires a LOCAL web run: `AppEnvironment.local` is absent from the environment picker in `mobile/lib/screens/developer_options_screen.dart`, and neither web build selects it — `mobile_web_production_deploy.yml` pins `DEFAULT_ENV=PRODUCTION`, and the PR-preview build pins no `DEFAULT_ENV` at all, so it inherits the compiled `PRODUCTION` default. CORS is not the obstacle — funnelcake, the relay, and Blossom all send `Access-Control-Allow-Origin: *`, Keycast allows any `http://localhost:<port>` origin before consulting `ALLOWED_ORIGINS`, and a WebSocket handshake is not subject to CORS at all.
- User-installed CAs are not trusted in any build. If you need to proxy-debug with Charles or mitmproxy, add a single `<certificates src="user" />` line to `<trust-anchors>` in `mobile/android/app/src/main/res/xml/network_security_config.xml` in your working copy and don't commit it; CI's transport-security guard will block any commit that re-enables user-CA trust. (For iOS and macOS, plist-edit `NSAppTransportSecurity` similarly and revert before commit.)
- If you add a new exception to any native config, update `mobile/scripts/check_native_transport_security.sh` so the guard recognises the allowance. It covers four files: the Android XML plus the `Runner` plists for iOS and macOS and the iOS notification-service extension.
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ New feature work follows the layered flow `UI -> BLoC/Cubit -> Repository -> Cli
The workspace is organized into around 75 packages, grouped roughly as:

- `mobile/packages/*_repository/` — data-access and feature repositories (feed, videos, comments, follow, profile, notifications, and more).
- `mobile/packages/*_client/` and `mobile/packages/*_api_client/` — API and platform client boundaries (for example `verifier_client`, `funnelcake_api_client`, `invite_api_client`).
- `mobile/packages/*_client/` and `mobile/packages/*_api_client/` — API and platform client boundaries (for example `verifier_client` and `funnelcake_api_client`).
- `mobile/packages/models/` — shared model types and generated serialization.
- `mobile/packages/nostr_*` — Nostr SDK, client, key management, and app-bridge behavior (`nostr_sdk`, `nostr_client`, `nostr_key_manager`, `nostr_app_bridge_repository`).
- `mobile/packages/divine_camera/`, `mobile/packages/divine_video_player/`, `mobile/packages/media_cache/` — capture, playback, and caching.
Expand Down
11 changes: 0 additions & 11 deletions codemagic.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -711,17 +711,8 @@ definitions:
--dart-define=PROOFMODE_SIGNING_SERVER_TOKEN=$PROOFMODE_SIGNING_SERVER_TOKEN \
--dart-define=SUPPORTERS_API_BASE_URL=$SUPPORTERS_API_BASE_URL \
--dart-define=FF_DIVINE_SUPPORTERS=$FF_DIVINE_SUPPORTERS

bash scripts/check_ios_analytics_product.sh \
--app build/ios/iphonesimulator/Runner.app
# GH_ACTIONS_PR_PREVIEW sets forceOpenOnboarding on the invite client
# (main.dart), so getClientConfig() reports OnboardingMode.open and the
# invite gate self-redirects to account creation. Without it every flow
# that signs up stops at "Add your invite code" and the suite would need
# a live invite code as a managed, expirable secret. The gate itself is
# covered directly by invite_gate_screen_test.dart.
# The flag is named for its original PR-preview use; it is not
# GitHub-Actions specific.
- &build_ios_e2e
name: Build iOS Simulator app
script: |
Expand All @@ -730,7 +721,6 @@ definitions:
--dart-define=ZENDESK_CLIENT_ID=$ZENDESK_CLIENT_ID \
--dart-define=ZENDESK_URL=$ZENDESK_URL \
--dart-define=DEFAULT_ENV=$DEFAULT_ENV \
--dart-define=GH_ACTIONS_PR_PREVIEW=true \
--dart-define=PROOFMODE_SIGNING_SERVER_ENDPOINT=$PROOFMODE_SIGNING_SERVER_ENDPOINT \
--dart-define=PROOFMODE_SIGNING_SERVER_TOKEN=$PROOFMODE_SIGNING_SERVER_TOKEN \
--dart-define=SUPPORTERS_API_BASE_URL=$SUPPORTERS_API_BASE_URL \
Expand All @@ -743,7 +733,6 @@ definitions:
--dart-define=ZENDESK_CLIENT_ID=$ZENDESK_CLIENT_ID \
--dart-define=ZENDESK_URL=$ZENDESK_URL \
--dart-define=DEFAULT_ENV=$DEFAULT_ENV \
--dart-define=GH_ACTIONS_PR_PREVIEW=true \
--dart-define=PROOFMODE_SIGNING_SERVER_ENDPOINT=$PROOFMODE_SIGNING_SERVER_ENDPOINT \
--dart-define=PROOFMODE_SIGNING_SERVER_TOKEN=$PROOFMODE_SIGNING_SERVER_TOKEN \
--dart-define=SUPPORTERS_API_BASE_URL=$SUPPORTERS_API_BASE_URL \
Expand Down
14 changes: 1 addition & 13 deletions docs/ANALYTICS_OBSERVABILITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,7 @@ The reserved Firebase Analytics `user_id` field is the deliberate exception to
the pubkey rule above. It is the authenticated account's exact 64-character hex
pubkey, never an npub and never a hash. It is identity metadata, not a custom
event parameter. Login and restored identity set the same value in Analytics
and Crashlytics; logout clears both and clears account-scoped invite
attribution. This is owned by `analyticsIdentitySync` in
and Crashlytics; logout clears both. This is owned by `analyticsIdentitySync` in
`mobile/lib/providers/auth_providers.dart`, kept deliberately independent of
the Zendesk identity sync so the campaign's BigQuery/ClickHouse join cannot be
broken by a change to the support-desk integration.
Expand Down Expand Up @@ -171,24 +170,13 @@ Arm B was previously a "Record a Video" action under the variant name
retired rather than reused so the two treatments never share a bucket — data
before and after the swap is not comparable within one variant name.

## Invite Attribution

After the invite service confirms redemption, the normalized code is set as
the Firebase Analytics user property `invite_code`. Failed redemptions do not
set it. Any change of authenticated identity clears it, so a second account on
the device cannot inherit the first account's attribution. Logout is not the
only such change: an in-place account switch never passes through an
unauthenticated state.

## Required Firebase Admin Setup

Complete this before campaign traffic. GA4 stores unregistered parameters but
does not make them queryable as dimensions retroactively.

1. Create an event-scoped custom dimension named `mode` for event parameter
`mode`.
2. Create a user-scoped custom dimension named `invite_code` for user property
`invite_code`.

The GA4 reporting identity setting does not gate the BigQuery `user_id` field;
use BigQuery as the campaign source of truth.
Expand Down
Loading
Loading