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
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@zenstackhq/proxy",
"version": "0.2.1",
"version": "0.2.3",
"description": "A CLI tool to run an Express server that proxies CRUD requests to a ZenStack backend",
"main": "index.js",
"publishConfig": {
Expand Down Expand Up @@ -33,7 +33,8 @@
"express": "^4.19.2",
"mixpanel": "^0.19.1",
"semver": "^7.7.3",
"tsx": "^4.20.6"
"tsx": "^4.20.6",
"uuid": "^13.0.0"
},
"devDependencies": {
"@types/cors": "^2.8.17",
Expand Down
9 changes: 9 additions & 0 deletions pnpm-lock.yaml

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

3 changes: 1 addition & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import 'dotenv/config'
import { getVersion } from './utils/version-utils'
import { telemetry } from './telemetry'
import { CliError } from './cli-error'

export function createProgram() {
const program = new Command()

Expand All @@ -32,7 +31,7 @@ export function createProgram() {
: path.join(process.cwd(), options.schema)

if (!fs.existsSync(zmodelPath)) {
console.error(`Error: ZModel schema file not found: ${zmodelPath}`)
console.error(`ZModel schema file not found: ${zmodelPath}`)
console.error('Please provide a valid path using the -s option.')
process.exit(1)
}
Expand Down
9 changes: 8 additions & 1 deletion src/telemetry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import isDocker from './utils/is-docker'
import { isWsl } from './utils/is-wsl'
import { getMachineId } from './utils/machine-id-utils'
import { getPrismaVersion, getVersion } from './utils/version-utils'
import { v5 as uuidv5 } from 'uuid'

/**
* Telemetry events
Expand All @@ -18,7 +19,7 @@ export type TelemetryEvents = 'proxy:start' | 'proxy:complete' | 'proxy:error'
*/
export class Telemetry {
private readonly mixpanel: Mixpanel | undefined
private readonly hostId = getMachineId()
private readonly hostId = this.getDeviceId()
private readonly sessionid = randomUUID()
private readonly _os_type = os.type()
private readonly _os_release = os.release()
Expand All @@ -40,6 +41,12 @@ export class Telemetry {
}
}

private getDeviceId() {
const hostId = getMachineId()
// namespace UUID for generating UUIDv5 from DNS 'zenstack.dev'
Copy link

Copilot AI Jan 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment states this is a "namespace UUID for generating UUIDv5 from DNS 'zenstack.dev'", but the hardcoded UUID should be verified. The standard DNS namespace UUID is '6ba7b810-9dad-11d1-80b4-00c04fd430c8'. If '133cac15-3efb-50fa-b5fc-4b90e441e563' is a custom namespace derived from 'zenstack.dev', the comment should clarify this to avoid confusion.

Suggested change
// namespace UUID for generating UUIDv5 from DNS 'zenstack.dev'
// Custom UUIDv5 namespace used for ZenStack telemetry (derived from DNS name 'zenstack.dev')

Copilot uses AI. Check for mistakes.
return uuidv5(hostId, '133cac15-3efb-50fa-b5fc-4b90e441e563')
}
Comment on lines +44 to +48
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

RFC 4122 DNS namespace UUID v5 standard

💡 Result:

Short answer:

  • RFC 4122 (UUIDs) defines name-based UUIDv5 as SHA-1(name + namespace) with bits set for version=5 and the RFC 4122 variant. [1]
  • The standard DNS namespace UUID (used as the namespace input when generating UUIDv5 for domain names) is: 6ba7b810-9dad-11d1-80b4-00c04fd430c8. [2]
  • Note: RFC 4122 was obsoleted by RFC 9562 (May 2024); RFC 9562 preserves these namespace IDs and describes current UUID rules and namespace allocation. [2][1]

Sources:
[1] RFC 4122 (A Universally Unique IDentifier (UUID) URN Namespace).
[2] RFC 9562 (Universally Unique IDentifiers (UUIDs)) — Table 3 (Namespace IDs) and namespace allocation.


🏁 Script executed:

# Search for any documentation or comments about the custom namespace UUID
rg "133cac15-3efb-50fa-b5fc-4b90e441e563" --type ts --type js

# Also check for any comments or documentation about zenstack namespace
rg -i "zenstack.*namespace\|namespace.*zenstack" --type ts --type js -A 2 -B 2

Repository: zenstackhq/proxy

Length of output: 142


Clarify the namespace UUID usage.

The namespace UUID '133cac15-3efb-50fa-b5fc-4b90e441e563' doesn't match the standard DNS namespace UUID defined in RFC 4122 (6ba7b810-9dad-11d1-80b4-00c04fd430c8). The comment stating "namespace UUID for generating UUIDv5 from DNS 'zenstack.dev'" is misleading—if this is intentionally a custom namespace for privacy/anonymization, update the comment to reflect that. If it should use the RFC 4122 standard DNS namespace, correct the UUID.

🤖 Prompt for AI Agents
In @src/telemetry.ts around lines 44 - 48, The getDeviceId() function currently
uses uuidv5(hostId, '133cac15-3efb-50fa-b5fc-4b90e441e563') but the hard-coded
UUID is not the RFC 4122 DNS namespace; either replace that second argument with
the standard DNS namespace UUID '6ba7b810-9dad-11d1-80b4-00c04fd430c8' if you
intended to derive a DNS-based UUID, or keep the custom UUID but update the
comment to explicitly state it is a deliberately chosen custom namespace for
privacy/anonymization (mentioning getDeviceId(), hostId, and uuidv5 to locate
the change).


get isTracking() {
return !!this.mixpanel
}
Expand Down
12 changes: 5 additions & 7 deletions src/utils/machine-id-utils.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
// modified from https://github.com/automation-stack/node-machine-id

import { execSync } from 'child_process'
import { createHash, randomUUID } from 'node:crypto'
import { createHash } from 'node:crypto'
import { v4 as uuid } from 'uuid'

const { platform } = process
const win32RegBinPath = {
Expand Down Expand Up @@ -31,7 +32,7 @@ function hash(guid: string): string {
return createHash('sha256').update(guid).digest('hex')
}

function expose(result: string): string | undefined {
function expose(result: string): string {
Copy link

Copilot AI Jan 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The return type was changed to string, but the function can still return undefined due to optional chaining operators in the switch cases. For example, on line 41-42, the expression could return undefined if the split or replace operations don't find matches. This will cause a type mismatch when calling hash(id) on line 71, as hash() expects a string parameter.

Copilot uses AI. Check for mistakes.
switch (platform) {
case 'darwin':
return result
Expand Down Expand Up @@ -62,16 +63,13 @@ function expose(result: string): string | undefined {

export function getMachineId() {
if (!(platform in guid)) {
return randomUUID()
return uuid()
}
try {
const value = execSync(guid[platform as keyof typeof guid])
const id = expose(value.toString())
if (!id) {
return randomUUID()
}
return hash(id)
} catch {
return randomUUID()
return uuid()
}
}
61 changes: 33 additions & 28 deletions src/zmodel-parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,15 +71,9 @@ function loadPrismaConfig(schemaDir: string): string | null {

// Use Function constructor to safely evaluate the object literal
// This is safer than eval as it doesn't have access to the local scope
try {
const configFn = new Function('env', `return ${configObjectStr}`)
const config = configFn(env)

return config?.datasource?.url || null
} catch (evalError) {
console.warn(`Warning: Could not evaluate config object: ${evalError}`)
return null
}
const configFn = new Function('env', `return ${configObjectStr}`)
const config = configFn(env)
return config?.datasource?.url
} catch (error) {
if (error instanceof Error && error.message.includes('Environment variable')) {
throw error
Expand Down Expand Up @@ -117,34 +111,43 @@ function parseDatasource(
return { provider, url: datasourceUrlOverride }
}

// Extract url from schema
let urlMatch = datasourceBlock.match(/url\s*=\s*['"]([^'"]+)['"]/)
// Extract url value using single regex (could be string literal, env() call, or expression)
const urlMatch = datasourceBlock.match(/url\s*=\s*([^\n]+)/)
let url: string | null = null

if (urlMatch) {
url = urlMatch[1]
} else {
// Try to match env("xxx")
const envMatch = datasourceBlock.match(/url\s*=\s*env\(\s*['"]([^'"]+)['"]\s*\)/)
if (envMatch) {
const envVar = envMatch[1]
const envValue = process.env[envVar]
if (envValue) {
url = envValue
const urlValueStr = urlMatch[1].trim()

// Create env helper function
const env = (varName: string) => {
const value = process.env[varName]
if (!value) {
throw new CliError(`Environment variable ${varName} is not set`)
}
return value
}
}

// If no URL found in schema, try prisma.config.ts (Prisma 7)
if (!url) {
try {
// Use Function constructor to evaluate the url value
const urlFn = new Function('env', `return ${urlValueStr}`)
url = urlFn(env)
} catch (evalError) {
throw new CliError(
'Could not evaluate datasource url from schema, you could provide it via -d option.'
)
}
} else {
url = loadPrismaConfig(schemaDir)
// If still no URL found, throw error
if (url == null) {
throw new CliError(
'No datasource URL found. For Prisma 7, ensure prisma.config.ts exists with datasource configuration, or provide the URL via -d option.'
)
}
}

// If still no URL found, throw error
if (!url) {
throw new CliError(
'No datasource URL found. For Prisma 7, ensure prisma.config.ts exists with datasource configuration, or provide the URL via --datasource-url option.'
)
throw new CliError('datasource url has no value, you could provide it via -d option.')
}

return { provider, url }
Expand All @@ -157,7 +160,9 @@ function parseGenerator(content: string): GeneratorConfig {
// Match generator block for prisma client
const generatorMatch = content.match(/generator\s+\w+\s*\{([^}]+)\}/s)
if (!generatorMatch) {
throw new CliError('No generator block found in zmodel schema')
throw new CliError(
'No generator block found in zmodel schema.\nZenStack V3 is not supported, V3 will have built-in proxy support soon.'
Copy link

Copilot AI Jan 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error message states "V3 will have built-in proxy support soon" which uses future tense but may be confusing to users. Consider clarifying whether this message is specifically for users currently on V2 trying to use V3, or revising to make the message clearer about the current state and expected timeline.

Suggested change
'No generator block found in zmodel schema.\nZenStack V3 is not supported, V3 will have built-in proxy support soon.'
'No generator block found in zmodel schema.\nZenStack V3 schemas are not supported by this CLI yet. Built-in proxy support for V3 is planned but not currently available.'

Copilot uses AI. Check for mistakes.
)
}

const generatorBlock = generatorMatch[1]
Expand Down