npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@restomenum/plugin-sdk

v1.11.0

Published

Restomenum eklenti platformu için resmi geliştirici SDK'sı — webhook imza doğrulama, OAuth token exchange, Callback API istemcisi, event/scope katalogu, tipler.

Readme

@restomenum/plugin-sdk

Restomenum eklenti platformu için resmi TypeScript geliştirici SDK'sı. Webhook imza doğrulama, OAuth token exchange, tipli Callback API istemcisi, event/scope katalogu ve tipler — tek pakette.

Node 20+ (global fetch). Sıfır runtime bağımlılığı (yalnız node:crypto).

Kurulum

npm install @restomenum/plugin-sdk

Webhook imza doğrulama

İmza ham gövde üzerinden doğrulanır (JSON.parse edilmiş değil), ±5 dk replay penceresi, timing-safe.

import { verifyWebhookSignature } from '@restomenum/plugin-sdk';

// Express (ham gövdeyi sakla: app.use(express.json({ verify: (req, _res, buf) => { req.rawBody = buf } })))
const ok = verifyWebhookSignature(req.rawBody.toString('utf8'), req.headers['x-restomenum-signature'], webhookSecret);
if (!ok) return res.status(401).json({ error: 'invalid_signature' });

Express middleware (hazır)

import { expressWebhook } from '@restomenum/plugin-sdk';

app.post('/webhook', express.json({ verify: (req, _r, buf) => { (req as any).rawBody = buf; } }),
  expressWebhook(
    { getSecret: (tenantId) => store.webhookSecretFor(tenantId) },   // senkron/async
    async (envelope) => {
      // imza + şekil doğrulandı; envelope = { id, type, tenantId, occurredAt, data }
      if (envelope.type === 'table.created') await handle(envelope.data);
      // yanıt yazmazsan otomatik 200 { ok:true }
    },
  ),
);

OAuth token exchange

import { exchangeCode } from '@restomenum/plugin-sdk';

const cred = await exchangeCode(
  { code, clientId, clientSecret },
  { environment: 'sandbox' }, // veya { baseUrl }
);
// cred = { tenantId, apiKey, webhookSecret, scopes }  → tenant başına SAKLA

Callback API istemcisi (tipli)

import { RestomenumClient } from '@restomenum/plugin-sdk';

const client = new RestomenumClient({ apiKey: cred.apiKey, environment: 'production' });

const packets = await client.packets.open();
const packet  = await client.packets.get('packetId');
const products = await client.products.list();          // { data, truncated?, total? }
const customer = await client.customers.get('custId');  // PII consent kuralı sunucuda uygulanır

// Müşteri listesi — cursor pagination (offset YOK):
for await (const c of client.customers.listAll(200)) {
  // tüm müşteriler, sayfa sayfa otomatik
}

Kaynak grupları: packets, tables, products, categories, paymentMethods, ingredients, users, customers.

Hata yönetimi

REST hata kodları ApiError'a çevrilir; OAuth hataları OAuthError; imza SignatureError.

import { ApiError } from '@restomenum/plugin-sdk';
try {
  await client.packets.get('yok');
} catch (e) {
  if (e instanceof ApiError) {
    console.log(e.status, e.code);          // 404, "plugin.packets.notFound"
    if (e.status === 429) wait(e.retryAfterSec); // Retry-After (sn)
  }
}

Action (buton/hook) yanıtı

import { actionResponse } from '@restomenum/plugin-sdk';
res.json(actionResponse(true, 'İşlem tamam', { level: 'success', display: 'toast' }));

Eklentiler-arası mesajlaşma (messaging.send)

Tenant'ın bağladığı mesaj sağlayıcısı (WhatsApp/SMS eklentisi) üzerinden mesaj gönder — sağlayıcının kimliğini bilmezsin; platform yönlendirir. to tipli birliktir — tam olarak biri: { customerId } (Cari/CRM müşteri) veya { packetId } (paket/teslimat müşterisi, walk-in dahil). Ham telefon YASAK; telefonu sağlayıcı gönderim anında resolveRecipientPhone ile çözer (rehber saklanmaz). idempotencyKey zorunlu; status:"accepted" teslim DEĞİLDİR.

// TÜKETİCİ (scope: messaging:send) — bağlı sağlayıcı üzerinden gönder:
const r = await client.messaging.send({
  to: { customerId },                            // veya { packetId } — teslimat siparişine SMS
  text: 'Rezervasyonunuz onaylandı',
  idempotencyKey: `rez-${rezId}-onay`,          // timeout'ta AYNI key ile retry (çift mesaj koruması)
});
// r = { requestId, status: 'accepted'|'sent'|'failed', providerMessageId?, idempotentReplay? }

// SAĞLAYICI (scope: messaging:provide) — actionUrl'e gelen imzalı type:"capability" POST'u işle:
import { verifyAndParseCapability, capabilityResponse, resolveRecipientPhone } from '@restomenum/plugin-sdk';
import type { MessagingSendPayload, MessagingStatus } from '@restomenum/plugin-sdk';
const req = await verifyAndParseCapability<MessagingSendPayload>(rawBody, headers['x-restomenum-signature'], {
  getSecret: (tenantId) => installStore.find(tenantId)?.webhookSecret,  // imza webhook ile AYNI şema
});
if (!req) return res.status(401).json({ error: 'invalid_signature' });
// ZORUNLU dedupe: aynı req.requestId tekrar gelirse mesajı YENİDEN GÖNDERME (önceki yanıtı dön).
// TEK RESOLVER: opak to → telefon (customerId→customers.get, packetId→packets.get; PII yetkisi yoksa null).
const phone = await resolveRecipientPhone(client, req.payload.to);
if (!phone) return res.json(capabilityResponse<MessagingStatus>('failed', { error: { code: 'no_phone' } }));
res.json(capabilityResponse<MessagingStatus>('accepted', { providerMessageId }));

// SAĞLAYICI: upstream DLR gelince teslim durumunu raporla — platform yalnız istek sahibi
// tüketiciye hedefli `messaging.message.status` event'i teslim eder (broadcast yok):
await client.messaging.reportStatus({ requestId: req.requestId, status: 'delivered', providerMessageId });

// TÜKETİCİ: manifest events[] += 'messaging.message.status' → webhook'ta
// data: { requestId, status: 'sent'|'delivered'|'read'|'failed', providerMessageId?, error? }

Tam örnek: examples/sample-plugin /capability (sağlayıcı) + api/messaging/sendMessage.mjs (tüketici).

Eklentiler-arası yetenekler (capabilities)

messaging.send'in jenerik hali: tenant'ın bağladığı sağlayıcı eklenti üzerinden herhangi bir yeteneği çağır — client.capabilities.invoke(capabilityId, payload) (tüketici) + client.capabilities.reportStatus(capabilityId, report) (sağlayıcı asenkron durum raporu). messaging.send de bu yolla çağrılabilir (capability-özel client.messaging yardımcıları korunur). Payload tipi capability id'den TÜRETİLİRinvoke('invoice.issue', …) girdiyi InvoiceIssueInput'a, sonucu InvoiceIssueResult'a otomatik daraltır (elle <…> tip argümanı GEREKMEZ; yanlış payload derleme-zamanı yakalanır). idempotencyKey her istekte zorunlu.

Hata kodları — tam kod plugin.<errorPrefix>.<suffix>; errorPrefix capability id'nin İLK segmentidir (plugin.messaging / plugin.notify / plugin.invoice — ör. notify.staff için plugin.notify.noProvider, plugin.notify.staff.* DEĞİL). Suffix sabitleri CAPABILITY_ERRORS'ta (tip-güvenli guard): noProvider→424 (sağlayıcı bağlı değil; özelliği gizle, retry etme), providerUnavailable→503, timeout→504, duplicateInProgress/idempotencyKeyReused/selfTarget/providerChanged→409, suspended/consumerBlocked→403, rawPiiForbidden/invalidPayload/idempotencyKeyRequired→400, notFound→404 (bilinmeyen capability). providerChanged = belirsiz sonuçtan (timeout) sonra tenant sağlayıcıyı değiştirdi → çift-işlem koruması, bu key ile retry engellendi.

import { CAPABILITY_ERRORS, capabilityErrorCode, ApiError } from '@restomenum/plugin-sdk';

// notify.staff — personele bildirim (asenkron event YOK; sonuç senkron döner):
const n = await client.capabilities.invoke('notify.staff', {
  to: { role: 'manager' },                 // veya { userId } — ham personel PII taşınmaz
  title: 'Gün sonu raporu hazır',          // zorunlu, ≤200
  body: 'Rapor panelde görüntülenebilir.', // opsiyonel, ≤1000
  idempotencyKey: `report-${gun}-hazir`,   // ZORUNLU (çift bildirim koruması)
});                                        // n: NotifyStaffResult (tip otomatik)
// n = { requestId, status: 'accepted'|'sent'|'failed', providerMessageId?, idempotentReplay? }

// invoice.issue — e-fatura kes (nihai sonuç ASENKRON invoice.status event'iyle gelir):
try {
  const f = await client.capabilities.invoke('invoice.issue', {
    to: { customerId },                        // opsiyonel; OPAK referans (ham VKN/ad/adres YASAK)
    amount: { total: 14550, currency: 'TRY' }, // KURUŞ (integer) — 14550 = ₺145,50; asla float; ≤ MAX_SAFE_INTEGER
    items: [{ description: 'Lahmacun', quantity: 2, unitPrice: 6000 }], // unitPrice KURUŞ (≥0); 1–100 kalem
    reference: `packet-${packetId}`,           // opsiyonel dış referans (≤100)
    idempotencyKey: `packet-${packetId}-fatura`, // ZORUNLU (mükerrer kesim = mali risk)
  });                                          // f: InvoiceIssueResult (tip otomatik)
  // f = { requestId, status: 'accepted'|'issued'|'failed', providerMessageId?, idempotentReplay? }
} catch (e) {
  if (e instanceof ApiError && e.code === capabilityErrorCode('invoice.issue', CAPABILITY_ERRORS.noProvider)) {
    // 424: tenant fatura sağlayıcısı bağlamamış → butonu gizle, retry etme
  }
}

Sağlayıcı tarafı: verifyAndParseCapability capability-agnostiktir — request.capability'ye göre payload'ı daralt (<NotifyStaffPayload> / <InvoiceIssuePayload>), aynı requestId'yi DEDUPE et (aynı requestId → işi tekrar YAPMA, önceki providerMessageId'yi dön), capabilityResponse<Status>(…) dön. Senkron durum kümesi capability-özeldir (capabilityResponse<InvoiceIssueStatus>('issued', …) — messaging'de 'sent', invoice'da 'issued'):

import { verifyAndParseCapability, capabilityResponse } from '@restomenum/plugin-sdk';
import type { InvoiceIssuePayload, InvoiceIssueStatus } from '@restomenum/plugin-sdk';

const req = await verifyAndParseCapability<InvoiceIssuePayload>(rawBody, sigHeader, { getSecret });
if (!req) return res.status(401).json({ error: 'invalid_signature' });
if (await alreadyProcessed(req.requestId)) return res.json(await priorResponse(req.requestId)); // DEDUPE zorunlu
const providerMessageId = await issueInvoiceAtGib(req.payload);
res.json(capabilityResponse<InvoiceIssueStatus>('issued', { providerMessageId }));

// Nihai GİB sonucu dakikalar sonra geldiğinde ASENKRON raporla → yalnız istek sahibi tüketiciye event:
await client.capabilities.reportStatus('invoice.issue', { requestId: req.requestId, status: 'paid', providerMessageId });

invoice.status event'i (tüketici): manifest events[] += 'invoice.status' → webhook'ta data: { requestId, status: 'issued'|'paid'|'void'|'failed', providerMessageId?, error? } (InvoiceStatusEventData — şekil messaging.message.status ile aynı). Yalnız istek sahibi tüketiciye hedefli, at-least-once → zarf id ile dedupe et. Durumlar monotonik: issued→paid yalnız ileri; void/failed terminal. (notify.staff'ın asenkron event'i yoktur.)

Session Token (iframe Custom UI)

Custom UI sayfan Restomenum panelinde iframe olarak açılır. Frontend App Bridge'den kısa-ömürlü bir session token alır ve backend'ine taşır; backend verifySessionToken ile doğrular → hangi tenant + kullanıcının baktığını güvenle öğrenir. Token JWT HS256'dır, tenant'ın webhookSecret'ı ile imzalı, aud = pluginId.

// iframe (frontend) — App Bridge ile token al, backend'ine taşı:
//   const { data } = await bridgeCall('getSessionToken');   // data = { token, tokenType:"Bearer", expiresIn:120 }
//   fetch('/api/me', { headers: { Authorization: 'Bearer ' + data.token } });

// backend — SDK ile doğrula (imza/kripto SDK'da; kendin yazma):
import { verifySessionToken, SessionError } from '@restomenum/plugin-sdk';

try {
  const claims = await verifySessionToken(req.headers.authorization, {
    pluginId,                                                  // aud bununla eşleşmeli (cross-plugin reddi)
    getSecret: (tenantId) => installStore.find(tenantId)?.webhookSecret,
  });
  // claims.tenantId · claims.sub (userId) · claims.role ('manager' | 'staff')
} catch (e) {
  if (e instanceof SessionError) res.status(401).json({ error: e.reason }); // malformed | wrong_algorithm | invalid_issuer | audience_mismatch | invalid_signature | expired | not_yet_valid
}

Doğrular: alg=HS256 (alg-confusion/"none" reddi) · iss=restomenum · aud=pluginId · HMAC imza (timing-safe) · exp ZORUNLU (exp'siz/expired red) · iat ileri-tarih reddi. Token kısa ömürlü → her isteği backend'de doğrula. Tam örnek: examples/sample-plugin /ui + /api/me.

Katalog & tipler

import { EVENT_TYPES, SCOPES, PII_SCOPES, isPiiScope, isEventType } from '@restomenum/plugin-sdk';
import type { EventType, Scope, WebhookEnvelope, Customer, Packet, ActionRequest } from '@restomenum/plugin-sdk';
import type { SessionTokenClaims } from '@restomenum/plugin-sdk';

İlgili

  • Dokümanlar: https://dev.restomenum.com/docs
  • OpenAPI spec: https://dev.restomenum.com/openapi.json
  • Örnek eklenti (starter): examples/sample-plugin — bu repo içinde, SDK'yı uçtan uca kullanır