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
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
0DEE5DC12BB40643004894AD /* SwiftOpenAI in Frameworks */ = {isa = PBXBuildFile; productRef = 0DEE5DC02BB40643004894AD /* SwiftOpenAI */; };
0DF957842BB53BEF00DD2013 /* ServiceSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0DF957832BB53BEF00DD2013 /* ServiceSelectionView.swift */; };
0DF957862BB543F100DD2013 /* AIProxyIntroView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0DF957852BB543F100DD2013 /* AIProxyIntroView.swift */; };
22BD10FE2F084C8000D238BC /* ProxLockIntroView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22BD10FD2F084C8000D238BC /* ProxLockIntroView.swift */; };
7B029E372C6893FD0025681A /* ChatStructuredOutputProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B029E362C6893FD0025681A /* ChatStructuredOutputProvider.swift */; };
7B029E392C68940D0025681A /* ChatStructuredOutputDemoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B029E382C68940D0025681A /* ChatStructuredOutputDemoView.swift */; };
7B029E3C2C69BE990025681A /* ChatStructuredOutputToolProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B029E3B2C69BE990025681A /* ChatStructuredOutputToolProvider.swift */; };
Expand Down Expand Up @@ -95,6 +96,7 @@
/* Begin PBXFileReference section */
0DF957832BB53BEF00DD2013 /* ServiceSelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServiceSelectionView.swift; sourceTree = "<group>"; };
0DF957852BB543F100DD2013 /* AIProxyIntroView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AIProxyIntroView.swift; sourceTree = "<group>"; };
22BD10FD2F084C8000D238BC /* ProxLockIntroView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProxLockIntroView.swift; sourceTree = "<group>"; };
7B029E362C6893FD0025681A /* ChatStructuredOutputProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatStructuredOutputProvider.swift; sourceTree = "<group>"; };
7B029E382C68940D0025681A /* ChatStructuredOutputDemoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatStructuredOutputDemoView.swift; sourceTree = "<group>"; };
7B029E3B2C69BE990025681A /* ChatStructuredOutputToolProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatStructuredOutputToolProvider.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -417,6 +419,7 @@
7BBE7E922AFCC9300096A693 /* Vision */,
7BA788CE2AE23A48008825D5 /* ApiKeyIntroView.swift */,
7B50DD272C2A9A390070A64D /* LocalHostEntryView.swift */,
22BD10FD2F084C8000D238BC /* ProxLockIntroView.swift */,
0DF957852BB543F100DD2013 /* AIProxyIntroView.swift */,
7B436B952AE24A04003CE281 /* OptionsListView.swift */,
0DF957832BB53BEF00DD2013 /* ServiceSelectionView.swift */,
Expand Down Expand Up @@ -645,6 +648,7 @@
buildActionMask = 2147483647;
files = (
7BBE7EA92B02E8E50096A693 /* ChatMessageView.swift in Sources */,
22BD10FE2F084C8000D238BC /* ProxLockIntroView.swift in Sources */,
7BE802592D2878170080E06A /* ChatPredictedOutputDemoView.swift in Sources */,
7B7239AE2AF9FF0000646679 /* ChatFunctionsCallStreamProvider.swift in Sources */,
7B436BA12AE25958003CE281 /* ChatProvider.swift in Sources */,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,13 @@
ReferencedContainer = "container:SwiftOpenAIExample.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
<EnvironmentVariables>
<EnvironmentVariable
key = "PROXLOCK_DEVICE_CHECK_BYPASS"
value = "21015698-3654-44A7-A5C6-28919004C781"
isEnabled = "YES">
</EnvironmentVariable>
</EnvironmentVariables>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
//
// ProxLockIntroView.swift
// SwiftOpenAIExample
//
// Created by Morris Richman on 1/2/2026.
//

import SwiftOpenAI
import SwiftUI

struct ProxLockIntroView: View {
var body: some View {
NavigationStack {
VStack {
Spacer()
VStack(spacing: 24) {
TextField("Enter partial key", text: $partialKey)
TextField("Enter your association ID", text: $associationID)
}
.padding()
.textFieldStyle(.roundedBorder)

Text("You receive a partial key and association ID when you configure an app in the ProxLock dashboard")
.font(.caption)

NavigationLink(destination: OptionsListView(
openAIService: proxlockService,
options: OptionsListView.APIOption.allCases.filter { $0 != .localChat }))
{
Text("Continue")
.padding()
.padding(.horizontal, 48)
.foregroundColor(.white)
.background(
Capsule()
.foregroundColor(canProceed ? Color(red: 64 / 255, green: 195 / 255, blue: 125 / 255) : .gray.opacity(0.2)))
}
.disabled(!canProceed)
Spacer()
Group {
Text(
"ProxLock keeps your OpenAI API key secure. To configure ProxLock for your project, or to learn more about how it works, please see the docs ") +
Text("[here](https://docs.proxlock.dev).")
}
.font(.caption)
}
.padding()
.navigationTitle("ProxLock Configuration")
}
}

@State private var partialKey = ""
@State private var associationID = ""

private var canProceed: Bool {
!(partialKey.isEmpty || associationID.isEmpty)
}

private var proxlockService: OpenAIService {
OpenAIServiceFactory.service(proxLockPartialKey: partialKey, proxLockAssociationID: associationID)
}
}

#Preview {
ApiKeyIntroView()
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,19 @@ struct ServiceSelectionView: View {
}
}

NavigationLink(destination: ProxLockIntroView()) {
VStack(alignment: .leading) {
Text("ProxLock Service")
.padding(.bottom, 10)
Group {
Text(
"Use this service to test SwiftOpenAI functionality with requests proxied through ProxLock for key protection.")
}
.font(.caption)
.fontWeight(.light)
}
}

NavigationLink(destination: AIProxyIntroView()) {
VStack(alignment: .leading) {
Text("AIProxy Service")
Expand Down
10 changes: 6 additions & 4 deletions Sources/OpenAI/AIProxy/Endpoint+AIProxy.swift
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,9 @@ extension Endpoint {
async throws -> URLRequest
{
let finalPath = path(in: openAIEnvironment)
var request = URLRequest(url: urlComponents(serviceURL: openAIEnvironment.baseURL, path: finalPath, queryItems: queryItems)
.url!)
var request = URLRequest(
url: urlComponents(serviceURL: openAIEnvironment.baseURL, path: finalPath, queryItems: queryItems)
.url!)
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
request.addValue(aiproxyPartialKey, forHTTPHeaderField: "aiproxy-partial-key")
if let organizationID {
Expand Down Expand Up @@ -84,8 +85,9 @@ extension Endpoint {
async throws -> URLRequest
{
let finalPath = path(in: openAIEnvironment)
var request = URLRequest(url: urlComponents(serviceURL: openAIEnvironment.baseURL, path: finalPath, queryItems: queryItems)
.url!)
var request = URLRequest(
url: urlComponents(serviceURL: openAIEnvironment.baseURL, path: finalPath, queryItems: queryItems)
.url!)
request.httpMethod = method.rawValue
request.addValue(aiproxyPartialKey, forHTTPHeaderField: "aiproxy-partial-key")
if let organizationID {
Expand Down
150 changes: 150 additions & 0 deletions Sources/OpenAI/ProxLock/Endpoint+ProxLock.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
//
// Endpoint+ProxLock.swift
// SwiftOpenAI
//
// Created by Morris Richman on 1/2/26.
//

#if !os(Linux)
import DeviceCheck
import Foundation

extension Endpoint {
func request(
apiKey: Authorization,
assosiationID: String,
openAIEnvironment: OpenAIEnvironment,
organizationID: String?,
method: HTTPMethod,
params: Encodable? = nil,
queryItems: [URLQueryItem] = [],
betaHeaderField: String? = nil,
extraHeaders: [String: String]? = nil)
async throws -> URLRequest
{
let finalPath = path(in: openAIEnvironment)
let components = urlComponents(base: openAIEnvironment.baseURL, path: finalPath, queryItems: queryItems)
guard let url = components.url else {
throw URLError(.badURL)
}
var request = URLRequest(url: url)
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
request.addValue("Bearer %ProxLock_PARTIAL_KEY:\(apiKey.value)%", forHTTPHeaderField: "Authorization")
if let organizationID {
request.addValue(organizationID, forHTTPHeaderField: "OpenAI-Organization")
}
if let betaHeaderField {
request.addValue(betaHeaderField, forHTTPHeaderField: "OpenAI-Beta")
}
if let extraHeaders {
for header in extraHeaders {
request.addValue(header.value, forHTTPHeaderField: header.key)
}
}
request.httpMethod = method.rawValue
if let params {
request.httpBody = try JSONEncoder().encode(params)
}

request = try await processURLRequest(request, associationID: assosiationID)
return request
}

func multiPartRequest(
apiKey: Authorization,
assosiationID: String,
openAIEnvironment: OpenAIEnvironment,
organizationID: String?,
method: HTTPMethod,
params: MultipartFormDataParameters,
queryItems: [URLQueryItem] = [])
async throws -> URLRequest
{
let finalPath = path(in: openAIEnvironment)
let components = urlComponents(base: openAIEnvironment.baseURL, path: finalPath, queryItems: queryItems)
guard let url = components.url else {
throw URLError(.badURL)
}
var request = URLRequest(url: url)
request.httpMethod = method.rawValue
let boundary = UUID().uuidString
request.addValue("Bearer %ProxLock_PARTIAL_KEY:\(apiKey.value)%", forHTTPHeaderField: "Authorization")
if let organizationID {
request.addValue(organizationID, forHTTPHeaderField: "OpenAI-Organization")
}
request.addValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")
request.httpBody = params.encode(boundary: boundary)
request = try await processURLRequest(request, associationID: assosiationID)
return request
}

private func urlComponents(
base: String,
path: String,
queryItems: [URLQueryItem])
-> URLComponents
{
guard var components = URLComponents(string: base) else {
fatalError("Invalid base URL: \(base)")
}
components.path = path
if !queryItems.isEmpty {
components.queryItems = queryItems
}
return components
}
}

// MARK: - Private
/// Translates your `URLRequest` into an object for ProxLock.
///
/// - Important: This does not include any form of authorization header. To use the bearer token, simply call ``bearerToken`` where you would like the real token to be constructed.
private func processURLRequest(_ request: URLRequest, associationID: String) async throws -> URLRequest {
var request = request

guard let destinationURL = request.url, let destinationMethod = request.httpMethod else {
throw URLError(.badURL)
}

// Set proxy components
request.url = URL(string: "https://api.proxlock.dev/proxy")
request.httpMethod = "POST"

// Update headers
request.setValue(destinationURL.absoluteString, forHTTPHeaderField: "ProxLock_DESTINATION")
request.setValue("device-check", forHTTPHeaderField: "ProxLock_VALIDATION_MODE")
request.setValue(destinationMethod.uppercased(), forHTTPHeaderField: "ProxLock_HTTP_METHOD")
request.setValue(associationID, forHTTPHeaderField: "ProxLock_ASSOCIATION_ID")
if let deviceCheckToken = try await getDeviceCheckToken() {
request.setValue(deviceCheckToken.base64EncodedString(), forHTTPHeaderField: "X-Apple-Device-Token")
}

return request
}

/// Generated token used for Apple Device Check
private func getDeviceCheckToken() async throws -> Data? {
#if targetEnvironment(simulator)
guard let bypassToken = ProcessInfo.processInfo.environment["PROXLOCK_DEVICE_CHECK_BYPASS"] else {
throw DCError(.featureUnsupported)
}

return bypassToken.data(using: .utf8)
#else
guard DCDevice.current.isSupported else {
throw DCError(.featureUnsupported)
}

return try await withCheckedThrowingContinuation { continuation in
DCDevice.current.generateToken { token, error in
if let error {
continuation.resume(throwing: error)
return
}

continuation.resume(returning: token)
}
}
#endif
}
#endif
Loading