Conversation
#19) * feat(provision): implement signed first-party bootstrap and bound tickets * fix(provision): preserve cookie scope and verify browser ticket exchange * fix(provision): exclude launch tickets from server telemetry
* fix(oauth): bind first-party consent to the verified launch session * fix(oauth): revoke superseded consent and fail closed on partial config
…ero-downtime routing
fix(beta): guard app-only deployment
…latform build tooling
fix(oauth): show stale consent errors
feat(extension): upgrade Chrome Extension Manifest V3 and add cross-platform build tooling
| console.log('4. Creating extension.zip...'); | ||
| try { | ||
| if (process.platform === 'win32') { | ||
| execSync(`powershell -Command "Compress-Archive -Path '${distDir}/*' -DestinationPath '${zipFile}' -Force"`, { |
Bugbot couldn't run - usage limit reachedBugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit. A user or team admin can review and increase usage limits in the Cursor dashboard. (requestId: serverGenReqId_281160b7-88c4-48c7-b445-a31ac07e8ccc) |
There was a problem hiding this comment.
Code Review
This pull request introduces a secure first-party bootstrap flow for provisioning and authentication, including new backend services, controllers, and repositories, along with proxy middleware, extensive integration tests, and Sentry telemetry filtering. A critical issue was identified in the BootstrapService where comparing stringified JSON directly in a Redis Lua script is fragile due to non-guaranteed object property ordering; it is recommended to retrieve the JSON and perform field-by-field validation in TypeScript instead.
| const expected: ConsentBinding = { | ||
| consentId: input.consentId, | ||
| subject: user.providerId, | ||
| userId: user.id, | ||
| orgId: input.orgId, | ||
| clientId: input.clientId, | ||
| state: input.state, | ||
| appId: input.appId, | ||
| redirectUri: input.registeredRedirectUri, | ||
| codeChallenge: null, | ||
| codeChallengeMethod: null, | ||
| }; | ||
| const consumed = await this.redis(() => | ||
| ioRedis.eval( | ||
| "local v = redis.call('GET', KEYS[1]); local active = redis.call('GET', KEYS[2]); if v == ARGV[1] and active == ARGV[2] then redis.call('DEL', KEYS[1], KEYS[2]); return 1; end; return 0", | ||
| 2, | ||
| consentKey(input.consentId), | ||
| activeConsentKey(user.providerId), | ||
| JSON.stringify(expected), | ||
| input.consentId | ||
| ) | ||
| ); | ||
| if (consumed !== 1) throw invalid(); |
There was a problem hiding this comment.
Relying on stringified JSON equality (v == ARGV[1]) in the Redis Lua script for object comparison is highly fragile. In JavaScript/TypeScript, object property ordering is not strictly guaranteed across different runtimes or refactorings. If a developer reorders the properties of the ConsentBinding object literal in either consume or consumeConsent, or if a new optional property is added, the stringified JSON representations will differ, causing consumeConsent to silently fail with an UnauthorizedException ("Consent session changed or expired; reconnect to continue").
To make this robust and maintainable, retrieve the stored JSON string from Redis, verify that the active consent ID matches atomically, and then parse and validate the individual fields in TypeScript. This completely eliminates the dependency on JSON string serialization order while maintaining atomicity.
const storedJson = await this.redis(() =>
ioRedis.eval(
"local v = redis.call('GET', KEYS[1]); local active = redis.call('GET', KEYS[2]); if active == ARGV[1] then redis.call('DEL', KEYS[1], KEYS[2]); return v; end; return nil",
2,
consentKey(input.consentId),
activeConsentKey(user.providerId),
input.consentId
)
) as string | null;
if (!storedJson) throw invalid();
const stored: ConsentBinding = JSON.parse(storedJson);
if (
stored.consentId !== input.consentId ||
stored.subject !== user.providerId ||
stored.userId !== user.id ||
stored.orgId !== input.orgId ||
stored.clientId !== input.clientId ||
stored.state !== input.state ||
stored.appId !== input.appId ||
stored.redirectUri !== input.registeredRedirectUri ||
stored.codeChallenge !== null ||
stored.codeChallengeMethod !== null
) {
throw invalid();
}
What kind of change does this PR introduce?
Release & Architectural Alignment
Why was this change needed?
Promotes verified
devbranch features tomain:__Host-crove-auth/__Host-crove-org), and atomic single-use ticket consumption preventing replay attacks.firstPartyConsentId) bound to organization, client, and state, with race condition protection and clean fallback for superseded sessions.apps/extension, multi-platform build script (build.mjs), declarative permissions for 7+ social platforms, and connectable domains (*.crove.com,*.crove.io,*.dos.me).EcosystemModuleandEcosystemServicewithENABLE_ECOSYSTEM_SYNCfeature flag inlibraries/helpers/src/utils/ecosystem.config.ts.connection_limit,pool_timeout, andconnect_timeoutparameters on Supabase Transaction Pooler (port 6543) and direct connection (port 5432).crove_postgres-beta-volume,crove_postiz-redis-beta-data), and zero-downtime dual-alias routing in Cloudflare Tunnel.Technical Details & Scope
apps/backend/src/api/routes/provision.controller.ts: Redis Lua script atomic consume.apps/backend/src/ecosystem/: Isolated ecosystem synchronization service and module.apps/extension/: Manifest V3 compliance and cross-platform build tooling.scripts/: Docker Compose and Cloudflare Tunnel naming standardization (crove-post,crove-post-beta).libraries/nestjs-libraries/src/database/prisma/schema.prisma: Cleaned dynamic Mastra tables from Prisma schema.Verification & Testing
beta-post.crove.comandpost.crove.com: HealthHTTP 200 OK, atomic ticket provision & single-use consumption confirmed.QA
mainand runpnpm installandpnpm run build.GET https://post.crove.com/api/healthandGET https://beta-post.crove.com/api/healthreturnHTTP 200 OK.pnpm dlx tsx scripts/branding-guard.tspasses with 0 failures.Checklist:
pnpm run build).pnpm dlx tsx scripts/branding-guard.ts).Note
High Risk
Touches authentication, OAuth approval, ticket exchange, and session cookies with new Redis-backed consent rules; misconfiguration or edge cases could block DOS-Me connects or weaken replay protection.
Overview
Adds a signed first-party bootstrap path for DOS-Me: HMAC-protected
POST /internal/first-party/bootstrapprojects user/org in Postgres, returns a short-livedlaunch_urlwith anfpt_ticket, and wires ticket exchange through/v1/ticket/consumeand the Next.js proxy so tickets never hit the client UI or logs.OAuth consent is session-bound for the configured first-party client via
firstPartyConsentIdin the auth JWT and Redis consent tuples (supersession across tabs, no PKCE/redirect override, fail-closed 503 when misconfigured). Legacy JWT provisioning tickets are unchanged.Ops and packaging: CI and
deploy-beta.ps1runvalidate:beta-deploy; beta compose starts PM2 only (no Prismadb push), setsMASTRA_DISABLE_STORAGE_INIT, and renames/pins volumes; nginx uses a safer log format and dedicated/oauth/authorizehandling. Crove Post extension rebrands, widens platform host permissions, and usesbuild.mjs. Sentry filters bootstrap/ticket/oauth authorize traffic. Large integration test suites cover bootstrap, consent, proxy, and telemetry.Reviewed by Cursor Bugbot for commit 9410cf8. Configure here.