diff --git a/AppleSignInBackend/.gitignore b/AppleSignInBackend/.gitignore new file mode 100644 index 000000000..0fa9f56b1 --- /dev/null +++ b/AppleSignInBackend/.gitignore @@ -0,0 +1,20 @@ +# Dependencies +node_modules/ + +# Environment +.env + +# Logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# OS +.DS_Store +Thumbs.db + +# TypeScript compiled output +dist/ +*.tsbuildinfo + diff --git a/AppleSignInBackend/README.md b/AppleSignInBackend/README.md new file mode 100644 index 000000000..d4876f15f --- /dev/null +++ b/AppleSignInBackend/README.md @@ -0,0 +1,327 @@ +# Apple Sign-in Backend (BFF) + +A Backend-for-Frontend (BFF) server for Apple Sign-in integration with Immutable Passport Unity SDK. This server securely handles Apple identity token verification and exchanges them for Passport authentication tokens. + +## Overview + +This BFF is a critical security component that: +- **Protects sensitive credentials**: Securely stores your Immutable Project API Key (must never be in client applications) +- **Verifies Apple tokens**: Validates Apple identity tokens against Apple's public keys +- **Performs secure token exchange**: Communicates server-to-server with Immutable's authentication service + +**Production Requirement**: Studios must host and maintain this BFF server for Apple Sign-in to work. The SDK will call your hosted BFF endpoint during the authentication flow. + +## Features + +- ✅ **Apple JWT Verification**: Real verification against Apple's public keys +- ✅ **Immutable Token Exchange**: Integrates with IMX Engine Auth Service +- ✅ **TypeScript Support**: Available in both TypeScript and JavaScript +- ✅ **Comprehensive Logging**: Detailed logs for debugging and monitoring +- ✅ **CORS Enabled**: Configurable for your client applications +- ✅ **Health Check**: Monitoring endpoint for uptime checks + +--- + +## Quick Start + +### 1. Install Dependencies + +```bash +cd AppleSignInBackend +npm install +``` + +### 2. Configure Environment + +Copy the example config: + +```bash +cp env.example .env +``` + +Edit `.env`: + +```bash +# Server Configuration +PORT=3000 + +# Apple Configuration +APPLE_BUNDLE_ID=com.yourstudio.yourgame + +# Immutable Configuration +IMX_AUTH_API_URL=https://api.sandbox.immutable.com # or https://api.immutable.com for production +IMMUTABLE_API_KEY=your_project_api_key_from_hub +PASSPORT_CLIENT_ID=your_passport_client_id +``` + +**Required Configuration:** +- `APPLE_BUNDLE_ID`: Your iOS app's bundle identifier (must match Xcode project) +- `IMMUTABLE_API_KEY`: Project API key from Immutable Hub (keep secret!) +- `PASSPORT_CLIENT_ID`: Your Passport OAuth client ID from Immutable Hub +- `IMX_AUTH_API_URL`: Environment endpoint (sandbox or production) + +### 3. Build and Start Server + +**TypeScript (Recommended):** + +```bash +npm run build +npm start +``` + +**Development mode with hot-reload:** + +```bash +npm run dev +``` + +**JavaScript (Legacy):** + +```bash +npm run start:js +``` + +You should see: + +``` +======================================== +Apple Sign-in Backend Started +======================================== +Port: 3000 +Bundle ID: com.yourstudio.yourgame + +IMX Engine Configuration: + Auth API URL: https://api.sandbox.immutable.com + API Key: sk_imapik_xxx... + Client ID: 2Dx7GLUZeFsMnmp1k... + +Endpoints: + POST http://localhost:3000/auth/apple - Apple Sign-in + GET http://localhost:3000/health - Health check + +Ready to receive Apple Sign-in requests from Unity! +======================================== +``` + +--- + +## Unity Configuration + +### Passport Initialization + +Configure Passport to use your hosted BFF: + +```csharp +// Initialize Passport with your environment +const string clientId = "YOUR_PASSPORT_CLIENT_ID"; +const string environment = "production"; // or "sandbox" + +var passport = await Passport.Init( + clientId, + environment, + redirectUri, + logoutRedirectUri +); + +// The SDK will determine the BFF URL based on environment +// For custom BFF URLs, configure via PassportConfig +``` + +The Unity SDK will call your BFF's `/auth/apple` endpoint during the Apple Sign-in flow. + +--- + +## API Endpoints + +### POST /auth/apple + +Main authentication endpoint called by Unity SDK during Apple Sign-in. + +**Request Headers:** +``` +Content-Type: application/json +``` + +**Request Body:** + +```json +{ + "identityToken": "eyJraWQiOiJIdlZJNkVzWlhKIi...", + "authorizationCode": "c11fa69428a5b47e4ae6...", + "userId": "001400.c6a985445c1e4e74a42e0d3dfe25697b.0005", + "email": "user@privaterelay.appleid.com", + "fullName": "John Doe", + "clientId": "YOUR_PASSPORT_CLIENT_ID" +} +``` + +**Response (Success - 200):** + +```json +{ + "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...", + "id_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...", + "refresh_token": "v1.abc123...", + "token_type": "Bearer", + "expires_in": 86400 +} +``` + +**Response (Error - 4xx/5xx):** + +```json +{ + "error": "invalid_token", + "error_description": "Apple token verification failed: invalid signature" +} +``` + +**Error Codes:** +- `400 Bad Request`: Missing required fields +- `401 Unauthorized`: Token verification failed +- `403 Forbidden`: Client ID mismatch +- `500 Internal Server Error`: Server error during token exchange + +### GET /health + +Health check endpoint for monitoring. + +**Response (200):** + +```json +{ + "status": "ok", + "environment": "https://api.sandbox.immutable.com", + "timestamp": "2025-10-30T12:34:56.789Z" +} +``` + +--- + +## How It Works + +### Authentication Flow + +1. **User initiates Apple Sign-in** in Unity app +2. **Apple authentication** completes, returns identity token +3. **Unity sends token to BFF** via POST /auth/apple +4. **BFF verifies Apple token** against Apple's public keys +5. **BFF extracts user email** from verified token +6. **BFF exchanges for Passport tokens** via IMX Engine Auth Service: + - Endpoint: `/v1/token-exchange` + - Headers: `x-immutable-api-key: YOUR_API_KEY` + - Body: `{ email, client_id }` +7. **BFF returns Auth0 tokens** to Unity +8. **Unity completes authentication** with Passport + +### Token Exchange (BYOA) + +The BFF uses BYOA connection: + +```typescript +// BFF calls IMX Engine Auth Service +POST https://api.immutable.com/v1/token-exchange +Headers: + Content-Type: application/json + x-immutable-api-key: YOUR_API_KEY +Body: + { + "email": "user@example.com", + "client_id": "YOUR_PASSPORT_CLIENT_ID" + } + +// Returns Auth0 tokens +Response: + { + "access_token": "...", + "id_token": "...", + "refresh_token": "...", + "token_type": "Bearer", + "expires_in": 86400 + } +``` + +--- + +## Testing + +### Health Check + +Verify the server is running: + +```bash +curl http://localhost:3000/health +``` + +Expected response: +```json +{ + "status": "ok", + "environment": "https://api.sandbox.immutable.com", + "timestamp": "2025-10-30T12:34:56.789Z" +} +``` + +### Test Authentication Flow + +You can test the endpoint with cURL (requires valid Apple identity token): + +```bash +curl -X POST http://localhost:3000/auth/apple \ + -H "Content-Type: application/json" \ + -d '{ + "identityToken": "VALID_APPLE_IDENTITY_TOKEN", + "userId": "001400.xxx.0005", + "email": "user@example.com", + "clientId": "YOUR_PASSPORT_CLIENT_ID" + }' +``` + +### Testing with Unity + +1. Build and deploy your BFF server +2. Configure Unity SDK to use your BFF URL +3. Run the Unity app on iOS device +4. Tap "Sign in with Apple" +5. Complete Apple authentication +6. Check BFF logs for the request flow + +**Expected BFF Logs:** + +``` +======================================== +Apple Sign-in Request Received +======================================== +User ID: 001400.xxx.0005 +Email: user@privaterelay.appleid.com +Client ID: 2Dx7GLUZeFsMnmp1k... + +Step 1: Verifying Apple identity token with Apple's public keys... +SUCCESS: Token verified successfully + Token subject (sub): 001400.xxx.0005 + Token email: user@privaterelay.appleid.com + +Step 2: Exchanging for Auth0 tokens via IMX Engine Auth Service... +SUCCESS: Token exchange successful + Status: 200 + Token Type: Bearer + Expires In: 86400 + +======================================== +SUCCESS: Apple Sign-in Complete - Returning Tokens +======================================== +``` + +--- + +## Scripts + +```bash +# TypeScript +npm run build # Compile TypeScript to JavaScript +npm start # Run compiled JavaScript (production) +npm run dev # Development with hot-reload + +# Build utilities +npm run dev:build # Watch mode - compile on changes +``` diff --git a/AppleSignInBackend/env.example b/AppleSignInBackend/env.example new file mode 100644 index 000000000..bada54387 --- /dev/null +++ b/AppleSignInBackend/env.example @@ -0,0 +1,17 @@ +# Server Configuration +PORT=3000 + +# Apple Configuration +# The Bundle ID configured in Apple Developer Console for Sign in with Apple +APPLE_BUNDLE_ID=com.immutable.Immutable-Sample-GameSDK + +# Auth Service Configuration +# Where to send requests (BFF forwards to this URL) +IMX_AUTH_API_URL=https://api.sandbox.immutable.com + +# Immutable Credentials (from Hub project) +# This BFF runs server-side, so the secret key is safe here +IMMUTABLE_API_KEY=sk_imapik-test-YOUR-SECRET-KEY-HERE + +# Passport Client ID +PASSPORT_CLIENT_ID=YOUR-CLIENT-ID-HERE diff --git a/AppleSignInBackend/package-lock.json b/AppleSignInBackend/package-lock.json new file mode 100644 index 000000000..b06772c0b --- /dev/null +++ b/AppleSignInBackend/package-lock.json @@ -0,0 +1,1730 @@ +{ + "name": "apple-signin-mock-backend", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "apple-signin-mock-backend", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "axios": "^1.6.0", + "cors": "^2.8.5", + "dotenv": "^16.3.1", + "express": "^4.18.2", + "jsonwebtoken": "^9.0.2", + "jwks-rsa": "^3.1.0" + }, + "devDependencies": { + "@types/cors": "^2.8.17", + "@types/express": "^4.17.21", + "@types/jsonwebtoken": "^9.0.5", + "@types/node": "^20.10.6", + "nodemon": "^3.0.1", + "ts-node": "^10.9.2", + "typescript": "^5.3.3" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", + "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", + "dev": true + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/cors": { + "version": "2.8.19", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/express": { + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.23.tgz", + "integrity": "sha512-Crp6WY9aTYP3qPi2wGDo9iUe/rceX01UMhnF1jmwDcKCFM6cx7YhGP/Mpr3y9AASpfHixIG0E6azCcL5OcDHsQ==", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.19.7", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.7.tgz", + "integrity": "sha512-FvPtiIf1LfhzsaIXhv/PHan/2FeQBbtBDtfX2QfvPxdUelMDEckK08SM6nqo1MIZY3RUlfA+HV8+hFUSio78qg==", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==" + }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.10", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz", + "integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==", + "dependencies": { + "@types/ms": "*", + "@types/node": "*" + } + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==" + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==" + }, + "node_modules/@types/node": { + "version": "20.19.24", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.24.tgz", + "integrity": "sha512-FE5u0ezmi6y9OZEzlJfg37mqqf6ZDSF2V/NLjUyGrR9uTZ7Sb9F7bLNZ03S4XVUNRWGA7Ck4c1kK+YnuWjl+DA==", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==" + }, + "node_modules/@types/send": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.0.tgz", + "integrity": "sha512-zBF6vZJn1IaMpg3xUF25VK3gd3l8zwE0ZLRX7dsQyQi+jp4E8mMDJNGDYnYse+bQhYwWERTxVwHpi3dMOq7RKQ==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.9.tgz", + "integrity": "sha512-dOTIuqpWLyl3BBXU3maNQsS4A3zuuoYRNIvYSxxhebPfXg2mzWQEPne/nlJ37yOse6uGgR386uTpdsx4D0QZWA==", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "<1" + } + }, + "node_modules/@types/serve-static/node_modules/@types/send": { + "version": "0.17.5", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.5.tgz", + "integrity": "sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w==", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dev": true, + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/axios": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz", + "integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.7.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.3.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.12", + "proxy-addr": "~2.0.7", + "qs": "6.13.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.19.0", + "serve-static": "1.16.2", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/jose": { + "version": "4.15.9", + "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.9.tgz", + "integrity": "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jsonwebtoken/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/jwa": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.2.tgz", + "integrity": "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jwks-rsa": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/jwks-rsa/-/jwks-rsa-3.2.0.tgz", + "integrity": "sha512-PwchfHcQK/5PSydeKCs1ylNym0w/SSv8a62DgHJ//7x2ZclCoinlsjAfDxAAbpoTPybOum/Jgy+vkvMmKz89Ww==", + "dependencies": { + "@types/express": "^4.17.20", + "@types/jsonwebtoken": "^9.0.4", + "debug": "^4.3.4", + "jose": "^4.15.4", + "limiter": "^1.1.5", + "lru-memoizer": "^2.2.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/jwks-rsa/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/jwks-rsa/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/limiter": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/limiter/-/limiter-1.1.5.tgz", + "integrity": "sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA==" + }, + "node_modules/lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==" + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/lru-memoizer": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/lru-memoizer/-/lru-memoizer-2.3.0.tgz", + "integrity": "sha512-GXn7gyHAMhO13WSKrIiNfztwxodVsP8IoZ3XfrJV4yH2x0/OeTO/FIaAHTY5YekdGgW94njfuKmyyt1E0mR6Ug==", + "dependencies": { + "lodash.clonedeep": "^4.5.0", + "lru-cache": "6.0.0" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/nodemon": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.10.tgz", + "integrity": "sha512-WDjw3pJ0/0jMFmyNDp3gvY2YizjLmmOUQo6DEBY+JgdvW/yQ9mEeSw6H5ythl5Ny2ytb7f9C2nIbjSxMNzbJXw==", + "dev": true, + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^4", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.1.2", + "pstree.remy": "^1.1.8", + "semver": "^7.5.3", + "simple-update-notifier": "^2.0.0", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/nodemon/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/nodemon/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, + "node_modules/pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true + }, + "node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/touch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", + "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", + "dev": true, + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "engines": { + "node": ">=6" + } + } + } +} diff --git a/AppleSignInBackend/package.json b/AppleSignInBackend/package.json new file mode 100644 index 000000000..d70e99d05 --- /dev/null +++ b/AppleSignInBackend/package.json @@ -0,0 +1,38 @@ +{ + "name": "apple-signin-mock-backend", + "version": "1.0.0", + "description": "Mock backend server for testing Apple Sign-in with Immutable Passport", + "main": "dist/server.js", + "scripts": { + "build": "tsc", + "start": "node dist/server.js", + "dev": "nodemon --watch '*.ts' --exec 'ts-node' server.ts", + "dev:build": "tsc --watch", + "start:js": "node server.js" + }, + "keywords": [ + "apple-signin", + "passport", + "immutable" + ], + "author": "", + "license": "MIT", + "dependencies": { + "express": "^4.18.2", + "cors": "^2.8.5", + "dotenv": "^16.3.1", + "jsonwebtoken": "^9.0.2", + "jwks-rsa": "^3.1.0", + "axios": "^1.6.0" + }, + "devDependencies": { + "nodemon": "^3.0.1", + "@types/express": "^4.17.21", + "@types/cors": "^2.8.17", + "@types/jsonwebtoken": "^9.0.5", + "@types/node": "^20.10.6", + "typescript": "^5.3.3", + "ts-node": "^10.9.2" + } +} + diff --git a/AppleSignInBackend/server.ts b/AppleSignInBackend/server.ts new file mode 100644 index 000000000..60e4f6d76 --- /dev/null +++ b/AppleSignInBackend/server.ts @@ -0,0 +1,388 @@ +import 'dotenv/config'; +import express, { Request, Response, NextFunction } from 'express'; +import cors from 'cors'; +import jwt, { JwtHeader, VerifyErrors } from 'jsonwebtoken'; +import jwksClient, { SigningKey, RsaSigningKey } from 'jwks-rsa'; +import axios, { AxiosError } from 'axios'; + +const app = express(); +const PORT = process.env.PORT || 3000; + +// Configuration +const APPLE_BUNDLE_ID = process.env.APPLE_BUNDLE_ID || 'com.immutable.Immutable-Sample-GameSDK'; + +// IMX Engine Auth Service Configuration +const IMX_AUTH_API_URL = process.env.IMX_AUTH_API_URL || 'https://api.sandbox.immutable.com'; +const IMMUTABLE_API_KEY = process.env.IMMUTABLE_API_KEY; +const PASSPORT_CLIENT_ID = process.env.PASSPORT_CLIENT_ID; + +// Validate required configuration +if (!IMMUTABLE_API_KEY) { + console.error('ERROR: IMMUTABLE_API_KEY is required but not set'); + console.error(' Set it in your .env file'); + process.exit(1); +} + +if (!PASSPORT_CLIENT_ID) { + console.error('ERROR: PASSPORT_CLIENT_ID is required but not set'); + console.error(' Set it in your .env file'); + process.exit(1); +} + +// Type definitions +interface AppleDecodedToken { + sub: string; + email?: string; + iss: string; + aud: string; + exp: number; + iat: number; +} + +interface AppleSignInRequest { + identityToken: string; + authorizationCode?: string; + userId: string; + email?: string; + fullName?: string; + clientId: string; +} + +interface Auth0TokenResponse { + access_token: string; + id_token?: string; + refresh_token?: string; + token_type: string; + expires_in: number; +} + +interface TokenExchangeRequest { + email: string; + client_id: string; +} + +interface ErrorResponse { + error: string; + error_description: string; +} + +interface HealthResponse { + status: string; + environment: string; + timestamp: string; +} + +// Middleware +app.use(cors()); +app.use(express.json()); + +// Logging middleware +app.use((req: Request, res: Response, next: NextFunction) => { + console.log(`[${new Date().toISOString()}] ${req.method} ${req.path}`); + next(); +}); + +// Apple JWKS client for real verification +const appleJwksClient = jwksClient({ + jwksUri: 'https://appleid.apple.com/auth/keys', + cache: true, + rateLimit: true +}); + +/** + * Get Apple's public key for JWT verification + */ +function getApplePublicKey( + header: JwtHeader, + callback: (err: Error | null, publicKey?: string) => void +): void { + appleJwksClient.getSigningKey(header.kid, (err: Error | null, key?: SigningKey) => { + if (err) { + callback(err); + return; + } + if (!key) { + callback(new Error('No signing key found')); + return; + } + const signingKey = (key as RsaSigningKey).rsaPublicKey || key.getPublicKey(); + callback(null, signingKey); + }); +} + +/** + * Verify Apple identity token using Apple's public keys + */ +async function verifyAppleToken(identityToken: string): Promise { + return new Promise((resolve, reject) => { + jwt.verify( + identityToken, + getApplePublicKey, + { + algorithms: ['RS256'], + issuer: 'https://appleid.apple.com', + audience: APPLE_BUNDLE_ID + }, + (err: VerifyErrors | null, decoded: any) => { + if (err) { + reject(err); + return; + } + resolve(decoded as AppleDecodedToken); + } + ); + }); +} + +/** + * Exchange for Auth0 tokens via IMX Engine Auth Service + * Uses the /v1/token-exchange endpoint + */ +async function exchangeForAuth0Tokens( + email: string, + clientId: string, + apiKey: string +): Promise { + if (!email) { + throw new Error('Email is required for token exchange'); + } + + console.log('Calling IMX Engine Auth Service token exchange API...'); + console.log(' URL:', `${IMX_AUTH_API_URL}/v1/token-exchange`); + console.log(' Email:', email); + console.log(' Client ID:', clientId); + + try { + const response = await axios.post( + `${IMX_AUTH_API_URL}/v1/token-exchange`, + { + email: email, + client_id: clientId + } as TokenExchangeRequest, + { + headers: { + 'Content-Type': 'application/json', + 'x-immutable-api-key': apiKey + } + } + ); + + console.log('SUCCESS: Token exchange successful'); + console.log(' Status:', response.status); + console.log(' Token Type:', response.data.token_type); + console.log(' Expires In:', response.data.expires_in); + console.log(' Response:', JSON.stringify(response.data, null, 2)); + + return response.data; + } catch (error) { + console.error('ERROR: Token exchange API call failed'); + + const axiosError = error as AxiosError; + if (axiosError.response) { + console.error(' Status:', axiosError.response.status); + console.error(' Response:', JSON.stringify(axiosError.response.data, null, 2)); + throw new Error(`Token exchange failed with status ${axiosError.response.status}: ${JSON.stringify(axiosError.response.data)}`); + } else if (axiosError.request) { + console.error(' No response received from server'); + throw new Error('Token exchange failed: No response from server'); + } else { + console.error(' Error:', axiosError.message); + throw new Error(`Token exchange failed: ${axiosError.message}`); + } + } +} + +// ======================================== +// ROUTES +// ======================================== + +/** + * Health check endpoint + */ +app.get('/health', (req: Request, res: Response) => { + res.json({ + status: 'ok', + environment: IMX_AUTH_API_URL, + timestamp: new Date().toISOString() + }); +}); + +/** + * Main Apple Sign-in endpoint + * This is what Unity calls + */ +app.post('/auth/apple', async (req: Request<{}, {}, AppleSignInRequest>, res: Response) => { + try { + const { identityToken, authorizationCode, userId, email, fullName, clientId } = req.body; + + console.log('\n========================================'); + console.log('Apple Sign-in Request Received'); + console.log('========================================'); + console.log('User ID:', userId); + console.log('Email:', email || '(not provided - will extract from token)'); + console.log('Full Name:', fullName || '(not provided)'); + console.log('Client ID:', clientId || '(missing)'); + console.log('Identity Token:', identityToken ? identityToken : '(missing)'); + console.log('Authorization Code:', authorizationCode ? authorizationCode : '(missing)'); + console.log('========================================\n'); + + // Validate required fields + if (!identityToken || !userId) { + return res.status(400).json({ + error: 'invalid_request', + error_description: 'Missing required fields: identityToken, userId' + }); + } + + // Validate client ID matches expected value (security check) + if (!clientId) { + console.error('ERROR: Client ID not provided in request'); + return res.status(400).json({ + error: 'invalid_request', + error_description: 'Client ID is required' + }); + } + + if (clientId !== PASSPORT_CLIENT_ID) { + console.error('ERROR: Client ID mismatch'); + console.error(' Expected:', PASSPORT_CLIENT_ID); + console.error(' Received:', clientId); + return res.status(403).json({ + error: 'invalid_client', + error_description: 'Client ID does not match expected value' + }); + } + + console.log('SUCCESS: Client ID validated'); + + // Step 1: Verify Apple identity token + console.log('Step 1: Verifying Apple identity token with Apple\'s public keys...'); + let decodedToken: AppleDecodedToken; + + try { + decodedToken = await verifyAppleToken(identityToken); + } catch (err) { + console.error('ERROR: Apple token verification failed:', (err as Error).message); + return res.status(401).json({ + error: 'invalid_token', + error_description: `Apple token verification failed: ${(err as Error).message}` + }); + } + + console.log('SUCCESS: Token verified successfully'); + console.log(' Token subject (sub):', decodedToken.sub); + console.log(' Token email:', decodedToken.email); + console.log(' Token issuer:', decodedToken.iss); + console.log(' Token audience (aud):', decodedToken.aud); + + // Validate bundle ID matches token audience + if (decodedToken.aud !== APPLE_BUNDLE_ID) { + console.error('ERROR: Bundle ID mismatch with token audience'); + console.error(' Expected:', APPLE_BUNDLE_ID); + console.error(' Token Audience:', decodedToken.aud); + return res.status(401).json({ + error: 'invalid_token', + error_description: 'Apple token audience does not match expected bundle ID' + }); + } + + console.log('SUCCESS: Bundle ID matches token audience'); + + // Step 2: Validate token claims + if (decodedToken.sub !== userId) { + console.error('ERROR: Token user ID mismatch'); + console.error(' Expected:', userId); + console.error(' Got:', decodedToken.sub); + return res.status(401).json({ + error: 'invalid_token', + error_description: 'Token user ID does not match provided user ID' + }); + } + + // Step 3: Extract email (prefer token email over provided email) + const userEmail = decodedToken.email || email; + if (!userEmail) { + console.error('ERROR: No email found in token or request'); + return res.status(400).json({ + error: 'invalid_request', + error_description: 'Email is required but was not found in token or request' + }); + } + + console.log('SUCCESS: Using email:', userEmail); + + // Step 4: Exchange for Auth0 tokens via IMX Engine Auth Service + console.log('\nStep 2: Exchanging for Auth0 tokens via IMX Engine Auth Service...'); + + try { + // { access_token, id_token, refresh_token, token_type, expires_in } + const auth0Tokens = await exchangeForAuth0Tokens( + userEmail, + PASSPORT_CLIENT_ID, + IMMUTABLE_API_KEY + ); + + console.log('SUCCESS: Auth0 tokens obtained'); + console.log(' Access Token:', auth0Tokens.access_token); + console.log(' ID Token:', auth0Tokens.id_token ? auth0Tokens.id_token : '(none)'); + console.log(' Refresh Token:', auth0Tokens.refresh_token ? auth0Tokens.refresh_token : '(none)'); + + console.log('\n========================================'); + console.log('SUCCESS: Apple Sign-in Complete - Returning Tokens'); + console.log('========================================\n'); + + return res.json(auth0Tokens); + } catch (err) { + console.error('ERROR: Auth0 token exchange failed:', (err as Error).message); + return res.status(401).json({ + error: 'token_exchange_failed', + error_description: `Failed to exchange for Auth0 tokens: ${(err as Error).message}` + }); + } + + } catch (error) { + console.error('\nERROR: Error processing Apple Sign-in:', error); + console.error('Stack trace:', (error as Error).stack); + + res.status(500).json({ + error: 'server_error', + error_description: `Internal server error: ${(error as Error).message}` + }); + } +}); + +// ======================================== +// START SERVER +// ======================================== + +app.listen(PORT, () => { + console.log('\n========================================'); + console.log('Apple Sign-in Backend Started'); + console.log('========================================'); + console.log('Port:', PORT); + console.log('Bundle ID:', APPLE_BUNDLE_ID); + console.log(''); + console.log('IMX Engine Configuration:'); + console.log(' Auth API URL:', IMX_AUTH_API_URL); + console.log(' API Key:', `${IMMUTABLE_API_KEY.substring(0, 20)}...`); + console.log(' Client ID:', `${PASSPORT_CLIENT_ID.substring(0, 20)}...`); + console.log(''); + console.log('Endpoints:'); + console.log(` POST http://localhost:${PORT}/auth/apple - Apple Sign-in`); + console.log(` GET http://localhost:${PORT}/health - Health check`); + console.log(''); + console.log('Ready to receive Apple Sign-in requests from Unity!'); + console.log('========================================\n'); +}); + +// Graceful shutdown +process.on('SIGTERM', () => { + console.log('SIGTERM signal received: closing HTTP server'); + process.exit(0); +}); + +process.on('SIGINT', () => { + console.log('\nSIGINT signal received: closing HTTP server'); + process.exit(0); +}); + diff --git a/AppleSignInBackend/tsconfig.json b/AppleSignInBackend/tsconfig.json new file mode 100644 index 000000000..69174ba06 --- /dev/null +++ b/AppleSignInBackend/tsconfig.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "lib": ["ES2020"], + "outDir": "./dist", + "rootDir": "./", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "moduleResolution": "node", + "allowSyntheticDefaultImports": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true + }, + "include": [ + "*.ts" + ], + "exclude": [ + "node_modules", + "dist" + ] +} + + diff --git a/sample/Assets/Scenes/Passport/AppleSignInTestScene.unity b/sample/Assets/Scenes/Passport/AppleSignInTestScene.unity new file mode 100644 index 000000000..2157e5712 --- /dev/null +++ b/sample/Assets/Scenes/Passport/AppleSignInTestScene.unity @@ -0,0 +1,1759 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!29 &1 +OcclusionCullingSettings: + m_ObjectHideFlags: 0 + serializedVersion: 2 + m_OcclusionBakeSettings: + smallestOccluder: 5 + smallestHole: 0.25 + backfaceThreshold: 100 + m_SceneGUID: 00000000000000000000000000000000 + m_OcclusionCullingData: {fileID: 0} +--- !u!104 &2 +RenderSettings: + m_ObjectHideFlags: 0 + serializedVersion: 9 + m_Fog: 0 + m_FogColor: {r: 0.5, g: 0.5, b: 0.5, a: 1} + m_FogMode: 3 + m_FogDensity: 0.01 + m_LinearFogStart: 0 + m_LinearFogEnd: 300 + m_AmbientSkyColor: {r: 0.212, g: 0.227, b: 0.259, a: 1} + m_AmbientEquatorColor: {r: 0.114, g: 0.125, b: 0.133, a: 1} + m_AmbientGroundColor: {r: 0.047, g: 0.043, b: 0.035, a: 1} + m_AmbientIntensity: 1 + m_AmbientMode: 3 + m_SubtractiveShadowColor: {r: 0.42, g: 0.478, b: 0.627, a: 1} + m_SkyboxMaterial: {fileID: 0} + m_HaloStrength: 0.5 + m_FlareStrength: 1 + m_FlareFadeSpeed: 3 + m_HaloTexture: {fileID: 0} + m_SpotCookie: {fileID: 10001, guid: 0000000000000000e000000000000000, type: 0} + m_DefaultReflectionMode: 0 + m_DefaultReflectionResolution: 128 + m_ReflectionBounces: 1 + m_ReflectionIntensity: 1 + m_CustomReflection: {fileID: 0} + m_Sun: {fileID: 0} + m_IndirectSpecularColor: {r: 0, g: 0, b: 0, a: 1} + m_UseRadianceAmbientProbe: 0 +--- !u!157 &3 +LightmapSettings: + m_ObjectHideFlags: 0 + serializedVersion: 12 + m_GIWorkflowMode: 1 + m_GISettings: + serializedVersion: 2 + m_BounceScale: 1 + m_IndirectOutputScale: 1 + m_AlbedoBoost: 1 + m_EnvironmentLightingMode: 0 + m_EnableBakedLightmaps: 0 + m_EnableRealtimeLightmaps: 0 + m_LightmapEditorSettings: + serializedVersion: 12 + m_Resolution: 2 + m_BakeResolution: 40 + m_AtlasSize: 1024 + m_AO: 0 + m_AOMaxDistance: 1 + m_CompAOExponent: 1 + m_CompAOExponentDirect: 0 + m_ExtractAmbientOcclusion: 0 + m_Padding: 2 + m_LightmapParameters: {fileID: 0} + m_LightmapsBakeMode: 1 + m_TextureCompression: 1 + m_FinalGather: 0 + m_FinalGatherFiltering: 1 + m_FinalGatherRayCount: 256 + m_ReflectionCompression: 2 + m_MixedBakeMode: 2 + m_BakeBackend: 1 + m_PVRSampling: 1 + m_PVRDirectSampleCount: 32 + m_PVRSampleCount: 512 + m_PVRBounces: 2 + m_PVREnvironmentSampleCount: 256 + m_PVREnvironmentReferencePointCount: 2048 + m_PVRFilteringMode: 1 + m_PVRDenoiserTypeDirect: 1 + m_PVRDenoiserTypeIndirect: 1 + m_PVRDenoiserTypeAO: 1 + m_PVRFilterTypeDirect: 0 + m_PVRFilterTypeIndirect: 0 + m_PVRFilterTypeAO: 0 + m_PVREnvironmentMIS: 1 + m_PVRCulling: 1 + m_PVRFilteringGaussRadiusDirect: 1 + m_PVRFilteringGaussRadiusIndirect: 5 + m_PVRFilteringGaussRadiusAO: 2 + m_PVRFilteringAtrousPositionSigmaDirect: 0.5 + m_PVRFilteringAtrousPositionSigmaIndirect: 2 + m_PVRFilteringAtrousPositionSigmaAO: 1 + m_ExportTrainingData: 0 + m_TrainingDataDestination: TrainingData + m_LightProbeSampleCountMultiplier: 4 + m_LightingDataAsset: {fileID: 0} + m_LightingSettings: {fileID: 0} +--- !u!196 &4 +NavMeshSettings: + serializedVersion: 2 + m_ObjectHideFlags: 0 + m_BuildSettings: + serializedVersion: 2 + agentTypeID: 0 + agentRadius: 0.5 + agentHeight: 2 + agentSlope: 45 + agentClimb: 0.4 + ledgeDropHeight: 0 + maxJumpAcrossDistance: 0 + minRegionArea: 2 + manualCellSize: 0 + cellSize: 0.16666667 + manualTileSize: 0 + tileSize: 256 + accuratePlacement: 0 + maxJobWorkers: 0 + preserveTilesOutsideBounds: 0 + debug: + m_Flags: 0 + m_NavMeshData: {fileID: 0} +--- !u!1 &26397528 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 26397529} + - component: {fileID: 26397532} + - component: {fileID: 26397531} + - component: {fileID: 26397530} + m_Layer: 5 + m_Name: Scroll View + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &26397529 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 26397528} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: + - {fileID: 488826838} + - {fileID: 305255339} + - {fileID: 211112646} + m_Father: {fileID: 1949690038} + m_RootOrder: 2 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 0} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 2000} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!114 &26397530 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 26397528} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 1aa08ab6e0800fa44ae55d278d1423e3, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Content: {fileID: 136561664} + m_Horizontal: 1 + m_Vertical: 1 + m_MovementType: 1 + m_Elasticity: 0.1 + m_Inertia: 1 + m_DecelerationRate: 0.135 + m_ScrollSensitivity: 1 + m_Viewport: {fileID: 488826838} + m_HorizontalScrollbar: {fileID: 305255340} + m_VerticalScrollbar: {fileID: 211112647} + m_HorizontalScrollbarVisibility: 2 + m_VerticalScrollbarVisibility: 2 + m_HorizontalScrollbarSpacing: -3 + m_VerticalScrollbarSpacing: -3 + m_OnValueChanged: + m_PersistentCalls: + m_Calls: [] +--- !u!114 &26397531 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 26397528} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 0.392} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 10907, guid: 0000000000000000f000000000000000, type: 0} + m_Type: 1 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!222 &26397532 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 26397528} + m_CullTransparentMesh: 1 +--- !u!1 &109000919 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 109000920} + - component: {fileID: 109000922} + - component: {fileID: 109000921} + - component: {fileID: 109000923} + m_Layer: 5 + m_Name: AppleSignInButton + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &109000920 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 109000919} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: + - {fileID: 274021658} + m_Father: {fileID: 1949690038} + m_RootOrder: 1 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0.5, y: 0.5} + m_AnchorMax: {x: 0.5, y: 0.5} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 800, y: 240} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!114 &109000921 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 109000919} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 10905, guid: 0000000000000000f000000000000000, type: 0} + m_Type: 1 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!222 &109000922 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 109000919} + m_CullTransparentMesh: 1 +--- !u!114 &109000923 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 109000919} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 4e29b1a8efbd4b44bb3f3716e73f07ff, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Navigation: + m_Mode: 3 + m_WrapAround: 0 + m_SelectOnUp: {fileID: 0} + m_SelectOnDown: {fileID: 0} + m_SelectOnLeft: {fileID: 0} + m_SelectOnRight: {fileID: 0} + m_Transition: 1 + m_Colors: + m_NormalColor: {r: 0, g: 0, b: 0, a: 1} + m_HighlightedColor: {r: 0.19607843, g: 0.19607843, b: 0.19607843, a: 1} + m_PressedColor: {r: 0.39215687, g: 0.39215687, b: 0.39215687, a: 1} + m_SelectedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1} + m_DisabledColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 0.5019608} + m_ColorMultiplier: 1 + m_FadeDuration: 0.1 + m_SpriteState: + m_HighlightedSprite: {fileID: 0} + m_PressedSprite: {fileID: 0} + m_SelectedSprite: {fileID: 0} + m_DisabledSprite: {fileID: 0} + m_AnimationTriggers: + m_NormalTrigger: Normal + m_HighlightedTrigger: Highlighted + m_PressedTrigger: Pressed + m_SelectedTrigger: Selected + m_DisabledTrigger: Disabled + m_Interactable: 1 + m_TargetGraphic: {fileID: 109000921} + m_OnClick: + m_PersistentCalls: + m_Calls: [] +--- !u!1 &136561663 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 136561664} + m_Layer: 5 + m_Name: Content + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &136561664 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 136561663} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: + - {fileID: 873773140} + m_Father: {fileID: 488826838} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 1} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 0, y: 0.00000003545523} + m_SizeDelta: {x: 0, y: 300} + m_Pivot: {x: 0, y: 1} +--- !u!1 &183834310 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 183834311} + - component: {fileID: 183834313} + - component: {fileID: 183834312} + m_Layer: 5 + m_Name: Handle + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &183834311 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 183834310} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 885700823} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 0, y: 0} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 20, y: 20} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!114 &183834312 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 183834310} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 10905, guid: 0000000000000000f000000000000000, type: 0} + m_Type: 1 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!222 &183834313 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 183834310} + m_CullTransparentMesh: 1 +--- !u!1 &208235394 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 208235395} + - component: {fileID: 208235396} + - component: {fileID: 208235397} + m_Layer: 5 + m_Name: StatusText + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &208235395 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 208235394} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 1949690038} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 1} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 0, y: -350} + m_SizeDelta: {x: 0, y: 80} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &208235396 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 208235394} + m_CullTransparentMesh: 1 +--- !u!114 &208235397 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 208235394} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 0, g: 0, b: 0, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 48 + m_FontStyle: 0 + m_BestFit: 0 + m_MinSize: 3 + m_MaxSize: 48 + m_Alignment: 4 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 1 + m_LineSpacing: 1 + m_Text: 'Apple Sign In Test + +' +--- !u!1 &211112645 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 211112646} + - component: {fileID: 211112649} + - component: {fileID: 211112648} + - component: {fileID: 211112647} + m_Layer: 5 + m_Name: Scrollbar Vertical + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &211112646 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 211112645} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: + - {fileID: 885700823} + m_Father: {fileID: 26397529} + m_RootOrder: 2 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 1, y: 0} + m_AnchorMax: {x: 1, y: 0} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 20, y: 0} + m_Pivot: {x: 1, y: 1} +--- !u!114 &211112647 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 211112645} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 2a4db7a114972834c8e4117be1d82ba3, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Navigation: + m_Mode: 3 + m_WrapAround: 0 + m_SelectOnUp: {fileID: 0} + m_SelectOnDown: {fileID: 0} + m_SelectOnLeft: {fileID: 0} + m_SelectOnRight: {fileID: 0} + m_Transition: 1 + m_Colors: + m_NormalColor: {r: 1, g: 1, b: 1, a: 1} + m_HighlightedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1} + m_PressedColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 1} + m_SelectedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1} + m_DisabledColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 0.5019608} + m_ColorMultiplier: 1 + m_FadeDuration: 0.1 + m_SpriteState: + m_HighlightedSprite: {fileID: 0} + m_PressedSprite: {fileID: 0} + m_SelectedSprite: {fileID: 0} + m_DisabledSprite: {fileID: 0} + m_AnimationTriggers: + m_NormalTrigger: Normal + m_HighlightedTrigger: Highlighted + m_PressedTrigger: Pressed + m_SelectedTrigger: Selected + m_DisabledTrigger: Disabled + m_Interactable: 1 + m_TargetGraphic: {fileID: 183834312} + m_HandleRect: {fileID: 183834311} + m_Direction: 2 + m_Value: 0 + m_Size: 1 + m_NumberOfSteps: 0 + m_OnValueChanged: + m_PersistentCalls: + m_Calls: [] +--- !u!114 &211112648 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 211112645} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 10907, guid: 0000000000000000f000000000000000, type: 0} + m_Type: 1 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!222 &211112649 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 211112645} + m_CullTransparentMesh: 1 +--- !u!1 &274021657 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 274021658} + - component: {fileID: 274021660} + - component: {fileID: 274021659} + m_Layer: 5 + m_Name: SignInButtonText + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &274021658 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 274021657} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 109000920} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!114 &274021659 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 274021657} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 48 + m_FontStyle: 0 + m_BestFit: 0 + m_MinSize: 0 + m_MaxSize: 48 + m_Alignment: 4 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 1 + m_LineSpacing: 1 + m_Text: "\U0001F34E\U0001F34E\U0001F34ESign in with Apple" +--- !u!222 &274021660 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 274021657} + m_CullTransparentMesh: 1 +--- !u!1 &305255338 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 305255339} + - component: {fileID: 305255342} + - component: {fileID: 305255341} + - component: {fileID: 305255340} + m_Layer: 5 + m_Name: Scrollbar Horizontal + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &305255339 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 305255338} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: + - {fileID: 1935460640} + m_Father: {fileID: 26397529} + m_RootOrder: 1 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 0, y: 0} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 20} + m_Pivot: {x: 0, y: 0} +--- !u!114 &305255340 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 305255338} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 2a4db7a114972834c8e4117be1d82ba3, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Navigation: + m_Mode: 3 + m_WrapAround: 0 + m_SelectOnUp: {fileID: 0} + m_SelectOnDown: {fileID: 0} + m_SelectOnLeft: {fileID: 0} + m_SelectOnRight: {fileID: 0} + m_Transition: 1 + m_Colors: + m_NormalColor: {r: 1, g: 1, b: 1, a: 1} + m_HighlightedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1} + m_PressedColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 1} + m_SelectedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1} + m_DisabledColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 0.5019608} + m_ColorMultiplier: 1 + m_FadeDuration: 0.1 + m_SpriteState: + m_HighlightedSprite: {fileID: 0} + m_PressedSprite: {fileID: 0} + m_SelectedSprite: {fileID: 0} + m_DisabledSprite: {fileID: 0} + m_AnimationTriggers: + m_NormalTrigger: Normal + m_HighlightedTrigger: Highlighted + m_PressedTrigger: Pressed + m_SelectedTrigger: Selected + m_DisabledTrigger: Disabled + m_Interactable: 1 + m_TargetGraphic: {fileID: 1371383498} + m_HandleRect: {fileID: 1371383497} + m_Direction: 0 + m_Value: 0 + m_Size: 1 + m_NumberOfSteps: 0 + m_OnValueChanged: + m_PersistentCalls: + m_Calls: [] +--- !u!114 &305255341 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 305255338} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 10907, guid: 0000000000000000f000000000000000, type: 0} + m_Type: 1 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!222 &305255342 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 305255338} + m_CullTransparentMesh: 1 +--- !u!1 &421225177 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 421225180} + - component: {fileID: 421225179} + - component: {fileID: 421225178} + m_Layer: 0 + m_Name: Main Camera + m_TagString: MainCamera + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!81 &421225178 +AudioListener: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 421225177} + m_Enabled: 1 +--- !u!20 &421225179 +Camera: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 421225177} + m_Enabled: 1 + serializedVersion: 2 + m_ClearFlags: 1 + m_BackGroundColor: {r: 0.19215687, g: 0.3019608, b: 0.4745098, a: 0} + m_projectionMatrixMode: 1 + m_GateFitMode: 2 + m_FOVAxisMode: 0 + m_SensorSize: {x: 36, y: 24} + m_LensShift: {x: 0, y: 0} + m_FocalLength: 50 + m_NormalizedViewPortRect: + serializedVersion: 2 + x: 0 + y: 0 + width: 1 + height: 1 + near clip plane: 0.3 + far clip plane: 1000 + field of view: 60 + orthographic: 1 + orthographic size: 5 + m_Depth: -1 + m_CullingMask: + serializedVersion: 2 + m_Bits: 4294967295 + m_RenderingPath: -1 + m_TargetTexture: {fileID: 0} + m_TargetDisplay: 0 + m_TargetEye: 3 + m_HDR: 1 + m_AllowMSAA: 1 + m_AllowDynamicResolution: 0 + m_ForceIntoRT: 0 + m_OcclusionCulling: 1 + m_StereoConvergence: 10 + m_StereoSeparation: 0.022 +--- !u!4 &421225180 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 421225177} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: -10} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_RootOrder: 1 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!1 &456567132 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 456567133} + - component: {fileID: 456567135} + - component: {fileID: 456567134} + m_Layer: 5 + m_Name: BackButtonText + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &456567133 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 456567132} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 1454467219} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!114 &456567134 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 456567132} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 0.19607843, g: 0.19607843, b: 0.19607843, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 24 + m_FontStyle: 0 + m_BestFit: 0 + m_MinSize: 2 + m_MaxSize: 40 + m_Alignment: 4 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: Back +--- !u!222 &456567135 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 456567132} + m_CullTransparentMesh: 1 +--- !u!1 &488826837 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 488826838} + - component: {fileID: 488826841} + - component: {fileID: 488826840} + - component: {fileID: 488826839} + m_Layer: 5 + m_Name: Viewport + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &488826838 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 488826837} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: + - {fileID: 136561664} + m_Father: {fileID: 26397529} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 0, y: 0} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0, y: 1} +--- !u!114 &488826839 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 488826837} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 31a19414c41e5ae4aae2af33fee712f6, type: 3} + m_Name: + m_EditorClassIdentifier: + m_ShowMaskGraphic: 0 +--- !u!114 &488826840 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 488826837} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 10917, guid: 0000000000000000f000000000000000, type: 0} + m_Type: 1 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!222 &488826841 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 488826837} + m_CullTransparentMesh: 1 +--- !u!1 &873773139 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 873773140} + - component: {fileID: 873773141} + - component: {fileID: 873773142} + m_Layer: 5 + m_Name: LogText + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &873773140 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 873773139} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 136561664} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 5, y: -5} + m_SizeDelta: {x: -10, y: -10} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &873773141 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 873773139} + m_CullTransparentMesh: 1 +--- !u!114 &873773142 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 873773139} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 0.19607843, g: 0.19607843, b: 0.19607843, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 36 + m_FontStyle: 0 + m_BestFit: 0 + m_MinSize: 0 + m_MaxSize: 48 + m_Alignment: 0 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: New Text +--- !u!1 &885700822 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 885700823} + m_Layer: 5 + m_Name: Sliding Area + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &885700823 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 885700822} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: + - {fileID: 183834311} + m_Father: {fileID: 211112646} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: -20, y: -20} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!1 &997317349 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 997317351} + - component: {fileID: 997317350} + m_Layer: 0 + m_Name: PassportManager + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!114 &997317350 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 997317349} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 91b840080cd8945349b79d86957b9d0b, type: 3} + m_Name: + m_EditorClassIdentifier: + TopPadding: {fileID: 0} + Output: {fileID: 0} +--- !u!4 &997317351 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 997317349} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_RootOrder: 4 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!1 &1371383496 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1371383497} + - component: {fileID: 1371383499} + - component: {fileID: 1371383498} + m_Layer: 5 + m_Name: Handle + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &1371383497 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1371383496} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 1935460640} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 0, y: 0} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 20, y: 20} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!114 &1371383498 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1371383496} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 10905, guid: 0000000000000000f000000000000000, type: 0} + m_Type: 1 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!222 &1371383499 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1371383496} + m_CullTransparentMesh: 1 +--- !u!1 &1454467215 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1454467219} + - component: {fileID: 1454467218} + - component: {fileID: 1454467217} + - component: {fileID: 1454467216} + m_Layer: 5 + m_Name: BackButton + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!114 &1454467216 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1454467215} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 4e29b1a8efbd4b44bb3f3716e73f07ff, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Navigation: + m_Mode: 3 + m_WrapAround: 0 + m_SelectOnUp: {fileID: 0} + m_SelectOnDown: {fileID: 0} + m_SelectOnLeft: {fileID: 0} + m_SelectOnRight: {fileID: 0} + m_Transition: 1 + m_Colors: + m_NormalColor: {r: 1, g: 1, b: 1, a: 1} + m_HighlightedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1} + m_PressedColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 1} + m_SelectedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1} + m_DisabledColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 0.5019608} + m_ColorMultiplier: 1 + m_FadeDuration: 0.1 + m_SpriteState: + m_HighlightedSprite: {fileID: 0} + m_PressedSprite: {fileID: 0} + m_SelectedSprite: {fileID: 0} + m_DisabledSprite: {fileID: 0} + m_AnimationTriggers: + m_NormalTrigger: Normal + m_HighlightedTrigger: Highlighted + m_PressedTrigger: Pressed + m_SelectedTrigger: Selected + m_DisabledTrigger: Disabled + m_Interactable: 1 + m_TargetGraphic: {fileID: 1454467217} + m_OnClick: + m_PersistentCalls: + m_Calls: [] +--- !u!114 &1454467217 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1454467215} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 10905, guid: 0000000000000000f000000000000000, type: 0} + m_Type: 1 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!222 &1454467218 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1454467215} + m_CullTransparentMesh: 1 +--- !u!224 &1454467219 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1454467215} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: + - {fileID: 456567133} + m_Father: {fileID: 1949690038} + m_RootOrder: 3 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 1} + m_AnchorMax: {x: 0, y: 1} + m_AnchoredPosition: {x: 95, y: -225} + m_SizeDelta: {x: 150, y: 50} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!1 &1627063169 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1627063171} + - component: {fileID: 1627063170} + m_Layer: 0 + m_Name: AppleSingInTestManager + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!114 &1627063170 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1627063169} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 9a8b7c6d5e4f3a2b1c0d9e8f7a6b5c4d, type: 3} + m_Name: + m_EditorClassIdentifier: + appleSignInButton: {fileID: 109000923} + statusText: {fileID: 208235397} + logText: {fileID: 873773142} + backButton: {fileID: 1454467216} +--- !u!4 &1627063171 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1627063169} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 693.61993, y: 132.68591, z: -21.172306} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_RootOrder: 3 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!1 &1935460639 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1935460640} + m_Layer: 5 + m_Name: Sliding Area + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &1935460640 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1935460639} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: + - {fileID: 1371383497} + m_Father: {fileID: 305255339} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: -20, y: -20} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!1 &1949690034 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1949690038} + - component: {fileID: 1949690037} + - component: {fileID: 1949690036} + - component: {fileID: 1949690035} + m_Layer: 5 + m_Name: Canvas + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!114 &1949690035 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1949690034} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: dc42784cf147c0c48a680349fa168899, type: 3} + m_Name: + m_EditorClassIdentifier: + m_IgnoreReversedGraphics: 1 + m_BlockingObjects: 0 + m_BlockingMask: + serializedVersion: 2 + m_Bits: 4294967295 +--- !u!114 &1949690036 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1949690034} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 0cd44c1031e13a943bb63640046fad76, type: 3} + m_Name: + m_EditorClassIdentifier: + m_UiScaleMode: 0 + m_ReferencePixelsPerUnit: 100 + m_ScaleFactor: 1 + m_ReferenceResolution: {x: 800, y: 600} + m_ScreenMatchMode: 0 + m_MatchWidthOrHeight: 0 + m_PhysicalUnit: 3 + m_FallbackScreenDPI: 96 + m_DefaultSpriteDPI: 96 + m_DynamicPixelsPerUnit: 1 + m_PresetInfoIsWorld: 0 +--- !u!223 &1949690037 +Canvas: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1949690034} + m_Enabled: 1 + serializedVersion: 3 + m_RenderMode: 0 + m_Camera: {fileID: 0} + m_PlaneDistance: 100 + m_PixelPerfect: 0 + m_ReceivesEvents: 1 + m_OverrideSorting: 0 + m_OverridePixelPerfect: 0 + m_SortingBucketNormalizedSize: 0 + m_AdditionalShaderChannelsFlag: 0 + m_SortingLayerID: 0 + m_SortingOrder: 0 + m_TargetDisplay: 0 +--- !u!224 &1949690038 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1949690034} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 0, y: 0, z: 0} + m_ConstrainProportionsScale: 0 + m_Children: + - {fileID: 208235395} + - {fileID: 109000920} + - {fileID: 26397529} + - {fileID: 1454467219} + m_Father: {fileID: 0} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 0, y: 0} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0, y: 0} +--- !u!1 &2048912023 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 2048912026} + - component: {fileID: 2048912025} + - component: {fileID: 2048912024} + m_Layer: 0 + m_Name: EventSystem + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!114 &2048912024 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2048912023} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 4f231c4fb786f3946a6b90b886c48677, type: 3} + m_Name: + m_EditorClassIdentifier: + m_SendPointerHoverToParent: 1 + m_HorizontalAxis: Horizontal + m_VerticalAxis: Vertical + m_SubmitButton: Submit + m_CancelButton: Cancel + m_InputActionsPerSecond: 10 + m_RepeatDelay: 0.5 + m_ForceModuleActive: 0 +--- !u!114 &2048912025 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2048912023} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 76c392e42b5098c458856cdf6ecaaaa1, type: 3} + m_Name: + m_EditorClassIdentifier: + m_FirstSelected: {fileID: 0} + m_sendNavigationEvents: 1 + m_DragThreshold: 10 +--- !u!4 &2048912026 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2048912023} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_RootOrder: 2 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} diff --git a/sample/Assets/Scenes/Passport/AppleSignInTestScene.unity.meta b/sample/Assets/Scenes/Passport/AppleSignInTestScene.unity.meta new file mode 100644 index 000000000..9af75f09f --- /dev/null +++ b/sample/Assets/Scenes/Passport/AppleSignInTestScene.unity.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: a5fffc41f93eef24393ff475b5c166ac +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/sample/Assets/Scripts/Passport/AppleSignInTest.meta b/sample/Assets/Scripts/Passport/AppleSignInTest.meta new file mode 100644 index 000000000..38639b19e --- /dev/null +++ b/sample/Assets/Scripts/Passport/AppleSignInTest.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 6785e3266d93845daae63a97616affe5 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/sample/Assets/Scripts/Passport/AppleSignInTest/AppleSignInTestScript.cs b/sample/Assets/Scripts/Passport/AppleSignInTest/AppleSignInTestScript.cs new file mode 100644 index 000000000..54d8d9bc5 --- /dev/null +++ b/sample/Assets/Scripts/Passport/AppleSignInTest/AppleSignInTestScript.cs @@ -0,0 +1,231 @@ +using System; +using UnityEngine; +using UnityEngine.UI; +using UnityEngine.SceneManagement; +using Immutable.Passport; +using Immutable.Passport.Model; + +/// +/// Test script for Apple Sign-in with Passport +/// Tests the full LoginWithApple() flow on iOS +/// +public class AppleSignInTestScript : MonoBehaviour +{ +#pragma warning disable CS8618 + [Header("UI Elements")] + [SerializeField] private Button appleSignInButton; + [SerializeField] private Text statusText; + [SerializeField] private Text logText; + [SerializeField] private Button backButton; + + private Passport passport; + private string logOutput = ""; +#pragma warning restore CS8618 + + void Start() + { + Log("Apple Sign-in Test Scene started"); + + // Get Passport instance + if (Passport.Instance != null) + { + passport = Passport.Instance; + Log("SUCCESS: Passport instance found"); + } + else + { + Log("ERROR: Passport instance is null - Passport must be initialized before using Apple Sign In"); + UpdateStatus("ERROR: Passport not initialized"); + } + + // Set up button listener + if (appleSignInButton != null) + { + appleSignInButton.onClick.AddListener(OnAppleSignInClicked); + Log("SUCCESS: Apple Sign In button listener added"); + } + else + { + Log("ERROR: Apple Sign In button not assigned"); + } + + if (backButton != null) + { + backButton.onClick.AddListener(OnBackClicked); + } + + // Platform detection info + Log($"Platform: {Application.platform}"); + Log($"iOS Device: {Application.platform == RuntimePlatform.IPhonePlayer}"); + Log($"Editor: {Application.isEditor}"); + +#if UNITY_IOS && !UNITY_EDITOR + Log("SUCCESS: Running on iOS device - Apple Sign In available"); + UpdateStatus("Ready to test Apple Sign In"); +#else + Log("WARNING: Not on iOS device - Apple Sign In only available on iOS"); + UpdateStatus("iOS device required for Apple Sign In"); +#endif + } + + private async void OnAppleSignInClicked() + { + Log("Apple Sign In button clicked!"); + UpdateStatus("Starting Apple Sign In..."); + +#if UNITY_IOS && !UNITY_EDITOR + // iOS Device: Use Passport.LoginWithApple() + if (passport == null) + { + Log("ERROR: Passport is null, cannot login"); + UpdateStatus("ERROR: Passport not initialized"); + return; + } + + try + { + Log("Starting Apple Sign-in with Passport..."); + Log("Backend URL determined by Passport environment configuration"); + + bool success = await passport.LoginWithApple(); + + if (success) + { + Log("SUCCESS: Apple Sign-in completed successfully!"); + Log("SUCCESS: User is now authenticated with Passport!"); + UpdateStatus("Login Successful!"); + + // Get user details + try + { + var address = await passport.GetAddress(); + var email = await passport.GetEmail(); + Log($"Email: {email}"); + Log($"Wallet Address: {address}"); + } + catch (Exception ex) + { + Log($"WARNING: Could not fetch user details: {ex.Message}"); + } + + // Wait a moment then navigate to authenticated scene + await System.Threading.Tasks.Task.Delay(3000); + SceneManager.LoadScene("AuthenticatedScene"); + } + else + { + Log("ERROR: Apple Sign-in failed"); + UpdateStatus("Login Failed"); + } + } + catch (OperationCanceledException) + { + Log("WARNING: User cancelled Apple Sign-in"); + UpdateStatus("Login Cancelled"); + } + catch (PassportException ex) + { + Log($"ERROR: Passport exception: {ex.Message}"); + Log($" Error Type: {ex.Type}"); + UpdateStatus($"ERROR: {ex.Message}"); + } + catch (Exception ex) + { + Log($"ERROR: Unexpected exception: {ex.Message}"); + Log($" Stack trace: {ex.StackTrace}"); + UpdateStatus($"ERROR: {ex.Message}"); + } +#else + // Not on iOS device - Fall back to regular Passport.Login() + Log("WARNING: Not on iOS device - using Passport.Login() fallback"); + + if (passport == null) + { + Log("ERROR: Passport is null, cannot login"); + UpdateStatus("ERROR: Passport not initialized"); + return; + } + + try + { + Log("Creating DirectLoginOptions for Apple..."); + var directLoginOptions = new DirectLoginOptions(DirectLoginMethod.Apple); + + Log("Calling Passport.Login()..."); + bool success = await passport.Login(useCachedSession: false, directLoginOptions: directLoginOptions); + + if (success) + { + Log("SUCCESS: Login successful!"); + UpdateStatus("Login Successful!"); + + // Wait a moment then load authenticated scene + await System.Threading.Tasks.Task.Delay(2000); + SceneManager.LoadScene("AuthenticatedScene"); + } + else + { + Log("ERROR: Login failed"); + UpdateStatus("Login Failed"); + } + } + catch (OperationCanceledException ex) + { + Log($"WARNING: Login cancelled: {ex.Message}"); + UpdateStatus("Login Cancelled"); + } + catch (Exception ex) + { + Log($"ERROR: Login exception: {ex.Message}"); + UpdateStatus($"ERROR: {ex.Message}"); + } +#endif + } + + private void OnBackClicked() + { + Log("Back button clicked, returning to UnauthenticatedScene"); + SceneManager.LoadScene("UnauthenticatedScene"); + } + + private void UpdateStatus(string message) + { + if (statusText != null) + { + statusText.text = message; + } + Log($"[STATUS] {message}"); + } + + private void Log(string message) + { + string timestamp = DateTime.Now.ToString("HH:mm:ss"); + string logMessage = $"[{timestamp}] {message}"; + + Debug.Log($"[AppleSignInTest] {message}"); + + logOutput += logMessage + "\n"; + + if (logText != null) + { + logText.text = logOutput; + + // Auto-scroll to bottom (if scrollview exists) + Canvas.ForceUpdateCanvases(); + } + } + + void OnDestroy() + { + // Clean up button listeners + if (appleSignInButton != null) + { + appleSignInButton.onClick.RemoveListener(OnAppleSignInClicked); + } + if (backButton != null) + { + backButton.onClick.RemoveListener(OnBackClicked); + } + } +} + diff --git a/sample/Assets/Scripts/Passport/AppleSignInTest/AppleSignInTestScript.cs.meta b/sample/Assets/Scripts/Passport/AppleSignInTest/AppleSignInTestScript.cs.meta new file mode 100644 index 000000000..155f5222f --- /dev/null +++ b/sample/Assets/Scripts/Passport/AppleSignInTest/AppleSignInTestScript.cs.meta @@ -0,0 +1,12 @@ +fileFormatVersion: 2 +guid: 9a8b7c6d5e4f3a2b1c0d9e8f7a6b5c4d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: + diff --git a/sample/Assets/Scripts/Passport/AppleSignInTest/README.md b/sample/Assets/Scripts/Passport/AppleSignInTest/README.md new file mode 100644 index 000000000..cbeb43674 --- /dev/null +++ b/sample/Assets/Scripts/Passport/AppleSignInTest/README.md @@ -0,0 +1,173 @@ +# Apple Sign In Test Scene + +This folder contains a dedicated test script for Apple Sign In functionality. + +## Files +- `AppleSignInTestScript.cs` - Test script for Apple Sign In button + +## How to Create the Test Scene + +Since Unity scenes can't be created via scripts, follow these steps: + +### 1. Create New Scene +1. Open Unity +2. File → New Scene +3. Choose "2D" or "UI" template +4. Save as `AppleSignInTestScene` in `Assets/Scenes/Passport/` + +### 2. Set Up UI +1. Right-click in Hierarchy → UI → Canvas (if not already present) +2. Add components: + - **Canvas** (should exist) + - **EventSystem** (should exist) + +### 3. Create UI Elements + +#### Status Text (Top) +1. Right-click Canvas → UI → Text - TextMeshPro (or Legacy Text) +2. Name: "StatusText" +3. Position: Top center +4. Text: "Apple Sign In Test" +5. Font Size: 36 +6. Alignment: Center +7. RectTransform: + - Anchor: Top-stretch + - Pos Y: -50 + - Height: 80 + +#### Apple Sign In Button +1. Right-click Canvas → UI → Button - TextMeshPro (or Legacy Button) +2. Name: "AppleSignInButton" +3. Position: Center +4. Size: 400 x 80 +5. Child Text: "🍎 Sign in with Apple" +6. Font Size: 28 +7. Colors: + - Normal: Black (#000000) + - Text: White (#FFFFFF) + +#### Log Text (Scrollable) +1. Right-click Canvas → UI → Scroll View +2. Name: "LogScrollView" +3. Position: Bottom half of screen +4. Content → Child: Text (rename to "LogText") +5. LogText settings: + - Alignment: Top-Left + - Font Size: 14 + - Text: "" (empty) + - Enable Rich Text: Yes + +#### Back Button +1. Right-click Canvas → UI → Button +2. Name: "BackButton" +3. Position: Bottom-left corner +4. Size: 150 x 50 +5. Text: "← Back" + +### 4. Add Script +1. Create empty GameObject in scene: "AppleSignInTestManager" +2. Add Component → AppleSignInTestScript +3. Assign references in Inspector: + - Apple Sign In Button: → AppleSignInButton + - Status Text: → StatusText + - Log Text: → LogText + - Back Button: → BackButton + +### 5. Test the Scene +1. File → Build Settings → Add Open Scenes (add AppleSignInTestScene) +2. Save scene +3. Build for iOS +4. Run in simulator +5. Test the button! + +## What the Test Script Does + +### On Start: +- ✅ Logs platform information +- ✅ Checks for Passport instance +- ✅ Sets up button listeners +- ✅ Shows UNITY_IOS flag status + +### When Button Clicked: +- ✅ Logs detailed progress +- ✅ Creates DirectLoginOptions with DirectLoginMethod.Apple +- ✅ Calls Passport.Login() +- ✅ Shows success/failure +- ✅ Navigates to AuthenticatedScene on success + +### Log Output: +All actions are logged to: +- Console (Unity Debug.Log) +- On-screen log text (scrollable) +- With timestamps + +## Expected Behavior + +### In iOS Simulator (without native plugin yet): +``` +[00:00:01] AppleSignInTestScript started +[00:00:01] ✅ Passport instance found +[00:00:01] ✅ Apple Sign In button listener added +[00:00:01] Platform: IPhonePlayer +[00:00:01] iOS: True +[00:00:01] Editor: False +[00:00:01] ✅ UNITY_IOS flag is defined +[STATUS] Ready to test Apple Sign In + +[User clicks button] + +[00:00:05] 🍎 Apple Sign In button clicked! +[00:00:05] Creating DirectLoginOptions for Apple... +[00:00:05] DirectLoginMethod: Apple +[00:00:05] Calling Passport.Login()... +[00:00:05] [PassportImpl] Login with DirectLoginMethod: Apple +[00:00:05] [PassportUI] Loading WebView... +... +[00:00:10] ❌ Login failed (expected - no native implementation yet) +[STATUS] Login Failed +``` + +### After Native Plugin is Implemented: +``` +[00:00:01] AppleSignInTestScript started +[00:00:01] ✅ Passport instance found +[00:00:01] ✅ Apple Sign In button listener added +[00:00:01] ✅ UNITY_IOS flag is defined +[STATUS] Ready to test Apple Sign In + +[User clicks button] + +[00:00:05] 🍎 Apple Sign In button clicked! +[00:00:05] Creating DirectLoginOptions for Apple... +[00:00:05] DirectLoginMethod: Apple +[00:00:05] Calling Passport.Login()... +[00:00:05] [AppleSignInAuth] Starting Apple Sign In... +[00:00:06] [iOS Native] Presenting ASAuthorizationController +[00:00:08] [User authenticates with Face ID] +[00:00:10] [AppleSignInAuth] Success! Identity token received +[00:00:11] ✅ Login successful! +[STATUS] Login Successful! +[00:00:13] [Loading AuthenticatedScene] +``` + +## Files to Create + +You need to create in Unity Editor: +1. Scene: `Assets/Scenes/Passport/AppleSignInTestScene.unity` +2. Canvas with UI elements (follow steps above) +3. AppleSignInTestManager GameObject with script attached + +## Quick Setup (Copy-Paste Ready) + +If you want to skip manual UI creation, you can: + +1. Duplicate `UnauthenticatedScene.unity` +2. Rename to `AppleSignInTestScene.unity` +3. Delete all UI except Canvas and EventSystem +4. Follow "Create UI Elements" steps above +5. Add AppleSignInTestScript + +--- + +**Status**: Script ready, scene needs to be created in Unity Editor + diff --git a/sample/Assets/Scripts/Passport/AppleSignInTest/README.md.meta b/sample/Assets/Scripts/Passport/AppleSignInTest/README.md.meta new file mode 100644 index 000000000..d357a38b5 --- /dev/null +++ b/sample/Assets/Scripts/Passport/AppleSignInTest/README.md.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: e888a08412d89824da66351153773d61 +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/sample/Assets/Scripts/Passport/PassportInitialisation/PassportInitialisationScript.cs b/sample/Assets/Scripts/Passport/PassportInitialisation/PassportInitialisationScript.cs index fe9be7ba6..9570771fe 100644 --- a/sample/Assets/Scripts/Passport/PassportInitialisation/PassportInitialisationScript.cs +++ b/sample/Assets/Scripts/Passport/PassportInitialisation/PassportInitialisationScript.cs @@ -49,14 +49,15 @@ private async void InitialisePassport() Passport.RedactTokensInLogs = false; // Initialise Passport - const string environment = Immutable.Passport.Model.Environment.SANDBOX; - const string clientId = "mp6rxfMDwwZDogcdgNrAaHnG0qMlXuMK"; - // const string clientId = "IllW5pJ54DShXtaSXzaAlghm40uQjptd"; + // Use "local" for testing against local BFF backend + const string environment = "local"; + const string clientId = "2Dx7GLUZeFsMnmp1kvOXJ2SYaWGhEpnF"; + var passport = await Passport.Init(clientId, environment, redirectUri, logoutRedirectUri); SampleAppManager.PassportInstance = passport; // Navigate to the unauthenticated scene after initialising Passport - SceneManager.LoadScene("UnauthenticatedScene"); + //SceneManager.LoadScene("UnauthenticatedScene"); } catch (Exception ex) { diff --git a/sample/ProjectSettings/EditorBuildSettings.asset b/sample/ProjectSettings/EditorBuildSettings.asset index ea6e3a667..528034c9e 100644 --- a/sample/ProjectSettings/EditorBuildSettings.asset +++ b/sample/ProjectSettings/EditorBuildSettings.asset @@ -5,15 +5,18 @@ EditorBuildSettings: m_ObjectHideFlags: 0 serializedVersion: 2 m_Scenes: - - enabled: 1 + - enabled: 0 path: Assets/Scenes/Passport/Initialisation.unity guid: bb0668e0c95b745ce8e2f127d5940ede - enabled: 0 path: Assets/Scenes/Passport/InitialisationWithUI.unity guid: b588e10e8f614e0458aee90c7d02a499 - enabled: 1 + path: Assets/Scenes/Passport/AppleSignInTestScene.unity + guid: a5fffc41f93eef24393ff475b5c166ac + - enabled: 0 path: Assets/Scenes/Passport/UnauthenticatedScene.unity - guid: 2cda990e2423bbf4892e6590ba056729 + guid: ac550d22a7a5f4909be272e42b0c7b46 - enabled: 1 path: Assets/Scenes/Passport/AuthenticatedScene.unity guid: 48b17d6cb0b0f409a9edf831addcbc0a @@ -26,13 +29,13 @@ EditorBuildSettings: - enabled: 1 path: Assets/Scenes/Passport/ZkEvm/ZkEvmGetTransactionReceipt.unity guid: ee999224a19ee442d998a452e74dab8c - - enabled: 1 + - enabled: 0 path: Assets/Scenes/Passport/Imx/ImxNftTransfer.unity guid: 2f14d9e7f1e6941d3bc021f86377a3c9 - - enabled: 1 + - enabled: 0 path: Assets/Scenes/Passport/ZkEvm/ZkEvmSignTypedData.unity guid: 7947e157cd8d541138343d5eba099466 - - enabled: 1 + - enabled: 0 path: Assets/Scenes/Passport/Other/SetCallTimeout.unity guid: 73ba07ed56efd1949b722042d50dc444 - enabled: 0 diff --git a/sample/ProjectSettings/ProjectSettings.asset b/sample/ProjectSettings/ProjectSettings.asset index 5d4bf39da..899fa17cb 100644 --- a/sample/ProjectSettings/ProjectSettings.asset +++ b/sample/ProjectSettings/ProjectSettings.asset @@ -179,7 +179,7 @@ PlayerSettings: keepLoadedShadersAlive: 0 StripUnusedMeshComponents: 0 VertexChannelCompressionMask: 4054 - iPhoneSdkVersion: 988 + iPhoneSdkVersion: 989 iOSTargetOSVersionString: 13.0 tvOSSdkVersion: 0 tvOSRequireExtendedGameController: 0 @@ -805,7 +805,7 @@ PlayerSettings: allowUnsafeCode: 0 useDeterministicCompilation: 1 enableRoslynAnalyzers: 1 - selectedPlatform: 0 + selectedPlatform: 3 additionalIl2CppArgs: scriptingRuntimeVersion: 1 gcIncremental: 1 diff --git a/src/Packages/Passport/Editor/AppleSignInPostProcessor.cs b/src/Packages/Passport/Editor/AppleSignInPostProcessor.cs new file mode 100644 index 000000000..3e56294f9 --- /dev/null +++ b/src/Packages/Passport/Editor/AppleSignInPostProcessor.cs @@ -0,0 +1,103 @@ +using UnityEditor; +using UnityEditor.Callbacks; +using UnityEditor.iOS.Xcode; +using System.IO; + +namespace Immutable.Passport.Editor +{ + /// + /// Automatically adds "Sign in with Apple" capability to Xcode project after Unity build + /// This prevents having to manually add it in Xcode every time + /// + public class AppleSignInPostProcessor + { + [PostProcessBuild(1)] + public static void OnPostProcessBuild(BuildTarget buildTarget, string pathToBuiltProject) + { + if (buildTarget != BuildTarget.iOS) + { + return; + } + + UnityEngine.Debug.Log("[AppleSignInPostProcessor] Adding 'Sign in with Apple' capability to Xcode project..."); + + // Paths + string projectPath = pathToBuiltProject + "/Unity-iPhone.xcodeproj/project.pbxproj"; + string entitlementsPath = pathToBuiltProject + "/Unity-iPhone/Unity-iPhone.entitlements"; + + // Load Xcode project + PBXProject project = new PBXProject(); + project.ReadFromFile(projectPath); + + // Get main target GUID +#if UNITY_2019_3_OR_NEWER + string targetGuid = project.GetUnityMainTargetGuid(); + string unityFrameworkTargetGuid = project.GetUnityFrameworkTargetGuid(); +#else + string targetGuid = project.TargetGuidByName(PBXProject.GetUnityTargetName()); + string unityFrameworkTargetGuid = targetGuid; +#endif + + // Add AuthenticationServices framework (required for Sign in with Apple) + project.AddFrameworkToProject(unityFrameworkTargetGuid, "AuthenticationServices.framework", false); + UnityEngine.Debug.Log("[AppleSignInPostProcessor] ✅ Added AuthenticationServices.framework"); + + // Create or update entitlements file + CreateOrUpdateEntitlements(entitlementsPath); + + // Add entitlements file to project + string entitlementsRelativePath = "Unity-iPhone/Unity-iPhone.entitlements"; + project.AddFile(entitlementsRelativePath, "Unity-iPhone.entitlements"); + project.AddBuildProperty(targetGuid, "CODE_SIGN_ENTITLEMENTS", entitlementsRelativePath); + + UnityEngine.Debug.Log("[AppleSignInPostProcessor] ✅ Added entitlements file to project"); + + // Save the modified project + project.WriteToFile(projectPath); + + UnityEngine.Debug.Log("[AppleSignInPostProcessor] ✅ 'Sign in with Apple' capability added successfully!"); + UnityEngine.Debug.Log("[AppleSignInPostProcessor] ⚠️ IMPORTANT: Make sure your Bundle ID has 'Sign in with Apple' enabled in Apple Developer Portal!"); + } + + /// + /// Creates or updates the entitlements file with Sign in with Apple capability + /// + private static void CreateOrUpdateEntitlements(string entitlementsPath) + { + PlistDocument entitlements = new PlistDocument(); + + // If entitlements file already exists, read it + if (File.Exists(entitlementsPath)) + { + entitlements.ReadFromFile(entitlementsPath); + UnityEngine.Debug.Log("[AppleSignInPostProcessor] Found existing entitlements file"); + } + else + { + UnityEngine.Debug.Log("[AppleSignInPostProcessor] Creating new entitlements file"); + } + + // Get or create root dictionary + PlistElementDict rootDict = entitlements.root; + + // Add "Sign in with Apple" capability + // Key: com.apple.developer.applesignin + // Value: array with "Default" + if (!rootDict.values.ContainsKey("com.apple.developer.applesignin")) + { + PlistElementArray signInWithApple = rootDict.CreateArray("com.apple.developer.applesignin"); + signInWithApple.AddString("Default"); + UnityEngine.Debug.Log("[AppleSignInPostProcessor] ✅ Added 'com.apple.developer.applesignin' entitlement"); + } + else + { + UnityEngine.Debug.Log("[AppleSignInPostProcessor] 'com.apple.developer.applesignin' entitlement already exists"); + } + + // Write the entitlements file + entitlements.WriteToFile(entitlementsPath); + UnityEngine.Debug.Log($"[AppleSignInPostProcessor] ✅ Entitlements file saved: {entitlementsPath}"); + } + } +} + diff --git a/src/Packages/Passport/Editor/AppleSignInPostProcessor.cs.meta b/src/Packages/Passport/Editor/AppleSignInPostProcessor.cs.meta new file mode 100644 index 000000000..e4dd725f3 --- /dev/null +++ b/src/Packages/Passport/Editor/AppleSignInPostProcessor.cs.meta @@ -0,0 +1,12 @@ +fileFormatVersion: 2 +guid: 7e3a9d5f2b4c6e8a9f0d1c3b5a7e9f1d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: + diff --git a/src/Packages/Passport/Runtime/Plugins.meta b/src/Packages/Passport/Runtime/Plugins.meta new file mode 100644 index 000000000..4b305521d --- /dev/null +++ b/src/Packages/Passport/Runtime/Plugins.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 17f29862eecb06441a5272a62d2f1283 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/src/Packages/Passport/Runtime/Plugins/iOS.meta b/src/Packages/Passport/Runtime/Plugins/iOS.meta new file mode 100644 index 000000000..12312586d --- /dev/null +++ b/src/Packages/Passport/Runtime/Plugins/iOS.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 16b61053a91ec8145afa5b82a7556488 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/src/Packages/Passport/Runtime/Plugins/iOS/AppleSignIn.h b/src/Packages/Passport/Runtime/Plugins/iOS/AppleSignIn.h new file mode 100644 index 000000000..8ab7132e4 --- /dev/null +++ b/src/Packages/Passport/Runtime/Plugins/iOS/AppleSignIn.h @@ -0,0 +1,31 @@ +#ifndef AppleSignIn_h +#define AppleSignIn_h + +#ifdef __cplusplus +extern "C" { +#endif + +// Initialize Apple Sign In (called once) +void AppleSignIn_Init(); + +// Check if Apple Sign In is available on this device/OS +bool AppleSignIn_IsAvailable(); + +// Start the Apple Sign In flow +void AppleSignIn_Start(); + +// Callbacks (set from Unity C#) +typedef void (*AppleSignIn_OnSuccess)(const char* identityToken, const char* authorizationCode, const char* userID, const char* email, const char* fullName); +typedef void (*AppleSignIn_OnError)(const char* errorCode, const char* errorMessage); +typedef void (*AppleSignIn_OnCancel)(); + +void AppleSignIn_SetOnSuccessCallback(AppleSignIn_OnSuccess callback); +void AppleSignIn_SetOnErrorCallback(AppleSignIn_OnError callback); +void AppleSignIn_SetOnCancelCallback(AppleSignIn_OnCancel callback); + +#ifdef __cplusplus +} +#endif + +#endif /* AppleSignIn_h */ + diff --git a/src/Packages/Passport/Runtime/Plugins/iOS/AppleSignIn.h.meta b/src/Packages/Passport/Runtime/Plugins/iOS/AppleSignIn.h.meta new file mode 100644 index 000000000..c2efe87ec --- /dev/null +++ b/src/Packages/Passport/Runtime/Plugins/iOS/AppleSignIn.h.meta @@ -0,0 +1,37 @@ +fileFormatVersion: 2 +guid: 3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f +PluginImporter: + externalObjects: {} + serializedVersion: 2 + iconMap: {} + executionOrder: {} + defineConstraints: [] + isPreloaded: 0 + isOverridable: 0 + isExplicitlyReferenced: 0 + validateReferences: 1 + platformData: + - first: + Any: + second: + enabled: 0 + settings: {} + - first: + Editor: Editor + second: + enabled: 0 + settings: + DefaultValueInitialized: true + - first: + iPhone: iOS + second: + enabled: 1 + settings: + AddToEmbeddedBinaries: false + CPU: AnyCPU + CompileFlags: + FrameworkDependencies: AuthenticationServices;UIKit;Foundation + userData: + assetBundleName: + assetBundleVariant: + diff --git a/src/Packages/Passport/Runtime/Plugins/iOS/AppleSignIn.mm b/src/Packages/Passport/Runtime/Plugins/iOS/AppleSignIn.mm new file mode 100644 index 000000000..68b2153ae --- /dev/null +++ b/src/Packages/Passport/Runtime/Plugins/iOS/AppleSignIn.mm @@ -0,0 +1,267 @@ +#import +#import +#import +#import "AppleSignIn.h" + +// Callbacks from Unity +static AppleSignIn_OnSuccess _onSuccessCallback = NULL; +static AppleSignIn_OnError _onErrorCallback = NULL; +static AppleSignIn_OnCancel _onCancelCallback = NULL; + +// Forward declaration +extern UIViewController* UnityGetGLViewController(); + +// Delegate class for ASAuthorizationController +@interface AppleSignInDelegate : NSObject +@end + +@implementation AppleSignInDelegate + +// Called when authorization succeeds +- (void)authorizationController:(ASAuthorizationController *)controller + didCompleteWithAuthorization:(ASAuthorization *)authorization API_AVAILABLE(ios(13.0)) +{ + NSLog(@"[AppleSignIn] ✅ Authorization completed successfully!"); + + if ([authorization.credential isKindOfClass:[ASAuthorizationAppleIDCredential class]]) { + ASAuthorizationAppleIDCredential *credential = (ASAuthorizationAppleIDCredential *)authorization.credential; + + // Extract user ID + NSString *userID = credential.user ?: @""; + NSLog(@"[AppleSignIn] User ID: %@", userID); + + // Extract email (may be empty on subsequent logins) + NSString *email = credential.email ?: @""; + if (email.length > 0) { + NSLog(@"[AppleSignIn] Email: %@", email); + } else { + NSLog(@"[AppleSignIn] Email: (not provided - may be subsequent login)"); + } + + // Extract full name (may be empty on subsequent logins) + NSString *fullName = @""; + if (credential.fullName) { + NSPersonNameComponents *name = credential.fullName; + NSPersonNameComponentsFormatter *formatter = [[NSPersonNameComponentsFormatter alloc] init]; + formatter.style = NSPersonNameComponentsFormatterStyleDefault; + fullName = [formatter stringFromPersonNameComponents:name] ?: @""; + if (fullName.length > 0) { + NSLog(@"[AppleSignIn] Full Name: %@", fullName); + } else { + NSLog(@"[AppleSignIn] Full Name: (not provided - may be subsequent login)"); + } + } + + // Extract identity token (JWT) + NSString *identityToken = @""; + if (credential.identityToken) { + identityToken = [[NSString alloc] initWithData:credential.identityToken encoding:NSUTF8StringEncoding] ?: @""; + NSLog(@"[AppleSignIn] Identity Token: %@...", [identityToken substringToIndex:MIN(50, identityToken.length)]); + } else { + NSLog(@"[AppleSignIn] ⚠️ Identity Token: (not provided)"); + } + + // Extract authorization code + NSString *authorizationCode = @""; + if (credential.authorizationCode) { + authorizationCode = [[NSString alloc] initWithData:credential.authorizationCode encoding:NSUTF8StringEncoding] ?: @""; + NSLog(@"[AppleSignIn] Authorization Code: %@...", [authorizationCode substringToIndex:MIN(20, authorizationCode.length)]); + } else { + NSLog(@"[AppleSignIn] ⚠️ Authorization Code: (not provided)"); + } + + // Call Unity success callback on main thread + dispatch_async(dispatch_get_main_queue(), ^{ + if (_onSuccessCallback) { + NSLog(@"[AppleSignIn] Calling Unity success callback..."); + _onSuccessCallback( + [identityToken UTF8String], + [authorizationCode UTF8String], + [userID UTF8String], + [email UTF8String], + [fullName UTF8String] + ); + } else { + NSLog(@"[AppleSignIn] ⚠️ Success callback is NULL!"); + } + }); + } else { + NSLog(@"[AppleSignIn] ❌ Unexpected credential type"); + dispatch_async(dispatch_get_main_queue(), ^{ + if (_onErrorCallback) { + _onErrorCallback("UNEXPECTED_CREDENTIAL", "Received unexpected credential type"); + } + }); + } +} + +// Called when authorization fails +- (void)authorizationController:(ASAuthorizationController *)controller + didCompleteWithError:(NSError *)error API_AVAILABLE(ios(13.0)) +{ + NSLog(@"[AppleSignIn] ❌ Authorization failed with error: %@", error.localizedDescription); + NSLog(@"[AppleSignIn] Error code: %ld", (long)error.code); + NSLog(@"[AppleSignIn] Error domain: %@", error.domain); + + NSString *errorCode = [NSString stringWithFormat:@"%ld", (long)error.code]; + NSString *errorMessage = error.localizedDescription ?: @"Unknown error"; + + // Check if user canceled (error code 1001) + if (error.code == ASAuthorizationErrorCanceled) { + NSLog(@"[AppleSignIn] User cancelled Apple Sign In"); + dispatch_async(dispatch_get_main_queue(), ^{ + if (_onCancelCallback) { + _onCancelCallback(); + } + }); + } else { + // Other error + dispatch_async(dispatch_get_main_queue(), ^{ + if (_onErrorCallback) { + _onErrorCallback([errorCode UTF8String], [errorMessage UTF8String]); + } + }); + } +} + +// Provide the window for presenting the authorization UI +- (ASPresentationAnchor)presentationAnchorForAuthorizationController:(ASAuthorizationController *)controller API_AVAILABLE(ios(13.0)) +{ + // Get Unity's view controller and return its window + UIViewController *unityViewController = UnityGetGLViewController(); + if (unityViewController && unityViewController.view.window) { + NSLog(@"[AppleSignIn] Using Unity view controller's window for presentation"); + return unityViewController.view.window; + } + + // Fallback: try to get key window + if (@available(iOS 13.0, *)) { + for (UIWindowScene *scene in [UIApplication sharedApplication].connectedScenes) { + if (scene.activationState == UISceneActivationStateForegroundActive) { + for (UIWindow *window in scene.windows) { + if (window.isKeyWindow) { + NSLog(@"[AppleSignIn] Using key window for presentation"); + return window; + } + } + } + } + } + + // Final fallback + NSLog(@"[AppleSignIn] ⚠️ Using fallback window for presentation"); + return [UIApplication sharedApplication].windows.firstObject; +} + +@end + +// Singleton delegate instance +static AppleSignInDelegate *_delegate = nil; + +// Initialize +void AppleSignIn_Init() +{ + NSLog(@"[AppleSignIn] Init called - initializing Apple Sign In plugin ✅"); + + if (_delegate == nil) { + _delegate = [[AppleSignInDelegate alloc] init]; + NSLog(@"[AppleSignIn] Delegate created"); + } +} + +// Check if available +bool AppleSignIn_IsAvailable() +{ + if (@available(iOS 13.0, *)) { + NSLog(@"[AppleSignIn] iOS 13.0+ detected - Apple Sign In is available ✅"); + return true; + } else { + NSLog(@"[AppleSignIn] iOS version too old - Apple Sign In requires iOS 13.0+ ❌"); + return false; + } +} + +// Start Apple Sign In +void AppleSignIn_Start() +{ + NSLog(@"[AppleSignIn] Start called - beginning Apple Sign In flow..."); + + if (@available(iOS 13.0, *)) { + // Ensure delegate is initialized + if (_delegate == nil) { + NSLog(@"[AppleSignIn] Delegate not initialized, calling Init..."); + AppleSignIn_Init(); + } + + // Create Apple ID provider + ASAuthorizationAppleIDProvider *provider = [[ASAuthorizationAppleIDProvider alloc] init]; + ASAuthorizationAppleIDRequest *request = [provider createRequest]; + + // Request email and full name scopes + request.requestedScopes = @[ASAuthorizationScopeEmail, ASAuthorizationScopeFullName]; + NSLog(@"[AppleSignIn] Requesting scopes: email, fullName"); + + // Create authorization controller + ASAuthorizationController *controller = [[ASAuthorizationController alloc] initWithAuthorizationRequests:@[request]]; + controller.delegate = _delegate; + controller.presentationContextProvider = _delegate; + + // Perform the authorization request + NSLog(@"[AppleSignIn] Presenting Apple Sign In UI..."); + [controller performRequests]; + } else { + NSLog(@"[AppleSignIn] ❌ Apple Sign In requires iOS 13.0 or later"); + if (_onErrorCallback) { + _onErrorCallback("UNSUPPORTED", "Apple Sign In requires iOS 13.0 or later"); + } + } +} + +// Set callbacks +void AppleSignIn_SetOnSuccessCallback(AppleSignIn_OnSuccess callback) +{ + NSLog(@"[AppleSignIn] Success callback registered"); + _onSuccessCallback = callback; +} + +void AppleSignIn_SetOnErrorCallback(AppleSignIn_OnError callback) +{ + NSLog(@"[AppleSignIn] Error callback registered"); + _onErrorCallback = callback; +} + +void AppleSignIn_SetOnCancelCallback(AppleSignIn_OnCancel callback) +{ + NSLog(@"[AppleSignIn] Cancel callback registered"); + _onCancelCallback = callback; +} + +/* + * SLICE 3 IMPLEMENTATION COMPLETE: + * + * This native plugin now implements full Apple Sign In functionality: + * ✅ Real ASAuthorizationController integration + * ✅ Shows native Apple Sign In UI sheet + * ✅ Handles Face ID / Touch ID / Passcode authentication + * ✅ Requests email and fullName scopes + * ✅ Returns real identity tokens (JWT) from Apple + * ✅ Returns authorization code for server-to-server validation + * ✅ Returns user ID, email, and full name + * ✅ Handles user cancellation (error code 1001) + * ✅ Handles other errors with proper error messages + * ✅ Properly presents UI using Unity's view controller + * + * IMPORTANT NOTES: + * - Email and full name are only provided on FIRST sign-in + * - Subsequent sign-ins will return empty strings for email/fullName + * - Backend must store this data from the first login + * - User ID (sub claim) is the stable identifier + * - Identity token is a JWT that must be verified by backend + * + * NEXT STEP (Slice 4): + * - Implement backend token exchange endpoint + * - Verify JWT with Apple's public keys + * - Create/update user account + * - Return Passport session tokens + */ + diff --git a/src/Packages/Passport/Runtime/Plugins/iOS/AppleSignIn.mm.meta b/src/Packages/Passport/Runtime/Plugins/iOS/AppleSignIn.mm.meta new file mode 100644 index 000000000..84ea03c30 --- /dev/null +++ b/src/Packages/Passport/Runtime/Plugins/iOS/AppleSignIn.mm.meta @@ -0,0 +1,37 @@ +fileFormatVersion: 2 +guid: 4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a +PluginImporter: + externalObjects: {} + serializedVersion: 2 + iconMap: {} + executionOrder: {} + defineConstraints: [] + isPreloaded: 0 + isOverridable: 0 + isExplicitlyReferenced: 0 + validateReferences: 1 + platformData: + - first: + Any: + second: + enabled: 0 + settings: {} + - first: + Editor: Editor + second: + enabled: 0 + settings: + DefaultValueInitialized: true + - first: + iPhone: iOS + second: + enabled: 1 + settings: + AddToEmbeddedBinaries: false + CPU: AnyCPU + CompileFlags: + FrameworkDependencies: UIKit;Foundation;AuthenticationServices + userData: + assetBundleName: + assetBundleVariant: + diff --git a/src/Packages/Passport/Runtime/Scripts/Private/AppleSignIn.meta b/src/Packages/Passport/Runtime/Scripts/Private/AppleSignIn.meta new file mode 100644 index 000000000..5d16c6fc9 --- /dev/null +++ b/src/Packages/Passport/Runtime/Scripts/Private/AppleSignIn.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 56508525b6a734dc9b689c832533a433 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/src/Packages/Passport/Runtime/Scripts/Private/AppleSignIn/AppleSignInNative.cs b/src/Packages/Passport/Runtime/Scripts/Private/AppleSignIn/AppleSignInNative.cs new file mode 100644 index 000000000..8425ae20c --- /dev/null +++ b/src/Packages/Passport/Runtime/Scripts/Private/AppleSignIn/AppleSignInNative.cs @@ -0,0 +1,220 @@ +#if UNITY_IOS +using System; +using System.Runtime.InteropServices; +using UnityEngine; +using Immutable.Passport.Core.Logging; + +namespace Immutable.Passport +{ + /// + /// Native iOS Apple Sign In wrapper + /// Slice 2: Stub implementation for testing P/Invoke bridge + /// + public class AppleSignInNative + { + private const string TAG = "[AppleSignInNative]"; + + // P/Invoke declarations + [DllImport("__Internal")] + private static extern void AppleSignIn_Init(); + + [DllImport("__Internal")] + private static extern bool AppleSignIn_IsAvailable(); + + [DllImport("__Internal")] + private static extern void AppleSignIn_Start(); + + // Callback delegates + private delegate void OnSuccessDelegate(string identityToken, string authorizationCode, string userID, string email, string fullName); + private delegate void OnErrorDelegate(string errorCode, string errorMessage); + private delegate void OnCancelDelegate(); + + [DllImport("__Internal")] + private static extern void AppleSignIn_SetOnSuccessCallback(OnSuccessDelegate callback); + + [DllImport("__Internal")] + private static extern void AppleSignIn_SetOnErrorCallback(OnErrorDelegate callback); + + [DllImport("__Internal")] + private static extern void AppleSignIn_SetOnCancelCallback(OnCancelDelegate callback); + + // Keep delegates alive to prevent garbage collection + private static OnSuccessDelegate _onSuccessDelegate; + private static OnErrorDelegate _onErrorDelegate; + private static OnCancelDelegate _onCancelDelegate; + + // Events that Unity code can subscribe to + public static event Action OnSuccess; + public static event Action OnError; + public static event Action OnCancel; + + private static bool _isInitialized = false; + + /// + /// Initialize the native plugin and set up callbacks + /// + public static void Initialize() + { + if (_isInitialized) + { + PassportLogger.Warn($"{TAG} Already initialized"); + return; + } + + PassportLogger.Info($"{TAG} Initializing native Apple Sign In plugin..."); + + try + { + // Initialize native plugin + AppleSignIn_Init(); + + // Set up callbacks (using static methods) + _onSuccessDelegate = new OnSuccessDelegate(OnSuccessCallback); + _onErrorDelegate = new OnErrorDelegate(OnErrorCallback); + _onCancelDelegate = new OnCancelDelegate(OnCancelCallback); + + AppleSignIn_SetOnSuccessCallback(_onSuccessDelegate); + AppleSignIn_SetOnErrorCallback(_onErrorDelegate); + AppleSignIn_SetOnCancelCallback(_onCancelDelegate); + + _isInitialized = true; + PassportLogger.Info($"{TAG} SUCCESS: Native plugin initialized successfully"); + } + catch (Exception ex) + { + PassportLogger.Error($"{TAG} ERROR: Failed to initialize: {ex.Message}"); + throw; + } + } + + /// + /// Check if Apple Sign In is available on this device + /// + public static bool IsAvailable() + { + if (!_isInitialized) + { + PassportLogger.Warn($"{TAG} Not initialized, calling Initialize()"); + Initialize(); + } + + try + { + bool available = AppleSignIn_IsAvailable(); + PassportLogger.Info($"{TAG} IsAvailable: {available}"); + return available; + } + catch (Exception ex) + { + PassportLogger.Error($"{TAG} Error checking availability: {ex.Message}"); + return false; + } + } + + /// + /// Start the Apple Sign In flow + /// + public static void Start() + { + if (!_isInitialized) + { + PassportLogger.Error($"{TAG} Not initialized! Call Initialize() first"); + throw new InvalidOperationException("AppleSignInNative not initialized"); + } + + PassportLogger.Info($"{TAG} Starting Apple Sign In flow..."); + + try + { + AppleSignIn_Start(); + } + catch (Exception ex) + { + PassportLogger.Error($"{TAG} Error starting Apple Sign In: {ex.Message}"); + throw; + } + } + + // Callback implementations (called from native code) + [AOT.MonoPInvokeCallback(typeof(OnSuccessDelegate))] + private static void OnSuccessCallback(string identityToken, string authorizationCode, string userID, string email, string fullName) + { + PassportLogger.Info($"{TAG} SUCCESS: Native callback - Success!"); + PassportLogger.Info($"{TAG} UserID: {userID}"); + PassportLogger.Info($"{TAG} Email: {email}"); + PassportLogger.Info($"{TAG} Full Name: {fullName}"); + PassportLogger.Info($"{TAG} Identity Token: {identityToken.Substring(0, Math.Min(50, identityToken.Length))}..."); + + // Invoke event on main thread + UnityMainThreadDispatcher.Enqueue(() => + { + OnSuccess?.Invoke(identityToken, authorizationCode, userID, email, fullName); + }); + } + + [AOT.MonoPInvokeCallback(typeof(OnErrorDelegate))] + private static void OnErrorCallback(string errorCode, string errorMessage) + { + PassportLogger.Error($"{TAG} ERROR: Native callback - Error: {errorCode} - {errorMessage}"); + + // Invoke event on main thread + UnityMainThreadDispatcher.Enqueue(() => + { + OnError?.Invoke(errorCode, errorMessage); + }); + } + + [AOT.MonoPInvokeCallback(typeof(OnCancelDelegate))] + private static void OnCancelCallback() + { + PassportLogger.Info($"{TAG} WARNING: Native callback - Cancelled"); + + // Invoke event on main thread + UnityMainThreadDispatcher.Enqueue(() => + { + OnCancel?.Invoke(); + }); + } + } + + /// + /// Helper to dispatch callbacks to Unity main thread + /// + internal static class UnityMainThreadDispatcher + { + private static readonly System.Collections.Generic.Queue _executionQueue = new System.Collections.Generic.Queue(); + + public static void Enqueue(Action action) + { + lock (_executionQueue) + { + _executionQueue.Enqueue(action); + } + } + + [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)] + private static void Initialize() + { + // Create a GameObject to run Update loop + var go = new GameObject("AppleSignInDispatcher"); + GameObject.DontDestroyOnLoad(go); + go.AddComponent(); + } + + private class MainThreadDispatcher : MonoBehaviour + { + private void Update() + { + lock (_executionQueue) + { + while (_executionQueue.Count > 0) + { + _executionQueue.Dequeue()?.Invoke(); + } + } + } + } + } +} +#endif + diff --git a/src/Packages/Passport/Runtime/Scripts/Private/AppleSignIn/AppleSignInNative.cs.meta b/src/Packages/Passport/Runtime/Scripts/Private/AppleSignIn/AppleSignInNative.cs.meta new file mode 100644 index 000000000..d5d3b85eb --- /dev/null +++ b/src/Packages/Passport/Runtime/Scripts/Private/AppleSignIn/AppleSignInNative.cs.meta @@ -0,0 +1,12 @@ +fileFormatVersion: 2 +guid: 5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: + diff --git a/src/Packages/Passport/Runtime/Scripts/Private/Model/AppleSignInModels.cs b/src/Packages/Passport/Runtime/Scripts/Private/Model/AppleSignInModels.cs new file mode 100644 index 000000000..ce9464465 --- /dev/null +++ b/src/Packages/Passport/Runtime/Scripts/Private/Model/AppleSignInModels.cs @@ -0,0 +1,122 @@ +using System; +using Newtonsoft.Json; + +namespace Immutable.Passport.Model +{ + /// + /// Request sent from Unity to your backend with Apple Sign-in tokens + /// Your backend will verify these tokens and exchange them with Passport + /// + [Serializable] + public class AppleSignInRequest + { + /// + /// JWT identity token from Apple (contains user's email, sub, etc.) + /// Your backend MUST verify this token with Apple's public keys + /// + [JsonProperty("identityToken")] + public string identityToken; + + /// + /// Optional: Authorization code from Apple (single-use, short-lived) + /// Can be used to get refresh tokens from Apple if needed + /// + [JsonProperty("authorizationCode")] + public string authorizationCode; + + /// + /// Apple's unique user identifier (from JWT 'sub' claim) + /// Stable across sign-ins, use this as primary identifier + /// + [JsonProperty("userId")] + public string userId; + + /// + /// User's email (only populated on first sign-in in native callback, but always in JWT) + /// Your backend should extract this from the JWT + /// + [JsonProperty("email")] + public string email; + + /// + /// User's full name (only populated on first sign-in) + /// Store this in your backend on first sign-in + /// + [JsonProperty("fullName")] + public string fullName; + + /// + /// Passport client ID - backend validates this matches expected client + /// + [JsonProperty("clientId")] + public string clientId; + + public AppleSignInRequest(string identityToken, string authorizationCode, string userId, string email, string fullName, string clientId) + { + this.identityToken = identityToken; + this.authorizationCode = authorizationCode; + this.userId = userId; + this.email = email; + this.fullName = fullName; + this.clientId = clientId; + } + } + + /// + /// Response from your backend containing Passport tokens + /// Your backend exchanges Apple tokens with Immutable's Passport backend and returns these + /// + [Serializable] + public class AppleSignInResponse + { + /// + /// Passport access token (use this for API calls) + /// + [JsonProperty("access_token")] + public string accessToken; + + /// + /// Passport ID token (JWT containing user identity) + /// + [JsonProperty("id_token")] + public string idToken; + + /// + /// Passport refresh token (use to get new access tokens) + /// + [JsonProperty("refresh_token")] + public string refreshToken; + + /// + /// Token expiration time in seconds + /// + [JsonProperty("expires_in")] + public int expiresIn; + + /// + /// Token type (usually "Bearer") + /// + [JsonProperty("token_type")] + public string tokenType; + } + + /// + /// Error response from backend if Apple Sign-in fails + /// + [Serializable] + public class AppleSignInErrorResponse + { + /// + /// Error code (e.g., "invalid_token", "verification_failed") + /// + [JsonProperty("error")] + public string error; + + /// + /// Human-readable error message + /// + [JsonProperty("error_description")] + public string errorDescription; + } +} + diff --git a/src/Packages/Passport/Runtime/Scripts/Private/Model/AppleSignInModels.cs.meta b/src/Packages/Passport/Runtime/Scripts/Private/Model/AppleSignInModels.cs.meta new file mode 100644 index 000000000..18fa59f0e --- /dev/null +++ b/src/Packages/Passport/Runtime/Scripts/Private/Model/AppleSignInModels.cs.meta @@ -0,0 +1,12 @@ +fileFormatVersion: 2 +guid: 8f2b4d6e9c3a7f5b1e4d8c2a6f9e3b7d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: + diff --git a/src/Packages/Passport/Runtime/Scripts/Private/PassportImpl.cs b/src/Packages/Passport/Runtime/Scripts/Private/PassportImpl.cs index 2a492d56d..5dde11328 100644 --- a/src/Packages/Passport/Runtime/Scripts/Private/PassportImpl.cs +++ b/src/Packages/Passport/Runtime/Scripts/Private/PassportImpl.cs @@ -29,6 +29,8 @@ public class PassportImpl private UniTaskCompletionSource? _pkceCompletionSource; private string _redirectUri; private string _logoutRedirectUri; + private string _environment; + private string _clientId; #if UNITY_ANDROID // Used for the PKCE callback @@ -49,8 +51,10 @@ public PassportImpl(IBrowserCommunicationsManager communicationsManager) public async UniTask Init(string clientId, string environment, string redirectUri, string logoutRedirectUri, string? deeplink = null) { + _clientId = clientId; _redirectUri = redirectUri; _logoutRedirectUri = logoutRedirectUri; + _environment = environment; #if (UNITY_ANDROID && !UNITY_EDITOR_WIN) || (UNITY_IPHONE && !UNITY_EDITOR_WIN) || UNITY_STANDALONE_OSX || UNITY_WEBGL _communicationsManager.OnAuthPostMessage += OnDeepLinkActivated; @@ -521,9 +525,291 @@ public async UniTask CompleteLogin(TokenResponse request) { var json = JsonUtility.ToJson(request); var callResponse = await _communicationsManager.Call(PassportFunction.STORE_TOKENS, json); - return callResponse.GetBoolResponse() ?? false; + + // Check if the call was successful + // The bridge returns success: true with a result object containing user info (sub, email) + // GetBoolResponse() looks for a boolean in 'result', but storeTokens returns an object + // So we need to deserialize and check the 'success' field instead + var response = callResponse.OptDeserializeObject(); + + if (response != null && response.success == true) + { + PassportLogger.Debug($"{TAG} STORE_TOKENS succeeded"); + return true; + } + + PassportLogger.Error($"{TAG} STORE_TOKENS failed or returned null"); + return false; } +#if UNITY_IOS && !UNITY_EDITOR + /// + /// Logs in with Apple Sign-in (iOS only). + /// This method: + /// 1. Initiates native Apple Sign-in flow + /// 2. Sends Apple tokens to backend for verification and Auth0 token exchange + /// - Backend verifies Apple identity token with Apple's public keys + /// - Backend calls IMX Engine Auth Service to exchange for Auth0 tokens + /// 3. Completes Passport login with Auth0 tokens + /// + /// The backend endpoint is automatically determined based on the environment. + /// + /// True if login successful, false otherwise + public async UniTask LoginWithApple() + { + try + { + PassportLogger.Info($"{TAG} Starting Apple Sign-in flow..."); + SendAuthEvent(PassportAuthEvent.LoggingInPKCE); + Track(PassportAnalytics.EventName.START_LOGIN_PKCE); + + // Step 1: Initiate native Apple Sign-in + var appleSignInTask = new UniTaskCompletionSource(); + + void OnSuccess(string identityToken, string authorizationCode, string userId, string email, string fullName) + { + PassportLogger.Info($"{TAG} Apple Sign-in native callback - Success!"); + var request = new AppleSignInRequest( + identityToken, + authorizationCode, + userId, + email, + fullName, + _clientId + ); + appleSignInTask.TrySetResult(request); + } + + void OnError(string errorCode, string errorMessage) + { + PassportLogger.Error($"{TAG} Apple Sign-in native callback - Error: {errorCode} - {errorMessage}"); + appleSignInTask.TrySetException(new PassportException( + $"Apple Sign-in failed: {errorMessage}", + PassportErrorType.AUTHENTICATION_ERROR + )); + } + + void OnCancel() + { + PassportLogger.Info($"{TAG} Apple Sign-in native callback - Cancelled by user"); + appleSignInTask.TrySetException(new OperationCanceledException("User cancelled Apple Sign-in")); + } + + // Subscribe to native callbacks + AppleSignInNative.OnSuccess += OnSuccess; + AppleSignInNative.OnError += OnError; + AppleSignInNative.OnCancel += OnCancel; + + try + { + // Check if Apple Sign-in is available + if (!AppleSignInNative.IsAvailable()) + { + throw new PassportException("Apple Sign-in is not available on this device", PassportErrorType.OPERATION_NOT_SUPPORTED_ERROR); + } + + // Start native Apple Sign-in flow + AppleSignInNative.Start(); + + // Wait for native callback + var appleRequest = await appleSignInTask.Task; + PassportLogger.Info($"{TAG} Received Apple tokens, exchanging with backend..."); + + // Step 2: Construct backend URL based on environment + string backendUrl = GetAppleSignInBackendUrl(); + PassportLogger.Debug($"{TAG} Using Apple Sign-in backend: {backendUrl}"); + + // Step 3: Exchange Apple tokens with backend + var passportTokens = await ExchangeAppleTokenWithBackend(backendUrl, appleRequest); + + if (passportTokens == null) + { + throw new PassportException("Backend returned null tokens", PassportErrorType.AUTHENTICATION_ERROR); + } + + PassportLogger.Info($"{TAG} Received Passport tokens, completing login..."); + + // Step 4: Complete login using existing infrastructure + var tokenResponse = new TokenResponse + { + access_token = passportTokens.accessToken, + id_token = passportTokens.idToken, + refresh_token = passportTokens.refreshToken, + expires_in = passportTokens.expiresIn, + token_type = passportTokens.tokenType + }; + + PassportLogger.Debug($"{TAG} Token response:"); + PassportLogger.Debug($"{TAG} access_token: {(string.IsNullOrEmpty(tokenResponse.access_token) ? "EMPTY" : tokenResponse.access_token)}"); + PassportLogger.Debug($"{TAG} id_token: {(string.IsNullOrEmpty(tokenResponse.id_token) ? "EMPTY" : tokenResponse.id_token)}"); + PassportLogger.Debug($"{TAG} refresh_token: {(string.IsNullOrEmpty(tokenResponse.refresh_token) ? "EMPTY" : tokenResponse.refresh_token)}"); + PassportLogger.Debug($"{TAG} expires_in: {tokenResponse.expires_in}"); + PassportLogger.Debug($"{TAG} token_type: {tokenResponse.token_type}"); + + PassportLogger.Debug($"{TAG} Calling CompleteLogin..."); + bool success = await CompleteLogin(tokenResponse); + PassportLogger.Debug($"{TAG} CompleteLogin returned: {success}"); + + if (success) + { + _isLoggedIn = true; + PassportLogger.Info($"{TAG} Apple Sign-in completed successfully!"); + Track(PassportAnalytics.EventName.COMPLETE_LOGIN_PKCE, success: true); + SendAuthEvent(PassportAuthEvent.LoginPKCESuccess); + } + else + { + throw new PassportException("Failed to complete login with Passport tokens", PassportErrorType.AUTHENTICATION_ERROR); + } + + return success; + } + finally + { + // Clean up event handlers + AppleSignInNative.OnSuccess -= OnSuccess; + AppleSignInNative.OnError -= OnError; + AppleSignInNative.OnCancel -= OnCancel; + } + } + catch (OperationCanceledException) + { + PassportLogger.Info($"{TAG} Apple Sign-in was cancelled by user"); + Track(PassportAnalytics.EventName.COMPLETE_LOGIN_PKCE, success: false); + SendAuthEvent(PassportAuthEvent.LoginPKCEFailed); + return false; + } + catch (Exception ex) + { + var errorMessage = $"Apple Sign-in failed: {ex.Message}"; + PassportLogger.Error($"{TAG} {errorMessage}"); + Track(PassportAnalytics.EventName.COMPLETE_LOGIN_PKCE, success: false); + SendAuthEvent(PassportAuthEvent.LoginPKCEFailed); + throw new PassportException(errorMessage, PassportErrorType.AUTHENTICATION_ERROR); + } + } + + /// + /// Gets the Apple Sign-in backend URL based on the configured environment + /// Points to the game developer's BFF (Backend-for-Frontend) which securely + /// forwards requests to IMX Engine with the API key + /// + private string GetAppleSignInBackendUrl() + { + string baseUrl = _environment.ToLower() switch + { + // For production/sandbox/dev, game developers need to deploy their own BFF + // that calls api.immutable.com/v1/apple-signin with their API key + "production" => "https://your-game-backend.com/auth/apple", + "sandbox" => "https://your-game-backend-sandbox.com/auth/apple", + "dev" or "development" => "https://your-game-backend-dev.com/auth/apple", + "local" => GetLocalUrl(), + _ => throw new PassportException($"Unknown environment: {_environment}", PassportErrorType.INITALISATION_ERROR) + }; + + return baseUrl; + } + + /// + /// Gets the local URL for development, handling iOS simulator networking + /// iOS simulator can't access host's localhost, so we use the Mac's IP address + /// + private string GetLocalUrl() + { +#if UNITY_IOS && !UNITY_EDITOR + // On iOS device/simulator, use Mac's IP address instead of localhost + // Update this IP address to match your Mac's local IP + return "http://192.168.4.27:3000/auth/apple"; +#else + return "http://localhost:3000/auth/apple"; +#endif + } + + /// + /// Sends Apple Sign-in tokens to backend for verification and Auth0 token exchange + /// The backend (BFF) forwards the request to IMX Engine with the API key + /// + private async UniTask ExchangeAppleTokenWithBackend( + string backendUrl, + AppleSignInRequest request) + { + PassportLogger.Info($"{TAG} Sending Apple tokens to backend for exchange..."); + PassportLogger.Debug($"{TAG} Backend URL: {backendUrl}"); + PassportLogger.Debug($"{TAG} User ID: {request.userId}"); + PassportLogger.Debug($"{TAG} Email: {request.email ?? "(not provided)"}"); + + // Serialize the Apple Sign-in request to JSON + string jsonBody = Newtonsoft.Json.JsonConvert.SerializeObject(request); + + using (var webRequest = UnityEngine.Networking.UnityWebRequest.Post(backendUrl, "")) + { + // Set JSON body + var bodyRaw = System.Text.Encoding.UTF8.GetBytes(jsonBody); + webRequest.uploadHandler = new UnityEngine.Networking.UploadHandlerRaw(bodyRaw); + webRequest.downloadHandler = new UnityEngine.Networking.DownloadHandlerBuffer(); + webRequest.SetRequestHeader("Content-Type", "application/json"); + + // Send request + PassportLogger.Debug($"{TAG} Sending request to backend..."); + await webRequest.SendWebRequest(); + + // Check for errors + if (webRequest.result != UnityEngine.Networking.UnityWebRequest.Result.Success) + { + string errorDetails = webRequest.downloadHandler?.text ?? "No response body"; + PassportLogger.Error($"{TAG} Backend request failed: {webRequest.error}"); + PassportLogger.Error($"{TAG} Response: {errorDetails}"); + + // Try to parse error response + try + { + var errorResponse = Newtonsoft.Json.JsonConvert.DeserializeObject(errorDetails); + throw new PassportException( + $"Apple Sign-in failed: {errorResponse?.errorDescription ?? webRequest.error}", + PassportErrorType.AUTHENTICATION_ERROR + ); + } + catch (Newtonsoft.Json.JsonException) + { + // Couldn't parse error response, use generic message + throw new PassportException( + $"Apple Sign-in failed: {webRequest.error}. Details: {errorDetails}", + PassportErrorType.AUTHENTICATION_ERROR + ); + } + } + + // Parse success response + var responseText = webRequest.downloadHandler.text; + PassportLogger.Info($"{TAG} Backend response received successfully"); + PassportLogger.Debug($"{TAG} Full response: {responseText}"); + + var passportTokens = Newtonsoft.Json.JsonConvert.DeserializeObject(responseText); + + if (passportTokens == null) + { + PassportLogger.Error($"{TAG} Failed to deserialize backend response"); + throw new PassportException("Backend returned invalid token response - deserialization failed", PassportErrorType.AUTHENTICATION_ERROR); + } + + PassportLogger.Debug($"{TAG} Deserialized tokens:"); + PassportLogger.Debug($"{TAG} accessToken: {(string.IsNullOrEmpty(passportTokens.accessToken) ? "EMPTY/NULL" : passportTokens.accessToken)}"); + PassportLogger.Debug($"{TAG} idToken: {(string.IsNullOrEmpty(passportTokens.idToken) ? "EMPTY/NULL" : passportTokens.idToken)}"); + PassportLogger.Debug($"{TAG} refreshToken: {(string.IsNullOrEmpty(passportTokens.refreshToken) ? "EMPTY/NULL" : passportTokens.refreshToken)}"); + PassportLogger.Debug($"{TAG} expiresIn: {passportTokens.expiresIn}"); + PassportLogger.Debug($"{TAG} tokenType: {passportTokens.tokenType}"); + + if (string.IsNullOrEmpty(passportTokens.accessToken)) + { + PassportLogger.Error($"{TAG} Backend returned empty/null access_token"); + throw new PassportException("Backend returned invalid token response - missing access_token", PassportErrorType.AUTHENTICATION_ERROR); + } + + return passportTokens; + } + } +#endif + public async UniTask GetAddress() { var response = await _communicationsManager.Call(PassportFunction.IMX.GET_ADDRESS); diff --git a/src/Packages/Passport/Runtime/Scripts/Public/AppleSignIn.meta b/src/Packages/Passport/Runtime/Scripts/Public/AppleSignIn.meta new file mode 100644 index 000000000..8cf7e38ba --- /dev/null +++ b/src/Packages/Passport/Runtime/Scripts/Public/AppleSignIn.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: cf12e37f1a3c54cb7b51ad60d5483e97 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/src/Packages/Passport/Runtime/Scripts/Public/Passport.cs b/src/Packages/Passport/Runtime/Scripts/Public/Passport.cs index f028b1140..c70404b6b 100644 --- a/src/Packages/Passport/Runtime/Scripts/Public/Passport.cs +++ b/src/Packages/Passport/Runtime/Scripts/Public/Passport.cs @@ -1,739 +1,762 @@ -using System.Collections.Generic; -using System; -using System.Text.RegularExpressions; -#if UNITY_STANDALONE_WIN || (UNITY_ANDROID && UNITY_EDITOR_WIN) || (UNITY_IPHONE && UNITY_EDITOR_WIN) -#if !IMMUTABLE_CUSTOM_BROWSER -using VoltstroStudios.UnityWebBrowser; -using VoltstroStudios.UnityWebBrowser.Core; -using VoltstroStudios.UnityWebBrowser.Shared; -using VoltstroStudios.UnityWebBrowser.Logging; -#endif -#elif (UNITY_ANDROID && !UNITY_EDITOR_WIN) || (UNITY_IPHONE && !UNITY_EDITOR_WIN) || UNITY_STANDALONE_OSX || UNITY_WEBGL -using Immutable.Browser.Gree; -#endif -using Immutable.Passport.Event; -using Immutable.Browser.Core; -using Immutable.Passport.Model; -using Immutable.Passport.Core; -using Immutable.Passport.Core.Logging; -using Cysharp.Threading.Tasks; -using UnityEngine; -#if UNITY_EDITOR -using UnityEditor; -#endif - -namespace Immutable.Passport -{ - -#if UNITY_STANDALONE_WIN || (UNITY_ANDROID && UNITY_EDITOR_WIN) || (UNITY_IPHONE && UNITY_EDITOR_WIN) - public class Passport : MonoBehaviour -#else - public class Passport -#endif - { - private const string TAG = "[Passport]"; - - public static Passport? Instance { get; private set; } - private PassportImpl? _passportImpl; - public string Environment { get; private set; } - - private IWebBrowserClient? _webBrowserClient; - - // Keeps track of the latest received deeplink - private static string? _deeplink; - private static bool _readySignalReceived; - - /// - /// Passport auth events - /// - /// - public event OnAuthEventDelegate? OnAuthEvent; - - /// - /// Gets or sets the log level for the SDK. - /// - /// - /// The log level determines which messages are recorded based on their severity. - /// - /// The default value is . - /// - /// - /// See for valid log levels and their meanings. - /// - /// - /// Passport.LogLevel = LogLevel.Debug; - /// - /// - public static LogLevel LogLevel - { - get => _logLevel; - set - { - _logLevel = value; - PassportLogger.CurrentLogLevel = _logLevel; - -#if !IMMUTABLE_CUSTOM_BROWSER && (UNITY_STANDALONE_WIN || (UNITY_ANDROID && UNITY_EDITOR_WIN) || (UNITY_IPHONE && UNITY_EDITOR_WIN)) - SetDefaultWindowsBrowserLogLevel(); -#endif - } - } - - private static LogLevel _logLevel = LogLevel.Info; - - /// - /// Determines whether sensitive token values should be redacted from SDK logs. - /// - /// - /// When set to true, access tokens and ID tokens will be replaced with [REDACTED] in log messages to enhance security. - /// This setting is useful for preventing sensitive data from appearing in logs, especially when debugging or sharing logs with others. - /// - /// The default value is false, meaning tokens will be logged in full at appropriate log levels. - /// - /// - /// Passport.RedactTokensInLogs = true; - /// - /// - public static bool RedactTokensInLogs - { - get => _redactTokensInLogs; - set - { - _redactTokensInLogs = value; - PassportLogger.RedactionHandler = value ? RedactTokenValues : null; - -#if !IMMUTABLE_CUSTOM_BROWSER && (UNITY_STANDALONE_WIN || (UNITY_ANDROID && UNITY_EDITOR_WIN) || (UNITY_IPHONE && UNITY_EDITOR_WIN)) - SetWindowsRedactionHandler(); -#endif - } - } - - private static bool _redactTokensInLogs; - - private Passport() - { - // Handle clean-up tasks when the application is quitting -#if UNITY_STANDALONE_WIN || (UNITY_ANDROID && UNITY_EDITOR_WIN) || (UNITY_IPHONE && UNITY_EDITOR_WIN) - Application.quitting += OnQuit; -#elif UNITY_IPHONE || UNITY_STANDALONE_OSX || UNITY_EDITOR_OSX - // Handle deeplinks for iOS and macOS - Application.deepLinkActivated += OnDeepLinkActivated; - - // Check if there is a deep link URL provided on application start - if (!string.IsNullOrEmpty(Application.absoluteURL)) - { - // Handle the deep link if provided during a cold start - OnDeepLinkActivated(Application.absoluteURL); - } -#endif - -#if UNITY_EDITOR - EditorApplication.playModeStateChanged += OnPlayModeStateChanged; -#endif - } - - /// - /// Initialises Passport with the specified parameters. - /// This sets up the Passport instance, configures the web browser, and waits for the ready signal. - /// - /// The client ID - /// The environment to connect to - /// The URL where the browser will redirect after successful authentication. On Windows, this must use a custom protocol (e.g., 'mygame://callback') instead of http/https. - /// The URL where the browser will redirect after logout is complete. On Windows, this must use a custom protocol (e.g., 'mygame://logout') instead of http/https. - /// (Windows only) Timeout duration in milliseconds to wait for the default Windows browser engine to start. - /// (Windows only) Custom Windows browser to use instead of the default browser in the SDK. - public static UniTask Init( - string clientId, - string environment, - string redirectUri, - string logoutRedirectUri -#if UNITY_STANDALONE_WIN || (UNITY_ANDROID && UNITY_EDITOR_WIN) || (UNITY_IPHONE && UNITY_EDITOR_WIN) - , int engineStartupTimeoutMs = 60000, - IWindowsWebBrowserClient? windowsWebBrowserClient = null -#endif - ) - { -#if UNITY_STANDALONE_WIN || (UNITY_ANDROID && UNITY_EDITOR_WIN) || (UNITY_IPHONE && UNITY_EDITOR_WIN) - ValidateWindowsProtocols(redirectUri, logoutRedirectUri); -#endif - - if (Instance == null) - { - PassportLogger.Info($"{TAG} Initialising Passport..."); - -#if UNITY_STANDALONE_WIN || (UNITY_ANDROID && UNITY_EDITOR_WIN) || (UNITY_IPHONE && UNITY_EDITOR_WIN) - var obj = new GameObject("Passport"); - Instance = obj.AddComponent(); - DontDestroyOnLoad(obj); -#else - Instance = new Passport(); -#endif - Instance.Environment = environment; - - // Start initialisation process - return Instance.Initialise( -#if UNITY_STANDALONE_WIN || (UNITY_ANDROID && UNITY_EDITOR_WIN) || (UNITY_IPHONE && UNITY_EDITOR_WIN) - engineStartupTimeoutMs, windowsWebBrowserClient -#endif - ) - .ContinueWith(async () => - { - // Wait for the ready signal - PassportLogger.Info($"{TAG} Waiting for ready signal..."); - await UniTask.WaitUntil(() => _readySignalReceived == true); - }) - .ContinueWith(async () => - { - if (_readySignalReceived) - { - // Initialise Passport with provided parameters - await Instance.GetPassportImpl().Init(clientId, environment, redirectUri, logoutRedirectUri, _deeplink); - return Instance; - } - else - { - PassportLogger.Error($"{TAG} Failed to initialise Passport"); - throw new PassportException("Failed to initialise Passport", PassportErrorType.INITALISATION_ERROR); - } - }); - } - - // Return the existing instance if already initialised - _readySignalReceived = true; - return UniTask.FromResult(Instance); - } - - /// - /// Initialises the appropriate web browser and sets up browser communication. - /// - /// (Windows only) Timeout duration in milliseconds to wait for the default Windows browser engine to start. - /// (Windows only) Custom Windows browser to use instead of the default browser in the SDK. - private async UniTask Initialise( -#if UNITY_STANDALONE_WIN || (UNITY_ANDROID && UNITY_EDITOR_WIN) || (UNITY_IPHONE && UNITY_EDITOR_WIN) - int engineStartupTimeoutMs, IWindowsWebBrowserClient? windowsWebBrowserClient -#endif - ) - { - try - { - // Initialise the web browser client -#if UNITY_STANDALONE_WIN || (UNITY_ANDROID && UNITY_EDITOR_WIN) || (UNITY_IPHONE && UNITY_EDITOR_WIN) - if (windowsWebBrowserClient != null) - { - // Use the provided custom Windows browser client - _webBrowserClient = new WindowsWebBrowserClientAdapter(windowsWebBrowserClient); - await ((WindowsWebBrowserClientAdapter)_webBrowserClient).Init(); - } - else - { -#if IMMUTABLE_CUSTOM_BROWSER - throw new PassportException("When 'IMMUTABLE_CUSTOM_BROWSER' is defined in Scripting Define Symbols, " + - " 'windowsWebBrowserClient' must not be null."); -#else - _webBrowserClient = gameObject.AddComponent(); - await ((UwbWebView)_webBrowserClient).Init(engineStartupTimeoutMs, _redactTokensInLogs, RedactTokenValues); - _readySignalReceived = true; -#endif - } -#elif (UNITY_ANDROID && !UNITY_EDITOR_WIN) || (UNITY_IPHONE && !UNITY_EDITOR_WIN) || UNITY_STANDALONE_OSX || UNITY_WEBGL - // Initialise default browser client for Android, iOS, and macOS - _webBrowserClient = new GreeBrowserClient(); - await UniTask.CompletedTask; -#else - throw new PassportException("Platform not supported"); -#endif - - // Set up browser communication - BrowserCommunicationsManager communicationsManager = new BrowserCommunicationsManager(_webBrowserClient); - -#if UNITY_WEBGL - _readySignalReceived = true; -#else - // Mark ready when browser is initialised and game bridge file is loaded - communicationsManager.OnReady += () => _readySignalReceived = true; -#endif - // Set up Passport implementation - _passportImpl = new PassportImpl(communicationsManager); - // Subscribe to Passport authentication events - _passportImpl.OnAuthEvent += OnPassportAuthEvent; - } - catch (Exception) - { - // Reset everything on error - _readySignalReceived = false; - Instance = null; - throw; - } - } - -#if UNITY_STANDALONE_WIN || (UNITY_ANDROID && UNITY_EDITOR_WIN) || (UNITY_IPHONE && UNITY_EDITOR_WIN) - private void Awake() - { - if (Instance == null) - { - Instance = this; - DontDestroyOnLoad(gameObject); - } - else if (Instance != this) - { - Destroy(gameObject); - } - } -#endif - - /// - /// Sets the timeout time for waiting for each call to respond (in milliseconds). - /// This only applies to functions that use the browser communications manager. - /// - public void SetCallTimeout(int ms) - { - GetPassportImpl().SetCallTimeout(ms); - } - - /// - /// Logs into Passport using Authorisation Code Flow with Proof Key for Code Exchange (PKCE). - /// This opens the user's default browser on desktop or an in-app browser on mobile. - /// If true, Passport will attempt to re-authenticate the player using stored credentials. If re-authentication fails, it won't automatically prompt the user to log in again. - /// Direct login options for authentication (defaults to email method). - /// - /// - /// Returns true if login is successful, otherwise false. - /// - public async UniTask Login(bool useCachedSession = false, DirectLoginOptions directLoginOptions = null) - { - return await GetPassportImpl().Login(useCachedSession, directLoginOptions); - } - - /// - /// Logs the user into Passport using Authorisation Code Flow with Proof Key for Code Exchange (PKCE) and sets up the Immutable X provider. - /// This opens the user's default browser on desktop or an in-app browser on mobile. - /// If true, Passport will attempt to re-authenticate the player using stored credentials. If re-authentication fails, it won't automatically prompt the user to log in again. - /// Direct login options for authentication (defaults to email method). - /// - public async UniTask ConnectImx(bool useCachedSession = false, DirectLoginOptions directLoginOptions = null) - { - return await GetPassportImpl().ConnectImx(useCachedSession, directLoginOptions); - } - - /// - /// Completes the login process by storing tokens received from the Bring Your Own Auth API token exchange endpoint. - /// This method enables authentication using existing auth systems without requiring users to log in twice.q - /// - /// The token request - /// - /// True if successful, otherwise false. - /// - public async UniTask CompleteLogin(TokenResponse request) - { - return await GetPassportImpl().CompleteLogin(request); - } - - /// - /// Gets the wallet address of the logged in user. - /// - /// The wallet address - /// - /// - public async UniTask GetAddress() - { - return await GetPassportImpl().GetAddress(); - } - - /// - /// Logs the user out of Passport and removes any stored credentials. - /// - /// If false, the user will not be logged out of Passport in the browser. The default is true. - public async UniTask Logout(bool hardLogout = true) - { - await GetPassportImpl().Logout(hardLogout); - } - - /// - /// Checks if credentials exist but does not check if they're valid - /// - /// True if there are credentials saved - /// - /// - public UniTask HasCredentialsSaved() - { - return GetPassportImpl().HasCredentialsSaved(); - } - - /// - /// Checks if the user is registered off-chain - /// - /// True if the user is registered with Immutable X, false otherwise - /// - /// - public async UniTask IsRegisteredOffchain() - { - return await GetPassportImpl().IsRegisteredOffchain(); - } - - /// - /// Registers the user to Immutable X if they are not already registered - /// - public async UniTask RegisterOffchain() - { - return await GetPassportImpl().RegisterOffchain(); - } - - /// - /// Retrieves the email address of the user whose credentials are currently stored. - /// - /// The email address - /// - /// - public async UniTask GetEmail() - { - return await GetPassportImpl().GetEmail(); - } - - /// - /// Retrieves the Passport ID of the user whose credentials are currently stored. - /// - /// The Passport ID - /// - /// - public async UniTask GetPassportId() - { - return await GetPassportImpl().GetPassportId(); - } - - /// - /// Gets the currently saved access token without verifying its validity. - /// - /// The access token - /// - /// - public async UniTask GetAccessToken() - { - return await GetPassportImpl().GetAccessToken(); - } - - /// - /// Gets the currently saved ID token without verifying its validity. - /// - /// The ID token - /// - /// - public async UniTask GetIdToken() - { - return await GetPassportImpl().GetIdToken(); - } - - /// - /// Gets the list of external wallets the user has linked to their Passport account via the - /// Dashboard. - /// - /// Linked addresses - /// - /// - public async UniTask> GetLinkedAddresses() - { - return await GetPassportImpl().GetLinkedAddresses(); - } - - /// - /// Create a new transfer request with the given unsigned transfer request. - /// - /// The transfer response if successful - /// - /// - public async UniTask ImxTransfer(UnsignedTransferRequest request) - { - return await GetPassportImpl().ImxTransfer(request); - } - - /// - /// Create a new batch nft transfer request with the given transfer details. - /// - /// The transfer response if successful - /// - /// - public async UniTask ImxBatchNftTransfer(NftTransferDetails[] details) - { - return await GetPassportImpl().ImxBatchNftTransfer(details); - } - - /// - /// Instantiates the zkEVM provider - /// - /// - public async UniTask ConnectEvm() - { - await GetPassportImpl().ConnectEvm(); - } - - /// - /// Sends a transaction to the network and signs it using the logged-in Passport account. - /// - /// The transaction hash, or the zero hash if the transaction is not yet available. - /// - /// - public async UniTask ZkEvmSendTransaction(TransactionRequest request) - { - return await GetPassportImpl().ZkEvmSendTransaction(request); - } - - /// - /// Similar to ZkEvmSendTransaction. Sends a transaction to the network, signs it using the logged-in Passport account, and waits for the transaction to be included in a block. - /// - /// The receipt of the transaction or null if it is still processing. - /// - /// - public async UniTask ZkEvmSendTransactionWithConfirmation(TransactionRequest request) - { - return await GetPassportImpl().ZkEvmSendTransactionWithConfirmation(request); - } - - /// - /// Retrieves the transaction information of a given transaction hash. This function uses the Ethereum JSON-RPC eth_getTransactionReceipt method. - /// - /// The receipt of the transaction or null if it is still processing. - /// - /// - public async UniTask ZkEvmGetTransactionReceipt(string hash) - { - return await GetPassportImpl().ZkEvmGetTransactionReceipt(hash); - } - - /// - /// Signs the EIP-712 structured message in JSON string format using the logged-in Passport account. - /// See EIP-712. - /// The EIP-712 structured data in JSON string format - /// - /// The signed payload string. - /// - /// - public async UniTask ZkEvmSignTypedDataV4(string payload) - { - return await GetPassportImpl().ZkEvmSignTypedDataV4(payload); - } - - /// - /// Returns a list of addresses owned by the user - /// - /// Addresses owned by the user - /// - /// - public async UniTask> ZkEvmRequestAccounts() - { - return await GetPassportImpl().ZkEvmRequestAccounts(); - } - - /// - /// Returns the balance of the account of given address. - /// - /// Address to check for balance - /// Integer block number, or the string "latest", "earliest" or "pending" - /// - /// The balance in wei - /// - public async UniTask ZkEvmGetBalance(string address, string blockNumberOrTag = "latest") - { - return await GetPassportImpl().ZkEvmGetBalance(address, blockNumberOrTag); - } - -#if (UNITY_IPHONE && !UNITY_EDITOR) || (UNITY_ANDROID && !UNITY_EDITOR) - /// - /// Clears the underlying WebView resource cache - /// Android: Note that the cache is per-application, so this will clear the cache for all WebViews used. - /// if false, only the RAM/in-memory cache is cleared - /// - /// - public void ClearCache(bool includeDiskFiles) - { - GetPassportImpl().ClearCache(includeDiskFiles); - } - - /// - /// Clears all the underlying WebView storage currently being used by the JavaScript storage APIs. - /// This includes Web SQL Database and the HTML5 Web Storage APIs. - /// - /// - public void ClearStorage() - { - GetPassportImpl().ClearStorage(); - } -#endif - -#if !IMMUTABLE_CUSTOM_BROWSER && (UNITY_STANDALONE_WIN || (UNITY_ANDROID && UNITY_EDITOR_WIN) || (UNITY_IPHONE && UNITY_EDITOR_WIN)) - /// - /// Updates the log severity for the default Windows browser based on the current SDK log level. - /// - private static void SetDefaultWindowsBrowserLogLevel() - { - if (Instance?._webBrowserClient is WebBrowserClient browserClient) - { - browserClient.logSeverity = _logLevel switch - { - LogLevel.Debug => LogSeverity.Debug, - LogLevel.Warn => LogSeverity.Warn, - LogLevel.Error => LogSeverity.Error, - _ => LogSeverity.Info - }; - } - } - - private static void SetWindowsRedactionHandler() - { - if (Instance?._webBrowserClient is WebBrowserClient browserClient) - { - browserClient.Logger = new DefaultUnityWebBrowserLogger(redactionHandler: _redactTokensInLogs ? RedactTokenValues : null); - } - } -#endif - - /// - /// Redacts access and ID token data from a log message if found. - /// - private static string RedactTokenValues(string message) - { - try - { - var match = Regex.Match(message, @"({.*})"); - if (match.Success) - { - var jsonPart = match.Groups[1].Value; - var response = JsonUtility.FromJson(jsonPart); - if (response?.responseFor is PassportFunction.GET_ACCESS_TOKEN or PassportFunction.GET_ID_TOKEN && !string.IsNullOrEmpty(response.result)) - { - response.result = "[REDACTED]"; - return message.Replace(jsonPart, JsonUtility.ToJson(response)); - } - } - } - catch (Exception) - { - // ignored - } - - return message; - } - - private PassportImpl GetPassportImpl() - { - if (_passportImpl != null) - { - return _passportImpl; - } - throw new PassportException("Passport not initialised"); - } - - private void OnDeepLinkActivated(string url) - { - _deeplink = url; - - if (_passportImpl != null) - { - GetPassportImpl().OnDeepLinkActivated(url); - } - } - - private void OnPassportAuthEvent(PassportAuthEvent authEvent) - { - OnAuthEvent?.Invoke(authEvent); - } - - /// - /// Handles clean-up when the application quits - /// - private void OnQuit() - { - PassportLogger.Info($"{TAG} Cleaning up Passport..."); - -#if UNITY_EDITOR - EditorApplication.playModeStateChanged -= OnPlayModeStateChanged; -#endif - - DisposeAll(); - } - -#if UNITY_EDITOR - /// - /// Handles play mode state changes in the editor - /// - /// The current play mode state - private void OnPlayModeStateChanged(PlayModeStateChange state) - { - // Dispose of all resources when exiting play mode - if (state == PlayModeStateChange.ExitingPlayMode) - { - DisposeAll(); - } - } -#endif - - /// - /// Disposes of all resources and unsubscribes from events - /// - private void DisposeAll() - { - // Dispose of the web browser client for Windows only -#if UNITY_STANDALONE_WIN || (UNITY_ANDROID && UNITY_EDITOR_WIN) || (UNITY_IPHONE && UNITY_EDITOR_WIN) - if (_webBrowserClient != null) - { - _webBrowserClient.Dispose(); - _webBrowserClient = null; - } -#endif - - // Unsubscribe from Passport authentication events - // and dispose of the Passport implementation - if (_passportImpl != null) - { - _passportImpl.OnAuthEvent -= OnPassportAuthEvent; - _passportImpl = null; - } - - // Unsubscribe from application quitting event - Application.quitting -= OnQuit; - -#if UNITY_IPHONE || UNITY_STANDALONE_OSX || UNITY_EDITOR_OSX - // Unsubscribe from deep link activation events on iOS and macOS - Application.deepLinkActivated -= OnDeepLinkActivated; -#endif - - // Reset static fields - Instance = null; - _deeplink = null; - _readySignalReceived = false; - } - -#if UNITY_STANDALONE_WIN || (UNITY_ANDROID && UNITY_EDITOR_WIN) || (UNITY_IPHONE && UNITY_EDITOR_WIN) - /// - /// Validates that custom protocols are used for Windows platforms instead of http/https. - /// Windows uses registry-based deep linking which requires custom protocols. - /// - private static void ValidateWindowsProtocols(string redirectUri, string logoutRedirectUri) - { - if (IsHttpProtocol(redirectUri)) - { - throw new PassportException( - $"Invalid redirectUri for Windows: '{redirectUri}'. " + - "Windows requires custom protocols (e.g., 'mygame://callback') instead of http/https. " + - "This is because Windows uses registry-based deep linking that cannot redirect http/https URLs back to your game.", - PassportErrorType.INITALISATION_ERROR); - } - - if (IsHttpProtocol(logoutRedirectUri)) - { - throw new PassportException( - $"Invalid logoutRedirectUri for Windows: '{logoutRedirectUri}'. " + - "Windows requires custom protocols (e.g., 'mygame://logout') instead of http/https. " + - "This is because Windows uses registry-based deep linking that cannot redirect http/https URLs back to your game.", - PassportErrorType.INITALISATION_ERROR); - } - } - - /// - /// Checks if a URI uses http or https protocol. - /// - private static bool IsHttpProtocol(string uri) - { - return uri.StartsWith("http://", StringComparison.OrdinalIgnoreCase) || - uri.StartsWith("https://", StringComparison.OrdinalIgnoreCase); - } -#endif - } -} +using System.Collections.Generic; +using System; +using System.Text.RegularExpressions; +#if UNITY_STANDALONE_WIN || (UNITY_ANDROID && UNITY_EDITOR_WIN) || (UNITY_IPHONE && UNITY_EDITOR_WIN) +#if !IMMUTABLE_CUSTOM_BROWSER +using VoltstroStudios.UnityWebBrowser; +using VoltstroStudios.UnityWebBrowser.Core; +using VoltstroStudios.UnityWebBrowser.Shared; +using VoltstroStudios.UnityWebBrowser.Logging; +#endif +#elif (UNITY_ANDROID && !UNITY_EDITOR_WIN) || (UNITY_IPHONE && !UNITY_EDITOR_WIN) || UNITY_STANDALONE_OSX || UNITY_WEBGL +using Immutable.Browser.Gree; +#endif +using Immutable.Passport.Event; +using Immutable.Browser.Core; +using Immutable.Passport.Model; +using Immutable.Passport.Core; +using Immutable.Passport.Core.Logging; +using Cysharp.Threading.Tasks; +using UnityEngine; +#if UNITY_EDITOR +using UnityEditor; +#endif + +namespace Immutable.Passport +{ + +#if UNITY_STANDALONE_WIN || (UNITY_ANDROID && UNITY_EDITOR_WIN) || (UNITY_IPHONE && UNITY_EDITOR_WIN) + public class Passport : MonoBehaviour +#else + public class Passport +#endif + { + private const string TAG = "[Passport]"; + + public static Passport? Instance { get; private set; } + private PassportImpl? _passportImpl; + public string Environment { get; private set; } + + private IWebBrowserClient? _webBrowserClient; + + // Keeps track of the latest received deeplink + private static string? _deeplink; + private static bool _readySignalReceived; + + /// + /// Passport auth events + /// + /// + public event OnAuthEventDelegate? OnAuthEvent; + + /// + /// Gets or sets the log level for the SDK. + /// + /// + /// The log level determines which messages are recorded based on their severity. + /// + /// The default value is . + /// + /// + /// See for valid log levels and their meanings. + /// + /// + /// Passport.LogLevel = LogLevel.Debug; + /// + /// + public static LogLevel LogLevel + { + get => _logLevel; + set + { + _logLevel = value; + PassportLogger.CurrentLogLevel = _logLevel; + +#if !IMMUTABLE_CUSTOM_BROWSER && (UNITY_STANDALONE_WIN || (UNITY_ANDROID && UNITY_EDITOR_WIN) || (UNITY_IPHONE && UNITY_EDITOR_WIN)) + SetDefaultWindowsBrowserLogLevel(); +#endif + } + } + + private static LogLevel _logLevel = LogLevel.Info; + + /// + /// Determines whether sensitive token values should be redacted from SDK logs. + /// + /// + /// When set to true, access tokens and ID tokens will be replaced with [REDACTED] in log messages to enhance security. + /// This setting is useful for preventing sensitive data from appearing in logs, especially when debugging or sharing logs with others. + /// + /// The default value is false, meaning tokens will be logged in full at appropriate log levels. + /// + /// + /// Passport.RedactTokensInLogs = true; + /// + /// + public static bool RedactTokensInLogs + { + get => _redactTokensInLogs; + set + { + _redactTokensInLogs = value; + PassportLogger.RedactionHandler = value ? RedactTokenValues : null; + +#if !IMMUTABLE_CUSTOM_BROWSER && (UNITY_STANDALONE_WIN || (UNITY_ANDROID && UNITY_EDITOR_WIN) || (UNITY_IPHONE && UNITY_EDITOR_WIN)) + SetWindowsRedactionHandler(); +#endif + } + } + + private static bool _redactTokensInLogs; + + private Passport() + { + // Handle clean-up tasks when the application is quitting +#if UNITY_STANDALONE_WIN || (UNITY_ANDROID && UNITY_EDITOR_WIN) || (UNITY_IPHONE && UNITY_EDITOR_WIN) + Application.quitting += OnQuit; +#elif UNITY_IPHONE || UNITY_STANDALONE_OSX || UNITY_EDITOR_OSX + // Handle deeplinks for iOS and macOS + Application.deepLinkActivated += OnDeepLinkActivated; + + // Check if there is a deep link URL provided on application start + if (!string.IsNullOrEmpty(Application.absoluteURL)) + { + // Handle the deep link if provided during a cold start + OnDeepLinkActivated(Application.absoluteURL); + } +#endif + +#if UNITY_EDITOR + EditorApplication.playModeStateChanged += OnPlayModeStateChanged; +#endif + } + + /// + /// Initialises Passport with the specified parameters. + /// This sets up the Passport instance, configures the web browser, and waits for the ready signal. + /// + /// The client ID + /// The environment to connect to + /// The URL where the browser will redirect after successful authentication. On Windows, this must use a custom protocol (e.g., 'mygame://callback') instead of http/https. + /// The URL where the browser will redirect after logout is complete. On Windows, this must use a custom protocol (e.g., 'mygame://logout') instead of http/https. + /// (Windows only) Timeout duration in milliseconds to wait for the default Windows browser engine to start. + /// (Windows only) Custom Windows browser to use instead of the default browser in the SDK. + public static UniTask Init( + string clientId, + string environment, + string redirectUri, + string logoutRedirectUri +#if UNITY_STANDALONE_WIN || (UNITY_ANDROID && UNITY_EDITOR_WIN) || (UNITY_IPHONE && UNITY_EDITOR_WIN) + , int engineStartupTimeoutMs = 60000, + IWindowsWebBrowserClient? windowsWebBrowserClient = null +#endif + ) + { +#if UNITY_STANDALONE_WIN || (UNITY_ANDROID && UNITY_EDITOR_WIN) || (UNITY_IPHONE && UNITY_EDITOR_WIN) + ValidateWindowsProtocols(redirectUri, logoutRedirectUri); +#endif + + if (Instance == null) + { + PassportLogger.Info($"{TAG} Initialising Passport..."); + +#if UNITY_STANDALONE_WIN || (UNITY_ANDROID && UNITY_EDITOR_WIN) || (UNITY_IPHONE && UNITY_EDITOR_WIN) + var obj = new GameObject("Passport"); + Instance = obj.AddComponent(); + DontDestroyOnLoad(obj); +#else + Instance = new Passport(); +#endif + Instance.Environment = environment; + + // Start initialisation process + return Instance.Initialise( +#if UNITY_STANDALONE_WIN || (UNITY_ANDROID && UNITY_EDITOR_WIN) || (UNITY_IPHONE && UNITY_EDITOR_WIN) + engineStartupTimeoutMs, windowsWebBrowserClient +#endif + ) + .ContinueWith(async () => + { + // Wait for the ready signal + PassportLogger.Info($"{TAG} Waiting for ready signal..."); + await UniTask.WaitUntil(() => _readySignalReceived == true); + }) + .ContinueWith(async () => + { + if (_readySignalReceived) + { + // Initialise Passport with provided parameters + await Instance.GetPassportImpl().Init(clientId, environment, redirectUri, logoutRedirectUri, _deeplink); + return Instance; + } + else + { + PassportLogger.Error($"{TAG} Failed to initialise Passport"); + throw new PassportException("Failed to initialise Passport", PassportErrorType.INITALISATION_ERROR); + } + }); + } + + // Return the existing instance if already initialised + _readySignalReceived = true; + return UniTask.FromResult(Instance); + } + + /// + /// Initialises the appropriate web browser and sets up browser communication. + /// + /// (Windows only) Timeout duration in milliseconds to wait for the default Windows browser engine to start. + /// (Windows only) Custom Windows browser to use instead of the default browser in the SDK. + private async UniTask Initialise( +#if UNITY_STANDALONE_WIN || (UNITY_ANDROID && UNITY_EDITOR_WIN) || (UNITY_IPHONE && UNITY_EDITOR_WIN) + int engineStartupTimeoutMs, IWindowsWebBrowserClient? windowsWebBrowserClient +#endif + ) + { + try + { + // Initialise the web browser client +#if UNITY_STANDALONE_WIN || (UNITY_ANDROID && UNITY_EDITOR_WIN) || (UNITY_IPHONE && UNITY_EDITOR_WIN) + if (windowsWebBrowserClient != null) + { + // Use the provided custom Windows browser client + _webBrowserClient = new WindowsWebBrowserClientAdapter(windowsWebBrowserClient); + await ((WindowsWebBrowserClientAdapter)_webBrowserClient).Init(); + } + else + { +#if IMMUTABLE_CUSTOM_BROWSER + throw new PassportException("When 'IMMUTABLE_CUSTOM_BROWSER' is defined in Scripting Define Symbols, " + + " 'windowsWebBrowserClient' must not be null."); +#else + _webBrowserClient = gameObject.AddComponent(); + await ((UwbWebView)_webBrowserClient).Init(engineStartupTimeoutMs, _redactTokensInLogs, RedactTokenValues); + _readySignalReceived = true; +#endif + } +#elif (UNITY_ANDROID && !UNITY_EDITOR_WIN) || (UNITY_IPHONE && !UNITY_EDITOR_WIN) || UNITY_STANDALONE_OSX || UNITY_WEBGL + // Initialise default browser client for Android, iOS, and macOS + _webBrowserClient = new GreeBrowserClient(); + await UniTask.CompletedTask; +#else + throw new PassportException("Platform not supported"); +#endif + + // Set up browser communication + BrowserCommunicationsManager communicationsManager = new BrowserCommunicationsManager(_webBrowserClient); + +#if UNITY_WEBGL + _readySignalReceived = true; +#else + // Mark ready when browser is initialised and game bridge file is loaded + communicationsManager.OnReady += () => _readySignalReceived = true; +#endif + // Set up Passport implementation + _passportImpl = new PassportImpl(communicationsManager); + // Subscribe to Passport authentication events + _passportImpl.OnAuthEvent += OnPassportAuthEvent; + } + catch (Exception) + { + // Reset everything on error + _readySignalReceived = false; + Instance = null; + throw; + } + } + +#if UNITY_STANDALONE_WIN || (UNITY_ANDROID && UNITY_EDITOR_WIN) || (UNITY_IPHONE && UNITY_EDITOR_WIN) + private void Awake() + { + if (Instance == null) + { + Instance = this; + DontDestroyOnLoad(gameObject); + } + else if (Instance != this) + { + Destroy(gameObject); + } + } +#endif + + /// + /// Sets the timeout time for waiting for each call to respond (in milliseconds). + /// This only applies to functions that use the browser communications manager. + /// + public void SetCallTimeout(int ms) + { + GetPassportImpl().SetCallTimeout(ms); + } + + /// + /// Logs into Passport using Authorisation Code Flow with Proof Key for Code Exchange (PKCE). + /// This opens the user's default browser on desktop or an in-app browser on mobile. + /// If true, Passport will attempt to re-authenticate the player using stored credentials. If re-authentication fails, it won't automatically prompt the user to log in again. + /// Direct login options for authentication (defaults to email method). + /// + /// + /// Returns true if login is successful, otherwise false. + /// + public async UniTask Login(bool useCachedSession = false, DirectLoginOptions directLoginOptions = null) + { + return await GetPassportImpl().Login(useCachedSession, directLoginOptions); + } + + /// + /// Logs the user into Passport using Authorisation Code Flow with Proof Key for Code Exchange (PKCE) and sets up the Immutable X provider. + /// This opens the user's default browser on desktop or an in-app browser on mobile. + /// If true, Passport will attempt to re-authenticate the player using stored credentials. If re-authentication fails, it won't automatically prompt the user to log in again. + /// Direct login options for authentication (defaults to email method). + /// + public async UniTask ConnectImx(bool useCachedSession = false, DirectLoginOptions directLoginOptions = null) + { + return await GetPassportImpl().ConnectImx(useCachedSession, directLoginOptions); + } + + /// + /// Completes the login process by storing tokens received from the Bring Your Own Auth API token exchange endpoint. + /// This method enables authentication using existing auth systems without requiring users to log in twice.q + /// + /// The token request + /// + /// True if successful, otherwise false. + /// + public async UniTask CompleteLogin(TokenResponse request) + { + return await GetPassportImpl().CompleteLogin(request); + } + +#if UNITY_IOS && !UNITY_EDITOR + /// + /// Logs the user into Passport using Apple Sign-in (iOS only). + /// + /// This method: + /// 1. Displays the native Apple Sign-in UI + /// 2. Exchanges Apple tokens with Immutable's Passport backend + /// 3. Completes the login and establishes a Passport session + /// + /// The backend endpoint is automatically determined based on the environment + /// configured during Passport initialization (production, sandbox, or development). + /// + /// + /// Returns true if login is successful, otherwise false. + /// Throws PassportException on failure. + /// Throws OperationCanceledException if user cancels. + /// + public async UniTask LoginWithApple() + { + return await GetPassportImpl().LoginWithApple(); + } +#endif + + /// + /// Gets the wallet address of the logged in user. + /// + /// The wallet address + /// + /// + public async UniTask GetAddress() + { + return await GetPassportImpl().GetAddress(); + } + + /// + /// Logs the user out of Passport and removes any stored credentials. + /// + /// If false, the user will not be logged out of Passport in the browser. The default is true. + public async UniTask Logout(bool hardLogout = true) + { + await GetPassportImpl().Logout(hardLogout); + } + + /// + /// Checks if credentials exist but does not check if they're valid + /// + /// True if there are credentials saved + /// + /// + public UniTask HasCredentialsSaved() + { + return GetPassportImpl().HasCredentialsSaved(); + } + + /// + /// Checks if the user is registered off-chain + /// + /// True if the user is registered with Immutable X, false otherwise + /// + /// + public async UniTask IsRegisteredOffchain() + { + return await GetPassportImpl().IsRegisteredOffchain(); + } + + /// + /// Registers the user to Immutable X if they are not already registered + /// + public async UniTask RegisterOffchain() + { + return await GetPassportImpl().RegisterOffchain(); + } + + /// + /// Retrieves the email address of the user whose credentials are currently stored. + /// + /// The email address + /// + /// + public async UniTask GetEmail() + { + return await GetPassportImpl().GetEmail(); + } + + /// + /// Retrieves the Passport ID of the user whose credentials are currently stored. + /// + /// The Passport ID + /// + /// + public async UniTask GetPassportId() + { + return await GetPassportImpl().GetPassportId(); + } + + /// + /// Gets the currently saved access token without verifying its validity. + /// + /// The access token + /// + /// + public async UniTask GetAccessToken() + { + return await GetPassportImpl().GetAccessToken(); + } + + /// + /// Gets the currently saved ID token without verifying its validity. + /// + /// The ID token + /// + /// + public async UniTask GetIdToken() + { + return await GetPassportImpl().GetIdToken(); + } + + /// + /// Gets the list of external wallets the user has linked to their Passport account via the + /// Dashboard. + /// + /// Linked addresses + /// + /// + public async UniTask> GetLinkedAddresses() + { + return await GetPassportImpl().GetLinkedAddresses(); + } + + /// + /// Create a new transfer request with the given unsigned transfer request. + /// + /// The transfer response if successful + /// + /// + public async UniTask ImxTransfer(UnsignedTransferRequest request) + { + return await GetPassportImpl().ImxTransfer(request); + } + + /// + /// Create a new batch nft transfer request with the given transfer details. + /// + /// The transfer response if successful + /// + /// + public async UniTask ImxBatchNftTransfer(NftTransferDetails[] details) + { + return await GetPassportImpl().ImxBatchNftTransfer(details); + } + + /// + /// Instantiates the zkEVM provider + /// + /// + public async UniTask ConnectEvm() + { + await GetPassportImpl().ConnectEvm(); + } + + /// + /// Sends a transaction to the network and signs it using the logged-in Passport account. + /// + /// The transaction hash, or the zero hash if the transaction is not yet available. + /// + /// + public async UniTask ZkEvmSendTransaction(TransactionRequest request) + { + return await GetPassportImpl().ZkEvmSendTransaction(request); + } + + /// + /// Similar to ZkEvmSendTransaction. Sends a transaction to the network, signs it using the logged-in Passport account, and waits for the transaction to be included in a block. + /// + /// The receipt of the transaction or null if it is still processing. + /// + /// + public async UniTask ZkEvmSendTransactionWithConfirmation(TransactionRequest request) + { + return await GetPassportImpl().ZkEvmSendTransactionWithConfirmation(request); + } + + /// + /// Retrieves the transaction information of a given transaction hash. This function uses the Ethereum JSON-RPC eth_getTransactionReceipt method. + /// + /// The receipt of the transaction or null if it is still processing. + /// + /// + public async UniTask ZkEvmGetTransactionReceipt(string hash) + { + return await GetPassportImpl().ZkEvmGetTransactionReceipt(hash); + } + + /// + /// Signs the EIP-712 structured message in JSON string format using the logged-in Passport account. + /// See EIP-712. + /// The EIP-712 structured data in JSON string format + /// + /// The signed payload string. + /// + /// + public async UniTask ZkEvmSignTypedDataV4(string payload) + { + return await GetPassportImpl().ZkEvmSignTypedDataV4(payload); + } + + /// + /// Returns a list of addresses owned by the user + /// + /// Addresses owned by the user + /// + /// + public async UniTask> ZkEvmRequestAccounts() + { + return await GetPassportImpl().ZkEvmRequestAccounts(); + } + + /// + /// Returns the balance of the account of given address. + /// + /// Address to check for balance + /// Integer block number, or the string "latest", "earliest" or "pending" + /// + /// The balance in wei + /// + public async UniTask ZkEvmGetBalance(string address, string blockNumberOrTag = "latest") + { + return await GetPassportImpl().ZkEvmGetBalance(address, blockNumberOrTag); + } + +#if (UNITY_IPHONE && !UNITY_EDITOR) || (UNITY_ANDROID && !UNITY_EDITOR) + /// + /// Clears the underlying WebView resource cache + /// Android: Note that the cache is per-application, so this will clear the cache for all WebViews used. + /// if false, only the RAM/in-memory cache is cleared + /// + /// + public void ClearCache(bool includeDiskFiles) + { + GetPassportImpl().ClearCache(includeDiskFiles); + } + + /// + /// Clears all the underlying WebView storage currently being used by the JavaScript storage APIs. + /// This includes Web SQL Database and the HTML5 Web Storage APIs. + /// + /// + public void ClearStorage() + { + GetPassportImpl().ClearStorage(); + } +#endif + +#if !IMMUTABLE_CUSTOM_BROWSER && (UNITY_STANDALONE_WIN || (UNITY_ANDROID && UNITY_EDITOR_WIN) || (UNITY_IPHONE && UNITY_EDITOR_WIN)) + /// + /// Updates the log severity for the default Windows browser based on the current SDK log level. + /// + private static void SetDefaultWindowsBrowserLogLevel() + { + if (Instance?._webBrowserClient is WebBrowserClient browserClient) + { + browserClient.logSeverity = _logLevel switch + { + LogLevel.Debug => LogSeverity.Debug, + LogLevel.Warn => LogSeverity.Warn, + LogLevel.Error => LogSeverity.Error, + _ => LogSeverity.Info + }; + } + } + + private static void SetWindowsRedactionHandler() + { + if (Instance?._webBrowserClient is WebBrowserClient browserClient) + { + browserClient.Logger = new DefaultUnityWebBrowserLogger(redactionHandler: _redactTokensInLogs ? RedactTokenValues : null); + } + } +#endif + + /// + /// Redacts access and ID token data from a log message if found. + /// + private static string RedactTokenValues(string message) + { + try + { + var match = Regex.Match(message, @"({.*})"); + if (match.Success) + { + var jsonPart = match.Groups[1].Value; + var response = JsonUtility.FromJson(jsonPart); + if (response?.responseFor is PassportFunction.GET_ACCESS_TOKEN or PassportFunction.GET_ID_TOKEN && !string.IsNullOrEmpty(response.result)) + { + response.result = "[REDACTED]"; + return message.Replace(jsonPart, JsonUtility.ToJson(response)); + } + } + } + catch (Exception) + { + // ignored + } + + return message; + } + + private PassportImpl GetPassportImpl() + { + if (_passportImpl != null) + { + return _passportImpl; + } + throw new PassportException("Passport not initialised"); + } + + private void OnDeepLinkActivated(string url) + { + _deeplink = url; + + if (_passportImpl != null) + { + GetPassportImpl().OnDeepLinkActivated(url); + } + } + + private void OnPassportAuthEvent(PassportAuthEvent authEvent) + { + OnAuthEvent?.Invoke(authEvent); + } + + /// + /// Handles clean-up when the application quits + /// + private void OnQuit() + { + PassportLogger.Info($"{TAG} Cleaning up Passport..."); + +#if UNITY_EDITOR + EditorApplication.playModeStateChanged -= OnPlayModeStateChanged; +#endif + + DisposeAll(); + } + +#if UNITY_EDITOR + /// + /// Handles play mode state changes in the editor + /// + /// The current play mode state + private void OnPlayModeStateChanged(PlayModeStateChange state) + { + // Dispose of all resources when exiting play mode + if (state == PlayModeStateChange.ExitingPlayMode) + { + DisposeAll(); + } + } +#endif + + /// + /// Disposes of all resources and unsubscribes from events + /// + private void DisposeAll() + { + // Dispose of the web browser client for Windows only +#if UNITY_STANDALONE_WIN || (UNITY_ANDROID && UNITY_EDITOR_WIN) || (UNITY_IPHONE && UNITY_EDITOR_WIN) + if (_webBrowserClient != null) + { + _webBrowserClient.Dispose(); + _webBrowserClient = null; + } +#endif + + // Unsubscribe from Passport authentication events + // and dispose of the Passport implementation + if (_passportImpl != null) + { + _passportImpl.OnAuthEvent -= OnPassportAuthEvent; + _passportImpl = null; + } + + // Unsubscribe from application quitting event + Application.quitting -= OnQuit; + +#if UNITY_IPHONE || UNITY_STANDALONE_OSX || UNITY_EDITOR_OSX + // Unsubscribe from deep link activation events on iOS and macOS + Application.deepLinkActivated -= OnDeepLinkActivated; +#endif + + // Reset static fields + Instance = null; + _deeplink = null; + _readySignalReceived = false; + } + +#if UNITY_STANDALONE_WIN || (UNITY_ANDROID && UNITY_EDITOR_WIN) || (UNITY_IPHONE && UNITY_EDITOR_WIN) + /// + /// Validates that custom protocols are used for Windows platforms instead of http/https. + /// Windows uses registry-based deep linking which requires custom protocols. + /// + private static void ValidateWindowsProtocols(string redirectUri, string logoutRedirectUri) + { + if (IsHttpProtocol(redirectUri)) + { + throw new PassportException( + $"Invalid redirectUri for Windows: '{redirectUri}'. " + + "Windows requires custom protocols (e.g., 'mygame://callback') instead of http/https. " + + "This is because Windows uses registry-based deep linking that cannot redirect http/https URLs back to your game.", + PassportErrorType.INITALISATION_ERROR); + } + + if (IsHttpProtocol(logoutRedirectUri)) + { + throw new PassportException( + $"Invalid logoutRedirectUri for Windows: '{logoutRedirectUri}'. " + + "Windows requires custom protocols (e.g., 'mygame://logout') instead of http/https. " + + "This is because Windows uses registry-based deep linking that cannot redirect http/https URLs back to your game.", + PassportErrorType.INITALISATION_ERROR); + } + } + + /// + /// Checks if a URI uses http or https protocol. + /// + private static bool IsHttpProtocol(string uri) + { + return uri.StartsWith("http://", StringComparison.OrdinalIgnoreCase) || + uri.StartsWith("https://", StringComparison.OrdinalIgnoreCase); + } +#endif + } +}