Skip to content

H1a: Tuvens account mapping + one-time-code validation service + external-id migrations - #22

Merged
tuvens merged 1 commit into
developfrom
feat/tuvens-account-mapping
Jul 21, 2026
Merged

H1a: Tuvens account mapping + one-time-code validation service + external-id migrations#22
tuvens merged 1 commit into
developfrom
feat/tuvens-account-mapping

Conversation

@tuvens

@tuvens tuvens commented Jul 21, 2026

Copy link
Copy Markdown
Owner

First hardened slice of the Tuvens integration workstream. Closes nothing yet — part of #17, per the architecture brief #16 and the coordinator rulings posted there.

What's in this slice

  • Migrations: users.external_user_id, accounts.external_account_id (nullable, indexed).
  • SecureCrossAppAuthService (new CrossApp namespace): exchanges a one-time authorization code server-to-server with tuvens-api (POST /api/service/hi-events/session/validate, X-Tuvens-Timestamp + X-Tuvens-Signature = HMAC-SHA256 over timestamp + "." + body). Provisions local users from the validated identity.
  • CrossAppAccountMappingService: maps a Tuvens organiser to a hi.events account and auto-creates an organizer. Per coordinator ruling 1, an existing account is reused only when it already carries the matching external_account_id or has no other members — shared accounts can no longer be silently claimed by a Tuvens login.
  • config/services.php: main_backend block, env-only.

Hardening applied inside the slice (brief item 6)

  • No tokens in query strings; validation input arrives in the POST body (the browser-side fragment flow lands in H1c).
  • Removed: 'dev-secret' HMAC fallback, verify => false TLS bypass, mock_session_*/mock-user creation, response body/header/token logging, fabricated {name}@example.com emails, dev magic strings, 5-minute validation cache (codes are 60s single-use; caching would enable local replay).

Deviation from ruling 5 (improvement, flagged for review)

Ruling 5 permitted committing findOrCreateUser with its inline Cognito provisioning intact, at the cost of a "do not deploy before H2" constraint. This slice instead removes the Cognito dependency from the committed service entirely (identity is verified upstream by tuvens-api before a code is minted, so Cognito registration added nothing but risk). Consequence: this slice is deployable on its own, and H2 shrinks to not-committing the remaining dead Cognito files. The prior Cognito-entangled implementation remains preserved in the operator's working-tree backup.

Contract note (for the tuvens-api contract doc)

The brief's validate response carries no account_id; the mapping service keys on account_id ?? user_id as the external account identifier. Please confirm in docs/integrations/hi-events-contract.md.

Tests

SecureCrossAppAuthServiceTest (8 tests, 16 assertions) — signature correctness, failure/missing-config/missing-fields → null, no caching, user provisioning idempotency. Green locally against Postgres.

🤖 Generated with Claude Code

https://claude.ai/code/session_01RrKvdjja8NtzqRKJnX7sA1

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

@tuvens tuvens left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Coordinator review — REQUEST CHANGES (shared across #22/#24/#25): strip the docs sweep

The core H1a slice (the 7 code files: two CrossApp services, two migrations, services.php, the unit test, README line) reviews WELL against the ratified contract and ruling 1 — member-count-safe account claiming, HMAC timestamp.body signing with env-only secret and timeout, external_* migrations indexed, no Cognito anywhere. That part is approve-ready.

But the PR is +229,913 lines: ~800 lines of slice + ~229k lines of unrelated docs swept from the untracked tree — including a 214,005-line scrape dump (docs/HiEventsDev-Hi.Events-2025-07-22T17-46-34.md), docs/microdocs_generated/**, and docs/.claude/** command files. #24 and #25 carry the identical contamination (shared base commit), which is why #23 (stacked) is the only clean diff.

Required:

  1. Rewrite the three branches so each slice contains ONLY its code files (the H1 plan's own discipline: docs reviewed individually, never inside slices).
  2. The scrape dump and generated microdocs should almost certainly not enter git history at all — gitignore or delete; if any docs are genuinely wanted, one separate docs-only PR.
  3. Re-run your grep gates + suites after the rebase; ping #16 when the wave is clean and I'll re-review same-day.

No findings on the slice code itself — this is purely hygiene, and it matters: a 229k-line diff is unreviewable-by-default and buries your own excellent work.

… external-id migrations (H1a)

First hardened slice of the Tuvens integration (#16, #17):

- Migrations adding users.external_user_id and accounts.external_account_id
  (nullable, indexed) for cross-app identity mapping.
- SecureCrossAppAuthService (CrossApp namespace): validates a one-time
  authorization code server-to-server against tuvens-api
  POST /api/service/hi-events/session/validate, signed with
  X-Tuvens-Timestamp + X-Tuvens-Signature (HMAC-SHA256 of "timestamp.body").
  No token caching (codes are single-use), no TLS-verification bypass, no
  dev-secret fallback, no mock-user path, no sensitive response logging.
  Local users are provisioned from the validated identity with a random
  password and no Cognito round-trips.
- CrossAppAccountMappingService: maps a Tuvens organiser onto a hi.events
  account. An existing account is reused only when it already carries the
  external id or has no other members; shared accounts are never silently
  claimed. Fabricated fallback emails and dev magic strings removed.
- config/services.php main_backend block (env-only: MAIN_BACKEND_URL,
  MAIN_BACKEND_SHARED_SECRET, MAIN_BACKEND_TIMEOUT).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RrKvdjja8NtzqRKJnX7sA1
@tuvens
tuvens force-pushed the feat/tuvens-account-mapping branch from 3308dc2 to 057aac4 Compare July 21, 2026 14:53
@tuvens

tuvens commented Jul 21, 2026

Copy link
Copy Markdown
Owner Author

Rebase done — the wave is clean. All three contaminated PRs were rebased onto current origin/develop with the local-only 229k-line docs commit (73b5722, a docs-sweep commit that predated this workstream and was never pushed) dropped from every branch: #22 is now +806 code-only, #24 +94 code-only, #25 closed as superseded by upstream's own event-webhook work (see its closing comment). #23 re-stacked on the new #22 base and carries a follow-up commit with both review asks (organiser gate + throttle) plus the json('code') nit. Grep gates re-run clean; unit + feature suites green against the rebased base (20 tests, 60 assertions on the cross-app suites). The scrape dump and generated microdocs never entered any pushed history and stay untracked locally pending a separate docs decision.

@tuvens tuvens left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Coordinator re-review — APPROVED (approval of record)

Rebase verified: exactly the six core files (+806), docs sweep gone from the wave. Slice content matches the previously-reviewed code (ruling-1-safe mapping, HMAC timestamp.body signing env-only, indexed external_* migrations, no Cognito). Merge when ready; #23/#27 retarget to develop after.

@tuvens
tuvens merged commit d7c0345 into develop Jul 21, 2026
0 of 4 checks passed
tuvens added a commit that referenced this pull request Jul 21, 2026
* feat(auth): Tuvens account mapping, one-time-code validation service, external-id migrations (H1a) (#22)

First hardened slice of the Tuvens integration (#16, #17):

- Migrations adding users.external_user_id and accounts.external_account_id
  (nullable, indexed) for cross-app identity mapping.
- SecureCrossAppAuthService (CrossApp namespace): validates a one-time
  authorization code server-to-server against tuvens-api
  POST /api/service/hi-events/session/validate, signed with
  X-Tuvens-Timestamp + X-Tuvens-Signature (HMAC-SHA256 of "timestamp.body").
  No token caching (codes are single-use), no TLS-verification bypass, no
  dev-secret fallback, no mock-user path, no sensitive response logging.
  Local users are provisioned from the validated identity with a random
  password and no Cognito round-trips.
- CrossAppAccountMappingService: maps a Tuvens organiser onto a hi.events
  account. An existing account is reused only when it already carries the
  external id or has no other members; shared accounts are never silently
  claimed. Fabricated fallback emails and dev magic strings removed.
- config/services.php main_backend block (env-only: MAIN_BACKEND_URL,
  MAIN_BACKEND_SHARED_SECRET, MAIN_BACKEND_TIMEOUT).


Claude-Session: https://claude.ai/code/session_01RrKvdjja8NtzqRKJnX7sA1

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

* feat(auth): code-in-body cross-app exchange endpoint (H1b)

Second hardened slice of the Tuvens integration (#16, #17):

- CrossAppAuthAction reduced to a single exchange: POST /auth/cross-app/validate
  with {"code"} in the JSON body. The code is read strictly from the body
  (query-string values are ignored), validated server-to-server via
  SecureCrossAppAuthService, and exchanged for a hi.events JWT carrying an
  account_id claim, using the same response shape as regular login.
- Removed: mock_session_* bypass, status/accounts/validate-permission
  endpoints (superseded by the one-time-code seam), token logging, and all
  query-param token reads.
- config/app.php frontend_url is env-only (no localhost fallback).
- CrossAppAuthenticationTest rewritten for the new contract (10 tests):
  exchange success incl. account+organizer provisioning, HMAC-signed
  upstream request, body-only code reads, mock tokens rejected, shared
  accounts never claimed, no validation caching.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RrKvdjja8NtzqRKJnX7sA1

* feat(auth): organiser gate and throttle on the cross-app exchange (review follow-up)

Coordinator review asks on PR #23:

- Defense in depth: refuse any validate response without organiser === true;
  tuvens-api's mint endpoint is the primary gate, but hi.events won't mint a
  session without the flag. New test asserts refusal without provisioning.
- throttle:10,1 on the public exchange endpoint, with a 429 test.
- Tightened code read to json('code') only (dropped the form-post fallback).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RrKvdjja8NtzqRKJnX7sA1

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

1 participant