From adf7c2084379fd33063f18a237d55f2fe43a6ccf Mon Sep 17 00:00:00 2001 From: 7ttp <117663341+7ttp@users.noreply.github.com> Date: Tue, 4 Aug 2026 04:25:25 +0530 Subject: [PATCH] fix(api): resync sso provider response schemas with the upstream spec --- .../commands/sso/add/add.integration.test.ts | 3 +- .../sso/list/list.integration.test.ts | 29 +++---- .../sso/remove/remove.integration.test.ts | 4 +- .../sso/show/show.integration.test.ts | 3 +- .../sso/update/update.integration.test.ts | 13 ++- packages/api/scripts/openapi-overrides.json | 20 ----- packages/api/src/effect.unit.test.ts | 84 ++++++++++++------ packages/api/src/generated/contracts.ts | 85 +++++++------------ packages/api/src/generated/openapi.json | 61 +++---------- 9 files changed, 126 insertions(+), 176 deletions(-) diff --git a/apps/cli/src/legacy/commands/sso/add/add.integration.test.ts b/apps/cli/src/legacy/commands/sso/add/add.integration.test.ts index 1ad938e554..61b037cbc1 100644 --- a/apps/cli/src/legacy/commands/sso/add/add.integration.test.ts +++ b/apps/cli/src/legacy/commands/sso/add/add.integration.test.ts @@ -23,11 +23,10 @@ import { legacySsoAdd } from "./add.handler.ts"; const RESPONSE_PROVIDER = { id: "b5ae62f9-ef1d-4f11-a02b-731c8bbb11e8", saml: { - id: "saml-1", entity_id: "https://example.com", attribute_mapping: { keys: { a: { name: "xyz", default: 3 } } }, }, - domains: [{ id: "d1", domain: "example.com" }], + domains: [{ domain: "example.com" }], }; const tempRoot = useLegacyTempWorkdir("supabase-sso-add-int-"); diff --git a/apps/cli/src/legacy/commands/sso/list/list.integration.test.ts b/apps/cli/src/legacy/commands/sso/list/list.integration.test.ts index cc926f8219..e50654461e 100644 --- a/apps/cli/src/legacy/commands/sso/list/list.integration.test.ts +++ b/apps/cli/src/legacy/commands/sso/list/list.integration.test.ts @@ -16,10 +16,12 @@ import { withJsonErrorHandling } from "../../../../shared/output/json-error-hand import { EventUpgradeSuggested } from "../../../../shared/telemetry/event-catalog.ts"; import { legacySsoList } from "./list.handler.ts"; +// Mirrors what the Management API returns: neither `saml.id` nor +// `domains[].id` is part of the provider response (nor of Go's +// `api.ListProvidersResponse`). const PROVIDER_ITEM = { id: "0b0d48f6-878b-4190-88d7-2ca33ed800bc", saml: { - id: "8682fcf4-4056-455c-bd93-f33295604929", entity_id: "https://example.com", metadata_url: "https://example.com", metadata_xml: '', @@ -27,7 +29,6 @@ const PROVIDER_ITEM = { }, domains: [ { - id: "9484591c-a203-4500-bea7-d0aaa845e2f5", domain: "example.com", created_at: "2023-03-28T13:50:14.464Z", updated_at: "2023-03-28T13:50:14.464Z", @@ -37,16 +38,6 @@ const PROVIDER_ITEM = { updated_at: "2023-03-28T13:50:14.464Z", }; -const PROVIDER_ITEM_WITHOUT_SAML_ID = { - ...PROVIDER_ITEM, - saml: { - entity_id: "https://example.com", - metadata_url: "https://example.com", - metadata_xml: '', - attribute_mapping: { keys: { a: { name: "xyz", default: 3 } } }, - }, -}; - const tempRoot = useLegacyTempWorkdir("supabase-sso-list-int-"); interface SetupOpts { @@ -161,12 +152,20 @@ describe("legacy sso list integration", () => { }).pipe(Effect.provide(layer)); }); - it.live("lists providers when the API omits items[].saml.id", () => { - const { layer, out } = setup({ body: { items: [PROVIDER_ITEM_WITHOUT_SAML_ID] } }); + // Some projects still echo the nested IDs the spec dropped. Go ignores them + // (no struct field), so they must neither break decoding nor reach `-o json`. + it.live("ignores nested saml.id / domains[].id when the API still sends them", () => { + const item = { + ...PROVIDER_ITEM, + saml: { ...PROVIDER_ITEM.saml, id: "8682fcf4-4056-455c-bd93-f33295604929" }, + domains: [{ ...PROVIDER_ITEM.domains[0], id: "9484591c-a203-4500-bea7-d0aaa845e2f5" }], + }; + const { layer, out } = setup({ goOutput: "json", body: { items: [item] } }); return Effect.gen(function* () { yield* legacySsoList({ projectRef: Option.none() }); expect(out.stdoutText).toContain("0b0d48f6-878b-4190-88d7-2ca33ed800bc"); - expect(out.stdoutText).toContain("example.com"); + expect(out.stdoutText).not.toContain("8682fcf4-4056-455c-bd93-f33295604929"); + expect(out.stdoutText).not.toContain("9484591c-a203-4500-bea7-d0aaa845e2f5"); }).pipe(Effect.provide(layer)); }); diff --git a/apps/cli/src/legacy/commands/sso/remove/remove.integration.test.ts b/apps/cli/src/legacy/commands/sso/remove/remove.integration.test.ts index 1e667e10e5..5b6181e6e9 100644 --- a/apps/cli/src/legacy/commands/sso/remove/remove.integration.test.ts +++ b/apps/cli/src/legacy/commands/sso/remove/remove.integration.test.ts @@ -19,8 +19,8 @@ const VALID_PROVIDER_ID = "b5ae62f9-ef1d-4f11-a02b-731c8bbb11e8"; const PROVIDER = { id: VALID_PROVIDER_ID, - saml: { id: "x", entity_id: "https://example.com" }, - domains: [{ id: "d1", domain: "example.com" }], + saml: { entity_id: "https://example.com" }, + domains: [{ domain: "example.com" }], }; const tempRoot = useLegacyTempWorkdir("supabase-sso-remove-int-"); diff --git a/apps/cli/src/legacy/commands/sso/show/show.integration.test.ts b/apps/cli/src/legacy/commands/sso/show/show.integration.test.ts index 4cbd6de8a3..fef8e55034 100644 --- a/apps/cli/src/legacy/commands/sso/show/show.integration.test.ts +++ b/apps/cli/src/legacy/commands/sso/show/show.integration.test.ts @@ -19,12 +19,11 @@ const VALID_PROVIDER_ID = "b5ae62f9-ef1d-4f11-a02b-731c8bbb11e8"; const PROVIDER = { id: VALID_PROVIDER_ID, saml: { - id: "8682fcf4-4056-455c-bd93-f33295604929", entity_id: "https://example.com", metadata_url: "https://example.com", metadata_xml: '', }, - domains: [{ id: "d1", domain: "example.com" }], + domains: [{ domain: "example.com" }], created_at: "2023-03-28T13:50:14.464Z", updated_at: "2023-03-28T13:50:14.464Z", }; diff --git a/apps/cli/src/legacy/commands/sso/update/update.integration.test.ts b/apps/cli/src/legacy/commands/sso/update/update.integration.test.ts index 32de4a3237..b06d2a9dd8 100644 --- a/apps/cli/src/legacy/commands/sso/update/update.integration.test.ts +++ b/apps/cli/src/legacy/commands/sso/update/update.integration.test.ts @@ -24,17 +24,14 @@ const VALID_PROVIDER_ID = "b5ae62f9-ef1d-4f11-a02b-731c8bbb11e8"; const EXISTING_PROVIDER = { id: VALID_PROVIDER_ID, - saml: { id: "saml-1", entity_id: "https://example.com" }, - domains: [ - { id: "d1", domain: "old1.com" }, - { id: "d2", domain: "old2.com" }, - ], + saml: { entity_id: "https://example.com" }, + domains: [{ domain: "old1.com" }, { domain: "old2.com" }], }; const RESPONSE_PROVIDER = { id: VALID_PROVIDER_ID, - saml: { id: "saml-1", entity_id: "https://example.com" }, - domains: [{ id: "d3", domain: "new.com" }], + saml: { entity_id: "https://example.com" }, + domains: [{ domain: "new.com" }], }; const tempRoot = useLegacyTempWorkdir("supabase-sso-update-int-"); @@ -1332,7 +1329,7 @@ describe("legacy sso update integration", () => { const { layer, api } = setup({ getBody: { ...EXISTING_PROVIDER, - domains: [{ id: "d1", domain: "" }, { id: "d2", domain: "old1.com" }, { id: "d3" }], + domains: [{ domain: "" }, { domain: "old1.com" }, {}], }, }); return Effect.gen(function* () { diff --git a/packages/api/scripts/openapi-overrides.json b/packages/api/scripts/openapi-overrides.json index 8cdf59f4ae..734bc078db 100644 --- a/packages/api/scripts/openapi-overrides.json +++ b/packages/api/scripts/openapi-overrides.json @@ -1,24 +1,4 @@ [ - { - "op": "test", - "path": "/components/schemas/ListProvidersResponse/properties/items/items/properties/saml/required", - "value": ["id", "entity_id"] - }, - { - "op": "replace", - "path": "/components/schemas/ListProvidersResponse/properties/items/items/properties/saml/required", - "value": ["entity_id"] - }, - { - "op": "test", - "path": "/components/schemas/GetProviderResponse/properties/saml/required", - "value": ["id", "entity_id"] - }, - { - "op": "replace", - "path": "/components/schemas/GetProviderResponse/properties/saml/required", - "value": ["entity_id"] - }, { "op": "test", "path": "/components/schemas/CreateProviderResponse/properties/saml/properties/attribute_mapping/required", diff --git a/packages/api/src/effect.unit.test.ts b/packages/api/src/effect.unit.test.ts index 48fb7d4850..75f7380ee2 100644 --- a/packages/api/src/effect.unit.test.ts +++ b/packages/api/src/effect.unit.test.ts @@ -7,7 +7,13 @@ import * as HttpClientResponse from "effect/unstable/http/HttpClientResponse"; import type * as HttpClientRequest from "effect/unstable/http/HttpClientRequest"; import { makeApiClient, operationDefinitions } from "./effect.ts"; -import { V1GetASsoProviderOutput, V1ListAllSsoProviderOutput } from "./generated/contracts.ts"; +import { + V1CreateASsoProviderOutput, + V1DeleteASsoProviderOutput, + V1GetASsoProviderOutput, + V1ListAllSsoProviderOutput, + V1UpdateASsoProviderOutput, +} from "./generated/contracts.ts"; const textDecoder = new TextDecoder(); @@ -51,34 +57,64 @@ const config = { userAgent: "supabase-api/test", } as const; -describe("makeApiClient", () => { - test("decodes SSO provider responses with empty attribute mappings and no SAML ID", () => { - const provider = { - id: "0b0d48f6-878b-4190-88d7-2ca33ed800bc", - saml: { - entity_id: "https://example.com", - metadata_url: "https://example.com", - metadata_xml: '', - attribute_mapping: {}, +describe("SSO provider response contracts", () => { + // The provider payload the Management API actually returns: no `saml.id` and + // no `domains[].id` — neither field exists in the spec (or in the Go CLI's + // `api.ListProvidersResponse`). Every SSO subcommand decodes one of these + // five schemas, so a stale required-key here breaks the whole command family + // (supabase/cli#5475, #5589, #6051). + const SPARSE_PROVIDER = { + id: "0b0d48f6-878b-4190-88d7-2ca33ed800bc", + saml: { + entity_id: "https://example.com", + metadata_url: "https://example.com", + metadata_xml: '', + attribute_mapping: {}, + }, + domains: [ + { + domain: "example.com", + created_at: "2023-03-28T13:50:14.464Z", + updated_at: "2023-03-28T13:50:14.464Z", }, - domains: [ - { - id: "9484591c-a203-4500-bea7-d0aaa845e2f5", - domain: "example.com", - created_at: "2023-03-28T13:50:14.464Z", - updated_at: "2023-03-28T13:50:14.464Z", - }, - ], - created_at: "2023-03-28T13:50:14.464Z", - updated_at: "2023-03-28T13:50:14.464Z", - }; - + ], + created_at: "2023-03-28T13:50:14.464Z", + updated_at: "2023-03-28T13:50:14.464Z", + }; + + const SINGLE_PROVIDER_SCHEMAS = [ + V1GetASsoProviderOutput, + V1CreateASsoProviderOutput, + V1UpdateASsoProviderOutput, + V1DeleteASsoProviderOutput, + ]; + + test("decodes SSO provider responses without nested SAML and domain IDs", () => { expect(() => - Schema.decodeUnknownSync(V1ListAllSsoProviderOutput)({ items: [provider] }), + Schema.decodeUnknownSync(V1ListAllSsoProviderOutput)({ items: [SPARSE_PROVIDER] }), ).not.toThrow(); - expect(() => Schema.decodeUnknownSync(V1GetASsoProviderOutput)(provider)).not.toThrow(); + for (const schema of SINGLE_PROVIDER_SCHEMAS) { + expect(() => Schema.decodeUnknownSync(schema)(SPARSE_PROVIDER)).not.toThrow(); + } }); + test("drops nested SAML and domain IDs the spec no longer declares", () => { + const withLegacyIds = { + ...SPARSE_PROVIDER, + saml: { ...SPARSE_PROVIDER.saml, id: "8682fcf4-4056-455c-bd93-f33295604929" }, + domains: [{ ...SPARSE_PROVIDER.domains[0], id: "9484591c-a203-4500-bea7-d0aaa845e2f5" }], + }; + + // Go's structs have no such fields, so `encoding/json` never echoes them. + for (const schema of SINGLE_PROVIDER_SCHEMAS) { + const decoded = Schema.decodeUnknownSync(schema)(withLegacyIds); + expect(decoded.saml).not.toHaveProperty("id"); + expect(decoded.domains?.[0]).not.toHaveProperty("id"); + } + }); +}); + +describe("makeApiClient", () => { test("allows raw operations to override generated request headers", async () => { let accept: string | undefined; diff --git a/packages/api/src/generated/contracts.ts b/packages/api/src/generated/contracts.ts index d694679387..dc45a03138 100644 --- a/packages/api/src/generated/contracts.ts +++ b/packages/api/src/generated/contracts.ts @@ -826,7 +826,6 @@ export const V1CreateASsoProviderOutput = Schema.Struct({ id: Schema.String, saml: Schema.optionalKey( Schema.Struct({ - id: Schema.String, entity_id: Schema.String, metadata_url: Schema.optionalKey(Schema.String), metadata_xml: Schema.optionalKey(Schema.String), @@ -839,15 +838,12 @@ export const V1CreateASsoProviderOutput = Schema.Struct({ name: Schema.optionalKey(Schema.String), names: Schema.optionalKey(Schema.Array(Schema.String)), default: Schema.optionalKey( - Schema.Union( - [ - Schema.Struct({}), - Schema.Number.check(Schema.isFinite()), - Schema.String, - Schema.Boolean, - ], - { mode: "oneOf" }, - ), + Schema.Union([ + Schema.Struct({}), + Schema.Number.check(Schema.isFinite()), + Schema.String, + Schema.Boolean, + ]), ), array: Schema.optionalKey(Schema.Boolean), }), @@ -868,7 +864,6 @@ export const V1CreateASsoProviderOutput = Schema.Struct({ domains: Schema.optionalKey( Schema.Array( Schema.Struct({ - id: Schema.String, domain: Schema.optionalKey(Schema.String), created_at: Schema.optionalKey(Schema.String), updated_at: Schema.optionalKey(Schema.String), @@ -1188,7 +1183,6 @@ export const V1DeleteASsoProviderOutput = Schema.Struct({ id: Schema.String, saml: Schema.optionalKey( Schema.Struct({ - id: Schema.String, entity_id: Schema.String, metadata_url: Schema.optionalKey(Schema.String), metadata_xml: Schema.optionalKey(Schema.String), @@ -1201,15 +1195,12 @@ export const V1DeleteASsoProviderOutput = Schema.Struct({ name: Schema.optionalKey(Schema.String), names: Schema.optionalKey(Schema.Array(Schema.String)), default: Schema.optionalKey( - Schema.Union( - [ - Schema.Struct({}), - Schema.Number.check(Schema.isFinite()), - Schema.String, - Schema.Boolean, - ], - { mode: "oneOf" }, - ), + Schema.Union([ + Schema.Struct({}), + Schema.Number.check(Schema.isFinite()), + Schema.String, + Schema.Boolean, + ]), ), array: Schema.optionalKey(Schema.Boolean), }), @@ -1230,7 +1221,6 @@ export const V1DeleteASsoProviderOutput = Schema.Struct({ domains: Schema.optionalKey( Schema.Array( Schema.Struct({ - id: Schema.String, domain: Schema.optionalKey(Schema.String), created_at: Schema.optionalKey(Schema.String), updated_at: Schema.optionalKey(Schema.String), @@ -1664,7 +1654,6 @@ export const V1GetASsoProviderOutput = Schema.Struct({ id: Schema.String, saml: Schema.optionalKey( Schema.Struct({ - id: Schema.optionalKey(Schema.String), entity_id: Schema.String, metadata_url: Schema.optionalKey(Schema.String), metadata_xml: Schema.optionalKey(Schema.String), @@ -1677,15 +1666,12 @@ export const V1GetASsoProviderOutput = Schema.Struct({ name: Schema.optionalKey(Schema.String), names: Schema.optionalKey(Schema.Array(Schema.String)), default: Schema.optionalKey( - Schema.Union( - [ - Schema.Struct({}), - Schema.Number.check(Schema.isFinite()), - Schema.String, - Schema.Boolean, - ], - { mode: "oneOf" }, - ), + Schema.Union([ + Schema.Struct({}), + Schema.Number.check(Schema.isFinite()), + Schema.String, + Schema.Boolean, + ]), ), array: Schema.optionalKey(Schema.Boolean), }), @@ -1706,7 +1692,6 @@ export const V1GetASsoProviderOutput = Schema.Struct({ domains: Schema.optionalKey( Schema.Array( Schema.Struct({ - id: Schema.String, domain: Schema.optionalKey(Schema.String), created_at: Schema.optionalKey(Schema.String), updated_at: Schema.optionalKey(Schema.String), @@ -3775,7 +3760,6 @@ export const V1ListAllSsoProviderOutput = Schema.Struct({ id: Schema.String, saml: Schema.optionalKey( Schema.Struct({ - id: Schema.optionalKey(Schema.String), entity_id: Schema.String, metadata_url: Schema.optionalKey(Schema.String), metadata_xml: Schema.optionalKey(Schema.String), @@ -3788,15 +3772,12 @@ export const V1ListAllSsoProviderOutput = Schema.Struct({ name: Schema.optionalKey(Schema.String), names: Schema.optionalKey(Schema.Array(Schema.String)), default: Schema.optionalKey( - Schema.Union( - [ - Schema.Struct({}), - Schema.Number.check(Schema.isFinite()), - Schema.String, - Schema.Boolean, - ], - { mode: "oneOf" }, - ), + Schema.Union([ + Schema.Struct({}), + Schema.Number.check(Schema.isFinite()), + Schema.String, + Schema.Boolean, + ]), ), array: Schema.optionalKey(Schema.Boolean), }), @@ -3817,7 +3798,6 @@ export const V1ListAllSsoProviderOutput = Schema.Struct({ domains: Schema.optionalKey( Schema.Array( Schema.Struct({ - id: Schema.String, domain: Schema.optionalKey(Schema.String), created_at: Schema.optionalKey(Schema.String), updated_at: Schema.optionalKey(Schema.String), @@ -4589,7 +4569,6 @@ export const V1UpdateASsoProviderOutput = Schema.Struct({ id: Schema.String, saml: Schema.optionalKey( Schema.Struct({ - id: Schema.String, entity_id: Schema.String, metadata_url: Schema.optionalKey(Schema.String), metadata_xml: Schema.optionalKey(Schema.String), @@ -4602,15 +4581,12 @@ export const V1UpdateASsoProviderOutput = Schema.Struct({ name: Schema.optionalKey(Schema.String), names: Schema.optionalKey(Schema.Array(Schema.String)), default: Schema.optionalKey( - Schema.Union( - [ - Schema.Struct({}), - Schema.Number.check(Schema.isFinite()), - Schema.String, - Schema.Boolean, - ], - { mode: "oneOf" }, - ), + Schema.Union([ + Schema.Struct({}), + Schema.Number.check(Schema.isFinite()), + Schema.String, + Schema.Boolean, + ]), ), array: Schema.optionalKey(Schema.Boolean), }), @@ -4631,7 +4607,6 @@ export const V1UpdateASsoProviderOutput = Schema.Struct({ domains: Schema.optionalKey( Schema.Array( Schema.Struct({ - id: Schema.String, domain: Schema.optionalKey(Schema.String), created_at: Schema.optionalKey(Schema.String), updated_at: Schema.optionalKey(Schema.String), diff --git a/packages/api/src/generated/openapi.json b/packages/api/src/generated/openapi.json index 3ae234fd03..eed6a9bb90 100644 --- a/packages/api/src/generated/openapi.json +++ b/packages/api/src/generated/openapi.json @@ -19666,9 +19666,6 @@ "saml": { "type": "object", "properties": { - "id": { - "type": "string" - }, "entity_id": { "type": "string" }, @@ -19696,7 +19693,7 @@ } }, "default": { - "oneOf": [ + "anyOf": [ { "type": "object", "properties": {} @@ -19731,16 +19728,13 @@ ] } }, - "required": ["id", "entity_id"] + "required": ["entity_id"] }, "domains": { "type": "array", "items": { "type": "object", "properties": { - "id": { - "type": "string" - }, "domain": { "type": "string" }, @@ -19750,8 +19744,7 @@ "updated_at": { "type": "string" } - }, - "required": ["id"] + } } }, "created_at": { @@ -19777,9 +19770,6 @@ "saml": { "type": "object", "properties": { - "id": { - "type": "string" - }, "entity_id": { "type": "string" }, @@ -19807,7 +19797,7 @@ } }, "default": { - "oneOf": [ + "anyOf": [ { "type": "object", "properties": {} @@ -19849,9 +19839,6 @@ "items": { "type": "object", "properties": { - "id": { - "type": "string" - }, "domain": { "type": "string" }, @@ -19861,8 +19848,7 @@ "updated_at": { "type": "string" } - }, - "required": ["id"] + } } }, "created_at": { @@ -19887,9 +19873,6 @@ "saml": { "type": "object", "properties": { - "id": { - "type": "string" - }, "entity_id": { "type": "string" }, @@ -19917,7 +19900,7 @@ } }, "default": { - "oneOf": [ + "anyOf": [ { "type": "object", "properties": {} @@ -19959,9 +19942,6 @@ "items": { "type": "object", "properties": { - "id": { - "type": "string" - }, "domain": { "type": "string" }, @@ -19971,8 +19951,7 @@ "updated_at": { "type": "string" } - }, - "required": ["id"] + } } }, "created_at": { @@ -20066,9 +20045,6 @@ "saml": { "type": "object", "properties": { - "id": { - "type": "string" - }, "entity_id": { "type": "string" }, @@ -20096,7 +20072,7 @@ } }, "default": { - "oneOf": [ + "anyOf": [ { "type": "object", "properties": {} @@ -20131,16 +20107,13 @@ ] } }, - "required": ["id", "entity_id"] + "required": ["entity_id"] }, "domains": { "type": "array", "items": { "type": "object", "properties": { - "id": { - "type": "string" - }, "domain": { "type": "string" }, @@ -20150,8 +20123,7 @@ "updated_at": { "type": "string" } - }, - "required": ["id"] + } } }, "created_at": { @@ -20172,9 +20144,6 @@ "saml": { "type": "object", "properties": { - "id": { - "type": "string" - }, "entity_id": { "type": "string" }, @@ -20202,7 +20171,7 @@ } }, "default": { - "oneOf": [ + "anyOf": [ { "type": "object", "properties": {} @@ -20237,16 +20206,13 @@ ] } }, - "required": ["id", "entity_id"] + "required": ["entity_id"] }, "domains": { "type": "array", "items": { "type": "object", "properties": { - "id": { - "type": "string" - }, "domain": { "type": "string" }, @@ -20256,8 +20222,7 @@ "updated_at": { "type": "string" } - }, - "required": ["id"] + } } }, "created_at": {