Skip to content

feat(swift): type OIDCClient by the spec's provider names - #902

Open
thisisabhash wants to merge 7 commits into
mainfrom
feat/swift-oidc-provider-enum
Open

thisisabhash wants to merge 7 commits into
mainfrom
feat/swift-oidc-provider-enum

Conversation

@thisisabhash

@thisisabhash thisisabhash commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

Problem

OIDCClient.signIn(provider:relayTo:launcher:) on Swift takes a raw String, so a typo in a provider name compiles and only fails at runtime with OIDCError.unknownProvider. The TS client already types this: AuthOIDC.createApi() declares getClient() as OIDCClient<ProviderName<P>>, which TypeScript resolves to OIDCClient<'google' | 'github'>. The spec drops those names, because detectTransferable emits x-blocks-type-args only for a written <...>, and a namespace built in another module, such as createApi()'s, never has one.

Issue #, if available: none.

Changes

// Before
let client = try await blocks.oidcAuthApi.getClient()
let user = try await client.signIn(provider: "google", relayTo: "myapp://auth", launcher: launcher)

// After: getClient() returns OIDCClient<OidcAuthApi.GetClient.Provider>, so a typo, or another
// namespace's provider, doesn't compile, and the enum lists the providers
let user = try await client.signIn(provider: .google, relayTo: "myapp://auth", launcher: launcher)
for provider in OidcAuthApi.GetClient.Provider.allCases { ... }

Spec generator (@aws-blocks/core). Without written type args, an oidc/client now also emits the provider names the type checker infers, as a string enum:

{ "x-blocks-transferable": "oidc/client", "x-blocks-type-args": [{ "type": "string", "enum": ["google", "github"] }] }

It emits them only when they resolve to string literals. Provider names that resolve to plain string, any or an unresolved type parameter emit nothing. generate-spec.ts is unchanged: an untitled enum stays inline and isn't moved into components.schemas.

The inference is limited to oidc/client on purpose. An earlier version applied it to every transferable, and review showed what that breaks:

  • An inferred realtime message type of any becomes {}. That crashes Kotlin codegen, and Swift turns it into an empty struct that drops every payload.
  • Inferred primitive messages generate Dart that doesn't compile.
  • Third-party bindings declared with genericArity: 0 fail arity validation.

Written type args behave exactly as on main.

Swift runtime and codegen (aws-blocks-swift). OIDCClient is now generic over the providers it signs in with, the way RealtimeChannel<T> is over its message type:

  • public actor OIDCClient<Provider: OIDCProviderName>. Provider is phantom: it only types signIn(provider:relayTo:launcher:), which sends provider.providerName. fromJSON and everything else are unchanged.
  • OIDCProviderName is a new protocol with var providerName: String. A String-backed enum gets it from rawValue, and String conforms, so an untyped spec's client is OIDCClient<String> and still signs in with a string.
  • A direct or optional oidc/client result whose first type arg is a string enum generates public enum Provider: String, CaseIterable, Codable, BlocksRuntime.OIDCProviderName in the operation's enum, and getClient() returns and hydrates OIDCClient<GetClient.Provider>. Without the type arg it returns OIDCClient<String>.
  • A nested oidc/client still decodes as JSONValue and gets no enum, inline or in a model.

How the names are made safe:

  • The enum is named ResultProvider, with a swift: warning, when the operation's own types declare or reference Provider: a parameter type, a root union's variant struct, or a model one of them names. A Provider model elsewhere in the spec doesn't rename it.
  • Case names are camelCased, made legal, and backticked when they're keywords. login.gov becomes case loginGov = "login.gov", and duplicates get numbered.

Breaking (Swift): a bare OIDCClient in a type position no longer compiles. Write OIDCClient<String>, or OIDCClient<…GetClient.Provider> for a typed spec, including for its static members (OIDCClient<String>.generateVerifier()). Swift has no default type parameters, so there's no non-breaking spelling of the generic.

Kotlin and Dart codegen. Both now ignore an oidc/client's type args, so their output is unchanged from main. Without this guard, the new provider names would break them:

  • Kotlin turned the names into an enum nothing uses, and code generation threw IllegalArgumentException: Can't escape identifier 'Login.gov' for a provider named login.gov.
  • Dart emitted constants such as default and okta:corp, which don't compile.

Their typed signIn is follow-up work.

What to review closely:

  • inferredProviderNames in packages/core/src/scripts/extract-ts-types.ts.
  • OIDCClient/OIDCProviderName in BlocksRuntime/OIDC/OIDCClient.swift.
  • oidcProviderArgs and referencedTypeNames in CodegenModelBuilder.swift.
  • The new SwiftCodeGenerator+OIDCProviders.swift, and the oidc/client branches in TransferableEmission.swift and SwiftCodeGenerator.swift.

Behavior and limitations reviewers should know:

  • Whether an app gets an enum depends on TypeScript inference. Providers passed through a separate as const array keep their literals, as in test-apps/comprehensive. An inline [stubIdp({ name: 'google', onAuthorize: (req) => ... })] widens to StubProvider<string>, so test-apps/native-bindings (the native E2E app) stays untyped and gets OIDCClient<String>.
  • Enum case order follows TypeScript's union order.

Fixture 37-oidc-provider-enum covers all three platforms, including my-sso and login.gov. It's numbered after #854's 28–36 so the two don't collide.

Validation

  • swift test --filter "BlocksCodegenTests|BlocksRuntimeTests|BlocksGeneratedCodeTests|BlocksGeneratedMainActorTests": 341 pass on top of main with feat(swift): emit typed transferable results for declared native bindings #807 merged. This includes:
    • generator tests for the enum, the typed and String clients, nested and in-model clients, optional results, case-name escaping, the ResultProvider fallback and its warning, a Provider model elsewhere in the spec not renaming the enum, and the rename terminating for a name with no PascalCase part;
    • the generated-code target binding OIDCClient<Auth.GetClient.Provider> from getClient() and signing in against a recording URLProtocol, with the right /authorize-params/my-sso path;
    • app-declared OIDCUser and BrowserLauncher types in the generated module;
    • a MainActor-default-isolation target signing in with an enum held in a main-actor model.
  • The Swift E2E sources (Tests/BlocksE2ETests) compile against code generated from test-apps/native-bindings' spec.
  • packages/core: the extractor, generate-spec, native-catalog and spec validation tests pass (117). New tests cover:
    • the AuthOIDC.createApi() pattern (typed, plain string, any);
    • inferred realtime args staying out of the spec. This test fails if the oidc/client gate is removed; I checked with a mutation;
    • the emitted spec shape.
  • Kotlin :codegen:check passes (130 tests), and so does the Dart blocks_codegen suite (185 tests), on Dart 3.11 with dart format clean.
  • Specs regenerated for 13 test apps and compared with main. The only difference is comprehensive's oidcAuthApi.getClient, which gains ["google", "corporate"]. The Swift output for that method compiles in Swift 6 with warnings as errors.
  • For every spec without an oidc/client, the Swift generator's output is byte-identical to main's, and no existing golden changed. An untyped oidc/client now returns OIDCClient<String>.
  • SwiftLint, SwiftFormat, and Biome lint show no new diagnostics.

Checklist

  • PR description included
  • Tests are changed or added
  • Relevant documentation is changed or added (and PR referenced):
    • docs: docs/native-clients/schema-generation-guide-for-devs.md, docs/native-clients/codegen-design.md, native/swift/README.md, native/swift/AGENTS.md
    • changesets: @aws-blocks/core and @aws-blocks/blocks, aws-blocks-swift, aws-blocks-kotlin
    • the Dart blocks_codegen changelog

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.

Without written type args, an oidc/client result now carries the provider
names the type checker infers, as a string enum. AuthOIDC.createApi() types
getClient() as OIDCClient<ProviderName<P>>, which resolves to the configured
names, so the spec gets e.g. [{ "type": "string", "enum": ["google", "github"] }].
Names that resolve to plain string or any emit nothing.

Inference stays limited to oidc/client: an inferred realtime message type can be
any or a primitive that native generators can't decode, and a declared binding's
arity check would reject inferred args. Written type args are unchanged.
The spec now fills an oidc/client's x-blocks-type-args with the app's provider
names. Neither generator uses them yet, and turning them into an unused enum
breaks codegen: Kotlin throws on a name such as login.gov, and Dart emits
constants such as default or okta:corp that don't compile. Both now drop the
type args, so the generated OidcClient result is unchanged.
A direct or optional oidc/client result whose first type arg is a string enum
generates a CaseIterable, Sendable Provider enum in the operation's enum
(e.g. OidcAuthApi.GetClient.Provider) and a signIn(provider:relayTo:launcher:)
overload on BlocksRuntime.OIDCClient that forwards its raw value, so
signIn(provider: .google, ...) checks the name at compile time. getClient()
still returns OIDCClient and the String overload stays.

The enum is named ResultProvider, with a swift: warning, when the method's own
types already use Provider. Case names are camelCased, made legal and
backticked when they're keywords. A nested oidc/client keeps decoding as
JSONValue with no enum.

Fixture 37-oidc-provider-enum covers all three platforms.
@thisisabhash
thisisabhash requested a review from a team as a code owner October 20, 2026 17:51
Resolve conflicts with #807, which moved the transferable helpers into
TransferableEmission.swift and routes emitted declarations through
emitDeclaration: the provider enum now goes through it too. Keep both
README feature bullets and AGENTS.md test rows.
@changeset-bot

changeset-bot Bot commented Oct 20, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 37dbd10

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 26 packages
Name Type
@aws-blocks/core Minor
@aws-blocks/blocks Minor
@aws-blocks/bb-logger Patch
@aws-blocks/bb-kv-store Patch
@aws-blocks/bb-distributed-table Patch
@aws-blocks/auth-common Patch
@aws-blocks/bb-app-setting Patch
@aws-blocks/bb-secret Patch
@aws-blocks/bb-data Patch
@aws-blocks/bb-distributed-data Patch
@aws-blocks/bb-auth-basic Patch
@aws-blocks/bb-auth-cognito Patch
@aws-blocks/bb-auth-oidc Patch
@aws-blocks/bb-realtime Patch
@aws-blocks/bb-async-job Patch
@aws-blocks/bb-dashboard Patch
@aws-blocks/bb-cron-job Patch
@aws-blocks/bb-file-bucket Patch
@aws-blocks/bb-agent Patch
@aws-blocks/bb-knowledge-base Patch
@aws-blocks/bb-email-client Patch
@aws-blocks/bb-tracer Patch
@aws-blocks/bb-metrics Patch
@aws-blocks/bb-lambda-compute Patch
@aws-blocks/bb-container-compute Patch
@aws-blocks/bb-compute Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

Picks up #926's e2e-slot action and E2E job-selection test, which the PR's
workflows (run from the merge ref) need in the checked-out head.
…ntry point

main's Kotlin Blocks entry point (#704) changed generated namespace and
Servers files, so fixture 37's Kotlin goldens are regenerated. The typed and
untyped OIDC clients still generate the same Kotlin.

This branch had an error being deployed

1 failed (outdated) and 2 inactive deployments
agent-bench — 37dbd10e Deployed Oct 22, 2026 by thisisabhash via bench / summary #3258
e2e — 37dbd10e Deployed Oct 22, 2026 by thisisabhash via Swift E2E (sandbox) #3840
publish — ae2b42f2 Deployed Oct 20, 2026 by thisisabhash via E2E Amplify Interop (Smoke) #3650
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants