تخطَّ إلى المحتوى

وحدة Nuxt

ما تركّبه @dukkan.one/nuxt، والعقد الوحيد الذي تطلب منك تنفيذه، ومتغيرات بيئتها، والعميل الذي تمنحه لكل مسار خادم.

آخر تحديث 10 أيلول 2026قراءة 4 د
في هذه الصفحة

هذه الصفحة للمطوّرين الذين يبنون تطبيق دكان على Nuxt 4. في نهايتها تعرف تماماً ما تفعله @dukkan.one/nuxt نيابةً عنك، وما يجب أن يوفّره server/dukkan.ts، وكيف تصل إلى واجهة المنصّة من أي مسار خادم.

التثبيت#

Shell
npm install @dukkan.one/nuxt @dukkan.one/app-sdk
nuxt.config.tsTypeScript
export default defineNuxtConfig({
  modules: ["@dukkan.one/nuxt"],
});

تقرأ الوحدة dukkan.app.toml عند البناء؛ راجع ملف dukkan.app.toml. يكتب dukkan app init الملفين لك.

ما تركّبه#

المسار ما يحدث
GET مسار التثبيت يعيد توجيه المضيفات البديلة إلى appBaseUrl، ثم يبدأ تدفق كود التفويض مع PKCE. يعيش state وكود التحقق في Cookie موقّعة، HttpOnly، بادئتها __Host-، لمدة 10 دقائق؛ لا يحمل الرابط أياً منهما. يمرّ ?store= و?install_token= من السوق كما هما.
GET مسار إعادة التوجيه يتحقق من الحالة الموقّعة، ويبدّل الكود، ويجعل install_id وstore_id مفتاح التثبيت، ويسجّل اشتراك Webhooks لمواضيع الملف (ويصكّ سراً فقط عندما لا تحمل واحداً)، ويستدعي onInstall، ويصكّ الجلسة عبر mintSession الخاصة بك، ثم يعيد التوجيه إلى return_to من الأصل نفسه الذي حمله طلب التثبيت.
POST مسار Webhooks يتحقق من HMAC على البايتات الخام، ويربط التسليم بالتثبيت المذكور داخل التوقيع، ويزيل التكرار عبر صندوقك الوارد، ويجيب بـ JSON صغير، ويرسل المعالجات بعد الإقرار. يعلّم app.uninstalled رموز التثبيت على أنها تحتاج إعادة اتصال قبل أن يعمل معالجك.
POST مسار Webhooks /:ref المستقبِل نفسه بلاحقة لكل تثبيت، للتطبيقات التي سجّلت نقطة نهاية لكل تثبيت قبل الحزمة.
/_dukkan/dev/* في nuxt dev فقط ومن الحلقة المحلية فقط: ما يخاطبه dukkan app dev (إعلانات النفق، وإطلاق التجارب، ومستند حالة لقائمة الإعداد).

تأتي المسارات الثلاثة من [urls] في الملف. كل إجابة من مسار Webhooks هي JSON الصغير الذي تتوقعه المنصّة؛ الحالات في Webhooks.

جانبك من العقد#

يصدّر server/dukkan.ts افتراضياً defineDukkanApp. تطلب الوحدة تحديداً ما لا تستطيع معرفته: أين تعيش الرموز والأسرار والجلسات.

server/dukkan.tsTypeScript
import { createPostgresTokenStore } from "@dukkan.one/app-sdk/tokens/postgres";
import { createAesGcmSealer } from "@dukkan.one/app-sdk";
import { sql } from "./db";
import { inbox, secrets, sessions } from "./stores";

const sealer = createAesGcmSealer({ currentKey: process.env.APP_ENCRYPTION_KEY! });

export default defineDukkanApp({
  tokenStore: createPostgresTokenStore({ sql, sealer }),
  webhookSecrets: secrets,
  inbox,
  mintSession: (event, install) => sessions.mint(event, install.id),
  onInstall: async ({ install, client, isReinstall }) => {
    const { store } = await client.installation.get();
    await sql`update installs set store_name = ${store.name} where install_id = ${install.id}`;
  },
  onUninstall: async (event) => {
    await sql`delete from customers where install_id = ${event.installId}`;
  },
  handlers: {
    "order.paid": async (event) => {
      await sql`insert into receipts (order_id, amount_minor) values (${event.data.order_id}, ${event.data.amount_minor})`;
    },
  },
});
الحقل مطلوب المعنى
tokenStore نعم TokenStore بالدوال load وsave وwithRefreshLock وmarkReconnectRequired. يأتي مع الحزمة محوّلان لـ Postgres وللمخازن المفتاحية؛ قفل التحديث هو ما يمنع نسختين من تقديم رمز التحديث نفسه.
webhookSecrets نعم load(installId) يعيد أسرار التوقيع (الحالي أولاً) والمتجر المرتبط بالتثبيت؛ وsave(installId, secret, storeId) يُستدعى من مسار إعادة التوجيه بسر مصكوك حديثاً.
inbox موصى به InboxStore تكون فيه insertIfNew ذرّية على معرّف الحدث الموقّع. من دونه يُرسل كل تسليم إلى المعالجات، بما فيها المكررة.
mintSession نعم المكان الوحيد الذي يجوز فيه إنشاء جلسة تاجر. يستقبل هوية التثبيت مباشرة من استجابة الرموز.
onInstall لا يعمل بعد حفظ الرموز والاشتراك، مع عميل جاهز. ازرع الافتراضيات هنا؛ يعمل أيضاً عند إعادة الموافقة (isReinstall).
onUninstall لا وصل app.uninstalled وماتت الرموز. احذف ما تتطلبه معالجة البيانات.
handlers لا دالة غير متزامنة لكل موضوع؛ event.data منمّط حسب الموضوع. تُلتقط الأخطاء وتُبلَّغ إلى onHandlerError فيبقى التسليم مُقَرّاً.
dispatch لا after-ack (الافتراضي) يجيب أولاً ويعالج في waitUntil؛ وinline ينتظر المعالجات قبل الإجابة.
listInstallIds لا التثبيتات التي يعرفها التطبيق، ليعيد dukkan app dev توجيه نقطة نهايتها عندما يتغيّر النفق.
rateLimiter لا محدِّد مشترك (hit(bucket, max, windowSeconds)) لمسارات الوحدة ومساراتك؛ الافتراضي نافذة منزلقة في الذاكرة. راجع تحديد المعدّل في مساراتك.

البيئة#

تأتي الأسرار من البيئة، لا من الملف أبداً:

المتغير الغرض
NUXT_DUKKAN_CLIENT_ID معرّف عميل OAuth. الافتراضي app.client_id من الملف.
NUXT_DUKKAN_CLIENT_SECRET المفتاح السري للعميل من البوابة.
NUXT_DUKKAN_SESSION_SECRET 32 حرفاً عشوائياً أو أكثر؛ يوقّع Cookie حالة OAuth.
NUXT_DUKKAN_APP_BASE_URL الأصل العام لهذا التطبيق. الافتراضي urls.app_url من الملف.
NUXT_DUKKAN_WEBHOOK_BASE_URL الأصل الذي ترسل إليه المنصّة عندما يختلف عن أصل التطبيق. يعلن dukkan app dev النفق هنا نيابةً عنك.
NUXT_DUKKAN_API_URL أصل المنصّة؛ https://dukkan.one ما لم تعمل على مضيف تجريبي.
NUXT_DUKKAN_ALLOWED_STORE_IDS معرّفات المتاجر التي يقبلها مسار إعادة التوجيه، مفصولة بفواصل. يضبطها dukkan app dev على متجرك التجريبي ما دام نفق عام مفتوحاً.

يرفض التطبيق الإقلاع في الإنتاج ما دام معرّف العميل أو مفتاحه السري أو سر الجلسة أو أصل التطبيق ناقصاً؛ في nuxt dev تُعرض المشكلات في قائمة الإعداد بدلاً من ذلك.

استدعاء الواجهة#

server/api/orders/recent.get.tsTypeScript
export default defineEventHandler(async (event) => {
  const session = await requireSession(event);
  const client = await useDukkanClient(event, session.installId);
  const page = await client.orders.list({ status: "placed", limit: 20 });
  return page.data.map((order) => ({ id: order.id, number: order.order_number, total: order.total }));
});

يعيد useDukkanClient(event, installId) عميلاً موصولاً بمخزن الرموز والاعتمادات نفسها التي تستخدمها الوحدة، فيُجدَّد رمز الوصول المرفوض مرة واحدة عبر قفل مخزنك. كل دالة مذكورة في مرجع حزمة التطبيقات.

ملاحظة

استخرج التثبيت من جلستك أنت، لا من معامل في الطلب أبداً. امتلاك معرّف تثبيت لا يمنح شيئاً على المنصّة، لكن عميلاً للتثبيت الخطأ سيقرأ بيانات ذلك المتجر برمز تطبيقك.

تحديد المعدّل في مساراتك#

تحدّ الوحدة معدّل مسارات التثبيت وإعادة التوجيه وWebhooks عبر المحدِّد الذي تمرّره بالحقل rateLimiter في defineDukkanApp (نافذة منزلقة في الذاكرة حين لا تمرّر شيئاً). ومنذ 0.3.0 تُصدَّر الدالتان نفساهما لمساراتك:

server/api/quotes/index.post.tsTypeScript
import { clientIp, enforceLimit, MemoryRateLimiter } from "@dukkan.one/nuxt/runtime";

const limiter = new MemoryRateLimiter(); // or the shared store you gave defineDukkanApp

export default defineEventHandler(async (event) => {
  await enforceLimit(event, limiter, `quotes:create:${clientIp(event)}`, 30, 60);
  // ...
});

تفضّل clientIp(event) العنوان الموثّق من الحافة (cf-connecting-ip)، ثم آخر قفزة في x-forwarded-for، ثم المقبس. وتعود enforceLimit(event, limiter, bucket, max, windowSeconds) حين تكون الضربة مسموحة؛ وإلا تضبط Retry-After وترمي 429 يحمل جسمه { code: "rate_limited" }، بالشكل نفسه الذي تستخدمه المنصّة.

الخطوات التالية#