Bygg NeuroSemantics AI: minimal mobilapp, en backend, IaC och CI/CD

Ersätter den tidigare webappen på denna branch med ett fokuserat monorepo:

- apps/mobile: Expo/React Native med tre vyer (Welcome, Chat, Paywall),
  Cognito hosted UI-inloggning (Apple/Google/e-post) och In-App
  Purchase/Play Billing via en gemensam purchases-modul.
- services/api: en enda Lambda-backend — OpenAI Responses API med
  Markdown-kunskapsbas som systeminstruktioner, free tier-gräns i
  PostgreSQL (HTTP 402 -> paywall) och kvittoverifiering bakom ett
  delat PaymentProvider-interface (Apple/Google, Stripe kan läggas
  till för webb senare).
- infra: AWS CDK-stack med API Gateway (JWT-authorizer), Lambda,
  Cognito, Aurora Serverless v2 och Secrets Manager.
- db/migrations: minimal datamodell (users + usage), inga
  konversationer sparas.
- GitHub Actions: CI (lint, typecheck, test) och deploy från main.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0118DaxZR36RpnY524vRqx3z
This commit is contained in:
Claude
2026-08-03 14:35:48 +00:00
parent 508cb534b0
commit 07fbeea09b
199 changed files with 13626 additions and 18001 deletions
+106
View File
@@ -0,0 +1,106 @@
import {
AuthRequest,
exchangeCodeAsync,
makeRedirectUri,
refreshAsync,
type DiscoveryDocument,
} from 'expo-auth-session';
import * as SecureStore from 'expo-secure-store';
import * as WebBrowser from 'expo-web-browser';
import { appConfig } from '../config';
WebBrowser.maybeCompleteAuthSession();
export type Provider = 'apple' | 'google' | 'email';
interface StoredSession {
accessToken: string;
refreshToken?: string;
expiresAt: number; // epoch ms
}
const STORE_KEY = 'neurosemantics.session';
const discovery: DiscoveryDocument = {
authorizationEndpoint: `${appConfig.cognitoDomain}/oauth2/authorize`,
tokenEndpoint: `${appConfig.cognitoDomain}/oauth2/token`,
revocationEndpoint: `${appConfig.cognitoDomain}/oauth2/revoke`,
};
const redirectUri = makeRedirectUri({ scheme: 'neurosemantics', path: 'redirect' });
/** Maps our provider names to Cognito hosted UI identity providers. */
const IDP: Record<Provider, string | undefined> = {
apple: 'SignInWithApple',
google: 'Google',
email: undefined, // Hosted UI's own email/password form
};
async function persist(session: StoredSession): Promise<void> {
await SecureStore.setItemAsync(STORE_KEY, JSON.stringify(session));
}
/** Signs in via the Cognito hosted UI with PKCE. Returns the session. */
export async function signIn(provider: Provider): Promise<StoredSession> {
const request = new AuthRequest({
clientId: appConfig.cognitoClientId,
redirectUri,
scopes: ['openid', 'email'],
usePKCE: true,
extraParams: IDP[provider] ? { identity_provider: IDP[provider] as string } : {},
});
const result = await request.promptAsync(discovery);
if (result.type !== 'success' || !result.params.code) {
throw new Error('Sign-in was cancelled');
}
const tokens = await exchangeCodeAsync(
{
clientId: appConfig.cognitoClientId,
code: result.params.code,
redirectUri,
extraParams: { code_verifier: request.codeVerifier ?? '' },
},
discovery,
);
const session: StoredSession = {
accessToken: tokens.accessToken,
refreshToken: tokens.refreshToken,
expiresAt: Date.now() + (tokens.expiresIn ?? 3600) * 1000,
};
await persist(session);
return session;
}
/** Returns a valid access token, refreshing if needed, or null if signed out. */
export async function getAccessToken(): Promise<string | null> {
const raw = await SecureStore.getItemAsync(STORE_KEY);
if (!raw) return null;
const session = JSON.parse(raw) as StoredSession;
if (Date.now() < session.expiresAt - 60_000) return session.accessToken;
if (!session.refreshToken) return null;
try {
const tokens = await refreshAsync(
{ clientId: appConfig.cognitoClientId, refreshToken: session.refreshToken },
discovery,
);
const refreshed: StoredSession = {
accessToken: tokens.accessToken,
refreshToken: tokens.refreshToken ?? session.refreshToken,
expiresAt: Date.now() + (tokens.expiresIn ?? 3600) * 1000,
};
await persist(refreshed);
return refreshed.accessToken;
} catch {
await signOut();
return null;
}
}
export async function signOut(): Promise<void> {
await SecureStore.deleteItemAsync(STORE_KEY);
}