Skip to content

AINATIVEM-44 OAuth callback, token repository, and pipedrive client generator#7

Merged
youssef-saber-3 merged 25 commits into
masterfrom
AINATIVEM-44
May 14, 2026
Merged

AINATIVEM-44 OAuth callback, token repository, and pipedrive client generator#7
youssef-saber-3 merged 25 commits into
masterfrom
AINATIVEM-44

Conversation

@youssef-saber-3
Copy link
Copy Markdown
Contributor

Summary

  • Generates a full OAuth 2.0 install flow: HMAC-signed stateless state, redirect route, and callback route with code exchange and token storage
  • Generates a token repository using Drizzle ORM with per-dialect upsert (Postgres, MySQL, SQLite)
  • Generates a Pipedrive API client wrapper pre-wired to the token repository for authenticated requests
  • Moves docker-compose.yml generation into the database generator step
  • Adds PIPEDRIVE_OAUTH_HOST override support for pointing at non-production OAuth servers (e.g. testboxes)
  • Refactors app.ts generator to use SourceFileBuilder for conditional imports/mounts
  • Introduces NodeProjectBuilder with a BuildStep pattern for composing scaffold steps without scattered conditionals
  • Adds RouterMountBuilder and envVarAccess template helpers

Test plan

  • npm test passes
  • npm run generate produces a working app in apps/test-app/
  • Generated app starts (npm run dev inside apps/test-app/) and runs migrations
  • OAuth redirect route generates a correctly signed state and authorization URL
  • OAuth callback route validates state, exchanges code, fetches user info, and stores token
  • Pipedrive client route returns data using the stored token

Adds optional host override to OAuth2Configuration in the generated oauth
router, and a commented-out PIPEDRIVE_OAUTH_HOST example to .env.example.
Useful for pointing at non-production OAuth servers (e.g. testbox).

Also adds a clean script to package.json to remove generated apps/ directory.
…in Dockerfile

separate complete templates per database type to avoid multi-line string
interpolation that left db service properties at wrong indentation level;
use npm install instead of npm ci since generated apps have no lockfile
@youssef-saber-3 youssef-saber-3 marked this pull request as ready for review May 13, 2026 13:44
Copilot AI review requested due to automatic review settings May 13, 2026 13:44
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds generator support for a complete OAuth install/callback flow, a Drizzle-backed token repository, and a Pipedrive API client that persists refreshed tokens; also removes the (previously optional) webhooks scaffolding and shifts generated developer workflow toward Docker Compose.

Changes:

  • Generate src/oauth/state.ts + a full OAuth callback handler and wire it into the default app route.
  • Generate src/database/tokenRepository.ts with dialect-specific upsert behavior and update the generated Pipedrive client to use it.
  • Remove webhooks prompts/generator and expand database generation to always emit docker-compose.yml plus Dockerfile artifacts.

Reviewed changes

Copilot reviewed 20 out of 20 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
src/prompts/webhooks.ts Removes the webhooks prompt (feature removal).
src/prompts/webhooks.test.ts Removes tests for the deleted webhooks prompt.
src/generators/node/webhooks.ts Removes the webhooks generator scaffold.
src/generators/node/webhooks.test.ts Removes tests for the deleted webhooks generator.
src/generators/node/projectBuilder.ts Removes webhooks build step; adds generated start script.
src/generators/node/projectBuilder.test.ts Updates builder tests for the new options shape (no webhooks).
src/generators/node/pipedriveClient.ts Generates a Pipedrive client wired to the token repository.
src/generators/node/pipedriveClient.test.ts Updates tests to assert token repository wiring and no TODO stubs.
src/generators/node/oauth.ts Generates OAuth state helper + callback router code.
src/generators/node/oauth.test.ts Expands tests to cover new generated OAuth state/router content.
src/generators/node/index.ts Removes conditional webhooks step from the node generator pipeline.
src/generators/node/index.test.ts Updates expected generated file set; docker-compose now expected for sqlite.
src/generators/node/database.ts Adds token repository generation; always generates docker-compose + Dockerfile/.dockerignore.
src/generators/node/database.test.ts Updates docker-compose expectations; adds tokenRepository generation assertions.
src/generators/node/appExtensions.test.ts Updates tests for new options shape (no webhooks).
src/generators/node/app.ts Adds root route that triggers OAuth and fetches deals; adds global error handler.
src/generators/node/app.test.ts Updates tests to validate root route redirect behavior and error handler output.
src/generators/interface.ts Removes webhooks from GeneratorOptions.
src/cli.ts Removes webhooks prompt; updates “Next steps” to Docker Compose-centric workflow.
package.json Adds a clean script.
docs/superpowers/specs/2026-05-12-oauth-db-repository-design.md Adds design spec for OAuth + DB token repository generation.
docs/superpowers/plans/2026-05-12-oauth-db-repository.md Adds detailed implementation plan documentation.
Comments suppressed due to low confidence (1)

src/generators/node/oauth.ts:75

  • The generated OAuth router exports createAuthRedirect() but does not register a redirect endpoint (e.g. router.get('/redirect', ...)) even though the PR description/spec mention generating a redirect route. Either add a /redirect handler that uses createAuthRedirect() or update the docs/README to match the actual flow.
			export function createAuthRedirect(): string {
				const state = createState();
				return \`\${oauth2.authorizationUrl}&state=\${encodeURIComponent(state)}\`;
			}

			const router = Router();

			router.get('/callback', async (req, res, next) => {

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/generators/node/oauth.ts Outdated
Comment on lines +19 to +38
const CLIENT_SECRET = process.env.PIPEDRIVE_CLIENT_SECRET!;
if (!CLIENT_SECRET) throw new Error('PIPEDRIVE_CLIENT_SECRET is required');

function base64url(data: string): string {
return Buffer.from(data).toString('base64url');
}

export function createState(): string {
const payload = JSON.stringify({ nonce: randomBytes(16).toString('hex'), exp: Date.now() + STATE_TTL_MS });
const encoded = base64url(payload);
const sig = createHmac('sha256', CLIENT_SECRET).update(encoded).digest('base64url');
return \`\${encoded}.\${sig}\`;
}

export function verifyState(state: string): boolean {
const dot = state.lastIndexOf('.');
if (dot === -1) return false;
const encoded = state.slice(0, dot);
const sig = state.slice(dot + 1);
const expected = createHmac('sha256', CLIENT_SECRET).update(encoded).digest('base64url');
Comment on lines +76 to +83
const { code, state } = req.query as { code?: string; state?: string };

if (!state || !verifyState(state)) {
res.status(400).send('Invalid state parameter');
return;
}

if (!code) {
Comment thread src/generators/node/oauth.ts Outdated
Comment on lines +62 to +66
const oauth2 = new v2.OAuth2Configuration({
clientId: process.env.PIPEDRIVE_CLIENT_ID ?? '',
clientSecret: process.env.PIPEDRIVE_CLIENT_SECRET ?? '',
redirectUri: process.env.PIPEDRIVE_REDIRECT_URI ?? '',
});
Comment thread src/generators/node/pipedriveClient.ts Outdated
Comment on lines +24 to +25
oauth2.onTokenUpdate = (token) => {
if (stored) upsertToken(stored.companyId, stored.userId, token);
Comment thread src/generators/node/database.ts Outdated
FROM node:22-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install
Comment thread package.json
"lint": "eslint src",
"test": "vitest run",
"typecheck": "tsc --noEmit",
"clean": "rm -rf apps/",
Comment thread src/cli.ts Outdated
console.log(' # fill in PIPEDRIVE_CLIENT_ID and PIPEDRIVE_CLIENT_SECRET');
if (!installDeps) console.log(' npm install');
console.log(' npm run dev');
console.log(' docker compose up');
…xtensions

- cli: use printNextSteps with docker compose up for all cases (no npm run dev)
- database: keep backend-always-in-compose for non-app-extensions; use master's
  complex app/app-extension-ui services when app extensions are selected
- database: add -d flag to postgres healthcheck pg_isready command
- app.ts: add NextFunction/Request/Response imports and static asset serving
  for app extensions from master
- projectBuilder: add dev:frontend/build:frontend scripts when app extensions
  selected; keep start script
- index.test: docker-compose always present for minimal; no app extension files
…upsertToken

Move PIPEDRIVE_CLIENT_SECRET validation from module load time into a
getClientSecret() helper called at use time, so a missing env var throws
when createState/verifyState is first called rather than crashing the app
on import.

Catch the upsertToken promise in onTokenUpdate so a DB error doesn't
surface as an unhandled rejection.
Comment thread src/generators/node/database.ts Outdated
await writeFile(
join(outputDir, 'Dockerfile'),
dedent`
FROM node:22-alpine
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

master is on node:24-alpine

Current setup we have on master:

async function generateAppDockerfile(outputDir: string): Promise<void> {
	await writeFile(
		join(outputDir, 'Dockerfile.app'),
		dedent`
			FROM node:24-alpine

			RUN apk add --no-cache su-exec
			WORKDIR /app
			ENV NPM_CONFIG_USERCONFIG=/tmp/.npmrc
			RUN mkdir -p /app/node_modules && chown -R node:node /app
			USER node
			RUN npm config set registry https://registry.npmjs.org/

			COPY --chown=node:node package.json ./
			COPY --chown=node:node tsconfig.json ./
			COPY --chown=node:node src ./src

			EXPOSE 3000
			CMD ["npm", "run", "dev"]
		`,
	);
}

Comment thread src/cli.ts Outdated
Comment on lines +23 to +24
if (!options.installDeps) steps.push('npm install');
steps.push('docker compose up');
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think this command will not work. Do we need this change?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

It fails with:

app-1               | 
app-1               | node:internal/modules/run_main:107
app-1               |     triggerUncaughtException(
app-1               |     ^
app-1               | Error [ERR_MODULE_NOT_FOUND]: Cannot find package 'postgres' imported from /app/node_modules/drizzle-orm/postgres-js/driver.js
app-1               |     at Object.getPackageJSONURL (node:internal/modules/package_json_reader:301:9)
app-1               |     at packageResolve (node:internal/modules/esm/resolve:764:81)
app-1               |     at moduleResolve (node:internal/modules/esm/resolve:855:18)
app-1               |     at defaultResolve (node:internal/modules/esm/resolve:988:11)
app-1               |     at nextResolve (node:internal/modules/esm/hooks:785:28)
app-1               |     at resolveBase (file:///app/node_modules/tsx/dist/esm/index.mjs?1778755514194:2:3744)
app-1               |     at resolveDirectory (file:///app/node_modules/tsx/dist/esm/index.mjs?1778755514194:2:4243)
app-1               |     at resolveTsPaths (file:///app/node_modules/tsx/dist/esm/index.mjs?1778755514194:2:4984)
app-1               |     at resolve (file:///app/node_modules/tsx/dist/esm/index.mjs?1778755514194:2:5361)
app-1               |     at nextResolve (node:internal/modules/esm/hooks:785:28) {
app-1               |   code: 'ERR_MODULE_NOT_FOUND'
app-1               | }
app-1               | 
app-1               | Node.js v24.15.0

const rootRoute = dedent`
app.get('/', async (_req, res, next) => {
try {
const rows = await db.select().from(pipedriveTokens).orderBy(desc(pipedriveTokens.updatedAt)).limit(1);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Let's implement encryption and decryption, this is what we want to enforce vendors: https://pipedrive.atlassian.net/browse/AINATIVEM-42

src/crypto/encrypt.ts — AES-256-GCM encrypt/decrypt using Node.js built-in crypto; DB stores encrypted_access_token and encrypted_refresh_token

…, node:24-alpine

OAuth2Configuration validated clientId at construction time and crashed
before any routes could register. Move instantiation into createOauth2()
called per request in createAuthRedirect and the callback handler.

Add ./src:/app/src volume to non-app-extensions compose services so tsx
watch inside the container reloads on local file changes without needing
docker compose watch.

Bump simple Dockerfile base image to node:24-alpine to match master.
docker compose handles dependencies in both cases: the non-app-extensions
Dockerfile bakes npm install at build time, and the app-extensions compose
runs it inside the container at startup. No manual npm install needed.
@youssef-saber-3 youssef-saber-3 merged commit 0d8f3d9 into master May 14, 2026
1 check passed
@youssef-saber-3 youssef-saber-3 deleted the AINATIVEM-44 branch May 14, 2026 12:12
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.

3 participants