Compare commits

..

7 Commits

Author SHA1 Message Date
b1f370f7db Merge pull request '[sub-spm-optional-products] LCFeatureControl review fixes: JWT log leak, platforms, request coalescing' (#13) from feature/spm/lcfeaturecontrol into main
Reviewed-on: #13
Reviewed-by: Daniel Arantes Loverde <developer@loverde.com.br>
2026-09-16 13:31:38 -03:00
Daniel Arantes Loverde
c6e9803c75 [lcfeaturecontrol-review-fixes] Fix JWT log leak, drop macOS/tvOS, coalesce requests
- FeatureControlManager/NotificationsClient: pass debug: false at every
  api.request(...) call site — the API default (debug: true) was printing
  Authorization: Bearer <jwt> on every request, release builds included.
- Package.swift: drop macOS/tvOS from platforms — LCEssentials.API (used by
  LCFeatureControl) is iOS/watchOS-only and the vendored xcframework has no
  macOS/tvOS slice.
- FeatureControlManager: coalesce concurrent evaluate() calls on a cold cache
  into a single in-flight request per key instead of firing one per caller.
- FeatureControlManager/NotificationsClient: treat a 200 response carrying
  {"error": true} as a failure instead of caching/returning it as success.
- FeatureControlNotificationsClient: thread a cursor param through list() so
  the already-decoded nextCursor can actually be used to page.
- Tests: 9 new tests (coalescing, envelope-error-on-200, cursor param, a real
  object payload decoded through a full evaluate response, date-decode
  failures) and removed the 7 remaining force-unwraps in test scaffolding.
- Documentation/FeatureControl.md: new guide for the LCFeatureControl product,
  cross-linked from README.md and Extensions.md.
2026-09-16 13:21:10 -03:00
Daniel Arantes Loverde
2ba487a6e8 [sub-spm-optional-products] Add LCECryptoKit and LCFeatureControl as optional sub-SPM products
LCEssentials installs standalone; each sub target-depends on it so linking a
sub's product always pulls LCEssentials in too, without the consumer having to
declare it separately.

- LCECryptoKit: internalized from the remote LCECryptoKitBinary git dependency
  (embedded token URL removed) into a local binaryTarget vendoring
  Frameworks/LCECryptoKit.xcframework. LCECryptoKitManager moved out of
  LCEssentials core into its own LCECryptoKitManager target/product; the
  no-op fallback for when the binary wasn't linked is gone (breaking change
  for existing consumers, see decisions/2026-09-15-sub-spm-optional-products.md).
- LCFeatureControl: new product wrapping Atomenta's Feature Control API
  (flag evaluation with TTL cache + safe-degrade fallback to defaults,
  notifications inbox, batched exposure telemetry). 45 new tests.
2026-09-16 09:23:57 -03:00
Daniel Arantes Loverde
d067791930 fix new token 2026-09-10 20:47:13 -03:00
1d5067212a Merge pull request '[fix] LCENavigationView: remove phantom glass button, center title safely' (#11) from fix/navigation/glass-duplicate-button into main
Reviewed-on: #11
2026-09-01 16:26:03 -03:00
Daniel Arantes Loverde
72a5b59c44 [fix] LCENavigationView: remove phantom glass button, center title safely
Dummy mirrored button used to balance title centering rendered as a
visible empty glass pill under iOS 26's automatic button chrome. Side
buttons now only render when actually configured, title centers via
ZStack independent of button widths, and title padding is derived from
measured button width so long titles truncate instead of overlapping.
Glass button style is opt-in via setGlassButtonsEnabled(_:), off by
default.
2026-09-01 16:20:19 -03:00
Daniel Arantes Loverde
d678965154 [docs] Trim README to Documentation links; fix signature image 2026-08-30 10:02:19 -03:00
50 changed files with 4674 additions and 239 deletions

View File

@@ -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()
</details>
<details>
<summary><b>LCECryptoKitManager</b> — OTP / peppered-login bridge (needs the <code>LCECryptoKit</code> binary)</summary>
<summary><b>LCECryptoKitManager</b> — OTP / peppered-login bridge (separate <code>LCECryptoKit</code> product)</summary>
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:)`

View File

@@ -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
```
<details>
<summary><b>FeatureControlConfiguration</b> — wiring for one module deployment</summary>
```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 |
</details>
<details>
<summary><b>Auth strategies</b> — <code>FeatureControlAuthorizing</code></summary>
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])
```
</details>
## Evaluating flags
<details open>
<summary><b>FeatureControlManager</b> — cache, safe-degrade, invalidation</summary>
```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.
</details>
## Notifications inbox
<details>
<summary><b>FeatureControlNotificationsClient</b> — JWT-only</summary>
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()
```
</details>
## Exposure telemetry
<details>
<summary><b>recordExposure(_:)</b> / <b>flushExposures()</b> — best-effort, batched</summary>
```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.
</details>
## Errors
<details>
<summary><b>FeatureControlError</b> — from <code>evaluateOrThrow</code> and the notifications client</summary>
```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).
</details>
---
See [Extensions.md](Extensions.md) for the rest of `LCEssentials`' Foundation
helpers, including the sibling `LCECryptoKit` sub-product.

View File

@@ -0,0 +1,44 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>AvailableLibraries</key>
<array>
<dict>
<key>BinaryPath</key>
<string>LCECryptoKit.framework/LCECryptoKit</string>
<key>LibraryIdentifier</key>
<string>ios-arm64_x86_64-simulator</string>
<key>LibraryPath</key>
<string>LCECryptoKit.framework</string>
<key>SupportedArchitectures</key>
<array>
<string>arm64</string>
<string>x86_64</string>
</array>
<key>SupportedPlatform</key>
<string>ios</string>
<key>SupportedPlatformVariant</key>
<string>simulator</string>
</dict>
<dict>
<key>BinaryPath</key>
<string>LCECryptoKit.framework/LCECryptoKit</string>
<key>LibraryIdentifier</key>
<string>ios-arm64</string>
<key>LibraryPath</key>
<string>LCECryptoKit.framework</string>
<key>SupportedArchitectures</key>
<array>
<string>arm64</string>
</array>
<key>SupportedPlatform</key>
<string>ios</string>
</dict>
</array>
<key>CFBundlePackageType</key>
<string>XFWK</string>
<key>XCFrameworkFormatVersion</key>
<string>1.0</string>
</dict>
</plist>

View File

@@ -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(<swift/objc-prologue.h>)
# include <swift/objc-prologue.h>
#endif
#pragma clang diagnostic ignored "-Wauto-import"
#if defined(__OBJC__)
#include <Foundation/Foundation.h>
#endif // defined(__OBJC__)
#if defined(__cplusplus)
#include <cstdint>
#include <cstddef>
#include <cstdbool>
#include <cstring>
#include <stdlib.h>
#include <new>
#include <type_traits>
#else
#include <stdint.h>
#include <stddef.h>
#include <stdbool.h>
#include <string.h>
#endif
#if defined(__cplusplus)
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wnon-modular-include-in-framework-module"
#if defined(__arm64e__) && __has_include(<ptrauth.h>)
# include <ptrauth.h>
#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(<uchar.h>)
# include <uchar.h>
# 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

View File

@@ -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": []
}

View File

@@ -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?
}

View File

@@ -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?
}

View File

@@ -0,0 +1,3 @@
framework module LCECryptoKit {
header "LCECryptoKit-Swift.h"
}

View File

@@ -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(<swift/objc-prologue.h>)
# include <swift/objc-prologue.h>
#endif
#pragma clang diagnostic ignored "-Wauto-import"
#if defined(__OBJC__)
#include <Foundation/Foundation.h>
#endif // defined(__OBJC__)
#if defined(__cplusplus)
#include <cstdint>
#include <cstddef>
#include <cstdbool>
#include <cstring>
#include <stdlib.h>
#include <new>
#include <type_traits>
#else
#include <stdint.h>
#include <stddef.h>
#include <stdbool.h>
#include <string.h>
#endif
#if defined(__cplusplus)
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wnon-modular-include-in-framework-module"
#if defined(__arm64e__) && __has_include(<ptrauth.h>)
# include <ptrauth.h>
#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(<uchar.h>)
# include <uchar.h>
# 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(<swift/objc-prologue.h>)
# include <swift/objc-prologue.h>
#endif
#pragma clang diagnostic ignored "-Wauto-import"
#if defined(__OBJC__)
#include <Foundation/Foundation.h>
#endif // defined(__OBJC__)
#if defined(__cplusplus)
#include <cstdint>
#include <cstddef>
#include <cstdbool>
#include <cstring>
#include <stdlib.h>
#include <new>
#include <type_traits>
#else
#include <stdint.h>
#include <stddef.h>
#include <stdbool.h>
#include <string.h>
#endif
#if defined(__cplusplus)
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wnon-modular-include-in-framework-module"
#if defined(__arm64e__) && __has_include(<ptrauth.h>)
# include <ptrauth.h>
#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(<uchar.h>)
# include <uchar.h>
# 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

View File

@@ -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": []
}

View File

@@ -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?
}

View File

@@ -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?
}

View File

@@ -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": []
}

View File

@@ -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?
}

View File

@@ -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?
}

View File

@@ -0,0 +1,3 @@
framework module LCECryptoKit {
header "LCECryptoKit-Swift.h"
}

View File

@@ -0,0 +1,234 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>files</key>
<dict>
<key>Headers/LCECryptoKit-Swift.h</key>
<data>
uIM6/aOz59qnT/jGSBAiOinS2qo=
</data>
<key>Info.plist</key>
<data>
N0tkj+ldmO7dEKF+W1/6CjYNBrg=
</data>
<key>Modules/LCECryptoKit.swiftmodule/arm64-apple-ios-simulator.abi.json</key>
<data>
mIejxCLstZ77ufLnGayqr8wDScc=
</data>
<key>Modules/LCECryptoKit.swiftmodule/arm64-apple-ios-simulator.private.swiftinterface</key>
<data>
GJVZwAs/SRyyfGkg1t2Twb8grXE=
</data>
<key>Modules/LCECryptoKit.swiftmodule/arm64-apple-ios-simulator.swiftdoc</key>
<data>
vDvqI7FV3rThZRI4r/zPL877Z04=
</data>
<key>Modules/LCECryptoKit.swiftmodule/arm64-apple-ios-simulator.swiftinterface</key>
<data>
GJVZwAs/SRyyfGkg1t2Twb8grXE=
</data>
<key>Modules/LCECryptoKit.swiftmodule/arm64-apple-ios-simulator.swiftmodule</key>
<data>
0n6YBQBivkPuELjPpXBIj0CQFIc=
</data>
<key>Modules/LCECryptoKit.swiftmodule/x86_64-apple-ios-simulator.abi.json</key>
<data>
mIejxCLstZ77ufLnGayqr8wDScc=
</data>
<key>Modules/LCECryptoKit.swiftmodule/x86_64-apple-ios-simulator.private.swiftinterface</key>
<data>
WheGrMD0QDYsEwZ2vOGY+CQwW1o=
</data>
<key>Modules/LCECryptoKit.swiftmodule/x86_64-apple-ios-simulator.swiftdoc</key>
<data>
f52HXxbH78iRln4x78whSwyigbc=
</data>
<key>Modules/LCECryptoKit.swiftmodule/x86_64-apple-ios-simulator.swiftinterface</key>
<data>
WheGrMD0QDYsEwZ2vOGY+CQwW1o=
</data>
<key>Modules/LCECryptoKit.swiftmodule/x86_64-apple-ios-simulator.swiftmodule</key>
<data>
FSzeAs9LdtELeWT3VL2PMjOFZcQ=
</data>
<key>Modules/module.modulemap</key>
<data>
OnB7ckFjsSU10c/y8kUFi69Cav4=
</data>
</dict>
<key>files2</key>
<dict>
<key>Headers/LCECryptoKit-Swift.h</key>
<dict>
<key>hash2</key>
<data>
gHRFTmnGK28MYikS6gvnWErwc/oZ/scYeFssVTBw9MI=
</data>
</dict>
<key>Modules/LCECryptoKit.swiftmodule/arm64-apple-ios-simulator.abi.json</key>
<dict>
<key>hash2</key>
<data>
/2qkU9Pje/wZPzf5sExK+0bliYzKjDZM7iqyIusKGGk=
</data>
</dict>
<key>Modules/LCECryptoKit.swiftmodule/arm64-apple-ios-simulator.private.swiftinterface</key>
<dict>
<key>hash2</key>
<data>
BsY3SInOh3u6xj+kXxauIrgLi4cTqE1mY6weF7QOk2g=
</data>
</dict>
<key>Modules/LCECryptoKit.swiftmodule/arm64-apple-ios-simulator.swiftdoc</key>
<dict>
<key>hash2</key>
<data>
Ldu7EDkaW9WinMjRHaBaSxpZoBNDXGvNn04410k9e5k=
</data>
</dict>
<key>Modules/LCECryptoKit.swiftmodule/arm64-apple-ios-simulator.swiftinterface</key>
<dict>
<key>hash2</key>
<data>
BsY3SInOh3u6xj+kXxauIrgLi4cTqE1mY6weF7QOk2g=
</data>
</dict>
<key>Modules/LCECryptoKit.swiftmodule/arm64-apple-ios-simulator.swiftmodule</key>
<dict>
<key>hash2</key>
<data>
cuezjPqokn+YGP9z//TY4ZUxA1Dcmd6ApUafMMaQPAQ=
</data>
</dict>
<key>Modules/LCECryptoKit.swiftmodule/x86_64-apple-ios-simulator.abi.json</key>
<dict>
<key>hash2</key>
<data>
/2qkU9Pje/wZPzf5sExK+0bliYzKjDZM7iqyIusKGGk=
</data>
</dict>
<key>Modules/LCECryptoKit.swiftmodule/x86_64-apple-ios-simulator.private.swiftinterface</key>
<dict>
<key>hash2</key>
<data>
lJH2d+h2iS4AwqKh/vWgSYIGEelZHLrCNQVlcCFB3V4=
</data>
</dict>
<key>Modules/LCECryptoKit.swiftmodule/x86_64-apple-ios-simulator.swiftdoc</key>
<dict>
<key>hash2</key>
<data>
5S2/W4WoD0L4bl+NBuPQI7sGxqpZ7y2s7tme6Eu2oIQ=
</data>
</dict>
<key>Modules/LCECryptoKit.swiftmodule/x86_64-apple-ios-simulator.swiftinterface</key>
<dict>
<key>hash2</key>
<data>
lJH2d+h2iS4AwqKh/vWgSYIGEelZHLrCNQVlcCFB3V4=
</data>
</dict>
<key>Modules/LCECryptoKit.swiftmodule/x86_64-apple-ios-simulator.swiftmodule</key>
<dict>
<key>hash2</key>
<data>
a5uCPhYWs0qR1YWQZb51vKm+4mYKUgc5ajZr2wEU6tU=
</data>
</dict>
<key>Modules/module.modulemap</key>
<dict>
<key>hash2</key>
<data>
X+gHfuxKBEgqt+p9I9kR0FYyzml9UPuJpKzZAbPNcc0=
</data>
</dict>
</dict>
<key>rules</key>
<dict>
<key>^.*</key>
<true/>
<key>^.*\.lproj/</key>
<dict>
<key>optional</key>
<true/>
<key>weight</key>
<real>1000</real>
</dict>
<key>^.*\.lproj/locversion.plist$</key>
<dict>
<key>omit</key>
<true/>
<key>weight</key>
<real>1100</real>
</dict>
<key>^Base\.lproj/</key>
<dict>
<key>weight</key>
<real>1010</real>
</dict>
<key>^version.plist$</key>
<true/>
</dict>
<key>rules2</key>
<dict>
<key>.*\.dSYM($|/)</key>
<dict>
<key>weight</key>
<real>11</real>
</dict>
<key>^(.*/)?\.DS_Store$</key>
<dict>
<key>omit</key>
<true/>
<key>weight</key>
<real>2000</real>
</dict>
<key>^.*</key>
<true/>
<key>^.*\.lproj/</key>
<dict>
<key>optional</key>
<true/>
<key>weight</key>
<real>1000</real>
</dict>
<key>^.*\.lproj/locversion.plist$</key>
<dict>
<key>omit</key>
<true/>
<key>weight</key>
<real>1100</real>
</dict>
<key>^Base\.lproj/</key>
<dict>
<key>weight</key>
<real>1010</real>
</dict>
<key>^Info\.plist$</key>
<dict>
<key>omit</key>
<true/>
<key>weight</key>
<real>20</real>
</dict>
<key>^PkgInfo$</key>
<dict>
<key>omit</key>
<true/>
<key>weight</key>
<real>20</real>
</dict>
<key>^embedded\.provisionprofile$</key>
<dict>
<key>weight</key>
<real>20</real>
</dict>
<key>^version\.plist$</key>
<dict>
<key>weight</key>
<real>20</real>
</dict>
</dict>
</dict>
</plist>

View File

@@ -1,15 +0,0 @@
{
"originHash" : "6e3fcf8724d6b7d2d0bbffc25b4ec3b9d8b65a6ef53d157498d5447fd26cf5db",
"pins" : [
{
"identity" : "lcecryptokitbinary",
"kind" : "remoteSourceControl",
"location" : "https://60c260c85d3a2fe840411b0ff98f521b5eca3c56@git.loverde.com.br/Loverde-Company-LTDA/LCECryptoKitBinary.git",
"state" : {
"revision" : "11f66b1b9785b4965e027c2fa4742f61e8453403",
"version" : "1.0.4"
}
}
],
"version" : 3
}

View File

@@ -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://60c260c85d3a2fe840411b0ff98f521b5eca3c56@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"]),
]
)

View File

@@ -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 |
---

View File

@@ -19,10 +19,8 @@
// 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 {
@@ -33,7 +31,7 @@ public final class LCECryptoKitManager {
self.hashKey = ""
}
public init(privateKey: String){
public init(privateKey: String) {
self.hashKey = privateKey
}
@@ -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

View File

@@ -49,6 +49,39 @@ class LCENavigationState: ObservableObject {
@Published var subTitle: (any View) = Text("")
/// The background color of the navigation bar.
@Published var navigationBarBackgroundColor: Color = .clear
/// Whether a left button was actually configured via `setLeftButton`.
@Published var hasLeftButton: Bool = false
/// Whether a right button was actually configured via `setRightButton`.
@Published var hasRightButton: Bool = false
/// Whether buttons should opt into the system Liquid Glass button style (iOS 26+). Off by default.
@Published var useGlassButtons: Bool = false
}
/// `PreferenceKey` used to measure the widest side button so the title can be padded symmetrically and stay centered without overlapping either button.
@available(iOS 15, *)
private struct LCENavButtonWidthPreferenceKey: PreferenceKey {
static var defaultValue: CGFloat { 0 }
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
value = max(value, nextValue())
}
}
@available(iOS 15, *)
private extension View {
/// Applies `.glass` button style on iOS 26+ when enabled, otherwise falls back to `.plain` the navigation bar is not designed around glass, but stays opt-in ready.
@ViewBuilder
func lce_applyGlassIfEnabled(_ enabled: Bool) -> some View {
if enabled {
if #available(iOS 26.0, *) {
self.buttonStyle(.glass)
} else {
self.buttonStyle(.plain)
}
} else {
self.buttonStyle(.plain)
}
}
}
/// `LCENavigationView` is a SwiftUI `View` that provides a customizable navigation bar.
@@ -61,6 +94,9 @@ public struct LCENavigationView<Content: View>: View {
/// The content view displayed below the navigation bar.
let content: Content
/// The measured width of the widest side button, used to pad the title so it never overlaps either button.
@State private var maxButtonWidth: CGFloat = 0
/// Initializes a new `LCENavigationView` instance.
/// - Parameters:
/// - title: The title view for the navigation bar. Defaults to an empty `Text`.
@@ -92,18 +128,29 @@ public struct LCENavigationView<Content: View>: View {
/// The private `NavigationBarView` that lays out the navigation bar components.
private var NavigationBarView: some View {
HStack {
NavLeftButton
Spacer()
ZStack {
TitleView
Spacer()
NavRightButton
.padding(.horizontal, maxButtonWidth)
.lineLimit(1)
.minimumScaleFactor(0.7)
.frame(maxWidth: .infinity)
HStack {
if state.hasLeftButton {
NavLeftButton
}
Spacer()
if state.hasRightButton {
NavRightButton
}
}
}
.font(.headline)
.padding()
.background {
state.navigationBarBackgroundColor.ignoresSafeArea(edges: .top)
}
.onPreferenceChange(LCENavButtonWidthPreferenceKey.self) { maxButtonWidth = $0 }
}
/// The private `TitleView` that displays the title and subtitle.
@@ -116,7 +163,7 @@ public struct LCENavigationView<Content: View>: View {
}
}
/// The private `NavLeftButton` view.
/// The private `NavLeftButton` view. Only rendered when `setLeftButton` was actually called never a hidden placeholder.
private var NavLeftButton: some View {
Button(action: state.leftButtonAction) {
HStack {
@@ -126,9 +173,15 @@ public struct LCENavigationView<Content: View>: View {
state.leftButtonText
}
}
.lce_applyGlassIfEnabled(state.useGlassButtons)
.background(
GeometryReader { proxy in
Color.clear.preference(key: LCENavButtonWidthPreferenceKey.self, value: proxy.size.width)
}
)
}
/// The private `NavRightButton` view.
/// The private `NavRightButton` view. Only rendered when `setRightButton` was actually called never a hidden placeholder.
private var NavRightButton: some View {
Button(action: state.rightButtonAction) {
HStack {
@@ -138,6 +191,12 @@ public struct LCENavigationView<Content: View>: View {
}
}
}
.lce_applyGlassIfEnabled(state.useGlassButtons)
.background(
GeometryReader { proxy in
Color.clear.preference(key: LCENavButtonWidthPreferenceKey.self, value: proxy.size.width)
}
)
}
/// Sets the configuration for the right button of the navigation bar.
@@ -158,13 +217,7 @@ public struct LCENavigationView<Content: View>: View {
}
state.rightButtonText = text
state.rightButtonAction = action
if let string = state.leftButtonText.string, string.isEmpty {
state.leftButtonText = text.foregroundColor(.clear)
}
if state.leftButtonImage == nil {
state.leftButtonImage = image?.foregroundColor(.clear) as? AnyView
}
state.hasRightButton = true
return self
}
@@ -187,14 +240,17 @@ public struct LCENavigationView<Content: View>: View {
}
state.leftButtonText = text
state.leftButtonAction = action
state.hasLeftButton = true
if let string = state.rightButtonText.string, string.isEmpty {
state.rightButtonText = text.foregroundColor(.clear)
}
if state.rightButtonImage == nil {
state.rightButtonImage = image?.foregroundColor(.clear) as? AnyView
}
return self
}
/// Opts the navigation bar's buttons into the system Liquid Glass `.glass` button style on iOS 26+.
/// The navigation bar is designed as a plain, non-glass component by default; call this to enable glass explicitly.
/// - Parameter enabled: Whether buttons should use the glass style.
/// - Returns: The `LCENavigationView` instance for chaining.
public func setGlassButtonsEnabled(_ enabled: Bool) -> LCENavigationView {
state.useGlassButtons = enabled
return self
}
@@ -229,101 +285,6 @@ public struct LCENavigationView<Content: View>: View {
}
}
/// Extension to `FormatStyle` to format any value as a string.
@available(iOS 15.0, *)
extension FormatStyle {
/// Formats an input value if it matches the `FormatInput` type.
/// - Parameter value: The value to format as `Any`.
/// - Returns: The formatted output, or `nil` if the value type does not match.
func format(any value: Any) -> FormatOutput? {
if let v = value as? FormatInput {
return format(v)
}
return nil
}
}
/// Extension to `LocalizedStringKey` to resolve localized strings.
@available(iOS 15.0, *)
extension LocalizedStringKey {
/// Resolves the localized string key into a `String`.
/// - Returns: The resolved string, or `nil` if resolution fails.
var resolved: String? {
let mirror = Mirror(reflecting: self)
guard let key = mirror.descendant("key") as? String else {
return nil
}
guard let args = mirror.descendant("arguments") as? [Any] else {
return nil
}
let values = args.map { arg -> Any? in
let mirror = Mirror(reflecting: arg)
if let value = mirror.descendant("storage", "value", ".0") {
return value
}
guard let format = mirror.descendant("storage", "formatStyleValue", "format") as? any FormatStyle,
let input = mirror.descendant("storage", "formatStyleValue", "input") else {
return nil
}
return format.format(any: input)
}
let va = values.compactMap { arg -> CVarArg? in
switch arg {
case let i as Int: return i
case let i as Int64: return i
case let i as Int8: return i
case let i as Int16: return i
case let i as Int32: return i
case let u as UInt: return u
case let u as UInt64: return u
case let u as UInt8: return u
case let u as UInt16: return u
case let u as UInt32: return u
case let f as Float: return f
case let f as CGFloat: return f
case let d as Double: return d
case let o as NSObject: return o
default: return nil
}
}
if va.count != values.count {
return nil
}
return String.localizedStringWithFormat(key, va)
}
}
/// Extension to `Text` to retrieve its string content.
@available(iOS 15.0, *)
extension Text {
/// Returns the string representation of the `Text` view.
/// - Returns: The string content, or `nil` if it cannot be extracted.
var string: String? {
let mirror = Mirror(reflecting: self)
if let s = mirror.descendant("storage", "verbatim") as? String {
return s
} else if let attrStr = mirror.descendant("storage", "anyTextStorage", "str") as? AttributedString {
return String(attrStr.characters)
} else if let key = mirror.descendant("storage", "anyTextStorage", "key") as? LocalizedStringKey {
return key.resolved
} else if let format = mirror.descendant("storage", "anyTextStorage", "storage", "format") as? any FormatStyle,
let input = mirror.descendant("storage", "anyTextStorage", "storage", "input") {
return format.format(any: input) as? String
} else if let formatter = mirror.descendant("storage", "anyTextStorage", "formatter") as? Formatter,
let object = mirror.descendant("storage", "anyTextStorage", "object") {
return formatter.string(for: object)
}
return nil
}
}
//@available(iOS 15.0, *)
//struct LCENavigationView_Previews: PreviewProvider {
// static var previews: some View {

View File

@@ -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
}
}
}

View File

@@ -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()
}
}

View File

@@ -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 3060s client-side.
public var cacheTTL: TimeInterval
/// FC-060 §5 recommends 1.53s 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
}
}

View File

@@ -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
}
}

View File

@@ -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)
}
}

View File

@@ -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
}
}

View File

@@ -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?
}

View File

@@ -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
}

View File

@@ -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")
}
}
}

View File

@@ -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<FeatureControlSnapshot, Error>] = [:]
/// `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<FeatureControlSnapshot, Error> {
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..<Swift.min($0 + size, count)]) }
}
}

View File

@@ -0,0 +1,65 @@
import Foundation
/// `GET /api/feature-control/notifications` `status` query filter.
public enum FeatureControlNotificationStatus: String, Sendable {
case all, unread
}
/// One item from `NotificationListItem`.
public struct FeatureControlNotification: Sendable, Equatable, Identifiable {
public let id: String
public let title: String
public let body: String
public let severity: String
public let createdAt: Date
public let read: Bool
public let ctaLabel: String?
public let ctaUrl: String?
public init(id: String, title: String, body: String, severity: String,
createdAt: Date, read: Bool, ctaLabel: String?, ctaUrl: String?) {
self.id = id
self.title = title
self.body = body
self.severity = severity
self.createdAt = createdAt
self.read = read
self.ctaLabel = ctaLabel
self.ctaUrl = ctaUrl
}
}
extension FeatureControlNotification: Decodable {
private enum CodingKeys: String, CodingKey {
case id, title, body, severity, createdAt, read, ctaLabel, ctaUrl
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let rawDate = try container.decode(String.self, forKey: .createdAt)
guard let date = FeatureControlDateParsing.parse(rawDate) else {
throw DecodingError.dataCorruptedError(forKey: .createdAt, in: container,
debugDescription: "Unrecognized date format: \(rawDate)")
}
self.id = try container.decode(String.self, forKey: .id)
self.title = try container.decode(String.self, forKey: .title)
self.body = try container.decode(String.self, forKey: .body)
self.severity = try container.decode(String.self, forKey: .severity)
self.createdAt = date
self.read = try container.decode(Bool.self, forKey: .read)
self.ctaLabel = try container.decodeIfPresent(String.self, forKey: .ctaLabel)
self.ctaUrl = try container.decodeIfPresent(String.self, forKey: .ctaUrl)
}
}
/// `GET /api/feature-control/notifications` `result` shape.
struct FeatureControlNotificationListResult: Decodable, Sendable {
let items: [FeatureControlNotification]
let nextCursor: String?
}
/// Wire envelope for the notifications list `{error, result}`.
struct FeatureControlNotificationListEnvelope: Decodable, Sendable {
let error: Bool
let result: FeatureControlNotificationListResult
}

View File

@@ -0,0 +1,87 @@
import Foundation
import LCEssentials
/// Protocol seam for DI, mirroring `FeatureControlEvaluating`.
public protocol FeatureControlNotifying: Sendable {
func list(status: FeatureControlNotificationStatus, limit: Int, cursor: String?) async throws
-> (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)
}
}
}

View File

@@ -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")
}
}

View File

@@ -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)
}
}

View File

@@ -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)")
}
}
}

View File

@@ -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)
}
}

View File

@@ -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)]))
}
}

View File

@@ -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)")
}
}
}

View File

@@ -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")
}
}

View File

@@ -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)")
}
}
}

View File

@@ -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
}
}