Commit Graph

11 Commits

Author SHA1 Message Date
Amy 3162da5f23 test: cover runtime automation gaps (#5976)
* test: cover runtime automation gaps

* fix: remove duplicate runtime startup declarations

Generated-By: looper 0.11.0 (runner=fixer, agent=codex)
2026-07-22 14:29:34 +00:00
Christopher Beaulieu 6bee94be03 fix(deploy): honor target preview/production for Cloudflare Pages (#4483) (#4576)
* fix(deploy): honour target for Cloudflare Pages branch and URL selection (#4483)

- deployToCloudflarePages: add optional target ('preview'|'production', default 'production')
- Branch derivation: preview→'preview', production/omitted→'main'
- URL selection: preview uses per-deploy alias; production uses production root
- routes/deploy.ts: read target from request body (default 'production'), pass to deployToCloudflarePages, persist actual target (not hardcoded 'preview')
- db.ts normalizeDeployment: read target from DB row instead of hardcoding 'preview'
- contracts: widen target fields to 'preview'|'production'; add target to DeployProjectFileRequest

* test(deploy): type FormData branch capture as string | undefined (#4483)

The Phase-1 branch-capture objects were typed `{ branch?: string }` but
assigned `string | undefined`, which fails `tsc` under
`exactOptionalPropertyTypes`. Type the field as `branch: string | undefined`
so the daemon typecheck gate passes; assertions are unchanged.

* fix(deploy): reject invalid deploy target with HTTP 400 (#4576)

The deploy route coerced any non-'preview' target to 'production', so a
malformed value (typo, empty string, wrong case) silently published to the
production Cloudflare branch instead of a gated preview. Validate the supplied
target up front and return 400 for anything outside preview|production; an
omitted target still defaults to production (non-breaking).

Addresses review feedback on #4576 / #4483.

* feat(deploy): add `od deploy` CLI subcommand with --target (#4483)

Land the CLI surface for the preview/production deploy capability so it is
not API-only. `od deploy <projectId> --file <name> [--target preview|
production] [--provider vercel-self|cloudflare-pages] [--cf-zone-id]
[--cf-zone-name] [--cf-domain-prefix] [--json] [--daemon-url]` POSTs to the
same `/api/projects/:id/deploy` endpoint the web UI uses, with local
validation of `--target` before any request (omitted target = no key, server
defaults to production).

Addresses the surface-closure review feedback on #4576 by shipping the
embeddability (CLI) surface alongside the API contract. The human web-UI
preview/production selector remains tracked as a follow-up in #4577.

Tests: apps/daemon/tests/cli-deploy.test.ts (11 cases, written test-first).

* feat(deploy): add web UI target selector for Cloudflare Pages deploys

Closes the web-UI surface gap PerishCode flagged on #4576: deploy target
(preview/production) had HTTP + od CLI support but no way to choose it
from the deploy modal. Adds a target <select> to the Cloudflare Pages
deploy panel, defaulted to Preview, threaded through deployProjectFile's
new optional target argument and forwarded in the deploy POST body.

References #4576.

* fix(deploy): reject vercel-self target=production instead of silently deploying preview

POST /api/projects/:id/deploy resolved the requested target but only
threaded it into deployToCloudflarePages(); deployToVercel() never
received it and always behaved as preview. A caller explicitly
requesting target=production against the Vercel provider was silently
deployed as preview with no error (Codex P2 finding on PR #4576).

Real Vercel production-target support is out of scope for this PR, so
reject the explicit request with 400 BAD_REQUEST instead. The guard
gates on the raw requested value, not the resolved target, so an
omitted target (which defaults to 'production' for Cloudflare Pages
parity) keeps deploying Vercel as preview exactly as before.

* fix(web): default Cloudflare Pages deploy target to production

The deploy target selector defaulted to 'preview', both in its initial
useState and when syncing from the (currently placeholder,
always-'preview') GET /api/deploy/config response. A user who opened
the Cloudflare Pages deploy modal and clicked Deploy without touching
the selector silently deployed to preview instead of production,
contradicting the daemon's documented default for an omitted target
and pre-PR behavior.

Default both sites to 'production'; explicit user selection of either
target is untouched. FileViewer.deploy-target.test.tsx covers the
default and both explicit selections.

Fixes #4483
2026-07-17 08:43:50 +00:00
Ethan Guo 60653d5805 fix(daemon): prevent ReDoS in deploy-preflight doctype check (#5067) 2026-07-07 09:10:49 +00:00
leessju 381e9a96e2 Make share deploys visibly complete (#2843)
* Make share deploys visibly complete

Share deploys were uploading only the referenced entry graph, so sibling screens could fall through to provider fallback pages after deployment. They also completed silently except for the result link block inside the deploy dialog, leaving users unsure whether a redeploy finished.

This includes visible files for Open Design-managed projects in real deploy/preflight payloads while preserving the selected entry as provider-root index.html. Linked-folder projects stay on the referenced-file graph so repo files that are visible in the file panel, like README.md or src/**, do not become public by accident. The web UI also shows a localized success toast at the top of the app after a successful Vercel or Cloudflare Pages upload.

Constraint: Cloudflare Pages Direct Upload serves missing files through its fallback behavior, so deployment payload completeness must be handled before upload.

Constraint: Linked-folder projects can expose arbitrary repository content through the file panel, so whole-project deploy expansion is limited to Open Design-managed project directories.

Rejected: Reintroduce an entry-file dropdown | users wanted full project deployment semantics rather than selecting a root-only artifact.

Rejected: Upload every visible linked-folder file | would make non-runtime repo content publicly reachable after Share deploy.

Confidence: high

Scope-risk: moderate

Directive: Do not remove the selected-entry-to-index.html mapping; it keeps alternate entries like index-v1.html deployable as the root without overwriting them with the launcher.

Directive: Do not expand linked-folder deploys beyond referenced web assets without an explicit user opt-in and review of the privacy model.

Tested: pnpm --filter @open-design/daemon test tests/deploy.test.ts tests/deploy-routes.test.ts

Tested: pnpm --filter @open-design/web test tests/components/FileViewer.test.tsx

Tested: pnpm --filter @open-design/web typecheck

Tested: pnpm guard

* fix(web): gate share-deploy ready hint on actual ready state

The 'Ready · Deployed URL' hint was unconditionally rendered whenever
deployResultCards was non-empty, so a successful deploy that came back
as link-delayed or protected showed contradictory copy next to the
'Public link pending' / 'Deployment protection enabled' badge.

Render the hint only when deployResultState(activeDeployment?.status)
is 'ready' so the success line stays consistent with the badge below.

---------

Co-authored-by: nicejames <nicejames@gmail.com>
2026-05-28 08:56:11 +00:00
kami 0f586c410d Fix Cloudflare Pages custom domain lookup (#958)
* Support Cloudflare Pages custom domains without hiding pages.dev fallback

Keep the default Pages preview as the first public link while optional owned-zone binding provisions DNS and Pages custom-domain state in parallel.

Constraint: Cloudflare deploys must use the existing direct-upload API path with no Wrangler dependency.

Constraint: pages.dev must stay visible even while custom-domain verification is pending.

Rejected: Vercel custom-domain support | outside requested Cloudflare-only scope.

Rejected: overwriting arbitrary CNAME records | risks taking over user-managed DNS.

Confidence: high

Scope-risk: moderate

Directive: Do not expose providerMetadata through public deploy contracts; keep custom-domain DNS ownership checks conservative.

Tested: pnpm --dir apps/daemon exec vitest run -c vitest.config.ts tests/deploy.test.ts tests/deploy-routes.test.ts

Tested: pnpm --filter @open-design/contracts build && pnpm --filter @open-design/contracts typecheck && pnpm --filter @open-design/contracts test

Tested: pnpm --filter @open-design/web typecheck && pnpm --filter @open-design/web test -- providers/registry.test.ts components/FileViewer.test.tsx i18n/locales.test.ts

Tested: pnpm i18n:check && pnpm guard && pnpm typecheck

Tested: pnpm --filter @open-design/daemon build && pnpm --filter @open-design/web build && git diff --check

Not-tested: real Cloudflare account/token/domain smoke test

* Preserve Cloudflare fallback correctness under large accounts and races

Constraint: Cloudflare Pages keeps pages.dev as the primary usable fallback while custom domains remain optional typed metadata.
Rejected: Treating custom-domain DNS or binding failure as a top-level deployment failure | pages.dev can still be ready and usable.
Confidence: high
Scope-risk: moderate
Directive: Keep custom-domain finality tied to Cloudflare Pages API active status plus URL reachability; do not expose providerMetadata.
Tested: pnpm --dir apps/daemon exec vitest run -c vitest.config.ts tests/deploy.test.ts tests/deploy-routes.test.ts; pnpm --filter @open-design/web test -- components/FileViewer.test.tsx i18n/locales.test.ts providers/registry.test.ts; pnpm --filter @open-design/daemon typecheck; pnpm --filter @open-design/web typecheck; pnpm i18n:check; git diff --check; pnpm guard; pnpm typecheck; pnpm --filter @open-design/daemon build; pnpm --filter @open-design/web build
Not-tested: Real Cloudflare token/account/zone smoke test.

* Keep impeccable design notes local

Constraint: .impeccable.md is local assistant/design context and should not be part of the PR diff.
Rejected: Keeping the file tracked while adding it to .gitignore | tracked files are not ignored by Git.
Confidence: high
Scope-risk: narrow
Directive: Keep .impeccable.md untracked and ignored; do not rely on it for required project documentation.
Tested: git check-ignore -v .impeccable.md; git diff --check
Not-tested: Full workspace tests not rerun for ignore-only metadata change.

* Use direct Pages domain lookup for custom bindings

Cloudflare rejects list-style options on Pages custom-domain lookup in some accounts, so the deploy path now reads the selected hostname directly before creating a binding. This keeps pages.dev deployment success intact while avoiding a failed custom-domain branch caused by page/per_page query parameters.

Constraint: Cloudflare Pages custom-domain lookup must not send unsupported page/per_page list options

Rejected: Continue paginating /domains | Cloudflare returns invalid list options before the binding can be created

Confidence: high

Scope-risk: narrow

Directive: Keep pages.dev as the primary deployment URL and treat custom-domain setup as a recoverable secondary branch

Tested: pnpm --dir apps/daemon exec vitest run -c vitest.config.ts tests/deploy.test.ts tests/deploy-routes.test.ts; pnpm --filter @open-design/daemon typecheck; pnpm guard; pnpm typecheck; git diff --check; pnpm --filter @open-design/daemon build

Not-tested: Live Cloudflare deployment was not retriggered from the browser
2026-05-08 21:20:48 +08:00
nettee 32d820e4ee fix(daemon): typecheck leaf modules (#943)
* update drift

* fix(daemon): typecheck leaf modules

* fix(daemon): decode Qoder stdout buffers

Generated-By: looper 0.5.6 (runner=fixer, agent=opencode)
2026-05-08 20:01:25 +08:00
kami 2eae7da24b feat: support Cloudflare Pages custom domains (#851)
* Support Cloudflare Pages custom domains without hiding pages.dev fallback

Keep the default Pages preview as the first public link while optional owned-zone binding provisions DNS and Pages custom-domain state in parallel.

Constraint: Cloudflare deploys must use the existing direct-upload API path with no Wrangler dependency.

Constraint: pages.dev must stay visible even while custom-domain verification is pending.

Rejected: Vercel custom-domain support | outside requested Cloudflare-only scope.

Rejected: overwriting arbitrary CNAME records | risks taking over user-managed DNS.

Confidence: high

Scope-risk: moderate

Directive: Do not expose providerMetadata through public deploy contracts; keep custom-domain DNS ownership checks conservative.

Tested: pnpm --dir apps/daemon exec vitest run -c vitest.config.ts tests/deploy.test.ts tests/deploy-routes.test.ts

Tested: pnpm --filter @open-design/contracts build && pnpm --filter @open-design/contracts typecheck && pnpm --filter @open-design/contracts test

Tested: pnpm --filter @open-design/web typecheck && pnpm --filter @open-design/web test -- providers/registry.test.ts components/FileViewer.test.tsx i18n/locales.test.ts

Tested: pnpm i18n:check && pnpm guard && pnpm typecheck

Tested: pnpm --filter @open-design/daemon build && pnpm --filter @open-design/web build && git diff --check

Not-tested: real Cloudflare account/token/domain smoke test

* Preserve Cloudflare fallback correctness under large accounts and races

Constraint: Cloudflare Pages keeps pages.dev as the primary usable fallback while custom domains remain optional typed metadata.
Rejected: Treating custom-domain DNS or binding failure as a top-level deployment failure | pages.dev can still be ready and usable.
Confidence: high
Scope-risk: moderate
Directive: Keep custom-domain finality tied to Cloudflare Pages API active status plus URL reachability; do not expose providerMetadata.
Tested: pnpm --dir apps/daemon exec vitest run -c vitest.config.ts tests/deploy.test.ts tests/deploy-routes.test.ts; pnpm --filter @open-design/web test -- components/FileViewer.test.tsx i18n/locales.test.ts providers/registry.test.ts; pnpm --filter @open-design/daemon typecheck; pnpm --filter @open-design/web typecheck; pnpm i18n:check; git diff --check; pnpm guard; pnpm typecheck; pnpm --filter @open-design/daemon build; pnpm --filter @open-design/web build
Not-tested: Real Cloudflare token/account/zone smoke test.

* Keep impeccable design notes local

Constraint: .impeccable.md is local assistant/design context and should not be part of the PR diff.
Rejected: Keeping the file tracked while adding it to .gitignore | tracked files are not ignored by Git.
Confidence: high
Scope-risk: narrow
Directive: Keep .impeccable.md untracked and ignored; do not rely on it for required project documentation.
Tested: git check-ignore -v .impeccable.md; git diff --check
Not-tested: Full workspace tests not rerun for ignore-only metadata change.
2026-05-08 11:11:22 +08:00
kami 09eb88f683 Add Cloudflare Pages artifact deployment
Adds Cloudflare Pages artifact deployment support.
2026-05-07 20:04:22 +08:00
Nagendhra Madishetti 8897cb85be feat(deploy): add /api/projects/:id/deploy/preflight for pre-upload inspection (#320)
* feat(deploy): add /api/projects/:id/deploy/preflight for pre-upload inspection

Today the deploy flow is a single POST that builds the file set, ships
it to Vercel, and waits for the public URL to come up. The user has no
visibility into what is actually being uploaded until the operation
either succeeds or fails with a generic 400. There is no way to tell
"my background image is missing" from "my deploy will exceed Vercel
quotas" without rolling the dice and waiting up to ~110 seconds.

Add a preflight endpoint that builds the plan, runs an analyzer, and
returns a typed report without touching Vercel. The endpoint is purely
additive, costs no network round-trips, and gives the UI everything
needed to render a confirm-before-deploy summary.

Implementation:

- packages/contracts/src/api/projects.ts: new public types
  DeployPreflightRequest, DeployPreflightResponse, DeployPreflightFile,
  DeployPreflightWarning, and the closed enum DeployPreflightWarningCode
  with values broken-reference, invalid-reference, large-asset,
  large-bundle, large-html, external-script, external-stylesheet,
  no-doctype, no-viewport.

- apps/daemon/src/deploy.ts: refactor buildDeployFileSet so the
  walk-and-collect logic lives in a non-throwing buildDeployFilePlan
  that returns { entryPath, html, files, missing, invalid }.
  buildDeployFileSet keeps its existing semantics by delegating to the
  plan and throwing when missing or invalid is non-empty, so every
  current caller and test is unchanged. Add analyzeDeployPlan that
  walks the entry HTML once via parseHtmlTags + parseHtmlAttributes
  (no new parser) and emits the warning vocabulary above. Add
  prepareDeployPreflight that combines the plan and the analyzer and
  returns the public DeployPreflightResponse shape. Soft thresholds
  (4 MiB per asset, 75 MiB bundle, 1 MiB entry HTML) are exported as
  DEPLOY_PREFLIGHT_LARGE_*_BYTES constants so tests and future tuning
  use the same numbers.

- apps/daemon/src/server.ts: new route POST /api/projects/:id/deploy/
  preflight. Same body shape as POST /deploy ({ fileName, providerId? }).
  Returns DeployPreflightResponse on success. Validation errors map to
  BAD_REQUEST, missing entry file maps to FILE_NOT_FOUND, exactly like
  the existing deploy POST so the client error-handling stays uniform.

Tests (apps/daemon/tests/deploy.test.ts, 14 new cases under a new
"deploy plan and analyzer" describe block):

- buildDeployFilePlan returns files plus missing and invalid lists
  without throwing.
- buildDeployFileSet still throws on missing/invalid, preserving the
  pre-refactor contract.
- Analyzer unit tests for broken-reference, invalid-reference,
  no-doctype, no-viewport, external-script, external-stylesheet,
  protocol-relative external script, large-asset (per-file but not
  entry HTML), large-html (entry HTML threshold), and a healthy-input
  case that produces zero warnings.
- Preflight integration tests: full payload shape (provider, entry,
  files, totals, warnings) on a healthy project, broken-reference path
  on a missing asset, and a non-HTML entry rejection.

Net diff: +439 / -8 across 4 files. No new dependencies, no public API
removals. The existing POST /deploy flow is byte-for-byte unchanged
because buildDeployFileSet is a thin wrapper around the new plan.

* fix(deploy): preflight review fixes (P2 doctype anchoring, P2 large-html source path, P3 logging, P3 jsdoc)

Address review feedback on PR #320:

P2 (Codex bot): the no-doctype check used /<!doctype\s+html/i with no
anchor, so a `<!doctype html>` substring inside a <script> template
literal or HTML comment would mask a missing real declaration. Switch
to `new RegExp('^\uFEFF?\s*(?:<!--[\s\S]*?-->\s*)*<!doctype\s+html', 'i')`
which anchors to the document prolog and accepts the HTML5-permitted
optional BOM, comments, and whitespace before the doctype.

P2 (Codex bot): the large-html warning hardcoded `path: 'index.html'`,
but `entry` in the preflight payload is the source project file (e.g.
`pages/landing.html`) and other warnings already use source paths.
Switch the warning path to `entryPath` so file-targeted UI actions
deep-link back to a real project file.

P3 (lefarcen): add a JSDoc block alongside the inline TS annotation
on analyzeDeployPlan so the function follows the JSDoc-first style
of the rest of the file. The TS inline annotation stays because
deploy.ts uses `// @ts-nocheck`, which makes JSDoc `@param`/`@returns`
non-authoritative for callers; both forms together give docs and
correct caller-side types.

P3 (lefarcen): the preflight route catch block now logs non-DeployError
exceptions via console.error before sending the generic 400, so
unexpected failures show up in the daemon log without leaking
internals to the client.

Tests:
- analyzeDeployPlan flags missing doctype even when a fake `<!doctype>`
  lives inside a <script> string literal.
- analyzeDeployPlan accepts a doctype that follows a leading HTML
  comment and BOM (HTML5 prolog forms).
- analyzeDeployPlan reports large-html against the source entry path
  (e.g. `pages/landing.html`), not the deploy-renamed `index.html`.

38 deploy tests pass, 264 across the daemon package, typecheck clean.

---------

Co-authored-by: Nagendhra <nagendhra405@gmail.com>
2026-05-03 09:00:46 +08:00
Nagendhra Madishetti 0943cb1452 fix(deploy): bundle and rewrite assets referenced from inline <style> blocks and style="" attributes (#314)
* fix(deploy): bundle and rewrite assets referenced from inline <style> blocks and style="" attributes

buildDeployFileSet only scanned HTML attributes (src/poster/href/srcset)
and external .css files for asset references. url() and @import refs
inside inline <style> blocks and style="" attributes were silently
skipped, so deployed pages were missing their @font-face sources, hero
backgrounds, and any image referenced from inline CSS, even though the
files existed in the project.

Add two helpers in deploy.ts:
- extractInlineCssReferences(html) collects url()/@import refs from
  <style> blocks and style attributes, reusing extractCssReferences and
  honoring htmlRawTextRanges so style-like text inside scripts and HTML
  comments is ignored.
- rewriteCssReferences(css, baseDir) rewrites url() and bare @import
  paths the same way rewriteHtmlReference rewrites src/href, so inline
  CSS keeps working when the entry HTML moves up to the deploy root.

Wire both into the existing pipeline:
- buildDeployFileSet pushes inline-style refs onto the pending queue
  alongside HTML and supportingFiles refs.
- rewriteEntryHtmlReferences first rewrites <style> block contents,
  then re-derives htmlRawTextRanges so the tag-attribute pass still
  skips raw-text regions whose offsets shifted.
- rewriteHtmlAttributes treats style="" the same way as srcset, routing
  through rewriteCssReferences while preserving the original quoting.

Tests cover extraction, bundling end-to-end (including @font-face,
@import, and subdirectory entries), rewriting of CSS content with
remote/data/absolute refs left intact, and the missing-file error path.

* fix(deploy): harden inline-CSS pipeline per review (P1 script-literal masking, P2 ReDoS, SVG coverage)

Address review feedback on the inline-CSS deploy pipeline:

P1 (Codex bot): rewriteEntryHtmlReferences now computes htmlRawTextRanges
on the input HTML before the <style> block pre-pass, and skips any
<style>...</style> match whose offset falls inside a script string
literal or HTML comment. Previously the rewriter mutated those textual
fragments, which could change runtime JS behavior on subdirectory
deploys. Mirrors the gate already used by extractInlineCssReferences.

P2 (lefarcen): tighten the CSS url() and @import regex character
classes from .*? to [^)]*? and [^'"]*? respectively. Quoted urls
containing literal `)` are now expected to be percent-encoded, which
matches CSS authoring conventions and removes the O(n^2) backtracking
risk on attacker-controlled CSS. The hardened patterns are shared
between extractCssReferences and rewriteCssReferences via
CSS_URL_REGEX / CSS_IMPORT_REGEX so the two paths cannot diverge.

P3 (lefarcen): add an explicit comment on the rewriteEntryHtmlReferences
double-pass so the perf tradeoff is visible inline.

New tests:
- <style> inside <svg>: extraction and subdir rewriting both work.
- <style>-like text inside <script> literals: round-trips byte-for-byte.
- <style>-like text inside HTML comments: untouched.
- Pathological unclosed url(...... runs in <500ms for 100k input.

All 36 deploy tests pass (262 across the daemon package).

---------

Co-authored-by: Nagendhra <nagendhra405@gmail.com>
2026-05-03 08:50:21 +08:00
Caprika 454e8373fb [feat] Add Vercel self deploy flow (#167)
* Add Vercel self deploy flow

* Fix Vercel deploy file state and nested assets

* Add deploy hook script injection
2026-04-30 20:39:06 +08:00