signal-bridge
v1.0.20
Published
Secure iframe communication bridge
Readme
🚀 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
npm install signal-bridgeatau via CDN (UMD):
<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)
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
bridge.init();Mengirim sinyal "ready" ke host.
3. Emit Event ke Host
bridge.emit("login", {
userId: 123,
token: "abc"
});4. Listen Event dari Host
bridge.listen((message) => {
console.log("Received:", message);
if (message.type === "logout") {
// handle logout
}
});5. Akses Global Instance
Jika ingin akses dari mana saja:
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
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
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
reporter.start({
headers?: Record<string, string> // merge ke headers yang sudah ada
requireAuth?: boolean // jika true, flush ditahan sampai ada Authorization
})Tanpa auth (default):
reporter.start()
// flush berjalan terus tanpa cek authDengan requireAuth — token belum tersedia saat init:
// 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 hilangToken tersedia langsung saat start:
reporter.start({
requireAuth: true,
headers: { Authorization: `Bearer ${token}` },
})
start()bersifat idempotent — interval tidak di-restart jika dipanggil ulang, hanyaheadersyang di-merge.
Penggunaan
// 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
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)
<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
allowedHostOrigins: ["https://host.com"]Hanya origin ini yang bisa kirim message ke iframe.
✅ Encryption (Optional)
cryptoKey: "SECRET_KEY"Payload akan otomatis:
- Encrypt saat
emit - Decrypt saat
receive
📡 Message Format
type BridgeMessage = {
type: string;
payload: any;
txn: string;
__source: "host" | "remote";
__origin?: string;
};🌍 Browser (UMD) Usage
<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)
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
cryptoKeydi-set
🛠️ Available Exports
// 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 (
__sourcecheck) - 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:
<script src="https://cdn.jsdelivr.net/npm/signal-bridge/dist/index.global.js"></script>Atau via NPM (Laravel Vite):
npm install signal-bridgeSetup Dasar
resources/views/remote.blade.php
<!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:
{ "app_id": "my-laravel-app", "type": "navigate", "payload": { "path": "/u/dashboard" }, "txn": "", "__origin": "https://your-laravel-app.com", "__source": "remote" }
Route:
// routes/web.php
Route::get('/remote', function () {
return view('remote');
});Menggunakan NPM + Vite
resources/js/bridge.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.comresources/views/remote.blade.php
<!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
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
allowedHostOriginssecara eksplisit, jangan pakai wildcard. txnadalah key dekripsi per-sesi yang dikirim Host — jangan simpan permanen.- Konfigurasi
Content-Security-Policy: frame-ancestorsagar hanya Host yang diizinkan bisa embed halaman Laravel:
// Middleware atau kernel
$response->headers->set(
'Content-Security-Policy',
"frame-ancestors 'self' https://host-app.example.com"
);Catatan Penting untuk Laravel
- Selalu set
allowedHostOriginssecara eksplisit, jangan gunakan wildcard. - Simpan
BRIDGE_KEYdi.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-ancestorsdikonfigurasi benar di Laravel agar hanya host yang diizinkan yang bisa embed halaman Anda.
Contoh CSP di app/Http/Middleware/:
// Di middleware atau kernel
$response->headers->set(
'Content-Security-Policy',
"frame-ancestors 'self' https://host-app.example.com"
);📜 License
MIT License © 2026 - EkaHersada
