AINATIVEM-44 OAuth callback, token repository, and pipedrive client generator#7
Conversation
…ation in oauth state
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.
…i_domain protocol
…le, support sqlite
…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
There was a problem hiding this comment.
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.tswith 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.ymlplus 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/redirecthandler that usescreateAuthRedirect()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.
| 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'); |
| const { code, state } = req.query as { code?: string; state?: string }; | ||
|
|
||
| if (!state || !verifyState(state)) { | ||
| res.status(400).send('Invalid state parameter'); | ||
| return; | ||
| } | ||
|
|
||
| if (!code) { |
| const oauth2 = new v2.OAuth2Configuration({ | ||
| clientId: process.env.PIPEDRIVE_CLIENT_ID ?? '', | ||
| clientSecret: process.env.PIPEDRIVE_CLIENT_SECRET ?? '', | ||
| redirectUri: process.env.PIPEDRIVE_REDIRECT_URI ?? '', | ||
| }); |
| oauth2.onTokenUpdate = (token) => { | ||
| if (stored) upsertToken(stored.companyId, stored.userId, token); |
| FROM node:22-alpine | ||
| WORKDIR /app | ||
| COPY package*.json ./ | ||
| RUN npm install |
| "lint": "eslint src", | ||
| "test": "vitest run", | ||
| "typecheck": "tsc --noEmit", | ||
| "clean": "rm -rf apps/", |
| 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.
| await writeFile( | ||
| join(outputDir, 'Dockerfile'), | ||
| dedent` | ||
| FROM node:22-alpine |
There was a problem hiding this comment.
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"]
`,
);
}
| if (!options.installDeps) steps.push('npm install'); | ||
| steps.push('docker compose up'); |
There was a problem hiding this comment.
I think this command will not work. Do we need this change?
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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.
Summary
docker-compose.ymlgeneration into the database generator stepPIPEDRIVE_OAUTH_HOSToverride support for pointing at non-production OAuth servers (e.g. testboxes)app.tsgenerator to useSourceFileBuilderfor conditional imports/mountsNodeProjectBuilderwith aBuildSteppattern for composing scaffold steps without scattered conditionalsRouterMountBuilderandenvVarAccesstemplate helpersTest plan
npm testpassesnpm run generateproduces a working app inapps/test-app/npm run devinsideapps/test-app/) and runs migrations