Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
98cfb90
fix: separate PostgreSQL planning connections from bookkeeping writes
rrrodzilla Sep 25, 2026
b43248e
fix: validate array filters and match PostgreSQL text literally
rrrodzilla Sep 25, 2026
d36a24f
fix(webhooks): enforce payload and destination boundaries
rrrodzilla Sep 25, 2026
54a32d1
test(webhooks): use canonical integer field syntax
rrrodzilla Sep 25, 2026
d7689bc
chore: integrate repository security policy
rrrodzilla Sep 25, 2026
a5cebdf
fix: enforce tenant and API validation boundaries
rrrodzilla Sep 25, 2026
a0f0193
fix(cli): preflight migrations and respect operator configuration
rrrodzilla Sep 25, 2026
46c67f2
fix(cli): list destructive batch steps before refusing writes
rrrodzilla Sep 25, 2026
d38af47
docs: clarify required tenant annotations
rrrodzilla Sep 25, 2026
24857cf
fix: validate patch relations after hooks and tenant lookup
rrrodzilla Sep 25, 2026
1f3cb23
chore(release): prepare schemaforge v0.46.0
rrrodzilla Sep 25, 2026
aa285ef
test: align tenant fixtures and verify subscription write validation
rrrodzilla Sep 25, 2026
919b759
fix(cli): match framework listener configuration providers
rrrodzilla Sep 25, 2026
a9ae273
fix: align configuration providers and write-order documentation
rrrodzilla Sep 25, 2026
9bd9aba
test: align connection diagnostics and order extractor definitions
rrrodzilla Sep 25, 2026
7f6bde5
test: preserve authorization assertions with redacted errors
rrrodzilla Sep 25, 2026
ebf5d88
ci: validate generated site types and lint before browser tests
rrrodzilla Sep 25, 2026
0aa7984
build: enable JSON serialization in site templates
rrrodzilla Sep 25, 2026
91f384f
fix: scope generated site requests and clarify error feedback
rrrodzilla Sep 25, 2026
06e17af
feat(site): configure product branding and asset overrides
rrrodzilla Sep 25, 2026
534e0d4
docs: remove organization branding from page template guidance
rrrodzilla Sep 25, 2026
ba29c48
test(site): keep mocked session expiry within browser timer bounds
rrrodzilla Sep 25, 2026
7982ee4
fix(site): authenticate proxied attachment downloads
rrrodzilla Sep 25, 2026
907a814
test(site): load Vite React default exports in download fixture
rrrodzilla Sep 25, 2026
4b96b9a
fix(site): generate typed fields and respect form write authority
rrrodzilla Sep 25, 2026
4b228f2
docs: include generated site fixes in v0.46.0 release notes
rrrodzilla Sep 25, 2026
9fde462
fix(site): preserve composites containing hidden descendants
rrrodzilla Sep 25, 2026
09da8f3
fix(site): serialize package names with JSON template escaping
rrrodzilla Sep 25, 2026
2d6bca2
fix(site): serialize exact form metadata without type casts
rrrodzilla Sep 25, 2026
085e615
test(site): follow shared authentication and payload helpers
rrrodzilla Sep 25, 2026
88ea9c8
test(site): exercise global notifications inside authenticated shell
rrrodzilla Sep 25, 2026
375358a
test(site): await successful entity creation before editing
rrrodzilla Sep 25, 2026
55c903e
fix(auth): align resource arrays with Cedar schema projection
rrrodzilla Sep 25, 2026
370b9ee
docs: note authorization projection correction
rrrodzilla Sep 25, 2026
a5e4748
ci(site): retain complete server logs on smoke failures
rrrodzilla Sep 25, 2026
7f2c13d
test(site): match schema-specific job entity identifiers
rrrodzilla Sep 25, 2026
63cc645
test(auth): construct nested array fixture with core types
rrrodzilla Sep 25, 2026
d5907a2
fix(api): preserve declared types in composite field values
rrrodzilla Sep 25, 2026
34dea03
docs: note typed composite conversion fix
rrrodzilla Sep 25, 2026
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
12 changes: 12 additions & 0 deletions .Codex/plans/cli-migration-behavior.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# CLI migration and operator behavior

Apply Rust planner/author standards using existing domain and error types.

1. Plan every desired schema before apply/migrate execution and reject any destructive noninteractive batch before migration, metadata, or revision preparation writes. Preserve interactive per-schema consent and dry-run plans. Test mixed safe/destructive batches with zero writes.
2. Preserve configured listener host/port with optional flags. Keep SchemaForge loopback as its unconfigured host, respecting the framework config search and ACTON environment layers. Test omitted flags, explicit default overrides, and config-file bind/port.
3. Keep the existing public RequiresConfirmation enum variant for source compatibility, but label it review in Display/serialized output, accept legacy serialized spelling, and document it as informational consistently. Introduce plan-aware step classification for fresh unique constraints and test both existing/new schema uniqueness.
4. Add bounded 429 retries to the entity HTTP client, with --max-retries and Retry-After seconds/date support. Rebuild identical requests only for explicit 429 responses, no transport or 5xx retries. Test exhaustion, eventual success, non-429 refusal, and delay parsing.
5. Correct the rule-ordering reference and document governor defaults, reverse-proxy trust and probe configuration.
6. Coordinate read-only CLI connections and webhook validation with owning agents.

Validation: cargo nextest run for core and CLI with postgres feature, cargo clippy warnings denied, formatting. Root performs workspace integration and release. Semver recommendation: minor because migration machine-readable review labels change and CLI functionality is added; retain deserialization compatibility.
15 changes: 15 additions & 0 deletions .Codex/plans/site-field-authority.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Generated field types and authority, issues 191 and 192

Use the existing view-model and template architecture. Keep display types complete while filtering form authority separately.

- Add duration (Go-style duration string validation), bytes (base64 text with decoded size constraint), and map (typed Record JSON textarea) mapping. Preserve recursive composite/array type projection. Format duration wire strings into readable units without altering submitted values.
- Project computed/read-role/write-role metadata on FieldView. Omit computed and derived fields from form controls and form validators. Use current auth roles at render/parse/submit time, not module initialization. Hide read-denied controls, render write-denied values inert, and ensure validation does not require denied fields.
- Use recursive metadata to filter initial and submitted state, stripping read-denied, computed/derived and unwritable payload fields including nested composites. Preserve readable, write-denied initial values for read-only display. Normalize JSON and composite values recursively so nested supported fields remain round-trippable.
- Update field-type and permissions documentation. Add generator regression checks plus Playwright behavior tests for serialization, validation and role changes. Fail generation for any remaining unsupported required field.
- Preserve existing generated styling and accessibility labels, avoiding controls which imply unavailable actions (UI design expert, interaction patterns).

No new dependencies or error types required. Semver: fixes in release already planned by root. Run only targeted cargo check locally; root runs generation, TypeScript build/lint, and browser checks in CI. Sign conventional commits; no push.

## CI follow-up: exact browser metadata

Site CI reported TS2352 because full FieldView JSON contains display-only properties. Introduce a dedicated recursive FormFieldSpec serialization type; use it for all template metadata arguments and entity normalizer tables. Remove casts, preserve hidden-descendant/role flags, and test the exact serialized keys at every depth.
3 changes: 3 additions & 0 deletions .Codex/ui-design-review-site-field-authority.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# UI Design Review

The generated forms imply users can edit values the server will discard. Apply Norman's affordance principle: omit computed inputs, hide unreadable fields, and render readable but unwritable values as inert text. Share gating across validation and payload construction so hidden required controls cannot block saving. Keep existing form styling and labels; provide duration and base64 format hints. Browser regression coverage should assert visible controls and actual submitted payloads for both permitted and denied roles.
11 changes: 11 additions & 0 deletions .github/workflows/site-e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,14 @@ jobs:
crates/schema-forge-cli/tests/site_e2e/playwright/test-results
if-no-files-found: ignore
retention-days: 7

- name: Upload complete server logs on failure
if: failure()
uses: actions/upload-artifact@v7
with:
name: site-server-logs
path: |
target/site-e2e-*/backend.log
target/site-e2e-*/vite.log
if-no-files-found: ignore
retention-days: 7
77 changes: 77 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,83 @@ is pre-1.0; breaking changes bump the **minor** version per

## [Unreleased]

## [0.46.0] - 2026-09-25

### Runtime and API behavior

- Validate tenant declarations consistently across startup, CLI schema application,
and runtime schema changes. Applications with a tenant root must annotate every
application schema; built-in system schemas remain shared.
- Validate relation targets against tenant scope and read authorization before
create, PUT, or PATCH persists. Platform administrators retain their documented
cross-tenant capabilities.
- Apply hidden-field projection to relation display labels and webhook payloads.
Webhooks use a fixed conservative field policy and plain JSON payload version 2.
- Enforce configured webhook URL schemes and public destinations during
configuration and delivery. Delivery checks and pins DNS results, with redirects
and environment proxies disabled.
- Reject undeclared entity fields before persistence. Foreign-key errors include
machine-readable `error` and `message` fields; entity and schema JSON rejections
use the API error envelope. Internal storage diagnostics stay out of REST and
GraphQL error messages, and database connection errors omit credentials.
- Keep authorization resource attributes aligned with the generated Cedar schema,
so arrays of unsupported policy types do not incorrectly deny valid writes.
- Convert nested composite values using their declared field types before storage.

### Database and operator fixes

- PostgreSQL planning and inspection connections perform no bookkeeping DDL.
Fresh databases plan as empty registries, and read-only roles can inspect existing
metadata without schema creation privileges.
- PostgreSQL `contains` and `startswith` now match literal, case-sensitive text.
Unsupported array filter comparisons return validation errors before execution.
- `apply` and `migrate --execute` preflight all selected migration plans before
applying a noninteractive batch. Refusals identify every destructive schema and
step requiring `--force`.
- Migration warnings display `review` when they are informational. New-table unique
constraints are safe, and new schemas retain their `CREATE` label.
- Explicit listener flags override environment and file settings; omitted flags
preserve configuration. An unconfigured server defaults to `127.0.0.1:3000`.
- Entity CLI requests retry HTTP 429 with `Retry-After` support and bounded fallback
backoff, controlled by `--max-retries`. Transport failures are not retried.
- Document governor quotas, proxy configuration, probe routes, the full write-rule
order, and webhook delivery guarantees.

### Generated sites

- Carry the active tenant on entity, invitation, and file requests, including
requests retried after a token refresh.
- Show readable API errors and avoid duplicate global notifications when pages
handle errors locally. Projects can customize the preserved error-toast helper.
- Generate typed duration, map, and base64 bytes fields in forms, lists, and
details. Form validation and payload normalization respect computed fields and
role-based field access. Composites with protected children remain read-only.
- Configure the product name, title suffix, and SVG logos and favicon through
`[schema_forge.site]` or generation flags. Default marks are neutral, and CSS,
title helpers, and SVG assets support template overrides and drift checking.
- CI now builds and lints generated TypeScript before running browser tests.

### Upgrade notes

Webhook consumers must support `payload_version: 2` and plain JSON field values.
Hidden fields and fields with field-access annotations are excluded. Webhooks
remain best effort with no durable history or replay; applications must reconcile
current state separately when delivery gaps matter. See [webhooks](docs/webhooks.md).

Before upgrading a tenanted deployment, annotate every application schema with its
intended tenant relationship and migrate existing ownership explicitly. Unannotated
application schemas are no longer implicitly shared when a tenant root exists.
See [tenant isolation](docs/tenant-isolation.md).

Migration safety serialization emits `Review`; legacy `RequiresConfirmation` input
is still accepted. Rust embedders must update webhook event constructor calls to
pass schema definitions, and handler callers must use the new JSON extractor.
Workspace crate versions are coordinated for the updated public core/backend types.

Regenerate sites to update owned API and branding helpers. Existing customized
page shells remain preserved; see the migration instructions for
[error feedback](docs/generated-site-errors.md) and [branding](docs/site-branding.md).

## [0.45.0] - 2026-09-24

### Security and correctness
Expand Down
21 changes: 12 additions & 9 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1056,3 +1056,7 @@ See the project repository for license information.
Platform administrators can browse recorded audit events and verify bounded chain ranges through the [audit API](docs/audit-api-reference.md). Access is deployment-wide, uses the active framework audit store, and reports collection limits separately from local chain consistency. Available in v0.42.0.

See [safe schema changes](docs/migrations/safe-schema-changes.md) for declared field renames, destructive migration opt-ins, PostgreSQL relation integrity, and explicit tenancy migrations.

See the [webhook delivery contract](docs/webhooks.md) for delivery guarantees, payload format, and destination policy.

Configure product names, title suffixes, and SVG marks with [generated-site branding](docs/site-branding.md).
4 changes: 2 additions & 2 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ Security fixes go into the latest release. Older versions do not receive backpor

| Version | Supported |
|---|---|
| 0.45.x (latest release) | Yes |
| earlier than 0.45 | No, please upgrade |
| 0.46.x (latest release) | Yes |
| earlier than 0.46 | No, please upgrade |

## Scope

Expand Down
4 changes: 2 additions & 2 deletions crates/schema-forge-acton/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "schema-forge-acton"
version = "0.44.0"
version = "0.45.0"
edition = "2021"

[dependencies]
Expand Down Expand Up @@ -44,7 +44,7 @@ aws-lc-rs = { version = "1", features = ["fips"], optional = true }
rustls = { version = "0.23", default-features = false, features = ["std", "aws_lc_rs", "logging"] }
schema-forge-signing = { version = "0.1.0", path = "../schema-forge-signing" }
lettre = { version = "0.11.22", default-features = false, features = ["tokio1-rustls", "aws-lc-rs", "webpki-roots", "smtp-transport", "builder", "pool", "hostname"] }
schema-forge-cel = { version = "0.11.0", path = "../schema-forge-cel" }
schema-forge-cel = { version = "0.12.0", path = "../schema-forge-cel" }
rust_xlsxwriter = { version = "0.95.0", features = ["chrono"] }
zip = "8.6.0"

Expand Down
32 changes: 21 additions & 11 deletions crates/schema-forge-acton/src/authz/adapters.rs
Original file line number Diff line number Diff line change
Expand Up @@ -220,13 +220,13 @@ pub fn build_resource_entity(

let mut attrs: HashMap<String, RestrictedExpression> = HashMap::new();
for (field_name, value) in &entity.fields {
// `@hidden` fields are never declared as Cedar attributes, so
// including them here would fail strict-mode entity validation.
// The schema's field definition is the canonical source of the
// hidden flag — entities loaded from storage may still carry the
// value, but it must not leak into authorization context.
// Only types declared by the schema generator may enter Cedar.
// Unsupported arrays otherwise become undeclared set attributes,
// causing strict validation to reject even permitted operations.
if let Some(field_def) = schema.field(field_name) {
if field_def.is_hidden() {
if field_def.is_hidden()
|| crate::cedar::schema_gen::cedar_type_for(&field_def.field_type).is_none()
{
continue;
}
// The Cedar schema declares file attributes as strings. Supply
Expand Down Expand Up @@ -304,10 +304,11 @@ pub fn build_resource_placeholder(schema: &SchemaDefinition) -> Result<CedarEnti

let mut attrs: HashMap<String, RestrictedExpression> = HashMap::new();
for field in &schema.fields {
if !field.is_required() || field.is_hidden() {
// Hidden fields are not declared in the Cedar schema, so the
// strict-mode entity validator would reject a placeholder that
// includes them.
if !field.is_required()
|| field.is_hidden()
|| crate::cedar::schema_gen::cedar_type_for(&field.field_type).is_none()
{
// Hidden and unsupported field types have no Cedar attribute.
continue;
}
if let Some(expr) = default_cedar_expr(&field.field_type) {
Expand Down Expand Up @@ -378,7 +379,7 @@ pub fn dynamic_to_cedar(value: &DynamicValue) -> Option<RestrictedExpression> {
}
DynamicValue::Array(items) => {
let mapped: Vec<RestrictedExpression> =
items.iter().filter_map(dynamic_to_cedar).collect();
items.iter().map(dynamic_to_cedar).collect::<Option<_>>()?;
Some(RestrictedExpression::new_set(mapped))
}
DynamicValue::Null | DynamicValue::Json(_) | DynamicValue::Composite(_) => None,
Expand Down Expand Up @@ -424,6 +425,15 @@ mod principal_claim_tests {
PrincipalClaimsConfig,
};

#[test]
fn array_projection_does_not_silently_drop_unrepresentable_members() {
let value = DynamicValue::Array(vec![
DynamicValue::Integer(1),
DynamicValue::Json(serde_json::json!({"value": 2})),
]);
assert!(dynamic_to_cedar(&value).is_none());
}

fn claims_with(custom: HashMap<String, serde_json::Value>) -> Claims {
Claims {
sub: "user:alice".into(),
Expand Down
2 changes: 1 addition & 1 deletion crates/schema-forge-acton/src/cedar/schema_gen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,7 @@ fn write_per_field_actions(
/// Returns `None` for types that have no clean Cedar representation
/// (composites, arbitrary JSON). Such fields will not appear as resource
/// attributes; policies cannot test them.
fn cedar_type_for(ft: &FieldType) -> Option<String> {
pub(crate) fn cedar_type_for(ft: &FieldType) -> Option<String> {
match ft {
FieldType::Text(_) | FieldType::RichText => Some("String".into()),
FieldType::Integer(_) => Some("Long".into()),
Expand Down
17 changes: 17 additions & 0 deletions crates/schema-forge-acton/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,21 @@ pub struct SchemaForgeSettings {
/// environment variables still override these values.
#[serde(default)]
pub client: ClientConfig,

/// Branding for generated sites.
#[serde(default)]
pub site: SiteBrandingConfig,
}

/// Optional generated-site identity and SVG assets.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct SiteBrandingConfig {
pub name: Option<String>,
/// Defaults to name; an empty string disables the suffix.
pub title_suffix: Option<String>,
pub logo: Option<PathBuf>,
pub logo_on_dark: Option<PathBuf>,
pub favicon: Option<PathBuf>,
}

/// `[schema_forge.client]` section of config.toml.
Expand Down Expand Up @@ -165,6 +180,7 @@ impl Default for SchemaForgeSettings {
authz: AuthzConfig::default(),
signing: SigningConfig::default(),
client: ClientConfig::default(),
site: SiteBrandingConfig::default(),
}
}
}
Expand Down Expand Up @@ -195,6 +211,7 @@ mod tests {
authz: AuthzConfig::default(),
signing: SigningConfig::default(),
client: ClientConfig::default(),
site: SiteBrandingConfig::default(),
},
};
let json = serde_json::to_string(&config).unwrap();
Expand Down
Loading
Loading