# 🚀 Signal Bridge

Lightweight communication bridge berbasis `window.postMessage` untuk komunikasi **Host ↔ Iframe (Remote App)** dengan fitur:

* 🔐 Optional encryption (AES)
* 🛡️ Origin validation (security)
* 🔁 Listener & emitter sederhana
* ♻️ Singleton instance (init hanya sekali)
* 🌐 Support UMD (browser global) + npm package
* 📊 Activity Reporter — batch tracking ke API eksternal

---

## 📦 Installation

```bash
npm install signal-bridge
```

atau via CDN (UMD):

```html
<script src="signal-bridge.umd.js"></script>
```

---

## ⚙️ Konsep Dasar

Bridge ini digunakan untuk komunikasi antara:

* **Host App** (parent window)
* **Remote App** (iframe)

⚠️ Library ini **hanya berjalan di dalam iframe (remote)**.

---

## 🧠 API Overview

### 1. Create Bridge (Singleton)

```ts
import { createBridge } from "signal-bridge";

const bridge = createBridge("my-app-id", {
  allowedHostOrigins: ["https://host.com"],
  cryptoKey: "SECRET_KEY", // optional
  debug: true
})
```

> Hanya akan dibuat **1x (singleton)**. Jika dipanggil ulang, akan reuse instance lama.

---

### 2. Init Bridge

```ts
bridge.init();
```

Mengirim sinyal `"ready"` ke host.

---

### 3. Emit Event ke Host

```ts
bridge.emit("login", {
  userId: 123,
  token: "abc"
});
```

---

### 4. Listen Event dari Host

```ts
bridge.listen((message) => {
  console.log("Received:", message);

  if (message.type === "logout") {
    // handle logout
  }
});
```

---

### 5. Akses Global Instance

Jika ingin akses dari mana saja:

```ts
import { signalBridge } from "signal-bridge";

signalBridge().emit("ping", {});


Auto Encrypted Data:
signalBridge().emitEncrypted("event-name", {id:123});
```

⚠️ Pastikan sudah `createBridge().init()` sebelumnya.

---

### 6. Reset Bridge

```ts
import { resetBridge } from "signal-bridge";

resetBridge();
```

---

## 📊 Activity Reporter

Kirim activity log ke API domain lain secara batch. Otomatis flush saat queue penuh (20 item) atau setiap 20 detik.

> Independen dari Signal Bridge — bisa dipakai tanpa `createBridge`.

### Setup

```ts
import { activity } from "@unictive/bridge-familia";

const reporter = activity({
  endpoint: "https://api.domain.com/activities/batch",
  headers: { Authorization: "Bearer <token>" }, // opsional di sini
  // batchSize: 20,   // default
  // interval: 20000, // default (ms)
});
```

### Methods

| Method | Deskripsi |
|---|---|
| `start(opts?)` | Mulai interval flush (20 detik) |
| `track(code, payload)` | Catat activity ke queue lokal |
| `flush()` | Kirim queue ke API sekarang (manual, opsional) |
| `stop()` | Hentikan interval |
| `destroy()` | Stop + clear queue |

### `start()` Options

```ts
reporter.start({
  headers?: Record<string, string>  // merge ke headers yang sudah ada
  requireAuth?: boolean             // jika true, flush ditahan sampai ada Authorization
})
```

**Tanpa auth (default):**

```ts
reporter.start()
// flush berjalan terus tanpa cek auth
```

**Dengan `requireAuth` — token belum tersedia saat init:**

```ts
// Init awal (misal di app entry, sebelum login)
const reporter = activity({ endpoint: "https://api.domain.com/activities/batch" })
reporter.start({ requireAuth: true })

// track() tetap berjalan & queue tersimpan, flush ditahan

// Setelah user login & token tersedia:
reporter.start({ headers: { Authorization: `Bearer ${token}` } })
// headers ter-merge, flush mulai berjalan — tidak ada data hilang
```

**Token tersedia langsung saat start:**

```ts
reporter.start({
  requireAuth: true,
  headers: { Authorization: `Bearer ${token}` },
})
```

> `start()` bersifat idempotent — interval tidak di-restart jika dipanggil ulang, hanya `headers` yang di-merge.

### Penggunaan

```ts
// Setiap track() membutuhkan field `status`
reporter.track("la_login",  { status: "SUCCESS" })
reporter.track("la_session",{ status: "TIMEOUT" })

// Data Activity — sertakan object_type & before/after sesuai kode
reporter.track("da_create", {
  status:      "SUCCESS",
  object_type: "MASTER",
  after_value: { id: 42, name: "Dokumen Baru" },
})

reporter.track("da_update", {
  status:       "SUCCESS",
  object_type:  "TRANSACTION",
  before_value: { status: "PENDING" },
  after_value:  { status: "APPROVED" },
})

// Flush manual saat user logout / tab ditutup
window.addEventListener("beforeunload", () => reporter.flush())
```

### Activity Codes

| Kategori | Kode | Status yang diizinkan |
|---|---|---|
| **LA** Login & Auth | `la_login` `la_logout` `la_pwd` `la_session` `la_mfa` | SUCCESS, FAILED *(session: TIMEOUT)* |
| **DA** Data Activity | `da_view` `da_create` `da_update` `da_delete` `da_submit` `da_approve` `da_reject` `da_return` `da_cancel` `da_close` `da_reopen` `da_print` `da_preview` | SUCCESS, FAILED |
| **IE** Import & Export | `ie_import` `ie_export` `ie_template` | SUCCESS, FAILED *(import: +PARTIAL)* |
| **FM** File Management | `fm_upload` `fm_download` `fm_replace` `fm_delete` `fm_preview` | SUCCESS, FAILED |
| **AP** API Integration | `ap_request` `ap_response` | SUCCESS, FAILED *(request: +TIMEOUT)* |
| **ER** Error Log | `er_log` | ERROR |
| **SE** Security Event | `se_detect` | DETECTED |
| **RP** Reporting | `rp_generate` `rp_export` `rp_print` `rp_download` | SUCCESS, FAILED |

### Payload yang dikirim ke API

```json
POST /activities/batch

{
  "activities": [
    { "event": "la_login",  "payload": { "status": "SUCCESS" }, "timestamp": 1719600001000 },
    { "event": "da_create", "payload": { "status": "SUCCESS", "object_type": "MASTER" }, "timestamp": 1719600005000 }
  ]
}
```

### UMD (Browser Global)

```html
<script src="bridge-familia.umd.js"></script>
<script>
  const reporter = window.signalBridge.activity({
    endpoint: "https://api.domain.com/activities/batch",
    headers: { Authorization: "Bearer <token>" },
  });

  reporter.start();
  reporter.track("la_login", { status: "SUCCESS" });
</script>
```

---

## 🔐 Security Features

### ✅ Origin Validation

```ts
allowedHostOrigins: ["https://host.com"]
```

Hanya origin ini yang bisa kirim message ke iframe.

---

### ✅ Encryption (Optional)

```ts
cryptoKey: "SECRET_KEY"
```

Payload akan otomatis:

* Encrypt saat `emit`
* Decrypt saat `receive`

---

## 📡 Message Format

```ts
type BridgeMessage = {
  type: string;
  payload: any;
  txn: string;
  __source: "host" | "remote";
  __origin?: string;
};
```

---

## 🌍 Browser (UMD) Usage

```html
<script>
  const bridge = window.SignalBridge.createBridge("app-id", {
    allowedHostOrigins: ["https://host.com"]
  });

  bridge.init();

  bridge.listen((msg) => {
    console.log(msg);
  });

  bridge.emit("hello", { foo: "bar" });
</script>
```

---

## 🧪 Example Flow

### Remote (Iframe)

```ts
const bridge = createBridge("child-app", {
  allowedHostOrigins: ["https://parent.com"]
});

bridge.init();

bridge.listen((msg) => {
  if (msg.type === "user-data") {
    console.log("User:", msg.payload);
  }
});

bridge.emit("ready", { status: "ok" });
```

---

## ⚠️ Important Notes

* ❗ Harus dijalankan di dalam iframe
* ❗ `init()` hanya akan trigger sekali (anti duplicate)
* ❗ Pastikan origin host sesuai (tidak akan menerima jika tidak match)
* ❗ Encryption hanya aktif jika `cryptoKey` di-set

---

## 🛠️ Available Exports

```ts
// Signal Bridge
createBridge
initBridge
signalBridge
resetBridge

// Activity Reporter
activity
ACTIVITY_META     // metadata lengkap per activity code
StartOptions      // type untuk parameter start()

// Crypto
encrypt
decrypt
generateKey

// Types
ActivityCode
ActivityStatus
ActivityObjectType
ActivityPayload
```

---

## 🧩 Internal Behavior

* Singleton global instance (`globalBridge`)
* Anti loop message (`__source` check)
* Auto ignore invalid message
* Optional debug logger

---

## 📄 Source

Implementasi utama bisa dilihat di file:



---

## 🧑‍💻 Author Notes

Dirancang untuk kebutuhan komunikasi microfrontend / iframe integration dengan fokus:

* Simple API
* Secure by default
* Minimal dependency

---

---

## 🐘 Tutorial: Integrasi dengan Laravel (sebagai Remote)

Posisi Laravel di sini adalah **Remote** — halaman Laravel berjalan di dalam iframe yang di-embed oleh aplikasi Host lain.

```
[ Host (parent window) ] ---postMessage---> [ Laravel (iframe / remote) ]
```

---

### Instalasi

Via CDN di Blade:

```html
<script src="https://cdn.jsdelivr.net/npm/signal-bridge/dist/index.global.js"></script>
```

Atau via NPM (Laravel Vite):

```bash
npm install signal-bridge
```

---

### Setup Dasar

**`resources/views/remote.blade.php`**

```html
<!DOCTYPE html>
<html>
<head>
    <title>Remote App</title>
    <script src="https://cdn.jsdelivr.net/npm/signal-bridge/dist/index.global.js"></script>
</head>
<body>
    <div id="app"></div>

    <script>
        const bridge = window.signalBridge.initBridge('my-laravel-app', {
            allowedHostOrigins: ['https://host-app.example.com'],
            debug: true,
        });

        bridge.listen(function (message) {

            /**
             * Handshake dari host.
             * Payload yang diterima:
             * {
             *   "type": "connection",
             *   "payload": {
             *     "token": "xxxx",
             *     "txn": "2mFMfAtLD",
             *     "idChangeRole": null,
             *     "ticket": "ST-1780470879-VGVVGUVY0L",
             *     "path": "/dashboard"
             *   },
             *   "__source": "host",
             *   "__origin": "https://host-app.example.com"
             * }
             */
            if (message.type === 'connection') {
                const { token, txn, ticket, path, idChangeRole } = message.payload;

                // Dekripsi token menggunakan txn sebagai key per-sesi
                const decryptedToken = window.signalBridge.decrypt(token, txn);

                console.log('Decrypted Token:', decryptedToken);
                console.log('Ticket:', ticket);
                console.log('Path:', path);
                console.log('idChangeRole:', idChangeRole);

                // Simpan token ke state / localStorage sesuai kebutuhan
                // Arahkan ke path yang dikirim host
                if (path && path !== window.location.pathname) {
                    window.location.href = path;
                }
            }

            // Host meminta ganti role
            if (message.type === 'change-role') {
                const { data, txn } = message.payload;
                const role = window.signalBridge.decrypt(data, txn);

                console.log('Decrypted Role selected:', role);
                // Lakukan reload atau update state sesuai role baru
            }

        });

        // Emit navigate saat halaman pertama kali dimuat
        bridge.emit('navigate', { path: window.location.pathname });

        // Emit navigate setiap perubahan URL (back/forward browser)
        window.addEventListener('popstate', function () {
            bridge.emit('navigate', { path: window.location.pathname });
        });

        // Emit navigate setiap perubahan hash (untuk hash-based routing)
        window.addEventListener('hashchange', function () {
            const path = location.hash.startsWith('#/')
                ? location.hash.slice(1)
                : location.pathname;
            bridge.emit('navigate', { path });
        });
    </script>
</body>
</html>
```

> Pesan yang dikirim ke host akan berbentuk:
> ```json
> {
>   "app_id": "my-laravel-app",
>   "type": "navigate",
>   "payload": { "path": "/u/dashboard" },
>   "txn": "",
>   "__origin": "https://your-laravel-app.com",
>   "__source": "remote"
> }
> ```

**Route:**

```php
// routes/web.php
Route::get('/remote', function () {
    return view('remote');
});
```

---

### Menggunakan NPM + Vite

**`resources/js/bridge.js`**

```js
import { initBridge } from 'signal-bridge';

const bridge = initBridge('my-laravel-app', {
    allowedHostOrigins: [import.meta.env.VITE_HOST_ORIGIN],
    debug: import.meta.env.DEV,
});

bridge.listen((message) => {

    if (message.type === 'connection') {
        const { token, txn, ticket, path, idChangeRole } = message.payload;

        const decryptedToken = window.signalBridge.decrypt(token, txn);

        console.log('Decrypted Token:', decryptedToken);
        console.log('Ticket:', ticket);
        console.log('Path:', path);
        console.log('idChangeRole:', idChangeRole);
    }

    if (message.type === 'change-role') {
        const { data, txn } = message.payload;
        const role = window.signalBridge.decrypt(data, txn);

        console.log('Decrypted Role selected:', role);
    }

});

// Emit navigate saat halaman pertama kali dimuat
bridge.emit('navigate', { path: window.location.pathname });

// Untuk Inertia.js — hook ke router event
// import { router } from '@inertiajs/vue3';
// router.on('navigate', () => bridge.emit('navigate', { path: window.location.pathname }));

export { bridge };
```

**`.env`**

```
VITE_HOST_ORIGIN=https://host-app.example.com
```

**`resources/views/remote.blade.php`**

```html
<!DOCTYPE html>
<html>
<head>
    @vite(['resources/js/app.js', 'resources/js/bridge.js'])
</head>
<body>
    <div id="app"></div>
</body>
</html>
```

---

### Akses Instance dari File Lain

```js
import { signalBridge } from 'signal-bridge';

// Ambil instance yang sudah dibuat tanpa init ulang
const bridge = signalBridge();
bridge.emit('form-submit', { id: 123 });
```

---

### Catatan Keamanan

- Selalu set `allowedHostOrigins` secara eksplisit, jangan pakai wildcard.
- `txn` adalah key dekripsi per-sesi yang dikirim Host — jangan simpan permanen.
- Konfigurasi `Content-Security-Policy: frame-ancestors` agar hanya Host yang diizinkan bisa embed halaman Laravel:

```php
// Middleware atau kernel
$response->headers->set(
    'Content-Security-Policy',
    "frame-ancestors 'self' https://host-app.example.com"
);
```

---

### Catatan Penting untuk Laravel

- Selalu set `allowedHostOrigins` secara eksplisit, jangan gunakan wildcard.
- Simpan `BRIDGE_KEY` di `.env`, bukan hardcode di Blade atau JS.
- `config('app.bridge_key')` yang di-render ke Blade tetap tampak di source HTML — hanya gunakan untuk data non-kritis.
- Untuk transfer token/kredensial sensitif, tetap gunakan endpoint API Laravel dengan autentikasi Sanctum/session, bukan lewat bridge.
- Pastikan header `X-Frame-Options` / `Content-Security-Policy: frame-ancestors` dikonfigurasi benar di Laravel agar hanya host yang diizinkan yang bisa embed halaman Anda.

**Contoh CSP di `app/Http/Middleware/`:**

```php
// Di middleware atau kernel
$response->headers->set(
    'Content-Security-Policy',
    "frame-ancestors 'self' https://host-app.example.com"
);
```

---

## 📜 License

MIT License © 2026 - EkaHersada  

---
