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
13 changes: 9 additions & 4 deletions api/spec/packages/aip-client-javascript/src/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,15 @@ export class Client {
beforeRequest: [
...(options.hooks?.beforeRequest ?? []),
async ({ request }) => {
// Browsers treat User-Agent as a forbidden header and silently drop
// it; that's fine here, this is only observable server-side (e.g.
// request logs) and Node/server callers get it.
if (!request.headers.has('User-Agent')) {
// User-Agent is settable on the web but is not CORS-safelisted, so
// setting it would preflight otherwise-simple cross-origin requests.
// Gate on a positive Node check, not `window`: workers are also
// CORS-bound yet have no `window`.
if (
typeof process !== 'undefined' &&
process.versions?.node != null &&
!request.headers.has('User-Agent')
) {
request.headers.set('User-Agent', `openmeter-node/${SDK_VERSION}`)
}

Expand Down
54 changes: 28 additions & 26 deletions api/spec/packages/aip-client-javascript/src/models/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,10 +163,8 @@ export const cursorMetaPage = z
.object({
first: z.string().optional().describe('URI to the first page.'),
last: z.string().optional().describe('URI to the last page.'),
next: z.union([z.string(), z.null()]).describe('URI to the next page.'),
previous: z
.union([z.string(), z.null()])
.describe('URI to the previous page.'),
next: z.string().nullable().describe('URI to the next page.'),
previous: z.string().nullable().describe('URI to the previous page.'),
size: z.number().int().describe('Requested page size.'),
})
.describe('Cursor pagination metadata.')
Expand Down Expand Up @@ -1829,14 +1827,16 @@ export const event = z
'Contains a value describing the type of event related to the originating occurrence.',
),
datacontenttype: z
.union([z.literal('application/json'), z.null()])
.literal('application/json')
.nullable()
.optional()

.describe(
'Content type of the CloudEvents data value. Only the value "application/json" is allowed over HTTP.',
),
dataschema: z
.union([z.string(), z.null()])
.string()
.nullable()
.optional()
.describe('Identifies the schema that data adheres to.'),
subject: z
Expand All @@ -1846,15 +1846,16 @@ export const event = z
.describe(
'Describes the subject of the event in the context of the event producer (identified by source).',
),
time: z
.union([dateTime, z.null()])
time: dateTime
.nullable()
.optional()

.describe(
'Timestamp of when the occurrence happened. Must adhere to RFC 3339.',
),
data: z
.union([z.record(z.string(), z.unknown()), z.null()])
.record(z.string(), z.unknown())
.nullable()
.optional()

.describe(
Expand Down Expand Up @@ -1978,8 +1979,8 @@ export const createCostBasisRequest = z
export const featureCostQueryRow = z
.object({
usage: numeric,
cost: z
.union([numeric, z.null()])
cost: numeric
.nullable()

.describe(
'The computed cost amount (usage × unit cost). Null when pricing is not available for the given combination of dimensions.',
Expand Down Expand Up @@ -4797,8 +4798,8 @@ export const createFeatureRequest = z

export const updateFeatureRequest = z
.object({
unitCost: z
.union([featureUnitCost, z.null()])
unitCost: featureUnitCost
.nullable()
.optional()

.describe(
Expand Down Expand Up @@ -6844,10 +6845,8 @@ export const cursorMetaPageWire = z
.strictObject({
first: z.string().optional().describe('URI to the first page.'),
last: z.string().optional().describe('URI to the last page.'),
next: z.union([z.string(), z.null()]).describe('URI to the next page.'),
previous: z
.union([z.string(), z.null()])
.describe('URI to the previous page.'),
next: z.string().nullable().describe('URI to the next page.'),
previous: z.string().nullable().describe('URI to the previous page.'),
size: z.number().int().describe('Requested page size.'),
})
.describe('Cursor pagination metadata.')
Expand Down Expand Up @@ -8503,14 +8502,16 @@ export const eventWire = z
'Contains a value describing the type of event related to the originating occurrence.',
),
datacontenttype: z
.union([z.literal('application/json'), z.null()])
.literal('application/json')
.nullable()
.optional()

.describe(
'Content type of the CloudEvents data value. Only the value "application/json" is allowed over HTTP.',
),
dataschema: z
.union([z.string(), z.null()])
.string()
.nullable()
.optional()
.describe('Identifies the schema that data adheres to.'),
subject: z
Expand All @@ -8520,15 +8521,16 @@ export const eventWire = z
.describe(
'Describes the subject of the event in the context of the event producer (identified by source).',
),
time: z
.union([dateTimeWire, z.null()])
time: dateTimeWire
.nullable()
.optional()

.describe(
'Timestamp of when the occurrence happened. Must adhere to RFC 3339.',
),
data: z
.union([z.record(z.string(), z.unknown()), z.null()])
.record(z.string(), z.unknown())
.nullable()
.optional()

.describe(
Expand Down Expand Up @@ -8652,8 +8654,8 @@ export const createCostBasisRequestWire = z
export const featureCostQueryRowWire = z
.strictObject({
usage: numericWire,
cost: z
.union([numericWire, z.null()])
cost: numericWire
.nullable()

.describe(
'The computed cost amount (usage × unit cost). Null when pricing is not available for the given combination of dimensions.',
Expand Down Expand Up @@ -11460,8 +11462,8 @@ export const createFeatureRequestWire = z

export const updateFeatureRequestWire = z
.strictObject({
unit_cost: z
.union([featureUnitCostWire, z.null()])
unit_cost: featureUnitCostWire
.nullable()
.optional()

.describe(
Expand Down
48 changes: 47 additions & 1 deletion api/spec/packages/aip-client-javascript/tests/client.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Code generated by @openmeter/typespec-typescript. DO NOT EDIT.

import fetchMock from '@fetch-mock/vitest'
import { beforeEach, describe, expect, it } from 'vitest'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { OpenMeter, ServerList } from '../src/index.js'
import { SDK_VERSION } from '../src/lib/version.js'

Expand Down Expand Up @@ -130,6 +130,10 @@ describe('option clobbering is prevented', () => {
})

describe('SDK telemetry headers', () => {
afterEach(() => {
vi.unstubAllGlobals()
})

it('sets a default User-Agent header', async () => {
mockMeter()
const sdk = new OpenMeter({
Expand All @@ -152,6 +156,48 @@ describe('SDK telemetry headers', () => {
await sdk.meters.get({ meterId: 'm' })
expect(lastUserAgent()).toBe('custom-agent/1.0')
})

it('does not set a default User-Agent in browsers', async () => {
vi.stubGlobal('window', {})
vi.stubGlobal('process', undefined)
mockMeter()
const sdk = new OpenMeter({
baseUrl: 'https://eu.api.konghq.com/v3',
apiKey: 'k',
fetch,
})
await sdk.meters.get({ meterId: 'm' })
expect(lastUserAgent()).toBeNull()
})

// Workers are CORS-bound but have no `window`, so a `window`-based gate would
// leak the header here and preflight the request.
it('does not set a default User-Agent in workers', async () => {
vi.stubGlobal('self', {})
vi.stubGlobal('process', undefined)
mockMeter()
const sdk = new OpenMeter({
baseUrl: 'https://eu.api.konghq.com/v3',
apiKey: 'k',
fetch,
})
await sdk.meters.get({ meterId: 'm' })
expect(lastUserAgent()).toBeNull()
})

it('honors a caller-provided User-Agent in browsers', async () => {
vi.stubGlobal('window', {})
vi.stubGlobal('process', undefined)
mockMeter()
const sdk = new OpenMeter({
baseUrl: 'https://eu.api.konghq.com/v3',
apiKey: 'k',
headers: { 'User-Agent': 'custom-agent/1.0' },
fetch,
})
await sdk.meters.get({ meterId: 'm' })
expect(lastUserAgent()).toBe('custom-agent/1.0')
})
})

describe('namespace composition', () => {
Expand Down
36 changes: 36 additions & 0 deletions api/spec/packages/typespec-typescript/src/zodBaseSchema.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -384,6 +384,42 @@ function unionBaseType(type: Union) {

const discriminated = $.union.getDiscriminatedUnion(type)

if ($.union.isExpression(type)) {
const variants = Array.from(type.variants.values())
const nonNullVariants = variants.filter(
(variant) =>
variant.type.kind !== 'Intrinsic' || variant.type.name !== 'null',
)

if (
nonNullVariants.length > 0 &&
nonNullVariants.length < variants.length
) {
const inner =
nonNullVariants.length === 1 ? (
<ZodSchema type={nonNullVariants[0]!.type} nested />
) : (
zodMemberExpr(
callPart(
'union',
<ArrayExpression>
<For each={nonNullVariants} comma line>
{(variant) => <ZodSchema type={variant.type} nested />}
</For>
</ArrayExpression>,
),
)
)

return (
<MemberExpression>
<MemberExpression.Part>{inner}</MemberExpression.Part>
{callPart('nullable')}
</MemberExpression>
)
}
}

if ($.union.isExpression(type) || !discriminated) {
return zodMemberExpr(
callPart(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,15 @@ export class Client {
beforeRequest: [
...(options.hooks?.beforeRequest ?? []),
async ({ request }) => {
// Browsers treat User-Agent as a forbidden header and silently drop
// it; that's fine here, this is only observable server-side (e.g.
// request logs) and Node/server callers get it.
if (!request.headers.has('User-Agent')) {
// User-Agent is settable on the web but is not CORS-safelisted, so
// setting it would preflight otherwise-simple cross-origin requests.
// Gate on a positive Node check, not `window`: workers are also
// CORS-bound yet have no `window`.
if (
typeof process !== 'undefined' &&
process.versions?.node != null &&
!request.headers.has('User-Agent')
) {
request.headers.set('User-Agent', `openmeter-node/${SDK_VERSION}`)
Comment on lines +34 to 39

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Polyfilled Process Triggers Browser Header

A browser bundle that provides a Node-compatible process.versions.node value passes this check even though requests are still subject to CORS. The SDK then adds User-Agent, causing an otherwise simple cross-origin request to preflight and potentially fail; the browser tests only cover an undefined process, not this common polyfilled state.

Context Used: api/spec/AGENTS.md (source)

Prompt To Fix With AI
This is a comment left during a code review.
Path: api/spec/packages/typespec-typescript/templates/runtime/core.ts
Line: 34-39

Comment:
**Polyfilled Process Triggers Browser Header**

A browser bundle that provides a Node-compatible `process.versions.node` value passes this check even though requests are still subject to CORS. The SDK then adds `User-Agent`, causing an otherwise simple cross-origin request to preflight and potentially fail; the browser tests only cover an undefined `process`, not this common polyfilled state.

**Context Used:** api/spec/AGENTS.md ([source](https://app.greptile.com/openmeter/github/openmeterio/openmeter/-/custom-context?memory=28ba6068-00f9-4629-9b78-8e49cc802858))

How can I resolve this? If you propose a fix, please make it concise.

Fix in Claude Code Fix in Codex

}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import fetchMock from '@fetch-mock/vitest'
import { beforeEach, describe, expect, it } from 'vitest'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { OpenMeter, ServerList } from '../src/index.js'
import { SDK_VERSION } from '../src/lib/version.js'

Expand Down Expand Up @@ -128,6 +128,10 @@ describe('option clobbering is prevented', () => {
})

describe('SDK telemetry headers', () => {
afterEach(() => {
vi.unstubAllGlobals()
})

it('sets a default User-Agent header', async () => {
mockMeter()
const sdk = new OpenMeter({
Expand All @@ -150,6 +154,48 @@ describe('SDK telemetry headers', () => {
await sdk.meters.get({ meterId: 'm' })
expect(lastUserAgent()).toBe('custom-agent/1.0')
})

it('does not set a default User-Agent in browsers', async () => {
vi.stubGlobal('window', {})
vi.stubGlobal('process', undefined)
mockMeter()
const sdk = new OpenMeter({
baseUrl: 'https://eu.api.konghq.com/v3',
apiKey: 'k',
fetch,
})
await sdk.meters.get({ meterId: 'm' })
expect(lastUserAgent()).toBeNull()
})

// Workers are CORS-bound but have no `window`, so a `window`-based gate would
// leak the header here and preflight the request.
it('does not set a default User-Agent in workers', async () => {
vi.stubGlobal('self', {})
vi.stubGlobal('process', undefined)
mockMeter()
const sdk = new OpenMeter({
baseUrl: 'https://eu.api.konghq.com/v3',
apiKey: 'k',
fetch,
})
await sdk.meters.get({ meterId: 'm' })
expect(lastUserAgent()).toBeNull()
})

it('honors a caller-provided User-Agent in browsers', async () => {
vi.stubGlobal('window', {})
vi.stubGlobal('process', undefined)
mockMeter()
const sdk = new OpenMeter({
baseUrl: 'https://eu.api.konghq.com/v3',
apiKey: 'k',
headers: { 'User-Agent': 'custom-agent/1.0' },
fetch,
})
await sdk.meters.get({ meterId: 'm' })
expect(lastUserAgent()).toBe('custom-agent/1.0')
})
})

describe('namespace composition', () => {
Expand Down
Loading
Loading