Skip to content
Merged
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## Unreleased

## 0.0.10 - 2025-11-24

- fix: ignore Zod.describe method to not interfere with type generation

## 0.0.9 - 2025-11-20

- feat: upgrade to zod v4 + support for `meta`
Expand Down
38 changes: 19 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ $ npm install -g @reforge-com/cli
$ reforge COMMAND
running command...
$ reforge (--version)
@reforge-com/cli/0.0.9 darwin-arm64 node-v24.4.1
@reforge-com/cli/0.0.10 darwin-arm64 node-v24.4.1
$ reforge --help [COMMAND]
USAGE
$ reforge COMMAND
Expand Down Expand Up @@ -91,7 +91,7 @@ EXAMPLES
$ reforge create my.new.string --type json --value="{\"key\": \"value\"}"
```

_See code: [src/commands/create.ts](https://github.com/ReforgeHQ/cli/blob/v0.0.9/src/commands/create.ts)_
_See code: [src/commands/create.ts](https://github.com/ReforgeHQ/cli/blob/v0.0.10/src/commands/create.ts)_

## `reforge download`

Expand Down Expand Up @@ -124,7 +124,7 @@ EXAMPLES
$ reforge download --environment=test --sdk-key=YOUR_SDK_KEY
```

_See code: [src/commands/download.ts](https://github.com/ReforgeHQ/cli/blob/v0.0.9/src/commands/download.ts)_
_See code: [src/commands/download.ts](https://github.com/ReforgeHQ/cli/blob/v0.0.10/src/commands/download.ts)_

## `reforge generate`

Expand Down Expand Up @@ -194,7 +194,7 @@ EXAMPLES
$ reforge generate --targets node-ts -o ./dist # combine with targets
```

_See code: [src/commands/generate.ts](https://github.com/ReforgeHQ/cli/blob/v0.0.9/src/commands/generate.ts)_
_See code: [src/commands/generate.ts](https://github.com/ReforgeHQ/cli/blob/v0.0.10/src/commands/generate.ts)_

## `reforge generate-new-hex-key`

Expand All @@ -217,7 +217,7 @@ EXAMPLES
$ reforge generate-new-hex-key
```

_See code: [src/commands/generate-new-hex-key.ts](https://github.com/ReforgeHQ/cli/blob/v0.0.9/src/commands/generate-new-hex-key.ts)_
_See code: [src/commands/generate-new-hex-key.ts](https://github.com/ReforgeHQ/cli/blob/v0.0.10/src/commands/generate-new-hex-key.ts)_

## `reforge get [NAME]`

Expand Down Expand Up @@ -250,7 +250,7 @@ EXAMPLES
$ reforge get my.config.name --environment=production
```

_See code: [src/commands/get.ts](https://github.com/ReforgeHQ/cli/blob/v0.0.9/src/commands/get.ts)_
_See code: [src/commands/get.ts](https://github.com/ReforgeHQ/cli/blob/v0.0.10/src/commands/get.ts)_

## `reforge info [NAME]`

Expand Down Expand Up @@ -281,7 +281,7 @@ EXAMPLES
$ reforge info my.config.name
```

_See code: [src/commands/info.ts](https://github.com/ReforgeHQ/cli/blob/v0.0.9/src/commands/info.ts)_
_See code: [src/commands/info.ts](https://github.com/ReforgeHQ/cli/blob/v0.0.10/src/commands/info.ts)_

## `reforge interactive`

Expand All @@ -299,7 +299,7 @@ EXAMPLES
$ reforge
```

_See code: [src/commands/interactive.ts](https://github.com/ReforgeHQ/cli/blob/v0.0.9/src/commands/interactive.ts)_
_See code: [src/commands/interactive.ts](https://github.com/ReforgeHQ/cli/blob/v0.0.10/src/commands/interactive.ts)_

## `reforge list`

Expand Down Expand Up @@ -336,7 +336,7 @@ EXAMPLES
$ reforge list --feature-flags
```

_See code: [src/commands/list.ts](https://github.com/ReforgeHQ/cli/blob/v0.0.9/src/commands/list.ts)_
_See code: [src/commands/list.ts](https://github.com/ReforgeHQ/cli/blob/v0.0.10/src/commands/list.ts)_

## `reforge login`

Expand Down Expand Up @@ -364,7 +364,7 @@ EXAMPLES
$ reforge login --profile myprofile
```

_See code: [src/commands/login.ts](https://github.com/ReforgeHQ/cli/blob/v0.0.9/src/commands/login.ts)_
_See code: [src/commands/login.ts](https://github.com/ReforgeHQ/cli/blob/v0.0.10/src/commands/login.ts)_

## `reforge logout`

Expand All @@ -387,7 +387,7 @@ EXAMPLES
$ reforge logout
```

_See code: [src/commands/logout.ts](https://github.com/ReforgeHQ/cli/blob/v0.0.9/src/commands/logout.ts)_
_See code: [src/commands/logout.ts](https://github.com/ReforgeHQ/cli/blob/v0.0.10/src/commands/logout.ts)_

## `reforge mcp`

Expand Down Expand Up @@ -420,7 +420,7 @@ EXAMPLES
$ reforge mcp --url http://local-launch.goatsofreforge.com:3003/api/v1/mcp
```

_See code: [src/commands/mcp.ts](https://github.com/ReforgeHQ/cli/blob/v0.0.9/src/commands/mcp.ts)_
_See code: [src/commands/mcp.ts](https://github.com/ReforgeHQ/cli/blob/v0.0.10/src/commands/mcp.ts)_

## `reforge override [NAME]`

Expand Down Expand Up @@ -459,7 +459,7 @@ EXAMPLES
$ reforge override my.double.config --value=3.14159
```

_See code: [src/commands/override.ts](https://github.com/ReforgeHQ/cli/blob/v0.0.9/src/commands/override.ts)_
_See code: [src/commands/override.ts](https://github.com/ReforgeHQ/cli/blob/v0.0.10/src/commands/override.ts)_

## `reforge profile`

Expand All @@ -482,7 +482,7 @@ EXAMPLES
$ reforge profile
```

_See code: [src/commands/profile.ts](https://github.com/ReforgeHQ/cli/blob/v0.0.9/src/commands/profile.ts)_
_See code: [src/commands/profile.ts](https://github.com/ReforgeHQ/cli/blob/v0.0.10/src/commands/profile.ts)_

## `reforge schema NAME`

Expand Down Expand Up @@ -516,7 +516,7 @@ EXAMPLES
$ reforge schema my-schema --get
```

_See code: [src/commands/schema.ts](https://github.com/ReforgeHQ/cli/blob/v0.0.9/src/commands/schema.ts)_
_See code: [src/commands/schema.ts](https://github.com/ReforgeHQ/cli/blob/v0.0.10/src/commands/schema.ts)_

## `reforge serve DATA-FILE`

Expand Down Expand Up @@ -552,7 +552,7 @@ EXAMPLES
$ reforge serve ./reforge.test.588.config.json --port=3099
```

_See code: [src/commands/serve.ts](https://github.com/ReforgeHQ/cli/blob/v0.0.9/src/commands/serve.ts)_
_See code: [src/commands/serve.ts](https://github.com/ReforgeHQ/cli/blob/v0.0.10/src/commands/serve.ts)_

## `reforge set-default [NAME]`

Expand Down Expand Up @@ -596,7 +596,7 @@ EXAMPLES
$ reforge set-default my.config.name --env-var=MY_ENV_VAR_NAME --environment=production
```

_See code: [src/commands/set-default.ts](https://github.com/ReforgeHQ/cli/blob/v0.0.9/src/commands/set-default.ts)_
_See code: [src/commands/set-default.ts](https://github.com/ReforgeHQ/cli/blob/v0.0.10/src/commands/set-default.ts)_

## `reforge whoami`

Expand All @@ -619,7 +619,7 @@ EXAMPLES
$ reforge whoami
```

_See code: [src/commands/whoami.ts](https://github.com/ReforgeHQ/cli/blob/v0.0.9/src/commands/whoami.ts)_
_See code: [src/commands/whoami.ts](https://github.com/ReforgeHQ/cli/blob/v0.0.10/src/commands/whoami.ts)_

## `reforge workspace`

Expand All @@ -642,7 +642,7 @@ EXAMPLES
$ reforge workspace
```

_See code: [src/commands/workspace.ts](https://github.com/ReforgeHQ/cli/blob/v0.0.9/src/commands/workspace.ts)_
_See code: [src/commands/workspace.ts](https://github.com/ReforgeHQ/cli/blob/v0.0.10/src/commands/workspace.ts)_
<!-- commandsstop -->

## Local Development
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"packageManager": "yarn@4.11.0",
"name": "@reforge-com/cli",
"version": "0.0.9",
"version": "0.0.10",
"author": "Jeffrey Chupp @semanticart",
"bugs": {
"url": "https://github.com/ReforgeHQ/cli/issues"
Expand Down
63 changes: 60 additions & 3 deletions src/codegen/schema-evaluator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,45 @@ export function validateAst(ast: any, parentMap: WeakMap<any, any>): {error?: st
return {error, isValid}
}

/**
* Removes .describe() calls from the AST by filtering them out
* This is more robust than regex as it properly handles nested structures
*
* @param ast - The AST to process
* @param schemaString - The original schema string
* @returns The schema string with .describe() calls removed
*/
function removeDescribeCalls(ast: any, schemaString: string): string {
const nodesToRemove: Array<{start: number; end: number}> = []

// Walk the AST and find all .describe() calls
walk.simple(ast, {
CallExpression(node: any) {
if (
node.callee.type === 'MemberExpression' &&
node.callee.property.type === 'Identifier' &&
node.callee.property.name === 'describe'
) {
// Mark the entire .describe(...) call for removal
// We want to remove from the dot before 'describe' to the closing paren
const start = node.callee.property.start - 1 // Include the dot
const end = node.end
nodesToRemove.push({start, end})
}
},
})

// Sort in reverse order so we can remove from the end without affecting earlier positions
nodesToRemove.sort((a, b) => b.start - a.start)

let result = schemaString
for (const {start, end} of nodesToRemove) {
result = result.slice(0, Math.max(0, start)) + result.slice(Math.max(0, end))
}

return result
}

/**
* Securely evaluates a Zod schema string using AST validation
*
Expand Down Expand Up @@ -265,11 +304,29 @@ export function secureEvaluateSchema(
if (!isValid) {
return {error, success: false}
}

// Remove .describe() calls after validation
const filteredSchema = removeDescribeCalls(ast, trimmedSchema)

// Phase 2: Execute with Function constructor
// We're deliberately avoiding VM modules due to security concerns
// The AST validation provides our primary security layer
// eslint-disable-next-line no-new-func
const constructSchema = new Function('z', `return ${filteredSchema}`)
const schema = constructSchema(z)

// Phase 3: Validate Result
if (!schema || typeof schema !== 'object' || !('_def' in schema)) {
return {
error: 'The provided string did not evaluate to a valid Zod schema',
success: false,
}
}

return {schema, success: true}
}

// Phase 2: Execute with Function constructor
// We're deliberately avoiding VM modules due to security concerns
// The AST validation provides our primary security layer
// If AST validation is disabled, just execute directly (not recommended)
// eslint-disable-next-line no-new-func
const constructSchema = new Function('z', `return ${trimmedSchema}`)
const schema = constructSchema(z)
Expand Down
24 changes: 24 additions & 0 deletions test/codegen/language-mappers/zod-to-typescript-mapper.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -413,5 +413,29 @@ describe('ZodToTypescriptMapper', () => {
)
})
})

describe('ignores describe', () => {
it('Can successfully parse a string with describe (value is ignored)', () => {
const zodAst = secureEvaluateSchema(`z.string().describe("User's email address")`)

const mapper = new ZodToTypescriptMapper({fieldName: 'email'})

const rendered = mapper.renderField(zodAst.schema!)

expect(rendered).to.equal('"email": string')
})

it('Can successfully parse a string with meta + describe (value is ignored)', () => {
const zodAst = secureEvaluateSchema(
`z.string().meta({description: "Meta description"}).describe("Description description")`,
)

const mapper = new ZodToTypescriptMapper({fieldName: 'email'})

const rendered = mapper.renderField(zodAst.schema!)

expect(rendered).to.equal('/** Meta description */ "email": string')
})
})
})
})
34 changes: 34 additions & 0 deletions test/codegen/schema-evaluator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,5 +120,39 @@ describe('SchemaEvaluator', () => {
expect(introspect.isBoolean(options[2])).to.be.true
}
})

it('should strip .describe() calls from schema', () => {
const result = secureEvaluateSchema('z.string().describe("A string field")')

expect(result.success).to.be.true
expect(result.schema).to.exist

// Verify the schema is a plain string schema without description
if (result.schema) {
expect(introspect.isString(result.schema)).to.be.true
}
})

it('should strip .describe() and keep .meta()', () => {
const result = secureEvaluateSchema('z.string().describe("desc").meta({ description: "A string field" })')

expect(result.success).to.be.true
expect(result.schema).to.exist

// Verify meta is still accessible
if (result.schema) {
expect(introspect.isString(result.schema)).to.be.true
const meta = introspect.getMeta(result.schema)
expect(meta).to.exist
expect(meta?.description).to.equal('A string field')
}
})

it('should allow .meta() with description', () => {
const result = secureEvaluateSchema('z.string().meta({ description: "A string field" })')

expect(result.success).to.be.true
expect(result.schema).to.exist
})
})
})