feat: release Drops Studio production platform - #13
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: e23b267f33
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| return working( | ||
| backendKind === "neon-postgres" | ||
| ? "transactional-neon-postgres" | ||
| : "transactional-private-blob-cas", | ||
| "Capability-scoped project data passed a real create, read, optimistic update and isolated cleanup cycle.", | ||
| ["project-data-create-live", "project-data-cas-live", "project-data-cleanup-live"], |
There was a problem hiding this comment.
Require capability signing before reporting project data healthy
When durable Blob or Postgres storage is configured but PROJECT_DATA_CAPABILITY_SECRET is absent, this direct backend exercise still emits a working receipt, and platformCapabilitySnapshot() prioritizes that receipt over configuration readiness. However, app/api/project-data/route.ts rejects every request without the signing secret, so the public platform reports working project data while generated apps can only receive 503 responses. Include capability creation and route-level authorization in this health check before publishing readiness.
AGENTS.md reference: AGENTS.md:L9-L10
Useful? React with 👍 / 👎.
| } finally { | ||
| await cleanupSandbox?.().catch(() => undefined); |
There was a problem hiding this comment.
Treat Sandbox deletion failure as a failed health check
If the Node command succeeds but sandbox.delete() rejects, this catch suppresses the failure after the function has already selected the working result. The persisted receipt then includes sandbox-delete-live and states that cleanup succeeded even though the persistent health Sandbox remains allocated, potentially leaking resources on every check. Cleanup failure must make the Sandbox check unavailable rather than being discarded.
AGENTS.md reference: AGENTS.md:L109-L109
Useful? React with 👍 / 👎.
| return status.status === "working" | ||
| ? working( | ||
| "neon-postgres-live", | ||
| "The Vercel-native Neon Postgres provider returned a successful bounded health query.", | ||
| ["neon-marketplace-resource", "postgres-select-live"], |
There was a problem hiding this comment.
Wire Postgres into managed operations before marking it working
Whenever SELECT 1 succeeds, this receipt causes the public managed-backend capability to be labeled working, but the new createNeonManagedPlatformDriver is referenced only by this health check; the actual managed platform still constructs InMemoryManagedData, and no production CRUD surface uses this driver. A reachable database therefore appears to be a completed managed relational backend even though product operations are not persisted there.
AGENTS.md reference: AGENTS.md:L7-L10
Useful? React with 👍 / 👎.
| const controller = new AbortController(); | ||
| let timer: ReturnType<typeof setTimeout> | undefined; | ||
| try { | ||
| const value = await Promise.race([ | ||
| operation(), | ||
| new Promise<never>((_resolve, reject) => { | ||
| timer = setTimeout(() => { | ||
| controller.abort(); |
There was a problem hiding this comment.
Cancel provider operations when their health deadline expires
When an external operation exceeds the deadline, the timeout only aborts an otherwise-unused controller; operation() receives no signal and continues running after Promise.race rejects. Hung Sandbox, Blob, or database work can therefore keep creating, writing, or deleting resources after the check reports unavailable, and subsequent scheduled checks may overlap it. Pass an abort signal through each operation or otherwise ensure timed-out work is terminated.
AGENTS.md reference: AGENTS.md:L109-L109
Useful? React with 👍 / 👎.
| font-size: 12px; | ||
| font-weight: 720; | ||
| gap: 6px; | ||
| min-height: 40px; |
There was a problem hiding this comment.
Enforce the preview control-size contract
On every home viewport where the new Share and Build project controls are visible, these declarations render their text at 12px and cap their height at 40px. e2e/contracts/ui-standards.spec.ts explicitly audits visible buttons for at least 14px text and 44px targets, so this preview cannot pass the repository's release UI gate; the copilot control also uses 12px text. Raise the new controls to the documented minimums.
AGENTS.md reference: AGENTS.md:L25-L28
Useful? React with 👍 / 👎.
| <div className="field-select-wrap"> | ||
| <select | ||
| aria-label={ariaLabel} | ||
| className="field-select" | ||
| value={value} |
There was a problem hiding this comment.
Keep the Radix select until a covered Base UI migration
This replaces the existing Radix Select with a raw native <select>, rather than the required Base UI 1.6 primitive, and therefore drops the current popup, positioning, and interaction behavior outside a bounded Base UI migration. Preserve the current Radix component or complete the mandated Base UI migration with keyboard, Axe, and visual coverage.
AGENTS.md reference: AGENTS.md:L18-L19
Useful? React with 👍 / 👎.
| signal: AbortSignal.timeout(HEALTH_TIMEOUT_MS), | ||
| }, | ||
| ); | ||
| if (!repositories.ok) throw new Error("repository receipt failed"); |
There was a problem hiding this comment.
Verify an allowlisted repository in the GitHub health check
If the installation token is valid but the installation exposes no repositories—or only repositories different from GITHUB_APP_ALLOWED_REPOSITORIES—this endpoint still returns 200 and the check is marked working because the response body is never inspected. The actual integration route permits platform credentials only for the configured allowlist, so every such operation can then fail at GitHub despite the public capability claiming a healthy installation. Validate that at least one configured allowlisted repository is present in the installation response.
AGENTS.md reference: AGENTS.md:L10-L10
Useful? React with 👍 / 👎.
| @@ -0,0 +1,128 @@ | |||
| const RECEIPT_PATH = "drops-studio/platform-health/v1/latest.json"; | |||
There was a problem hiding this comment.
Namespace persisted health receipts by environment
When preview and production deployments share the same private Blob store, both write this single latest.json object. A preview or local operator check can therefore overwrite the production receipt; production then rejects it because its environment differs and reports all provider capabilities unavailable until the next production check, while a production run similarly invalidates preview evidence. Include the deployment environment in the receipt path.
AGENTS.md reference: AGENTS.md:L10-L10
Useful? React with 👍 / 👎.
| projectSyncAvailable: context.configured | ||
| && memberProjectSyncReadiness(readinessEnvironment), |
There was a problem hiding this comment.
Advertise guest sync for every supported Blob auth mode
In a Vercel deployment with BLOB_STORE_ID and the platform-provided VERCEL=1 marker but no literal VERCEL_OIDC_TOKEN environment variable, db/project-v2-snapshots.ts considers durable Project V2 storage configured and can service the route, while memberProjectSyncReadiness() returns false. This newly added guest response consequently tells the client that sync is unavailable, so guest builds never upload or reopen their otherwise supported private snapshots. Derive this flag from the same storage readiness predicate used by the Project V2 route.
Useful? React with 👍 / 👎.
| const secrets = [ | ||
| process.env.CRON_SECRET?.trim(), | ||
| process.env.DROPS_PLATFORM_HEALTH_OPERATOR_SECRET?.trim(), | ||
| ].filter((value): value is string => Boolean(value)); |
There was a problem hiding this comment.
Reject weak health-trigger secrets in production
In production, any nonempty CRON_SECRET or DROPS_PLATFORM_HEALTH_OPERATOR_SECRET is accepted, whereas the existing Sandbox cleanup route rejects secrets shorter than 32 characters. A weak operator secret can be guessed against this unaudited endpoint to repeatedly trigger costly Sandbox creation, Blob mutations, provider requests, and GitHub token issuance. Enforce the same production secret-strength boundary before authorizing the health run.
AGENTS.md reference: AGENTS.md:L109-L109
Useful? React with 👍 / 👎.
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: ⛔ Files ignored due to path filters (89)
📒 Files selected for processing (44)
📝 WalkthroughWalkthroughThe change adds private Blob and Neon project storage, provider health receipts, health-aware capability reporting, project-sync access handling, and a redesigned responsive Drops Studio builder and preview. ChangesPlatform storage and health
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant HealthRoute as /api/platform/health
participant HealthRunner as runPlatformProviderHealthChecks
participant ReceiptStore as platform health receipt storage
participant CapabilityLoader as platformCapabilitySnapshotWithHealth
HealthRoute->>HealthRunner: run provider health checks
HealthRunner->>ReceiptStore: write validated receipt
CapabilityLoader->>ReceiptStore: read private receipt
CapabilityLoader-->>CapabilityLoader: build capability snapshot
Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1🛠️ Fix failing CI checks 💡
📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 20
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
app/api/project-data/route.ts (1)
103-127: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick winTwo routes now duplicate the forwarded-origin check with different behavior. Both routes derive a visible origin from
hostandx-forwarded-proto, but only the project-data route normalizes it, and neither restrictshostto expected values. Extract one shared helper and give it an allowlist of accepted hosts.
app/api/project-data/route.ts#L103-L127: move this implementation into a shared module, for examplelib/request-origin.ts, and validate the derived host against a configured allowlist before accepting it.app/api/projects/v2/route.ts#L83-L108: import the shared helper and delete the local copy, which also removes the default-port normalization defect.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/api/project-data/route.ts` around lines 103 - 127, Extract the requireSameOrigin implementation from app/api/project-data/route.ts lines 103-127 into a shared helper module, such as lib/request-origin.ts, accepting a configured allowlist of valid hosts and validating the derived host before accepting the origin. Update app/api/project-data/route.ts lines 103-127 to import and use the shared helper. In app/api/projects/v2/route.ts lines 83-108, remove the duplicated local origin-checking logic and import the same helper so both routes share normalized forwarded-origin handling.
🧹 Nitpick comments (9)
lib/project-data/durable-backend.ts (1)
89-99: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick winUse a state-based signal for first-write conflicts.
@vercel/blobprovidesBlobPreconditionFailedErrorforifMatchmismatches, but first-write pathname collisions remain genericBlobErrorvalues. Re-read the envelope after a failed first write. If it exists, returnconflict; otherwise returnstorage_unavailable. This avoids relying on provider message text.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@lib/project-data/durable-backend.ts` around lines 89 - 99, Update the first-write conflict handling around isBlobCompareAndSwapConflict to stop matching BlobError.message text. After a failed write without a current ETag, re-read the envelope and return conflict when it now exists; return storage_unavailable when it does not. Preserve BlobPreconditionFailedError handling for ifMatch mismatches.tests/access-tier.test.mjs (1)
195-205: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAdd an assertion for the nested
account.projectSyncvalue.The test locks the top-level
projectSyncand the note text. It does not lockguestWithPrivateStorage.account.projectSync, which staysfalseby design inlib/access-tier.tsat Line 385. Assert that value so the intended difference between the two fields is protected.💚 Proposed addition
assert.equal(guestWithPrivateStorage.projectSync, true); assert.equal(guestWithPrivateStorage.account.connected, false); + assert.equal(guestWithPrivateStorage.account.projectSync, false); assert.match(guestWithPrivateStorage.account.note, /Guest projects use actor-owned private storage/);🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tests/access-tier.test.mjs` around lines 195 - 205, Add an assertion for guestWithPrivateStorage.account.projectSync in the accessMetadata test, expecting false, while preserving the existing top-level projectSync assertion and account note checks.lib/platform-provider-health.ts (2)
115-119: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueAdd observability to the empty catch blocks.
Every check converts any failure into an
unavailablereceipt without recording the cause. When a provider degrades, operators see the status but not the reason. Log the error at warning level with the check id and without secret values.Also applies to: 245-254, 295-300, 367-372, 405-410, 440-445
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@lib/platform-provider-health.ts` around lines 115 - 119, Update the catch blocks in the health-check functions, including the block returning the "vercel-sandbox-health-failed" receipt and the additional listed checks, to accept the caught error and emit a warning-level log containing the check id and sanitized error details. Preserve each existing unavailable receipt and avoid logging secret values.
176-182: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winGuard the failure-path cleanup.
Line 177 awaits
cleanupProject?.()without acatch. Every other failure path in this module uses best-effort cleanup.runPlatformProviderHealthChecksruns all checks withPromise.all, so a rejection here would fail the whole health run instead of returning oneunavailablecheck.♻️ Proposed change
} catch { - await cleanupProject?.(); + await cleanupProject?.().catch(() => undefined); return unavailable(🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@lib/platform-provider-health.ts` around lines 176 - 182, Update the catch branch in runPlatformProviderHealthChecks to make cleanupProject best-effort by handling and suppressing any rejection from cleanupProject?.(). Ensure cleanup failure does not replace the existing unavailable("project-data-health-failed", ...) result or reject the overall Promise.all health run.lib/access-tier.ts (1)
381-388: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winTwo fields named
projectSynccarry different meanings.For a guest with private storage configured,
access.projectSyncistrueat Line 360 whileaccess.account.projectSyncisfalseat Line 385. The note at Lines 386-388 states that guest projects already use private storage. A consumer that reads the nested field concludes that sync is off; a consumer that reads the top-level field concludes that sync is on. Rename the nested field to express the account-bound meaning, for exampleaccountProjectSync, or document the distinction at the type level.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@lib/access-tier.ts` around lines 381 - 388, Rename the nested account-level projectSync field in the access object returned by the relevant access-tier logic to accountProjectSync, and update its type and all consumers accordingly. Preserve the top-level projectSync field for guest private-storage availability while making the nested field explicitly represent account-bound synchronization.components/drops-studio.tsx (1)
1708-1717: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winUse a ref instead of an
aria-labelselector to focus the prompt.The
EDIT PLANbranch locates the textarea withdocument.querySelector('textarea[aria-label="Describe your crypto project"]'). This ties runtime behavior to an accessible-name string. If that label is reworded or localized, the query returns null and the focus is lost without any error. The component already uses refs forcarouselRefandguestIdRef.♻️ Proposed refactor
const carouselRef = useRef<HTMLDivElement>(null); + const promptRef = useRef<HTMLTextAreaElement>(null);if (label === "EDIT PLAN") { if (!prompt.trim()) { - document - .querySelector<HTMLTextAreaElement>( - 'textarea[aria-label="Describe your crypto project"]', - ) - ?.focus(); + promptRef.current?.focus(); setToast("Describe your product, then create an editable plan."); return; }<textarea + ref={promptRef} value={prompt}🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@components/drops-studio.tsx` around lines 1708 - 1717, Replace the document.querySelector focus lookup in the EDIT PLAN branch with a dedicated textarea ref, following the existing carouselRef and guestIdRef patterns. Attach the ref to the prompt textarea and focus it through the ref when prompt.trim() is empty, without changing the toast or return behavior.tests/platform-capabilities.test.mjs (1)
58-89: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueConsider covering the
DROPS_MANAGED_POSTGRES_URLalias.This test exercises
DROPS_MANAGED_DATABASE_URL. The implementation at lib/platform-capabilities.ts line 56 also acceptsDROPS_MANAGED_POSTGRES_URLas an alternative marker. Add one case for that alias so a future removal of either branch fails a test.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tests/platform-capabilities.test.mjs` around lines 58 - 89, Extend the fresh production provider receipt coverage in the test around platformCapabilitySnapshot to include a case using DROPS_MANAGED_POSTGRES_URL instead of DROPS_MANAGED_DATABASE_URL. Assert the same matching capability upgrade and receipt redaction behavior so both accepted provider-marker branches remain covered.lib/platform-capabilities.ts (2)
220-230: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winBound the per-request receipt read, or cache it in process.
platformCapabilitySnapshotWithHealthawaitsreadPlatformHealthReceipt()on every call. Bothapp/platform/page.tsxandapp/api/platform/capabilities/route.tsareforce-dynamic, so each page render and each API request performs an uncached private Blob read.readPlatformHealthReceiptcatches errors but does not bound latency, so a slow Blob response delays the response.The receipt written by
runPlatformProviderHealthChecksis valid for 36 hours, so a per-request read adds no freshness. Add a short in-process TTL cache, or pass anAbortSignal.timeout(...)through the read.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@lib/platform-capabilities.ts` around lines 220 - 230, The platformCapabilitySnapshotWithHealth flow must avoid an unbounded private Blob read on every request. Add a short in-process TTL cache around readPlatformHealthReceipt, reusing cached receipt data within the TTL while preserving refresh and error behavior; alternatively, propagate an AbortSignal.timeout through the receipt-read path. Keep platformCapabilitySnapshot’s existing inputs and health snapshot behavior unchanged.
113-209: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueConsider extracting the repeated receipt-resolution pattern.
The same shape
health?.field ?? (flag ? working : missing)repeats for nine capabilities. Line 125 and line 126 also nest three ternaries on one line. A small helper would remove the duplication and make the precedence explicit.One behavioral detail is worth confirming: when a receipt reports
status: "unavailable",mode,detail, andevidencestill come from that receipt whilestatefalls back to the credential-derived value. Formanaged-backendandcollaboration, that produces a failed-checkdetailnext tostate: "working-local-test". Confirm this pairing is intended.♻️ Sketch of the helper
+ const resolve = ( + health: PlatformProviderHealthCheck | undefined, + configuredState: PlatformCapabilityState, + unconfiguredState: PlatformCapabilityState, + configured: boolean, + ) => ({ + state: health?.status === "working" + ? ("working" as const) + : configured ? configuredState : unconfiguredState, + });🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@lib/platform-capabilities.ts` around lines 113 - 209, Extract the repeated receipt-resolution logic in the platform capability definitions into a small helper that makes receipt precedence explicit for mode, detail, and evidence while deriving state from receipt status or the credential/local-test fallback. Apply it across all nine capabilities, including the nested state/mode expressions for project-data and managed-backend. Preserve the behavior where an unavailable receipt supplies its own mode, detail, and evidence while state uses the fallback value, including the managed-backend and collaboration local-test pairings.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@app/api/access/route.ts`:
- Around line 68-69: Update the projectSyncAvailable calculation in the
/api/access handler to avoid treating a merely present request OIDC header as
readiness; derive projectSync only from verified provider-backed readiness or a
validated token that Project V2 can use as storage credentials. Keep the
existing configured check, and ensure BLOB_STORE_ID-only requests cannot return
projectSync: true.
In `@app/api/platform/health/route.ts`:
- Around line 28-41: Bound the side effects in GET by guarding
runPlatformProviderHealthChecks with a shared single-flight lock and minimum-run
interval. When a run is active or the interval has not elapsed, return the last
persisted health receipt without starting provider checks; otherwise execute
once, persist the receipt, and release the guard reliably. Ensure POST,
currently aliased to GET, uses the same guard and cannot trigger a concurrent
duplicate run.
In `@app/api/project-data/route.ts`:
- Around line 30-50: Update the backend() initialization flow so backendPromise
is reset to null when the cached initialization promise rejects, allowing later
requests to retry createDurableProjectDataBackend(). Preserve successful caching
and globalThis.__DROPS_STUDIO_PROJECT_DATA_BACKEND_V2__ assignment, and mirror
the failure-cache clearing behavior used by
PostgresProjectDataBackend.#ensureSchema.
In `@app/api/projects/v2/route.ts`:
- Around line 90-104: Normalize visibleOrigin using URL origin canonicalization
before comparing it with parsedOrigin, matching the established approach in the
project-data route. Update the origin validation block around parsedOrigin so
default ports and equivalent forwarded Host values compare consistently while
preserving the existing fallback to request.nextUrl.origin.
In `@app/styles/drops-studio.previews.css`:
- Around line 790-808: The `@media` rules for .landing-studio-copilot conflict
across the 1260px and 900px breakpoints. Update the responsive styles so the
copilot remains hidden throughout the intended tablet range, or restore it only
with an explicit min-width condition, while keeping the surrounding layout
behavior consistent.
- Around line 512-523: Update the .landing-studio-toolbar button rule so its
min-height is 44px instead of 40px, ensuring visible Share and Build controls
meet the required interactive target size.
In `@components/drops-studio.tsx`:
- Around line 1824-1839: Remove the aria-label attribute from the build button
in the runPrompt("build") control. Preserve the existing visible span text and
loading-state behavior so the button’s accessible name reflects “Build now,”
“Planning…,” or “Building…” as displayed.
- Around line 1605-1635: Update the `/api/access` save flow around
`applyAccessStatus`, `projectSyncAvailable`, and `saveMemberProjectToCloud` to
pass the fresh access payload into `applyAccessStatus` and use its returned sync
availability for both the member-save gate and toast selection. Also separate
the builder snapshot and member-project save results, or rename the combined
flag to explicitly reflect intentional coupling, so each save outcome is
represented accurately.
In `@components/preview-canvas.tsx`:
- Around line 311-316: Replace the native button elements for the Share and
Build project controls in the preview canvas, along with the editable-plan
controls around the referenced section, with the shared Base UI 1.6 Button
component. Preserve each control’s existing onClick behavior, labels, icons, and
primary styling by mapping them to the Button API.
- Around line 422-425: Update the landing studio status bar in PreviewCanvas so
the “DropsTab receipt verified” label is driven by validated
receipt/provider-health state from the server capability flow, not the
client-side dataMode value. Extend or reuse the appropriate PreviewCanvasProps
evidence field and render the verified label only when that evidence is present;
retain the sample-data label for non-verified data.
In `@design-qa.md`:
- Line 5: Update the release approval entry in design-qa.md to record the
results of npm audit --omit=dev --audit-level=high, npm run build:vercel, npm
run test:e2e:prepared, and npm run test:lighthouse:prepared for release
candidate 6e06b28311d5. Retain the approval only when all four required gates
pass.
In `@lib/managed-platform/postgres-driver.ts`:
- Around line 36-79: Update the Postgres client setup in transaction() to
configure an explicit connection timeout and per-statement timeout, and ensure
the transaction queries inherit that statement bound. Update health() to use the
same connection timeout configuration and bound its SELECT 1 AS ok health query
with an explicit timeout, preserving the existing validation and latency
behavior.
- Around line 40-62: Update the transaction implementation around Pool creation
so the pool is initialized once at module scope and reused across calls, rather
than instantiated inside each transaction. Remove the per-transaction pool.end()
cleanup while preserving client acquisition, transaction commit/rollback,
release, and operation behavior.
In `@lib/platform-provider-health.ts`:
- Around line 361-366: The provider health receipts claim operations that were
not executed. In lib/platform-provider-health.ts lines 361-366, either compare
the repositories returned by the preceding check with
GITHUB_APP_ALLOWED_REPOSITORIES before emitting github-app-installation-live, or
remove the bounded-scope wording and implied allowlist evidence. In lines
466-479, either perform a real audit append and checksummed restore probe before
emitting private-recovery-storage-live, or replace that token and “configured
and live” detail with wording explicitly derived from the managed-backend and
organizations checks.
- Around line 52-76: Update timed and its callers so the AbortController signal
is passed into operation and forwarded to each provider request, including
GitHub, Blob, Postgres, and fetch calls, allowing timed-out work to stop. In
sandboxHealth, assign cleanupSandbox before starting the command and ensure the
named sandbox is deleted if creation remains pending when the timeout aborts.
- Around line 330-360: Update the timed callback around the installation token
fetch and repository request to revoke the token via DELETE
https://api.github.com/installation/token in a finally block after successful
token creation. Use the token for the revocation request and ensure revocation
runs whether the repository health check succeeds or throws, while preserving
the existing success and failure behavior.
In `@lib/project-data/durable-backend.ts`:
- Around line 281-311: Make deleted projects recreatable in both durable
backends by removing records on deletion rather than persisting tombstones. In
lib/project-data/durable-backend.ts lines 281-311, update deleteProject to call
the typed BlobStorage.del member with the existing revision/precondition
handling. In lib/project-data/durable-backend.ts lines 411-440, replace the
tombstone UPDATE with a conditional DELETE from drops_project_data_snapshots
using project_key, project_id, and store_revision, preserving conflict behavior
for revision mismatches.
- Around line 23-39: Update the information_schema.columns probe in
POSTGRES_SCHEMA_MIGRATION to constrain table_schema to the current schema,
ensuring the conditional check and subsequent ALTER TABLE target the same
search-path table rather than a same-named table elsewhere.
In `@scripts/verify-platform-providers.mjs`:
- Around line 3-6: Update the invocation of runPlatformProviderHealthChecks to
require separate explicit environment-flag opt-ins for live provider checks and
receipt persistence. Replace the current opt-out includeSandbox logic with an
opt-in condition, and gate persist: true behind its own explicit persistence
flag so both values are false by default; preserve the existing health-check
call and receipt behavior when each corresponding flag is enabled.
- Line 1: Update the execution configuration for
scripts/verify-platform-providers.mjs so its TypeScript import works across the
declared Node >=22.13.0 range, using the required runtime flag or an
appropriately raised minimum version. Add a package.json npm script that invokes
verify-platform-providers.mjs.
---
Outside diff comments:
In `@app/api/project-data/route.ts`:
- Around line 103-127: Extract the requireSameOrigin implementation from
app/api/project-data/route.ts lines 103-127 into a shared helper module, such as
lib/request-origin.ts, accepting a configured allowlist of valid hosts and
validating the derived host before accepting the origin. Update
app/api/project-data/route.ts lines 103-127 to import and use the shared helper.
In app/api/projects/v2/route.ts lines 83-108, remove the duplicated local
origin-checking logic and import the same helper so both routes share normalized
forwarded-origin handling.
---
Nitpick comments:
In `@components/drops-studio.tsx`:
- Around line 1708-1717: Replace the document.querySelector focus lookup in the
EDIT PLAN branch with a dedicated textarea ref, following the existing
carouselRef and guestIdRef patterns. Attach the ref to the prompt textarea and
focus it through the ref when prompt.trim() is empty, without changing the toast
or return behavior.
In `@lib/access-tier.ts`:
- Around line 381-388: Rename the nested account-level projectSync field in the
access object returned by the relevant access-tier logic to accountProjectSync,
and update its type and all consumers accordingly. Preserve the top-level
projectSync field for guest private-storage availability while making the nested
field explicitly represent account-bound synchronization.
In `@lib/platform-capabilities.ts`:
- Around line 220-230: The platformCapabilitySnapshotWithHealth flow must avoid
an unbounded private Blob read on every request. Add a short in-process TTL
cache around readPlatformHealthReceipt, reusing cached receipt data within the
TTL while preserving refresh and error behavior; alternatively, propagate an
AbortSignal.timeout through the receipt-read path. Keep
platformCapabilitySnapshot’s existing inputs and health snapshot behavior
unchanged.
- Around line 113-209: Extract the repeated receipt-resolution logic in the
platform capability definitions into a small helper that makes receipt
precedence explicit for mode, detail, and evidence while deriving state from
receipt status or the credential/local-test fallback. Apply it across all nine
capabilities, including the nested state/mode expressions for project-data and
managed-backend. Preserve the behavior where an unavailable receipt supplies its
own mode, detail, and evidence while state uses the fallback value, including
the managed-backend and collaboration local-test pairings.
In `@lib/platform-provider-health.ts`:
- Around line 115-119: Update the catch blocks in the health-check functions,
including the block returning the "vercel-sandbox-health-failed" receipt and the
additional listed checks, to accept the caught error and emit a warning-level
log containing the check id and sanitized error details. Preserve each existing
unavailable receipt and avoid logging secret values.
- Around line 176-182: Update the catch branch in
runPlatformProviderHealthChecks to make cleanupProject best-effort by handling
and suppressing any rejection from cleanupProject?.(). Ensure cleanup failure
does not replace the existing unavailable("project-data-health-failed", ...)
result or reject the overall Promise.all health run.
In `@lib/project-data/durable-backend.ts`:
- Around line 89-99: Update the first-write conflict handling around
isBlobCompareAndSwapConflict to stop matching BlobError.message text. After a
failed write without a current ETag, re-read the envelope and return conflict
when it now exists; return storage_unavailable when it does not. Preserve
BlobPreconditionFailedError handling for ifMatch mismatches.
In `@tests/access-tier.test.mjs`:
- Around line 195-205: Add an assertion for
guestWithPrivateStorage.account.projectSync in the accessMetadata test,
expecting false, while preserving the existing top-level projectSync assertion
and account note checks.
In `@tests/platform-capabilities.test.mjs`:
- Around line 58-89: Extend the fresh production provider receipt coverage in
the test around platformCapabilitySnapshot to include a case using
DROPS_MANAGED_POSTGRES_URL instead of DROPS_MANAGED_DATABASE_URL. Assert the
same matching capability upgrade and receipt redaction behavior so both accepted
provider-marker branches remain covered.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 3b5b7194-45c5-431c-9938-4c5bedcc6538
⛔ Files ignored due to path filters (89)
docs/design/current-home-actual.pngis excluded by!**/*.pnge2e/visual/home.spec.ts-snapshots/home-builder-chromium-1024-linux-system.pngis excluded by!**/*.pnge2e/visual/home.spec.ts-snapshots/home-builder-chromium-1440-linux-system.pngis excluded by!**/*.pnge2e/visual/home.spec.ts-snapshots/home-builder-chromium-390-linux-system.pngis excluded by!**/*.pngpackage-lock.jsonis excluded by!**/package-lock.jsonstorybook-e2e/visual.spec.ts-snapshots/action-engine-desktop-chromium-1024-linux-system.pngis excluded by!**/*.pngstorybook-e2e/visual.spec.ts-snapshots/action-engine-desktop-chromium-1024-linux.pngis excluded by!**/*.pngstorybook-e2e/visual.spec.ts-snapshots/action-engine-desktop-chromium-1440-linux-system.pngis excluded by!**/*.pngstorybook-e2e/visual.spec.ts-snapshots/action-engine-desktop-chromium-1440-linux.pngis excluded by!**/*.pngstorybook-e2e/visual.spec.ts-snapshots/action-engine-desktop-chromium-390-linux-system.pngis excluded by!**/*.pngstorybook-e2e/visual.spec.ts-snapshots/action-engine-desktop-chromium-390-linux.pngis excluded by!**/*.pngstorybook-e2e/visual.spec.ts-snapshots/alpha-channel-disconnected-chromium-1024-linux-system.pngis excluded by!**/*.pngstorybook-e2e/visual.spec.ts-snapshots/alpha-channel-disconnected-chromium-1024-linux.pngis excluded by!**/*.pngstorybook-e2e/visual.spec.ts-snapshots/alpha-channel-disconnected-chromium-1440-linux-system.pngis excluded by!**/*.pngstorybook-e2e/visual.spec.ts-snapshots/alpha-channel-disconnected-chromium-1440-linux.pngis excluded by!**/*.pngstorybook-e2e/visual.spec.ts-snapshots/alpha-channel-disconnected-chromium-390-linux-system.pngis excluded by!**/*.pngstorybook-e2e/visual.spec.ts-snapshots/alpha-channel-disconnected-chromium-390-linux.pngis excluded by!**/*.pngstorybook-e2e/visual.spec.ts-snapshots/crypto-aggregator-connected-chromium-1024-linux-system.pngis excluded by!**/*.pngstorybook-e2e/visual.spec.ts-snapshots/crypto-aggregator-connected-chromium-1024-linux.pngis excluded by!**/*.pngstorybook-e2e/visual.spec.ts-snapshots/crypto-aggregator-connected-chromium-1440-linux-system.pngis excluded by!**/*.pngstorybook-e2e/visual.spec.ts-snapshots/crypto-aggregator-connected-chromium-1440-linux.pngis excluded by!**/*.pngstorybook-e2e/visual.spec.ts-snapshots/crypto-aggregator-connected-chromium-390-linux-system.pngis excluded by!**/*.pngstorybook-e2e/visual.spec.ts-snapshots/crypto-aggregator-connected-chromium-390-linux.pngis excluded by!**/*.pngstorybook-e2e/visual.spec.ts-snapshots/crypto-game-desktop-chromium-1024-linux-system.pngis excluded by!**/*.pngstorybook-e2e/visual.spec.ts-snapshots/crypto-game-desktop-chromium-1024-linux.pngis excluded by!**/*.pngstorybook-e2e/visual.spec.ts-snapshots/crypto-game-desktop-chromium-1440-linux-system.pngis excluded by!**/*.pngstorybook-e2e/visual.spec.ts-snapshots/crypto-game-desktop-chromium-1440-linux.pngis excluded by!**/*.pngstorybook-e2e/visual.spec.ts-snapshots/crypto-game-desktop-chromium-390-linux-system.pngis excluded by!**/*.pngstorybook-e2e/visual.spec.ts-snapshots/crypto-game-desktop-chromium-390-linux.pngis excluded by!**/*.pngstorybook-e2e/visual.spec.ts-snapshots/crypto-product-hunt-empty-chromium-1024-linux-system.pngis excluded by!**/*.pngstorybook-e2e/visual.spec.ts-snapshots/crypto-product-hunt-empty-chromium-1024-linux.pngis excluded by!**/*.pngstorybook-e2e/visual.spec.ts-snapshots/crypto-product-hunt-empty-chromium-1440-linux-system.pngis excluded by!**/*.pngstorybook-e2e/visual.spec.ts-snapshots/crypto-product-hunt-empty-chromium-1440-linux.pngis excluded by!**/*.pngstorybook-e2e/visual.spec.ts-snapshots/crypto-product-hunt-empty-chromium-390-linux-system.pngis excluded by!**/*.pngstorybook-e2e/visual.spec.ts-snapshots/crypto-product-hunt-empty-chromium-390-linux.pngis excluded by!**/*.pngstorybook-e2e/visual.spec.ts-snapshots/crypto-radio-playing-chromium-1024-linux-system.pngis excluded by!**/*.pngstorybook-e2e/visual.spec.ts-snapshots/crypto-radio-playing-chromium-1024-linux.pngis excluded by!**/*.pngstorybook-e2e/visual.spec.ts-snapshots/crypto-radio-playing-chromium-1440-linux-system.pngis excluded by!**/*.pngstorybook-e2e/visual.spec.ts-snapshots/crypto-radio-playing-chromium-1440-linux.pngis excluded by!**/*.pngstorybook-e2e/visual.spec.ts-snapshots/crypto-radio-playing-chromium-390-linux-system.pngis excluded by!**/*.pngstorybook-e2e/visual.spec.ts-snapshots/crypto-radio-playing-chromium-390-linux.pngis excluded by!**/*.pngstorybook-e2e/visual.spec.ts-snapshots/crypto-siri-mobile-chromium-1024-linux-system.pngis excluded by!**/*.pngstorybook-e2e/visual.spec.ts-snapshots/crypto-siri-mobile-chromium-1024-linux.pngis excluded by!**/*.pngstorybook-e2e/visual.spec.ts-snapshots/crypto-siri-mobile-chromium-1440-linux-system.pngis excluded by!**/*.pngstorybook-e2e/visual.spec.ts-snapshots/crypto-siri-mobile-chromium-1440-linux.pngis excluded by!**/*.pngstorybook-e2e/visual.spec.ts-snapshots/crypto-siri-mobile-chromium-390-linux-system.pngis excluded by!**/*.pngstorybook-e2e/visual.spec.ts-snapshots/crypto-siri-mobile-chromium-390-linux.pngis excluded by!**/*.pngstorybook-e2e/visual.spec.ts-snapshots/loading-product-plan-chromium-1024-linux-system.pngis excluded by!**/*.pngstorybook-e2e/visual.spec.ts-snapshots/loading-product-plan-chromium-1024-linux.pngis excluded by!**/*.pngstorybook-e2e/visual.spec.ts-snapshots/loading-product-plan-chromium-1440-linux-system.pngis excluded by!**/*.pngstorybook-e2e/visual.spec.ts-snapshots/loading-product-plan-chromium-1440-linux.pngis excluded by!**/*.pngstorybook-e2e/visual.spec.ts-snapshots/loading-product-plan-chromium-390-linux-system.pngis excluded by!**/*.pngstorybook-e2e/visual.spec.ts-snapshots/loading-product-plan-chromium-390-linux.pngis excluded by!**/*.pngstorybook-e2e/visual.spec.ts-snapshots/morning-alpha-data-error-chromium-1024-linux-system.pngis excluded by!**/*.pngstorybook-e2e/visual.spec.ts-snapshots/morning-alpha-data-error-chromium-1024-linux.pngis excluded by!**/*.pngstorybook-e2e/visual.spec.ts-snapshots/morning-alpha-data-error-chromium-1440-linux-system.pngis excluded by!**/*.pngstorybook-e2e/visual.spec.ts-snapshots/morning-alpha-data-error-chromium-1440-linux.pngis excluded by!**/*.pngstorybook-e2e/visual.spec.ts-snapshots/morning-alpha-data-error-chromium-390-linux-system.pngis excluded by!**/*.pngstorybook-e2e/visual.spec.ts-snapshots/morning-alpha-data-error-chromium-390-linux.pngis excluded by!**/*.pngstorybook-e2e/visual.spec.ts-snapshots/morning-alpha-populated-chromium-1024-linux-system.pngis excluded by!**/*.pngstorybook-e2e/visual.spec.ts-snapshots/morning-alpha-populated-chromium-1024-linux.pngis excluded by!**/*.pngstorybook-e2e/visual.spec.ts-snapshots/morning-alpha-populated-chromium-1440-linux-system.pngis excluded by!**/*.pngstorybook-e2e/visual.spec.ts-snapshots/morning-alpha-populated-chromium-1440-linux.pngis excluded by!**/*.pngstorybook-e2e/visual.spec.ts-snapshots/morning-alpha-populated-chromium-390-linux-system.pngis excluded by!**/*.pngstorybook-e2e/visual.spec.ts-snapshots/morning-alpha-populated-chromium-390-linux.pngis excluded by!**/*.pngstorybook-e2e/visual.spec.ts-snapshots/personal-companion-mobile-chromium-1024-linux-system.pngis excluded by!**/*.pngstorybook-e2e/visual.spec.ts-snapshots/personal-companion-mobile-chromium-1024-linux.pngis excluded by!**/*.pngstorybook-e2e/visual.spec.ts-snapshots/personal-companion-mobile-chromium-1440-linux-system.pngis excluded by!**/*.pngstorybook-e2e/visual.spec.ts-snapshots/personal-companion-mobile-chromium-1440-linux.pngis excluded by!**/*.pngstorybook-e2e/visual.spec.ts-snapshots/personal-companion-mobile-chromium-390-linux-system.pngis excluded by!**/*.pngstorybook-e2e/visual.spec.ts-snapshots/personal-companion-mobile-chromium-390-linux.pngis excluded by!**/*.pngstorybook-e2e/visual.spec.ts-snapshots/portfolio-tamagotchi-empty-chromium-1024-linux-system.pngis excluded by!**/*.pngstorybook-e2e/visual.spec.ts-snapshots/portfolio-tamagotchi-empty-chromium-1024-linux.pngis excluded by!**/*.pngstorybook-e2e/visual.spec.ts-snapshots/portfolio-tamagotchi-empty-chromium-1440-linux-system.pngis excluded by!**/*.pngstorybook-e2e/visual.spec.ts-snapshots/portfolio-tamagotchi-empty-chromium-1440-linux.pngis excluded by!**/*.pngstorybook-e2e/visual.spec.ts-snapshots/portfolio-tamagotchi-empty-chromium-390-linux-system.pngis excluded by!**/*.pngstorybook-e2e/visual.spec.ts-snapshots/portfolio-tamagotchi-empty-chromium-390-linux.pngis excluded by!**/*.pngstorybook-e2e/visual.spec.ts-snapshots/prediction-impact-desktop-chromium-1024-linux-system.pngis excluded by!**/*.pngstorybook-e2e/visual.spec.ts-snapshots/prediction-impact-desktop-chromium-1024-linux.pngis excluded by!**/*.pngstorybook-e2e/visual.spec.ts-snapshots/prediction-impact-desktop-chromium-1440-linux-system.pngis excluded by!**/*.pngstorybook-e2e/visual.spec.ts-snapshots/prediction-impact-desktop-chromium-1440-linux.pngis excluded by!**/*.pngstorybook-e2e/visual.spec.ts-snapshots/prediction-impact-desktop-chromium-390-linux-system.pngis excluded by!**/*.pngstorybook-e2e/visual.spec.ts-snapshots/prediction-impact-desktop-chromium-390-linux.pngis excluded by!**/*.pngstorybook-e2e/visual.spec.ts-snapshots/smart-money-copy-empty-chromium-1024-linux-system.pngis excluded by!**/*.pngstorybook-e2e/visual.spec.ts-snapshots/smart-money-copy-empty-chromium-1024-linux.pngis excluded by!**/*.pngstorybook-e2e/visual.spec.ts-snapshots/smart-money-copy-empty-chromium-1440-linux-system.pngis excluded by!**/*.pngstorybook-e2e/visual.spec.ts-snapshots/smart-money-copy-empty-chromium-1440-linux.pngis excluded by!**/*.pngstorybook-e2e/visual.spec.ts-snapshots/smart-money-copy-empty-chromium-390-linux-system.pngis excluded by!**/*.pngstorybook-e2e/visual.spec.ts-snapshots/smart-money-copy-empty-chromium-390-linux.pngis excluded by!**/*.png
📒 Files selected for processing (44)
.env.exampleapp/api/access/route.tsapp/api/platform/capabilities/route.tsapp/api/platform/health/route.tsapp/api/project-data/route.tsapp/api/projects/v2/route.tsapp/platform/page.tsxapp/styles/drops-studio.builder.cssapp/styles/drops-studio.previews.cssapp/styles/drops-studio.responsive.cssapp/styles/drops-studio.setup.cssapp/styles/drops-studio.shell.csscomponents/drops-studio-setup.tsxcomponents/drops-studio.tsxcomponents/dropsbot-webhook-connection.tsxcomponents/platform/integration-catalog.tsxcomponents/platform/organization-console.tsxcomponents/platform/platform-overview.tsxcomponents/platform/platform-shell.tsxcomponents/platform/project-library.tsxcomponents/platform/template-catalog.tsxcomponents/preview-canvas.tsxcomponents/project-studio.tsxcomponents/project-v2-agent-intelligence.tsxcomponents/project-v2-studio-surface.tsxdesign-qa.mde2e/contracts/home-builder-p1.spec.tslib/access-tier.tslib/managed-platform/index.tslib/managed-platform/postgres-driver.tslib/platform-capabilities.tslib/platform-health-receipts.tslib/platform-provider-health.tslib/project-data/durable-backend.tslib/project-data/index.tslib/project-data/types.tspackage.jsonscripts/verify-platform-providers.mjstests/access-tier.test.mjstests/platform-capabilities.test.mjstests/platform-public-surfaces.test.mjstests/project-data-durable-backend.test.mjstests/vercel-sandbox-runtime-cleanup.test.mjsvercel.json
| export async function GET(request: NextRequest) { | ||
| if (!authorized(request)) { | ||
| return NextResponse.json( | ||
| { error: "Platform health authorization is required." }, | ||
| { status: 401, headers: { "cache-control": "private, no-store" } }, | ||
| ); | ||
| } | ||
| const receipt = await runPlatformProviderHealthChecks(); | ||
| return NextResponse.json(receipt, { | ||
| headers: { "cache-control": "private, no-store" }, | ||
| }); | ||
| } | ||
|
|
||
| export const POST = GET; |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift
Bound the external side effects of this endpoint.
Each authorized request runs the full provider suite. That suite creates a persistent Vercel Sandbox, writes and deletes private Blob objects, writes and deletes real project-data rows, mints a GitHub installation token, and overwrites the shared health receipt. Two concurrent requests, for example the cron invocation plus one operator call, run all of that twice in parallel and race on the single receipt path. Add a single-flight guard plus a minimum interval between runs, and return the last stored receipt when a run is already in progress.
Also note that GET and POST share one non-idempotent, state-mutating handler.
As per coding guidelines: "Every external or destructive tool must have explicit approval, timeout, quota, audit record, bounded output, and idempotency behavior."
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@app/api/platform/health/route.ts` around lines 28 - 41, Bound the side
effects in GET by guarding runPlatformProviderHealthChecks with a shared
single-flight lock and minimum-run interval. When a run is active or the
interval has not elapsed, return the last persisted health receipt without
starting provider checks; otherwise execute once, persist the receipt, and
release the guard reliably. Ensure POST, currently aliased to GET, uses the same
guard and cannot trigger a concurrent duplicate run.
Source: Coding guidelines
| let backendPromise: Promise<ProjectDataBackend> | null = null; | ||
|
|
||
| async function backend() { | ||
| if (globalThis.__DROPS_STUDIO_PROJECT_DATA_BACKEND_V2__) { | ||
| return globalThis.__DROPS_STUDIO_PROJECT_DATA_BACKEND_V2__; | ||
| } | ||
| if (process.env.DROPS_STUDIO_LOCAL_PROJECT_DATA !== "1") { | ||
| backendPromise ??= (async () => { | ||
| const durable = await createDurableProjectDataBackend(); | ||
| if (durable) return durable; | ||
| if (process.env.DROPS_STUDIO_LOCAL_PROJECT_DATA === "1") { | ||
| return new MemoryProjectDataBackend(); | ||
| } | ||
| throw new ProjectDataError( | ||
| "storage_unavailable", | ||
| "Project data storage is not configured. The generated app can continue with its labelled browser-local fallback.", | ||
| ); | ||
| } | ||
| globalThis.__DROPS_STUDIO_PROJECT_DATA_BACKEND_V2__ = new MemoryProjectDataBackend(); | ||
| return globalThis.__DROPS_STUDIO_PROJECT_DATA_BACKEND_V2__; | ||
| })(); | ||
| const resolved = await backendPromise; | ||
| globalThis.__DROPS_STUDIO_PROJECT_DATA_BACKEND_V2__ = resolved; | ||
| return resolved; | ||
| } |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
Reset backendPromise when initialization fails.
??= caches the rejected promise. After one failed createDurableProjectDataBackend() call, every later request awaits the same rejection and returns 503 for the lifetime of the process. PostgresProjectDataBackend.#ensureSchema already clears its cached promise on failure. Apply the same handling here.
🐛 Proposed fix to clear the cache on failure
- const resolved = await backendPromise;
- globalThis.__DROPS_STUDIO_PROJECT_DATA_BACKEND_V2__ = resolved;
- return resolved;
+ try {
+ const resolved = await backendPromise;
+ globalThis.__DROPS_STUDIO_PROJECT_DATA_BACKEND_V2__ = resolved;
+ return resolved;
+ } catch (error) {
+ backendPromise = null;
+ throw error;
+ }📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| let backendPromise: Promise<ProjectDataBackend> | null = null; | |
| async function backend() { | |
| if (globalThis.__DROPS_STUDIO_PROJECT_DATA_BACKEND_V2__) { | |
| return globalThis.__DROPS_STUDIO_PROJECT_DATA_BACKEND_V2__; | |
| } | |
| if (process.env.DROPS_STUDIO_LOCAL_PROJECT_DATA !== "1") { | |
| backendPromise ??= (async () => { | |
| const durable = await createDurableProjectDataBackend(); | |
| if (durable) return durable; | |
| if (process.env.DROPS_STUDIO_LOCAL_PROJECT_DATA === "1") { | |
| return new MemoryProjectDataBackend(); | |
| } | |
| throw new ProjectDataError( | |
| "storage_unavailable", | |
| "Project data storage is not configured. The generated app can continue with its labelled browser-local fallback.", | |
| ); | |
| } | |
| globalThis.__DROPS_STUDIO_PROJECT_DATA_BACKEND_V2__ = new MemoryProjectDataBackend(); | |
| return globalThis.__DROPS_STUDIO_PROJECT_DATA_BACKEND_V2__; | |
| })(); | |
| const resolved = await backendPromise; | |
| globalThis.__DROPS_STUDIO_PROJECT_DATA_BACKEND_V2__ = resolved; | |
| return resolved; | |
| } | |
| let backendPromise: Promise<ProjectDataBackend> | null = null; | |
| async function backend() { | |
| if (globalThis.__DROPS_STUDIO_PROJECT_DATA_BACKEND_V2__) { | |
| return globalThis.__DROPS_STUDIO_PROJECT_DATA_BACKEND_V2__; | |
| } | |
| backendPromise ??= (async () => { | |
| const durable = await createDurableProjectDataBackend(); | |
| if (durable) return durable; | |
| if (process.env.DROPS_STUDIO_LOCAL_PROJECT_DATA === "1") { | |
| return new MemoryProjectDataBackend(); | |
| } | |
| throw new ProjectDataError( | |
| "storage_unavailable", | |
| "Project data storage is not configured. The generated app can continue with its labelled browser-local fallback.", | |
| ); | |
| })(); | |
| try { | |
| const resolved = await backendPromise; | |
| globalThis.__DROPS_STUDIO_PROJECT_DATA_BACKEND_V2__ = resolved; | |
| return resolved; | |
| } catch (error) { | |
| backendPromise = null; | |
| throw error; | |
| } | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@app/api/project-data/route.ts` around lines 30 - 50, Update the backend()
initialization flow so backendPromise is reset to null when the cached
initialization promise rejects, allowing later requests to retry
createDurableProjectDataBackend(). Preserve successful caching and
globalThis.__DROPS_STUDIO_PROJECT_DATA_BACKEND_V2__ assignment, and mirror the
failure-cache clearing behavior used by
PostgresProjectDataBackend.#ensureSchema.
| .landing-studio-toolbar button { | ||
| background: #fff; | ||
| border: 1px solid #dce4ef; | ||
| border-radius: 9px; | ||
| color: #31415b; | ||
| cursor: pointer; | ||
| font-size: 12px; | ||
| font-weight: 720; | ||
| gap: 6px; | ||
| min-height: 40px; | ||
| padding: 0 11px; | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Increase the toolbar target height to 44px.
Line 521 sets visible Share and Build targets to 40px high. This is below the required 44 by 44 CSS pixels.
As per coding guidelines, "Every visible interactive target must be at least 44 by 44 CSS pixels."
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@app/styles/drops-studio.previews.css` around lines 512 - 523, Update the
.landing-studio-toolbar button rule so its min-height is 44px instead of 40px,
ensuring visible Share and Build controls meet the required interactive target
size.
Source: Coding guidelines
| return working( | ||
| "github-app-installation-live", | ||
| "The GitHub App exchanged an installation token and read its bounded repository scope.", | ||
| ["github-app-jwt-live", "github-installation-repositories-live"], | ||
| receipt.latencyMs, | ||
| ); |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Provider evidence tokens describe operations that this module never performed. Both receipts build their detail and evidence from configuration presence or from neighbouring check results, not from the operation named in the token. Apply one rule: emit an evidence token only for an operation that the current run executed.
lib/platform-provider-health.ts#L361-L366: compare the repositories returned at Line 348 againstGITHUB_APP_ALLOWED_REPOSITORIES, or drop the "bounded repository scope" wording and the implied allowlist evidence.lib/platform-provider-health.ts#L466-L479: run a real audit append and a checksummed restore probe, or replaceprivate-recovery-storage-liveand the "configured and live" detail with wording that states the result is derived from themanaged-backendandorganizationschecks.
As per coding guidelines: "Keep external actions explicit and consent-based, and never claim trades, alerts, channels, deployments, or connections succeeded without verified provider evidence."
📍 Affects 1 file
lib/platform-provider-health.ts#L361-L366(this comment)lib/platform-provider-health.ts#L466-L479
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@lib/platform-provider-health.ts` around lines 361 - 366, The provider health
receipts claim operations that were not executed. In
lib/platform-provider-health.ts lines 361-366, either compare the repositories
returned by the preceding check with GITHUB_APP_ALLOWED_REPOSITORIES before
emitting github-app-installation-live, or remove the bounded-scope wording and
implied allowlist evidence. In lines 466-479, either perform a real audit append
and checksummed restore probe before emitting private-recovery-storage-live, or
replace that token and “configured and live” detail with wording explicitly
derived from the managed-backend and organizations checks.
Source: Coding guidelines
| const POSTGRES_SCHEMA_MIGRATION = ` | ||
| DO $$ | ||
| BEGIN | ||
| IF EXISTS ( | ||
| SELECT 1 | ||
| FROM information_schema.columns | ||
| WHERE table_name = 'drops_project_data_snapshots' | ||
| AND column_name = 'snapshot_json' | ||
| AND data_type = 'jsonb' | ||
| ) THEN | ||
| ALTER TABLE drops_project_data_snapshots | ||
| ALTER COLUMN snapshot_json TYPE TEXT | ||
| USING snapshot_json::text; | ||
| END IF; | ||
| END | ||
| $$ | ||
| `; |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win
Restrict the migration probe to the current schema.
The information_schema.columns lookup matches drops_project_data_snapshots in any schema of the connected database. If another schema holds a same-named table, the probe can report jsonb and the ALTER TABLE then runs against the search-path table. Add a table_schema predicate.
🛡️ Proposed fix to scope the probe
IF EXISTS (
SELECT 1
FROM information_schema.columns
- WHERE table_name = 'drops_project_data_snapshots'
+ WHERE table_schema = current_schema()
+ AND table_name = 'drops_project_data_snapshots'
AND column_name = 'snapshot_json'
AND data_type = 'jsonb'
) THEN📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const POSTGRES_SCHEMA_MIGRATION = ` | |
| DO $$ | |
| BEGIN | |
| IF EXISTS ( | |
| SELECT 1 | |
| FROM information_schema.columns | |
| WHERE table_name = 'drops_project_data_snapshots' | |
| AND column_name = 'snapshot_json' | |
| AND data_type = 'jsonb' | |
| ) THEN | |
| ALTER TABLE drops_project_data_snapshots | |
| ALTER COLUMN snapshot_json TYPE TEXT | |
| USING snapshot_json::text; | |
| END IF; | |
| END | |
| $$ | |
| `; | |
| const POSTGRES_SCHEMA_MIGRATION = ` | |
| DO $$ | |
| BEGIN | |
| IF EXISTS ( | |
| SELECT 1 | |
| FROM information_schema.columns | |
| WHERE table_schema = current_schema() | |
| AND table_name = 'drops_project_data_snapshots' | |
| AND column_name = 'snapshot_json' | |
| AND data_type = 'jsonb' | |
| ) THEN | |
| ALTER TABLE drops_project_data_snapshots | |
| ALTER COLUMN snapshot_json TYPE TEXT | |
| USING snapshot_json::text; | |
| END IF; | |
| END | |
| $$ | |
| `; |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@lib/project-data/durable-backend.ts` around lines 23 - 39, Update the
information_schema.columns probe in POSTGRES_SCHEMA_MIGRATION to constrain
table_schema to the current schema, ensuring the conditional check and
subsequent ALTER TABLE target the same search-path table rather than a
same-named table elsewhere.
| async deleteProject( | ||
| projectIdInput: string, | ||
| expectedStoreRevision: number, | ||
| ): Promise<void> { | ||
| const projectId = validateProjectDataProjectId(projectIdInput); | ||
| const current = await this.#readEnvelope(projectId); | ||
| if (currentRevision(current.envelope) !== expectedStoreRevision) { | ||
| throw conflict(currentRevision(current.envelope)); | ||
| } | ||
| if (!current.envelope || current.envelope.deleted) return; | ||
| try { | ||
| await (await this.#client()).put( | ||
| this.#path(projectId), | ||
| JSON.stringify(nextEnvelope(projectId, null, expectedStoreRevision + 1)), | ||
| { | ||
| access: "private", | ||
| addRandomSuffix: false, | ||
| allowOverwrite: true, | ||
| cacheControlMaxAge: 60, | ||
| contentType: "application/json; charset=utf-8", | ||
| ...(current.etag ? { ifMatch: current.etag } : {}), | ||
| }, | ||
| ); | ||
| } catch (error) { | ||
| if (error instanceof BlobPreconditionFailedError) { | ||
| const latest = await this.#readEnvelope(projectId).catch(() => ({ envelope: null })); | ||
| throw conflict(currentRevision(latest.envelope)); | ||
| } | ||
| throw storageFailure(); | ||
| } | ||
| } |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🔴 Critical | 🏗️ Heavy lift
Tombstones make a deleted project permanently unwritable in both durable backends. read() returns null for a tombstone, so ProjectDataStore treats the project as empty and calls compareAndSwap(projectId, 0, next), while the backend still reports the tombstone revision. Every later write then throws conflict. Either remove the record on delete, or accept a tombstone revision during recreation.
lib/project-data/durable-backend.ts#L281-L311: delete the blob with the already-typeddelmember ofBlobStorageinstead of writing a tombstone envelope, or acceptexpectedStoreRevision = 0when the current envelope is a tombstone.lib/project-data/durable-backend.ts#L411-L440: replace the tombstoneUPDATEwith aDELETE FROM drops_project_data_snapshots WHERE project_key = $1 AND project_id = $2 AND store_revision = $3, or extend the create branch toON CONFLICT (project_key) DO UPDATE ... WHERE drops_project_data_snapshots.deleted = TRUE.
📍 Affects 1 file
lib/project-data/durable-backend.ts#L281-L311(this comment)lib/project-data/durable-backend.ts#L411-L440
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@lib/project-data/durable-backend.ts` around lines 281 - 311, Make deleted
projects recreatable in both durable backends by removing records on deletion
rather than persisting tombstones. In lib/project-data/durable-backend.ts lines
281-311, update deleteProject to call the typed BlobStorage.del member with the
existing revision/precondition handling. In lib/project-data/durable-backend.ts
lines 411-440, replace the tombstone UPDATE with a conditional DELETE from
drops_project_data_snapshots using project_key, project_id, and store_revision,
preserving conflict behavior for revision mismatches.
| @@ -0,0 +1,19 @@ | |||
| import { runPlatformProviderHealthChecks } from "../lib/platform-provider-health.ts"; | |||
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Description: Check the declared Node version, the script wiring, and whether other .mjs files already import .ts modules.
set -uo pipefail
fd -H -t f 'package.json' -d 2 --exec sh -c 'echo "== $1"; jq "{name, engines, scripts}" "$1"' _ {}
fd -H -t f '.node-version|.nvmrc|.tool-versions' -d 2 --exec sh -c 'echo "== $1"; cat "$1"' _ {}
echo "== workflows node-version"
fd -t f -e yml -e yaml . .github 2>/dev/null | xargs -r rg -n 'node-version' || true
echo "== other .mjs files importing .ts modules"
rg -n --glob '*.mjs' 'from "\.\.?/[^"]+\.ts"' | head -40
echo "== references to verify-platform-providers"
rg -n 'verify-platform-providers' || trueRepository: svg8bit/drops-studio
Length of output: 2042
🏁 Script executed:
#!/bin/bash
set -u
echo "== script and package locations"
fd -H -t f 'verify-platform-providers.mjs|package.json' -d 3 -x sh -c 'echo "== $1"; if [[ "$1" == *verify-platform-providers.mjs ]]; then cat -n "$1"; else jq "{engines, scripts, dependencies, devDependencies}" "$1"; fi' _ {}
echo "== exact script references"
rg -n -F 'scripts/verify-platform-providers.mjs' . || true
rg -n -F 'verify-platform-providers' . || true
echo "== Node runtime capabilities"
node --version 2>&1 || true
node --help 2>&1 | rg -n 'strip-types|experimental-strip-types' || trueRepository: svg8bit/drops-studio
Length of output: 4850
Add an npm entry point and support the declared Node range.
package.json declares Node >=22.13.0, but this .ts import requires native type stripping enabled by default in newer Node versions or an explicit runtime flag. Add the required flag or raise the minimum Node version. Add an npm script for scripts/verify-platform-providers.mjs; no current npm script references it.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@scripts/verify-platform-providers.mjs` at line 1, Update the execution
configuration for scripts/verify-platform-providers.mjs so its TypeScript import
works across the declared Node >=22.13.0 range, using the required runtime flag
or an appropriately raised minimum version. Add a package.json npm script that
invokes verify-platform-providers.mjs.
Source: Coding guidelines
| const receipt = await runPlatformProviderHealthChecks({ | ||
| includeSandbox: process.env.DROPS_SKIP_SANDBOX_HEALTH !== "1", | ||
| persist: true, | ||
| }); |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
Require an explicit opt-in before live provider checks and receipt persistence.
The script runs live provider checks by default and persists the result with persist: true. runPlatformProviderHealthChecks creates a real Vercel Sandbox, writes and deletes real project-data rows, calls the GitHub and Vercel APIs, and then overwrites the shared health receipt through writePlatformHealthReceipt. Any shell that has production credentials in its environment will therefore mutate production state and replace the receipt that platformCapabilitySnapshotWithHealth serves to /platform.
Sandbox execution is currently opt-out through DROPS_SKIP_SANDBOX_HEALTH, and persistence has no gate at all. Invert both: require an explicit flag to run the live checks and a separate explicit flag to persist.
🔒️ Proposed gating
+if (process.env.DROPS_RUN_PROVIDER_HEALTH !== "1") {
+ console.error(
+ "Refusing to run live provider health checks. Set DROPS_RUN_PROVIDER_HEALTH=1 to opt in.",
+ );
+ process.exitCode = 1;
+} else {
const receipt = await runPlatformProviderHealthChecks({
- includeSandbox: process.env.DROPS_SKIP_SANDBOX_HEALTH !== "1",
- persist: true,
+ includeSandbox: process.env.DROPS_INCLUDE_SANDBOX_HEALTH === "1",
+ persist: process.env.DROPS_PERSIST_HEALTH_RECEIPT === "1",
});As per coding guidelines: "Every external or destructive tool must have explicit approval, timeout, quota, audit record, bounded output, and idempotency behavior," and "Keep external actions explicit and consent-based."
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@scripts/verify-platform-providers.mjs` around lines 3 - 6, Update the
invocation of runPlatformProviderHealthChecks to require separate explicit
environment-flag opt-ins for live provider checks and receipt persistence.
Replace the current opt-out includeSandbox logic with an opt-in condition, and
gate persist: true behind its own explicit persistence flag so both values are
false by default; preserve the existing health-check call and receipt behavior
when each corresponding flag is enabled.
Source: Coding guidelines
Outcome
Releases the complete v0/Replit-like Drops Studio surface and production capability control plane while preserving legacy projects, all 12 crypto recipes,
/p/{slug}, ZIP export, BYOK providers, DropsTab and Drops Bot/Telegram flows.Production platform
Verification
npm run lint— passnpm run typecheck— passnpm run guardrails:ui— passnpm run test:unit— 709 total, 707 pass, 2 opt-in skips, 0 failnpm run build:vercel— pass, 22/22 pagesnpm run build— pass (Cloudflare-compatible Vinext)npm run test:e2e:prepared— 201 total, 161 pass, 40 intentional skips, 0 failRelease notes
Production aliases are updated only after merge and a fresh deployment from the verified
origin/mainHEAD. External credentials remain disabled and honestly labeled until their owners configure them.Summary by CodeRabbit
New Features
Improvements
Bug Fixes