@xemahq/api-client-generator 0.3.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,2 +1,2 @@
1
- export declare const CANONICAL_CUSTOM_FETCH = "/**\n * Configurable fetch wrapper for Orval-generated clients.\n *\n * Consumers must call `configureClient()` before using any endpoint function.\n * The baseUrl is prepended to the relative paths generated by Orval.\n */\n\nexport interface ClientConfig {\n /** Static base URL (e.g. 'http://localhost:3140') \u2014 no trailing slash. */\n baseUrl?: string;\n /** Per-request base-URL resolver (service-registry-safe). */\n baseUrlResolver?: () => string | Promise<string>;\n /** Optional async callback to get an auth token. */\n getAuthToken?: () => Promise<string>;\n /** Optional callback returning headers to inject on every request. */\n getHeaders?: () => Record<string, string> | Promise<Record<string, string>>;\n /** Optional callback invoked on 401 before a single retry. */\n onUnauthorized?: () => Promise<void>;\n /** Maximum retry attempts for transient failures (default: 3). */\n maxRetries?: number;\n}\n\nexport class ClientError extends Error {\n constructor(\n public readonly status: number,\n public readonly url: string,\n public readonly body: unknown,\n ) {\n super(`HTTP ${status} from ${url}`);\n this.name = 'ClientError';\n }\n}\n\nlet clientConfig: ClientConfig | null = null;\n\nexport function configureClient(config: ClientConfig): void {\n clientConfig = config;\n}\n\nexport function getClientConfig(): ClientConfig {\n if (!clientConfig) {\n throw new Error(\n 'Client not configured. Call configureClient({ baseUrl }) before using endpoint functions.',\n );\n }\n return clientConfig;\n}\n\nconst RETRYABLE_STATUSES = [429, 502, 503, 504];\n\nasync function buildHeaders(\n config: ClientConfig,\n callerHeaders: RequestInit['headers'],\n): Promise<Headers> {\n const headers = new Headers(callerHeaders);\n if (config.getHeaders) {\n const globalHeaders = await Promise.resolve(config.getHeaders());\n for (const [key, value] of Object.entries(globalHeaders)) {\n if (!headers.has(key)) {\n headers.set(key, value);\n }\n }\n }\n if (config.getAuthToken && !headers.has('Authorization')) {\n const token = await config.getAuthToken();\n headers.set('Authorization', `Bearer ${token}`);\n }\n return headers;\n}\n\nexport const customFetch = async <T>(\n url: string,\n options: RequestInit,\n): Promise<T> => {\n const config = getClientConfig();\n const base = config.baseUrlResolver\n ? await config.baseUrlResolver()\n : config.baseUrl;\n if (base === undefined) {\n throw new Error(\n 'Client not configured: set baseUrl or baseUrlResolver via configureClient().',\n );\n }\n const fullUrl = `${base}${url}`;\n const maxRetries = config.maxRetries ?? 3;\n\n const headers = await buildHeaders(config, options.headers);\n const requestInit: RequestInit = { ...options, headers };\n\n let delay = 1000;\n let lastError: Error | undefined;\n\n for (let attempt = 0; attempt <= maxRetries; attempt++) {\n try {\n const response = await fetch(fullUrl, requestInit);\n\n if (response.status === 401 && config.onUnauthorized && attempt === 0) {\n await config.onUnauthorized();\n const refreshedHeaders = await buildHeaders(config, options.headers);\n const retryResponse = await fetch(fullUrl, { ...options, headers: refreshedHeaders });\n const retryBody = await parseBody(retryResponse);\n if (retryResponse.status >= 400) {\n throw new ClientError(retryResponse.status, fullUrl, retryBody);\n }\n return unwrapEnvelope(retryBody) as T;\n }\n\n if (!RETRYABLE_STATUSES.includes(response.status) || attempt >= maxRetries) {\n const body = await parseBody(response);\n if (response.status >= 400) {\n throw new ClientError(response.status, fullUrl, body);\n }\n return unwrapEnvelope(body) as T;\n }\n\n const retryAfter = parseRetryAfter(response.headers.get('Retry-After'));\n const waitMs = retryAfter ?? addJitter(delay);\n await sleep(waitMs);\n delay = Math.min(delay * 2, 30_000);\n } catch (error) {\n if (error instanceof ClientError) throw error;\n lastError = error instanceof Error ? error : new Error(String(error));\n if (attempt >= maxRetries) throw lastError;\n const waitMs = addJitter(delay);\n await sleep(waitMs);\n delay = Math.min(delay * 2, 30_000);\n }\n }\n\n throw lastError ?? new Error(`All retries exhausted for ${fullUrl}`);\n};\n\n/**\n * Xema platform services wrap every 2xx payload in a { data: T } envelope via\n * the global ResponseEnvelopeInterceptor (platform-common). Generated types\n * describe the INNER T (controllers declare their return type as T, not\n * { data: T }), so the client must peel data to keep the static types and the\n * wire shape consistent \u2014 otherwise consumers read fields off the envelope and\n * get undefined.\n *\n * Only 2xx bodies reach this helper: non-2xx responses are surfaced as a thrown\n * ClientError before unwrap, and error envelopes are emitted by the\n * GlobalExceptionFilter, never wrapped in { data }. Paginated lists arrive as\n * { data: T[], pagination: {...} }; consumers read both fields, so that envelope\n * is kept intact (detected by the presence of pagination). Any body without a\n * top-level data field is returned as-is.\n */\nfunction unwrapEnvelope(body: unknown): unknown {\n if (\n body === null ||\n typeof body !== 'object' ||\n Array.isArray(body) ||\n !('data' in body)\n ) {\n return body;\n }\n const envelope = body as { data: unknown; pagination?: unknown };\n if ('pagination' in envelope) {\n return envelope;\n }\n return envelope.data;\n}\n\nasync function parseBody(response: Response): Promise<unknown> {\n const contentType = response.headers.get('content-type');\n if (contentType?.includes('application/json')) {\n return response.json();\n }\n if (response.status === 204) {\n return undefined;\n }\n return response.text();\n}\n\nfunction parseRetryAfter(value: string | null): number | undefined {\n if (!value) return undefined;\n const seconds = Number(value);\n if (!isNaN(seconds) && seconds >= 0) return seconds * 1000;\n const date = new Date(value);\n if (!isNaN(date.getTime())) return Math.max(0, date.getTime() - Date.now());\n return undefined;\n}\n\nfunction addJitter(delay: number): number {\n return delay + (Math.random() * 2 - 1) * delay * 0.25;\n}\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise(resolve => setTimeout(resolve, ms));\n}\n\nexport default customFetch;\n";
1
+ export declare const CANONICAL_CUSTOM_FETCH = "/**\n * Configurable fetch wrapper for Orval-generated clients.\n *\n * Consumers must call `configureClient()` before using any endpoint function.\n * The baseUrl is prepended to the relative paths generated by Orval.\n */\n\nexport interface ClientConfig {\n /** Static base URL (e.g. 'http://localhost:3140') \u2014 no trailing slash. */\n baseUrl?: string;\n /** Per-request base-URL resolver (service-registry-safe). */\n baseUrlResolver?: () => string | Promise<string>;\n /** Optional async callback to get an auth token. */\n getAuthToken?: () => Promise<string>;\n /** Optional callback returning headers to inject on every request. */\n getHeaders?: () => Record<string, string> | Promise<Record<string, string>>;\n /** Optional callback invoked on 401 before a single retry. */\n onUnauthorized?: () => Promise<void>;\n /** Maximum retry attempts for transient failures (default: 3). */\n maxRetries?: number;\n}\n\nexport class ClientError extends Error {\n constructor(\n public readonly status: number,\n public readonly url: string,\n public readonly body: unknown,\n ) {\n super(`HTTP ${status} from ${url}`);\n this.name = 'ClientError';\n }\n}\n\nlet clientConfig: ClientConfig | null = null;\n\nexport function configureClient(config: ClientConfig): void {\n clientConfig = config;\n}\n\nexport function getClientConfig(): ClientConfig {\n if (!clientConfig) {\n throw new Error(\n 'Client not configured. Call configureClient({ baseUrl }) before using endpoint functions.',\n );\n }\n return clientConfig;\n}\n\nconst RETRYABLE_STATUSES = [429, 502, 503, 504];\n\nasync function buildHeaders(\n config: ClientConfig,\n callerHeaders: RequestInit['headers'],\n): Promise<Headers> {\n const headers = new Headers(callerHeaders);\n if (config.getHeaders) {\n const globalHeaders = await Promise.resolve(config.getHeaders());\n for (const [key, value] of Object.entries(globalHeaders)) {\n if (!headers.has(key)) {\n headers.set(key, value);\n }\n }\n }\n if (config.getAuthToken && !headers.has('Authorization')) {\n const token = await config.getAuthToken();\n headers.set('Authorization', `Bearer ${token}`);\n }\n return headers;\n}\n\nexport const customFetch = async <T>(\n url: string,\n options: RequestInit,\n): Promise<T> => {\n const config = getClientConfig();\n const base = config.baseUrlResolver\n ? await config.baseUrlResolver()\n : config.baseUrl;\n if (base === undefined) {\n throw new Error(\n 'Client not configured: set baseUrl or baseUrlResolver via configureClient().',\n );\n }\n const fullUrl = `${base}${url}`;\n const maxRetries = config.maxRetries ?? 3;\n\n const headers = await buildHeaders(config, options.headers);\n const requestInit: RequestInit = { ...options, headers };\n\n let delay = 1000;\n let lastError: Error | undefined;\n\n for (let attempt = 0; attempt <= maxRetries; attempt++) {\n try {\n const response = await fetch(fullUrl, requestInit);\n\n if (response.status === 401 && config.onUnauthorized && attempt === 0) {\n await config.onUnauthorized();\n const refreshedHeaders = await buildHeaders(config, options.headers);\n const retryResponse = await fetch(fullUrl, { ...options, headers: refreshedHeaders });\n const retryBody = await parseBody(retryResponse);\n if (retryResponse.status >= 400) {\n throw new ClientError(retryResponse.status, fullUrl, retryBody);\n }\n return retryBody as T;\n }\n\n if (!RETRYABLE_STATUSES.includes(response.status) || attempt >= maxRetries) {\n const body = await parseBody(response);\n if (response.status >= 400) {\n throw new ClientError(response.status, fullUrl, body);\n }\n return body as T;\n }\n\n const retryAfter = parseRetryAfter(response.headers.get('Retry-After'));\n const waitMs = retryAfter ?? addJitter(delay);\n await sleep(waitMs);\n delay = Math.min(delay * 2, 30_000);\n } catch (error) {\n if (error instanceof ClientError) throw error;\n lastError = error instanceof Error ? error : new Error(String(error));\n if (attempt >= maxRetries) throw lastError;\n const waitMs = addJitter(delay);\n await sleep(waitMs);\n delay = Math.min(delay * 2, 30_000);\n }\n }\n\n throw lastError ?? new Error(`All retries exhausted for ${fullUrl}`);\n};\n\n/*\n * THE TRANSPORT RETURNS THE BODY UNCHANGED. Do not reintroduce an unwrap here.\n *\n * Xema services wrap every 2xx payload in a { data: T } envelope via the global\n * ResponseEnvelopeInterceptor (platform-common), and the generator now emits\n * types that describe THAT ENVELOPE \u2014 every endpoint returns\n * Promise<XDataEnvelope> or Promise<XPaginatedEnvelope>, never a bare inner\n * T. Consumers read the data property themselves.\n *\n * This template used to peel data, justified by a comment claiming generated\n * types describe the inner T. That stopped being true when the generator moved\n * to envelope-typed returns, and the comment outlived the fact \u2014 so the peel\n * then contradicted every type in every package it seeded. Four clients shipped\n * that way (resource-governance x3, workload-runtime-api): declared\n * *DataEnvelope, returned the already-peeled inner object, so .data read\n * undefined at runtime on every non-paginated endpoint. Paginated calls hid\n * it, because the old peel deliberately preserved an envelope carrying\n * pagination.\n *\n * ensureCustomFetch seeds this template into any client that lacks a\n * transport, so a peel here is not one bug \u2014 it is re-imported into every new\n * client generated from now on, which is exactly how those four were born after\n * the hand-fix of their siblings.\n *\n * Enforced fleet-wide by check-client-transport-envelope in xema-base, which\n * reads the RETURN SITES rather than banning a name \u2014 a rename, an inline\n * .data, or ?? body.data all walk past a string ban.\n */\n\nasync function parseBody(response: Response): Promise<unknown> {\n const contentType = response.headers.get('content-type');\n if (contentType?.includes('application/json')) {\n return response.json();\n }\n if (response.status === 204) {\n return undefined;\n }\n return response.text();\n}\n\nfunction parseRetryAfter(value: string | null): number | undefined {\n if (!value) return undefined;\n const seconds = Number(value);\n if (!isNaN(seconds) && seconds >= 0) return seconds * 1000;\n const date = new Date(value);\n if (!isNaN(date.getTime())) return Math.max(0, date.getTime() - Date.now());\n return undefined;\n}\n\nfunction addJitter(delay: number): number {\n return delay + (Math.random() * 2 - 1) * delay * 0.25;\n}\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise(resolve => setTimeout(resolve, ms));\n}\n\nexport default customFetch;\n";
2
2
  //# sourceMappingURL=custom-fetch-template.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"custom-fetch-template.d.ts","sourceRoot":"","sources":["../../src/lib/custom-fetch-template.ts"],"names":[],"mappings":"AAOA,eAAO,MAAM,sBAAsB,69MAgMlC,CAAC"}
1
+ {"version":3,"file":"custom-fetch-template.d.ts","sourceRoot":"","sources":["../../src/lib/custom-fetch-template.ts"],"names":[],"mappings":"AAOA,eAAO,MAAM,sBAAsB,6yNA8LlC,CAAC"}
@@ -105,7 +105,7 @@ export const customFetch = async <T>(
105
105
  if (retryResponse.status >= 400) {
106
106
  throw new ClientError(retryResponse.status, fullUrl, retryBody);
107
107
  }
108
- return unwrapEnvelope(retryBody) as T;
108
+ return retryBody as T;
109
109
  }
110
110
 
111
111
  if (!RETRYABLE_STATUSES.includes(response.status) || attempt >= maxRetries) {
@@ -113,7 +113,7 @@ export const customFetch = async <T>(
113
113
  if (response.status >= 400) {
114
114
  throw new ClientError(response.status, fullUrl, body);
115
115
  }
116
- return unwrapEnvelope(body) as T;
116
+ return body as T;
117
117
  }
118
118
 
119
119
  const retryAfter = parseRetryAfter(response.headers.get('Retry-After'));
@@ -133,36 +133,34 @@ export const customFetch = async <T>(
133
133
  throw lastError ?? new Error(\`All retries exhausted for \${fullUrl}\`);
134
134
  };
135
135
 
136
- /**
137
- * Xema platform services wrap every 2xx payload in a { data: T } envelope via
138
- * the global ResponseEnvelopeInterceptor (platform-common). Generated types
139
- * describe the INNER T (controllers declare their return type as T, not
140
- * { data: T }), so the client must peel data to keep the static types and the
141
- * wire shape consistent — otherwise consumers read fields off the envelope and
142
- * get undefined.
136
+ /*
137
+ * THE TRANSPORT RETURNS THE BODY UNCHANGED. Do not reintroduce an unwrap here.
143
138
  *
144
- * Only 2xx bodies reach this helper: non-2xx responses are surfaced as a thrown
145
- * ClientError before unwrap, and error envelopes are emitted by the
146
- * GlobalExceptionFilter, never wrapped in { data }. Paginated lists arrive as
147
- * { data: T[], pagination: {...} }; consumers read both fields, so that envelope
148
- * is kept intact (detected by the presence of pagination). Any body without a
149
- * top-level data field is returned as-is.
139
+ * Xema services wrap every 2xx payload in a { data: T } envelope via the global
140
+ * ResponseEnvelopeInterceptor (platform-common), and the generator now emits
141
+ * types that describe THAT ENVELOPE — every endpoint returns
142
+ * Promise<XDataEnvelope> or Promise<XPaginatedEnvelope>, never a bare inner
143
+ * T. Consumers read the data property themselves.
144
+ *
145
+ * This template used to peel data, justified by a comment claiming generated
146
+ * types describe the inner T. That stopped being true when the generator moved
147
+ * to envelope-typed returns, and the comment outlived the fact — so the peel
148
+ * then contradicted every type in every package it seeded. Four clients shipped
149
+ * that way (resource-governance x3, workload-runtime-api): declared
150
+ * *DataEnvelope, returned the already-peeled inner object, so .data read
151
+ * undefined at runtime on every non-paginated endpoint. Paginated calls hid
152
+ * it, because the old peel deliberately preserved an envelope carrying
153
+ * pagination.
154
+ *
155
+ * ensureCustomFetch seeds this template into any client that lacks a
156
+ * transport, so a peel here is not one bug — it is re-imported into every new
157
+ * client generated from now on, which is exactly how those four were born after
158
+ * the hand-fix of their siblings.
159
+ *
160
+ * Enforced fleet-wide by check-client-transport-envelope in xema-base, which
161
+ * reads the RETURN SITES rather than banning a name — a rename, an inline
162
+ * .data, or ?? body.data all walk past a string ban.
150
163
  */
151
- function unwrapEnvelope(body: unknown): unknown {
152
- if (
153
- body === null ||
154
- typeof body !== 'object' ||
155
- Array.isArray(body) ||
156
- !('data' in body)
157
- ) {
158
- return body;
159
- }
160
- const envelope = body as { data: unknown; pagination?: unknown };
161
- if ('pagination' in envelope) {
162
- return envelope;
163
- }
164
- return envelope.data;
165
- }
166
164
 
167
165
  async function parseBody(response: Response): Promise<unknown> {
168
166
  const contentType = response.headers.get('content-type');
@@ -1 +1 @@
1
- {"version":3,"file":"custom-fetch-template.js","sourceRoot":"","sources":["../../src/lib/custom-fetch-template.ts"],"names":[],"mappings":";;;AAOa,QAAA,sBAAsB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAgMrC,CAAC"}
1
+ {"version":3,"file":"custom-fetch-template.js","sourceRoot":"","sources":["../../src/lib/custom-fetch-template.ts"],"names":[],"mappings":";;;AAOa,QAAA,sBAAsB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA8LrC,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"seed-openapi-env.d.ts","sourceRoot":"","sources":["../../src/lib/seed-openapi-env.ts"],"names":[],"mappings":"AA0CA,wBAAgB,oBAAoB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAI9D;AAMD,wBAAgB,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAoFtD"}
1
+ {"version":3,"file":"seed-openapi-env.d.ts","sourceRoot":"","sources":["../../src/lib/seed-openapi-env.ts"],"names":[],"mappings":"AA0CA,wBAAgB,oBAAoB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAI9D;AAMD,wBAAgB,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAmFtD"}
@@ -36,7 +36,6 @@ function seedOpenApiEnv(serviceId) {
36
36
  JWT_PUBLIC_KEY_URL: 'http://localhost:8180/realms/dummy/protocol/openid-connect/certs',
37
37
  JWT_ISSUER: 'http://localhost:8180/realms/dummy',
38
38
  GATEWAY_JWT_SECRET: 'dummy-gateway-jwt-secret-for-openapi-extraction',
39
- SHARE_TOKEN_SECRET: 'dummy-share-token-secret',
40
39
  OPENCODE_WORKER_PASSWORD: 'dummy-worker-password',
41
40
  XEMA_ETCD_ENDPOINTS: 'localhost:2379',
42
41
  EVENT_HUB_URL: 'http://localhost:3128',
@@ -1 +1 @@
1
- {"version":3,"file":"seed-openapi-env.js","sourceRoot":"","sources":["../../src/lib/seed-openapi-env.ts"],"names":[],"mappings":";;AA0CA,oDAIC;AAMD,wCAoFC;AAxHD,MAAM,wBAAwB,GAC5B,kEAAkE,CAAC;AACrE,MAAM,gBAAgB,GAAG,8CAA8C,CAAC;AAGxE,SAAS,IAAI,CAAC,IAA4B;IACxC,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;QAChD,IAAI,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,KAAK,SAAS,EAAE,CAAC;YACnC,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC;QAC3B,CAAC;IACH,CAAC;AACH,CAAC;AAGD,MAAM,eAAe,GAAG,eAAe,CAAC;AAYxC,SAAgB,oBAAoB,CAAC,SAAiB;IACpD,OAAO,WAAW,SAAS,CAAC,OAAO,CAAC,gBAAgB,EAAE,GAAG,CAAC,EAAE;SACzD,WAAW,EAAE;SACb,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;AAClB,CAAC;AAMD,SAAgB,cAAc,CAAC,SAAiB;IAC9C,MAAM,MAAM,GAAG,oBAAoB,CAAC,SAAS,CAAC,CAAC;IAC/C,IAAI,CAAC;QACH,QAAQ,EAAE,aAAa;QASvB,uBAAuB,EAAE,GAAG;QAO5B,YAAY,EAAE,2CAA2C,eAAe,WAAW,MAAM,EAAE;QAC3F,OAAO,EAAE,WAAW;QACpB,OAAO,EAAE,MAAM;QACf,OAAO,EAAE,eAAe;QACxB,SAAS,EAAE,MAAM;QACjB,OAAO,EAAE,OAAO;QAChB,WAAW,EAAE,OAAO;QACpB,SAAS,EAAE,wBAAwB;QACnC,cAAc,EAAE,uBAAuB;QAGvC,gBAAgB,EAAE,uBAAuB;QACzC,2BAA2B,EAAE,aAAa;QAC1C,kBAAkB,EAChB,kEAAkE;QACpE,UAAU,EAAE,oCAAoC;QAChD,kBAAkB,EAAE,iDAAiD;QACrE,kBAAkB,EAAE,0BAA0B;QAC9C,wBAAwB,EAAE,uBAAuB;QAKjD,mBAAmB,EAAE,gBAAgB;QAMrC,aAAa,EAAE,uBAAuB;QACtC,WAAW,EAAE,uCAAuC;QAGpD,eAAe,EAAE,uBAAuB;QACxC,cAAc,EAAE,uBAAuB;QACvC,oBAAoB,EAAE,uBAAuB;QAC7C,mBAAmB,EAAE,uBAAuB;QAC5C,sBAAsB,EAAE,uBAAuB;QAC/C,sBAAsB,EAAE,uBAAuB;QAC/C,wBAAwB,EAAE,uBAAuB;QACjD,yBAAyB,EAAE,uBAAuB;QAClD,6BAA6B,EAAE,uBAAuB;QACtD,cAAc,EAAE,uBAAuB;QACvC,eAAe,EAAE,uBAAuB;QACxC,2BAA2B,EAAE,uBAAuB;QAGpD,kBAAkB,EAAE,sBAAsB;QAC1C,oBAAoB,EAAE,gCAAgC;QACtD,SAAS,EAAE,uBAAuB;QAClC,gBAAgB,EAAE,aAAa;QAC/B,gBAAgB,EAAE,kBAAkB;QAGpC,qBAAqB,EAAE,wBAAwB;QAC/C,qBAAqB,EAAE,gBAAgB;QACvC,2BAA2B,EACzB,kEAAkE;QACpE,uBAAuB,EACrB,kEAAkE;QAGpE,yBAAyB,EAAE,+BAA+B;QAC1D,uBAAuB,EAAE,6BAA6B;KACvD,CAAC,CAAC;AACL,CAAC"}
1
+ {"version":3,"file":"seed-openapi-env.js","sourceRoot":"","sources":["../../src/lib/seed-openapi-env.ts"],"names":[],"mappings":";;AA0CA,oDAIC;AAMD,wCAmFC;AAvHD,MAAM,wBAAwB,GAC5B,kEAAkE,CAAC;AACrE,MAAM,gBAAgB,GAAG,8CAA8C,CAAC;AAGxE,SAAS,IAAI,CAAC,IAA4B;IACxC,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;QAChD,IAAI,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,KAAK,SAAS,EAAE,CAAC;YACnC,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC;QAC3B,CAAC;IACH,CAAC;AACH,CAAC;AAGD,MAAM,eAAe,GAAG,eAAe,CAAC;AAYxC,SAAgB,oBAAoB,CAAC,SAAiB;IACpD,OAAO,WAAW,SAAS,CAAC,OAAO,CAAC,gBAAgB,EAAE,GAAG,CAAC,EAAE;SACzD,WAAW,EAAE;SACb,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;AAClB,CAAC;AAMD,SAAgB,cAAc,CAAC,SAAiB;IAC9C,MAAM,MAAM,GAAG,oBAAoB,CAAC,SAAS,CAAC,CAAC;IAC/C,IAAI,CAAC;QACH,QAAQ,EAAE,aAAa;QASvB,uBAAuB,EAAE,GAAG;QAO5B,YAAY,EAAE,2CAA2C,eAAe,WAAW,MAAM,EAAE;QAC3F,OAAO,EAAE,WAAW;QACpB,OAAO,EAAE,MAAM;QACf,OAAO,EAAE,eAAe;QACxB,SAAS,EAAE,MAAM;QACjB,OAAO,EAAE,OAAO;QAChB,WAAW,EAAE,OAAO;QACpB,SAAS,EAAE,wBAAwB;QACnC,cAAc,EAAE,uBAAuB;QAGvC,gBAAgB,EAAE,uBAAuB;QACzC,2BAA2B,EAAE,aAAa;QAC1C,kBAAkB,EAChB,kEAAkE;QACpE,UAAU,EAAE,oCAAoC;QAChD,kBAAkB,EAAE,iDAAiD;QACrE,wBAAwB,EAAE,uBAAuB;QAKjD,mBAAmB,EAAE,gBAAgB;QAMrC,aAAa,EAAE,uBAAuB;QACtC,WAAW,EAAE,uCAAuC;QAGpD,eAAe,EAAE,uBAAuB;QACxC,cAAc,EAAE,uBAAuB;QACvC,oBAAoB,EAAE,uBAAuB;QAC7C,mBAAmB,EAAE,uBAAuB;QAC5C,sBAAsB,EAAE,uBAAuB;QAC/C,sBAAsB,EAAE,uBAAuB;QAC/C,wBAAwB,EAAE,uBAAuB;QACjD,yBAAyB,EAAE,uBAAuB;QAClD,6BAA6B,EAAE,uBAAuB;QACtD,cAAc,EAAE,uBAAuB;QACvC,eAAe,EAAE,uBAAuB;QACxC,2BAA2B,EAAE,uBAAuB;QAGpD,kBAAkB,EAAE,sBAAsB;QAC1C,oBAAoB,EAAE,gCAAgC;QACtD,SAAS,EAAE,uBAAuB;QAClC,gBAAgB,EAAE,aAAa;QAC/B,gBAAgB,EAAE,kBAAkB;QAGpC,qBAAqB,EAAE,wBAAwB;QAC/C,qBAAqB,EAAE,gBAAgB;QACvC,2BAA2B,EACzB,kEAAkE;QACpE,uBAAuB,EACrB,kEAAkE;QAGpE,yBAAyB,EAAE,+BAA+B;QAC1D,uBAAuB,EAAE,6BAA6B;KACvD,CAAC,CAAC;AACL,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xemahq/api-client-generator",
3
- "version": "0.3.0",
3
+ "version": "0.4.1",
4
4
  "description": "Layer 1 — the single, central OpenAPI + ORVAL client generator for every Xema NestJS service. Replaces the per-service orval.config.ts / scripts/extract-openapi.ts / scripts/build-client-index.js trio with two bins (xema-openapi, xema-client-generate). Extracts one OpenAPI document per ApiSurface (public/internal/operator) using the @XemaApiSurface decorator metadata, generates one ORVAL client package per non-empty surface (all sharing the owning service's version), and stamps deterministic `xema` provenance metadata into each generated client package.json. No heuristics: a route ships in a surface only if it (or its controller) is explicitly classified.",
5
5
  "keywords": [
6
6
  "xema",
@@ -63,7 +63,7 @@
63
63
  "prettier": "3.6.2",
64
64
  "reflect-metadata": "0.2.2",
65
65
  "typescript": "5.9.3",
66
- "@xemahq/xema-decorators": "0.5.2"
66
+ "@xemahq/xema-decorators": "0.5.3"
67
67
  },
68
68
  "exports": {
69
69
  ".": {
@@ -109,7 +109,7 @@ export const customFetch = async <T>(
109
109
  if (retryResponse.status >= 400) {
110
110
  throw new ClientError(retryResponse.status, fullUrl, retryBody);
111
111
  }
112
- return unwrapEnvelope(retryBody) as T;
112
+ return retryBody as T;
113
113
  }
114
114
 
115
115
  if (!RETRYABLE_STATUSES.includes(response.status) || attempt >= maxRetries) {
@@ -117,7 +117,7 @@ export const customFetch = async <T>(
117
117
  if (response.status >= 400) {
118
118
  throw new ClientError(response.status, fullUrl, body);
119
119
  }
120
- return unwrapEnvelope(body) as T;
120
+ return body as T;
121
121
  }
122
122
 
123
123
  const retryAfter = parseRetryAfter(response.headers.get('Retry-After'));
@@ -137,36 +137,34 @@ export const customFetch = async <T>(
137
137
  throw lastError ?? new Error(\`All retries exhausted for \${fullUrl}\`);
138
138
  };
139
139
 
140
- /**
141
- * Xema platform services wrap every 2xx payload in a { data: T } envelope via
142
- * the global ResponseEnvelopeInterceptor (platform-common). Generated types
143
- * describe the INNER T (controllers declare their return type as T, not
144
- * { data: T }), so the client must peel data to keep the static types and the
145
- * wire shape consistent — otherwise consumers read fields off the envelope and
146
- * get undefined.
140
+ /*
141
+ * THE TRANSPORT RETURNS THE BODY UNCHANGED. Do not reintroduce an unwrap here.
142
+ *
143
+ * Xema services wrap every 2xx payload in a { data: T } envelope via the global
144
+ * ResponseEnvelopeInterceptor (platform-common), and the generator now emits
145
+ * types that describe THAT ENVELOPE — every endpoint returns
146
+ * Promise<XDataEnvelope> or Promise<XPaginatedEnvelope>, never a bare inner
147
+ * T. Consumers read the data property themselves.
148
+ *
149
+ * This template used to peel data, justified by a comment claiming generated
150
+ * types describe the inner T. That stopped being true when the generator moved
151
+ * to envelope-typed returns, and the comment outlived the fact — so the peel
152
+ * then contradicted every type in every package it seeded. Four clients shipped
153
+ * that way (resource-governance x3, workload-runtime-api): declared
154
+ * *DataEnvelope, returned the already-peeled inner object, so .data read
155
+ * undefined at runtime on every non-paginated endpoint. Paginated calls hid
156
+ * it, because the old peel deliberately preserved an envelope carrying
157
+ * pagination.
158
+ *
159
+ * ensureCustomFetch seeds this template into any client that lacks a
160
+ * transport, so a peel here is not one bug — it is re-imported into every new
161
+ * client generated from now on, which is exactly how those four were born after
162
+ * the hand-fix of their siblings.
147
163
  *
148
- * Only 2xx bodies reach this helper: non-2xx responses are surfaced as a thrown
149
- * ClientError before unwrap, and error envelopes are emitted by the
150
- * GlobalExceptionFilter, never wrapped in { data }. Paginated lists arrive as
151
- * { data: T[], pagination: {...} }; consumers read both fields, so that envelope
152
- * is kept intact (detected by the presence of pagination). Any body without a
153
- * top-level data field is returned as-is.
164
+ * Enforced fleet-wide by check-client-transport-envelope in xema-base, which
165
+ * reads the RETURN SITES rather than banning a name — a rename, an inline
166
+ * .data, or ?? body.data all walk past a string ban.
154
167
  */
155
- function unwrapEnvelope(body: unknown): unknown {
156
- if (
157
- body === null ||
158
- typeof body !== 'object' ||
159
- Array.isArray(body) ||
160
- !('data' in body)
161
- ) {
162
- return body;
163
- }
164
- const envelope = body as { data: unknown; pagination?: unknown };
165
- if ('pagination' in envelope) {
166
- return envelope;
167
- }
168
- return envelope.data;
169
- }
170
168
 
171
169
  async function parseBody(response: Response): Promise<unknown> {
172
170
  const contentType = response.headers.get('content-type');
@@ -86,7 +86,6 @@ export function seedOpenApiEnv(serviceId: string): void {
86
86
  'http://localhost:8180/realms/dummy/protocol/openid-connect/certs',
87
87
  JWT_ISSUER: 'http://localhost:8180/realms/dummy',
88
88
  GATEWAY_JWT_SECRET: 'dummy-gateway-jwt-secret-for-openapi-extraction',
89
- SHARE_TOKEN_SECRET: 'dummy-share-token-secret',
90
89
  OPENCODE_WORKER_PASSWORD: 'dummy-worker-password',
91
90
 
92
91
  // Service registry (etcd-only autodiscovery). `createKernelState` fail-fasts