Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
name: CI

on:
push:
branches: [main]
pull_request:

# The workflow only checks out code and runs tests; it never needs write access.
permissions:
contents: read

jobs:
ci:
runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4

- name: Set up Node.js
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version-file: '.nvmrc'
cache: 'npm'

- name: Set up PHP
uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # v2
with:
php-version: '8.3'

- name: Install dependencies
run: npm ci

- name: Run tests
run: npm test

- name: Build production bundle
run: npm run build

- name: Run deploy guard
run: npm run test:deploy-guard

- name: Run proxy allowlist tests
run: npm run test:proxies

- name: Scan production bundle for API key/endpoint leaks
run: python3 scripts/scan_bundle_for_leaks.py
46 changes: 33 additions & 13 deletions DEPLOYMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,34 +79,54 @@ in `.dart` untouched so a re-deploy is a no-op and an editor rename survives.

Before writing anything to the project the runner compiles the generated
classes in a throwaway Flutter package built from the project's own dependency
versions and refuses the deploy (HTTP 422) if the analyzer reports an error.
versions, and refuses the deploy (HTTP 422) if the analyzer reports an error.
The FlutterFlow DSL only checks that code is formattable, which accepts a call
to a named argument the package never declared. This needs the Flutter SDK in
the image, so the runner takes longer to build and to cold-start, and the
deploy script raises Cloud Run's request timeout to 900s.

A class that imports FlutterFlow scaffolding (`/backend/schema/structs/index.dart`)
is compiled too: the deploy gathers the transitive closure of each class's
project-relative imports from the export and ships it alongside, bounded by
file and byte caps. The class is written to `lib/custom_code/<stem>.dart` -
where FlutterFlow files it, so a relative import resolves from the right
directory - and the project's generated Dart under `lib/`, which is what the
leading slash resolves against. Analysis is scoped to the deployed class files
with `--no-fatal-warnings`, so an error inside the project's own scaffolding,
or a warning-only class, cannot refuse code that compiles.
A class is compiled when every package it imports resolves the same way the
project resolves it. Two cases are left out of the compile set, and each is
reported rather than silently dropped:

* It imports something FlutterFlow only generates once the app is built -
`/backend/schema/structs/index.dart`, `../flutter_flow/lat_lng.dart`. Those
files do not exist until after the deploy.
* It imports a package the project declares from a source this manifest cannot
express: a `git:` or `path:` source, or a nested `hosted:` block. The scratch
package would resolve different code, and a check against the wrong version
reports a result that does not describe what ships.

The second case is decided **per class**, from that class's own imports. A
`git:` dependency stops only the classes that import it; classes that do not are
still compiled. Deciding it once for the whole project meant one such entry left
an entire deploy uncompiled.

**Which packages the Flutter SDK supplies is read from the project's own
pubspec** - a dependency written in block form under `sdk:` is an SDK package by
definition. Nothing keeps a list of SDK package names, because a list goes stale
the moment the SDK ships one it has not heard of, and a stale list reads a
perfectly valid package as unreproducible. That is not hypothetical: a list
missing `flutter_web_plugins` refused whole deploys for projects that declared
it.

The manifest is sent as package names and version constraints, never as
pubspec.yaml text, and the runner builds the document itself - it runs
`pub get` against it on a publicly reachable route, so a caller must not be
able to name a git or path source. `dependency_overrides` are carried over,
because a scratch package resolving different versions than the project would
approve code the project rejects.
approve code the project rejects. The runner validates each name against a
package-name pattern rather than against a list of SDK packages, and writes
`sdk: flutter` itself, so that value never comes from a caller.

A client still sending the older `pubspec` format is recognized, never
executed, and its deploy proceeds with the check reported as unavailable.

**Nothing unverified is deployed.** The web app refuses the deploy on any class
it cannot compile, naming each one and why, before the runner is called.
**What could not be verified still deploys, and is reported.** A class with a
real analyzer error refuses the deploy - HTTP 422, naming the error. A class the
deploy could not compile, for either reason above, does not: it is pushed, and
the warning naming it is logged *before* the provisioning request, so it
describes the mutation before it happens rather than after.

The runner streams its progress. When the request body sets `"stream": true`
the response is NDJSON — one `{"event":"phase"|"log"|"result"}` object per
Expand Down
28 changes: 25 additions & 3 deletions app.js
Original file line number Diff line number Diff line change
Expand Up @@ -2113,7 +2113,7 @@ async function provisionMissingCodeFiles(
) {
const missingCodeFiles = findMissingCodeFiles(fileMap, remoteFiles);
if (missingCodeFiles.length === 0) {
return { remoteFiles, syncFileMap: fileMap, unverified: [] };
return { remoteFiles, syncFileMap: fileMap, unverified: [], approximate: [] };
}

// The runner compiles these against the project's own package versions
Expand All @@ -2137,6 +2137,14 @@ async function provisionMissingCodeFiles(
const unverified = verificationPlan.skipped.map((entry) => entry.reason);
unverified.forEach((reason) => console.warn(`[custom class deploy] ${reason}`));

// Classes that do not import an unrepresentable package directly are still
// checked, but against a scratch graph that omits it. That approximation is
// disclosed here - before the push - and returned alongside `unverified`,
// so it reaches the deploy result and is never mistaken for a full-graph
// check.
const approximate = verificationPlan.approximate;
approximate.forEach((notice) => console.warn(`[custom class deploy] ${notice}`));

console.log(
`Provisioning ${missingCodeFiles.length} new FlutterFlow custom code file(s) before sync.`,
);
Expand Down Expand Up @@ -2178,6 +2186,7 @@ async function provisionMissingCodeFiles(
remoteFiles,
syncFileMap: excludeProvisionedCodeFiles(fileMap, missingCodeFiles),
unverified,
approximate,
};
}

Expand Down Expand Up @@ -2870,6 +2879,7 @@ async function commitToFlutterFlow(dartCode, fileName, options = {}) {
message: `Successfully committed ${fileName} to FlutterFlow project ${projectId}`,
addedDependencies: pubspecMerge.added,
unverified: provisioning.unverified,
approximate: provisioning.approximate,
warnings: result.errorMap ? Array.from(result.errorMap.entries()) : [],
};
} catch (error) {
Expand Down Expand Up @@ -3043,6 +3053,7 @@ async function executeCommit(code, options = {}) {
metadata,
addedDependencies: pubspecMerge.added,
unverified: provisioning.unverified,
approximate: provisioning.approximate,
warnings: result.errorMap ? Array.from(result.errorMap.entries()) : [],
elapsedTime: commitState.getElapsedTime(),
};
Expand Down Expand Up @@ -3207,6 +3218,7 @@ async function executeBundleCommit(bundlePlan, options = {}) {
metadata,
addedDependencies: pubspecMerge.added,
unverified: provisioning.unverified,
approximate: provisioning.approximate,
warnings: result.errorMap ? Array.from(result.errorMap.entries()) : [],
elapsedTime: commitState.getElapsedTime(),
};
Expand Down Expand Up @@ -5347,14 +5359,24 @@ function showCommitSuccessModal(result) {
const warningsSection = document.getElementById("success-warnings-section");
const warningsList = document.getElementById("success-warnings-list");
// A class that could not be compiled before the push is reported here rather
// than left implicit, so "deployed" never reads as "checked".
// than left implicit, so "deployed" never reads as "checked". A class that
// was compiled against a reduced package graph is reported beside it: the
// check ran, but approximately, and that has to stay visible too.
const unverified = result.unverified || [];
const approximate = result.approximate || [];
const fileWarnings = result.warnings || [];
if ((fileWarnings.length > 0 || unverified.length > 0) && warningsSection && warningsList) {
if (
(fileWarnings.length > 0 || unverified.length > 0 || approximate.length > 0) &&
warningsSection &&
warningsList
) {
warningsList.innerHTML = [
...unverified.map((reason) =>
`<li><span class="font-medium">Not verified before deploying:</span> ${escapeHtml(String(reason))}</li>`
),
...approximate.map((notice) =>
`<li><span class="font-medium">Verified approximately:</span> ${escapeHtml(String(notice))}</li>`
),
...fileWarnings.map(([file, errs]) =>
`<li><span class="font-medium">${escapeHtml(file)}:</span> ${escapeHtml(String(errs))}</li>`
),
Expand Down
29 changes: 16 additions & 13 deletions cloud-run/ffai-runner/bin/server.dart
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,13 @@ final _packageNamePattern = RegExp(r'^[a-z_][a-z0-9_]*$');
// unrepresentable rather than merely discouraged.
final _constraintPattern = RegExp(r'^[A-Za-z0-9^~<>=*+._ -]+$');

// The packages the Flutter SDK supplies, i.e. the only valid `sdk: flutter`
// entries besides `flutter` itself.
const allowedSdkPackages = <String>{
'flutter',
'flutter_test',
'flutter_driver',
'flutter_localizations',
'integration_test',
};
// Which packages the Flutter SDK supplies is not decided here. A caller names
// the packages its project declares as `sdk: flutter`, and this runner emits
// them as `name: {sdk: flutter}` - a value it writes itself, never one a caller
// sent. There was an allowlist of SDK package names here, and it refused good
// deploys: the client classified a package correctly by its pubspec source, and
// the runner rejected it for not appearing on a list that had gone stale.
// `_packageNamePattern` is what actually keeps a key from carrying YAML.

Future<void> main() async {
final port = int.tryParse(Platform.environment['PORT'] ?? '') ?? 8080;
Expand Down Expand Up @@ -538,8 +536,13 @@ Map<String, String> _normalizeDependencyMap(Object? value, String field) {
return result;
}

/// Reads the SDK-supplied package names. Restricted to the packages the Flutter
/// SDK actually provides, so a caller cannot request an arbitrary `sdk:` value.
/// Reads the SDK-supplied package names a caller's project declares.
///
/// Validated as package names rather than against a list of the packages the
/// Flutter SDK happens to ship: a name the SDK does not provide fails
/// `pub get` with an honest error, whereas a list refuses correct deploys the
/// moment it falls behind the SDK. The pattern is the load-bearing check - each
/// name becomes a key in the generated pubspec.
List<String> _normalizeSdkPackages(Object? value) {
if (value == null) return const <String>[];
if (value is! List) {
Expand All @@ -549,8 +552,8 @@ List<String> _normalizeSdkPackages(Object? value) {
final result = <String>[];
for (final raw in value) {
final name = '$raw';
if (!allowedSdkPackages.contains(name)) {
throw FormatException('Unsupported SDK package: $name.');
if (!_packageNamePattern.hasMatch(name)) {
throw FormatException('Invalid SDK package name: $name.');
}
if (!result.contains(name)) result.add(name);
}
Expand Down
320 changes: 0 additions & 320 deletions dist/assets/index-BAqMCFqg.js

This file was deleted.

320 changes: 320 additions & 0 deletions dist/assets/index-ZsXrM4ik.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -2420,7 +2420,7 @@
.pm-cards { padding: 0 16px; }
}
</style>
<script type="module" crossorigin src="/assets/index-BAqMCFqg.js"></script>
<script type="module" crossorigin src="/assets/index-ZsXrM4ik.js"></script>
</head>
<body>
<div class="app-shell">
Expand Down
57 changes: 57 additions & 0 deletions docs/design/BEHAVIOR-OVERRIDES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# Production behavior overrides

The handoff prototype contains demo-only copy and stale pricing. The production implementation must follow these overrides instead of copying the prototype verbatim.

## 1. Runs counter

- **Handoff shows:** topbar credits button labeled `50`, modal reads `50 credits remaining`, and a demo-only note.
- **Production override:**
- Topbar shows the **real remaining monthly runs** for the current user and period.
- Clicking the counter opens the usage modal.
- The modal text changes to the longer-form explanation: `"<used> / <limit> runs this month"` (e.g. `"31 / 50 runs this month"`).
- The modal contains a **Manage Subscription** button that calls the existing `openCustomerPortal()` function (`app.js:4787`).
- **Rationale:** The backend exposes monthly run allowances, not a credits balance. No new backend is required.

## 2. Tier limits and pricing

Source: current `app.js` at `d1dc19c` (`codex/redesign-plan`).

| Tier | Runs | Price (AUD) |
|---|---|---|
| Free | 2 | — |
| Professional | 50 | A$11 |
| Power | 2,000 | A$49 |

- **Override:** Use the values above.
- **Reject these handoff values:**
- Power "unlimited" runs.
- Hero modal A$29 Power price.
- Implementation points:
- `TIER_LIMITS.free === 2` (`app.js:129`)
- `TIER_LIMITS.professional === 50` (`app.js:130`)
- `TIER_LIMITS.power === 2000` (`app.js:131`)
- `BASE_PRICES_AUD.professional === 11` (`app.js:347`)
- `BASE_PRICES_AUD.power === 49` (`app.js:347`)

## 3. Suggestions

- **Override:** Use the 13 client-side regex patterns documented in [TOKENS-AND-ASSETS.md](./TOKENS-AND-ASSETS.md). No remote completion service should be introduced.
- If `config.apiKey` is absent, the hero already falls back to the local list (`custom-code-connect-hero.html:1414`). Keep that behavior exactly.

## 4. Unsupported backend capabilities — truthful states only

The handoff shows several demo-only concepts. Production must not fabricate them:

| Do not fabricate | Truthful production state |
|---|---|
| Purchases inside the hero modal | Pricing modal links to Stripe checkout / portal via existing `startCheckout` / `openCustomerPortal`. |
| Connection success animations | Show real connection status; use loading, error, and retry states. |
| Usage breakdown by day/category | Show only the data the backend returns (`used / limit` and current period). |
| Generation history table | Do not add a history table unless the backend endpoint exists and is enabled. |
| Account deletion flow | Do not add account deletion; if unavailable, omit the control or show a disabled / support message. |

## 5. Other production rules

- Keep the brand logomark inline (SVG), not as an external image request.
- Preserve the real copy and labels from the export, but replace any static placeholders with live data equivalents listed above.
- Do not add OpenDesign chrome, preview labels, or design-process annotations to the production UI.
40 changes: 40 additions & 0 deletions docs/design/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Custom Code Connect — redesign source record

> Linear issue: **STU-371** — Record the canonical redesign sources and production behavior overrides
> Milestone: Custom Code Connect UI
> Branch: `sgardoll/stu-371-record-the-canonical-redesign-sources-and-production-behavior-overrides`
> Base: `codex/redesign-plan` @ `d1dc19cda9ac6fe2fea3f583ae0ff98633938343`

This folder archives the design-to-production contract for the Custom Code Connect UI redesign. It is intended to be read by implementers before they touch `app.js`, `index.html`, or any new component.

## Source archive

The canonical design files live in the read-only handoff folder:

```
/Users/home/Projects/Custom-Code-Connect-redesign/
```

Do not copy the entire archive into `public/`. Reference the specific files and assets documented below, or map them through build-time paths / local-dev symlinks.

## Surface classification

| Surface | File(s) | Role | Production route equivalent |
|---|---|---|---|
| **Hero** | `custom-code-connect-hero.html` | Primary app composition: prompt composer, walkthrough, results. | `/` or `/create` |
| **Journey** | `journey-connect.html` | Supplementary workflow guidance for the multi-step connect flow. | `/journey` or modal/wizard state |
| **Account** | `account-admin.html` | Separate account/product surface (identity, usage, keys, history, security). | `/account` |
| **Plans / monetisation** | `monetisation.html` | Separate plans/pricing surface. | `/plans` or pricing modal |
| **Launcher** | `index.html` | Handoff overview only; **not** a production route. | Excluded |
| **Before/outro/walkthrough variants** | `custom-code-connect-hero-*.html`, `custom-code-connect-logo-outro*.html`, `custom-code-connect-walkthrough.html` | Design process artifacts. | Excluded |
| **Milestone plan** | `MILESTONE-PLAN.html` | Project-management artifact. | Excluded |

See [SOURCE-MANIFEST.json](./SOURCE-MANIFEST.json) for SHA-256 hashes of the canonical files.

## Companion documents

- [TOKENS-AND-ASSETS.md](./TOKENS-AND-ASSETS.md) — extracted design tokens, fonts, and asset paths.
- [RESPONSIVE.md](./RESPONSIVE.md) — viewport matrix, reflow expectations, and layout notes.
- [BEHAVIOR-OVERRIDES.md](./BEHAVIOR-OVERRIDES.md) — production behavior that overrides stale handoff copy.
- [REFERENCE.md](./REFERENCE.md) — how workers access the local handoff folder and screenshot references.
- [TRACEABILITY.md](./TRACEABILITY.md) — requirement-to-ticket/surface mapping.
Loading
Loading