وحدة Nuxt
ما تركّبه @dukkan.one/nuxt، والعقد الوحيد الذي تطلب منك تنفيذه، ومتغيرات بيئتها، والعميل الذي تمنحه لكل مسار خادم.
في هذه الصفحة
هذه الصفحة للمطوّرين الذين يبنون تطبيق دكان على Nuxt 4. في نهايتها تعرف تماماً ما تفعله @dukkan.one/nuxt نيابةً عنك، وما يجب أن يوفّره server/dukkan.ts، وكيف تصل إلى واجهة المنصّة من أي مسار خادم.
التثبيت#
npm install @dukkan.one/nuxt @dukkan.one/app-sdkexport 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. تطلب الوحدة تحديداً ما لا تستطيع معرفته: أين تعيش الرموز والأسرار والجلسات.
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 تُعرض المشكلات في قائمة الإعداد بدلاً من ذلك.
استدعاء الواجهة#
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 تُصدَّر الدالتان نفساهما لمساراتك:
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" }، بالشكل نفسه الذي تستخدمه المنصّة.