-
Notifications
You must be signed in to change notification settings - Fork 29
Description
Goal
Investigate if we can achieve this fully flattened structure:
await outpost.destinations.create("acme-corp", {
type: "webhook",
topics: ["order.created"],
config: { url: "https://acme.com/webhooks" },
});Where tenantId is a direct parameter (first position) and request body fields are flattened.
Problem Summary
Why We Have This Problem
-
Dual Authentication Support: Outpost supports both Admin API Key and JWT authentication:
- Admin API Key: Requires
tenantIdin the path (required parameter) - JWT Token: Infers
tenantIdfrom the token (optional in path, but validated if provided)
- Admin API Key: Requires
-
OpenAPI Specification: The OpenAPI spec marks
tenant_idasrequired: truein the path, but includes the description "Required when using AdminApiKey authentication", which implies it's conditionally required. -
Global Parameter Override: The
speakeasy-modifications-overlay.yamldefinestenant_idas a global parameter, which makes Speakeasy treat it as optional in all method signatures (since globals can be provided at the SDK client level). -
Speakeasy Behavior: When a parameter is optional, Speakeasy places optional parameters after required ones when flattening. Since the request body (
params) is always required, it appears before the optionaltenantId.
Why tenantId is Optional
The tenantId parameter is optional in the generated SDK because:
-
Global Parameter Definition: In
sdks/schemas/speakeasy-modifications-overlay.yaml(lines 254-264),tenant_idis defined as a global parameter. Global parameters are treated as optional in method signatures because they can be provided at the SDK client level. -
JWT Authentication Support: When using JWT authentication, the API can infer the
tenantIdfrom the token, making it technically optional in the path (though the OpenAPI spec still marks it asrequired: true). -
API Behavior: The API middleware (
internal/apirouter/auth_middleware.go:116-120) validates that if atenantIdis provided in the path, it matches the JWT's tenant ID. If notenantIdis provided, it uses the one from the token.
Why tenantId Can't Be First Parameter
When tenantId is optional and we enable flattening (maxMethodParams > 0):
-
Parameter Ordering: Speakeasy orders parameters as: required parameters first, then optional parameters. Since:
- Request body (
params) is always required tenantIdis optional (due to global parameter definition)- The result is:
create(params, tenantId?, options?)instead ofcreate(tenantId, params, options?)
- Request body (
-
Flattening Order: The
flatteningOrder: parameters-firstsetting only applies when both parameters are required. When one is optional, Speakeasy prioritizes required parameters. -
TypeScript Limitation: TypeScript SDK doesn't have a
flattenRequestsoption like Python, which would flatten request body fields into individual parameters.
Findings
Test 1: maxMethodParams: 0 (Current Configuration)
Result: All parameters bundled into a single request object
async create(
request: operations.CreateTenantDestinationRequest,
options?: RequestOptions,
): Promise<components.Destination>Usage:
await outpost.destinations.create({
tenantId: "acme-corp",
params: {
type: "webhook",
topics: ["order.created"],
config: { url: "https://acme.com/webhooks" },
},
});Test 2: maxMethodParams: 20 (Flattening Enabled)
Result: Parameters are flattened, but request body fields remain nested
async create(
params: components.DestinationCreate,
tenantId?: string | undefined,
options?: RequestOptions,
): Promise<components.Destination>Usage:
await outpost.destinations.create(
{ type: "webhook", topics: ["order.created"], config: {...} },
"acme-corp"
);Issues:
- Request body fields (
type,topics,config) are still nested inparamsobject - Parameter order is wrong:
paramscomes beforetenantId(despiteflatteningOrder: parameters-first) tenantIdis optional because it's defined as a global parameter- TypeScript doesn't have a
flattenRequestsoption like Python
Solution: Making tenantId Required
If We Remove the Global Parameter
If we remove the global parameter definition from the overlay:
-
tenantIdbecomes required: Since the OpenAPI spec marks it asrequired: true, removing the global override will make it required in the SDK. -
Parameter order improves: With
maxMethodParams: 10andflatteningOrder: parameters-first, we should get:async create( tenantId: string, params: components.DestinationCreate, options?: RequestOptions, ): Promise<components.Destination>
-
JWT authentication impact: JWT users would need to pass
tenantIdexplicitly, even though it's in their token. The API will validate that the pathtenantIdmatches the JWT's tenant ID (security check). -
Trade-offs:
- ✅ Consistent SDK signature for both auth methods
- ✅ Better parameter ordering (
tenantIdfirst) - ✅ Security validation (API ensures tenant ID matches JWT)
⚠️ Slightly more verbose for JWT users (must pass tenant ID that's already in token)
Best Available Structure (With Global Parameter Removed)
With maxMethodParams: 10, requestBodyFieldName: "params", and global parameter removed:
await outpost.destinations.create(
"acme-corp", // tenantId (required, first parameter)
{ // params (request body)
type: "webhook",
topics: ["order.created"],
config: { url: "https://acme.com/webhooks" },
}
);Note: Request body fields (type, topics, config) remain nested in the params object because TypeScript SDK doesn't support field-level flattening like Python's flattenRequests: true.
Conclusion
What IS Achievable
✅ Parameter-level flattening with correct order:
tenantIdas first required parameterparamsas second required parameteroptionsas third optional parameter
What IS NOT Achievable
❌ Field-level flattening:
- Cannot flatten request body fields (
type,topics,config) into individual parameters - TypeScript SDK doesn't have
flattenRequestsoption like Python
Recommendation
- Remove the global parameter from
speakeasy-modifications-overlay.yamlto maketenantIdrequired - Set
maxMethodParams: 10to enable parameter flattening - Keep
requestBodyFieldName: "params"for consistent naming - Accept the trade-off: JWT users must pass
tenantIdexplicitly (validated by API)
This gives us the best possible structure:
await outpost.destinations.create("acme-corp", {
type: "webhook",
topics: ["order.created"],
config: { url: "https://acme.com/webhooks" },
});Alternative Approaches (Not Recommended)
- Custom Code Regions: Could manually flatten parameters, but requires maintenance across all SDKs and breaks on regeneration
- Keep Global Parameter: Would maintain JWT convenience but prevent
tenantIdfrom being first parameter - Feature Request to Speakeasy: Request TypeScript support for request body field flattening similar to Python
Questions for Discussion
- Should we prioritize JWT convenience (optional
tenantId) or better parameter ordering (requiredtenantId)? - Is the trade-off of requiring JWT users to pass
tenantIdexplicitly acceptable? - Should we consider different SDK structures for different authentication methods?
- Are there other approaches we should consider?
Metadata
Metadata
Assignees
Labels
Type
Projects
Status