diff --git a/Documentation/Extensions.md b/Documentation/Extensions.md index 4a02881..a5bdcd8 100644 --- a/Documentation/Extensions.md +++ b/Documentation/Extensions.md @@ -2,7 +2,8 @@ Foundation, value-type, string, collection, numeric, date, and crypto helpers, plus the `LCEssentials` namespace itself. UIKit extensions live in -[UIKit.md](UIKit.md); SwiftUI helpers in [SwiftUI.md](SwiftUI.md). +[UIKit.md](UIKit.md); SwiftUI helpers in [SwiftUI.md](SwiftUI.md); the +`LCFeatureControl` sub-product in [FeatureControl.md](FeatureControl.md). Every section is a collapsible block — click a heading to expand it. @@ -1760,12 +1761,24 @@ digest.map { String(format: "%02x", $0) }.joined()
-LCECryptoKitManager — OTP / peppered-login bridge (needs the LCECryptoKit binary) +LCECryptoKitManager — OTP / peppered-login bridge (separate LCECryptoKit product) -A thin facade over the optional `LCECryptoKit` binary product (enabled by the -`LCE_ENABLE_CRYPTO_BINARY` build flag). **When the binary is not linked every -method is a no-op** returning `nil` / `""` / `false`, so calling code still -compiles and runs. +Lives in its own SPM product, `LCECryptoKit` (target `LCECryptoKitManager`), not in +`LCEssentials` itself — an optional sub-package that target-depends on `LCEssentials`. +Add it explicitly alongside `LCEssentials` to a consumer's `Package.swift`: + +```swift +.product(name: "LCEssentials", package: "LCEssentials"), +.product(name: "LCECryptoKit", package: "LCEssentials"), +``` + +```swift +import LCECryptoKitManager +``` + +Wraps the vendored `LCECryptoKit.xcframework` (iOS device + simulator only — no +macOS/tvOS/watchOS slices). Consumers who don't link this product simply don't get +the symbol; there is no no-op fallback anymore. ### `init()` / `init(privateKey:)` diff --git a/Documentation/FeatureControl.md b/Documentation/FeatureControl.md new file mode 100644 index 0000000..c0223ea --- /dev/null +++ b/Documentation/FeatureControl.md @@ -0,0 +1,175 @@ +# LCEssentials — Feature Control + +A typed Swift client for Atomenta's Feature Control API: flag evaluation with a +TTL cache and safe-degrade fallback, an in-app notifications inbox, and batched +exposure telemetry. Lives in its own SPM product, `LCFeatureControl`, not in +`LCEssentials` itself — an optional sub-package that target-depends on +`LCEssentials` (same shape as [`LCECryptoKit`](Extensions.md), see its section +there for the sub-SPM rationale). + +Every section is a collapsible block — click a heading to expand it. + +## Contents + +- [Setup](#setup) +- [Evaluating flags](#evaluating-flags) +- [Notifications inbox](#notifications-inbox) +- [Exposure telemetry](#exposure-telemetry) +- [Errors](#errors) + +--- + +## Setup + +Add the product explicitly alongside `LCEssentials` to a consumer's `Package.swift`: + +```swift +.product(name: "LCEssentials", package: "LCEssentials"), +.product(name: "LCFeatureControl", package: "LCEssentials"), +``` + +```swift +import LCFeatureControl +``` + +
+FeatureControlConfiguration — wiring for one module deployment + +```swift +let configuration = FeatureControlConfiguration( + baseURL: "https://api.example.com", + environment: "production", + auth: FeatureControlBearerAuth { await session.currentJWT() } +) +``` + +| Property | Default | Notes | +| --- | --- | --- | +| `cacheTTL` | `45` seconds | In-memory client-side cache window | +| `requestTimeout` | `3` seconds | Per-request timeout budget | +| `evaluatePath` / `notificationsPath` / `telemetryPath` | `/api/feature-control/...` | Override only for a non-default BFF mount | + +
+ +
+Auth strategiesFeatureControlAuthorizing + +No `Atomenta-Token` (module token) type exists in this package on purpose — that +token must never be embedded in a customer-facing app. Pick one: + +```swift +// Internal/admin app hitting Atomenta directly with a panel-role JWT. +let auth = FeatureControlBearerAuth { await session.currentJWT() } + +// Customer app calling its own BFF, which enforces its own auth — +// adds exactly the given headers, never synthesizes Authorization. +let auth = FeatureControlHeaderAuth(headers: ["X-BFF-Session": sessionToken]) +``` + +
+ +## Evaluating flags + +
+FeatureControlManager — cache, safe-degrade, invalidation + +```swift +let manager = FeatureControlManager( + configuration: configuration, + defaults: ["fc.checkout_v2": FeatureControlFlag(enabled: false, variant: nil, + payload: nil, reason: "default")] +) + +let context = FeatureControlContext(subjectType: .customer, subjectId: user.id, + storeId: store.id, platform: "ios", + appVersion: appVersion) + +let snapshot = await manager.evaluate(keys: ["fc.checkout_v2"], context: context) +snapshot.flags["fc.checkout_v2"]?.enabled // Bool +``` + +`evaluate(keys:context:)` **never throws** — fresh fetch → stale cache → the +`defaults` passed at init, in that order. Use it everywhere a flag gates real +product behaviour. `evaluateOrThrow(keys:context:)` surfaces the real failure +instead, for admin/debug tooling only. + +Concurrent calls for the same key on a cold cache are coalesced onto a single +in-flight request — three callers evaluating the same key at once produce one +network call, not three. + +```swift +await manager.invalidateCache() // call on user/store change +``` + +The cache key is `environment + subjectId + sortedKeys + platform + appVersion` +— it does **not** include `storeId` or `attributes`. If a subject's flags can +legitimately differ by store, call `invalidateCache()` on store switch. + +
+ +## Notifications inbox + +
+FeatureControlNotificationsClient — JWT-only + +The OpenAPI fragment declares `security: [bearerAuth]` for every notifications +route, no module-token alternative — configure with `FeatureControlBearerAuth`. + +```swift +let client = FeatureControlNotificationsClient(configuration: configuration) + +let (items, nextCursor) = try await client.list(status: .unread, limit: 20) +let (more, _) = try await client.list(status: .unread, limit: 20, cursor: nextCursor) + +try await client.markRead(id: notification.id) +try await client.markAllRead() +``` + +
+ +## Exposure telemetry + +
+recordExposure(_:) / flushExposures() — best-effort, batched + +```swift +await manager.recordExposure(FeatureControlExposureEvent( + featureKey: "fc.checkout_v2", variant: "on", + subjectType: .customer, storeId: store.id +)) +// ... later, e.g. on app background or a periodic timer: +await manager.flushExposures() +``` + +Buffers locally — nothing is sent until `flushExposures()` is called. Splits +into batches of ≤100 (server `maxItems: 100`). Not a critical-path operation: a +failed batch is dropped, never retried indefinitely. + +
+ +## Errors + +
+FeatureControlError — from evaluateOrThrow and the notifications client + +```swift +public enum FeatureControlError: Error, Sendable, Equatable { + case invalidContext(code: String) // 400 + case unauthorized(code: String) // 401 + case forbidden(code: String) // 403 + case rateLimited(code: String) // 429 + case server(code: String, status: Int) + case transport(message: String) // decode failure / non-HTTP error +} +``` + +The safe `evaluate(...)` path never surfaces this — it degrades to cache/defaults +instead. `evaluateOrThrow` also throws `.server(code:status: 200)` if the server +responds `200` with an `{"error": true}` envelope (a degraded-but-200 response). + +
+ +--- + +See [Extensions.md](Extensions.md) for the rest of `LCEssentials`' Foundation +helpers, including the sibling `LCECryptoKit` sub-product. diff --git a/Frameworks/LCECryptoKit.xcframework/Info.plist b/Frameworks/LCECryptoKit.xcframework/Info.plist new file mode 100644 index 0000000..c628319 --- /dev/null +++ b/Frameworks/LCECryptoKit.xcframework/Info.plist @@ -0,0 +1,44 @@ + + + + + AvailableLibraries + + + BinaryPath + LCECryptoKit.framework/LCECryptoKit + LibraryIdentifier + ios-arm64_x86_64-simulator + LibraryPath + LCECryptoKit.framework + SupportedArchitectures + + arm64 + x86_64 + + SupportedPlatform + ios + SupportedPlatformVariant + simulator + + + BinaryPath + LCECryptoKit.framework/LCECryptoKit + LibraryIdentifier + ios-arm64 + LibraryPath + LCECryptoKit.framework + SupportedArchitectures + + arm64 + + SupportedPlatform + ios + + + CFBundlePackageType + XFWK + XCFrameworkFormatVersion + 1.0 + + diff --git a/Frameworks/LCECryptoKit.xcframework/ios-arm64/LCECryptoKit.framework/Headers/LCECryptoKit-Swift.h b/Frameworks/LCECryptoKit.xcframework/ios-arm64/LCECryptoKit.framework/Headers/LCECryptoKit-Swift.h new file mode 100644 index 0000000..d6185ab --- /dev/null +++ b/Frameworks/LCECryptoKit.xcframework/ios-arm64/LCECryptoKit.framework/Headers/LCECryptoKit-Swift.h @@ -0,0 +1,382 @@ +#if 0 +#elif defined(__arm64__) && __arm64__ +// Generated by Apple Swift version 6.3.3 effective-5.10 (swiftlang-6.3.3.1.3 clang-2100.1.1.101) +#ifndef LCECRYPTOKIT_SWIFT_H +#define LCECRYPTOKIT_SWIFT_H +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wgcc-compat" + +#if !defined(__has_include) +# define __has_include(x) 0 +#endif +#if !defined(__has_attribute) +# define __has_attribute(x) 0 +#endif +#if !defined(__has_feature) +# define __has_feature(x) 0 +#endif +#if !defined(__has_warning) +# define __has_warning(x) 0 +#endif + +#if __has_include() +# include +#endif + +#pragma clang diagnostic ignored "-Wauto-import" +#if defined(__OBJC__) +#include +#endif // defined(__OBJC__) +#if defined(__cplusplus) +#include +#include +#include +#include +#include +#include +#include +#else +#include +#include +#include +#include +#endif +#if defined(__cplusplus) +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wnon-modular-include-in-framework-module" +#if defined(__arm64e__) && __has_include() +# include +#else +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wreserved-macro-identifier" +# ifndef __ptrauth_swift_value_witness_function_pointer +# define __ptrauth_swift_value_witness_function_pointer(x) +# endif +# ifndef __ptrauth_swift_class_method_pointer +# define __ptrauth_swift_class_method_pointer(x) +# endif +#pragma clang diagnostic pop +#endif +#pragma clang diagnostic pop +#endif + +#if !defined(SWIFT_TYPEDEFS) +# define SWIFT_TYPEDEFS 1 +# if __has_include() +# include +# elif !defined(__cplusplus) +typedef unsigned char char8_t; +typedef uint_least16_t char16_t; +typedef uint_least32_t char32_t; +# endif +typedef float swift_float2 __attribute__((__ext_vector_type__(2))); +typedef float swift_float3 __attribute__((__ext_vector_type__(3))); +typedef float swift_float4 __attribute__((__ext_vector_type__(4))); +typedef double swift_double2 __attribute__((__ext_vector_type__(2))); +typedef double swift_double3 __attribute__((__ext_vector_type__(3))); +typedef double swift_double4 __attribute__((__ext_vector_type__(4))); +typedef int swift_int2 __attribute__((__ext_vector_type__(2))); +typedef int swift_int3 __attribute__((__ext_vector_type__(3))); +typedef int swift_int4 __attribute__((__ext_vector_type__(4))); +typedef unsigned int swift_uint2 __attribute__((__ext_vector_type__(2))); +typedef unsigned int swift_uint3 __attribute__((__ext_vector_type__(3))); +typedef unsigned int swift_uint4 __attribute__((__ext_vector_type__(4))); +#endif + +#if !defined(SWIFT_PASTE) +# define SWIFT_PASTE_HELPER(x, y) x##y +# define SWIFT_PASTE(x, y) SWIFT_PASTE_HELPER(x, y) +#endif +#if !defined(SWIFT_METATYPE) +# define SWIFT_METATYPE(X) Class +#endif +#if !defined(SWIFT_CLASS_PROPERTY) +# if __has_feature(objc_class_property) +# define SWIFT_CLASS_PROPERTY(...) __VA_ARGS__ +# else +# define SWIFT_CLASS_PROPERTY(...) +# endif +#endif +#if !defined(SWIFT_RUNTIME_NAME) +# if __has_attribute(objc_runtime_name) +# define SWIFT_RUNTIME_NAME(X) __attribute__((objc_runtime_name(X))) +# else +# define SWIFT_RUNTIME_NAME(X) +# endif +#endif +#if !defined(SWIFT_COMPILE_NAME) +# if __has_attribute(swift_name) +# define SWIFT_COMPILE_NAME(X) __attribute__((swift_name(X))) +# else +# define SWIFT_COMPILE_NAME(X) +# endif +#endif +#if !defined(SWIFT_METHOD_FAMILY) +# if __has_attribute(objc_method_family) +# define SWIFT_METHOD_FAMILY(X) __attribute__((objc_method_family(X))) +# else +# define SWIFT_METHOD_FAMILY(X) +# endif +#endif +#if !defined(SWIFT_NOESCAPE) +# if __has_attribute(noescape) +# define SWIFT_NOESCAPE __attribute__((noescape)) +# else +# define SWIFT_NOESCAPE +# endif +#endif +#if !defined(SWIFT_RELEASES_ARGUMENT) +# if __has_attribute(ns_consumed) +# define SWIFT_RELEASES_ARGUMENT __attribute__((ns_consumed)) +# else +# define SWIFT_RELEASES_ARGUMENT +# endif +#endif +#if !defined(SWIFT_WARN_UNUSED_RESULT) +# if __has_attribute(warn_unused_result) +# define SWIFT_WARN_UNUSED_RESULT __attribute__((warn_unused_result)) +# else +# define SWIFT_WARN_UNUSED_RESULT +# endif +#endif +#if !defined(SWIFT_NORETURN) +# if __has_attribute(noreturn) +# define SWIFT_NORETURN __attribute__((noreturn)) +# else +# define SWIFT_NORETURN +# endif +#endif +#if !defined(SWIFT_CLASS_EXTRA) +# define SWIFT_CLASS_EXTRA +#endif +#if !defined(SWIFT_PROTOCOL_EXTRA) +# define SWIFT_PROTOCOL_EXTRA +#endif +#if !defined(SWIFT_ENUM_EXTRA) +# define SWIFT_ENUM_EXTRA +#endif +#if !defined(SWIFT_CLASS) +# if __has_attribute(objc_subclassing_restricted) +# define SWIFT_CLASS(SWIFT_NAME) SWIFT_RUNTIME_NAME(SWIFT_NAME) __attribute__((objc_subclassing_restricted)) SWIFT_CLASS_EXTRA +# define SWIFT_CLASS_NAMED(SWIFT_NAME) __attribute__((objc_subclassing_restricted)) SWIFT_COMPILE_NAME(SWIFT_NAME) SWIFT_CLASS_EXTRA +# else +# define SWIFT_CLASS(SWIFT_NAME) SWIFT_RUNTIME_NAME(SWIFT_NAME) SWIFT_CLASS_EXTRA +# define SWIFT_CLASS_NAMED(SWIFT_NAME) SWIFT_COMPILE_NAME(SWIFT_NAME) SWIFT_CLASS_EXTRA +# endif +#endif +#if !defined(SWIFT_RESILIENT_CLASS) +# if __has_attribute(objc_class_stub) +# define SWIFT_RESILIENT_CLASS(SWIFT_NAME) SWIFT_CLASS(SWIFT_NAME) __attribute__((objc_class_stub)) +# define SWIFT_RESILIENT_CLASS_NAMED(SWIFT_NAME) __attribute__((objc_class_stub)) SWIFT_CLASS_NAMED(SWIFT_NAME) +# else +# define SWIFT_RESILIENT_CLASS(SWIFT_NAME) SWIFT_CLASS(SWIFT_NAME) +# define SWIFT_RESILIENT_CLASS_NAMED(SWIFT_NAME) SWIFT_CLASS_NAMED(SWIFT_NAME) +# endif +#endif +#if !defined(SWIFT_PROTOCOL) +# define SWIFT_PROTOCOL(SWIFT_NAME) SWIFT_RUNTIME_NAME(SWIFT_NAME) SWIFT_PROTOCOL_EXTRA +# define SWIFT_PROTOCOL_NAMED(SWIFT_NAME) SWIFT_COMPILE_NAME(SWIFT_NAME) SWIFT_PROTOCOL_EXTRA +#endif +#if !defined(SWIFT_EXTENSION) +# define SWIFT_EXTENSION(M) SWIFT_PASTE(M##_Swift_, __LINE__) +#endif +#if !defined(OBJC_DESIGNATED_INITIALIZER) +# if __has_attribute(objc_designated_initializer) +# define OBJC_DESIGNATED_INITIALIZER __attribute__((objc_designated_initializer)) +# else +# define OBJC_DESIGNATED_INITIALIZER +# endif +#endif +#if !defined(SWIFT_ENUM_ATTR) +# if __has_attribute(enum_extensibility) +# define SWIFT_ENUM_ATTR(_extensibility) __attribute__((enum_extensibility(_extensibility))) +# else +# define SWIFT_ENUM_ATTR(_extensibility) +# endif +#endif +#if !defined(SWIFT_ENUM) +# if (defined(__cplusplus) && __cplusplus >= 201103L) || (defined(__STDC_VERSION__) && __STDC_VERSION__ >= 202311L) || __has_feature(objc_fixed_enum) +# define SWIFT_ENUM(_type, _name, _extensibility) enum _name : _type _name; enum SWIFT_ENUM_ATTR(_extensibility) SWIFT_ENUM_EXTRA _name : _type +# if __has_feature(generalized_swift_name) +# define SWIFT_ENUM_NAMED(_type, _name, SWIFT_NAME, _extensibility) enum _name : _type _name SWIFT_COMPILE_NAME(SWIFT_NAME); enum SWIFT_COMPILE_NAME(SWIFT_NAME) SWIFT_ENUM_ATTR(_extensibility) SWIFT_ENUM_EXTRA _name : _type +# else +# define SWIFT_ENUM_NAMED(_type, _name, SWIFT_NAME, _extensibility) SWIFT_ENUM(_type, _name, _extensibility) +# endif +# else +# define SWIFT_ENUM(_type, _name, _extensibility) _type _name; enum +# define SWIFT_ENUM_NAMED(_type, _name, SWIFT_NAME, _extensibility) SWIFT_ENUM(_type, _name, _extensibility) +# endif +#endif +#if !defined(SWIFT_ENUM_TAG) +# if (defined(__cplusplus) && __cplusplus >= 201103L) || (defined(__STDC_VERSION__) && __STDC_VERSION__ >= 202311L) || __has_feature(objc_fixed_enum) +# define SWIFT_ENUM_TAG enum +# else +# define SWIFT_ENUM_TAG +# endif +#endif +#if !defined(SWIFT_ENUM_FWD_DECL) +# if (defined(__cplusplus) && __cplusplus >= 201103L) || (defined(__STDC_VERSION__) && __STDC_VERSION__ >= 202311L) || __has_feature(objc_fixed_enum) +# define SWIFT_ENUM_FWD_DECL(_type, _name) enum _name : _type; +# else +# define SWIFT_ENUM_FWD_DECL(_type, _name) typedef _type _name; +# endif +#endif +#if !defined(SWIFT_UNAVAILABLE) +# define SWIFT_UNAVAILABLE __attribute__((unavailable)) +#endif +#if !defined(SWIFT_UNAVAILABLE_MSG) +# define SWIFT_UNAVAILABLE_MSG(msg) __attribute__((unavailable(msg))) +#endif +#if !defined(SWIFT_AVAILABILITY) +# define SWIFT_AVAILABILITY(plat, ...) __attribute__((availability(plat, __VA_ARGS__))) +#endif +#if !defined(SWIFT_AVAILABILITY_DOMAIN) +# define SWIFT_AVAILABILITY_DOMAIN(dom, ...) __attribute__((availability(domain: dom, __VA_ARGS__))) +#endif +#if !defined(SWIFT_WEAK_IMPORT) +# define SWIFT_WEAK_IMPORT __attribute__((weak_import)) +#endif +#if !defined(SWIFT_DEPRECATED) +# define SWIFT_DEPRECATED __attribute__((deprecated)) +#endif +#if !defined(SWIFT_DEPRECATED_MSG) +# define SWIFT_DEPRECATED_MSG(...) __attribute__((deprecated(__VA_ARGS__))) +#endif +#if !defined(SWIFT_DEPRECATED_OBJC) +# if __has_feature(attribute_diagnose_if_objc) +# define SWIFT_DEPRECATED_OBJC(Msg) __attribute__((diagnose_if(1, Msg, "warning"))) +# else +# define SWIFT_DEPRECATED_OBJC(Msg) SWIFT_DEPRECATED_MSG(Msg) +# endif +#endif +#if defined(__OBJC__) +#if !defined(IBSegueAction) +# define IBSegueAction +#endif +#endif +#if !defined(SWIFT_EXTERN) +# if defined(__cplusplus) +# define SWIFT_EXTERN extern "C" +# else +# define SWIFT_EXTERN extern +# endif +#endif +#if !defined(SWIFT_CALL) +# define SWIFT_CALL __attribute__((swiftcall)) +#endif +#if !defined(SWIFT_INDIRECT_RESULT) +# define SWIFT_INDIRECT_RESULT __attribute__((swift_indirect_result)) +#endif +#if !defined(SWIFT_CONTEXT) +# define SWIFT_CONTEXT __attribute__((swift_context)) +#endif +#if !defined(SWIFT_ERROR_RESULT) +# define SWIFT_ERROR_RESULT __attribute__((swift_error_result)) +#endif +#if defined(__cplusplus) +# define SWIFT_NOEXCEPT noexcept +#else +# define SWIFT_NOEXCEPT +#endif +#if !defined(SWIFT_C_INLINE_THUNK) +# if __has_attribute(always_inline) +# if __has_attribute(nodebug) +# define SWIFT_C_INLINE_THUNK inline __attribute__((always_inline)) __attribute__((nodebug)) +# else +# define SWIFT_C_INLINE_THUNK inline __attribute__((always_inline)) +# endif +# else +# define SWIFT_C_INLINE_THUNK inline +# endif +#endif +#if defined(_WIN32) +#if !defined(SWIFT_IMPORT_STDLIB_SYMBOL) +# define SWIFT_IMPORT_STDLIB_SYMBOL __declspec(dllimport) +#endif +#else +#if !defined(SWIFT_IMPORT_STDLIB_SYMBOL) +# define SWIFT_IMPORT_STDLIB_SYMBOL +#endif +#endif +#if !__has_feature(nullability) +# define _Nonnull +# define _Nullable +# define _Null_unspecified +#elif !defined(__OBJC__) +# pragma clang diagnostic ignored "-Wnullability-extension" +#endif +#if !__has_feature(nullability_nullable_result) +# define _Nullable_result _Nullable +#endif +#if __has_feature(objc_modules) +#if __has_warning("-Watimport-in-framework-header") +#pragma clang diagnostic ignored "-Watimport-in-framework-header" +#endif +#endif + +#pragma clang diagnostic ignored "-Wproperty-attribute-mismatch" +#pragma clang diagnostic ignored "-Wduplicate-method-arg" +#if __has_warning("-Wpragma-clang-attribute") +# pragma clang diagnostic ignored "-Wpragma-clang-attribute" +#endif +#pragma clang diagnostic ignored "-Wunknown-pragmas" +#pragma clang diagnostic ignored "-Wnullability" +#pragma clang diagnostic ignored "-Wdollar-in-identifier-extension" +#pragma clang diagnostic ignored "-Wunsafe-buffer-usage" + +#if __has_attribute(external_source_symbol) +# pragma push_macro("any") +# undef any +# pragma clang attribute push(__attribute__((external_source_symbol(language="Swift", defined_in="LCECryptoKit",generated_declaration))), apply_to=any(function,enum,objc_interface,objc_category,objc_protocol)) +# pragma pop_macro("any") +#endif + +#if defined(__cplusplus) +extern "C" { +#endif + +#if defined(__cplusplus) +} // extern "C" +#endif +#if __has_attribute(external_source_symbol) +# pragma clang attribute pop +#endif +#if defined(__OBJC__) +#if __has_feature(objc_modules) +#if __has_warning("-Watimport-in-framework-header") +#pragma clang diagnostic ignored "-Watimport-in-framework-header" +#endif +#endif + +#endif // defined(__OBJC__) +#pragma clang diagnostic ignored "-Wproperty-attribute-mismatch" +#pragma clang diagnostic ignored "-Wduplicate-method-arg" +#if __has_warning("-Wpragma-clang-attribute") +# pragma clang diagnostic ignored "-Wpragma-clang-attribute" +#endif +#pragma clang diagnostic ignored "-Wunknown-pragmas" +#pragma clang diagnostic ignored "-Wnullability" +#pragma clang diagnostic ignored "-Wdollar-in-identifier-extension" +#pragma clang diagnostic ignored "-Wunsafe-buffer-usage" + +#if __has_attribute(external_source_symbol) +# pragma push_macro("any") +# undef any +# pragma clang attribute push(__attribute__((external_source_symbol(language="Swift", defined_in="LCECryptoKit",generated_declaration))), apply_to=any(function,enum,objc_interface,objc_category,objc_protocol)) +# pragma pop_macro("any") +#endif + +#if defined(__OBJC__) + +#endif // defined(__OBJC__) +#if __has_attribute(external_source_symbol) +# pragma clang attribute pop +#endif +#if defined(__cplusplus) +#endif +#pragma clang diagnostic pop +#endif + +#else +#error unsupported Swift architecture +#endif diff --git a/Frameworks/LCECryptoKit.xcframework/ios-arm64/LCECryptoKit.framework/Info.plist b/Frameworks/LCECryptoKit.xcframework/ios-arm64/LCECryptoKit.framework/Info.plist new file mode 100644 index 0000000..359fa30 Binary files /dev/null and b/Frameworks/LCECryptoKit.xcframework/ios-arm64/LCECryptoKit.framework/Info.plist differ diff --git a/Frameworks/LCECryptoKit.xcframework/ios-arm64/LCECryptoKit.framework/LCECryptoKit b/Frameworks/LCECryptoKit.xcframework/ios-arm64/LCECryptoKit.framework/LCECryptoKit new file mode 100755 index 0000000..2db08c2 Binary files /dev/null and b/Frameworks/LCECryptoKit.xcframework/ios-arm64/LCECryptoKit.framework/LCECryptoKit differ diff --git a/Frameworks/LCECryptoKit.xcframework/ios-arm64/LCECryptoKit.framework/Modules/LCECryptoKit.swiftmodule/arm64-apple-ios.abi.json b/Frameworks/LCECryptoKit.xcframework/ios-arm64/LCECryptoKit.framework/Modules/LCECryptoKit.swiftmodule/arm64-apple-ios.abi.json new file mode 100644 index 0000000..32ec755 --- /dev/null +++ b/Frameworks/LCECryptoKit.xcframework/ios-arm64/LCECryptoKit.framework/Modules/LCECryptoKit.swiftmodule/arm64-apple-ios.abi.json @@ -0,0 +1,417 @@ +{ + "ABIRoot": { + "kind": "Root", + "name": "LCECryptoKit", + "printedName": "LCECryptoKit", + "children": [ + { + "kind": "Import", + "name": "Foundation", + "printedName": "Foundation", + "declKind": "Import", + "moduleName": "LCECryptoKit" + }, + { + "kind": "Import", + "name": "CommonCrypto", + "printedName": "CommonCrypto", + "declKind": "Import", + "moduleName": "LCECryptoKit" + }, + { + "kind": "TypeDecl", + "name": "LCECryptoKit", + "printedName": "LCECryptoKit", + "children": [ + { + "kind": "Function", + "name": "generateRandomAESKeyString", + "printedName": "generateRandomAESKeyString()", + "children": [ + { + "kind": "TypeNominal", + "name": "String", + "printedName": "Swift.String", + "usr": "s:SS" + } + ], + "declKind": "Func", + "usr": "s:12LCECryptoKitAAV26generateRandomAESKeyStringSSyFZ", + "mangledName": "$s12LCECryptoKitAAV26generateRandomAESKeyStringSSyFZ", + "moduleName": "LCECryptoKit", + "static": true, + "declAttributes": [ + "AccessControl" + ], + "funcSelfKind": "NonMutating" + }, + { + "kind": "Function", + "name": "encodeSeed", + "printedName": "encodeSeed(email:password:)", + "children": [ + { + "kind": "TypeNominal", + "name": "Optional", + "printedName": "Swift.String?", + "children": [ + { + "kind": "TypeNominal", + "name": "String", + "printedName": "Swift.String", + "usr": "s:SS" + } + ], + "usr": "s:Sq" + }, + { + "kind": "TypeNominal", + "name": "String", + "printedName": "Swift.String", + "usr": "s:SS" + }, + { + "kind": "TypeNominal", + "name": "String", + "printedName": "Swift.String", + "usr": "s:SS" + } + ], + "declKind": "Func", + "usr": "s:12LCECryptoKitAAV10encodeSeed5email8passwordSSSgSS_SStFZ", + "mangledName": "$s12LCECryptoKitAAV10encodeSeed5email8passwordSSSgSS_SStFZ", + "moduleName": "LCECryptoKit", + "static": true, + "declAttributes": [ + "AccessControl" + ], + "funcSelfKind": "NonMutating" + }, + { + "kind": "Function", + "name": "encodeSeed", + "printedName": "encodeSeed(email:password:hashKey:)", + "children": [ + { + "kind": "TypeNominal", + "name": "Optional", + "printedName": "Swift.String?", + "children": [ + { + "kind": "TypeNominal", + "name": "String", + "printedName": "Swift.String", + "usr": "s:SS" + } + ], + "usr": "s:Sq" + }, + { + "kind": "TypeNominal", + "name": "String", + "printedName": "Swift.String", + "usr": "s:SS" + }, + { + "kind": "TypeNominal", + "name": "String", + "printedName": "Swift.String", + "usr": "s:SS" + }, + { + "kind": "TypeNominal", + "name": "String", + "printedName": "Swift.String", + "usr": "s:SS" + } + ], + "declKind": "Func", + "usr": "s:12LCECryptoKitAAV10encodeSeed5email8password7hashKeySSSgSS_S2StFZ", + "mangledName": "$s12LCECryptoKitAAV10encodeSeed5email8password7hashKeySSSgSS_S2StFZ", + "moduleName": "LCECryptoKit", + "static": true, + "declAttributes": [ + "AccessControl" + ], + "funcSelfKind": "NonMutating" + }, + { + "kind": "Function", + "name": "decodeSeed", + "printedName": "decodeSeed(otpKey:)", + "children": [ + { + "kind": "TypeNominal", + "name": "Optional", + "printedName": "Swift.String?", + "children": [ + { + "kind": "TypeNominal", + "name": "String", + "printedName": "Swift.String", + "usr": "s:SS" + } + ], + "usr": "s:Sq" + }, + { + "kind": "TypeNominal", + "name": "String", + "printedName": "Swift.String", + "usr": "s:SS" + } + ], + "declKind": "Func", + "usr": "s:12LCECryptoKitAAV10decodeSeed6otpKeySSSgSS_tFZ", + "mangledName": "$s12LCECryptoKitAAV10decodeSeed6otpKeySSSgSS_tFZ", + "moduleName": "LCECryptoKit", + "static": true, + "declAttributes": [ + "AccessControl" + ], + "funcSelfKind": "NonMutating" + }, + { + "kind": "Function", + "name": "decodeSeed", + "printedName": "decodeSeed(otpKey:hashKey:)", + "children": [ + { + "kind": "TypeNominal", + "name": "Bool", + "printedName": "Swift.Bool", + "usr": "s:Sb" + }, + { + "kind": "TypeNominal", + "name": "String", + "printedName": "Swift.String", + "usr": "s:SS" + }, + { + "kind": "TypeNominal", + "name": "String", + "printedName": "Swift.String", + "usr": "s:SS" + } + ], + "declKind": "Func", + "usr": "s:12LCECryptoKitAAV10decodeSeed6otpKey04hashF0SbSS_SStFZ", + "mangledName": "$s12LCECryptoKitAAV10decodeSeed6otpKey04hashF0SbSS_SStFZ", + "moduleName": "LCECryptoKit", + "static": true, + "declAttributes": [ + "AccessControl" + ], + "funcSelfKind": "NonMutating" + }, + { + "kind": "Function", + "name": "generateSalt", + "printedName": "generateSalt()", + "children": [ + { + "kind": "TypeNominal", + "name": "String", + "printedName": "Swift.String", + "usr": "s:SS" + } + ], + "declKind": "Func", + "usr": "s:12LCECryptoKitAAV12generateSaltSSyFZ", + "mangledName": "$s12LCECryptoKitAAV12generateSaltSSyFZ", + "moduleName": "LCECryptoKit", + "static": true, + "declAttributes": [ + "AccessControl", + "RawDocComment" + ], + "funcSelfKind": "NonMutating" + }, + { + "kind": "Function", + "name": "computeClientHash", + "printedName": "computeClientHash(email:password:salt:)", + "children": [ + { + "kind": "TypeNominal", + "name": "String", + "printedName": "Swift.String", + "usr": "s:SS" + }, + { + "kind": "TypeNominal", + "name": "String", + "printedName": "Swift.String", + "usr": "s:SS" + }, + { + "kind": "TypeNominal", + "name": "String", + "printedName": "Swift.String", + "usr": "s:SS" + }, + { + "kind": "TypeNominal", + "name": "String", + "printedName": "Swift.String", + "usr": "s:SS" + } + ], + "declKind": "Func", + "usr": "s:12LCECryptoKitAAV17computeClientHash5email8password4saltS2S_S2StFZ", + "mangledName": "$s12LCECryptoKitAAV17computeClientHash5email8password4saltS2S_S2StFZ", + "moduleName": "LCECryptoKit", + "static": true, + "declAttributes": [ + "AccessControl", + "RawDocComment" + ], + "funcSelfKind": "NonMutating" + }, + { + "kind": "Function", + "name": "computeLoginBearerToken", + "printedName": "computeLoginBearerToken(userId:clientHash:)", + "children": [ + { + "kind": "TypeNominal", + "name": "Optional", + "printedName": "Swift.String?", + "children": [ + { + "kind": "TypeNominal", + "name": "String", + "printedName": "Swift.String", + "usr": "s:SS" + } + ], + "usr": "s:Sq" + }, + { + "kind": "TypeNominal", + "name": "String", + "printedName": "Swift.String", + "usr": "s:SS" + }, + { + "kind": "TypeNominal", + "name": "String", + "printedName": "Swift.String", + "usr": "s:SS" + } + ], + "declKind": "Func", + "usr": "s:12LCECryptoKitAAV23computeLoginBearerToken6userId10clientHashSSSgSS_SStFZ", + "mangledName": "$s12LCECryptoKitAAV23computeLoginBearerToken6userId10clientHashSSSgSS_SStFZ", + "moduleName": "LCECryptoKit", + "static": true, + "declAttributes": [ + "AccessControl", + "RawDocComment" + ], + "funcSelfKind": "NonMutating" + }, + { + "kind": "Function", + "name": "otpEncode", + "printedName": "otpEncode(_:)", + "children": [ + { + "kind": "TypeNominal", + "name": "Optional", + "printedName": "Swift.String?", + "children": [ + { + "kind": "TypeNominal", + "name": "String", + "printedName": "Swift.String", + "usr": "s:SS" + } + ], + "usr": "s:Sq" + }, + { + "kind": "TypeNominal", + "name": "String", + "printedName": "Swift.String", + "usr": "s:SS" + } + ], + "declKind": "Func", + "usr": "s:12LCECryptoKitAAV9otpEncodeySSSgSSFZ", + "mangledName": "$s12LCECryptoKitAAV9otpEncodeySSSgSSFZ", + "moduleName": "LCECryptoKit", + "static": true, + "declAttributes": [ + "AccessControl", + "RawDocComment" + ], + "funcSelfKind": "NonMutating" + }, + { + "kind": "Function", + "name": "otpDecode", + "printedName": "otpDecode(_:)", + "children": [ + { + "kind": "TypeNominal", + "name": "Optional", + "printedName": "Swift.String?", + "children": [ + { + "kind": "TypeNominal", + "name": "String", + "printedName": "Swift.String", + "usr": "s:SS" + } + ], + "usr": "s:Sq" + }, + { + "kind": "TypeNominal", + "name": "String", + "printedName": "Swift.String", + "usr": "s:SS" + } + ], + "declKind": "Func", + "usr": "s:12LCECryptoKitAAV9otpDecodeySSSgSSFZ", + "mangledName": "$s12LCECryptoKitAAV9otpDecodeySSSgSSFZ", + "moduleName": "LCECryptoKit", + "static": true, + "declAttributes": [ + "AccessControl" + ], + "funcSelfKind": "NonMutating" + } + ], + "declKind": "Struct", + "usr": "s:12LCECryptoKitAAV", + "mangledName": "$s12LCECryptoKitAAV", + "moduleName": "LCECryptoKit", + "declAttributes": [ + "AccessControl" + ], + "conformances": [ + { + "kind": "Conformance", + "name": "Copyable", + "printedName": "Copyable", + "usr": "s:s8CopyableP", + "mangledName": "$ss8CopyableP" + }, + { + "kind": "Conformance", + "name": "Escapable", + "printedName": "Escapable", + "usr": "s:s9EscapableP", + "mangledName": "$ss9EscapableP" + } + ] + } + ], + "json_format_version": 8 + }, + "ConstValues": [] +} \ No newline at end of file diff --git a/Frameworks/LCECryptoKit.xcframework/ios-arm64/LCECryptoKit.framework/Modules/LCECryptoKit.swiftmodule/arm64-apple-ios.private.swiftinterface b/Frameworks/LCECryptoKit.xcframework/ios-arm64/LCECryptoKit.framework/Modules/LCECryptoKit.swiftmodule/arm64-apple-ios.private.swiftinterface new file mode 100644 index 0000000..d8cb5f9 --- /dev/null +++ b/Frameworks/LCECryptoKit.xcframework/ios-arm64/LCECryptoKit.framework/Modules/LCECryptoKit.swiftmodule/arm64-apple-ios.private.swiftinterface @@ -0,0 +1,22 @@ +// swift-interface-format-version: 1.0 +// swift-compiler-version: Apple Swift version 6.3.3 effective-5.10 (swiftlang-6.3.3.1.3 clang-2100.1.1.101) +// swift-module-flags: -target arm64-apple-ios13.0 -enable-objc-interop -enable-library-evolution -swift-version 5 -O -enable-experimental-feature DebugDescriptionMacro -enable-bare-slash-regex -module-name LCECryptoKit +// swift-module-flags-ignorable: -no-verify-emitted-module-interface -formal-cxx-interoperability-mode=off -interface-compiler-version 6.3.3 +import CommonCrypto +import Foundation +import Swift +import _Concurrency +import _StringProcessing +import _SwiftConcurrencyShims +public struct LCECryptoKit { + public static func generateRandomAESKeyString() -> Swift.String + public static func encodeSeed(email: Swift.String, password: Swift.String) -> Swift.String? + public static func encodeSeed(email: Swift.String, password: Swift.String, hashKey: Swift.String) -> Swift.String? + public static func decodeSeed(otpKey: Swift.String) -> Swift.String? + public static func decodeSeed(otpKey: Swift.String, hashKey: Swift.String) -> Swift.Bool + public static func generateSalt() -> Swift.String + public static func computeClientHash(email: Swift.String, password: Swift.String, salt: Swift.String) -> Swift.String + public static func computeLoginBearerToken(userId: Swift.String, clientHash: Swift.String) -> Swift.String? + public static func otpEncode(_ plainText: Swift.String) -> Swift.String? + public static func otpDecode(_ otpEncoded: Swift.String) -> Swift.String? +} diff --git a/Frameworks/LCECryptoKit.xcframework/ios-arm64/LCECryptoKit.framework/Modules/LCECryptoKit.swiftmodule/arm64-apple-ios.swiftdoc b/Frameworks/LCECryptoKit.xcframework/ios-arm64/LCECryptoKit.framework/Modules/LCECryptoKit.swiftmodule/arm64-apple-ios.swiftdoc new file mode 100644 index 0000000..c147959 Binary files /dev/null and b/Frameworks/LCECryptoKit.xcframework/ios-arm64/LCECryptoKit.framework/Modules/LCECryptoKit.swiftmodule/arm64-apple-ios.swiftdoc differ diff --git a/Frameworks/LCECryptoKit.xcframework/ios-arm64/LCECryptoKit.framework/Modules/LCECryptoKit.swiftmodule/arm64-apple-ios.swiftinterface b/Frameworks/LCECryptoKit.xcframework/ios-arm64/LCECryptoKit.framework/Modules/LCECryptoKit.swiftmodule/arm64-apple-ios.swiftinterface new file mode 100644 index 0000000..d8cb5f9 --- /dev/null +++ b/Frameworks/LCECryptoKit.xcframework/ios-arm64/LCECryptoKit.framework/Modules/LCECryptoKit.swiftmodule/arm64-apple-ios.swiftinterface @@ -0,0 +1,22 @@ +// swift-interface-format-version: 1.0 +// swift-compiler-version: Apple Swift version 6.3.3 effective-5.10 (swiftlang-6.3.3.1.3 clang-2100.1.1.101) +// swift-module-flags: -target arm64-apple-ios13.0 -enable-objc-interop -enable-library-evolution -swift-version 5 -O -enable-experimental-feature DebugDescriptionMacro -enable-bare-slash-regex -module-name LCECryptoKit +// swift-module-flags-ignorable: -no-verify-emitted-module-interface -formal-cxx-interoperability-mode=off -interface-compiler-version 6.3.3 +import CommonCrypto +import Foundation +import Swift +import _Concurrency +import _StringProcessing +import _SwiftConcurrencyShims +public struct LCECryptoKit { + public static func generateRandomAESKeyString() -> Swift.String + public static func encodeSeed(email: Swift.String, password: Swift.String) -> Swift.String? + public static func encodeSeed(email: Swift.String, password: Swift.String, hashKey: Swift.String) -> Swift.String? + public static func decodeSeed(otpKey: Swift.String) -> Swift.String? + public static func decodeSeed(otpKey: Swift.String, hashKey: Swift.String) -> Swift.Bool + public static func generateSalt() -> Swift.String + public static func computeClientHash(email: Swift.String, password: Swift.String, salt: Swift.String) -> Swift.String + public static func computeLoginBearerToken(userId: Swift.String, clientHash: Swift.String) -> Swift.String? + public static func otpEncode(_ plainText: Swift.String) -> Swift.String? + public static func otpDecode(_ otpEncoded: Swift.String) -> Swift.String? +} diff --git a/Frameworks/LCECryptoKit.xcframework/ios-arm64/LCECryptoKit.framework/Modules/module.modulemap b/Frameworks/LCECryptoKit.xcframework/ios-arm64/LCECryptoKit.framework/Modules/module.modulemap new file mode 100644 index 0000000..cccd3c0 --- /dev/null +++ b/Frameworks/LCECryptoKit.xcframework/ios-arm64/LCECryptoKit.framework/Modules/module.modulemap @@ -0,0 +1,3 @@ +framework module LCECryptoKit { + header "LCECryptoKit-Swift.h" +} diff --git a/Frameworks/LCECryptoKit.xcframework/ios-arm64_x86_64-simulator/LCECryptoKit.framework/Headers/LCECryptoKit-Swift.h b/Frameworks/LCECryptoKit.xcframework/ios-arm64_x86_64-simulator/LCECryptoKit.framework/Headers/LCECryptoKit-Swift.h new file mode 100644 index 0000000..1f9e2d6 --- /dev/null +++ b/Frameworks/LCECryptoKit.xcframework/ios-arm64_x86_64-simulator/LCECryptoKit.framework/Headers/LCECryptoKit-Swift.h @@ -0,0 +1,760 @@ +#if 0 +#elif defined(__arm64__) && __arm64__ +// Generated by Apple Swift version 6.3.3 effective-5.10 (swiftlang-6.3.3.1.3 clang-2100.1.1.101) +#ifndef LCECRYPTOKIT_SWIFT_H +#define LCECRYPTOKIT_SWIFT_H +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wgcc-compat" + +#if !defined(__has_include) +# define __has_include(x) 0 +#endif +#if !defined(__has_attribute) +# define __has_attribute(x) 0 +#endif +#if !defined(__has_feature) +# define __has_feature(x) 0 +#endif +#if !defined(__has_warning) +# define __has_warning(x) 0 +#endif + +#if __has_include() +# include +#endif + +#pragma clang diagnostic ignored "-Wauto-import" +#if defined(__OBJC__) +#include +#endif // defined(__OBJC__) +#if defined(__cplusplus) +#include +#include +#include +#include +#include +#include +#include +#else +#include +#include +#include +#include +#endif +#if defined(__cplusplus) +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wnon-modular-include-in-framework-module" +#if defined(__arm64e__) && __has_include() +# include +#else +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wreserved-macro-identifier" +# ifndef __ptrauth_swift_value_witness_function_pointer +# define __ptrauth_swift_value_witness_function_pointer(x) +# endif +# ifndef __ptrauth_swift_class_method_pointer +# define __ptrauth_swift_class_method_pointer(x) +# endif +#pragma clang diagnostic pop +#endif +#pragma clang diagnostic pop +#endif + +#if !defined(SWIFT_TYPEDEFS) +# define SWIFT_TYPEDEFS 1 +# if __has_include() +# include +# elif !defined(__cplusplus) +typedef unsigned char char8_t; +typedef uint_least16_t char16_t; +typedef uint_least32_t char32_t; +# endif +typedef float swift_float2 __attribute__((__ext_vector_type__(2))); +typedef float swift_float3 __attribute__((__ext_vector_type__(3))); +typedef float swift_float4 __attribute__((__ext_vector_type__(4))); +typedef double swift_double2 __attribute__((__ext_vector_type__(2))); +typedef double swift_double3 __attribute__((__ext_vector_type__(3))); +typedef double swift_double4 __attribute__((__ext_vector_type__(4))); +typedef int swift_int2 __attribute__((__ext_vector_type__(2))); +typedef int swift_int3 __attribute__((__ext_vector_type__(3))); +typedef int swift_int4 __attribute__((__ext_vector_type__(4))); +typedef unsigned int swift_uint2 __attribute__((__ext_vector_type__(2))); +typedef unsigned int swift_uint3 __attribute__((__ext_vector_type__(3))); +typedef unsigned int swift_uint4 __attribute__((__ext_vector_type__(4))); +#endif + +#if !defined(SWIFT_PASTE) +# define SWIFT_PASTE_HELPER(x, y) x##y +# define SWIFT_PASTE(x, y) SWIFT_PASTE_HELPER(x, y) +#endif +#if !defined(SWIFT_METATYPE) +# define SWIFT_METATYPE(X) Class +#endif +#if !defined(SWIFT_CLASS_PROPERTY) +# if __has_feature(objc_class_property) +# define SWIFT_CLASS_PROPERTY(...) __VA_ARGS__ +# else +# define SWIFT_CLASS_PROPERTY(...) +# endif +#endif +#if !defined(SWIFT_RUNTIME_NAME) +# if __has_attribute(objc_runtime_name) +# define SWIFT_RUNTIME_NAME(X) __attribute__((objc_runtime_name(X))) +# else +# define SWIFT_RUNTIME_NAME(X) +# endif +#endif +#if !defined(SWIFT_COMPILE_NAME) +# if __has_attribute(swift_name) +# define SWIFT_COMPILE_NAME(X) __attribute__((swift_name(X))) +# else +# define SWIFT_COMPILE_NAME(X) +# endif +#endif +#if !defined(SWIFT_METHOD_FAMILY) +# if __has_attribute(objc_method_family) +# define SWIFT_METHOD_FAMILY(X) __attribute__((objc_method_family(X))) +# else +# define SWIFT_METHOD_FAMILY(X) +# endif +#endif +#if !defined(SWIFT_NOESCAPE) +# if __has_attribute(noescape) +# define SWIFT_NOESCAPE __attribute__((noescape)) +# else +# define SWIFT_NOESCAPE +# endif +#endif +#if !defined(SWIFT_RELEASES_ARGUMENT) +# if __has_attribute(ns_consumed) +# define SWIFT_RELEASES_ARGUMENT __attribute__((ns_consumed)) +# else +# define SWIFT_RELEASES_ARGUMENT +# endif +#endif +#if !defined(SWIFT_WARN_UNUSED_RESULT) +# if __has_attribute(warn_unused_result) +# define SWIFT_WARN_UNUSED_RESULT __attribute__((warn_unused_result)) +# else +# define SWIFT_WARN_UNUSED_RESULT +# endif +#endif +#if !defined(SWIFT_NORETURN) +# if __has_attribute(noreturn) +# define SWIFT_NORETURN __attribute__((noreturn)) +# else +# define SWIFT_NORETURN +# endif +#endif +#if !defined(SWIFT_CLASS_EXTRA) +# define SWIFT_CLASS_EXTRA +#endif +#if !defined(SWIFT_PROTOCOL_EXTRA) +# define SWIFT_PROTOCOL_EXTRA +#endif +#if !defined(SWIFT_ENUM_EXTRA) +# define SWIFT_ENUM_EXTRA +#endif +#if !defined(SWIFT_CLASS) +# if __has_attribute(objc_subclassing_restricted) +# define SWIFT_CLASS(SWIFT_NAME) SWIFT_RUNTIME_NAME(SWIFT_NAME) __attribute__((objc_subclassing_restricted)) SWIFT_CLASS_EXTRA +# define SWIFT_CLASS_NAMED(SWIFT_NAME) __attribute__((objc_subclassing_restricted)) SWIFT_COMPILE_NAME(SWIFT_NAME) SWIFT_CLASS_EXTRA +# else +# define SWIFT_CLASS(SWIFT_NAME) SWIFT_RUNTIME_NAME(SWIFT_NAME) SWIFT_CLASS_EXTRA +# define SWIFT_CLASS_NAMED(SWIFT_NAME) SWIFT_COMPILE_NAME(SWIFT_NAME) SWIFT_CLASS_EXTRA +# endif +#endif +#if !defined(SWIFT_RESILIENT_CLASS) +# if __has_attribute(objc_class_stub) +# define SWIFT_RESILIENT_CLASS(SWIFT_NAME) SWIFT_CLASS(SWIFT_NAME) __attribute__((objc_class_stub)) +# define SWIFT_RESILIENT_CLASS_NAMED(SWIFT_NAME) __attribute__((objc_class_stub)) SWIFT_CLASS_NAMED(SWIFT_NAME) +# else +# define SWIFT_RESILIENT_CLASS(SWIFT_NAME) SWIFT_CLASS(SWIFT_NAME) +# define SWIFT_RESILIENT_CLASS_NAMED(SWIFT_NAME) SWIFT_CLASS_NAMED(SWIFT_NAME) +# endif +#endif +#if !defined(SWIFT_PROTOCOL) +# define SWIFT_PROTOCOL(SWIFT_NAME) SWIFT_RUNTIME_NAME(SWIFT_NAME) SWIFT_PROTOCOL_EXTRA +# define SWIFT_PROTOCOL_NAMED(SWIFT_NAME) SWIFT_COMPILE_NAME(SWIFT_NAME) SWIFT_PROTOCOL_EXTRA +#endif +#if !defined(SWIFT_EXTENSION) +# define SWIFT_EXTENSION(M) SWIFT_PASTE(M##_Swift_, __LINE__) +#endif +#if !defined(OBJC_DESIGNATED_INITIALIZER) +# if __has_attribute(objc_designated_initializer) +# define OBJC_DESIGNATED_INITIALIZER __attribute__((objc_designated_initializer)) +# else +# define OBJC_DESIGNATED_INITIALIZER +# endif +#endif +#if !defined(SWIFT_ENUM_ATTR) +# if __has_attribute(enum_extensibility) +# define SWIFT_ENUM_ATTR(_extensibility) __attribute__((enum_extensibility(_extensibility))) +# else +# define SWIFT_ENUM_ATTR(_extensibility) +# endif +#endif +#if !defined(SWIFT_ENUM) +# if (defined(__cplusplus) && __cplusplus >= 201103L) || (defined(__STDC_VERSION__) && __STDC_VERSION__ >= 202311L) || __has_feature(objc_fixed_enum) +# define SWIFT_ENUM(_type, _name, _extensibility) enum _name : _type _name; enum SWIFT_ENUM_ATTR(_extensibility) SWIFT_ENUM_EXTRA _name : _type +# if __has_feature(generalized_swift_name) +# define SWIFT_ENUM_NAMED(_type, _name, SWIFT_NAME, _extensibility) enum _name : _type _name SWIFT_COMPILE_NAME(SWIFT_NAME); enum SWIFT_COMPILE_NAME(SWIFT_NAME) SWIFT_ENUM_ATTR(_extensibility) SWIFT_ENUM_EXTRA _name : _type +# else +# define SWIFT_ENUM_NAMED(_type, _name, SWIFT_NAME, _extensibility) SWIFT_ENUM(_type, _name, _extensibility) +# endif +# else +# define SWIFT_ENUM(_type, _name, _extensibility) _type _name; enum +# define SWIFT_ENUM_NAMED(_type, _name, SWIFT_NAME, _extensibility) SWIFT_ENUM(_type, _name, _extensibility) +# endif +#endif +#if !defined(SWIFT_ENUM_TAG) +# if (defined(__cplusplus) && __cplusplus >= 201103L) || (defined(__STDC_VERSION__) && __STDC_VERSION__ >= 202311L) || __has_feature(objc_fixed_enum) +# define SWIFT_ENUM_TAG enum +# else +# define SWIFT_ENUM_TAG +# endif +#endif +#if !defined(SWIFT_ENUM_FWD_DECL) +# if (defined(__cplusplus) && __cplusplus >= 201103L) || (defined(__STDC_VERSION__) && __STDC_VERSION__ >= 202311L) || __has_feature(objc_fixed_enum) +# define SWIFT_ENUM_FWD_DECL(_type, _name) enum _name : _type; +# else +# define SWIFT_ENUM_FWD_DECL(_type, _name) typedef _type _name; +# endif +#endif +#if !defined(SWIFT_UNAVAILABLE) +# define SWIFT_UNAVAILABLE __attribute__((unavailable)) +#endif +#if !defined(SWIFT_UNAVAILABLE_MSG) +# define SWIFT_UNAVAILABLE_MSG(msg) __attribute__((unavailable(msg))) +#endif +#if !defined(SWIFT_AVAILABILITY) +# define SWIFT_AVAILABILITY(plat, ...) __attribute__((availability(plat, __VA_ARGS__))) +#endif +#if !defined(SWIFT_AVAILABILITY_DOMAIN) +# define SWIFT_AVAILABILITY_DOMAIN(dom, ...) __attribute__((availability(domain: dom, __VA_ARGS__))) +#endif +#if !defined(SWIFT_WEAK_IMPORT) +# define SWIFT_WEAK_IMPORT __attribute__((weak_import)) +#endif +#if !defined(SWIFT_DEPRECATED) +# define SWIFT_DEPRECATED __attribute__((deprecated)) +#endif +#if !defined(SWIFT_DEPRECATED_MSG) +# define SWIFT_DEPRECATED_MSG(...) __attribute__((deprecated(__VA_ARGS__))) +#endif +#if !defined(SWIFT_DEPRECATED_OBJC) +# if __has_feature(attribute_diagnose_if_objc) +# define SWIFT_DEPRECATED_OBJC(Msg) __attribute__((diagnose_if(1, Msg, "warning"))) +# else +# define SWIFT_DEPRECATED_OBJC(Msg) SWIFT_DEPRECATED_MSG(Msg) +# endif +#endif +#if defined(__OBJC__) +#if !defined(IBSegueAction) +# define IBSegueAction +#endif +#endif +#if !defined(SWIFT_EXTERN) +# if defined(__cplusplus) +# define SWIFT_EXTERN extern "C" +# else +# define SWIFT_EXTERN extern +# endif +#endif +#if !defined(SWIFT_CALL) +# define SWIFT_CALL __attribute__((swiftcall)) +#endif +#if !defined(SWIFT_INDIRECT_RESULT) +# define SWIFT_INDIRECT_RESULT __attribute__((swift_indirect_result)) +#endif +#if !defined(SWIFT_CONTEXT) +# define SWIFT_CONTEXT __attribute__((swift_context)) +#endif +#if !defined(SWIFT_ERROR_RESULT) +# define SWIFT_ERROR_RESULT __attribute__((swift_error_result)) +#endif +#if defined(__cplusplus) +# define SWIFT_NOEXCEPT noexcept +#else +# define SWIFT_NOEXCEPT +#endif +#if !defined(SWIFT_C_INLINE_THUNK) +# if __has_attribute(always_inline) +# if __has_attribute(nodebug) +# define SWIFT_C_INLINE_THUNK inline __attribute__((always_inline)) __attribute__((nodebug)) +# else +# define SWIFT_C_INLINE_THUNK inline __attribute__((always_inline)) +# endif +# else +# define SWIFT_C_INLINE_THUNK inline +# endif +#endif +#if defined(_WIN32) +#if !defined(SWIFT_IMPORT_STDLIB_SYMBOL) +# define SWIFT_IMPORT_STDLIB_SYMBOL __declspec(dllimport) +#endif +#else +#if !defined(SWIFT_IMPORT_STDLIB_SYMBOL) +# define SWIFT_IMPORT_STDLIB_SYMBOL +#endif +#endif +#if !__has_feature(nullability) +# define _Nonnull +# define _Nullable +# define _Null_unspecified +#elif !defined(__OBJC__) +# pragma clang diagnostic ignored "-Wnullability-extension" +#endif +#if !__has_feature(nullability_nullable_result) +# define _Nullable_result _Nullable +#endif +#if __has_feature(objc_modules) +#if __has_warning("-Watimport-in-framework-header") +#pragma clang diagnostic ignored "-Watimport-in-framework-header" +#endif +#endif + +#pragma clang diagnostic ignored "-Wproperty-attribute-mismatch" +#pragma clang diagnostic ignored "-Wduplicate-method-arg" +#if __has_warning("-Wpragma-clang-attribute") +# pragma clang diagnostic ignored "-Wpragma-clang-attribute" +#endif +#pragma clang diagnostic ignored "-Wunknown-pragmas" +#pragma clang diagnostic ignored "-Wnullability" +#pragma clang diagnostic ignored "-Wdollar-in-identifier-extension" +#pragma clang diagnostic ignored "-Wunsafe-buffer-usage" + +#if __has_attribute(external_source_symbol) +# pragma push_macro("any") +# undef any +# pragma clang attribute push(__attribute__((external_source_symbol(language="Swift", defined_in="LCECryptoKit",generated_declaration))), apply_to=any(function,enum,objc_interface,objc_category,objc_protocol)) +# pragma pop_macro("any") +#endif + +#if defined(__cplusplus) +extern "C" { +#endif + +#if defined(__cplusplus) +} // extern "C" +#endif +#if __has_attribute(external_source_symbol) +# pragma clang attribute pop +#endif +#if defined(__OBJC__) +#if __has_feature(objc_modules) +#if __has_warning("-Watimport-in-framework-header") +#pragma clang diagnostic ignored "-Watimport-in-framework-header" +#endif +#endif + +#endif // defined(__OBJC__) +#pragma clang diagnostic ignored "-Wproperty-attribute-mismatch" +#pragma clang diagnostic ignored "-Wduplicate-method-arg" +#if __has_warning("-Wpragma-clang-attribute") +# pragma clang diagnostic ignored "-Wpragma-clang-attribute" +#endif +#pragma clang diagnostic ignored "-Wunknown-pragmas" +#pragma clang diagnostic ignored "-Wnullability" +#pragma clang diagnostic ignored "-Wdollar-in-identifier-extension" +#pragma clang diagnostic ignored "-Wunsafe-buffer-usage" + +#if __has_attribute(external_source_symbol) +# pragma push_macro("any") +# undef any +# pragma clang attribute push(__attribute__((external_source_symbol(language="Swift", defined_in="LCECryptoKit",generated_declaration))), apply_to=any(function,enum,objc_interface,objc_category,objc_protocol)) +# pragma pop_macro("any") +#endif + +#if defined(__OBJC__) + +#endif // defined(__OBJC__) +#if __has_attribute(external_source_symbol) +# pragma clang attribute pop +#endif +#if defined(__cplusplus) +#endif +#pragma clang diagnostic pop +#endif + +#elif defined(__x86_64__) && __x86_64__ +// Generated by Apple Swift version 6.3.3 effective-5.10 (swiftlang-6.3.3.1.3 clang-2100.1.1.101) +#ifndef LCECRYPTOKIT_SWIFT_H +#define LCECRYPTOKIT_SWIFT_H +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wgcc-compat" + +#if !defined(__has_include) +# define __has_include(x) 0 +#endif +#if !defined(__has_attribute) +# define __has_attribute(x) 0 +#endif +#if !defined(__has_feature) +# define __has_feature(x) 0 +#endif +#if !defined(__has_warning) +# define __has_warning(x) 0 +#endif + +#if __has_include() +# include +#endif + +#pragma clang diagnostic ignored "-Wauto-import" +#if defined(__OBJC__) +#include +#endif // defined(__OBJC__) +#if defined(__cplusplus) +#include +#include +#include +#include +#include +#include +#include +#else +#include +#include +#include +#include +#endif +#if defined(__cplusplus) +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wnon-modular-include-in-framework-module" +#if defined(__arm64e__) && __has_include() +# include +#else +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wreserved-macro-identifier" +# ifndef __ptrauth_swift_value_witness_function_pointer +# define __ptrauth_swift_value_witness_function_pointer(x) +# endif +# ifndef __ptrauth_swift_class_method_pointer +# define __ptrauth_swift_class_method_pointer(x) +# endif +#pragma clang diagnostic pop +#endif +#pragma clang diagnostic pop +#endif + +#if !defined(SWIFT_TYPEDEFS) +# define SWIFT_TYPEDEFS 1 +# if __has_include() +# include +# elif !defined(__cplusplus) +typedef unsigned char char8_t; +typedef uint_least16_t char16_t; +typedef uint_least32_t char32_t; +# endif +typedef float swift_float2 __attribute__((__ext_vector_type__(2))); +typedef float swift_float3 __attribute__((__ext_vector_type__(3))); +typedef float swift_float4 __attribute__((__ext_vector_type__(4))); +typedef double swift_double2 __attribute__((__ext_vector_type__(2))); +typedef double swift_double3 __attribute__((__ext_vector_type__(3))); +typedef double swift_double4 __attribute__((__ext_vector_type__(4))); +typedef int swift_int2 __attribute__((__ext_vector_type__(2))); +typedef int swift_int3 __attribute__((__ext_vector_type__(3))); +typedef int swift_int4 __attribute__((__ext_vector_type__(4))); +typedef unsigned int swift_uint2 __attribute__((__ext_vector_type__(2))); +typedef unsigned int swift_uint3 __attribute__((__ext_vector_type__(3))); +typedef unsigned int swift_uint4 __attribute__((__ext_vector_type__(4))); +#endif + +#if !defined(SWIFT_PASTE) +# define SWIFT_PASTE_HELPER(x, y) x##y +# define SWIFT_PASTE(x, y) SWIFT_PASTE_HELPER(x, y) +#endif +#if !defined(SWIFT_METATYPE) +# define SWIFT_METATYPE(X) Class +#endif +#if !defined(SWIFT_CLASS_PROPERTY) +# if __has_feature(objc_class_property) +# define SWIFT_CLASS_PROPERTY(...) __VA_ARGS__ +# else +# define SWIFT_CLASS_PROPERTY(...) +# endif +#endif +#if !defined(SWIFT_RUNTIME_NAME) +# if __has_attribute(objc_runtime_name) +# define SWIFT_RUNTIME_NAME(X) __attribute__((objc_runtime_name(X))) +# else +# define SWIFT_RUNTIME_NAME(X) +# endif +#endif +#if !defined(SWIFT_COMPILE_NAME) +# if __has_attribute(swift_name) +# define SWIFT_COMPILE_NAME(X) __attribute__((swift_name(X))) +# else +# define SWIFT_COMPILE_NAME(X) +# endif +#endif +#if !defined(SWIFT_METHOD_FAMILY) +# if __has_attribute(objc_method_family) +# define SWIFT_METHOD_FAMILY(X) __attribute__((objc_method_family(X))) +# else +# define SWIFT_METHOD_FAMILY(X) +# endif +#endif +#if !defined(SWIFT_NOESCAPE) +# if __has_attribute(noescape) +# define SWIFT_NOESCAPE __attribute__((noescape)) +# else +# define SWIFT_NOESCAPE +# endif +#endif +#if !defined(SWIFT_RELEASES_ARGUMENT) +# if __has_attribute(ns_consumed) +# define SWIFT_RELEASES_ARGUMENT __attribute__((ns_consumed)) +# else +# define SWIFT_RELEASES_ARGUMENT +# endif +#endif +#if !defined(SWIFT_WARN_UNUSED_RESULT) +# if __has_attribute(warn_unused_result) +# define SWIFT_WARN_UNUSED_RESULT __attribute__((warn_unused_result)) +# else +# define SWIFT_WARN_UNUSED_RESULT +# endif +#endif +#if !defined(SWIFT_NORETURN) +# if __has_attribute(noreturn) +# define SWIFT_NORETURN __attribute__((noreturn)) +# else +# define SWIFT_NORETURN +# endif +#endif +#if !defined(SWIFT_CLASS_EXTRA) +# define SWIFT_CLASS_EXTRA +#endif +#if !defined(SWIFT_PROTOCOL_EXTRA) +# define SWIFT_PROTOCOL_EXTRA +#endif +#if !defined(SWIFT_ENUM_EXTRA) +# define SWIFT_ENUM_EXTRA +#endif +#if !defined(SWIFT_CLASS) +# if __has_attribute(objc_subclassing_restricted) +# define SWIFT_CLASS(SWIFT_NAME) SWIFT_RUNTIME_NAME(SWIFT_NAME) __attribute__((objc_subclassing_restricted)) SWIFT_CLASS_EXTRA +# define SWIFT_CLASS_NAMED(SWIFT_NAME) __attribute__((objc_subclassing_restricted)) SWIFT_COMPILE_NAME(SWIFT_NAME) SWIFT_CLASS_EXTRA +# else +# define SWIFT_CLASS(SWIFT_NAME) SWIFT_RUNTIME_NAME(SWIFT_NAME) SWIFT_CLASS_EXTRA +# define SWIFT_CLASS_NAMED(SWIFT_NAME) SWIFT_COMPILE_NAME(SWIFT_NAME) SWIFT_CLASS_EXTRA +# endif +#endif +#if !defined(SWIFT_RESILIENT_CLASS) +# if __has_attribute(objc_class_stub) +# define SWIFT_RESILIENT_CLASS(SWIFT_NAME) SWIFT_CLASS(SWIFT_NAME) __attribute__((objc_class_stub)) +# define SWIFT_RESILIENT_CLASS_NAMED(SWIFT_NAME) __attribute__((objc_class_stub)) SWIFT_CLASS_NAMED(SWIFT_NAME) +# else +# define SWIFT_RESILIENT_CLASS(SWIFT_NAME) SWIFT_CLASS(SWIFT_NAME) +# define SWIFT_RESILIENT_CLASS_NAMED(SWIFT_NAME) SWIFT_CLASS_NAMED(SWIFT_NAME) +# endif +#endif +#if !defined(SWIFT_PROTOCOL) +# define SWIFT_PROTOCOL(SWIFT_NAME) SWIFT_RUNTIME_NAME(SWIFT_NAME) SWIFT_PROTOCOL_EXTRA +# define SWIFT_PROTOCOL_NAMED(SWIFT_NAME) SWIFT_COMPILE_NAME(SWIFT_NAME) SWIFT_PROTOCOL_EXTRA +#endif +#if !defined(SWIFT_EXTENSION) +# define SWIFT_EXTENSION(M) SWIFT_PASTE(M##_Swift_, __LINE__) +#endif +#if !defined(OBJC_DESIGNATED_INITIALIZER) +# if __has_attribute(objc_designated_initializer) +# define OBJC_DESIGNATED_INITIALIZER __attribute__((objc_designated_initializer)) +# else +# define OBJC_DESIGNATED_INITIALIZER +# endif +#endif +#if !defined(SWIFT_ENUM_ATTR) +# if __has_attribute(enum_extensibility) +# define SWIFT_ENUM_ATTR(_extensibility) __attribute__((enum_extensibility(_extensibility))) +# else +# define SWIFT_ENUM_ATTR(_extensibility) +# endif +#endif +#if !defined(SWIFT_ENUM) +# if (defined(__cplusplus) && __cplusplus >= 201103L) || (defined(__STDC_VERSION__) && __STDC_VERSION__ >= 202311L) || __has_feature(objc_fixed_enum) +# define SWIFT_ENUM(_type, _name, _extensibility) enum _name : _type _name; enum SWIFT_ENUM_ATTR(_extensibility) SWIFT_ENUM_EXTRA _name : _type +# if __has_feature(generalized_swift_name) +# define SWIFT_ENUM_NAMED(_type, _name, SWIFT_NAME, _extensibility) enum _name : _type _name SWIFT_COMPILE_NAME(SWIFT_NAME); enum SWIFT_COMPILE_NAME(SWIFT_NAME) SWIFT_ENUM_ATTR(_extensibility) SWIFT_ENUM_EXTRA _name : _type +# else +# define SWIFT_ENUM_NAMED(_type, _name, SWIFT_NAME, _extensibility) SWIFT_ENUM(_type, _name, _extensibility) +# endif +# else +# define SWIFT_ENUM(_type, _name, _extensibility) _type _name; enum +# define SWIFT_ENUM_NAMED(_type, _name, SWIFT_NAME, _extensibility) SWIFT_ENUM(_type, _name, _extensibility) +# endif +#endif +#if !defined(SWIFT_ENUM_TAG) +# if (defined(__cplusplus) && __cplusplus >= 201103L) || (defined(__STDC_VERSION__) && __STDC_VERSION__ >= 202311L) || __has_feature(objc_fixed_enum) +# define SWIFT_ENUM_TAG enum +# else +# define SWIFT_ENUM_TAG +# endif +#endif +#if !defined(SWIFT_ENUM_FWD_DECL) +# if (defined(__cplusplus) && __cplusplus >= 201103L) || (defined(__STDC_VERSION__) && __STDC_VERSION__ >= 202311L) || __has_feature(objc_fixed_enum) +# define SWIFT_ENUM_FWD_DECL(_type, _name) enum _name : _type; +# else +# define SWIFT_ENUM_FWD_DECL(_type, _name) typedef _type _name; +# endif +#endif +#if !defined(SWIFT_UNAVAILABLE) +# define SWIFT_UNAVAILABLE __attribute__((unavailable)) +#endif +#if !defined(SWIFT_UNAVAILABLE_MSG) +# define SWIFT_UNAVAILABLE_MSG(msg) __attribute__((unavailable(msg))) +#endif +#if !defined(SWIFT_AVAILABILITY) +# define SWIFT_AVAILABILITY(plat, ...) __attribute__((availability(plat, __VA_ARGS__))) +#endif +#if !defined(SWIFT_AVAILABILITY_DOMAIN) +# define SWIFT_AVAILABILITY_DOMAIN(dom, ...) __attribute__((availability(domain: dom, __VA_ARGS__))) +#endif +#if !defined(SWIFT_WEAK_IMPORT) +# define SWIFT_WEAK_IMPORT __attribute__((weak_import)) +#endif +#if !defined(SWIFT_DEPRECATED) +# define SWIFT_DEPRECATED __attribute__((deprecated)) +#endif +#if !defined(SWIFT_DEPRECATED_MSG) +# define SWIFT_DEPRECATED_MSG(...) __attribute__((deprecated(__VA_ARGS__))) +#endif +#if !defined(SWIFT_DEPRECATED_OBJC) +# if __has_feature(attribute_diagnose_if_objc) +# define SWIFT_DEPRECATED_OBJC(Msg) __attribute__((diagnose_if(1, Msg, "warning"))) +# else +# define SWIFT_DEPRECATED_OBJC(Msg) SWIFT_DEPRECATED_MSG(Msg) +# endif +#endif +#if defined(__OBJC__) +#if !defined(IBSegueAction) +# define IBSegueAction +#endif +#endif +#if !defined(SWIFT_EXTERN) +# if defined(__cplusplus) +# define SWIFT_EXTERN extern "C" +# else +# define SWIFT_EXTERN extern +# endif +#endif +#if !defined(SWIFT_CALL) +# define SWIFT_CALL __attribute__((swiftcall)) +#endif +#if !defined(SWIFT_INDIRECT_RESULT) +# define SWIFT_INDIRECT_RESULT __attribute__((swift_indirect_result)) +#endif +#if !defined(SWIFT_CONTEXT) +# define SWIFT_CONTEXT __attribute__((swift_context)) +#endif +#if !defined(SWIFT_ERROR_RESULT) +# define SWIFT_ERROR_RESULT __attribute__((swift_error_result)) +#endif +#if defined(__cplusplus) +# define SWIFT_NOEXCEPT noexcept +#else +# define SWIFT_NOEXCEPT +#endif +#if !defined(SWIFT_C_INLINE_THUNK) +# if __has_attribute(always_inline) +# if __has_attribute(nodebug) +# define SWIFT_C_INLINE_THUNK inline __attribute__((always_inline)) __attribute__((nodebug)) +# else +# define SWIFT_C_INLINE_THUNK inline __attribute__((always_inline)) +# endif +# else +# define SWIFT_C_INLINE_THUNK inline +# endif +#endif +#if defined(_WIN32) +#if !defined(SWIFT_IMPORT_STDLIB_SYMBOL) +# define SWIFT_IMPORT_STDLIB_SYMBOL __declspec(dllimport) +#endif +#else +#if !defined(SWIFT_IMPORT_STDLIB_SYMBOL) +# define SWIFT_IMPORT_STDLIB_SYMBOL +#endif +#endif +#if !__has_feature(nullability) +# define _Nonnull +# define _Nullable +# define _Null_unspecified +#elif !defined(__OBJC__) +# pragma clang diagnostic ignored "-Wnullability-extension" +#endif +#if !__has_feature(nullability_nullable_result) +# define _Nullable_result _Nullable +#endif +#if __has_feature(objc_modules) +#if __has_warning("-Watimport-in-framework-header") +#pragma clang diagnostic ignored "-Watimport-in-framework-header" +#endif +#endif + +#pragma clang diagnostic ignored "-Wproperty-attribute-mismatch" +#pragma clang diagnostic ignored "-Wduplicate-method-arg" +#if __has_warning("-Wpragma-clang-attribute") +# pragma clang diagnostic ignored "-Wpragma-clang-attribute" +#endif +#pragma clang diagnostic ignored "-Wunknown-pragmas" +#pragma clang diagnostic ignored "-Wnullability" +#pragma clang diagnostic ignored "-Wdollar-in-identifier-extension" +#pragma clang diagnostic ignored "-Wunsafe-buffer-usage" + +#if __has_attribute(external_source_symbol) +# pragma push_macro("any") +# undef any +# pragma clang attribute push(__attribute__((external_source_symbol(language="Swift", defined_in="LCECryptoKit",generated_declaration))), apply_to=any(function,enum,objc_interface,objc_category,objc_protocol)) +# pragma pop_macro("any") +#endif + +#if defined(__cplusplus) +extern "C" { +#endif + +#if defined(__cplusplus) +} // extern "C" +#endif +#if __has_attribute(external_source_symbol) +# pragma clang attribute pop +#endif +#if defined(__OBJC__) +#if __has_feature(objc_modules) +#if __has_warning("-Watimport-in-framework-header") +#pragma clang diagnostic ignored "-Watimport-in-framework-header" +#endif +#endif + +#endif // defined(__OBJC__) +#pragma clang diagnostic ignored "-Wproperty-attribute-mismatch" +#pragma clang diagnostic ignored "-Wduplicate-method-arg" +#if __has_warning("-Wpragma-clang-attribute") +# pragma clang diagnostic ignored "-Wpragma-clang-attribute" +#endif +#pragma clang diagnostic ignored "-Wunknown-pragmas" +#pragma clang diagnostic ignored "-Wnullability" +#pragma clang diagnostic ignored "-Wdollar-in-identifier-extension" +#pragma clang diagnostic ignored "-Wunsafe-buffer-usage" + +#if __has_attribute(external_source_symbol) +# pragma push_macro("any") +# undef any +# pragma clang attribute push(__attribute__((external_source_symbol(language="Swift", defined_in="LCECryptoKit",generated_declaration))), apply_to=any(function,enum,objc_interface,objc_category,objc_protocol)) +# pragma pop_macro("any") +#endif + +#if defined(__OBJC__) + +#endif // defined(__OBJC__) +#if __has_attribute(external_source_symbol) +# pragma clang attribute pop +#endif +#if defined(__cplusplus) +#endif +#pragma clang diagnostic pop +#endif + +#else +#error unsupported Swift architecture +#endif diff --git a/Frameworks/LCECryptoKit.xcframework/ios-arm64_x86_64-simulator/LCECryptoKit.framework/Info.plist b/Frameworks/LCECryptoKit.xcframework/ios-arm64_x86_64-simulator/LCECryptoKit.framework/Info.plist new file mode 100644 index 0000000..4782fa8 Binary files /dev/null and b/Frameworks/LCECryptoKit.xcframework/ios-arm64_x86_64-simulator/LCECryptoKit.framework/Info.plist differ diff --git a/Frameworks/LCECryptoKit.xcframework/ios-arm64_x86_64-simulator/LCECryptoKit.framework/LCECryptoKit b/Frameworks/LCECryptoKit.xcframework/ios-arm64_x86_64-simulator/LCECryptoKit.framework/LCECryptoKit new file mode 100755 index 0000000..70c713e Binary files /dev/null and b/Frameworks/LCECryptoKit.xcframework/ios-arm64_x86_64-simulator/LCECryptoKit.framework/LCECryptoKit differ diff --git a/Frameworks/LCECryptoKit.xcframework/ios-arm64_x86_64-simulator/LCECryptoKit.framework/Modules/LCECryptoKit.swiftmodule/arm64-apple-ios-simulator.abi.json b/Frameworks/LCECryptoKit.xcframework/ios-arm64_x86_64-simulator/LCECryptoKit.framework/Modules/LCECryptoKit.swiftmodule/arm64-apple-ios-simulator.abi.json new file mode 100644 index 0000000..32ec755 --- /dev/null +++ b/Frameworks/LCECryptoKit.xcframework/ios-arm64_x86_64-simulator/LCECryptoKit.framework/Modules/LCECryptoKit.swiftmodule/arm64-apple-ios-simulator.abi.json @@ -0,0 +1,417 @@ +{ + "ABIRoot": { + "kind": "Root", + "name": "LCECryptoKit", + "printedName": "LCECryptoKit", + "children": [ + { + "kind": "Import", + "name": "Foundation", + "printedName": "Foundation", + "declKind": "Import", + "moduleName": "LCECryptoKit" + }, + { + "kind": "Import", + "name": "CommonCrypto", + "printedName": "CommonCrypto", + "declKind": "Import", + "moduleName": "LCECryptoKit" + }, + { + "kind": "TypeDecl", + "name": "LCECryptoKit", + "printedName": "LCECryptoKit", + "children": [ + { + "kind": "Function", + "name": "generateRandomAESKeyString", + "printedName": "generateRandomAESKeyString()", + "children": [ + { + "kind": "TypeNominal", + "name": "String", + "printedName": "Swift.String", + "usr": "s:SS" + } + ], + "declKind": "Func", + "usr": "s:12LCECryptoKitAAV26generateRandomAESKeyStringSSyFZ", + "mangledName": "$s12LCECryptoKitAAV26generateRandomAESKeyStringSSyFZ", + "moduleName": "LCECryptoKit", + "static": true, + "declAttributes": [ + "AccessControl" + ], + "funcSelfKind": "NonMutating" + }, + { + "kind": "Function", + "name": "encodeSeed", + "printedName": "encodeSeed(email:password:)", + "children": [ + { + "kind": "TypeNominal", + "name": "Optional", + "printedName": "Swift.String?", + "children": [ + { + "kind": "TypeNominal", + "name": "String", + "printedName": "Swift.String", + "usr": "s:SS" + } + ], + "usr": "s:Sq" + }, + { + "kind": "TypeNominal", + "name": "String", + "printedName": "Swift.String", + "usr": "s:SS" + }, + { + "kind": "TypeNominal", + "name": "String", + "printedName": "Swift.String", + "usr": "s:SS" + } + ], + "declKind": "Func", + "usr": "s:12LCECryptoKitAAV10encodeSeed5email8passwordSSSgSS_SStFZ", + "mangledName": "$s12LCECryptoKitAAV10encodeSeed5email8passwordSSSgSS_SStFZ", + "moduleName": "LCECryptoKit", + "static": true, + "declAttributes": [ + "AccessControl" + ], + "funcSelfKind": "NonMutating" + }, + { + "kind": "Function", + "name": "encodeSeed", + "printedName": "encodeSeed(email:password:hashKey:)", + "children": [ + { + "kind": "TypeNominal", + "name": "Optional", + "printedName": "Swift.String?", + "children": [ + { + "kind": "TypeNominal", + "name": "String", + "printedName": "Swift.String", + "usr": "s:SS" + } + ], + "usr": "s:Sq" + }, + { + "kind": "TypeNominal", + "name": "String", + "printedName": "Swift.String", + "usr": "s:SS" + }, + { + "kind": "TypeNominal", + "name": "String", + "printedName": "Swift.String", + "usr": "s:SS" + }, + { + "kind": "TypeNominal", + "name": "String", + "printedName": "Swift.String", + "usr": "s:SS" + } + ], + "declKind": "Func", + "usr": "s:12LCECryptoKitAAV10encodeSeed5email8password7hashKeySSSgSS_S2StFZ", + "mangledName": "$s12LCECryptoKitAAV10encodeSeed5email8password7hashKeySSSgSS_S2StFZ", + "moduleName": "LCECryptoKit", + "static": true, + "declAttributes": [ + "AccessControl" + ], + "funcSelfKind": "NonMutating" + }, + { + "kind": "Function", + "name": "decodeSeed", + "printedName": "decodeSeed(otpKey:)", + "children": [ + { + "kind": "TypeNominal", + "name": "Optional", + "printedName": "Swift.String?", + "children": [ + { + "kind": "TypeNominal", + "name": "String", + "printedName": "Swift.String", + "usr": "s:SS" + } + ], + "usr": "s:Sq" + }, + { + "kind": "TypeNominal", + "name": "String", + "printedName": "Swift.String", + "usr": "s:SS" + } + ], + "declKind": "Func", + "usr": "s:12LCECryptoKitAAV10decodeSeed6otpKeySSSgSS_tFZ", + "mangledName": "$s12LCECryptoKitAAV10decodeSeed6otpKeySSSgSS_tFZ", + "moduleName": "LCECryptoKit", + "static": true, + "declAttributes": [ + "AccessControl" + ], + "funcSelfKind": "NonMutating" + }, + { + "kind": "Function", + "name": "decodeSeed", + "printedName": "decodeSeed(otpKey:hashKey:)", + "children": [ + { + "kind": "TypeNominal", + "name": "Bool", + "printedName": "Swift.Bool", + "usr": "s:Sb" + }, + { + "kind": "TypeNominal", + "name": "String", + "printedName": "Swift.String", + "usr": "s:SS" + }, + { + "kind": "TypeNominal", + "name": "String", + "printedName": "Swift.String", + "usr": "s:SS" + } + ], + "declKind": "Func", + "usr": "s:12LCECryptoKitAAV10decodeSeed6otpKey04hashF0SbSS_SStFZ", + "mangledName": "$s12LCECryptoKitAAV10decodeSeed6otpKey04hashF0SbSS_SStFZ", + "moduleName": "LCECryptoKit", + "static": true, + "declAttributes": [ + "AccessControl" + ], + "funcSelfKind": "NonMutating" + }, + { + "kind": "Function", + "name": "generateSalt", + "printedName": "generateSalt()", + "children": [ + { + "kind": "TypeNominal", + "name": "String", + "printedName": "Swift.String", + "usr": "s:SS" + } + ], + "declKind": "Func", + "usr": "s:12LCECryptoKitAAV12generateSaltSSyFZ", + "mangledName": "$s12LCECryptoKitAAV12generateSaltSSyFZ", + "moduleName": "LCECryptoKit", + "static": true, + "declAttributes": [ + "AccessControl", + "RawDocComment" + ], + "funcSelfKind": "NonMutating" + }, + { + "kind": "Function", + "name": "computeClientHash", + "printedName": "computeClientHash(email:password:salt:)", + "children": [ + { + "kind": "TypeNominal", + "name": "String", + "printedName": "Swift.String", + "usr": "s:SS" + }, + { + "kind": "TypeNominal", + "name": "String", + "printedName": "Swift.String", + "usr": "s:SS" + }, + { + "kind": "TypeNominal", + "name": "String", + "printedName": "Swift.String", + "usr": "s:SS" + }, + { + "kind": "TypeNominal", + "name": "String", + "printedName": "Swift.String", + "usr": "s:SS" + } + ], + "declKind": "Func", + "usr": "s:12LCECryptoKitAAV17computeClientHash5email8password4saltS2S_S2StFZ", + "mangledName": "$s12LCECryptoKitAAV17computeClientHash5email8password4saltS2S_S2StFZ", + "moduleName": "LCECryptoKit", + "static": true, + "declAttributes": [ + "AccessControl", + "RawDocComment" + ], + "funcSelfKind": "NonMutating" + }, + { + "kind": "Function", + "name": "computeLoginBearerToken", + "printedName": "computeLoginBearerToken(userId:clientHash:)", + "children": [ + { + "kind": "TypeNominal", + "name": "Optional", + "printedName": "Swift.String?", + "children": [ + { + "kind": "TypeNominal", + "name": "String", + "printedName": "Swift.String", + "usr": "s:SS" + } + ], + "usr": "s:Sq" + }, + { + "kind": "TypeNominal", + "name": "String", + "printedName": "Swift.String", + "usr": "s:SS" + }, + { + "kind": "TypeNominal", + "name": "String", + "printedName": "Swift.String", + "usr": "s:SS" + } + ], + "declKind": "Func", + "usr": "s:12LCECryptoKitAAV23computeLoginBearerToken6userId10clientHashSSSgSS_SStFZ", + "mangledName": "$s12LCECryptoKitAAV23computeLoginBearerToken6userId10clientHashSSSgSS_SStFZ", + "moduleName": "LCECryptoKit", + "static": true, + "declAttributes": [ + "AccessControl", + "RawDocComment" + ], + "funcSelfKind": "NonMutating" + }, + { + "kind": "Function", + "name": "otpEncode", + "printedName": "otpEncode(_:)", + "children": [ + { + "kind": "TypeNominal", + "name": "Optional", + "printedName": "Swift.String?", + "children": [ + { + "kind": "TypeNominal", + "name": "String", + "printedName": "Swift.String", + "usr": "s:SS" + } + ], + "usr": "s:Sq" + }, + { + "kind": "TypeNominal", + "name": "String", + "printedName": "Swift.String", + "usr": "s:SS" + } + ], + "declKind": "Func", + "usr": "s:12LCECryptoKitAAV9otpEncodeySSSgSSFZ", + "mangledName": "$s12LCECryptoKitAAV9otpEncodeySSSgSSFZ", + "moduleName": "LCECryptoKit", + "static": true, + "declAttributes": [ + "AccessControl", + "RawDocComment" + ], + "funcSelfKind": "NonMutating" + }, + { + "kind": "Function", + "name": "otpDecode", + "printedName": "otpDecode(_:)", + "children": [ + { + "kind": "TypeNominal", + "name": "Optional", + "printedName": "Swift.String?", + "children": [ + { + "kind": "TypeNominal", + "name": "String", + "printedName": "Swift.String", + "usr": "s:SS" + } + ], + "usr": "s:Sq" + }, + { + "kind": "TypeNominal", + "name": "String", + "printedName": "Swift.String", + "usr": "s:SS" + } + ], + "declKind": "Func", + "usr": "s:12LCECryptoKitAAV9otpDecodeySSSgSSFZ", + "mangledName": "$s12LCECryptoKitAAV9otpDecodeySSSgSSFZ", + "moduleName": "LCECryptoKit", + "static": true, + "declAttributes": [ + "AccessControl" + ], + "funcSelfKind": "NonMutating" + } + ], + "declKind": "Struct", + "usr": "s:12LCECryptoKitAAV", + "mangledName": "$s12LCECryptoKitAAV", + "moduleName": "LCECryptoKit", + "declAttributes": [ + "AccessControl" + ], + "conformances": [ + { + "kind": "Conformance", + "name": "Copyable", + "printedName": "Copyable", + "usr": "s:s8CopyableP", + "mangledName": "$ss8CopyableP" + }, + { + "kind": "Conformance", + "name": "Escapable", + "printedName": "Escapable", + "usr": "s:s9EscapableP", + "mangledName": "$ss9EscapableP" + } + ] + } + ], + "json_format_version": 8 + }, + "ConstValues": [] +} \ No newline at end of file diff --git a/Frameworks/LCECryptoKit.xcframework/ios-arm64_x86_64-simulator/LCECryptoKit.framework/Modules/LCECryptoKit.swiftmodule/arm64-apple-ios-simulator.private.swiftinterface b/Frameworks/LCECryptoKit.xcframework/ios-arm64_x86_64-simulator/LCECryptoKit.framework/Modules/LCECryptoKit.swiftmodule/arm64-apple-ios-simulator.private.swiftinterface new file mode 100644 index 0000000..f75100d --- /dev/null +++ b/Frameworks/LCECryptoKit.xcframework/ios-arm64_x86_64-simulator/LCECryptoKit.framework/Modules/LCECryptoKit.swiftmodule/arm64-apple-ios-simulator.private.swiftinterface @@ -0,0 +1,22 @@ +// swift-interface-format-version: 1.0 +// swift-compiler-version: Apple Swift version 6.3.3 effective-5.10 (swiftlang-6.3.3.1.3 clang-2100.1.1.101) +// swift-module-flags: -target arm64-apple-ios13.0-simulator -enable-objc-interop -enable-library-evolution -swift-version 5 -O -enable-experimental-feature DebugDescriptionMacro -enable-bare-slash-regex -module-name LCECryptoKit +// swift-module-flags-ignorable: -no-verify-emitted-module-interface -formal-cxx-interoperability-mode=off -interface-compiler-version 6.3.3 +import CommonCrypto +import Foundation +import Swift +import _Concurrency +import _StringProcessing +import _SwiftConcurrencyShims +public struct LCECryptoKit { + public static func generateRandomAESKeyString() -> Swift.String + public static func encodeSeed(email: Swift.String, password: Swift.String) -> Swift.String? + public static func encodeSeed(email: Swift.String, password: Swift.String, hashKey: Swift.String) -> Swift.String? + public static func decodeSeed(otpKey: Swift.String) -> Swift.String? + public static func decodeSeed(otpKey: Swift.String, hashKey: Swift.String) -> Swift.Bool + public static func generateSalt() -> Swift.String + public static func computeClientHash(email: Swift.String, password: Swift.String, salt: Swift.String) -> Swift.String + public static func computeLoginBearerToken(userId: Swift.String, clientHash: Swift.String) -> Swift.String? + public static func otpEncode(_ plainText: Swift.String) -> Swift.String? + public static func otpDecode(_ otpEncoded: Swift.String) -> Swift.String? +} diff --git a/Frameworks/LCECryptoKit.xcframework/ios-arm64_x86_64-simulator/LCECryptoKit.framework/Modules/LCECryptoKit.swiftmodule/arm64-apple-ios-simulator.swiftdoc b/Frameworks/LCECryptoKit.xcframework/ios-arm64_x86_64-simulator/LCECryptoKit.framework/Modules/LCECryptoKit.swiftmodule/arm64-apple-ios-simulator.swiftdoc new file mode 100644 index 0000000..9fa6aa2 Binary files /dev/null and b/Frameworks/LCECryptoKit.xcframework/ios-arm64_x86_64-simulator/LCECryptoKit.framework/Modules/LCECryptoKit.swiftmodule/arm64-apple-ios-simulator.swiftdoc differ diff --git a/Frameworks/LCECryptoKit.xcframework/ios-arm64_x86_64-simulator/LCECryptoKit.framework/Modules/LCECryptoKit.swiftmodule/arm64-apple-ios-simulator.swiftinterface b/Frameworks/LCECryptoKit.xcframework/ios-arm64_x86_64-simulator/LCECryptoKit.framework/Modules/LCECryptoKit.swiftmodule/arm64-apple-ios-simulator.swiftinterface new file mode 100644 index 0000000..f75100d --- /dev/null +++ b/Frameworks/LCECryptoKit.xcframework/ios-arm64_x86_64-simulator/LCECryptoKit.framework/Modules/LCECryptoKit.swiftmodule/arm64-apple-ios-simulator.swiftinterface @@ -0,0 +1,22 @@ +// swift-interface-format-version: 1.0 +// swift-compiler-version: Apple Swift version 6.3.3 effective-5.10 (swiftlang-6.3.3.1.3 clang-2100.1.1.101) +// swift-module-flags: -target arm64-apple-ios13.0-simulator -enable-objc-interop -enable-library-evolution -swift-version 5 -O -enable-experimental-feature DebugDescriptionMacro -enable-bare-slash-regex -module-name LCECryptoKit +// swift-module-flags-ignorable: -no-verify-emitted-module-interface -formal-cxx-interoperability-mode=off -interface-compiler-version 6.3.3 +import CommonCrypto +import Foundation +import Swift +import _Concurrency +import _StringProcessing +import _SwiftConcurrencyShims +public struct LCECryptoKit { + public static func generateRandomAESKeyString() -> Swift.String + public static func encodeSeed(email: Swift.String, password: Swift.String) -> Swift.String? + public static func encodeSeed(email: Swift.String, password: Swift.String, hashKey: Swift.String) -> Swift.String? + public static func decodeSeed(otpKey: Swift.String) -> Swift.String? + public static func decodeSeed(otpKey: Swift.String, hashKey: Swift.String) -> Swift.Bool + public static func generateSalt() -> Swift.String + public static func computeClientHash(email: Swift.String, password: Swift.String, salt: Swift.String) -> Swift.String + public static func computeLoginBearerToken(userId: Swift.String, clientHash: Swift.String) -> Swift.String? + public static func otpEncode(_ plainText: Swift.String) -> Swift.String? + public static func otpDecode(_ otpEncoded: Swift.String) -> Swift.String? +} diff --git a/Frameworks/LCECryptoKit.xcframework/ios-arm64_x86_64-simulator/LCECryptoKit.framework/Modules/LCECryptoKit.swiftmodule/x86_64-apple-ios-simulator.abi.json b/Frameworks/LCECryptoKit.xcframework/ios-arm64_x86_64-simulator/LCECryptoKit.framework/Modules/LCECryptoKit.swiftmodule/x86_64-apple-ios-simulator.abi.json new file mode 100644 index 0000000..32ec755 --- /dev/null +++ b/Frameworks/LCECryptoKit.xcframework/ios-arm64_x86_64-simulator/LCECryptoKit.framework/Modules/LCECryptoKit.swiftmodule/x86_64-apple-ios-simulator.abi.json @@ -0,0 +1,417 @@ +{ + "ABIRoot": { + "kind": "Root", + "name": "LCECryptoKit", + "printedName": "LCECryptoKit", + "children": [ + { + "kind": "Import", + "name": "Foundation", + "printedName": "Foundation", + "declKind": "Import", + "moduleName": "LCECryptoKit" + }, + { + "kind": "Import", + "name": "CommonCrypto", + "printedName": "CommonCrypto", + "declKind": "Import", + "moduleName": "LCECryptoKit" + }, + { + "kind": "TypeDecl", + "name": "LCECryptoKit", + "printedName": "LCECryptoKit", + "children": [ + { + "kind": "Function", + "name": "generateRandomAESKeyString", + "printedName": "generateRandomAESKeyString()", + "children": [ + { + "kind": "TypeNominal", + "name": "String", + "printedName": "Swift.String", + "usr": "s:SS" + } + ], + "declKind": "Func", + "usr": "s:12LCECryptoKitAAV26generateRandomAESKeyStringSSyFZ", + "mangledName": "$s12LCECryptoKitAAV26generateRandomAESKeyStringSSyFZ", + "moduleName": "LCECryptoKit", + "static": true, + "declAttributes": [ + "AccessControl" + ], + "funcSelfKind": "NonMutating" + }, + { + "kind": "Function", + "name": "encodeSeed", + "printedName": "encodeSeed(email:password:)", + "children": [ + { + "kind": "TypeNominal", + "name": "Optional", + "printedName": "Swift.String?", + "children": [ + { + "kind": "TypeNominal", + "name": "String", + "printedName": "Swift.String", + "usr": "s:SS" + } + ], + "usr": "s:Sq" + }, + { + "kind": "TypeNominal", + "name": "String", + "printedName": "Swift.String", + "usr": "s:SS" + }, + { + "kind": "TypeNominal", + "name": "String", + "printedName": "Swift.String", + "usr": "s:SS" + } + ], + "declKind": "Func", + "usr": "s:12LCECryptoKitAAV10encodeSeed5email8passwordSSSgSS_SStFZ", + "mangledName": "$s12LCECryptoKitAAV10encodeSeed5email8passwordSSSgSS_SStFZ", + "moduleName": "LCECryptoKit", + "static": true, + "declAttributes": [ + "AccessControl" + ], + "funcSelfKind": "NonMutating" + }, + { + "kind": "Function", + "name": "encodeSeed", + "printedName": "encodeSeed(email:password:hashKey:)", + "children": [ + { + "kind": "TypeNominal", + "name": "Optional", + "printedName": "Swift.String?", + "children": [ + { + "kind": "TypeNominal", + "name": "String", + "printedName": "Swift.String", + "usr": "s:SS" + } + ], + "usr": "s:Sq" + }, + { + "kind": "TypeNominal", + "name": "String", + "printedName": "Swift.String", + "usr": "s:SS" + }, + { + "kind": "TypeNominal", + "name": "String", + "printedName": "Swift.String", + "usr": "s:SS" + }, + { + "kind": "TypeNominal", + "name": "String", + "printedName": "Swift.String", + "usr": "s:SS" + } + ], + "declKind": "Func", + "usr": "s:12LCECryptoKitAAV10encodeSeed5email8password7hashKeySSSgSS_S2StFZ", + "mangledName": "$s12LCECryptoKitAAV10encodeSeed5email8password7hashKeySSSgSS_S2StFZ", + "moduleName": "LCECryptoKit", + "static": true, + "declAttributes": [ + "AccessControl" + ], + "funcSelfKind": "NonMutating" + }, + { + "kind": "Function", + "name": "decodeSeed", + "printedName": "decodeSeed(otpKey:)", + "children": [ + { + "kind": "TypeNominal", + "name": "Optional", + "printedName": "Swift.String?", + "children": [ + { + "kind": "TypeNominal", + "name": "String", + "printedName": "Swift.String", + "usr": "s:SS" + } + ], + "usr": "s:Sq" + }, + { + "kind": "TypeNominal", + "name": "String", + "printedName": "Swift.String", + "usr": "s:SS" + } + ], + "declKind": "Func", + "usr": "s:12LCECryptoKitAAV10decodeSeed6otpKeySSSgSS_tFZ", + "mangledName": "$s12LCECryptoKitAAV10decodeSeed6otpKeySSSgSS_tFZ", + "moduleName": "LCECryptoKit", + "static": true, + "declAttributes": [ + "AccessControl" + ], + "funcSelfKind": "NonMutating" + }, + { + "kind": "Function", + "name": "decodeSeed", + "printedName": "decodeSeed(otpKey:hashKey:)", + "children": [ + { + "kind": "TypeNominal", + "name": "Bool", + "printedName": "Swift.Bool", + "usr": "s:Sb" + }, + { + "kind": "TypeNominal", + "name": "String", + "printedName": "Swift.String", + "usr": "s:SS" + }, + { + "kind": "TypeNominal", + "name": "String", + "printedName": "Swift.String", + "usr": "s:SS" + } + ], + "declKind": "Func", + "usr": "s:12LCECryptoKitAAV10decodeSeed6otpKey04hashF0SbSS_SStFZ", + "mangledName": "$s12LCECryptoKitAAV10decodeSeed6otpKey04hashF0SbSS_SStFZ", + "moduleName": "LCECryptoKit", + "static": true, + "declAttributes": [ + "AccessControl" + ], + "funcSelfKind": "NonMutating" + }, + { + "kind": "Function", + "name": "generateSalt", + "printedName": "generateSalt()", + "children": [ + { + "kind": "TypeNominal", + "name": "String", + "printedName": "Swift.String", + "usr": "s:SS" + } + ], + "declKind": "Func", + "usr": "s:12LCECryptoKitAAV12generateSaltSSyFZ", + "mangledName": "$s12LCECryptoKitAAV12generateSaltSSyFZ", + "moduleName": "LCECryptoKit", + "static": true, + "declAttributes": [ + "AccessControl", + "RawDocComment" + ], + "funcSelfKind": "NonMutating" + }, + { + "kind": "Function", + "name": "computeClientHash", + "printedName": "computeClientHash(email:password:salt:)", + "children": [ + { + "kind": "TypeNominal", + "name": "String", + "printedName": "Swift.String", + "usr": "s:SS" + }, + { + "kind": "TypeNominal", + "name": "String", + "printedName": "Swift.String", + "usr": "s:SS" + }, + { + "kind": "TypeNominal", + "name": "String", + "printedName": "Swift.String", + "usr": "s:SS" + }, + { + "kind": "TypeNominal", + "name": "String", + "printedName": "Swift.String", + "usr": "s:SS" + } + ], + "declKind": "Func", + "usr": "s:12LCECryptoKitAAV17computeClientHash5email8password4saltS2S_S2StFZ", + "mangledName": "$s12LCECryptoKitAAV17computeClientHash5email8password4saltS2S_S2StFZ", + "moduleName": "LCECryptoKit", + "static": true, + "declAttributes": [ + "AccessControl", + "RawDocComment" + ], + "funcSelfKind": "NonMutating" + }, + { + "kind": "Function", + "name": "computeLoginBearerToken", + "printedName": "computeLoginBearerToken(userId:clientHash:)", + "children": [ + { + "kind": "TypeNominal", + "name": "Optional", + "printedName": "Swift.String?", + "children": [ + { + "kind": "TypeNominal", + "name": "String", + "printedName": "Swift.String", + "usr": "s:SS" + } + ], + "usr": "s:Sq" + }, + { + "kind": "TypeNominal", + "name": "String", + "printedName": "Swift.String", + "usr": "s:SS" + }, + { + "kind": "TypeNominal", + "name": "String", + "printedName": "Swift.String", + "usr": "s:SS" + } + ], + "declKind": "Func", + "usr": "s:12LCECryptoKitAAV23computeLoginBearerToken6userId10clientHashSSSgSS_SStFZ", + "mangledName": "$s12LCECryptoKitAAV23computeLoginBearerToken6userId10clientHashSSSgSS_SStFZ", + "moduleName": "LCECryptoKit", + "static": true, + "declAttributes": [ + "AccessControl", + "RawDocComment" + ], + "funcSelfKind": "NonMutating" + }, + { + "kind": "Function", + "name": "otpEncode", + "printedName": "otpEncode(_:)", + "children": [ + { + "kind": "TypeNominal", + "name": "Optional", + "printedName": "Swift.String?", + "children": [ + { + "kind": "TypeNominal", + "name": "String", + "printedName": "Swift.String", + "usr": "s:SS" + } + ], + "usr": "s:Sq" + }, + { + "kind": "TypeNominal", + "name": "String", + "printedName": "Swift.String", + "usr": "s:SS" + } + ], + "declKind": "Func", + "usr": "s:12LCECryptoKitAAV9otpEncodeySSSgSSFZ", + "mangledName": "$s12LCECryptoKitAAV9otpEncodeySSSgSSFZ", + "moduleName": "LCECryptoKit", + "static": true, + "declAttributes": [ + "AccessControl", + "RawDocComment" + ], + "funcSelfKind": "NonMutating" + }, + { + "kind": "Function", + "name": "otpDecode", + "printedName": "otpDecode(_:)", + "children": [ + { + "kind": "TypeNominal", + "name": "Optional", + "printedName": "Swift.String?", + "children": [ + { + "kind": "TypeNominal", + "name": "String", + "printedName": "Swift.String", + "usr": "s:SS" + } + ], + "usr": "s:Sq" + }, + { + "kind": "TypeNominal", + "name": "String", + "printedName": "Swift.String", + "usr": "s:SS" + } + ], + "declKind": "Func", + "usr": "s:12LCECryptoKitAAV9otpDecodeySSSgSSFZ", + "mangledName": "$s12LCECryptoKitAAV9otpDecodeySSSgSSFZ", + "moduleName": "LCECryptoKit", + "static": true, + "declAttributes": [ + "AccessControl" + ], + "funcSelfKind": "NonMutating" + } + ], + "declKind": "Struct", + "usr": "s:12LCECryptoKitAAV", + "mangledName": "$s12LCECryptoKitAAV", + "moduleName": "LCECryptoKit", + "declAttributes": [ + "AccessControl" + ], + "conformances": [ + { + "kind": "Conformance", + "name": "Copyable", + "printedName": "Copyable", + "usr": "s:s8CopyableP", + "mangledName": "$ss8CopyableP" + }, + { + "kind": "Conformance", + "name": "Escapable", + "printedName": "Escapable", + "usr": "s:s9EscapableP", + "mangledName": "$ss9EscapableP" + } + ] + } + ], + "json_format_version": 8 + }, + "ConstValues": [] +} \ No newline at end of file diff --git a/Frameworks/LCECryptoKit.xcframework/ios-arm64_x86_64-simulator/LCECryptoKit.framework/Modules/LCECryptoKit.swiftmodule/x86_64-apple-ios-simulator.private.swiftinterface b/Frameworks/LCECryptoKit.xcframework/ios-arm64_x86_64-simulator/LCECryptoKit.framework/Modules/LCECryptoKit.swiftmodule/x86_64-apple-ios-simulator.private.swiftinterface new file mode 100644 index 0000000..81da07e --- /dev/null +++ b/Frameworks/LCECryptoKit.xcframework/ios-arm64_x86_64-simulator/LCECryptoKit.framework/Modules/LCECryptoKit.swiftmodule/x86_64-apple-ios-simulator.private.swiftinterface @@ -0,0 +1,22 @@ +// swift-interface-format-version: 1.0 +// swift-compiler-version: Apple Swift version 6.3.3 effective-5.10 (swiftlang-6.3.3.1.3 clang-2100.1.1.101) +// swift-module-flags: -target x86_64-apple-ios13.0-simulator -enable-objc-interop -enable-library-evolution -swift-version 5 -O -enable-experimental-feature DebugDescriptionMacro -enable-bare-slash-regex -module-name LCECryptoKit +// swift-module-flags-ignorable: -no-verify-emitted-module-interface -formal-cxx-interoperability-mode=off -interface-compiler-version 6.3.3 +import CommonCrypto +import Foundation +import Swift +import _Concurrency +import _StringProcessing +import _SwiftConcurrencyShims +public struct LCECryptoKit { + public static func generateRandomAESKeyString() -> Swift.String + public static func encodeSeed(email: Swift.String, password: Swift.String) -> Swift.String? + public static func encodeSeed(email: Swift.String, password: Swift.String, hashKey: Swift.String) -> Swift.String? + public static func decodeSeed(otpKey: Swift.String) -> Swift.String? + public static func decodeSeed(otpKey: Swift.String, hashKey: Swift.String) -> Swift.Bool + public static func generateSalt() -> Swift.String + public static func computeClientHash(email: Swift.String, password: Swift.String, salt: Swift.String) -> Swift.String + public static func computeLoginBearerToken(userId: Swift.String, clientHash: Swift.String) -> Swift.String? + public static func otpEncode(_ plainText: Swift.String) -> Swift.String? + public static func otpDecode(_ otpEncoded: Swift.String) -> Swift.String? +} diff --git a/Frameworks/LCECryptoKit.xcframework/ios-arm64_x86_64-simulator/LCECryptoKit.framework/Modules/LCECryptoKit.swiftmodule/x86_64-apple-ios-simulator.swiftdoc b/Frameworks/LCECryptoKit.xcframework/ios-arm64_x86_64-simulator/LCECryptoKit.framework/Modules/LCECryptoKit.swiftmodule/x86_64-apple-ios-simulator.swiftdoc new file mode 100644 index 0000000..f2ae825 Binary files /dev/null and b/Frameworks/LCECryptoKit.xcframework/ios-arm64_x86_64-simulator/LCECryptoKit.framework/Modules/LCECryptoKit.swiftmodule/x86_64-apple-ios-simulator.swiftdoc differ diff --git a/Frameworks/LCECryptoKit.xcframework/ios-arm64_x86_64-simulator/LCECryptoKit.framework/Modules/LCECryptoKit.swiftmodule/x86_64-apple-ios-simulator.swiftinterface b/Frameworks/LCECryptoKit.xcframework/ios-arm64_x86_64-simulator/LCECryptoKit.framework/Modules/LCECryptoKit.swiftmodule/x86_64-apple-ios-simulator.swiftinterface new file mode 100644 index 0000000..81da07e --- /dev/null +++ b/Frameworks/LCECryptoKit.xcframework/ios-arm64_x86_64-simulator/LCECryptoKit.framework/Modules/LCECryptoKit.swiftmodule/x86_64-apple-ios-simulator.swiftinterface @@ -0,0 +1,22 @@ +// swift-interface-format-version: 1.0 +// swift-compiler-version: Apple Swift version 6.3.3 effective-5.10 (swiftlang-6.3.3.1.3 clang-2100.1.1.101) +// swift-module-flags: -target x86_64-apple-ios13.0-simulator -enable-objc-interop -enable-library-evolution -swift-version 5 -O -enable-experimental-feature DebugDescriptionMacro -enable-bare-slash-regex -module-name LCECryptoKit +// swift-module-flags-ignorable: -no-verify-emitted-module-interface -formal-cxx-interoperability-mode=off -interface-compiler-version 6.3.3 +import CommonCrypto +import Foundation +import Swift +import _Concurrency +import _StringProcessing +import _SwiftConcurrencyShims +public struct LCECryptoKit { + public static func generateRandomAESKeyString() -> Swift.String + public static func encodeSeed(email: Swift.String, password: Swift.String) -> Swift.String? + public static func encodeSeed(email: Swift.String, password: Swift.String, hashKey: Swift.String) -> Swift.String? + public static func decodeSeed(otpKey: Swift.String) -> Swift.String? + public static func decodeSeed(otpKey: Swift.String, hashKey: Swift.String) -> Swift.Bool + public static func generateSalt() -> Swift.String + public static func computeClientHash(email: Swift.String, password: Swift.String, salt: Swift.String) -> Swift.String + public static func computeLoginBearerToken(userId: Swift.String, clientHash: Swift.String) -> Swift.String? + public static func otpEncode(_ plainText: Swift.String) -> Swift.String? + public static func otpDecode(_ otpEncoded: Swift.String) -> Swift.String? +} diff --git a/Frameworks/LCECryptoKit.xcframework/ios-arm64_x86_64-simulator/LCECryptoKit.framework/Modules/module.modulemap b/Frameworks/LCECryptoKit.xcframework/ios-arm64_x86_64-simulator/LCECryptoKit.framework/Modules/module.modulemap new file mode 100644 index 0000000..cccd3c0 --- /dev/null +++ b/Frameworks/LCECryptoKit.xcframework/ios-arm64_x86_64-simulator/LCECryptoKit.framework/Modules/module.modulemap @@ -0,0 +1,3 @@ +framework module LCECryptoKit { + header "LCECryptoKit-Swift.h" +} diff --git a/Frameworks/LCECryptoKit.xcframework/ios-arm64_x86_64-simulator/LCECryptoKit.framework/_CodeSignature/CodeResources b/Frameworks/LCECryptoKit.xcframework/ios-arm64_x86_64-simulator/LCECryptoKit.framework/_CodeSignature/CodeResources new file mode 100644 index 0000000..0e9463b --- /dev/null +++ b/Frameworks/LCECryptoKit.xcframework/ios-arm64_x86_64-simulator/LCECryptoKit.framework/_CodeSignature/CodeResources @@ -0,0 +1,234 @@ + + + + + files + + Headers/LCECryptoKit-Swift.h + + uIM6/aOz59qnT/jGSBAiOinS2qo= + + Info.plist + + N0tkj+ldmO7dEKF+W1/6CjYNBrg= + + Modules/LCECryptoKit.swiftmodule/arm64-apple-ios-simulator.abi.json + + mIejxCLstZ77ufLnGayqr8wDScc= + + Modules/LCECryptoKit.swiftmodule/arm64-apple-ios-simulator.private.swiftinterface + + GJVZwAs/SRyyfGkg1t2Twb8grXE= + + Modules/LCECryptoKit.swiftmodule/arm64-apple-ios-simulator.swiftdoc + + vDvqI7FV3rThZRI4r/zPL877Z04= + + Modules/LCECryptoKit.swiftmodule/arm64-apple-ios-simulator.swiftinterface + + GJVZwAs/SRyyfGkg1t2Twb8grXE= + + Modules/LCECryptoKit.swiftmodule/arm64-apple-ios-simulator.swiftmodule + + 0n6YBQBivkPuELjPpXBIj0CQFIc= + + Modules/LCECryptoKit.swiftmodule/x86_64-apple-ios-simulator.abi.json + + mIejxCLstZ77ufLnGayqr8wDScc= + + Modules/LCECryptoKit.swiftmodule/x86_64-apple-ios-simulator.private.swiftinterface + + WheGrMD0QDYsEwZ2vOGY+CQwW1o= + + Modules/LCECryptoKit.swiftmodule/x86_64-apple-ios-simulator.swiftdoc + + f52HXxbH78iRln4x78whSwyigbc= + + Modules/LCECryptoKit.swiftmodule/x86_64-apple-ios-simulator.swiftinterface + + WheGrMD0QDYsEwZ2vOGY+CQwW1o= + + Modules/LCECryptoKit.swiftmodule/x86_64-apple-ios-simulator.swiftmodule + + FSzeAs9LdtELeWT3VL2PMjOFZcQ= + + Modules/module.modulemap + + OnB7ckFjsSU10c/y8kUFi69Cav4= + + + files2 + + Headers/LCECryptoKit-Swift.h + + hash2 + + gHRFTmnGK28MYikS6gvnWErwc/oZ/scYeFssVTBw9MI= + + + Modules/LCECryptoKit.swiftmodule/arm64-apple-ios-simulator.abi.json + + hash2 + + /2qkU9Pje/wZPzf5sExK+0bliYzKjDZM7iqyIusKGGk= + + + Modules/LCECryptoKit.swiftmodule/arm64-apple-ios-simulator.private.swiftinterface + + hash2 + + BsY3SInOh3u6xj+kXxauIrgLi4cTqE1mY6weF7QOk2g= + + + Modules/LCECryptoKit.swiftmodule/arm64-apple-ios-simulator.swiftdoc + + hash2 + + Ldu7EDkaW9WinMjRHaBaSxpZoBNDXGvNn04410k9e5k= + + + Modules/LCECryptoKit.swiftmodule/arm64-apple-ios-simulator.swiftinterface + + hash2 + + BsY3SInOh3u6xj+kXxauIrgLi4cTqE1mY6weF7QOk2g= + + + Modules/LCECryptoKit.swiftmodule/arm64-apple-ios-simulator.swiftmodule + + hash2 + + cuezjPqokn+YGP9z//TY4ZUxA1Dcmd6ApUafMMaQPAQ= + + + Modules/LCECryptoKit.swiftmodule/x86_64-apple-ios-simulator.abi.json + + hash2 + + /2qkU9Pje/wZPzf5sExK+0bliYzKjDZM7iqyIusKGGk= + + + Modules/LCECryptoKit.swiftmodule/x86_64-apple-ios-simulator.private.swiftinterface + + hash2 + + lJH2d+h2iS4AwqKh/vWgSYIGEelZHLrCNQVlcCFB3V4= + + + Modules/LCECryptoKit.swiftmodule/x86_64-apple-ios-simulator.swiftdoc + + hash2 + + 5S2/W4WoD0L4bl+NBuPQI7sGxqpZ7y2s7tme6Eu2oIQ= + + + Modules/LCECryptoKit.swiftmodule/x86_64-apple-ios-simulator.swiftinterface + + hash2 + + lJH2d+h2iS4AwqKh/vWgSYIGEelZHLrCNQVlcCFB3V4= + + + Modules/LCECryptoKit.swiftmodule/x86_64-apple-ios-simulator.swiftmodule + + hash2 + + a5uCPhYWs0qR1YWQZb51vKm+4mYKUgc5ajZr2wEU6tU= + + + Modules/module.modulemap + + hash2 + + X+gHfuxKBEgqt+p9I9kR0FYyzml9UPuJpKzZAbPNcc0= + + + + rules + + ^.* + + ^.*\.lproj/ + + optional + + weight + 1000 + + ^.*\.lproj/locversion.plist$ + + omit + + weight + 1100 + + ^Base\.lproj/ + + weight + 1010 + + ^version.plist$ + + + rules2 + + .*\.dSYM($|/) + + weight + 11 + + ^(.*/)?\.DS_Store$ + + omit + + weight + 2000 + + ^.* + + ^.*\.lproj/ + + optional + + weight + 1000 + + ^.*\.lproj/locversion.plist$ + + omit + + weight + 1100 + + ^Base\.lproj/ + + weight + 1010 + + ^Info\.plist$ + + omit + + weight + 20 + + ^PkgInfo$ + + omit + + weight + 20 + + ^embedded\.provisionprofile$ + + weight + 20 + + ^version\.plist$ + + weight + 20 + + + + diff --git a/Package.resolved b/Package.resolved deleted file mode 100644 index 563f2c1..0000000 --- a/Package.resolved +++ /dev/null @@ -1,15 +0,0 @@ -{ - "originHash" : "6e3fcf8724d6b7d2d0bbffc25b4ec3b9d8b65a6ef53d157498d5447fd26cf5db", - "pins" : [ - { - "identity" : "lcecryptokitbinary", - "kind" : "remoteSourceControl", - "location" : "https://7876d8abdb7260a455259d01de49c4022f5da13c@git.loverde.com.br/Loverde-Company-LTDA/LCECryptoKitBinary.git", - "state" : { - "revision" : "11f66b1b9785b4965e027c2fa4742f61e8453403", - "version" : "1.0.4" - } - } - ], - "version" : 3 -} diff --git a/Package.swift b/Package.swift index d5c1b1b..02e19d1 100644 --- a/Package.swift +++ b/Package.swift @@ -1,46 +1,54 @@ // swift-tools-version: 6.0 import PackageDescription -import Foundation -let isLocalDevelopment = false //FileManager.default.fileExists(atPath: "../LCECryptoKit/PrivateLib/LCECryptoKitBinary") -let enableCryptoBinary = ProcessInfo.processInfo.environment["LCE_ENABLE_CRYPTO_BINARY"] != "0" - -let cryptoPackageURL = isLocalDevelopment - ? "../LCECryptoKit/PrivateLib/LCECryptoKitBinary" - : "https://7876d8abdb7260a455259d01de49c4022f5da13c@git.loverde.com.br/Loverde-Company-LTDA/LCECryptoKitBinary.git" - -let packageDependencies: [Package.Dependency] = enableCryptoBinary - ? [ - .package(url: cryptoPackageURL, exact: "1.0.4") - ] - : [] - -let targetDependencies: [Target.Dependency] = enableCryptoBinary - ? [ - .product(name: "LCECryptoKit", package: "lcecryptokitbinary") - ] - : [] +// LCECryptoKit ships as a prebuilt .xcframework (`Frameworks/LCECryptoKit.xcframework`) — +// vendored locally in this repo, no remote package dependency. iOS device + simulator slices +// only. +// +// Sub-SPM rule: `LCEssentials` installs standalone (no sub required). Every sub +// (`LCECryptoKit`, `LCFeatureControl`) target-depends on `LCEssentials`, so linking the sub's +// product always pulls `LCEssentials` in too — a consumer never has to declare it separately. +// +// Platforms are iOS + watchOS only: `LCEssentials.API` (used by `LCFeatureControl`) is +// `#if os(iOS) || os(watchOS)`, and the vendored xcframework has no macOS/tvOS slice. let package = Package( name: "LCEssentials", platforms: [ .iOS(.v15), - .macOS(.v10_15), - .tvOS(.v13), .watchOS(.v8) ], products: [ .library( name: "LCEssentials", targets: ["LCEssentials"]), + .library( + name: "LCECryptoKit", + targets: ["LCECryptoKitManager"]), + .library( + name: "LCFeatureControl", + targets: ["LCFeatureControl"]), ], - dependencies: packageDependencies, targets: [ .target( - name: "LCEssentials", - dependencies: targetDependencies), + name: "LCEssentials"), + .binaryTarget( + name: "LCECryptoKit", + path: "Frameworks/LCECryptoKit.xcframework"), + .target( + name: "LCECryptoKitManager", + dependencies: [ + "LCEssentials", + "LCECryptoKit" + ]), + .target( + name: "LCFeatureControl", + dependencies: ["LCEssentials"]), .testTarget( name: "LCEssentialsTests", dependencies: ["LCEssentials"]), + .testTarget( + name: "LCFeatureControlTests", + dependencies: ["LCFeatureControl", "LCEssentials"]), ] ) diff --git a/README.md b/README.md index 0900419..d7599e2 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,7 @@ import LCEssentials | **[Extensions.md](Documentation/Extensions.md)** | Foundation / value-type / string / collection / numeric / date / crypto extensions and the `LCEssentials` namespace | | **[SwiftUI.md](Documentation/SwiftUI.md)** | SwiftUI components (`LCENavigationView`) and `View` helpers | | **[UIKit.md](Documentation/UIKit.md)** | Programmatic layout & constraints, view/control extensions, navigation, tables, and drop-in components (`LCSnackBarView`, image picker/zoom, GIF loading) | +| **[FeatureControl.md](Documentation/FeatureControl.md)** | `LCFeatureControl` — Atomenta Feature Control client: flag evaluation, cache/safe-degrade, notifications inbox, exposure telemetry | --- diff --git a/Sources/LCEssentials/Classes/LCECrypto.swift b/Sources/LCECryptoKitManager/LCECryptoKitManager.swift similarity index 68% rename from Sources/LCEssentials/Classes/LCECrypto.swift rename to Sources/LCECryptoKitManager/LCECryptoKitManager.swift index 731d61f..e5428cf 100644 --- a/Sources/LCEssentials/Classes/LCECrypto.swift +++ b/Sources/LCECryptoKitManager/LCECryptoKitManager.swift @@ -1,4 +1,4 @@ -// +// // Copyright (c) 2025 Loverde Co. // // Permission is hereby granted, free of charge, to any person obtaining a copy @@ -18,43 +18,41 @@ // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. - import Foundation - -#if canImport(LCECryptoKit) +import LCEssentials import LCECryptoKit public final class LCECryptoKitManager { - + private let hashKey: String - + public init() { self.hashKey = "" } - - public init(privateKey: String){ + + public init(privateKey: String) { self.hashKey = privateKey } - + public static func generateKey() -> String { LCECryptoKit.generateRandomAESKeyString() } - + public func encodeTP(email: String, password: String) -> String? { return LCECryptoKit.encodeSeed(email: email, password: password) } - + public func decodeOTP(_ otpHash: String) -> String? { return LCECryptoKit.decodeSeed(otpKey: otpHash) } - + // MARK: Need hashKey to decode - + public func encodeOTPWithKey(email: String, password: String) -> String? { return LCECryptoKit.encodeSeed(email: email, password: password, hashKey: self.hashKey) } - + public func decodeOTPWithKey(_ otpHash: String) -> Bool { LCECryptoKit.decodeSeed(otpKey: otpHash, hashKey: self.hashKey) } @@ -81,60 +79,3 @@ public final class LCECryptoKitManager { LCECryptoKit.otpDecode(otpEncoded) } } -#else - -public final class LCECryptoKitManager { - - private let hashKey: String - - public init() { - self.hashKey = "" - } - - public init(privateKey: String){ - self.hashKey = privateKey - } - - public static func generateKey() -> String { - "" - } - - public func encodeTP(email: String, password: String) -> String? { - nil - } - - public func decodeOTP(_ otpHash: String) -> String? { - nil - } - - public func encodeOTPWithKey(email: String, password: String) -> String? { - nil - } - - public func decodeOTPWithKey(_ otpHash: String) -> Bool { - false - } - - // MARK: - Salted/Iterated/Peppered Login (atomenta-cryptokit-pepper-refactor-sdd.md) - - public static func generateSalt() -> String { - "" - } - - public static func computeClientHash(email: String, password: String, salt: String) -> String { - "" - } - - public static func computeLoginBearerToken(userId: String, clientHash: String) -> String? { - nil - } - - public static func otpEncode(_ plainText: String) -> String? { - nil - } - - public static func otpDecode(_ otpEncoded: String) -> String? { - nil - } -} -#endif diff --git a/Sources/LCFeatureControl/FeatureControlAuthorizing.swift b/Sources/LCFeatureControl/FeatureControlAuthorizing.swift new file mode 100644 index 0000000..8fd3124 --- /dev/null +++ b/Sources/LCFeatureControl/FeatureControlAuthorizing.swift @@ -0,0 +1,39 @@ +import Foundation + +/// Pluggable auth for outgoing Feature Control requests. No `Atomenta-Token` type +/// exists here on purpose — that module token must never be embedded in a +/// customer-facing app (see FC-060 §1 in Atomenta's `docs/feature-control/`). +/// A consumer who insists on it does so explicitly via `FeatureControlHeaderAuth`. +public protocol FeatureControlAuthorizing: Sendable { + func authorize(_ headers: inout [String: String]) async +} + +/// For internal/admin apps hitting Atomenta directly with a panel-role JWT. +public struct FeatureControlBearerAuth: FeatureControlAuthorizing { + private let tokenProvider: @Sendable () async -> String? + + public init(tokenProvider: @escaping @Sendable () async -> String?) { + self.tokenProvider = tokenProvider + } + + public func authorize(_ headers: inout [String: String]) async { + guard let token = await tokenProvider() else { return } + headers["Authorization"] = "Bearer \(token)" + } +} + +/// For a customer app calling its own BFF, which enforces its own auth. Adds +/// exactly the given headers — never synthesizes an `Authorization` header. +public struct FeatureControlHeaderAuth: FeatureControlAuthorizing { + private let headers: [String: String] + + public init(headers: [String: String]) { + self.headers = headers + } + + public func authorize(_ headers: inout [String: String]) async { + for (key, value) in self.headers { + headers[key] = value + } + } +} diff --git a/Sources/LCFeatureControl/FeatureControlCache.swift b/Sources/LCFeatureControl/FeatureControlCache.swift new file mode 100644 index 0000000..a519854 --- /dev/null +++ b/Sources/LCFeatureControl/FeatureControlCache.swift @@ -0,0 +1,46 @@ +import Foundation + +/// Identifies a cached evaluation — FC-060 §5: "environment + subjectId + keys(sorted) + +/// platform + appVersion". `sortedKeys` means caller `keys` order never affects hits. +struct FeatureControlCacheKey: Hashable, Sendable { + let environment: String + let subjectId: String + let sortedKeys: [String] + let platform: String? + let appVersion: String? +} + +/// In-memory-only TTL cache (no disk persistence — see SDD §2 Non-Goals). The clock is +/// injectable so tests can force expiry deterministically instead of sleeping. +actor FeatureControlCache { + private struct Entry { + let snapshot: FeatureControlSnapshot + let storedAt: Date + let ttl: TimeInterval + } + + private var entries: [FeatureControlCacheKey: Entry] = [:] + private let now: @Sendable () -> Date + + init(now: @escaping @Sendable () -> Date = { Date() }) { + self.now = now + } + + /// `allowStale: true` returns an expired entry rather than `nil` — used by the + /// safe-degrade fallback path (SDD §4.4/§4.5). + func get(_ key: FeatureControlCacheKey, allowStale: Bool) -> FeatureControlSnapshot? { + guard let entry = entries[key] else { return nil } + let age = now().timeIntervalSince(entry.storedAt) + if age <= entry.ttl { return entry.snapshot } + return allowStale ? entry.snapshot : nil + } + + /// Always overwrites — a fresh fetch's `configVersion` simply replaces whatever was there. + func set(_ key: FeatureControlCacheKey, snapshot: FeatureControlSnapshot, ttl: TimeInterval) { + entries[key] = Entry(snapshot: snapshot, storedAt: now(), ttl: ttl) + } + + func invalidateAll() { + entries.removeAll() + } +} diff --git a/Sources/LCFeatureControl/FeatureControlConfiguration.swift b/Sources/LCFeatureControl/FeatureControlConfiguration.swift new file mode 100644 index 0000000..10ecc45 --- /dev/null +++ b/Sources/LCFeatureControl/FeatureControlConfiguration.swift @@ -0,0 +1,33 @@ +import Foundation + +/// Wiring for one Atomenta Feature Control module deployment. +public struct FeatureControlConfiguration: Sendable { + public var baseURL: String + public var evaluatePath: String + public var notificationsPath: String + public var telemetryPath: String + public var environment: String + public var auth: any FeatureControlAuthorizing + /// In-memory cache TTL. FC-060 §5 recommends 30–60s client-side. + public var cacheTTL: TimeInterval + /// FC-060 §5 recommends 1.5–3s at the BFF; same budget applies here. + public var requestTimeout: TimeInterval + + public init(baseURL: String, + environment: String, + auth: any FeatureControlAuthorizing, + evaluatePath: String = "/api/feature-control/evaluate", + notificationsPath: String = "/api/feature-control/notifications", + telemetryPath: String = "/api/feature-control/telemetry/exposure", + cacheTTL: TimeInterval = 45, + requestTimeout: TimeInterval = 3) { + self.baseURL = baseURL + self.environment = environment + self.auth = auth + self.evaluatePath = evaluatePath + self.notificationsPath = notificationsPath + self.telemetryPath = telemetryPath + self.cacheTTL = cacheTTL + self.requestTimeout = requestTimeout + } +} diff --git a/Sources/LCFeatureControl/FeatureControlContext.swift b/Sources/LCFeatureControl/FeatureControlContext.swift new file mode 100644 index 0000000..89787d0 --- /dev/null +++ b/Sources/LCFeatureControl/FeatureControlContext.swift @@ -0,0 +1,30 @@ +import Foundation + +/// Who/what a flag evaluation is for — mirrors Atomenta's `context.subjectType` enum. +public enum FeatureControlSubjectType: String, Codable, Sendable { + case user, customer, store, anonymous +} + +/// Evaluation context sent as `context` in `POST /api/feature-control/evaluate`. +public struct FeatureControlContext: Encodable, Sendable, Equatable { + public var subjectType: FeatureControlSubjectType + public var subjectId: String + public var storeId: String? + public var platform: String? + public var appVersion: String? + public var attributes: [String: String]? + + public init(subjectType: FeatureControlSubjectType, + subjectId: String, + storeId: String? = nil, + platform: String? = nil, + appVersion: String? = nil, + attributes: [String: String]? = nil) { + self.subjectType = subjectType + self.subjectId = subjectId + self.storeId = storeId + self.platform = platform + self.appVersion = appVersion + self.attributes = attributes + } +} diff --git a/Sources/LCFeatureControl/FeatureControlDateParsing.swift b/Sources/LCFeatureControl/FeatureControlDateParsing.swift new file mode 100644 index 0000000..f66095e --- /dev/null +++ b/Sources/LCFeatureControl/FeatureControlDateParsing.swift @@ -0,0 +1,18 @@ +import Foundation + +/// Parses the ISO-8601 timestamps Atomenta sends (`2026-04-16T12:00:00.000Z`, with +/// milliseconds). `LCEssentials.API`'s internal `JSONDecoder` uses the default +/// (`.deferredToDate`, numeric epoch) strategy, so `Date` fields on wire models +/// decode the raw string manually instead of relying on `Decodable`'s default +/// date handling. +enum FeatureControlDateParsing { + static func parse(_ string: String) -> Date? { + let withFractional = ISO8601DateFormatter() + withFractional.formatOptions = [.withInternetDateTime, .withFractionalSeconds] + if let date = withFractional.date(from: string) { return date } + + let plain = ISO8601DateFormatter() + plain.formatOptions = [.withInternetDateTime] + return plain.date(from: string) + } +} diff --git a/Sources/LCFeatureControl/FeatureControlError.swift b/Sources/LCFeatureControl/FeatureControlError.swift new file mode 100644 index 0000000..2bf0876 --- /dev/null +++ b/Sources/LCFeatureControl/FeatureControlError.swift @@ -0,0 +1,51 @@ +import Foundation +import LCEssentials + +/// Explicit failure for callers that want to see it (`evaluateOrThrow`, the +/// notifications client). The safe `evaluate(...)` path never surfaces this — +/// it degrades to cache/defaults instead (FC-060 §4). +public enum FeatureControlError: Error, Sendable, Equatable { + /// 400 `FEATURE_CONTROL_CONTEXT_INVALID` + case invalidContext(code: String) + /// 401 `…MISSING_AUTH` / `…INVALID_MODULE_TOKEN` + case unauthorized(code: String) + /// 403 `…PANEL_ROLE_REQUIRED` / `…INSUFFICIENT_PERMISSIONS` + case forbidden(code: String) + /// 429 `FEATURE_CONTROL_RATE_LIMIT` + case rateLimited(code: String) + /// Any other non-2xx status. + case server(code: String, status: Int) + /// Decoding failure, or any error not raised by `LCEssentials.API`'s HTTP path. + case transport(message: String) +} + +enum FeatureControlErrorMapper { + /// `LCEssentials.API` throws an `NSError` (domain `LCEssentials.DEFAULT_ERROR_DOMAIN`, + /// `code` = HTTP status, `localizedFailureReason` = pretty-printed body) for non-2xx + /// responses, or a bridged `DecodingError`/`URLError` for anything else. Only the + /// former carries a real HTTP status to map. + static func map(_ error: Error) -> FeatureControlError { + let nsError = error as NSError + guard nsError.domain == LCEssentials.DEFAULT_ERROR_DOMAIN else { + return .transport(message: nsError.localizedDescription) + } + + let status = nsError.code + let bodyCode = extractCode(from: nsError.localizedFailureReason) ?? "UNKNOWN" + switch status { + case 400: return .invalidContext(code: bodyCode) + case 401: return .unauthorized(code: bodyCode) + case 403: return .forbidden(code: bodyCode) + case 429: return .rateLimited(code: bodyCode) + default: return .server(code: bodyCode, status: status) + } + } + + private static func extractCode(from prettyJSON: String?) -> String? { + guard let prettyJSON, + let data = prettyJSON.data(using: .utf8), + let object = try? JSONSerialization.jsonObject(with: data) as? [String: Any], + let code = object["code"] as? String else { return nil } + return code + } +} diff --git a/Sources/LCFeatureControl/FeatureControlExposureEvent.swift b/Sources/LCFeatureControl/FeatureControlExposureEvent.swift new file mode 100644 index 0000000..d8f6523 --- /dev/null +++ b/Sources/LCFeatureControl/FeatureControlExposureEvent.swift @@ -0,0 +1,28 @@ +import Foundation + +/// One exposure event for `POST /api/feature-control/telemetry/exposure`. +public struct FeatureControlExposureEvent: Encodable, Sendable, Equatable { + public let featureKey: String + public let variant: String? + public let subjectType: FeatureControlSubjectType + public let storeId: String? + + public init(featureKey: String, variant: String?, subjectType: FeatureControlSubjectType, storeId: String?) { + self.featureKey = featureKey + self.variant = variant + self.subjectType = subjectType + self.storeId = storeId + } +} + +/// Request body — server caps `events` at 100 (`maxItems: 100`); batching into that +/// limit is the caller's (`FeatureControlManager`'s) job, not this type's. +struct FeatureControlExposureBatchBody: Encodable, Sendable { + let events: [FeatureControlExposureEvent] +} + +/// Wire envelope for the telemetry response — `{error, code, result: {count}}`. +struct FeatureControlExposureBatchEnvelope: Decodable, Sendable { + let error: Bool + let code: String? +} diff --git a/Sources/LCFeatureControl/FeatureControlFlag.swift b/Sources/LCFeatureControl/FeatureControlFlag.swift new file mode 100644 index 0000000..8e19f65 --- /dev/null +++ b/Sources/LCFeatureControl/FeatureControlFlag.swift @@ -0,0 +1,61 @@ +import Foundation + +/// One evaluated flag, as returned inside `EvaluateResponse.result.flags[key]`. +public struct FeatureControlFlag: Decodable, Sendable, Equatable { + public let enabled: Bool + public let variant: String? + public let payload: FeatureControlJSON? + public let reason: String? + + public init(enabled: Bool, variant: String?, payload: FeatureControlJSON?, reason: String?) { + self.enabled = enabled + self.variant = variant + self.payload = payload + self.reason = reason + } +} + +/// `EvaluateResponse.result` — the batch evaluation result for a set of keys. +public struct FeatureControlSnapshot: Sendable, Equatable { + public let evaluatedAt: Date + public let configVersion: Int + public let flags: [String: FeatureControlFlag] + + public init(evaluatedAt: Date, configVersion: Int, flags: [String: FeatureControlFlag]) { + self.evaluatedAt = evaluatedAt + self.configVersion = configVersion + self.flags = flags + } +} + +extension FeatureControlSnapshot: Decodable { + private enum CodingKeys: String, CodingKey { + case evaluatedAt, configVersion, flags + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + let rawDate = try container.decode(String.self, forKey: .evaluatedAt) + guard let date = FeatureControlDateParsing.parse(rawDate) else { + throw DecodingError.dataCorruptedError(forKey: .evaluatedAt, in: container, + debugDescription: "Unrecognized date format: \(rawDate)") + } + self.evaluatedAt = date + self.configVersion = try container.decode(Int.self, forKey: .configVersion) + self.flags = try container.decode([String: FeatureControlFlag].self, forKey: .flags) + } +} + +/// Wire envelope for `POST /api/feature-control/evaluate` — `{error, code, result}`. +struct FeatureControlEvaluateEnvelope: Decodable, Sendable { + let error: Bool + let code: String? + let result: FeatureControlSnapshot +} + +/// Request body for `POST /api/feature-control/evaluate`. +struct FeatureControlEvaluateRequestBody: Encodable, Sendable { + let environment: String + let keys: [String] + let context: FeatureControlContext +} diff --git a/Sources/LCFeatureControl/FeatureControlJSON.swift b/Sources/LCFeatureControl/FeatureControlJSON.swift new file mode 100644 index 0000000..3e33f71 --- /dev/null +++ b/Sources/LCFeatureControl/FeatureControlJSON.swift @@ -0,0 +1,33 @@ +import Foundation + +/// Minimal "any JSON" box for a flag's `payload`, which the backend declares as +/// `additionalProperties: true` (arbitrary shape). No force operations — an +/// unrecognized shape throws a `DecodingError`, it never crashes. +public indirect enum FeatureControlJSON: Decodable, Sendable, Equatable { + case null + case bool(Bool) + case number(Double) + case string(String) + case array([FeatureControlJSON]) + case object([String: FeatureControlJSON]) + + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + if container.decodeNil() { + self = .null + } else if let value = try? container.decode(Bool.self) { + self = .bool(value) + } else if let value = try? container.decode(Double.self) { + self = .number(value) + } else if let value = try? container.decode(String.self) { + self = .string(value) + } else if let value = try? container.decode([FeatureControlJSON].self) { + self = .array(value) + } else if let value = try? container.decode([String: FeatureControlJSON].self) { + self = .object(value) + } else { + throw DecodingError.dataCorruptedError(in: container, + debugDescription: "Unsupported JSON value for FeatureControlJSON") + } + } +} diff --git a/Sources/LCFeatureControl/FeatureControlManager.swift b/Sources/LCFeatureControl/FeatureControlManager.swift new file mode 100644 index 0000000..b4adc36 --- /dev/null +++ b/Sources/LCFeatureControl/FeatureControlManager.swift @@ -0,0 +1,142 @@ +import Foundation +import LCEssentials + +/// Protocol seam for DI — ViewModels/Interactors depend on this, never on the +/// concrete actor, so a mock can stand in for tests. +public protocol FeatureControlEvaluating: Sendable { + /// Never throws. Fresh fetch → stale cache → app-supplied `defaults`, in that + /// order (FC-060 §4 "nunca bloquear fluxo crítico"). Use this everywhere a + /// flag gates real product behaviour. + func evaluate(keys: [String], context: FeatureControlContext) async -> FeatureControlSnapshot + /// Same fetch, but surfaces the real failure. For admin/debug tooling only. + func evaluateOrThrow(keys: [String], context: FeatureControlContext) async throws -> FeatureControlSnapshot + /// Drops every cached entry. Call on user/store change, or app foreground if desired — + /// this type does not observe app lifecycle itself (SDD §2 Non-Goals). + func invalidateCache() async +} + +public actor FeatureControlManager: FeatureControlEvaluating { + private let configuration: FeatureControlConfiguration + private let api: API + private let defaults: [String: FeatureControlFlag] + private let cache: FeatureControlCache + private var exposureBuffer: [FeatureControlExposureEvent] = [] + /// Coalesces concurrent cold-cache callers onto one in-flight request per key, + /// instead of firing one POST per caller (which was hammering the 429 limit). + private var inFlight: [FeatureControlCacheKey: Task] = [:] + + /// `configVersion` sentinel returned when no network response and no cache exist — + /// distinguishes "never evaluated" from any real server value (server versions are ≥ 0). + public static let unresolvedConfigVersion = -1 + + public init(configuration: FeatureControlConfiguration, + api: API = .shared, + defaults: [String: FeatureControlFlag] = [:], + now: @escaping @Sendable () -> Date = { Date() }) { + self.configuration = configuration + self.api = api + self.defaults = defaults + self.cache = FeatureControlCache(now: now) + } + + public func evaluate(keys: [String], context: FeatureControlContext) async -> FeatureControlSnapshot { + do { + return try await evaluateOrThrow(keys: keys, context: context) + } catch { + let key = cacheKey(keys: keys, context: context) + if let stale = await cache.get(key, allowStale: true) { return stale } + return FeatureControlSnapshot(evaluatedAt: Date(), + configVersion: Self.unresolvedConfigVersion, + flags: defaults) + } + } + + public func evaluateOrThrow(keys: [String], context: FeatureControlContext) async throws -> FeatureControlSnapshot { + let key = cacheKey(keys: keys, context: context) + if let fresh = await cache.get(key, allowStale: false) { return fresh } + + if let inFlightTask = inFlight[key] { + return try await inFlightTask.value + } + + let task = Task { + var headers: [String: String] = [:] + await self.configuration.auth.authorize(&headers) + + let body = jsonBody(FeatureControlEvaluateRequestBody( + environment: self.configuration.environment, keys: keys, context: context)) + + do { + let envelope: FeatureControlEvaluateEnvelope = try await self.api.request( + url: self.configuration.baseURL + self.configuration.evaluatePath, + method: .post, + body: body, + headers: headers, + debug: false, + timeoutInterval: self.configuration.requestTimeout + ) + guard !envelope.error else { + throw FeatureControlError.server(code: envelope.code ?? "UNKNOWN", status: 200) + } + await self.cache.set(key, snapshot: envelope.result, ttl: self.configuration.cacheTTL) + return envelope.result + } catch let error as FeatureControlError { + throw error + } catch { + throw FeatureControlErrorMapper.map(error) + } + } + inFlight[key] = task + defer { inFlight[key] = nil } + return try await task.value + } + + public func invalidateCache() async { + await cache.invalidateAll() + } + + private func cacheKey(keys: [String], context: FeatureControlContext) -> FeatureControlCacheKey { + FeatureControlCacheKey(environment: configuration.environment, + subjectId: context.subjectId, + sortedKeys: keys.sorted(), + platform: context.platform, + appVersion: context.appVersion) + } +} + +// MARK: - Exposure telemetry + +extension FeatureControlManager { + /// Buffers locally. Nothing is sent until `flushExposures()` is called. + public func recordExposure(_ event: FeatureControlExposureEvent) { + exposureBuffer.append(event) + } + + /// Best-effort — batches of ≤100 (server `maxItems: 100`); a failed batch is + /// dropped, never retried indefinitely (not a critical-path operation). + public func flushExposures() async { + guard !exposureBuffer.isEmpty else { return } + let events = exposureBuffer + exposureBuffer.removeAll() + + for batch in events.lce_featureControlChunked(into: 100) { + var headers: [String: String] = [:] + await configuration.auth.authorize(&headers) + let body = jsonBody(FeatureControlExposureBatchBody(events: batch)) + _ = try? await api.request( + url: configuration.baseURL + configuration.telemetryPath, + method: .post, + body: body, + headers: headers, + debug: false, + timeoutInterval: configuration.requestTimeout + ) as FeatureControlExposureBatchEnvelope + } + } +} + +private extension Array { + func lce_featureControlChunked(into size: Int) -> [[Element]] { + stride(from: 0, to: count, by: size).map { Array(self[$0.. (items: [FeatureControlNotification], nextCursor: String?) + func markRead(id: String) async throws + func markAllRead() async throws +} + +/// JWT-only — the OpenAPI fragment declares `security: [bearerAuth]` for every +/// notifications route, no module-token alternative. Configure `configuration.auth` +/// with `FeatureControlBearerAuth`; anything else gets a `401` from the server, +/// surfaced as `FeatureControlError.unauthorized`. +public actor FeatureControlNotificationsClient: FeatureControlNotifying { + private let configuration: FeatureControlConfiguration + private let api: API + + public init(configuration: FeatureControlConfiguration, api: API = .shared) { + self.configuration = configuration + self.api = api + } + + public func list(status: FeatureControlNotificationStatus = .all, limit: Int = 20, cursor: String? = nil) async throws + -> (items: [FeatureControlNotification], nextCursor: String?) { + var headers: [String: String] = [:] + await configuration.auth.authorize(&headers) + + var query = "?status=\(status.rawValue)&limit=\(limit)" + if let cursor, let encodedCursor = cursor.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) { + query += "&cursor=\(encodedCursor)" + } + do { + let envelope: FeatureControlNotificationListEnvelope = try await api.request( + url: configuration.baseURL + configuration.notificationsPath + query, + method: .get, + headers: headers, + debug: false, + timeoutInterval: configuration.requestTimeout + ) + guard !envelope.error else { + throw FeatureControlError.server(code: "UNKNOWN", status: 200) + } + return (envelope.result.items, envelope.result.nextCursor) + } catch let error as FeatureControlError { + throw error + } catch { + throw FeatureControlErrorMapper.map(error) + } + } + + public func markRead(id: String) async throws { + var headers: [String: String] = [:] + await configuration.auth.authorize(&headers) + + let encodedId = id.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? id + do { + let _: String = try await api.request( + url: configuration.baseURL + configuration.notificationsPath + "/\(encodedId)/read", + method: .post, + headers: headers, + debug: false, + timeoutInterval: configuration.requestTimeout + ) + } catch { + throw FeatureControlErrorMapper.map(error) + } + } + + public func markAllRead() async throws { + var headers: [String: String] = [:] + await configuration.auth.authorize(&headers) + + do { + let _: String = try await api.request( + url: configuration.baseURL + configuration.notificationsPath + "/read-all", + method: .post, + headers: headers, + debug: false, + timeoutInterval: configuration.requestTimeout + ) + } catch { + throw FeatureControlErrorMapper.map(error) + } + } +} diff --git a/Tests/LCFeatureControlTests/FeatureControlAuthTests.swift b/Tests/LCFeatureControlTests/FeatureControlAuthTests.swift new file mode 100644 index 0000000..adeaf0c --- /dev/null +++ b/Tests/LCFeatureControlTests/FeatureControlAuthTests.swift @@ -0,0 +1,38 @@ +import XCTest +@testable import LCFeatureControl + +final class FeatureControlAuthTests: XCTestCase { + + func testBearerAuthAddsAuthorizationHeader() async { + let auth = FeatureControlBearerAuth { "jwt-123" } + var headers: [String: String] = [:] + await auth.authorize(&headers) + XCTAssertEqual(headers["Authorization"], "Bearer jwt-123") + } + + func testBearerAuthAddsNoHeaderWhenTokenIsNil() async { + let auth = FeatureControlBearerAuth { nil } + var headers: [String: String] = [:] + await auth.authorize(&headers) + XCTAssertNil(headers["Authorization"]) + } + + func testHeaderAuthAddsExactHeadersOnly() async { + let auth = FeatureControlHeaderAuth(headers: ["X-BFF-Session": "abc"]) + var headers: [String: String] = [:] + await auth.authorize(&headers) + XCTAssertEqual(headers, ["X-BFF-Session": "abc"]) + XCTAssertNil(headers["Authorization"]) + } + + func testConfigurationDefaults() { + let config = FeatureControlConfiguration(baseURL: "https://api.example.com", + environment: "production", + auth: FeatureControlHeaderAuth(headers: [:])) + XCTAssertEqual(config.cacheTTL, 45) + XCTAssertEqual(config.requestTimeout, 3) + XCTAssertEqual(config.evaluatePath, "/api/feature-control/evaluate") + XCTAssertEqual(config.notificationsPath, "/api/feature-control/notifications") + XCTAssertEqual(config.telemetryPath, "/api/feature-control/telemetry/exposure") + } +} diff --git a/Tests/LCFeatureControlTests/FeatureControlCacheTests.swift b/Tests/LCFeatureControlTests/FeatureControlCacheTests.swift new file mode 100644 index 0000000..2751859 --- /dev/null +++ b/Tests/LCFeatureControlTests/FeatureControlCacheTests.swift @@ -0,0 +1,70 @@ +import XCTest +@testable import LCFeatureControl + +/// Test-only mutable clock box — a `var` captured directly by the cache's +/// escaping `@Sendable` closure would trip strict-concurrency capture checks; +/// this mirrors the `@unchecked Sendable` pattern `StubURLProtocol` already uses. +private final class MutableClock: @unchecked Sendable { + var value: Date + init(_ value: Date) { self.value = value } +} + +final class FeatureControlCacheTests: XCTestCase { + + private func snapshot(configVersion: Int) -> FeatureControlSnapshot { + FeatureControlSnapshot(evaluatedAt: Date(), configVersion: configVersion, flags: [:]) + } + + private func key(_ keys: [String] = ["a", "b"]) -> FeatureControlCacheKey { + FeatureControlCacheKey(environment: "production", subjectId: "cust_1", + sortedKeys: keys.sorted(), platform: "ios", appVersion: "1.0") + } + + func testHitWithinTTLReturnsSameSnapshot() async { + let cache = FeatureControlCache(now: { Date() }) + await cache.set(key(), snapshot: snapshot(configVersion: 1), ttl: 45) + let hit = await cache.get(key(), allowStale: false) + XCTAssertEqual(hit?.configVersion, 1) + } + + func testMissAfterTTLExpiryReturnsNilWhenStaleNotAllowed() async { + let clock = MutableClock(Date()) + let cache = FeatureControlCache(now: { clock.value }) + await cache.set(key(), snapshot: snapshot(configVersion: 1), ttl: 1) + clock.value = clock.value.addingTimeInterval(2) + let hit = await cache.get(key(), allowStale: false) + XCTAssertNil(hit) + } + + func testStaleAllowedReturnsExpiredEntry() async { + let clock = MutableClock(Date()) + let cache = FeatureControlCache(now: { clock.value }) + await cache.set(key(), snapshot: snapshot(configVersion: 1), ttl: 1) + clock.value = clock.value.addingTimeInterval(2) + let hit = await cache.get(key(), allowStale: true) + XCTAssertEqual(hit?.configVersion, 1) + } + + func testKeyDiffersBySortedKeysNotInputOrder() async { + let cache = FeatureControlCache(now: { Date() }) + await cache.set(key(["a", "b"]), snapshot: snapshot(configVersion: 1), ttl: 45) + let hit = await cache.get(key(["b", "a"]), allowStale: false) + XCTAssertEqual(hit?.configVersion, 1) + } + + func testSetOverwritesPriorConfigVersion() async { + let cache = FeatureControlCache(now: { Date() }) + await cache.set(key(), snapshot: snapshot(configVersion: 7), ttl: 45) + await cache.set(key(), snapshot: snapshot(configVersion: 8), ttl: 45) + let hit = await cache.get(key(), allowStale: false) + XCTAssertEqual(hit?.configVersion, 8) + } + + func testInvalidateAllClearsEntries() async { + let cache = FeatureControlCache(now: { Date() }) + await cache.set(key(), snapshot: snapshot(configVersion: 1), ttl: 45) + await cache.invalidateAll() + let hit = await cache.get(key(), allowStale: true) + XCTAssertNil(hit) + } +} diff --git a/Tests/LCFeatureControlTests/FeatureControlErrorTests.swift b/Tests/LCFeatureControlTests/FeatureControlErrorTests.swift new file mode 100644 index 0000000..a53fec2 --- /dev/null +++ b/Tests/LCFeatureControlTests/FeatureControlErrorTests.swift @@ -0,0 +1,76 @@ +import XCTest +@testable import LCFeatureControl +@testable import LCEssentials + +final class FeatureControlErrorTests: XCTestCase { + + private var api = API.lce_featureControlTestInstance() + + override func tearDown() { + StubURLProtocol.reset() + super.tearDown() + } + + private func makeManager() -> FeatureControlManager { + let config = FeatureControlConfiguration(baseURL: "https://api.example.com", + environment: "production", + auth: FeatureControlHeaderAuth(headers: [:])) + return FeatureControlManager(configuration: config, api: api) + } + + private func context() -> FeatureControlContext { + FeatureControlContext(subjectType: .customer, subjectId: "cust_1") + } + + private func expectMappedError(status: Int, bodyCode: String) async throws -> FeatureControlError { + StubURLProtocol.setStub(.init(statusCode: status, body: Data(#"{"code":"\#(bodyCode)"}"#.utf8))) + let manager = makeManager() + do { + _ = try await manager.evaluateOrThrow(keys: ["x"], context: context()) + XCTFail("expected FeatureControlError") + throw FeatureControlError.transport(message: "unreachable") + } catch let error as FeatureControlError { + return error + } + } + + func testMaps400ToInvalidContext() async throws { + let error = try await expectMappedError(status: 400, bodyCode: "FEATURE_CONTROL_CONTEXT_INVALID") + XCTAssertEqual(error, .invalidContext(code: "FEATURE_CONTROL_CONTEXT_INVALID")) + } + + func testMaps401ToUnauthorized() async throws { + let error = try await expectMappedError(status: 401, bodyCode: "FEATURE_CONTROL_MISSING_AUTH") + XCTAssertEqual(error, .unauthorized(code: "FEATURE_CONTROL_MISSING_AUTH")) + } + + func testMaps403ToForbidden() async throws { + let error = try await expectMappedError(status: 403, bodyCode: "FEATURE_CONTROL_PANEL_ROLE_REQUIRED") + XCTAssertEqual(error, .forbidden(code: "FEATURE_CONTROL_PANEL_ROLE_REQUIRED")) + } + + func testMaps429ToRateLimited() async throws { + let error = try await expectMappedError(status: 429, bodyCode: "FEATURE_CONTROL_RATE_LIMIT") + XCTAssertEqual(error, .rateLimited(code: "FEATURE_CONTROL_RATE_LIMIT")) + } + + func testMaps500ToServerWithStatusCode() async throws { + let error = try await expectMappedError(status: 500, bodyCode: "FEATURE_CONTROL_ERROR") + XCTAssertEqual(error, .server(code: "FEATURE_CONTROL_ERROR", status: 500)) + } + + func testDecodingFailureMapsToTransport() async { + StubURLProtocol.setStub(.init(statusCode: 200, body: Data("not json at all".utf8))) + let manager = makeManager() + do { + _ = try await manager.evaluateOrThrow(keys: ["x"], context: context()) + XCTFail("expected throw") + } catch let error as FeatureControlError { + guard case .transport = error else { + return XCTFail("expected .transport, got \(error)") + } + } catch { + XCTFail("expected FeatureControlError, got \(error)") + } + } +} diff --git a/Tests/LCFeatureControlTests/FeatureControlExposureTests.swift b/Tests/LCFeatureControlTests/FeatureControlExposureTests.swift new file mode 100644 index 0000000..8a6b8f6 --- /dev/null +++ b/Tests/LCFeatureControlTests/FeatureControlExposureTests.swift @@ -0,0 +1,75 @@ +import XCTest +@testable import LCFeatureControl +@testable import LCEssentials + +final class FeatureControlExposureTests: XCTestCase { + + private var api = API.lce_featureControlTestInstance() + + override func tearDown() { + StubURLProtocol.reset() + super.tearDown() + } + + private func makeManager() -> FeatureControlManager { + let config = FeatureControlConfiguration(baseURL: "https://api.example.com", + environment: "production", + auth: FeatureControlHeaderAuth(headers: [:])) + return FeatureControlManager(configuration: config, api: api) + } + + private func event(_ index: Int) -> FeatureControlExposureEvent { + FeatureControlExposureEvent(featureKey: "fc.checkout_v2", variant: "on", + subjectType: .customer, storeId: "store_\(index)") + } + + func testFlushSendsSingleBatchUnderLimit() async { + StubURLProtocol.setStub(.init(statusCode: 200, body: Data(#"{"error":false,"code":"OK"}"#.utf8))) + let manager = makeManager() + for i in 0..<10 { await manager.recordExposure(event(i)) } + + await manager.flushExposures() + + XCTAssertEqual(StubURLProtocol.requestCount, 1) + } + + func testFlushSplitsIntoMultipleBatchesOverLimit() async { + StubURLProtocol.setStub(.init(statusCode: 200, body: Data(#"{"error":false,"code":"OK"}"#.utf8))) + let manager = makeManager() + for i in 0..<130 { await manager.recordExposure(event(i)) } + + await manager.flushExposures() + + XCTAssertEqual(StubURLProtocol.requestCount, 2) + } + + func testFlushSplitsExactlyAtTheServerBatchLimit() async { + StubURLProtocol.setStub(.init(statusCode: 200, body: Data(#"{"error":false,"code":"OK"}"#.utf8))) + let manager = makeManager() + for i in 0..<101 { await manager.recordExposure(event(i)) } // maxItems: 100, plus 1 + + await manager.flushExposures() + + XCTAssertEqual(StubURLProtocol.requestCount, 2) + let eventCountsPerBatch = StubURLProtocol.capturedBodies.map { body in + String(decoding: body, as: UTF8.self).components(separatedBy: "\"featureKey\"").count - 1 + } + XCTAssertEqual(eventCountsPerBatch, [100, 1]) + } + + func testFailedBatchIsDroppedNotRetriedIndefinitely() async { + StubURLProtocol.setStub(.init(statusCode: 500, body: Data())) + let manager = makeManager() + for i in 0..<10 { await manager.recordExposure(event(i)) } + + await manager.flushExposures() // must not throw, must not hang + + XCTAssertEqual(StubURLProtocol.requestCount, 1) + + // buffer was cleared even though the batch failed — a second flush sends nothing new + StubURLProtocol.reset() + StubURLProtocol.setStub(.init(statusCode: 200, body: Data(#"{"error":false,"code":"OK"}"#.utf8))) + await manager.flushExposures() + XCTAssertEqual(StubURLProtocol.requestCount, 0) + } +} diff --git a/Tests/LCFeatureControlTests/FeatureControlJSONTests.swift b/Tests/LCFeatureControlTests/FeatureControlJSONTests.swift new file mode 100644 index 0000000..5da1eb9 --- /dev/null +++ b/Tests/LCFeatureControlTests/FeatureControlJSONTests.swift @@ -0,0 +1,40 @@ +import XCTest +@testable import LCFeatureControl + +final class FeatureControlJSONTests: XCTestCase { + + private func decode(_ json: String) throws -> FeatureControlJSON { + try JSONDecoder().decode(FeatureControlJSON.self, from: Data(json.utf8)) + } + + func testDecodesNull() throws { + XCTAssertEqual(try decode("null"), .null) + } + + func testDecodesBool() throws { + XCTAssertEqual(try decode("true"), .bool(true)) + } + + func testDecodesNumber() throws { + XCTAssertEqual(try decode("5"), .number(5)) + } + + func testDecodesString() throws { + XCTAssertEqual(try decode("\"hello\""), .string("hello")) + } + + func testDecodesArray() throws { + XCTAssertEqual(try decode("[\"a\",\"b\"]"), .array([.string("a"), .string("b")])) + } + + func testDecodesNestedObject() throws { + let json = #"{"limit": 5, "tags": ["a","b"], "nested": {"x": true}}"# + let value = try decode(json) + guard case let .object(object) = value else { + return XCTFail("expected .object, got \(value)") + } + XCTAssertEqual(object["limit"], .number(5)) + XCTAssertEqual(object["tags"], .array([.string("a"), .string("b")])) + XCTAssertEqual(object["nested"], .object(["x": .bool(true)])) + } +} diff --git a/Tests/LCFeatureControlTests/FeatureControlManagerTests.swift b/Tests/LCFeatureControlTests/FeatureControlManagerTests.swift new file mode 100644 index 0000000..b6d6157 --- /dev/null +++ b/Tests/LCFeatureControlTests/FeatureControlManagerTests.swift @@ -0,0 +1,187 @@ +import XCTest +@testable import LCFeatureControl +@testable import LCEssentials + +private final class MutableClock: @unchecked Sendable { + var value: Date + init(_ value: Date) { self.value = value } +} + +final class FeatureControlManagerTests: XCTestCase { + + private var api = API.lce_featureControlTestInstance() + + override func tearDown() { + StubURLProtocol.reset() + super.tearDown() + } + + private func makeManager(defaults: [String: FeatureControlFlag] = [:], + now: @escaping @Sendable () -> Date = { Date() }) -> FeatureControlManager { + let config = FeatureControlConfiguration(baseURL: "https://api.example.com", + environment: "production", + auth: FeatureControlHeaderAuth(headers: [:])) + return FeatureControlManager(configuration: config, api: api, defaults: defaults, now: now) + } + + private func evaluateResponseBody(configVersion: Int) -> Data { + Data(""" + {"error": false, "code": "FEATURE_CONTROL_EVALUATED", "result": { + "evaluatedAt": "2026-04-16T12:00:00.000Z", + "configVersion": \(configVersion), + "flags": {"fc.checkout_v2": {"enabled": true, "variant": "on", "payload": null, "reason": "rollout"}} + }} + """.utf8) + } + + private func context() -> FeatureControlContext { + FeatureControlContext(subjectType: .customer, subjectId: "cust_1") + } + + // MARK: - Happy path (T4) + + func testEvaluateFetchesAndCachesOnMiss() async throws { + StubURLProtocol.setStub(.init(statusCode: 200, body: evaluateResponseBody(configVersion: 7))) + let manager = makeManager() + + let snapshot = try await manager.evaluateOrThrow(keys: ["fc.checkout_v2"], context: context()) + + XCTAssertEqual(snapshot.configVersion, 7) + XCTAssertEqual(StubURLProtocol.requestCount, 1) + let sent = StubURLProtocol.capturedRequests.first + XCTAssertEqual(sent?.httpMethod, "POST") + XCTAssertEqual(sent?.url?.absoluteString, "https://api.example.com/api/feature-control/evaluate") + } + + func testEvaluateReturnsCacheWithoutNetworkCallOnHit() async { + StubURLProtocol.setStub(.init(statusCode: 200, body: evaluateResponseBody(configVersion: 7))) + let manager = makeManager() + + _ = await manager.evaluate(keys: ["fc.checkout_v2"], context: context()) + _ = await manager.evaluate(keys: ["fc.checkout_v2"], context: context()) + + XCTAssertEqual(StubURLProtocol.requestCount, 1) + } + + func testConfigVersionBumpReplacesCacheAfterExpiry() async { + let clock = MutableClock(Date()) + StubURLProtocol.setStub(.init(statusCode: 200, body: evaluateResponseBody(configVersion: 7))) + let manager = makeManager(now: { clock.value }) + + let first = await manager.evaluate(keys: ["fc.checkout_v2"], context: context()) + XCTAssertEqual(first.configVersion, 7) + + clock.value = clock.value.addingTimeInterval(1000) // past default 45s TTL + StubURLProtocol.setStub(.init(statusCode: 200, body: evaluateResponseBody(configVersion: 8))) + + let second = await manager.evaluate(keys: ["fc.checkout_v2"], context: context()) + XCTAssertEqual(second.configVersion, 8) + XCTAssertEqual(StubURLProtocol.requestCount, 1) + } + + func testInvalidateCacheForcesFreshFetch() async { + StubURLProtocol.setStub(.init(statusCode: 200, body: evaluateResponseBody(configVersion: 7))) + let manager = makeManager() + _ = await manager.evaluate(keys: ["fc.checkout_v2"], context: context()) + + await manager.invalidateCache() + StubURLProtocol.setStub(.init(statusCode: 200, body: evaluateResponseBody(configVersion: 9))) + let second = await manager.evaluate(keys: ["fc.checkout_v2"], context: context()) + + XCTAssertEqual(second.configVersion, 9) + XCTAssertEqual(StubURLProtocol.requestCount, 1) + } + + func testEvaluateOrThrowPropagatesDecodingFailure() async { + StubURLProtocol.setStub(.init(statusCode: 200, body: Data("not json".utf8))) + let manager = makeManager() + + do { + _ = try await manager.evaluateOrThrow(keys: ["x"], context: context()) + XCTFail("expected throw") + } catch { + // any throw is correct — evaluateOrThrow must not silently degrade + } + } + + // MARK: - Safe-degrade fallback chain (T5) + + func testEvaluate429FallsBackToStaleCache() async { + let clock = MutableClock(Date()) + StubURLProtocol.setStub(.init(statusCode: 200, body: evaluateResponseBody(configVersion: 7))) + let manager = makeManager(now: { clock.value }) + _ = await manager.evaluate(keys: ["fc.checkout_v2"], context: context()) + + clock.value = clock.value.addingTimeInterval(1000) + StubURLProtocol.setStub(.init(statusCode: 429, body: Data(#"{"code":"FEATURE_CONTROL_RATE_LIMIT"}"#.utf8))) + + let result = await manager.evaluate(keys: ["fc.checkout_v2"], context: context()) + XCTAssertEqual(result.configVersion, 7) + } + + func testEvaluate500FallsBackToStaleCache() async { + let clock = MutableClock(Date()) + StubURLProtocol.setStub(.init(statusCode: 200, body: evaluateResponseBody(configVersion: 7))) + let manager = makeManager(now: { clock.value }) + _ = await manager.evaluate(keys: ["fc.checkout_v2"], context: context()) + + clock.value = clock.value.addingTimeInterval(1000) + StubURLProtocol.setStub(.init(statusCode: 500, body: Data())) + + let result = await manager.evaluate(keys: ["fc.checkout_v2"], context: context()) + XCTAssertEqual(result.configVersion, 7) + } + + func testEvaluateNoCacheAndServerDownReturnsDefaults() async { + StubURLProtocol.setStub(.init(statusCode: 500, body: Data())) + let defaultFlag = FeatureControlFlag(enabled: false, variant: nil, payload: nil, reason: "default") + let manager = makeManager(defaults: ["fc.checkout_v2": defaultFlag]) + + let result = await manager.evaluate(keys: ["fc.checkout_v2"], context: context()) + + XCTAssertEqual(result.configVersion, FeatureControlManager.unresolvedConfigVersion) + XCTAssertEqual(result.flags["fc.checkout_v2"]?.enabled, false) + } + + func testEvaluateNeverThrowsEvenWhenServerAlwaysErrors() async { + StubURLProtocol.setStub(.init(statusCode: 500, body: Data())) + let manager = makeManager() + // No `try` above this line compiles only because `evaluate` truly never throws. + _ = await manager.evaluate(keys: ["x"], context: context()) + } + + // MARK: - Concurrency & envelope validation + + func testConcurrentEvaluateOnColdCacheCoalescesIntoSingleRequest() async { + StubURLProtocol.setStub(.init(statusCode: 200, body: evaluateResponseBody(configVersion: 7))) + let manager = makeManager() + let ctx = context() + + async let first = manager.evaluate(keys: ["fc.checkout_v2"], context: ctx) + async let second = manager.evaluate(keys: ["fc.checkout_v2"], context: ctx) + async let third = manager.evaluate(keys: ["fc.checkout_v2"], context: ctx) + let results = await [first, second, third] + + XCTAssertEqual(Set(results.map(\.configVersion)), [7]) + XCTAssertEqual(StubURLProtocol.requestCount, 1) + } + + func testEvaluateOrThrowThrowsWhenEnvelopeErrorIsTrueDespite200() async { + let body = """ + {"error": true, "code": "FEATURE_CONTROL_DEGRADED", "result": { + "evaluatedAt": "2026-04-16T12:00:00.000Z", "configVersion": 1, "flags": {} + }} + """ + StubURLProtocol.setStub(.init(statusCode: 200, body: Data(body.utf8))) + let manager = makeManager() + + do { + _ = try await manager.evaluateOrThrow(keys: ["x"], context: context()) + XCTFail("expected throw") + } catch let error as FeatureControlError { + XCTAssertEqual(error, .server(code: "FEATURE_CONTROL_DEGRADED", status: 200)) + } catch { + XCTFail("expected FeatureControlError, got \(error)") + } + } +} diff --git a/Tests/LCFeatureControlTests/FeatureControlModelsTests.swift b/Tests/LCFeatureControlTests/FeatureControlModelsTests.swift new file mode 100644 index 0000000..42c8c3b --- /dev/null +++ b/Tests/LCFeatureControlTests/FeatureControlModelsTests.swift @@ -0,0 +1,164 @@ +import XCTest +@testable import LCFeatureControl + +/// Exact evaluate-response shape from Atomenta's `docs/feature-control/EXTERNAL_CLIENTS.md` §3. +private let evaluateResponseFixture = """ +{ + "error": false, + "code": "FEATURE_CONTROL_EVALUATED", + "result": { + "evaluatedAt": "2026-04-16T12:00:00.000Z", + "configVersion": 7, + "flags": { + "fc.checkout_v2": { + "enabled": true, + "variant": "on", + "payload": null, + "reason": "rollout" + }, + "fc.search_ranking_v3": { + "enabled": false, + "variant": "off", + "payload": null, + "reason": "default" + } + } + } +} +""" + +final class FeatureControlModelsTests: XCTestCase { + + func testFlagDecodesFromEvaluateResponseFixture() throws { + let data = Data(evaluateResponseFixture.utf8) + let envelope = try JSONDecoder().decode(FeatureControlEvaluateEnvelope.self, from: data) + + XCTAssertFalse(envelope.error) + XCTAssertEqual(envelope.result.configVersion, 7) + + let checkout = try XCTUnwrap(envelope.result.flags["fc.checkout_v2"]) + XCTAssertTrue(checkout.enabled) + XCTAssertEqual(checkout.variant, "on") + XCTAssertNil(checkout.payload) + XCTAssertEqual(checkout.reason, "rollout") + + let ranking = try XCTUnwrap(envelope.result.flags["fc.search_ranking_v3"]) + XCTAssertFalse(ranking.enabled) + } + + func testEvaluatedAtParsesFractionalSecondsISO8601() throws { + let data = Data(evaluateResponseFixture.utf8) + let envelope = try JSONDecoder().decode(FeatureControlEvaluateEnvelope.self, from: data) + + var calendar = Calendar(identifier: .gregorian) + calendar.timeZone = try XCTUnwrap(TimeZone(identifier: "UTC")) + let components = calendar.dateComponents([.year, .month, .day, .hour, .minute, .second], + from: envelope.result.evaluatedAt) + XCTAssertEqual(components.year, 2026) + XCTAssertEqual(components.month, 4) + XCTAssertEqual(components.day, 16) + XCTAssertEqual(components.hour, 12) + } + + func testEvaluatedAtDecodeFailsOnUnrecognizedDateFormat() { + let json = #"{"evaluatedAt": "not-a-date", "configVersion": 1, "flags": {}}"# + XCTAssertThrowsError(try JSONDecoder().decode(FeatureControlSnapshot.self, from: Data(json.utf8))) { error in + guard case DecodingError.dataCorrupted = error else { + return XCTFail("expected .dataCorrupted, got \(error)") + } + } + } + + func testFlagDecodesObjectPayloadThroughEvaluateEnvelope() throws { + let json = """ + {"error": false, "code": "FEATURE_CONTROL_EVALUATED", "result": { + "evaluatedAt": "2026-04-16T12:00:00.000Z", + "configVersion": 3, + "flags": {"fc.promo_banner": {"enabled": true, "variant": "on", + "payload": {"title": "Sale", "limit": 5, "tags": ["a", "b"]}, "reason": "rollout"}} + }} + """ + let envelope = try JSONDecoder().decode(FeatureControlEvaluateEnvelope.self, from: Data(json.utf8)) + let flag = try XCTUnwrap(envelope.result.flags["fc.promo_banner"]) + guard case let .object(payload) = flag.payload else { + return XCTFail("expected .object payload, got \(String(describing: flag.payload))") + } + XCTAssertEqual(payload["title"], .string("Sale")) + XCTAssertEqual(payload["limit"], .number(5)) + XCTAssertEqual(payload["tags"], .array([.string("a"), .string("b")])) + } + + func testContextEncodesExactOpenAPIShape() throws { + let context = FeatureControlContext(subjectType: .customer, subjectId: "cust_123", + storeId: "store_001", platform: "ios", + appVersion: "2.3.1", + attributes: ["city": "Belo Horizonte"]) + let data = try JSONEncoder().encode(context) + let object = try XCTUnwrap(try JSONSerialization.jsonObject(with: data) as? [String: Any]) + + XCTAssertEqual(object["subjectType"] as? String, "customer") + XCTAssertEqual(object["subjectId"] as? String, "cust_123") + XCTAssertEqual(object["storeId"] as? String, "store_001") + XCTAssertEqual(object["platform"] as? String, "ios") + XCTAssertEqual(object["appVersion"] as? String, "2.3.1") + let attributes = try XCTUnwrap(object["attributes"] as? [String: String]) + XCTAssertEqual(attributes["city"], "Belo Horizonte") + } + + func testNotificationDecodesFromOpenAPIShape() throws { + let json = """ + { + "id": "n1", + "title": "Manutenção", + "body": "Janela de manutenção às 22h", + "severity": "warning", + "createdAt": "2026-04-16T12:00:00.000Z", + "read": false, + "ctaLabel": "Ver detalhes", + "ctaUrl": "https://example.com" + } + """ + let notification = try JSONDecoder().decode(FeatureControlNotification.self, from: Data(json.utf8)) + XCTAssertEqual(notification.id, "n1") + XCTAssertEqual(notification.severity, "warning") + XCTAssertFalse(notification.read) + XCTAssertEqual(notification.ctaLabel, "Ver detalhes") + } + + func testNotificationDecodesWithoutOptionalCTAFields() throws { + let json = """ + { + "id": "n2", + "title": "Info", + "body": "Just FYI", + "severity": "info", + "createdAt": "2026-04-16T12:00:00Z", + "read": true + } + """ + let notification = try JSONDecoder().decode(FeatureControlNotification.self, from: Data(json.utf8)) + XCTAssertNil(notification.ctaLabel) + XCTAssertNil(notification.ctaUrl) + } + + func testNotificationCreatedAtDecodeFailsOnUnrecognizedDateFormat() { + let json = """ + {"id": "n3", "title": "T", "body": "B", "severity": "info", + "createdAt": "not-a-date", "read": false} + """ + XCTAssertThrowsError(try JSONDecoder().decode(FeatureControlNotification.self, from: Data(json.utf8))) { error in + guard case DecodingError.dataCorrupted = error else { + return XCTFail("expected .dataCorrupted, got \(error)") + } + } + } + + func testExposureEventEncodesExpectedShape() throws { + let event = FeatureControlExposureEvent(featureKey: "fc.checkout_v2", variant: "on", + subjectType: .customer, storeId: "store_001") + let data = try JSONEncoder().encode(event) + let object = try XCTUnwrap(try JSONSerialization.jsonObject(with: data) as? [String: Any]) + XCTAssertEqual(object["featureKey"] as? String, "fc.checkout_v2") + XCTAssertEqual(object["subjectType"] as? String, "customer") + } +} diff --git a/Tests/LCFeatureControlTests/FeatureControlNotificationsClientTests.swift b/Tests/LCFeatureControlTests/FeatureControlNotificationsClientTests.swift new file mode 100644 index 0000000..02d4cd9 --- /dev/null +++ b/Tests/LCFeatureControlTests/FeatureControlNotificationsClientTests.swift @@ -0,0 +1,122 @@ +import XCTest +@testable import LCFeatureControl +@testable import LCEssentials + +final class FeatureControlNotificationsClientTests: XCTestCase { + + private var api = API.lce_featureControlTestInstance() + + override func tearDown() { + StubURLProtocol.reset() + super.tearDown() + } + + private func makeClient() -> FeatureControlNotificationsClient { + let config = FeatureControlConfiguration(baseURL: "https://api.example.com", + environment: "production", + auth: FeatureControlBearerAuth { "jwt-123" }) + return FeatureControlNotificationsClient(configuration: config, api: api) + } + + func testListDecodesItemsAndNextCursor() async throws { + let body = """ + {"error": false, "result": {"items": [ + {"id": "n1", "title": "T", "body": "B", "severity": "info", + "createdAt": "2026-04-16T12:00:00.000Z", "read": false} + ], "nextCursor": "cursor-2"}} + """ + StubURLProtocol.setStub(.init(statusCode: 200, body: Data(body.utf8))) + let client = makeClient() + + let (items, nextCursor) = try await client.list(status: .unread, limit: 20) + + XCTAssertEqual(items.count, 1) + XCTAssertEqual(items.first?.id, "n1") + XCTAssertEqual(nextCursor, "cursor-2") + } + + func testListPassesStatusAndLimitAsQueryParams() async throws { + let body = #"{"error": false, "result": {"items": [], "nextCursor": null}}"# + StubURLProtocol.setStub(.init(statusCode: 200, body: Data(body.utf8))) + let client = makeClient() + + _ = try await client.list(status: .unread, limit: 5) + + let url = StubURLProtocol.capturedRequests.first?.url?.absoluteString + XCTAssertEqual(url, "https://api.example.com/api/feature-control/notifications?status=unread&limit=5") + XCTAssertEqual(StubURLProtocol.capturedRequests.first?.value(forHTTPHeaderField: "Authorization"), + "Bearer jwt-123") + } + + func testMarkReadPostsToCorrectPathAndSucceedsOn200() async throws { + StubURLProtocol.setStub(.init(statusCode: 200, headers: ["Content-Type": "text/plain"], body: Data("ok".utf8))) + let client = makeClient() + + try await client.markRead(id: "n1") + + let sent = StubURLProtocol.capturedRequests.first + XCTAssertEqual(sent?.httpMethod, "POST") + XCTAssertEqual(sent?.url?.absoluteString, "https://api.example.com/api/feature-control/notifications/n1/read") + } + + func testMarkAllReadSucceedsOn200() async throws { + StubURLProtocol.setStub(.init(statusCode: 200, headers: ["Content-Type": "text/plain"], body: Data("ok".utf8))) + let client = makeClient() + + try await client.markAllRead() + + XCTAssertEqual(StubURLProtocol.capturedRequests.first?.url?.absoluteString, + "https://api.example.com/api/feature-control/notifications/read-all") + } + + func testListIncludesEncodedCursorWhenProvided() async throws { + let body = #"{"error": false, "result": {"items": [], "nextCursor": null}}"# + StubURLProtocol.setStub(.init(statusCode: 200, body: Data(body.utf8))) + let client = makeClient() + + _ = try await client.list(status: .all, limit: 20, cursor: "cursor abc") + + let url = StubURLProtocol.capturedRequests.first?.url?.absoluteString + XCTAssertEqual(url, "https://api.example.com/api/feature-control/notifications?status=all&limit=20&cursor=cursor%20abc") + } + + func testListOmitsCursorWhenNil() async throws { + let body = #"{"error": false, "result": {"items": [], "nextCursor": null}}"# + StubURLProtocol.setStub(.init(statusCode: 200, body: Data(body.utf8))) + let client = makeClient() + + _ = try await client.list(status: .all, limit: 20) + + let url = StubURLProtocol.capturedRequests.first?.url?.absoluteString + XCTAssertEqual(url, "https://api.example.com/api/feature-control/notifications?status=all&limit=20") + } + + func testListThrowsWhenEnvelopeErrorIsTrueDespite200() async { + let body = #"{"error": true, "result": {"items": [], "nextCursor": null}}"# + StubURLProtocol.setStub(.init(statusCode: 200, body: Data(body.utf8))) + let client = makeClient() + + do { + _ = try await client.list(status: .all, limit: 20) + XCTFail("expected throw") + } catch let error as FeatureControlError { + XCTAssertEqual(error, .server(code: "UNKNOWN", status: 200)) + } catch { + XCTFail("expected FeatureControlError, got \(error)") + } + } + + func testUnauthorizedMapsToFeatureControlErrorNotCrash() async { + StubURLProtocol.setStub(.init(statusCode: 401, body: Data(#"{"code":"FEATURE_CONTROL_MISSING_AUTH"}"#.utf8))) + let client = makeClient() + + do { + try await client.markAllRead() + XCTFail("expected throw") + } catch let error as FeatureControlError { + XCTAssertEqual(error, .unauthorized(code: "FEATURE_CONTROL_MISSING_AUTH")) + } catch { + XCTFail("expected FeatureControlError, got \(error)") + } + } +} diff --git a/Tests/LCFeatureControlTests/Support/StubURLProtocol.swift b/Tests/LCFeatureControlTests/Support/StubURLProtocol.swift new file mode 100644 index 0000000..8babdaa --- /dev/null +++ b/Tests/LCFeatureControlTests/Support/StubURLProtocol.swift @@ -0,0 +1,137 @@ +import Foundation +@testable import LCEssentials + +/// Shared factory so test classes don't each declare a force-unwrapped `API!` +/// var for the XCTest setUp/tearDown lifecycle. +extension API { + static func lce_featureControlTestInstance() -> API { + let cfg = URLSessionConfiguration.ephemeral + cfg.protocolClasses = [StubURLProtocol.self] + return API(testConfiguration: cfg) + } +} + +/// Test double for `URLProtocol`. Intercepts every request on a `URLSession` +/// configured with it, records the outgoing `URLRequest`, and replays a canned +/// response or error supplied by the test. +/// +/// Register via: +/// ``` +/// let cfg = URLSessionConfiguration.ephemeral +/// cfg.protocolClasses = [StubURLProtocol.self] +/// let session = URLSession(configuration: cfg) +/// ``` +final class StubURLProtocol: URLProtocol, @unchecked Sendable { + + struct Stub { + var statusCode: Int = 200 + var headers: [String: String] = ["Content-Type": "application/json"] + var body: Data = Data() + var error: Error? + /// Bytes reported through `URLSession`'s upload progress, in order. + var uploadProgressChunks: [Int] = [] + } + + // MARK: - Test-facing state (guarded) + + private static let lock = NSLock() + // Access is serialised through `lock`; the unsafe opt-out is the documented + // pattern for lock-guarded mutable statics under strict concurrency. + nonisolated(unsafe) private static var _stub = Stub() + nonisolated(unsafe) private static var _capturedRequests: [URLRequest] = [] + nonisolated(unsafe) private static var _capturedBodies: [Data] = [] + + static func setStub(_ stub: Stub) { + lock.lock(); defer { lock.unlock() } + _stub = stub + _capturedRequests = [] + _capturedBodies = [] + } + + static func reset() { setStub(Stub()) } + + static var capturedRequests: [URLRequest] { + lock.lock(); defer { lock.unlock() } + return _capturedRequests + } + + /// Body of the last intercepted request. `URLProtocol` strips `httpBody` for + /// stream bodies, so this reads `httpBodyStream` when needed. + static var lastCapturedBody: Data? { + lock.lock(); defer { lock.unlock() } + return _capturedBodies.last + } + + static var requestCount: Int { + lock.lock(); defer { lock.unlock() } + return _capturedRequests.count + } + + /// Bodies of every intercepted request, in order — for tests asserting batch + /// boundaries across multiple requests (`lastCapturedBody` only sees the last one). + static var capturedBodies: [Data] { + lock.lock(); defer { lock.unlock() } + return _capturedBodies + } + + private static func currentStub() -> Stub { + lock.lock(); defer { lock.unlock() } + return _stub + } + + private static func record(_ request: URLRequest, body: Data) { + lock.lock(); defer { lock.unlock() } + _capturedRequests.append(request) + _capturedBodies.append(body) + } + + // MARK: - URLProtocol + + override class func canInit(with request: URLRequest) -> Bool { true } + + override class func canonicalRequest(for request: URLRequest) -> URLRequest { request } + + override func startLoading() { + let stub = Self.currentStub() + Self.record(request, body: Self.bodyData(from: request)) + + guard let client = client else { return } + + if let error = stub.error { + client.urlProtocol(self, didFailWithError: error) + return + } + + guard let url = request.url ?? URL(string: "https://stub.invalid"), + let response = HTTPURLResponse(url: url, + statusCode: stub.statusCode, + httpVersion: "HTTP/1.1", + headerFields: stub.headers) else { + client.urlProtocol(self, didFailWithError: URLError(.badServerResponse)) + return + } + client.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed) + client.urlProtocol(self, didLoad: stub.body) + client.urlProtocolDidFinishLoading(self) + } + + override func stopLoading() {} + + // MARK: - Body extraction + + private static func bodyData(from request: URLRequest) -> Data { + if let body = request.httpBody { return body } + guard let stream = request.httpBodyStream else { return Data() } + stream.open() + defer { stream.close() } + var data = Data() + let bufferSize = 64 * 1024 + var buffer = [UInt8](repeating: 0, count: bufferSize) + while stream.hasBytesAvailable { + let read = stream.read(&buffer, maxLength: bufferSize) + if read <= 0 { break } + data.append(buffer, count: read) + } + return data + } +}