Skip to content

[in-progress] shared dev credentials for Google#2553

Open
stopachka wants to merge 14 commits intomainfrom
sharedcred
Open

[in-progress] shared dev credentials for Google#2553
stopachka wants to merge 14 commits intomainfrom
sharedcred

Conversation

@stopachka
Copy link
Copy Markdown
Contributor

@stopachka stopachka commented Apr 13, 2026

Problem

Say you want to add Google Oauth.

Even to test in development, you have to go through Google Cloud Console and create an Oauth client. Google Cloud Console is quite annoying to navigate, which means poor devex for us.

Solution

We will provide default oauth credentials for development!

Users don't have to go through Google Cloud Console. They can Oauth Clients without providing client-id and client-secret values. In development, Instant will handle those values for them.

This PR adds support for Google, but we will add support for Apple, LinkedIn, GitHub in short order

User Experience

Now the user can select "Use shared credentials".

This lets them create a google client without a client-id or client-secret:

image

During runtime, we can fill those values in for them.

Once they are ready to go to production, they can insert their product client-id and client-secret:

image

Accepted Origins

By default clients with dev credentials automatically accept localhost and exp:// as allowed origins. The user can also expand this by adding custom origins.

Limits

This is strictly meant for development. To make sure, clients with dev credentials will start to throw after 100 sign-ups.

It's also good note that when the user signs in, the consent screen says "Instant Shared Dev Credentials"

Native Flows are not supported

Right now this works for web flows, but not Native IOS, or Google Button. Those experiences require the user to know a client-id.

We chose not to share a client-id with the user, in case we end up needing to change the oauth client.

Since this is dev only though, perhaps it's not too big of a deal. We may change our decision about this later.

Implementation

1. We added shared-oauth-clients in instant-config

I've added a shared-oauth-clients table in instant-config. It has:

providerName: i.string().unique().indexed(),
clientId: i.string(),
encryptedClientSecretHexString: i.string(),

Note: we encrypt client secrets just like we do with normal oauth clients. The encrypted result is a byte array though. To store this in Instant, I encode the bytes as a hex string.

Note: providerName is currently unique. In the future we may want to have multiple clients for one provider. This will be useful to share load. At that point we can remove the unique constraint though.

shared-oauth-clients give us instant-wide client-id and client-secrets.

2. app-oauth-clients now store useSharedCredentials

  1. We store useSharedCredentials in an app-oauth-client's meta field
  2. When useSharedCredentials is enabled:
  3. ->OauthClient will find the client-id + client-secret for this provider in flags/shared-oauth-clients, and use them
  4. When we check for allowed redirect uris and origins, we automatically insert both "localhost://" and "exp://"
    5. Users can optionally add their own allowed redirect uris -- i.e their domain, or app scheme

Todos

  1. Add changes for instant-config and OuathClient on the backend
  2. Add the UI for Google
  3. Figure out how Expo will work
  4. Let's add a limit of 100 for dev credentials users
    1. Add this limit in "create client"
    2. Add this limit in oauth start
  5. Make sure we handle cli too
  6. Create a script that can "seed" a development client with our shared credentials.
  7. Create a script that can "add" a new credential, both for dev and prod

@stopachka stopachka changed the title [in-progress] shared deve credentials [in-progress] shared dev credentials Apr 13, 2026
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 13, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds optional shared OAuth credentials: UI toggle and upgrade path for Google web clients; server schema and flags ingestion for shared-oauth-clients; encryption helpers and server-side checks to substitute or validate credentials and relax redirect-origin rules when shared credentials are used.

Changes

Cohort / File(s) Summary
Client: Google auth UI & helper
client/www/components/dash/auth/Google.tsx, client/www/components/dash/auth/shared.tsx
Add web-only credential-mode toggle (shared vs custom), conditional validation/submission, hide web setup when using shared creds, add upgrade flow UI state and updateClientCredentials(...) POST helper to switch a client from shared→custom.
Server schema: flags entity
server/flags-config/instant.schema.ts
Add shared-oauth-clients entity with providerName (unique, indexed), clientId, and encryptedClientSecretHexString.
Server: flags ingestion & accessor
server/src/instant/flags.clj
Parse shared-oauth-clients from flags query results, convert hex secret to bytes, group by providerName, expose shared-oauth-clients in query-result.
Server: app oauth client model
server/src/instant/model/app_oauth_client.clj
Introduce helpers: use-shared-credentials?, encrypt-client-secret, get-shared-credential(!), provider-name resolver, and assertion helpers; refactor ->OAuthClient to substitute id/client_id/client_secret when meta.useSharedCredentials; persist encrypted secrets and client_id in create!/update!.
Server: redirect origin matching
server/src/instant/model/app_authorized_redirect_origin.clj
Add shared-credentials-default-match and authorized-origin? to allow localhost/exp-scheme matches when shared credentials are used.
Server: runtime & dash routes (OAuth flows)
server/src/instant/runtime/routes.clj, server/src/instant/dash/routes.clj
Add assert-authorized-redirect-uri! and assert-authorized-request-origin! using the new matcher and use-shared-credentials?; update OAuth start/callbacks to use these; enforce shared-credentials configuration/allowance on create/update; accept/encrypt client_id/client_secret on update.
Server: app user helper
server/src/instant/model/app_user.clj
Add users-at-least? (arity 1 and 2) to check whether an app has at least N users (used by shared-credential gating).

Sequence Diagrams

sequenceDiagram
    participant User
    participant ClientUI as Client UI (Google.tsx)
    participant API as Dash API (routes)
    participant Flags as Flags Store (shared-oauth-clients)
    participant DB as Database

    User->>ClientUI: Select credential mode (shared or custom) & submit
    alt Shared selected
        ClientUI->>API: POST create/update client (meta.useSharedCredentials = true)
        API->>Flags: Read shared-oauth-clients for provider
        Flags-->>API: Return provider credential (encrypted)
        API->>API: Decrypt secret, substitute client_id/secret
        API->>DB: Persist OAuth client record (meta.useSharedCredentials = true)
        API-->>ClientUI: Return OAuthClient (credentials substituted, not exposed)
    else Custom selected
        ClientUI->>API: POST create/update client (client_id, client_secret)
        API->>API: Encrypt client_secret
        API->>DB: Persist encrypted secret and client_id
        API-->>ClientUI: Return OAuthClient (client_id visible)
    end
    ClientUI-->>User: Render client details / setup steps (conditional)
Loading
sequenceDiagram
    participant BrowserApp as OAuth App (redirect_uri)
    participant AuthServer as Authorization Routes
    participant Matcher as Redirect Validator

    BrowserApp->>AuthServer: Authorization request / token callback (Origin/redirect_uri)
    AuthServer->>AuthServer: Load client metadata
    alt client.meta.useSharedCredentials == true
        AuthServer->>Matcher: shared-credentials-default-match(redirect_uri)
        Matcher-->>AuthServer: match result (localhost/exp allowed)
    else
        AuthServer->>Matcher: find-match(redirect_uri, authorized-origins)
        Matcher-->>AuthServer: match result
    end
    alt matched
        AuthServer-->>BrowserApp: Proceed (allow redirect / token exchange)
    else
        AuthServer-->>BrowserApp: Error (unauthorized origin)
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Suggested reviewers

  • nezaj
🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title describes the main feature: adding shared dev credentials for Google OAuth. It aligns with the primary objective of the PR, though '[in-progress]' indicates it is still being developed.
Description check ✅ Passed The pull request description clearly explains the problem (poor developer experience creating OAuth clients), the solution (providing shared dev credentials), and the user experience with concrete examples and implementation details.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch sharedcred

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (3)
server/src/instant/runtime/routes.clj (1)

210-216: Minor: get-all-for-app called unconditionally but unused when use-shared-credentials? is true.

The authorized-origins binding is fetched even when use-shared-credentials? is true, but it's only used in the else branch. Consider moving the fetch inside the conditional to avoid the DB call.

♻️ Proposed refactor
         use-shared-credentials? (get (:meta client) "useSharedCredentials")
-        authorized-origins (app-authorized-redirect-origin-model/get-all-for-app
-                            {:app-id (:app_id client)})
         matched-origin (if use-shared-credentials?
                          (app-authorized-redirect-origin-model/localhost-match redirect-uri)
-                         (app-authorized-redirect-origin-model/find-match
-                          authorized-origins
-                          redirect-uri))
+                         (let [authorized-origins (app-authorized-redirect-origin-model/get-all-for-app
+                                                    {:app-id (:app_id client)})]
+                           (app-authorized-redirect-origin-model/find-match
+                            authorized-origins
+                            redirect-uri)))
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/src/instant/runtime/routes.clj` around lines 210 - 216, The code
currently calls app-authorized-redirect-origin-model/get-all-for-app and binds
authorized-origins unconditionally even when use-shared-credentials? true; to
avoid the unnecessary DB call, move the get-all-for-app invocation so it only
runs in the branch where use-shared-credentials? is false (i.e., call
app-authorized-redirect-origin-model/localhost-match when
use-shared-credentials? is true, and otherwise fetch authorized-origins with
app-authorized-redirect-origin-model/get-all-for-app and pass it into
app-authorized-redirect-origin-model/find-match along with redirect-uri and
client).
server/src/instant/model/app_authorized_redirect_origin.clj (1)

71-75: localhost-match ignores port, but find-match considers it — potential inconsistency.

find-match (lines 79-83) appends the port to the host when present (e.g., localhost:3000), but localhost-match only checks :host. A redirect URI like http://localhost:3000/callback would match via localhost-match (host = "localhost"), which is likely the desired behavior for dev credentials. However, this diverges from how find-match constructs the host string.

If the intent is that any localhost port should work for shared credentials, the current implementation is correct. If consistency is needed, consider documenting this difference or aligning the approaches.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/src/instant/model/app_authorized_redirect_origin.clj` around lines 71
- 75, localhost-match currently only checks :host while find-match builds a
host+port string; to make behavior consistent, update localhost-match to include
the port when present (e.g., produce {:service (str "localhost:" port)}) by
reading :port from parsed-url, or alternatively change find-match to normalize
localhost to just "localhost"—choose one and apply consistently across
localhost-match and find-match so both use the same host-with-port format;
locate and modify the localhost-match function and ensure any callers that
expect {:service "localhost"} are updated accordingly.
server/src/instant/model/app_oauth_client.clj (1)

167-178: Consider clarifying the UUID source in the comment.

The comment instructs to "Copy the id from the modal" but it might be clearer to specify this is the id of the newly created shared-oauth-clients entity in the instant-config app, not a randomly generated UUID.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/src/instant/model/app_oauth_client.clj` around lines 167 - 178,
Clarify that the UUID referenced in the example is the id of the newly created
shared-oauth-clients entity in the instant-config app (not a random UUID);
update the comment around encrypt-client-secret and encryptedClientSecret to
explicitly say "use the id of the shared-oauth-clients entry from the
instant-config modal" and optionally note where to find that id in the UI so
readers know exactly which value to copy.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@client/www/components/dash/auth/Google.tsx`:
- Around line 433-476: handleUpgradeCredentials currently deletes the existing
OAuth client with deleteClient before calling addClient, which can leave the
user without any client if addClient fails; change the flow in
handleUpgradeCredentials to create the new client first (call addClient with the
provided upgradeClientId/upgradeClientSecret and a temporary clientName if
needed), on successful add call onAddClient(resp.client) and only then call
deleteClient for the old client and onDeleteClient(client); also handle the case
where deleteClient fails after a successful add by surfacing an error and
optionally cleaning up the newly-created client, and ensure isLoading/errorToast
handling remains correct.

---

Nitpick comments:
In `@server/src/instant/model/app_authorized_redirect_origin.clj`:
- Around line 71-75: localhost-match currently only checks :host while
find-match builds a host+port string; to make behavior consistent, update
localhost-match to include the port when present (e.g., produce {:service (str
"localhost:" port)}) by reading :port from parsed-url, or alternatively change
find-match to normalize localhost to just "localhost"—choose one and apply
consistently across localhost-match and find-match so both use the same
host-with-port format; locate and modify the localhost-match function and ensure
any callers that expect {:service "localhost"} are updated accordingly.

In `@server/src/instant/model/app_oauth_client.clj`:
- Around line 167-178: Clarify that the UUID referenced in the example is the id
of the newly created shared-oauth-clients entity in the instant-config app (not
a random UUID); update the comment around encrypt-client-secret and
encryptedClientSecret to explicitly say "use the id of the shared-oauth-clients
entry from the instant-config modal" and optionally note where to find that id
in the UI so readers know exactly which value to copy.

In `@server/src/instant/runtime/routes.clj`:
- Around line 210-216: The code currently calls
app-authorized-redirect-origin-model/get-all-for-app and binds
authorized-origins unconditionally even when use-shared-credentials? true; to
avoid the unnecessary DB call, move the get-all-for-app invocation so it only
runs in the branch where use-shared-credentials? is false (i.e., call
app-authorized-redirect-origin-model/localhost-match when
use-shared-credentials? is true, and otherwise fetch authorized-origins with
app-authorized-redirect-origin-model/get-all-for-app and pass it into
app-authorized-redirect-origin-model/find-match along with redirect-uri and
client).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 57ea0fb5-ff09-4311-839e-4c3e70999abd

📥 Commits

Reviewing files that changed from the base of the PR and between 8e12145 and ac19d4b.

📒 Files selected for processing (6)
  • client/www/components/dash/auth/Google.tsx
  • server/flags-config/instant.schema.ts
  • server/src/instant/flags.clj
  • server/src/instant/model/app_authorized_redirect_origin.clj
  • server/src/instant/model/app_oauth_client.clj
  • server/src/instant/runtime/routes.clj

Comment thread client/www/components/dash/auth/Google.tsx
…tion

Default localhost and exp:// for shared credentials, while still
checking user-configured authorized redirect origins first.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@server/src/instant/model/app_authorized_redirect_origin.clj`:
- Around line 71-80: The function shared-credential-match currently allows any
URI with host "localhost" regardless of scheme; restrict this by requiring the
scheme to be a web scheme (e.g. "http" or "https") before returning {:service
"localhost"} and keep the existing special-case for (= scheme "exp") returning
{:service "custom-scheme"}; update the conditional in shared-credential-match
(which binds parsed-url, host and scheme) to require both (= host "localhost")
and that scheme is one of the allowed web schemes.

In `@server/src/instant/runtime/routes.clj`:
- Around line 602-611: The oauth-id-token-callback path currently only validates
origin when (:client_secret client) is present and calls find-match, which skips
the shared-credential logic and can incorrectly accept/reject origins; change
its origin-check to mirror oauth-token-callback by using use-shared-credentials?
(from (get (:meta client) "useSharedCredentials")) and computing matched as (or
(app-authorized-redirect-origin-model/find-match authorized-origins origin)
(when use-shared-credentials?
(app-authorized-redirect-origin-model/shared-credential-match origin))),
removing the client_secret gate so the shared credential rules apply correctly
to the effective client (->OAuthClient).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: dced14b0-425f-47a8-a61e-7cda56401947

📥 Commits

Reviewing files that changed from the base of the PR and between 153c5ac and 289e37c.

📒 Files selected for processing (2)
  • server/src/instant/model/app_authorized_redirect_origin.clj
  • server/src/instant/runtime/routes.clj

Comment thread server/src/instant/model/app_authorized_redirect_origin.clj Outdated
Comment thread server/src/instant/runtime/routes.clj Outdated
(and (not (contains? reserved-uri-schemes uri-scheme))
(= scheme uri-scheme)))

(defn shared-credential-match [url]
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Maybe this should be something like shared-credentials-default-match.

We should add a comment block in here, saying why:

When users choose shared credentials, we automatically allow localhost and expo dev, without them having to set those explicitly in their allowed origins.

For more custom urls, they can always set them in allowed origins

(throw (ex-info "Unsupported OAuth client" {:oauth-client oauth-client}))))
(let [provider-name (get (:meta oauth-client) "providerName")
use-shared-credential? (get (:meta oauth-client) "useSharedCredentials")
oauth-client (if use-shared-credential?
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

maybe we do if-not here?

@stopachka stopachka changed the title [in-progress] shared dev credentials [in-progress] shared dev credentials for Google Apr 16, 2026
Comment thread server/src/instant/runtime/routes.clj Outdated
:let [value (get-in req [:params param])]
:when value]
[param value]))
use-shared-credentials? (get (:meta client) "useSharedCredentials")
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

small nit, but imo we should have a use-shared-credentials? function in app-oauth-client, feels brittle to be querying meta like this in multiple places

Comment thread server/src/instant/runtime/routes.clj
Comment thread server/src/instant/runtime/routes.clj Outdated
:when value]
[param value]))
use-shared-credentials? (get (:meta client) "useSharedCredentials")
authorized-origins (app-authorized-redirect-origin-model/get-all-for-app
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I do wonder if we should have a single function like assert-authorized origin, which we can re-use here and in our check below.

}
try {
setIsLoading(true);
await deleteClient({
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This is a bad idea. it can lead to a temporary break in service. imo we should add an update-client endpoint, which can take a client name, and update only the clientId and clientSecret.

- Consolidate origin validation into assert-authorized-redirect-uri! /
  assert-authorized-request-origin! helpers (oauth-start,
  oauth-token-callback, oauth-id-token-callback). Drop the stale
  :client_secret gate so shared clients still validate.
- Enforce a 100-user cap for shared credentials at oauth-start and on
  client create/update via assert-shared-credentials-allowed! +
  users-at-least? (bounded LIMIT query).
- Add use-shared-credentials? predicate and switch all call sites.
- Rename shared-credential-match -> shared-credentials-default-match
  and restrict the localhost shortcut to http/https.
- Extend update-oauth-client to accept client_id / client_secret so the
  upgrade-to-custom-credentials flow can be a single atomic update
  instead of delete + recreate.
Comment thread client/www/components/dash/auth/Google.tsx Outdated
Comment thread client/www/components/dash/auth/Google.tsx Outdated
Comment thread client/www/components/dash/auth/Google.tsx
Comment thread server/src/instant/dash/routes.clj Outdated
discovery-endpoint (when-not (= "github" provider-name)
(ex/get-param! req [:body :discovery_endpoint] string-util/coerce-non-blank-str))

_ (when (get meta "useSharedCredentials")
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

we should use a lil helper like (uses-shared-credentials? client) that checks get meta

Comment thread server/src/instant/dash/routes.clj Outdated
(ex/get-param! req [:body :discovery_endpoint] string-util/coerce-non-blank-str))

_ (when (get meta "useSharedCredentials")
(app-oauth-client-model/assert-shared-credentials-allowed! {:app-id app-id}))
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

would just pass in app-id as positional arg

Comment thread server/src/instant/dash/routes.clj Outdated
redirect-to
(url-util/redirect-url-validation-errors
redirect-to :allow-localhost? true)))
_ (when (get meta "useSharedCredentials")
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

same

;; localhost (http/https) and expo dev (exp://) as redirect targets,
;; without them having to set those explicitly in their allowed
;; origins. For custom domains or schemes, they can still add them to
;; allowed origins.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

this should be in a "" block for the function def

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@server/src/instant/dash/routes.clj`:
- Around line 631-632: When meta has "useSharedCredentials" true you must also
validate that meta contains a "providerName" and that the provider is supported
before saving; update the branch that currently calls
app-oauth-client-model/assert-shared-credentials-allowed! to first check (get
meta "providerName") and call (or add and call) an app-oauth-client-model
function like assert-shared-credential-provider-supported! (passing {:app-id
app-id :providerName providerName}) so the provider is validated early (do the
same change in the other branch referenced around lines 658-659) to avoid later
failure in app-oauth-client-model/->OAuthClient.

In `@server/src/instant/model/app_user.clj`:
- Around line 119-128: The users-at-least? check currently requests LIMIT (inc
n) and then tests (> (count rows) n), which makes the predicate true only when
there are more than n rows; update the SQL call in users-at-least? (look for
attr-id, rows and the sql/select) to pass n as the LIMIT (not (inc n)) and
change the boolean test to (>= (count rows) n) so the function returns true when
there are at least n users.

In `@server/src/instant/runtime/routes.clj`:
- Around line 233-234: The shared-credentials cap check is currently enforced
too early in the OAuth start flow; remove the call to
app-oauth-client-model/assert-shared-credentials-allowed! from the
authorization-start path (the when using
app-oauth-client-model/use-shared-credentials? block) and instead invoke that
check only on the new-user creation path: after you verify the OAuth identity
does NOT already map to an existing account and immediately before calling
app-user-model/create!; keep the guard that the client uses shared credentials
by calling app-oauth-client-model/use-shared-credentials? and then call
app-oauth-client-model/assert-shared-credentials-allowed! only in that new-user
branch.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 2264ffdf-5f1e-4301-bfb3-38227a5338aa

📥 Commits

Reviewing files that changed from the base of the PR and between 289e37c and 9a046e2.

📒 Files selected for processing (7)
  • client/www/components/dash/auth/Google.tsx
  • client/www/components/dash/auth/shared.tsx
  • server/src/instant/dash/routes.clj
  • server/src/instant/model/app_authorized_redirect_origin.clj
  • server/src/instant/model/app_oauth_client.clj
  • server/src/instant/model/app_user.clj
  • server/src/instant/runtime/routes.clj

Comment thread server/src/instant/dash/routes.clj Outdated
Comment thread server/src/instant/model/app_user.clj Outdated
Comment thread server/src/instant/runtime/routes.clj Outdated
@github-actions
Copy link
Copy Markdown
Contributor

View Vercel preview at instant-www-js-sharedcred-jsv.vercel.app.

Comment thread server/src/instant/model/app_user.clj Outdated
(fn [{:keys [get-entity]}]
(get-entity id)))))

;; Bounded existence check: returns true when the app has at least
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

move this into "" block for fn

([params] (users-at-least? (aurora/conn-pool :read) params))
([conn {:keys [app-id n]}]
(let [attr-id (system-catalog/get-attr-id "$users" "id")
rows (sql/select conn
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I wonder if we can use our hsql preformat -- imo it's cooler

- Move the 100-user cap from oauth-start to the new-user branch of
  upsert-oauth-link! so returning users aren't blocked once the app
  hits the limit.
- Derive providerName from provider_id server-side in ->OAuthClient;
  drop the redundant providerName field from the client-side shared-
  credentials meta payload.
- Validate that a shared credential is configured for the provider on
  client create/update, so saves don't silently defer failure to
  runtime.
- Rewrite users-at-least? with uhsql/preformat + formatp and fix the
  off-by-one so the predicate trips at n, not n+1.
- use-shared-credentials? now takes a meta map directly; assert-
  shared-credentials-allowed! takes app-id positionally; docstrings
  replace comment blocks on shared-credentials-default-match and
  users-at-least?.
- Drop the showUpgrade toggle in the Google Client panel; always show
  the upgrade form inline when meta.useSharedCredentials is true.
- Copy update: "localhost" -> "development (localhost and Expo)" in
  the two shared-credential notices.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@client/www/components/dash/auth/Google.tsx`:
- Around line 221-225: Fix the typo in the rel attribute for the external anchor
elements in the Google.tsx component: replace the incorrect "noreferer" with the
correct "noreferrer" so the security attribute works as intended; update each
JSX <a> instance that currently uses rel="noopener noreferer" (the external
links in this component — look for anchor tags with className="underline" and
other external hrefs inside the Google component) to use rel="noopener
noreferrer".
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 76c483e3-55fb-44ae-986d-981aa5c8d5fd

📥 Commits

Reviewing files that changed from the base of the PR and between 9a046e2 and 8984250.

📒 Files selected for processing (6)
  • client/www/components/dash/auth/Google.tsx
  • server/src/instant/dash/routes.clj
  • server/src/instant/model/app_authorized_redirect_origin.clj
  • server/src/instant/model/app_oauth_client.clj
  • server/src/instant/model/app_user.clj
  • server/src/instant/runtime/routes.clj
🚧 Files skipped from review as they are similar to previous changes (3)
  • server/src/instant/dash/routes.clj
  • server/src/instant/model/app_authorized_redirect_origin.clj
  • server/src/instant/model/app_user.clj

Comment thread client/www/components/dash/auth/Google.tsx
Comment thread client/www/components/dash/auth/Google.tsx Outdated
The dashboard has bounced between h-full, min-h-full, and h-[100dvh] on
the outer MainDashLayout div three times:

- pre #1983: h-full -> Auth tab scrolled at the wrong level
- #1983 (c71f9f5): h-full -> min-h-full -> Dashboard overflow in other
  tabs
- #2252 (a6ff11a): min-h-full -> h-[100dvh] -> Current bug: body paints
  through below the app (visible html #FBF9F6 beige band under both the
  sidebar and the content pane, with the fixed Intercom avatar sitting in
  that band - proof the visible viewport is bigger than what 100dvh
  resolves to)

Each round flipped a single Tailwind height class and traded one tab's
symptom for another's. Root cause nobody addressed: the dashboard's
sizing depends on a cascade of html/body/#__next height rules, and
viewport units (100dvh in particular) don't always match the actual
visible viewport (browser chrome transitions, DevTools docking, zoom, and
iframe embedding can all make dvh resolve smaller).

The layout is trying to satisfy three simultaneous needs:

1. Sidebar: exactly viewport-tall, never scrolls, never shrinks.
2. Explorer tab: exactly viewport-tall with independent internal scroll
   panes.
3. Auth tab: taller than viewport; needs internal scrolling in the main
   pane.

A single h-* class on a flex container can't solve all three because the
height propagation depends on parents whose rules aren't fully under the
dashboard's control.

Fix: replace 'flex h-[100dvh] w-full flex-col' with 'fixed inset-0 flex
flex-col'. fixed inset-0 makes the wrapper exactly the visual viewport
regardless of html/body/#__next sizing, address-bar state, viewport-unit
quirks, or page embedding. The sidebar's h-full now resolves against a
container that is actually viewport-tall, so it can never shrink. The
internal overflow-y-auto scroll container only triggers when tab content
genuinely exceeds the viewport.

This breaks the coupling that's been generating the whack-a-mole cycle,
so future layout refactors shouldn't re-expose the seam.
The old helper took camelCase TS args (clientId, clientSecret) and
translated them to snake_case JSON (client_id, client_secret) plus a
hardcoded meta: { useSharedCredentials: false } — two layers of
indirection for a function that's really just "POST this body to the
client update endpoint."

Rewrite as updateClient that takes the body fields directly (snake_case,
matching the wire format) via a rest spread. Callers now spell the
fields exactly as they appear in the request, and the
useSharedCredentials: false flag moves to the one caller that actually
needs it (the upgrade-to-custom-credentials flow in Google.tsx) instead
of being baked into the helper for every future caller.
The sensitive-type comment previously led with the mechanism ("use
type=text + CSS masking") which made it hard to tell why we weren't
just using type=password. Rewrite to lead with the problem: LastPass
attaches its UI to <input type=password> even when data-lpignore is
set, so when we need it to leave the field alone we render as text
and mask the value with -webkit-text-security.
rel="noopener noreferer" was spelled with a single r in 9 places across
the OAuth provider tabs and the NavTabBar. Browsers ignore unknown rel
tokens silently, so the typo meant we were not actually requesting the
no-referrer behaviour we intended for those external links. Correcting
to the standard rel="noopener noreferrer".
Two logical concerns were tangled into one comment block:

1. The general "try to prevent password managers from attaching" intent,
   which belongs with the data-*ignore / autoComplete cluster.
2. The specific reason we render sensitive fields as type="text" rather
   than type="password" — LastPass ignores data-lpignore on password
   inputs, so we opt out by never being a password input in the first
   place and mask the value visually via -webkit-text-security.

Move `type` to sit next to `style` (where the masking lives) and put
the LastPass-specific comment on that line. Put the general "password
managers, please ignore" comment on the data-*ignore block above it.
The shared/non-shared credentials section in Client was an inline IIFE
(~90 lines) that defined cancelUpgrade, googleConsoleLink, editForm, and
two conditional return branches. The surrounding Client component also
held all the upgrade-related state (showUpgrade, upgradeClientId,
upgradeClientSecret) and the handleUpgradeCredentials handler, even
though none of it is used elsewhere in Client.

Pull it out into a CredentialsEditor component that owns:
- its own showUpgrade / upgradeClientId / upgradeClientSecret /
  isLoading state
- cancelUpgrade + handleUpgradeCredentials
- the shared-credentials notice + "Set custom credentials" button
- the non-shared Copyable + "Update credentials" button
- the shared edit form used by both branches

Client drops the now-unused state and handler; its JSX shrinks to a
single <CredentialsEditor ... /> use next to the Client name Copyable.

Also fixes the noreferer -> noreferrer typo that was present on
googleConsoleLink and on several other external links in Google.tsx.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant