Skip to content

Commit c33018f

Browse files
committed
fix(builder): sanitize standalone payload against schema fields
1 parent 17ef2ed commit c33018f

3 files changed

Lines changed: 176 additions & 19 deletions

File tree

Lines changed: 23 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
1-
name: Release
1+
name: Publish
22

33
on:
4-
release:
5-
types:
6-
- published
4+
push:
5+
tags:
6+
- "v*"
77

88
permissions:
99
contents: read
1010
id-token: write
1111

1212
concurrency:
13-
group: release-${{ github.workflow }}-${{ github.ref }}
13+
group: publish-${{ github.ref }}
1414
cancel-in-progress: false
1515

1616
jobs:
@@ -46,9 +46,6 @@ jobs:
4646
name: Publish to npm
4747
runs-on: ubuntu-latest
4848
needs: quality
49-
permissions:
50-
contents: read
51-
id-token: write
5249

5350
steps:
5451
- name: Checkout
@@ -62,27 +59,36 @@ jobs:
6259
- name: Setup Node
6360
uses: actions/setup-node@v6
6461
with:
65-
node-version: "22"
62+
node-version: "22.14.0"
63+
check-latest: true
6664
registry-url: "https://registry.npmjs.org"
6765

66+
- name: Update npm
67+
run: npm install -g npm@^11.5.1
68+
69+
- name: Show versions
70+
run: |
71+
node --version
72+
npm --version
73+
echo "GITHUB_REF=${GITHUB_REF}"
74+
echo "GITHUB_REF_NAME=${GITHUB_REF_NAME}"
75+
6876
- name: Install dependencies
6977
run: bun install --frozen-lockfile
7078

71-
- name: Sync version from release tag
79+
- name: Verify tag matches package.json
7280
run: |
73-
TAG="${{ github.event.release.tag_name }}"
81+
TAG="${GITHUB_REF_NAME}"
7482
VERSION="${TAG#v}"
83+
PACKAGE_VERSION="$(node -p "require('./package.json').version")"
7584
76-
if ! echo "$VERSION" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+([-.][0-9A-Za-z.]+)?(\+[0-9A-Za-z.-]+)?$'; then
77-
echo "Invalid release tag: $TAG"
78-
echo "Expected semver tag like v1.2.3 or 1.2.3"
85+
if [ "$VERSION" != "$PACKAGE_VERSION" ]; then
86+
echo "Tag version ($VERSION) does not match package.json version ($PACKAGE_VERSION)"
7987
exit 1
8088
fi
8189
82-
npm version "$VERSION" --no-git-tag-version --allow-same-version
83-
8490
- name: Build package
8591
run: bun run build
8692

8793
- name: Publish package
88-
run: npm publish --access public
94+
run: npm publish --provenance

.github/workflows/version.yml

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
name: Version
2+
3+
on:
4+
workflow_dispatch:
5+
inputs:
6+
release_type:
7+
description: Release type
8+
required: true
9+
type: choice
10+
options:
11+
- patch
12+
- minor
13+
- major
14+
15+
permissions:
16+
contents: write
17+
18+
concurrency:
19+
group: version-${{ github.ref }}
20+
cancel-in-progress: false
21+
22+
jobs:
23+
version:
24+
name: Bump version and create tag
25+
runs-on: ubuntu-latest
26+
27+
steps:
28+
- name: Checkout
29+
uses: actions/checkout@v6
30+
with:
31+
fetch-depth: 0
32+
33+
- name: Setup Node
34+
uses: actions/setup-node@v6
35+
with:
36+
node-version: "22.14.0"
37+
check-latest: true
38+
39+
- name: Update npm
40+
run: npm install -g npm@^11.5.1
41+
42+
- name: Show versions
43+
run: |
44+
node --version
45+
npm --version
46+
47+
- name: Configure git
48+
run: |
49+
git config user.name "github-actions[bot]"
50+
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
51+
52+
- name: Bump version
53+
run: |
54+
npm version ${{ inputs.release_type }} -m "chore(release): %s"
55+
56+
- name: Push commit and tag
57+
run: |
58+
git push origin HEAD
59+
git push origin --tags

src/runtime/renderers/default/FormForgeRenderer.vue

Lines changed: 94 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,53 @@ function getResolvedZodSchema(): object | undefined {
278278
return internalForm.zodSchema.value as object | undefined
279279
}
280280
281+
function resolveSchemaFieldNames(schema: FormForgeFormSchema): Set<string> {
282+
const names = new Set<string>()
283+
284+
if (Array.isArray(schema.fields)) {
285+
for (const field of schema.fields) {
286+
if (typeof field.name === 'string' && field.name !== '') {
287+
names.add(field.name)
288+
}
289+
}
290+
}
291+
292+
if (Array.isArray(schema.pages)) {
293+
for (const page of schema.pages) {
294+
if (!Array.isArray(page.fields)) {
295+
continue
296+
}
297+
298+
for (const field of page.fields) {
299+
if (typeof field.name === 'string' && field.name !== '') {
300+
names.add(field.name)
301+
}
302+
}
303+
}
304+
}
305+
306+
return names
307+
}
308+
309+
function sanitizePayloadWithSchema(value: FormForgeSubmissionPayload, schema: FormForgeFormSchema): FormForgeSubmissionPayload {
310+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
311+
return {}
312+
}
313+
314+
const allowedFieldNames = resolveSchemaFieldNames(schema)
315+
const sanitizedPayload: FormForgeSubmissionPayload = {}
316+
317+
for (const [key, entryValue] of Object.entries(value)) {
318+
if (!allowedFieldNames.has(key)) {
319+
continue
320+
}
321+
322+
sanitizedPayload[key] = entryValue
323+
}
324+
325+
return sanitizedPayload
326+
}
327+
281328
const formState = computed<FormForgeSubmissionPayload>({
282329
get: () => {
283330
const value = usesExternalModel.value
@@ -288,6 +335,13 @@ const formState = computed<FormForgeSubmissionPayload>({
288335
return {}
289336
}
290337
338+
if (usesExternalModel.value) {
339+
const schema = getResolvedSchema()
340+
if (schema !== null) {
341+
return sanitizePayloadWithSchema(value, schema)
342+
}
343+
}
344+
291345
return value
292346
},
293347
set: (value: FormForgeSubmissionPayload): void => {
@@ -300,6 +354,26 @@ const formState = computed<FormForgeSubmissionPayload>({
300354
}
301355
})
302356
357+
watch(
358+
() => [usesExternalModel.value, getResolvedSchema(), unwrapModelValueProp(props.modelValue)] as const,
359+
([externalModel, schema, modelValue]) => {
360+
if (!externalModel || schema === null) {
361+
return
362+
}
363+
364+
const sanitizedValue = sanitizePayloadWithSchema(modelValue, schema)
365+
if (areValuesEqual(modelValue, sanitizedValue)) {
366+
return
367+
}
368+
369+
emit('update:modelValue', sanitizedValue)
370+
},
371+
{
372+
immediate: true,
373+
deep: true
374+
}
375+
)
376+
303377
const displayPages = computed<FormForgePageSchema[]>(() => {
304378
const schema = getResolvedSchema()
305379
if (schema === null) {
@@ -1234,6 +1308,24 @@ function getFieldMetaUi(field: FormForgeFieldSchema): { formField?: FormForgeDyn
12341308
}
12351309
}
12361310
1311+
function mergeUiClass(defaultClass: string, value: unknown): string {
1312+
if (typeof value !== 'string' || value.trim() === '') {
1313+
return defaultClass
1314+
}
1315+
1316+
return `${defaultClass} ${value}`
1317+
}
1318+
1319+
function getResolvedFormFieldUi(field: FormForgeFieldSchema): FormForgeDynamicObject {
1320+
const metaUi = getFieldMetaUi(field).formField
1321+
1322+
return {
1323+
...metaUi,
1324+
label: mergeUiClass('text-default', metaUi?.label),
1325+
help: mergeUiClass('text-muted', metaUi?.help)
1326+
}
1327+
}
1328+
12371329
function getFieldValue(field: FormForgeFieldSchema): FormForgeSubmissionPayload[string] {
12381330
return formState.value[field.name]
12391331
}
@@ -1514,7 +1606,7 @@ async function onSubmit(): Promise<void> {
15141606
>
15151607
<h3
15161608
v-if="page.title !== ''"
1517-
class="text-base font-semibold"
1609+
class="text-base font-semibold text-default"
15181610
>
15191611
{{ page.title }}
15201612
</h3>
@@ -1535,7 +1627,7 @@ async function onSubmit(): Promise<void> {
15351627
:label="field.label"
15361628
:help="field.help_text"
15371629
:required="isFieldRequired(field)"
1538-
:ui="getFieldMetaUi(field).formField"
1630+
:ui="getResolvedFormFieldUi(field)"
15391631
@focusout="() => onFieldBlur(field.name)"
15401632
>
15411633
<UInput

0 commit comments

Comments
 (0)