Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .changeset/inferred-transferable-type-args.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
"@aws-blocks/core": minor
"@aws-blocks/blocks": minor
---

feat(core): emit an OIDC client's inferred provider names

`blocks-generate-spec` now writes `x-blocks-type-args` for an `oidc/client` result
from the provider names the type checker infers, including for a namespace built
in another module. `AuthOIDC.createApi()`'s `getClient()`, typed
`OIDCClient<ProviderName<P>>`, now carries the configured provider names as a
string enum, for example `[{ "type": "string", "enum": ["google", "github"] }]`,
so native clients can generate a provider type. Provider names that resolve to
plain `string` or `any` emit no type args. Other transferables are unchanged.

`@aws-blocks/blocks` gets the same bump because it re-exports `@aws-blocks/core/scripts`.
2 changes: 1 addition & 1 deletion docs/native-clients/codegen-design.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ The OpenRPC document is emitted as a single self-contained file (`blocks.spec.js

### Blocks-specific extensions

Two `x-blocks-*` extension fields cover the gaps where standard OpenRPC doesn't capture Blocks semantics: `x-blocks-transferable` and `x-blocks-type-args` (on result schemas for Realtime / FileBucket handles).
Two `x-blocks-*` extension fields cover the gaps where standard OpenRPC doesn't capture Blocks semantics: `x-blocks-transferable` and `x-blocks-type-args` (on result schemas for Realtime / FileBucket / OIDC handles).

The full reference, including the tag table for native codegen authors, lives in [`schema-generation-guide-for-devs.md` § Spec extensions reference](./schema-generation-guide-for-devs.md#spec-extensions-reference).

Expand Down
17 changes: 15 additions & 2 deletions docs/native-clients/schema-generation-guide-for-devs.md
Original file line number Diff line number Diff line change
Expand Up @@ -399,7 +399,7 @@ Tags currently emitted:
| `realtime/channel` | `RealtimeChannel<T>` (where `T` comes from `x-blocks-type-args[0]`) | `bb-realtime` |
| `file-bucket/download` | `FileDownloadHandle` | `bb-file-bucket` |
| `file-bucket/upload` | `FileUploadHandle` | `bb-file-bucket` |
| `oidc/client` | `OidcClient` (`OIDCClient` on Swift) | `bb-auth-oidc` |
| `oidc/client` | `OidcClient` (`OIDCClient<P>` on Swift, where `P` is the provider enum from `x-blocks-type-args[0]`, or `String`); `x-blocks-type-args[0]`, when the emitter infers it, is a string enum of provider names | `bb-auth-oidc` |

**Codegen contract.** When `x-blocks-transferable` is present on a result schema, ignore any other properties on the schema and emit the SDK type for that tag. The platform SDK is responsible for hydrating the descriptor returned over the wire into a live handle.

Expand All @@ -418,7 +418,20 @@ Carries the JSON Schema for each generic type argument of the SDK type, in decla
}
```

Each entry is a valid JSON Schema and may use `$ref` against `components.schemas`. For non-generic transferables (`file-bucket/download`, `file-bucket/upload`, `oidc/client` today), this field is omitted.
Each entry is a valid JSON Schema and may use `$ref` against `components.schemas`. The emitter writes the field when a method's return type spells out the arguments, on a namespace the entry file exports directly (`export const api = new ApiNamespace(...)`). It's omitted for non-generic transferables (`file-bucket/download`, `file-bucket/upload`), for a `RealtimeChannel` whose message type isn't written in the return type, and for every method of a namespace built elsewhere, such as a block's `createApi()` or a factory function.

The one exception is an `oidc/client`, which carries the provider names the type checker infers, as a string enum, on either path. `AuthOIDC.createApi()` types `getClient()` as `OIDCClient<ProviderName<P>>`, which the checker resolves from the configured providers. Provider names that resolve to plain `string` (or `any`) emit no type args:

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

Swift turns it into a `Provider` enum in the method's namespace (`OidcAuthApi.GetClient.Provider`, or `ResultProvider` with a `swift:` warning when the method's own types already use that name) and returns `OIDCClient<OidcAuthApi.GetClient.Provider>`, whose `signIn(provider:relayTo:launcher:)` takes only those providers. Without the field, the client is `OIDCClient<String>`. Kotlin and Dart ignore it for now.

### `x-blocks-native-packages` (document root)

Expand Down
49 changes: 49 additions & 0 deletions native/codegen-fixtures/37-oidc-provider-enum/dart/client.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 20 additions & 0 deletions native/codegen-fixtures/37-oidc-provider-enum/kotlin/Auth.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.example.app

import com.aws.blocks.kotlin.Blocks
import com.aws.blocks.kotlin.BlocksClient
import kotlin.Deprecated
import kotlin.DeprecationLevel
import kotlin.Nothing

public class Auth(
private val client: BlocksClient,
) {
@Deprecated(
message = "OIDC is not configured. Add oidc { relayTo = \"...\" } to your awsBlocks block to enable this method.",
level = DeprecationLevel.ERROR,
)
public suspend fun getClient(): Nothing = throw NotImplementedError("OIDC is not configured. Add oidc { relayTo = \"...\" } to your awsBlocks block to enable this method.")
}

public val Blocks.auth: Auth
get() = Auth(client())
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.example.app

import com.aws.blocks.kotlin.Blocks
import com.aws.blocks.kotlin.BlocksClient
import kotlin.Deprecated
import kotlin.DeprecationLevel
import kotlin.Nothing

public class LegacyAuth(
private val client: BlocksClient,
) {
@Deprecated(
message = "OIDC is not configured. Add oidc { relayTo = \"...\" } to your awsBlocks block to enable this method.",
level = DeprecationLevel.ERROR,
)
public suspend fun getClient(): Nothing = throw NotImplementedError("OIDC is not configured. Add oidc { relayTo = \"...\" } to your awsBlocks block to enable this method.")
}

public val Blocks.legacyAuth: LegacyAuth
get() = LegacyAuth(client())
11 changes: 11 additions & 0 deletions native/codegen-fixtures/37-oidc-provider-enum/kotlin/Servers.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.example.app

import com.aws.blocks.kotlin.Blocks
import com.aws.blocks.kotlin.BlocksServer

public object Servers {
public val local: BlocksServer =
BlocksServer(name = "local", url = "http://localhost:3001/aws-blocks/api")
}

public operator fun Blocks.Companion.invoke(): Blocks = Blocks(Servers.local)
27 changes: 27 additions & 0 deletions native/codegen-fixtures/37-oidc-provider-enum/spec.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"openrpc": "1.3.2",
"info": { "title": "test", "version": "1.0.0" },
"methods": [
{
"name": "auth.getClient",
"params": [],
"result": {
"name": "GetClientResult",
"schema": {
"x-blocks-transferable": "oidc/client",
"x-blocks-type-args": [
{ "type": "string", "enum": ["google", "github", "my-sso", "login.gov"] }
]
}
}
},
{
"name": "legacyAuth.getClient",
"params": [],
"result": {
"name": "GetClientResult",
"schema": { "x-blocks-transferable": "oidc/client" }
}
}
]
}
99 changes: 99 additions & 0 deletions native/codegen-fixtures/37-oidc-provider-enum/swift/Api.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import Foundation
import BlocksRuntime

public final nonisolated class Auth: Sendable {
private let client: BlocksClient

public init(client: BlocksClient) {
self.client = client
}

@available(*, deprecated, message: "Use blocks.auth from one Blocks() or Blocks(server:) you hold")
public convenience init(server: BlocksServer = Servers.local) {
self.init(client: BlocksClient(server: server))
}

/// Calls `auth.getClient`.
public func getClient() async throws -> OIDCClient<GetClient.Provider> {
let request = BlocksRequest(method: "auth.getClient", params: [], id: BlocksRequest.nextId())
let result = try await client.execute(request)
guard let result else { throw RPCError(message: "Unexpected null result for auth.getClient") }
guard let descriptor = try JSONSerialization.jsonObject(with: result) as? [String: Any] else {
throw RPCError(message: "Invalid OIDC client descriptor for auth.getClient")
}
return try OIDCClient<GetClient.Provider>.fromJSON(descriptor, baseUrl: self.client.baseUrl, client: self.client)
}

public enum GetClient {

/// The providers `auth.getClient` signs in with.
public enum Provider: String, CaseIterable, Codable, BlocksRuntime.OIDCProviderName {
case google
case github
case mySso = "my-sso"
case loginGov = "login.gov"
}
}
}

public final nonisolated class LegacyAuth: Sendable {
private let client: BlocksClient

public init(client: BlocksClient) {
self.client = client
}

@available(*, deprecated, message: "Use blocks.legacyAuth from one Blocks() or Blocks(server:) you hold")
public convenience init(server: BlocksServer = Servers.local) {
self.init(client: BlocksClient(server: server))
}

/// Calls `legacyAuth.getClient`.
public func getClient() async throws -> OIDCClient<String> {
let request = BlocksRequest(method: "legacyAuth.getClient", params: [], id: BlocksRequest.nextId())
let result = try await client.execute(request)
guard let result else { throw RPCError(message: "Unexpected null result for legacyAuth.getClient") }
guard let descriptor = try JSONSerialization.jsonObject(with: result) as? [String: Any] else {
throw RPCError(message: "Invalid OIDC client descriptor for legacyAuth.getClient")
}
return try OIDCClient<String>.fromJSON(descriptor, baseUrl: self.client.baseUrl, client: self.client)
}
}


// MARK: - Blocks

/// One BlocksClient, so one URLSession (RPC and OIDC calls) and one token store, for every namespace in this spec;
/// file transfers and realtime use the shared runtime session. Hold one while the backend is in use: `let blocks = Blocks()`.
public final nonisolated class Blocks: Sendable {
/// The server every API calls.
public let server: BlocksRuntime.BlocksServer
/// The client every API runs on.
public let client: BlocksRuntime.BlocksClient

public let auth: Auth
public let legacyAuth: LegacyAuth

/// Defaults to the spec's first server; a `session` you pass stays yours.
public init(
server: BlocksRuntime.BlocksServer = Servers.local,
session: URLSession? = nil,
tokenStore: any BlocksRuntime.TokenStore = BlocksRuntime.InMemoryTokenStore()
) {
self.server = server
self.client = BlocksRuntime.BlocksClient(server: server, session: session, tokenStore: tokenStore)
self.auth = .init(client: client)
self.legacyAuth = .init(client: client)
}

/// Later calls throw ClientClosedError; calls in flight finish, then the session this instance created is released.
public func close() {
client.close()
}
}

// MARK: - Servers

public nonisolated enum Servers {
public static let local = BlocksServer(name: "local", url: "http://localhost:3001/aws-blocks/api")
}
Empty file.
4 changes: 4 additions & 0 deletions native/dart/packages/blocks_codegen/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
## Unreleased

- Ignore an `oidc/client` result's `x-blocks-type-args`, which
`blocks-generate-spec` now fills with the app's provider names, so the
generated `OidcClient` result is unchanged. Turning those names into an unused
enum emits constants such as `default` or `okta:corp` that don't compile.
- Resolve a direct result's transferable tag through the spec's
`x-blocks-native-packages` and `x-blocks-native-bindings` catalogs, emitting
the declared import, its export in upper camel case, and
Expand Down
10 changes: 7 additions & 3 deletions native/dart/packages/blocks_codegen/lib/src/builder.dart
Original file line number Diff line number Diff line change
Expand Up @@ -534,9 +534,13 @@ class CodegenModelBuilder {
TransferableRef(blocksType: final kt, typeArgs: final args) =>
TransferableType(
blocksType: kt,
typeArgs: args
.map((a) => _resolveTypeArgWithDedup(a, hint, path))
.toList(),
// An oidc/client's type argument names its providers, which the
// generated client doesn't use yet.
typeArgs: kt == 'oidc/client'
? const []
: args
.map((a) => _resolveTypeArgWithDedup(a, hint, path))
.toList(),
binding: _bindings[kt],
),
MapRef(valueType: final vt) => MapType(
Expand Down
8 changes: 8 additions & 0 deletions native/kotlin/.changeset/kotlin-oidc-type-args-ignored.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"aws-blocks-kotlin": patch
---

Ignore an `oidc/client` result's `x-blocks-type-args`, which `blocks-generate-spec`
now fills with the app's provider names, so the generated `OidcClient` result is
unchanged. Turning those names into an unused enum fails code generation for a
provider name such as `login.gov`.
Original file line number Diff line number Diff line change
Expand Up @@ -308,10 +308,15 @@ object OpenRpcParser {
// Handle x-blocks-transferable
schema["x-blocks-transferable"]?.let { transferableEl ->
val transferableName = transferableEl.jsonPrimitive.content
val typeArgs = schema["x-blocks-type-args"]?.jsonArray?.map { argEl ->
val argObj = argEl.jsonObject
resolveSchema(argObj, components)
} ?: emptyList()
// An oidc/client's type argument names its providers, which the generated client doesn't use yet.
val typeArgs = if (transferableName == "oidc/client") {
emptyList()
} else {
schema["x-blocks-type-args"]?.jsonArray?.map { argEl ->
val argObj = argEl.jsonObject
resolveSchema(argObj, components)
} ?: emptyList()
}
return TypeRef.Transferable(transferableName, typeArgs)
}

Expand Down
21 changes: 21 additions & 0 deletions native/swift/.changeset/swift-oidc-provider-enum.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
---
"aws-blocks-swift": minor
---

feat(swift): type `OIDCClient` by the spec's provider names

**Breaking:** `OIDCClient` is now generic over the providers
`signIn(provider:relayTo:launcher:)` takes. A direct or optional `oidc/client`
result whose `x-blocks-type-args[0]` is a string enum generates a `CaseIterable`
`Provider` enum in the method's namespace and returns
`OIDCClient<OidcAuthApi.GetClient.Provider>`, so `signIn(provider: .google, ...)`
accepts only that client's own providers and a typo doesn't compile. An untyped
spec returns `OIDCClient<String>`, whose `signIn` still takes a `String`. Write
`OIDCClient<String>` (or the generated enum) where code names `OIDCClient`,
including for its static members, as in `OIDCClient<String>.generateVerifier()`.
The new `OIDCProviderName` protocol is what a provider type conforms to.

When the method's own types already use the name `Provider`, the enum is
`ResultProvider` and codegen prints a `swift:` warning that names it. A nested
`oidc/client` still decodes as `JSONValue` and gets no enum. A namespace that
would generate a class named `CaseIterable` now takes an `Api` suffix.
2 changes: 1 addition & 1 deletion native/swift/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ A Swift Package that generates type-safe Swift client code from an OpenRPC speci
| Entry-point naming and emission | `Tests/BlocksCodegenTests/BlocksEntryPointGeneratorTests.swift` |
| Declared native binding resolution | `Tests/BlocksCodegenTests/DeclaredBindingParserTests.swift` |
| Declared transferable emission | `Tests/BlocksCodegenTests/DeclaredTransferableBindingTests.swift` |
| Golden files for the 27 shared fixtures | `Tests/BlocksCodegenTests/GoldenFileTests.swift` |
| Golden files for the 28 shared fixtures | `Tests/BlocksCodegenTests/GoldenFileTests.swift` |
| Hybrid-arm fixture spec | `../codegen-fixtures/18-hybrid-arm/spec.json` |
| Runtime tests | `Tests/BlocksRuntimeTests/*` |
| Build-plugin output compiled in Swift 6 language mode with `.treatAllWarnings(as: .error)`; any new compiler warning in generated code, such as one a runner Xcode update brings, fails CI | `Tests/BlocksGeneratedCodeTests/` (its `blocks.spec.json` is un-ignored in `.gitignore`) |
Expand Down
1 change: 1 addition & 0 deletions native/swift/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ Create one `Blocks` in app-scoped state, such as the Demo's `AppState`, not in a
- **Schema-ref reuse.** A `oneOf` variant referencing a component schema reuses that named type instead of inventing a duplicate struct.
- **Unbound transferables.** A direct result with an unbound transferable tag returns `UnknownTransferable`, a carrier for the raw `tag` and `descriptor`, instead of a raw `Data?` that did not type-check. Only bare direct results are covered.
- **Declared native packages.** When `x-blocks-native-bindings` binds the tag and its package has a `swift` field, generated source imports the declared module and returns that type via `<Type>.fromBlocksDescriptor`. See [the schema generation guide](../../docs/native-clients/schema-generation-guide-for-devs.md#x-blocks-native-bindings-document-root).
- **Typed OIDC providers.** `OIDCClient` is generic over the providers it signs in with. When the spec names an OIDC client's providers, the method's namespace gets a `CaseIterable` `Provider` enum and `getClient()` returns `OIDCClient<OidcAuthApi.GetClient.Provider>`, so `let user = try await client.signIn(provider: .google, relayTo: "myapp://auth", launcher: launcher)` accepts only that client's own providers. An untyped spec returns `OIDCClient<String>`, whose `signIn` takes a `String`. When the method's own types already use the name `Provider`, the enum is `ResultProvider` and codegen prints a `swift:` warning that names it.

## Configuration

Expand Down
Loading
Loading