Skip to content

Commit 675216b

Browse files
wip v28
1 parent 19c9c15 commit 675216b

20 files changed

Lines changed: 1310 additions & 100 deletions

File tree

.env

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,11 @@ SUPABASE_JWT_SECRET=your-supabase-jwt-secret
1010
SUPABASE_PUBLISHABLE_KEY=sb_publishable_your-key
1111
SUPABASE_SECRET_KEY=sb_secret_your-key
1212

13+
# Bitcode Bitcoin-wallet OAuth provider for Supabase custom auth
14+
BITCODE_BITCOIN_OAUTH_CLIENT_ID=bitcode-bitcoin-wallet
15+
BITCODE_BITCOIN_OAUTH_CLIENT_SECRET=2d6601ae3a5a6b299181794bf51459968dde1f2e5dbaa57ccd71fe4db12a7740
16+
BITCODE_BITCOIN_OAUTH_ALLOWED_REDIRECT_ORIGINS=https://tkpyosihuouusyaxtbau.supabase.co
17+
BITCODE_BITCOIN_OAUTH_ALLOWED_REDIRECT_URIS=https://tkpyosihuouusyaxtbau.supabase.co/auth/v1/callback
18+
BITCODE_BITCOIN_OAUTH_SUPABASE_CALLBACK_URL=https://tkpyosihuouusyaxtbau.supabase.co/auth/v1/callback
19+
1320
BITCODE_MOCK_EXA=true

.env.local

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,3 +51,10 @@ BITCODE_WRITE_STEP_TRACES=1
5151
BITCODE_DEBUG_STOP_AFTER_PLAN=1
5252
# Optional size guard for full traces (characters). Leave unset for full detail.
5353
#BITCODE_TRACE_MAX_SIZE=5000
54+
55+
# Bitcode Bitcoin-wallet OAuth provider for Supabase custom auth
56+
BITCODE_BITCOIN_OAUTH_CLIENT_ID=bitcode-bitcoin-wallet
57+
BITCODE_BITCOIN_OAUTH_CLIENT_SECRET=2d6601ae3a5a6b299181794bf51459968dde1f2e5dbaa57ccd71fe4db12a7740
58+
BITCODE_BITCOIN_OAUTH_ALLOWED_REDIRECT_ORIGINS=https://tkpyosihuouusyaxtbau.supabase.co
59+
BITCODE_BITCOIN_OAUTH_ALLOWED_REDIRECT_URIS=https://tkpyosihuouusyaxtbau.supabase.co/auth/v1/callback
60+
BITCODE_BITCOIN_OAUTH_SUPABASE_CALLBACK_URL=https://tkpyosihuouusyaxtbau.supabase.co/auth/v1/callback

BITCODE_SPEC_V28_NOTES.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,16 @@ The BTD tracker hover/action area now identifies the connected wallet and opens
220220
Connects must use that same wallet identity as a sufficient prerequisite to render GitHub connection controls; optional email/Supabase session state may improve persistence and notifications, but must not hide the second onboarding step after Bitcoin wallet identity is staged.
221221
Verbose QA telemetry is allowed behind explicit flags so local testnet passes can observe provider detection, Leather/Xverse signing, local wallet persistence, user-data merge, navigation identity, and BTD auxillary entry without making normal production consoles noisy.
222222

223+
The May 12 Supabase auth correction makes signed Bitcoin wallet auth the session origin, not a post-hoc anonymous-session attachment.
224+
Supabase built-in Web3 providers do not cover Bitcoin wallet providers, so V28 defines a Bitcode-owned custom OAuth2 provider named `custom:bitcode-bitcoin`.
225+
The provider authorization page is `/tps/wallet/authorize`; it detects Bitcoin-capable browser wallets, captures a signed Bitcode Bitcoin authentication message, and redirects back to the Supabase callback with an authorization code.
226+
The machine endpoints are `/api/wallet/oauth/token` and `/api/wallet/oauth/userinfo`.
227+
Token exchange validates the configured OAuth client secret, allowed Supabase callback origin, and PKCE challenge when present.
228+
UserInfo returns a stable Bitcoin subject and wallet claims (`bitcoin_address`, provider, network, auth/payment address, proof kind) with email optional.
229+
After Supabase creates the session, Profile synchronizes the captured local wallet proof into `/api/wallet/authenticate` so the Supabase data realm can project wallet identity from the ledger-facing signed proof.
230+
Sign-out clears both the Supabase auth session and the local staged wallet identity.
231+
V28 still treats BIP322 signature verification as a captured-proof boundary (`server_signature_verifier_pending`) unless a verified server-side Bitcoin signature verifier is added before promotion; that verifier should become a V28 blocker only if signed-wallet identity is promoted from captured proof to cryptographic verification.
232+
223233
These changes remain V28 scope because they are MVP-readiness and trust issues.
224234
The deeper Auxillaries version retains hierarchy, spacing, diagnostics, recovery, and provider-management expansion after V28 proves the contained shell, mocked prerequisite reads, fail-closed testnet-readiness, and no-console-error baseline.
225235

BITCODE_V28_QA.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -342,6 +342,11 @@ Automated verification after this implementation pass:
342342
- `pnpm -C uapi exec jest --runInBand --testMatch '**/tests/userConnectionsGithubRoute.test.ts'`: 6 passed after GitHub connection-status token redaction.
343343
- `pnpm -C uapi exec tsc --noEmit --pretty false`: pass after GitHub App callback/setup and public install-link wiring.
344344
- `pnpm -C uapi run build`: pass after GitHub App callback/setup and public install-link wiring.
345+
- May 12 Bitcoin wallet auth formalization: V28 no longer uses anonymous Supabase sign-in as the wallet persistence origin. Profile starts Supabase custom OAuth through `custom:bitcode-bitcoin`, `/tps/wallet/authorize` captures the signed wallet proof, `/api/wallet/oauth/token` and `/api/wallet/oauth/userinfo` let Supabase create the user session, and Profile then synchronizes the same wallet proof into `/api/wallet/authenticate`.
346+
- Supabase custom provider dashboard values for testnet: provider type `OAuth2`, identifier `custom:bitcode-bitcoin`, name `Bitcode Bitcoin Wallet`, authorization URL `<public staging-or-tunnel origin>/tps/wallet/authorize`, token URL `<public staging-or-tunnel origin>/api/wallet/oauth/token`, userinfo URL `<public staging-or-tunnel origin>/api/wallet/oauth/userinfo`, scopes `profile wallet:bitcoin`, PKCE enabled, and email optional enabled. Cloud Supabase cannot call `127.0.0.1` token/userinfo URLs, so local QA needs either a deployed preview/staging origin or a tunnel that uses the same OAuth client secret as the local lane.
347+
- Required matching app env for testnet lane: `BITCODE_BITCOIN_OAUTH_CLIENT_ID=<same client id configured in Supabase>`, `BITCODE_BITCOIN_OAUTH_CLIENT_SECRET=<same client secret configured in Supabase>`, `BITCODE_BITCOIN_OAUTH_ALLOWED_REDIRECT_ORIGINS=https://tkpyosihuouusyaxtbau.supabase.co`, and `BITCODE_BITCOIN_OAUTH_ALLOWED_REDIRECT_URIS=https://tkpyosihuouusyaxtbau.supabase.co/auth/v1/callback` if the Supabase URL env is unavailable, plus the existing Supabase publishable/secret keys.
348+
- `pnpm -C uapi exec jest --runInBand tests/api/walletOAuthRoutes.test.ts tests/api/walletAuthenticateRoute.test.ts`: 9 passed after Bitcoin wallet custom OAuth implementation.
349+
- `pnpm -C uapi exec tsc --noEmit --pretty false`: pass after Bitcoin wallet custom OAuth implementation.
345350
- `pnpm -C uapi exec tsc --noEmit --pretty false`: pass after the formal protocol package split.
346351
- `pnpm -C uapi run test:e2e:commercial-mvp`: 50 passed after Conversations streaming, Conversations exit, and Terminal transaction-search stabilization.
347352
- `npm --prefix protocol-demonstration run test:integration`: 58 passed after standalone demonstration/package-boundary cleanup.
@@ -410,6 +415,11 @@ NEXT_PUBLIC_SUPABASE_URL=https://tkpyosihuouusyaxtbau.supabase.co \
410415
SUPABASE_URL=https://tkpyosihuouusyaxtbau.supabase.co \
411416
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY=<testnet Supabase publishable key> \
412417
SUPABASE_SECRET_KEY=<testnet Supabase secret key> \
418+
BITCODE_BITCOIN_OAUTH_CLIENT_ID=bitcode-bitcoin-wallet \
419+
BITCODE_BITCOIN_OAUTH_CLIENT_SECRET=<same value configured in Supabase custom provider> \
420+
BITCODE_BITCOIN_OAUTH_ALLOWED_REDIRECT_ORIGINS=https://tkpyosihuouusyaxtbau.supabase.co \
421+
BITCODE_BITCOIN_OAUTH_ALLOWED_REDIRECT_URIS=https://tkpyosihuouusyaxtbau.supabase.co/auth/v1/callback \
422+
BITCODE_BITCOIN_OAUTH_SUPABASE_CALLBACK_URL=https://tkpyosihuouusyaxtbau.supabase.co/auth/v1/callback \
413423
GITHUB_APP_CLIENT_ID=<GitHub App client ID> \
414424
GITHUB_APP_CLIENT_SECRET=<GitHub App client secret> \
415425
GITHUB_APP_REDIRECT_URI=http://127.0.0.1:3001/github/callback \

uapi/.env

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,13 @@ SUPABASE_AUTH_GITHUB_CLIENT_SECRET=YOUR_GITHUB_OAUTH_CLIENT_SECRET
4141
GITHUB_OAUTH_CLIENT_ID=YOUR_GITHUB_OAUTH_CLIENT_ID
4242
GITHUB_OAUTH_CLIENT_SECRET=YOUR_GITHUB_OAUTH_CLIENT_SECRET
4343

44+
# Bitcode Bitcoin-wallet OAuth provider for Supabase custom auth
45+
BITCODE_BITCOIN_OAUTH_CLIENT_ID=bitcode-bitcoin-wallet
46+
BITCODE_BITCOIN_OAUTH_CLIENT_SECRET=2d6601ae3a5a6b299181794bf51459968dde1f2e5dbaa57ccd71fe4db12a7740
47+
BITCODE_BITCOIN_OAUTH_ALLOWED_REDIRECT_ORIGINS=https://tkpyosihuouusyaxtbau.supabase.co
48+
BITCODE_BITCOIN_OAUTH_ALLOWED_REDIRECT_URIS=https://tkpyosihuouusyaxtbau.supabase.co/auth/v1/callback
49+
BITCODE_BITCOIN_OAUTH_SUPABASE_CALLBACK_URL=https://tkpyosihuouusyaxtbau.supabase.co/auth/v1/callback
50+
4451
# Optional build + runtime hooks ---------------------------------------------
4552
REGISTRY=
4653
GITHUB_TOKEN=

uapi/.env.example

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,14 @@ SUPABASE_AUTH_GOOGLE_CLIENT_SECRET=<your Google OAuth client secret>
1111
SUPABASE_AUTH_GITHUB_CLIENT_ID=<your GitHub OAuth client ID>
1212
SUPABASE_AUTH_GITHUB_CLIENT_SECRET=<your GitHub OAuth client secret>
1313

14+
# Supabase custom OAuth2 provider for Bitcode Bitcoin wallet authentication.
15+
# The Supabase provider identifier is `custom:bitcode-bitcoin`.
16+
BITCODE_BITCOIN_OAUTH_CLIENT_ID=bitcode-bitcoin-wallet
17+
BITCODE_BITCOIN_OAUTH_CLIENT_SECRET=<same value configured as the custom provider client secret>
18+
BITCODE_BITCOIN_OAUTH_ALLOWED_REDIRECT_ORIGINS=<your Supabase project origin, e.g. https://project-ref.supabase.co>
19+
BITCODE_BITCOIN_OAUTH_ALLOWED_REDIRECT_URIS=<your Supabase auth callback URL, e.g. https://project-ref.supabase.co/auth/v1/callback>
20+
BITCODE_BITCOIN_OAUTH_SUPABASE_CALLBACK_URL=<your Supabase auth callback URL>
21+
1422
# Supabase JWT secret (server-side)
1523
SUPABASE_JWT_SECRET=<your Supabase JWT secret>
1624

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import {
2+
BITCODE_BITCOIN_OAUTH_SCOPES,
3+
createBitcoinWalletAuthorizationCode,
4+
} from '@/lib/bitcoin-wallet-oauth-provider';
5+
6+
export const runtime = 'nodejs';
7+
8+
type AuthorizationCodeRequest = {
9+
oauth?: {
10+
client_id?: unknown;
11+
redirect_uri?: unknown;
12+
scope?: unknown;
13+
code_challenge?: unknown;
14+
code_challenge_method?: unknown;
15+
};
16+
wallet?: unknown;
17+
};
18+
19+
function readString(value: unknown) {
20+
return typeof value === 'string' && value.trim() ? value.trim() : null;
21+
}
22+
23+
function jsonResponse(body: unknown, init?: ResponseInit) {
24+
return new Response(JSON.stringify(body), {
25+
...init,
26+
headers: {
27+
'Content-Type': 'application/json',
28+
...(init?.headers ?? {}),
29+
},
30+
});
31+
}
32+
33+
export async function POST(request: Request) {
34+
let body: AuthorizationCodeRequest;
35+
try {
36+
body = (await request.json()) as AuthorizationCodeRequest;
37+
} catch {
38+
return jsonResponse({ error: 'Invalid Bitcoin wallet OAuth request body.' }, { status: 400 });
39+
}
40+
41+
try {
42+
const code = createBitcoinWalletAuthorizationCode({
43+
clientId: readString(body.oauth?.client_id) ?? '',
44+
redirectUri: readString(body.oauth?.redirect_uri) ?? '',
45+
scope: readString(body.oauth?.scope) ?? BITCODE_BITCOIN_OAUTH_SCOPES,
46+
codeChallenge: readString(body.oauth?.code_challenge),
47+
codeChallengeMethod: readString(body.oauth?.code_challenge_method),
48+
wallet: (body.wallet ?? {}) as any,
49+
});
50+
51+
return jsonResponse({
52+
code,
53+
expires_in: 300,
54+
});
55+
} catch (error) {
56+
return jsonResponse(
57+
{ error: error instanceof Error ? error.message : 'Bitcoin wallet OAuth authorization failed.' },
58+
{ status: 400 },
59+
);
60+
}
61+
}
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import {
2+
createBitcoinWalletAccessToken,
3+
readOAuthClientCredentials,
4+
validateOAuthClientCredentials,
5+
verifyBitcoinWalletAuthorizationCode,
6+
verifyPkce,
7+
} from '@/lib/bitcoin-wallet-oauth-provider';
8+
9+
export const runtime = 'nodejs';
10+
11+
function readString(value: FormDataEntryValue | null) {
12+
return typeof value === 'string' && value.trim() ? value.trim() : null;
13+
}
14+
15+
function jsonResponse(body: unknown, init?: ResponseInit) {
16+
return new Response(JSON.stringify(body), {
17+
...init,
18+
headers: {
19+
'Content-Type': 'application/json',
20+
...(init?.headers ?? {}),
21+
},
22+
});
23+
}
24+
25+
async function readBody(request: Request) {
26+
const contentType = request.headers.get('content-type') ?? '';
27+
if (contentType.includes('application/json')) {
28+
const json = await request.json();
29+
return new URLSearchParams(
30+
Object.entries(json && typeof json === 'object' ? json : {}).map(([key, value]) => [
31+
key,
32+
typeof value === 'string' ? value : value == null ? '' : String(value),
33+
]),
34+
);
35+
}
36+
37+
return new URLSearchParams(await request.text());
38+
}
39+
40+
export async function POST(request: Request) {
41+
let body: URLSearchParams;
42+
try {
43+
body = await readBody(request);
44+
} catch {
45+
return jsonResponse({ error: 'invalid_request', error_description: 'Invalid OAuth token body.' }, { status: 400 });
46+
}
47+
48+
const credentials = readOAuthClientCredentials(request, body);
49+
if (!validateOAuthClientCredentials(credentials)) {
50+
return jsonResponse({ error: 'invalid_client' }, { status: 401 });
51+
}
52+
53+
if (body.get('grant_type') !== 'authorization_code') {
54+
return jsonResponse({ error: 'unsupported_grant_type' }, { status: 400 });
55+
}
56+
57+
const code = body.get('code');
58+
if (!code) {
59+
return jsonResponse({ error: 'invalid_request', error_description: 'Missing authorization code.' }, { status: 400 });
60+
}
61+
62+
try {
63+
const codePayload = verifyBitcoinWalletAuthorizationCode(code);
64+
const redirectUri = readString(body.get('redirect_uri'));
65+
if (redirectUri && redirectUri !== codePayload.redirect_uri) {
66+
return jsonResponse({ error: 'invalid_grant', error_description: 'Redirect URI mismatch.' }, { status: 400 });
67+
}
68+
69+
if (!verifyPkce({
70+
codeChallenge: codePayload.code_challenge,
71+
codeChallengeMethod: codePayload.code_challenge_method,
72+
codeVerifier: body.get('code_verifier'),
73+
})) {
74+
return jsonResponse({ error: 'invalid_grant', error_description: 'PKCE verification failed.' }, { status: 400 });
75+
}
76+
77+
const token = createBitcoinWalletAccessToken({
78+
codePayload,
79+
scope: body.get('scope'),
80+
});
81+
82+
return jsonResponse({
83+
access_token: token.accessToken,
84+
token_type: 'Bearer',
85+
expires_in: token.expiresIn,
86+
scope: codePayload.scope,
87+
});
88+
} catch (error) {
89+
return jsonResponse(
90+
{
91+
error: 'invalid_grant',
92+
error_description: error instanceof Error ? error.message : 'Authorization code is invalid.',
93+
},
94+
{ status: 400 },
95+
);
96+
}
97+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import {
2+
buildBitcoinWalletUserInfo,
3+
verifyBitcoinWalletAccessToken,
4+
} from '@/lib/bitcoin-wallet-oauth-provider';
5+
6+
export const runtime = 'nodejs';
7+
8+
function readBearerToken(request: Request) {
9+
const authorization = request.headers.get('authorization') ?? '';
10+
const match = authorization.match(/^Bearer\s+(.+)$/i);
11+
return match?.[1]?.trim() || null;
12+
}
13+
14+
function jsonResponse(body: unknown, init?: ResponseInit) {
15+
return new Response(JSON.stringify(body), {
16+
...init,
17+
headers: {
18+
'Content-Type': 'application/json',
19+
...(init?.headers ?? {}),
20+
},
21+
});
22+
}
23+
24+
async function handleUserInfo(request: Request) {
25+
const token = readBearerToken(request);
26+
if (!token) {
27+
return jsonResponse({ error: 'missing_token' }, { status: 401 });
28+
}
29+
30+
try {
31+
const payload = verifyBitcoinWalletAccessToken(token);
32+
return jsonResponse(buildBitcoinWalletUserInfo(payload));
33+
} catch (error) {
34+
return jsonResponse(
35+
{
36+
error: 'invalid_token',
37+
error_description: error instanceof Error ? error.message : 'Bitcoin wallet token is invalid.',
38+
},
39+
{ status: 401 },
40+
);
41+
}
42+
}
43+
44+
export const GET = handleUserInfo;
45+
export const POST = handleUserInfo;

0 commit comments

Comments
 (0)