From db7898f892d346621bcf00dd080737c457cf4405 Mon Sep 17 00:00:00 2001 From: Amp Date: Tue, 3 Nov 2026 06:41:31 +0000 Subject: [PATCH 1/5] Expose control plane server error details Amp-Thread-ID: https://ampcode.com/threads/T-779716dc-4a73-4420-a108-bebb78971a92 Co-authored-by: Max Carter --- README.md | 6 ++++++ packages/control-plane/index.test.ts | 17 +++++++++++++++++ packages/control-plane/index.ts | 13 ++++++++++--- 3 files changed, 33 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 556a114..9882b57 100644 --- a/README.md +++ b/README.md @@ -67,6 +67,12 @@ on outbound records can be `mixed`. Recorded service/status is not independent channel or delivery proof. Recorded request/response/error fields can contain sensitive data; do not log them or assume that server text is sanitized. +FaceTime control-plane failures expose the server's error code and message as +`ChertApiError.serverCode` and `ChertApiError.serverMessage` when present. These +fields are untrusted and may contain sensitive data. They are non-enumerable and +are not included in the error's default message; sanitize them before display or +logging. The existing `code` remains a safe, allowlisted code. + ## Languages and release status | Language | Available on `main` | Release / unified-channel work | Documentation | diff --git a/packages/control-plane/index.test.ts b/packages/control-plane/index.test.ts index e7e4e4d..9fa2f9c 100644 --- a/packages/control-plane/index.test.ts +++ b/packages/control-plane/index.test.ts @@ -89,6 +89,23 @@ describe('control-plane wire contracts',()=>{ }); }); describe('safe failures',()=>{ + it('exposes server error information separately from safe error text',async()=>{ + const responses=[ + Response.json({error:{code:'livekit_not_provisioned',message:'Use Shared or your own LiveKit.'}},{status:409}), + Response.json({error:{code:'new_server_code'}},{status:400}), + new Response('{invalid',{status:502}), + ]; + const client=new ChertControlPlane({fetch:(async()=>responses.shift()!) as unknown as typeof fetch}); + const first=await client.beginLogin({device_name:'Test'}).catch(e=>e); + expect(first).toMatchObject({status:409,code:'request_failed',serverCode:'livekit_not_provisioned',serverMessage:'Use Shared or your own LiveKit.'}); + expect(String(first)).not.toContain('Use Shared'); + expect(JSON.stringify(first)).not.toContain('Use Shared'); + expect(JSON.stringify(first)).not.toContain('livekit_not_provisioned'); + const second=await client.beginLogin({device_name:'Test'}).catch(e=>e); + expect(second).toMatchObject({status:400,serverCode:'new_server_code',serverMessage:undefined}); + const third=await client.beginLogin({device_name:'Test'}).catch(e=>e); + expect(third).toMatchObject({status:502,code:'invalid_response',serverCode:undefined,serverMessage:undefined}); + }); it('bounds response bytes and attaches a request deadline',async()=>{let signal:AbortSignal|undefined;const client=new ChertControlPlane({fetch:(async(_url: Parameters[0],init?: RequestInit)=>{signal=init?.signal??undefined;return new Response('x'.repeat(1_048_577));}) as typeof fetch});await expect(client.beginLogin({device_name:'Test'})).rejects.toMatchObject({code:'invalid_response'});expect(signal).toBeInstanceOf(AbortSignal);}); it('preserves permission failures without retry or leaking server data',async()=>{let calls=0;const client=new ChertControlPlane({token:'private',projectId:project,fetch:(async()=>{calls++;return Response.json({error:{code:'forbidden',message:'private token',details:{secret:'bad'}}},{status:403});}) as unknown as typeof fetch});try{await client.createCheckout({plan:'shared'},'stable-key');throw Error('expected');}catch(error){expect(error).toBeInstanceOf(ChertApiError);expect((error as ChertApiError).code).toBe('forbidden');expect(String(error)).not.toContain('private');expect(JSON.stringify(error)).not.toContain('secret');}expect(calls).toBe(1);}); it('terminal billing: purchase, change_plan and payment_method send keyed actions and accept only Stripe-hosted URLs',async()=>{ diff --git a/packages/control-plane/index.ts b/packages/control-plane/index.ts index 3fac495..d9e53dd 100644 --- a/packages/control-plane/index.ts +++ b/packages/control-plane/index.ts @@ -100,8 +100,15 @@ export interface Checkout {url:string} export interface CheckoutInput {plan:BillingPlan;coupon_code?:string} export interface BillingCancellation {status:'cancellation_requested';message:string} export interface CouponValidation {code:string;percent_off:number;mode:'live'|'test';currency:string;monthly_total?:number;due_now?:number;access_hours?:number;plans?:BillingPlan[]} -/** Server text, bodies, URLs and credentials are deliberately not included in errors. */ -export class ChertApiError extends Error {constructor(public readonly status:number,public readonly code:string){super(`Chert request failed (${status}, ${code}).`);this.name='ChertApiError';}} +/** Server fields are untrusted and may contain sensitive data; do not log them without sanitizing. */ +export class ChertApiError extends Error { + declare readonly serverCode?:string; + declare readonly serverMessage?:string; + constructor(public readonly status:number,public readonly code:string,serverCode?:string,serverMessage?:string){ + super(`Chert request failed (${status}, ${code}).`);this.name='ChertApiError'; + Object.defineProperties(this,{serverCode:{value:serverCode},serverMessage:{value:serverMessage}}); + } +} const safeCodes = new Set(['transcript_not_found','billing_plan_change_unavailable','living_capability_unsupported','living_surface_conflict','living_state_conflict','living_deployment_not_ready','living_input_not_allowed','living_session_not_found','media_capability_unsupported','media_session_conflict','call_not_active','call_ended','call_not_found','media_session_expired','media_transport_unavailable','unauthorized','forbidden','cli_scope_denied','billing_owner_required','billing_unavailable','billing_binding_mismatch','billing_checkout_exists','billing_coupon_unavailable','billing_dedicated_no_inventory','billing_dedicated_unqualified','billing_idempotency_conflict','billing_invalid','billing_no_subscription','billing_paid_period_required','billing_price_mismatch','billing_provider_mismatch','billing_provider_unavailable','billing_reservation_expired','billing_reservation_not_found','billing_reservation_pending','billing_sales_disabled','billing_subscription_conflict','billing_subscription_exists','billing_payment_failed','billing_plan_unchanged','billing_unconfigured','shared_no_capacity','project_required','invalid_request','cli_login_expired','cli_login_not_found','cli_login_forbidden','slow_down','cli_login_rate_limited','cli_login_busy','not_found','conflict','caller_screen_not_shared','media_content_unsupported','caller_screen_unsupported','media_limit_exceeded','video_composition_unsupported','composition_invalid','composition_revision_conflict','composition_base_unavailable']); export class ChertControlPlane { private readonly origin:string; private readonly token?:string;private readonly projectId?:string;private readonly fetcher:typeof globalThis.fetch; @@ -137,7 +144,7 @@ export class ChertControlPlane { throw new ChertApiError(response.status,response.status===404?'endpoint_unavailable':'invalid_response'); } if(!envelope||typeof envelope!=='object')throw new ChertApiError(response.status,'invalid_response'); - if(!response.ok){const error=(envelope as {error?:unknown}).error;const code=error&&typeof error==='object'?(error as {code?:unknown}).code:undefined;throw new ChertApiError(response.status,typeof code==='string'&&safeCodes.has(code)?code:'request_failed');} + if(!response.ok){const error=(envelope as {error?:unknown}).error;const fields=error&&typeof error==='object'?error as {code?:unknown;message?:unknown}:undefined;const code=typeof fields?.code==='string'?fields.code:undefined;const message=typeof fields?.message==='string'?fields.message:undefined;throw new ChertApiError(response.status,code&&safeCodes.has(code)?code:'request_failed',code,message);} if(!Object.hasOwn(envelope,'data'))throw new ChertApiError(response.status,'invalid_response'); return (envelope as {data:T}).data; } From b086aa1e19aa47cf4cf2176eb6bd9e9849c10a6c Mon Sep 17 00:00:00 2001 From: Amp Date: Tue, 3 Nov 2026 07:06:34 +0000 Subject: [PATCH 2/5] Expose server error codes without messages Amp-Thread-ID: https://ampcode.com/threads/T-779716dc-4a73-4420-a108-bebb78971a92 Co-authored-by: Max Carter --- README.md | 8 +++----- packages/control-plane/index.test.ts | 12 ++++++------ packages/control-plane/index.ts | 8 ++------ 3 files changed, 11 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 9882b57..c644670 100644 --- a/README.md +++ b/README.md @@ -67,11 +67,9 @@ on outbound records can be `mixed`. Recorded service/status is not independent channel or delivery proof. Recorded request/response/error fields can contain sensitive data; do not log them or assume that server text is sanitized. -FaceTime control-plane failures expose the server's error code and message as -`ChertApiError.serverCode` and `ChertApiError.serverMessage` when present. These -fields are untrusted and may contain sensitive data. They are non-enumerable and -are not included in the error's default message; sanitize them before display or -logging. The existing `code` remains a safe, allowlisted code. +FaceTime control-plane failures expose the server's error code as the enumerable +`ChertApiError.serverCode` when present. The existing `code` remains an +allowlisted code, and the default error text does not include server messages. ## Languages and release status diff --git a/packages/control-plane/index.test.ts b/packages/control-plane/index.test.ts index 9fa2f9c..bc280a4 100644 --- a/packages/control-plane/index.test.ts +++ b/packages/control-plane/index.test.ts @@ -89,7 +89,7 @@ describe('control-plane wire contracts',()=>{ }); }); describe('safe failures',()=>{ - it('exposes server error information separately from safe error text',async()=>{ + it('exposes server error codes without retaining server messages',async()=>{ const responses=[ Response.json({error:{code:'livekit_not_provisioned',message:'Use Shared or your own LiveKit.'}},{status:409}), Response.json({error:{code:'new_server_code'}},{status:400}), @@ -97,14 +97,14 @@ describe('safe failures',()=>{ ]; const client=new ChertControlPlane({fetch:(async()=>responses.shift()!) as unknown as typeof fetch}); const first=await client.beginLogin({device_name:'Test'}).catch(e=>e); - expect(first).toMatchObject({status:409,code:'request_failed',serverCode:'livekit_not_provisioned',serverMessage:'Use Shared or your own LiveKit.'}); + expect(first).toMatchObject({status:409,code:'request_failed',serverCode:'livekit_not_provisioned'}); expect(String(first)).not.toContain('Use Shared'); - expect(JSON.stringify(first)).not.toContain('Use Shared'); - expect(JSON.stringify(first)).not.toContain('livekit_not_provisioned'); + expect(JSON.stringify(first)).toContain('livekit_not_provisioned'); + expect(JSON.stringify(first,Object.getOwnPropertyNames(first))).not.toContain('Use Shared'); const second=await client.beginLogin({device_name:'Test'}).catch(e=>e); - expect(second).toMatchObject({status:400,serverCode:'new_server_code',serverMessage:undefined}); + expect(second).toMatchObject({status:400,serverCode:'new_server_code'}); const third=await client.beginLogin({device_name:'Test'}).catch(e=>e); - expect(third).toMatchObject({status:502,code:'invalid_response',serverCode:undefined,serverMessage:undefined}); + expect(third).toMatchObject({status:502,code:'invalid_response',serverCode:undefined}); }); it('bounds response bytes and attaches a request deadline',async()=>{let signal:AbortSignal|undefined;const client=new ChertControlPlane({fetch:(async(_url: Parameters[0],init?: RequestInit)=>{signal=init?.signal??undefined;return new Response('x'.repeat(1_048_577));}) as typeof fetch});await expect(client.beginLogin({device_name:'Test'})).rejects.toMatchObject({code:'invalid_response'});expect(signal).toBeInstanceOf(AbortSignal);}); it('preserves permission failures without retry or leaking server data',async()=>{let calls=0;const client=new ChertControlPlane({token:'private',projectId:project,fetch:(async()=>{calls++;return Response.json({error:{code:'forbidden',message:'private token',details:{secret:'bad'}}},{status:403});}) as unknown as typeof fetch});try{await client.createCheckout({plan:'shared'},'stable-key');throw Error('expected');}catch(error){expect(error).toBeInstanceOf(ChertApiError);expect((error as ChertApiError).code).toBe('forbidden');expect(String(error)).not.toContain('private');expect(JSON.stringify(error)).not.toContain('secret');}expect(calls).toBe(1);}); diff --git a/packages/control-plane/index.ts b/packages/control-plane/index.ts index d9e53dd..61d3d7a 100644 --- a/packages/control-plane/index.ts +++ b/packages/control-plane/index.ts @@ -100,13 +100,9 @@ export interface Checkout {url:string} export interface CheckoutInput {plan:BillingPlan;coupon_code?:string} export interface BillingCancellation {status:'cancellation_requested';message:string} export interface CouponValidation {code:string;percent_off:number;mode:'live'|'test';currency:string;monthly_total?:number;due_now?:number;access_hours?:number;plans?:BillingPlan[]} -/** Server fields are untrusted and may contain sensitive data; do not log them without sanitizing. */ export class ChertApiError extends Error { - declare readonly serverCode?:string; - declare readonly serverMessage?:string; - constructor(public readonly status:number,public readonly code:string,serverCode?:string,serverMessage?:string){ + constructor(public readonly status:number,public readonly code:string,public readonly serverCode?:string){ super(`Chert request failed (${status}, ${code}).`);this.name='ChertApiError'; - Object.defineProperties(this,{serverCode:{value:serverCode},serverMessage:{value:serverMessage}}); } } const safeCodes = new Set(['transcript_not_found','billing_plan_change_unavailable','living_capability_unsupported','living_surface_conflict','living_state_conflict','living_deployment_not_ready','living_input_not_allowed','living_session_not_found','media_capability_unsupported','media_session_conflict','call_not_active','call_ended','call_not_found','media_session_expired','media_transport_unavailable','unauthorized','forbidden','cli_scope_denied','billing_owner_required','billing_unavailable','billing_binding_mismatch','billing_checkout_exists','billing_coupon_unavailable','billing_dedicated_no_inventory','billing_dedicated_unqualified','billing_idempotency_conflict','billing_invalid','billing_no_subscription','billing_paid_period_required','billing_price_mismatch','billing_provider_mismatch','billing_provider_unavailable','billing_reservation_expired','billing_reservation_not_found','billing_reservation_pending','billing_sales_disabled','billing_subscription_conflict','billing_subscription_exists','billing_payment_failed','billing_plan_unchanged','billing_unconfigured','shared_no_capacity','project_required','invalid_request','cli_login_expired','cli_login_not_found','cli_login_forbidden','slow_down','cli_login_rate_limited','cli_login_busy','not_found','conflict','caller_screen_not_shared','media_content_unsupported','caller_screen_unsupported','media_limit_exceeded','video_composition_unsupported','composition_invalid','composition_revision_conflict','composition_base_unavailable']); @@ -144,7 +140,7 @@ export class ChertControlPlane { throw new ChertApiError(response.status,response.status===404?'endpoint_unavailable':'invalid_response'); } if(!envelope||typeof envelope!=='object')throw new ChertApiError(response.status,'invalid_response'); - if(!response.ok){const error=(envelope as {error?:unknown}).error;const fields=error&&typeof error==='object'?error as {code?:unknown;message?:unknown}:undefined;const code=typeof fields?.code==='string'?fields.code:undefined;const message=typeof fields?.message==='string'?fields.message:undefined;throw new ChertApiError(response.status,code&&safeCodes.has(code)?code:'request_failed',code,message);} + if(!response.ok){const error=(envelope as {error?:unknown}).error;const fields=error&&typeof error==='object'?error as {code?:unknown}:undefined;const code=typeof fields?.code==='string'?fields.code:undefined;throw new ChertApiError(response.status,code&&safeCodes.has(code)?code:'request_failed',code);} if(!Object.hasOwn(envelope,'data'))throw new ChertApiError(response.status,'invalid_response'); return (envelope as {data:T}).data; } From a98c71e362275c96d43426a1d4e8e0890a75cf9b Mon Sep 17 00:00:00 2001 From: Amp Date: Tue, 3 Nov 2026 07:09:02 +0000 Subject: [PATCH 3/5] Restrict exposed error codes to recognized identifiers Amp-Thread-ID: https://ampcode.com/threads/T-779716dc-4a73-4420-a108-bebb78971a92 Co-authored-by: Max Carter --- README.md | 6 +++--- packages/control-plane/index.test.ts | 8 ++++++-- packages/control-plane/index.ts | 2 +- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index c644670..4291c67 100644 --- a/README.md +++ b/README.md @@ -67,9 +67,9 @@ on outbound records can be `mixed`. Recorded service/status is not independent channel or delivery proof. Recorded request/response/error fields can contain sensitive data; do not log them or assume that server text is sanitized. -FaceTime control-plane failures expose the server's error code as the enumerable -`ChertApiError.serverCode` when present. The existing `code` remains an -allowlisted code, and the default error text does not include server messages. +FaceTime control-plane failures expose recognized server error codes as the +enumerable `ChertApiError.serverCode`; unknown codes are omitted. The existing +`code` remains unchanged, and the default error text does not include server messages. ## Languages and release status diff --git a/packages/control-plane/index.test.ts b/packages/control-plane/index.test.ts index bc280a4..4eac084 100644 --- a/packages/control-plane/index.test.ts +++ b/packages/control-plane/index.test.ts @@ -93,6 +93,7 @@ describe('safe failures',()=>{ const responses=[ Response.json({error:{code:'livekit_not_provisioned',message:'Use Shared or your own LiveKit.'}},{status:409}), Response.json({error:{code:'new_server_code'}},{status:400}), + Response.json({error:{code:'billing_private token: secret',message:'secret'}},{status:500}), new Response('{invalid',{status:502}), ]; const client=new ChertControlPlane({fetch:(async()=>responses.shift()!) as unknown as typeof fetch}); @@ -102,9 +103,12 @@ describe('safe failures',()=>{ expect(JSON.stringify(first)).toContain('livekit_not_provisioned'); expect(JSON.stringify(first,Object.getOwnPropertyNames(first))).not.toContain('Use Shared'); const second=await client.beginLogin({device_name:'Test'}).catch(e=>e); - expect(second).toMatchObject({status:400,serverCode:'new_server_code'}); + expect(second).toMatchObject({status:400,serverCode:undefined}); const third=await client.beginLogin({device_name:'Test'}).catch(e=>e); - expect(third).toMatchObject({status:502,code:'invalid_response',serverCode:undefined}); + expect(third).toMatchObject({status:500,code:'request_failed',serverCode:undefined}); + expect(JSON.stringify(third,Object.getOwnPropertyNames(third))).not.toContain('secret'); + const fourth=await client.beginLogin({device_name:'Test'}).catch(e=>e); + expect(fourth).toMatchObject({status:502,code:'invalid_response',serverCode:undefined}); }); it('bounds response bytes and attaches a request deadline',async()=>{let signal:AbortSignal|undefined;const client=new ChertControlPlane({fetch:(async(_url: Parameters[0],init?: RequestInit)=>{signal=init?.signal??undefined;return new Response('x'.repeat(1_048_577));}) as typeof fetch});await expect(client.beginLogin({device_name:'Test'})).rejects.toMatchObject({code:'invalid_response'});expect(signal).toBeInstanceOf(AbortSignal);}); it('preserves permission failures without retry or leaking server data',async()=>{let calls=0;const client=new ChertControlPlane({token:'private',projectId:project,fetch:(async()=>{calls++;return Response.json({error:{code:'forbidden',message:'private token',details:{secret:'bad'}}},{status:403});}) as unknown as typeof fetch});try{await client.createCheckout({plan:'shared'},'stable-key');throw Error('expected');}catch(error){expect(error).toBeInstanceOf(ChertApiError);expect((error as ChertApiError).code).toBe('forbidden');expect(String(error)).not.toContain('private');expect(JSON.stringify(error)).not.toContain('secret');}expect(calls).toBe(1);}); diff --git a/packages/control-plane/index.ts b/packages/control-plane/index.ts index 61d3d7a..4f1e2c0 100644 --- a/packages/control-plane/index.ts +++ b/packages/control-plane/index.ts @@ -140,7 +140,7 @@ export class ChertControlPlane { throw new ChertApiError(response.status,response.status===404?'endpoint_unavailable':'invalid_response'); } if(!envelope||typeof envelope!=='object')throw new ChertApiError(response.status,'invalid_response'); - if(!response.ok){const error=(envelope as {error?:unknown}).error;const fields=error&&typeof error==='object'?error as {code?:unknown}:undefined;const code=typeof fields?.code==='string'?fields.code:undefined;throw new ChertApiError(response.status,code&&safeCodes.has(code)?code:'request_failed',code);} + if(!response.ok){const error=(envelope as {error?:unknown}).error;const fields=error&&typeof error==='object'?error as {code?:unknown}:undefined;const code=typeof fields?.code==='string'?fields.code:undefined;const safeCode=code&&(safeCodes.has(code)||code==='livekit_not_provisioned')?code:undefined;throw new ChertApiError(response.status,code&&safeCodes.has(code)?code:'request_failed',safeCode);} if(!Object.hasOwn(envelope,'data'))throw new ChertApiError(response.status,'invalid_response'); return (envelope as {data:T}).data; } From 4b3b0dd25586a69c67baeef5acf9f250e66eab39 Mon Sep 17 00:00:00 2001 From: Amp Date: Tue, 3 Nov 2026 07:13:01 +0000 Subject: [PATCH 4/5] Distinguish unknown server error codes Amp-Thread-ID: https://ampcode.com/threads/T-779716dc-4a73-4420-a108-bebb78971a92 Co-authored-by: Max Carter --- README.md | 5 +++-- packages/control-plane/index.test.ts | 9 ++++++--- packages/control-plane/index.ts | 5 ++++- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 4291c67..4eb2a8e 100644 --- a/README.md +++ b/README.md @@ -68,8 +68,9 @@ channel or delivery proof. Recorded request/response/error fields can contain sensitive data; do not log them or assume that server text is sanitized. FaceTime control-plane failures expose recognized server error codes as the -enumerable `ChertApiError.serverCode`; unknown codes are omitted. The existing -`code` remains unchanged, and the default error text does not include server messages. +enumerable `ChertApiError.serverCode`. Unrecognized codes map to `unknown`; +when no code is provided, `serverCode` is undefined. The existing `code` remains +unchanged, and the default error text does not include server messages. ## Languages and release status diff --git a/packages/control-plane/index.test.ts b/packages/control-plane/index.test.ts index 4eac084..503466a 100644 --- a/packages/control-plane/index.test.ts +++ b/packages/control-plane/index.test.ts @@ -94,6 +94,7 @@ describe('safe failures',()=>{ Response.json({error:{code:'livekit_not_provisioned',message:'Use Shared or your own LiveKit.'}},{status:409}), Response.json({error:{code:'new_server_code'}},{status:400}), Response.json({error:{code:'billing_private token: secret',message:'secret'}},{status:500}), + Response.json({error:{message:'private message'}},{status:400}), new Response('{invalid',{status:502}), ]; const client=new ChertControlPlane({fetch:(async()=>responses.shift()!) as unknown as typeof fetch}); @@ -103,12 +104,14 @@ describe('safe failures',()=>{ expect(JSON.stringify(first)).toContain('livekit_not_provisioned'); expect(JSON.stringify(first,Object.getOwnPropertyNames(first))).not.toContain('Use Shared'); const second=await client.beginLogin({device_name:'Test'}).catch(e=>e); - expect(second).toMatchObject({status:400,serverCode:undefined}); + expect(second).toMatchObject({status:400,serverCode:'unknown'}); const third=await client.beginLogin({device_name:'Test'}).catch(e=>e); - expect(third).toMatchObject({status:500,code:'request_failed',serverCode:undefined}); + expect(third).toMatchObject({status:500,code:'request_failed',serverCode:'unknown'}); expect(JSON.stringify(third,Object.getOwnPropertyNames(third))).not.toContain('secret'); const fourth=await client.beginLogin({device_name:'Test'}).catch(e=>e); - expect(fourth).toMatchObject({status:502,code:'invalid_response',serverCode:undefined}); + expect(fourth).toMatchObject({status:400,serverCode:undefined}); + const fifth=await client.beginLogin({device_name:'Test'}).catch(e=>e); + expect(fifth).toMatchObject({status:502,code:'invalid_response',serverCode:undefined}); }); it('bounds response bytes and attaches a request deadline',async()=>{let signal:AbortSignal|undefined;const client=new ChertControlPlane({fetch:(async(_url: Parameters[0],init?: RequestInit)=>{signal=init?.signal??undefined;return new Response('x'.repeat(1_048_577));}) as typeof fetch});await expect(client.beginLogin({device_name:'Test'})).rejects.toMatchObject({code:'invalid_response'});expect(signal).toBeInstanceOf(AbortSignal);}); it('preserves permission failures without retry or leaking server data',async()=>{let calls=0;const client=new ChertControlPlane({token:'private',projectId:project,fetch:(async()=>{calls++;return Response.json({error:{code:'forbidden',message:'private token',details:{secret:'bad'}}},{status:403});}) as unknown as typeof fetch});try{await client.createCheckout({plan:'shared'},'stable-key');throw Error('expected');}catch(error){expect(error).toBeInstanceOf(ChertApiError);expect((error as ChertApiError).code).toBe('forbidden');expect(String(error)).not.toContain('private');expect(JSON.stringify(error)).not.toContain('secret');}expect(calls).toBe(1);}); diff --git a/packages/control-plane/index.ts b/packages/control-plane/index.ts index 4f1e2c0..70bd5fa 100644 --- a/packages/control-plane/index.ts +++ b/packages/control-plane/index.ts @@ -106,6 +106,9 @@ export class ChertApiError extends Error { } } const safeCodes = new Set(['transcript_not_found','billing_plan_change_unavailable','living_capability_unsupported','living_surface_conflict','living_state_conflict','living_deployment_not_ready','living_input_not_allowed','living_session_not_found','media_capability_unsupported','media_session_conflict','call_not_active','call_ended','call_not_found','media_session_expired','media_transport_unavailable','unauthorized','forbidden','cli_scope_denied','billing_owner_required','billing_unavailable','billing_binding_mismatch','billing_checkout_exists','billing_coupon_unavailable','billing_dedicated_no_inventory','billing_dedicated_unqualified','billing_idempotency_conflict','billing_invalid','billing_no_subscription','billing_paid_period_required','billing_price_mismatch','billing_provider_mismatch','billing_provider_unavailable','billing_reservation_expired','billing_reservation_not_found','billing_reservation_pending','billing_sales_disabled','billing_subscription_conflict','billing_subscription_exists','billing_payment_failed','billing_plan_unchanged','billing_unconfigured','shared_no_capacity','project_required','invalid_request','cli_login_expired','cli_login_not_found','cli_login_forbidden','slow_down','cli_login_rate_limited','cli_login_busy','not_found','conflict','caller_screen_not_shared','media_content_unsupported','caller_screen_unsupported','media_limit_exceeded','video_composition_unsupported','composition_invalid','composition_revision_conflict','composition_base_unavailable']); +const serverCodes = new Set([...safeCodes, + 'livekit_not_provisioned', +]); export class ChertControlPlane { private readonly origin:string; private readonly token?:string;private readonly projectId?:string;private readonly fetcher:typeof globalThis.fetch; constructor(options:ClientOptions={}){ @@ -140,7 +143,7 @@ export class ChertControlPlane { throw new ChertApiError(response.status,response.status===404?'endpoint_unavailable':'invalid_response'); } if(!envelope||typeof envelope!=='object')throw new ChertApiError(response.status,'invalid_response'); - if(!response.ok){const error=(envelope as {error?:unknown}).error;const fields=error&&typeof error==='object'?error as {code?:unknown}:undefined;const code=typeof fields?.code==='string'?fields.code:undefined;const safeCode=code&&(safeCodes.has(code)||code==='livekit_not_provisioned')?code:undefined;throw new ChertApiError(response.status,code&&safeCodes.has(code)?code:'request_failed',safeCode);} + if(!response.ok){const error=(envelope as {error?:unknown}).error;const fields=error&&typeof error==='object'?error as {code?:unknown}:undefined;const code=typeof fields?.code==='string'?fields.code:undefined;const serverCode=code===undefined?undefined:serverCodes.has(code)?code:'unknown';throw new ChertApiError(response.status,code&&safeCodes.has(code)?code:'request_failed',serverCode);} if(!Object.hasOwn(envelope,'data'))throw new ChertApiError(response.status,'invalid_response'); return (envelope as {data:T}).data; } From 2fdcb2d6cbe5968bc3d2f0b0a75a65ca979b81ca Mon Sep 17 00:00:00 2001 From: Amp Date: Tue, 3 Nov 2026 07:15:40 +0000 Subject: [PATCH 5/5] Distinguish malformed server codes from absent codes Amp-Thread-ID: https://ampcode.com/threads/T-779716dc-4a73-4420-a108-bebb78971a92 Co-authored-by: Max Carter --- packages/control-plane/index.test.ts | 11 +++++++++-- packages/control-plane/index.ts | 2 +- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/packages/control-plane/index.test.ts b/packages/control-plane/index.test.ts index 503466a..6703de2 100644 --- a/packages/control-plane/index.test.ts +++ b/packages/control-plane/index.test.ts @@ -95,6 +95,9 @@ describe('safe failures',()=>{ Response.json({error:{code:'new_server_code'}},{status:400}), Response.json({error:{code:'billing_private token: secret',message:'secret'}},{status:500}), Response.json({error:{message:'private message'}},{status:400}), + Response.json({error:{code:null}},{status:400}), + Response.json({error:{code:123}},{status:400}), + Response.json({error:{code:''}},{status:400}), new Response('{invalid',{status:502}), ]; const client=new ChertControlPlane({fetch:(async()=>responses.shift()!) as unknown as typeof fetch}); @@ -110,8 +113,12 @@ describe('safe failures',()=>{ expect(JSON.stringify(third,Object.getOwnPropertyNames(third))).not.toContain('secret'); const fourth=await client.beginLogin({device_name:'Test'}).catch(e=>e); expect(fourth).toMatchObject({status:400,serverCode:undefined}); - const fifth=await client.beginLogin({device_name:'Test'}).catch(e=>e); - expect(fifth).toMatchObject({status:502,code:'invalid_response',serverCode:undefined}); + for(let i=0;i<3;i++){ + const malformed=await client.beginLogin({device_name:'Test'}).catch(e=>e); + expect(malformed).toMatchObject({status:400,serverCode:'unknown'}); + } + const invalid=await client.beginLogin({device_name:'Test'}).catch(e=>e); + expect(invalid).toMatchObject({status:502,code:'invalid_response',serverCode:undefined}); }); it('bounds response bytes and attaches a request deadline',async()=>{let signal:AbortSignal|undefined;const client=new ChertControlPlane({fetch:(async(_url: Parameters[0],init?: RequestInit)=>{signal=init?.signal??undefined;return new Response('x'.repeat(1_048_577));}) as typeof fetch});await expect(client.beginLogin({device_name:'Test'})).rejects.toMatchObject({code:'invalid_response'});expect(signal).toBeInstanceOf(AbortSignal);}); it('preserves permission failures without retry or leaking server data',async()=>{let calls=0;const client=new ChertControlPlane({token:'private',projectId:project,fetch:(async()=>{calls++;return Response.json({error:{code:'forbidden',message:'private token',details:{secret:'bad'}}},{status:403});}) as unknown as typeof fetch});try{await client.createCheckout({plan:'shared'},'stable-key');throw Error('expected');}catch(error){expect(error).toBeInstanceOf(ChertApiError);expect((error as ChertApiError).code).toBe('forbidden');expect(String(error)).not.toContain('private');expect(JSON.stringify(error)).not.toContain('secret');}expect(calls).toBe(1);}); diff --git a/packages/control-plane/index.ts b/packages/control-plane/index.ts index 70bd5fa..2db7936 100644 --- a/packages/control-plane/index.ts +++ b/packages/control-plane/index.ts @@ -143,7 +143,7 @@ export class ChertControlPlane { throw new ChertApiError(response.status,response.status===404?'endpoint_unavailable':'invalid_response'); } if(!envelope||typeof envelope!=='object')throw new ChertApiError(response.status,'invalid_response'); - if(!response.ok){const error=(envelope as {error?:unknown}).error;const fields=error&&typeof error==='object'?error as {code?:unknown}:undefined;const code=typeof fields?.code==='string'?fields.code:undefined;const serverCode=code===undefined?undefined:serverCodes.has(code)?code:'unknown';throw new ChertApiError(response.status,code&&safeCodes.has(code)?code:'request_failed',serverCode);} + if(!response.ok){const error=(envelope as {error?:unknown}).error;const fields=error&&typeof error==='object'?error as {code?:unknown}:undefined;const code=typeof fields?.code==='string'?fields.code:undefined;const serverCode=!fields||!Object.hasOwn(fields,'code')?undefined:code!==undefined&&serverCodes.has(code)?code:'unknown';throw new ChertApiError(response.status,code&&safeCodes.has(code)?code:'request_failed',serverCode);} if(!Object.hasOwn(envelope,'data'))throw new ChertApiError(response.status,'invalid_response'); return (envelope as {data:T}).data; }