Custom UI sayfan iframe içinde açıldığında, hangi tenant'ın baktığını App Bridge'den aldığın session token ile öğrenirsin. Token'ı backend'inde webhookSecret ile doğrularsın; böylece iframe'den gelen istekler güvenle tenant'a bağlanır.
iframe sayfan (frontend)
│ const { data } = await bridgeCall('getSessionToken'); // App Bridge (postMessage)
│ // data = { token, tokenType: "Bearer", expiresIn: 120 } — TTL 120 sn, önceden alıp bekletme
│ fetch('/api/me', { headers: { Authorization: 'Bearer ' + data.token } })
▼
Senin backend'in
1) token'ı webhookSecret ile doğrula (JWT HS256) — audience = pluginId ZORUNLU
2) claims: { iss, aud, sub, role, tenantId, pluginId, environment, iat, exp }
tenant = claims.tenantId · userId = claims.sub · role = claims.role (manager|staff)
environment = claims.environment ("sandbox" | "production") ◄── HANGİ kurulum
3) o tenant + kullanıcı için veriyi güvenle döndürFrontend yalnız token'ı taşır; doğrulama backend'de.
bridgeCall bir SDK fonksiyonu değildir — iframe sayfanda sen yazarsın. Panelin tanıdığı tek mesaj şekli aşağıdadır; başka bir zarf (örn. { source, payload }) panel tarafından tanınmaz ve çağrın sessizce yanıtsız kalır.
| Yön | Mesaj şekli |
|---|---|
| istek iframe → panel | { type: "restomenum-bridge", requestId, action, params } |
| yanıt panel → iframe | { type: "restomenum-bridge-response", requestId, result } result = { success, data?, message? } |
// ── App Bridge TEL PROTOKOLÜ (panelin tanıdığı TEK mesaj şekli) ──
// istek : { type: "restomenum-bridge", requestId, action, params }
// yanıt : { type: "restomenum-bridge-response", requestId, result }
// result = { success, data?, message? }
// Başka bir şekil (örn. { source, payload }) panel tarafından TANINMAZ — sessizce yanıtsız kalırsın.
// Panel origin'i: sabit origin'ler VEYA offline/yerel kurulumda localhost/127.0.0.1 (DEĞİŞKEN port).
// ⚠️ postMessage targetOrigin JOKER KABUL ETMEZ ('https://localhost:*' → SyntaxError) ve üst pencerenin
// origin'i document.referrer'dan ÖĞRENİLEMEZ (iframe no-referrer ile yüklenir). Yerel panelde hedef
// ?panelOrigin= parametresinden okunur ve DOĞRULANIR (bkz. /docs/iframe-security).
const PANEL_ORIGINS = ["https://app.restomenum.com","https://test-restomenu.web.app"];
const LOCAL_PANEL = /^https?:\/\/(localhost|127\.0\.0\.1)(:\d{1,5})?$/;
const isPanelOrigin = (o) => PANEL_ORIGINS.includes(o) || LOCAL_PANEL.test(o); // SDK: isPanelOrigin
// Doğrulanmış hedef (yoksa null → sabit origin'lere pinli gönderim). SDK: readPanelOrigin()
const rawPanelOrigin = new URLSearchParams(location.search).get("panelOrigin");
let panelOrigin = rawPanelOrigin && isPanelOrigin(rawPanelOrigin) ? rawPanelOrigin : null;
function bridgeCall(action, params) {
return new Promise((res) => {
const requestId = Math.random().toString(36).slice(2);
function onMsg(e) {
if (!isPanelOrigin(e.origin)) return; // gelen origin doğrula (includes() YETMEZ — yerel port)
const d = e.data;
if (!d || d.type !== "restomenum-bridge-response" || d.requestId !== requestId) return;
panelOrigin ??= e.origin; // öğrenilen origin'i pinle (sonraki çağrılar buna gider)
window.removeEventListener("message", onMsg);
res(d.result); // { success, data?, message? }
}
window.addEventListener("message", onMsg);
const msg = { type: "restomenum-bridge", requestId, action, params };
// Hedef origin-pinli ('*' YASAK). targetOrigin eşleşmeyen gönderim teslim EDİLMEZ.
if (panelOrigin) window.parent.postMessage(msg, panelOrigin);
else PANEL_ORIGINS.forEach((origin) => window.parent.postMessage(msg, origin));
});
}
// ⚠️ TIMEOUT KOYMA: openUrl / resolve / close kullanıcı etkileşimi bekler (panel onay dialogu
// gösterir). Kısa bir timeout koyarsan kullanıcı onaylamadan "timeout" alırsın.Aynı köprü tüm action'lar için geçerlidir (getContext, getSessionToken, resolve, close, resize, toast, openUrl) — gate akışındaki kullanımı iframe App Bridge'de.
openUrl, resolve ve close panelde kullanıcıya bir onay dialogu gösterir; yanıt kullanıcı karar verene kadar gelmez. Bu "timeout'u uzun tut" demek değildir — kullanıcının ne zaman karar vereceği bilinemez, o yüzden timeout kullanma. requestId'yi eşleştir, gelen e.origin'i doğrula, hedefi pinle ('*' yasak).@restomenum/plugin-sdk verifySessionToken tüm kontrolleri (HS256, aud=pluginId, iss=restomenum, HMAC timing-safe, exp zorunlu, iat ileri-tarih reddi) tek çağrıda yapar — imza/kripto kendin yazma:
import { verifySessionToken, SessionError } from '@restomenum/plugin-sdk';
// GET /api/me — iframe'in Bearer token'ını doğrula:
try {
const claims = await verifySessionToken(req.headers.authorization, {
pluginId, // aud bununla eşleşmeli
// Kurulumu tenantId + environment ile ara — aynı tenant iki ortamda kurulu olabilir,
// secret'lar ayrıdır (yalnız tenantId ile ararsan yanlış secret'ı seçebilirsin).
getSecret: (tenantId, environment) => installStore.find(tenantId, environment)?.webhookSecret,
});
// claims.tenantId · claims.environment · claims.sub (userId) · claims.role ('manager' | 'staff')
} catch (e) {
if (e instanceof SessionError) res.status(401).json({ error: e.reason });
}/ui (App Bridge) + /api/me (verifySessionToken) uçları.// iframe Custom UI — App Bridge session token doğrulama (Node)
// Sayfan Restomenum içinde iframe olarak açılır; tenant kimliğini App Bridge'den alırsın:
// const { data } = await bridgeCall('getSessionToken'); // App Bridge → { token, tokenType:"Bearer", expiresIn:120 }
// fetch('/api/me', { headers: { Authorization: 'Bearer ' + data.token } });
// Backend'inde token'ı webhookSecret ile doğrula (JWT HS256) — veya resmi SDK: verifySessionToken (önerilen):
import jwt from 'jsonwebtoken';
function verifySessionToken(bearer, webhookSecret, pluginId) {
const token = bearer.replace(/^Bearer /, '');
// HS256, webhookSecret ile imzalı. Claim'ler (canlı token'dan teyitli):
// { iss, aud, sub, role, tenantId, pluginId, environment, iat, exp }
// sub = userId (iframe'i açan kullanıcı) role = manager | staff
// environment = "sandbox" | "production" → HANGİ kurulum (secret'ı bununla çöz)
// audience (= pluginId) doğrulaması ZORUNLU — başka eklentinin token'ı kabul edilmesin:
const claims = jwt.verify(token, webhookSecret, { algorithms: ['HS256'], audience: pluginId });
// per-user yetki: claims.role / claims.sub ile kur (ad/PII için users/get — users:read + consent)
return {
tenantId: claims.tenantId,
environment: claims.environment,
userId: claims.sub,
role: claims.role,
exp: claims.exp,
};
}
// ⚠️ webhookSecret'ı ÇÖZERKEN ortamı da kullan: aynı tenantId hem sandbox hem production'da kurulu
// olabilir ve secret'lar AYRIDIR. Doğru sıra: token'ı decode et (imzasız) → tenantId+environment
// ile kurulumu bul → o kurulumun secret'ıyla imzayı DOĞRULA.
// Resmi SDK verifySessionToken bunu yapar: getSecret(tenantId, environment).webhookSecret'ıdır (token exchange'ten).audience = pluginId doğrulaması ZORUNLU (jwt.verify(…, { audience: pluginId }') — başka eklentinin token'ı kabul edilmesin. exp'in geçmediğini de doğrula.sub = userId (iframe'i açan kullanıcı), role = manager | staff → per-user yetkiyi bununla kur (ad/PII için users/get, users:read + consent).environment (sandbox | production) — hangi kurulumun token'ı olduğunu söyler. Aynı tenantId iki ortamda birden kurulu olabilir ve her kurulumun webhookSecret'ı ayrıdır → imza anahtarını tenantId + environment ile çöz. Ayrıntı: Ortamlar./plugins/ui, bridge, action, hook) tüm tenant kullanıcılarına açıktır (yalnız eklenti yönetimi manager-only). Belirli işlemleri kısıtlamak senin işin — gövdedeki/token'daki actor.role ile uygula (örn. "iadeyi yalnız manager yapabilir").https://app.restomenum.com + https://test-restomenu.web.app — postMessage yalnız bu origin'lerle yapılmalı. Sayfan mutlaka iframe güvenliği kurallarına uymalı (CSP frame-ancestors + origin-pinli postMessage).