|
| 1 | +import Foundation |
| 2 | +import TurnkeyTypes |
| 3 | +import TurnkeyHttp |
| 4 | + |
| 5 | +extension TurnkeyContext { |
| 6 | + |
| 7 | + /// Fetches an existing user by P-256 API key public key, or creates a new one if none exists. |
| 8 | + /// |
| 9 | + /// - Parameters: |
| 10 | + /// - params: Params containing the P-256 public key and desired user/api key names. |
| 11 | + /// - organizationId: Optional organization override. Defaults to active session org. |
| 12 | + /// - Returns: The existing or newly created `v1User`. |
| 13 | + public func fetchOrCreateP256ApiKeyUser( |
| 14 | + params: CreateP256ApiKeyUserParams, |
| 15 | + organizationId: String? = nil |
| 16 | + ) async throws -> v1User { |
| 17 | + guard |
| 18 | + authState == .authenticated, |
| 19 | + let client = client |
| 20 | + else { |
| 21 | + throw TurnkeySwiftError.invalidSession |
| 22 | + } |
| 23 | + |
| 24 | + let orgId = organizationId ?? self.session?.organizationId |
| 25 | + guard let orgId else { |
| 26 | + throw TurnkeySwiftError.invalidSession |
| 27 | + } |
| 28 | + |
| 29 | + // attempt to find an existing user with this P-256 API key |
| 30 | + let usersResp = try await client.getUsers(TGetUsersBody(organizationId: orgId)) |
| 31 | + if let existing = usersResp.users.first(where: { user in |
| 32 | + user.apiKeys.contains(where: { apiKey in |
| 33 | + apiKey.credential.publicKey == params.publicKey && |
| 34 | + apiKey.credential.type == .credential_type_api_key_p256 |
| 35 | + }) |
| 36 | + }) { |
| 37 | + return existing |
| 38 | + } |
| 39 | + |
| 40 | + // not found, create a new user with this API key |
| 41 | + let userName = params.userName.isEmpty ? "Public Key User" : params.userName |
| 42 | + let apiKeyName = params.apiKeyName.isEmpty ? "public-key-user-\(params.publicKey)" : params.apiKeyName |
| 43 | + |
| 44 | + let apiKeyParams = v1ApiKeyParamsV2( |
| 45 | + apiKeyName: apiKeyName, |
| 46 | + curveType: .api_key_curve_p256, |
| 47 | + expirationSeconds: nil, |
| 48 | + publicKey: params.publicKey |
| 49 | + ) |
| 50 | + |
| 51 | + let createParamsV3 = v1UserParamsV3( |
| 52 | + apiKeys: [apiKeyParams], |
| 53 | + authenticators: [], |
| 54 | + oauthProviders: [], |
| 55 | + userEmail: nil, |
| 56 | + userName: userName, |
| 57 | + userPhoneNumber: nil, |
| 58 | + userTags: [] |
| 59 | + ) |
| 60 | + |
| 61 | + let createResp = try await client.createUsers(TCreateUsersBody( |
| 62 | + organizationId: orgId, |
| 63 | + users: [createParamsV3] |
| 64 | + )) |
| 65 | + |
| 66 | + guard let newUserId = createResp.userIds.first, !newUserId.isEmpty else { |
| 67 | + throw TurnkeySwiftError.invalidResponse |
| 68 | + } |
| 69 | + |
| 70 | + let userResp = try await client.getUser(TGetUserBody( |
| 71 | + organizationId: orgId, |
| 72 | + userId: newUserId |
| 73 | + )) |
| 74 | + return userResp.user |
| 75 | + } |
| 76 | + |
| 77 | + /// Fetches each requested policy if it exists, or creates it if it does not. |
| 78 | + /// |
| 79 | + /// This function is idempotent: |
| 80 | + /// - Multiple calls with the same `policies` will not create duplicates. |
| 81 | + /// - For every policy in the request: |
| 82 | + /// - If it already exists, it is returned with its `policyId`. |
| 83 | + /// - If it does not exist, it is created and returned with its new `policyId`. |
| 84 | + /// |
| 85 | + /// - Parameters: |
| 86 | + /// - policies: The list of policies to fetch or create. |
| 87 | + /// - organizationId: Optional organization override. Defaults to the current session's `organizationId`. |
| 88 | + /// - Returns: An array of items where each contains: |
| 89 | + /// - `policyId`: The unique identifier of the policy. |
| 90 | + /// - `policyName`: Human-readable name of the policy. |
| 91 | + /// - `effect`: The instruction to DENY or ALLOW an activity. |
| 92 | + /// - `condition`: Optional condition expression that triggers the effect. |
| 93 | + /// - `consensus`: Optional consensus expression that triggers the effect. |
| 94 | + /// - `notes`: Optional developer notes or description for the policy. |
| 95 | + /// - Throws: If there is no active session, if the input is invalid, |
| 96 | + /// if fetching existing policies fails, or if creating policies fails. |
| 97 | + public func fetchOrCreatePolicies( |
| 98 | + policies: [v1CreatePolicyIntentV3], |
| 99 | + organizationId: String? = nil |
| 100 | + ) async throws -> [Policy] { |
| 101 | + guard |
| 102 | + authState == .authenticated, |
| 103 | + let client = client |
| 104 | + else { |
| 105 | + throw TurnkeySwiftError.invalidSession |
| 106 | + } |
| 107 | + |
| 108 | + guard !policies.isEmpty else { |
| 109 | + throw TurnkeySwiftError.invalidConfiguration("'policies' must be a non-empty array of policy definitions.") |
| 110 | + } |
| 111 | + |
| 112 | + let orgId = organizationId ?? self.session?.organizationId |
| 113 | + guard let orgId else { |
| 114 | + throw TurnkeySwiftError.invalidSession |
| 115 | + } |
| 116 | + |
| 117 | + let existingResp = try await client.getPolicies(TGetPoliciesBody(organizationId: orgId)) |
| 118 | + let existing = existingResp.policies |
| 119 | + |
| 120 | + var existingBySignature: [String: String] = [:] // signature -> policyId |
| 121 | + for p in existing { |
| 122 | + existingBySignature[policySignature(p)] = p.policyId |
| 123 | + } |
| 124 | + |
| 125 | + var alreadyExisting: [Policy] = [] |
| 126 | + var missing: [v1CreatePolicyIntentV3] = [] |
| 127 | + |
| 128 | + for intent in policies { |
| 129 | + let sig = policySignature(intent) |
| 130 | + if let policyId = existingBySignature[sig] { |
| 131 | + alreadyExisting.append( |
| 132 | + .init( |
| 133 | + policyId: policyId, |
| 134 | + policyName: intent.policyName, |
| 135 | + effect: intent.effect, |
| 136 | + condition: intent.condition, |
| 137 | + consensus: intent.consensus, |
| 138 | + notes: intent.notes |
| 139 | + ) |
| 140 | + ) |
| 141 | + } else { |
| 142 | + missing.append(intent) |
| 143 | + } |
| 144 | + } |
| 145 | + |
| 146 | + if missing.isEmpty { |
| 147 | + return alreadyExisting |
| 148 | + } |
| 149 | + |
| 150 | + let createResp = try await client.createPolicies(TCreatePoliciesBody( |
| 151 | + organizationId: orgId, |
| 152 | + policies: missing |
| 153 | + )) |
| 154 | + |
| 155 | + let createdIds = createResp.policyIds |
| 156 | + guard createdIds.count == missing.count else { |
| 157 | + throw TurnkeySwiftError.invalidResponse |
| 158 | + } |
| 159 | + |
| 160 | + let newlyCreated: [Policy] = zip(missing, createdIds).map { (intent, policyId) in |
| 161 | + .init( |
| 162 | + policyId: policyId, |
| 163 | + policyName: intent.policyName, |
| 164 | + effect: intent.effect, |
| 165 | + condition: intent.condition, |
| 166 | + consensus: intent.consensus, |
| 167 | + notes: intent.notes |
| 168 | + ) |
| 169 | + } |
| 170 | + |
| 171 | + return alreadyExisting + newlyCreated |
| 172 | + } |
| 173 | + |
| 174 | + // MARK: - Helpers |
| 175 | + private func policySignature(_ policy: v1Policy) -> String { |
| 176 | + [ |
| 177 | + policy.policyName, |
| 178 | + policy.effect.rawValue, |
| 179 | + policy.condition, |
| 180 | + policy.consensus, |
| 181 | + policy.notes |
| 182 | + ].joined(separator: "|") |
| 183 | + } |
| 184 | + |
| 185 | + private func policySignature(_ intent: v1CreatePolicyIntentV3) -> String { |
| 186 | + [ |
| 187 | + intent.policyName, |
| 188 | + intent.effect.rawValue, |
| 189 | + intent.condition ?? "", |
| 190 | + intent.consensus ?? "", |
| 191 | + intent.notes ?? "" |
| 192 | + ].joined(separator: "|") |
| 193 | + } |
| 194 | +} |
| 195 | + |
| 196 | + |
0 commit comments