Skip to content

Commit 72d332c

Browse files
Isolator acmclaude
authored andcommitted
fix(share): preserve the shared query across the OAuth login redirect
A share link carries the SQL in the URL hash (#<base64>). When the recipient wasn't signed in, the OAuth redirect to the IdP dropped the hash (redirect_uri is path-only and fragments don't round-trip), and the in-memory tab was wiped by the full-page navigation — so they landed on a blank /sql after signing in. bootstrap now stashes the decoded query in sessionStorage (`oauth_shared_sql`), which survives the same-tab redirect like oauth_state/verifier, and restores it when the hash is gone, clearing it once consumed on a signed-in render. Verified in-browser: not-signed-in load stashes the query + shows login; the post-redirect load (no hash, signed in) restores it into the editor and clears the stash. 353 tests pass; main.js at 100%. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QennTvGKAtJZrv9EpQagef
1 parent 6ba2943 commit 72d332c

2 files changed

Lines changed: 26 additions & 4 deletions

File tree

src/main.js

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,15 +49,24 @@ export async function bootstrap(app, env) {
4949
hist.replaceState(null, '', loc.origin + loc.pathname + (qs ? '?' + qs : '') + loc.hash);
5050
}
5151

52-
const sharedSql = decodeSqlFromHash(loc.hash);
52+
// A shared query rides in the URL hash, which is lost through the OAuth
53+
// redirect (and we strip it below). Stash it in sessionStorage so it survives
54+
// the round-trip and restore it once we're back, signed in.
55+
let sharedSql = decodeSqlFromHash(loc.hash);
56+
if (sharedSql) ss.setItem('oauth_shared_sql', sharedSql);
57+
else sharedSql = ss.getItem('oauth_shared_sql') || '';
5358
if (sharedSql) {
5459
app.state.tabs[0].sql = sharedSql;
5560
app.state.tabs[0].name = 'Shared query';
5661
hist.replaceState(null, '', loc.pathname + loc.search);
5762
}
5863

59-
if (app.token && !isTokenExpired(app.token, 0)) app.renderApp();
60-
else app.showLogin(callbackError);
64+
if (app.token && !isTokenExpired(app.token, 0)) {
65+
ss.removeItem('oauth_shared_sql'); // consumed
66+
app.renderApp();
67+
} else {
68+
app.showLogin(callbackError);
69+
}
6170
return { callbackError, signedIn: app.isSignedIn() };
6271
}
6372

tests/unit/main.test.js

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,14 +120,27 @@ describe('bootstrap', () => {
120120
expect(app.showLogin).toHaveBeenCalledWith('OAuth token exchange failed: plain failure');
121121
});
122122

123-
it('seeds the first tab from a share-link hash', async () => {
123+
it('seeds the first tab from a share-link hash (and stashes it for login)', async () => {
124124
const app = fakeApp();
125125
const sql = 'SELECT 1';
126126
const hash = '#' + btoa(unescape(encodeURIComponent(sql)));
127127
const env = fakeEnv({ location: { href: 'https://ch/sql' + hash, origin: 'https://ch', pathname: '/sql', search: '', hash } });
128128
await bootstrap(app, env);
129129
expect(app.state.tabs[0].sql).toBe('SELECT 1');
130130
expect(app.state.tabs[0].name).toBe('Shared query');
131+
expect(env.sessionStorage.getItem('oauth_shared_sql')).toBe('SELECT 1'); // survives a login redirect
132+
});
133+
134+
it('restores a shared query from sessionStorage after the OAuth round-trip', async () => {
135+
// The hash is gone after the IdP redirect; the stash carries it through.
136+
const app = fakeApp({ token: valid, isSignedIn: () => true });
137+
const env = fakeEnv({ location: { href: 'https://ch/sql', origin: 'https://ch', pathname: '/sql', search: '', hash: '' } });
138+
env.sessionStorage.setItem('oauth_shared_sql', 'SELECT 42');
139+
await bootstrap(app, env);
140+
expect(app.state.tabs[0].sql).toBe('SELECT 42');
141+
expect(app.state.tabs[0].name).toBe('Shared query');
142+
expect(app.renderApp).toHaveBeenCalled();
143+
expect(env.sessionStorage.getItem('oauth_shared_sql')).toBeNull(); // consumed on render
131144
});
132145

133146
it('preserves extra query params while stripping oauth ones', async () => {

0 commit comments

Comments
 (0)