i18n - docs translations (#21884)
Created by Github action <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21884?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> Co-authored-by: github-actions <github-actions@twenty.com>
This commit is contained in:
committed by
GitHub
parent
6423c4cd3c
commit
2e1da86535
@@ -60,6 +60,7 @@ export default defineLogicFunction({
|
||||
* **cron**: يشغّل وظيفتك على جدول باستخدام تعبير CRON.
|
||||
* **databaseEvent**: يعمل على أحداث دورة حياة كائنات مساحة العمل. عندما تكون عملية الحدث هي `updated`، يمكن تحديد الحقول المحددة المراد الاستماع إليها في مصفوفة `updatedFields`. إذا تُركت غير معرّفة أو فارغة، فسيؤدي أي تحديث إلى تشغيل الدالة.
|
||||
> مثال: `person.updated`، `*.created`، `company.*`
|
||||
* **serverWebhook**: يستقبل خطافات الويب الواردة من خدمة خارجية (Stripe وGitHub وSvix و…) على نقطة نهاية واحدة ضمن نطاق التسجيل ويحدد مساحة العمل المستهدفة من الحمولة. راجع [مشغّل خطاف الويب على الخادم](#server-webhook-trigger).
|
||||
|
||||
<Note>
|
||||
يمكنك أيضًا تنفيذ دالة يدويًا باستخدام CLI:
|
||||
@@ -105,7 +106,7 @@ const handler = async (event: RoutePayload) => {
|
||||
| `body` | `object \| null` | جسم الطلب المُحلَّل (JSON) | `{ id: 1 }` -> `{ id: 1 }` |
|
||||
| `rawBody` | `string \| undefined` | نص الطلب الأصلي بترميز UTF-8، قبل تحليل JSON. مفيد للتحقق من تواقيع خطافات الويب على نمط HMAC (مثل `X-Hub-Signature-256` الخاص بـ GitHub وStripe). `undefined` عندما لم يحتفظ وقت التشغيل بها. | |
|
||||
| `isBase64Encoded` | `boolean` | ما إذا كان جسم الطلب مُرمَّزًا بترميز base64 | |
|
||||
| `requestContext.http.method` | `string` | طريقة HTTP (GET, POST, PUT, PATCH, DELETE) | |
|
||||
| `requestContext.http.method` | `سلسلة نصية` | طريقة HTTP (GET, POST, PUT, PATCH, DELETE) | |
|
||||
| `requestContext.http.path` | `string` | المسار الخام للطلب | |
|
||||
|
||||
|
||||
@@ -171,6 +172,90 @@ const handler = async (event: RoutePayload) => {
|
||||
يجب أن يكون رمز الحالة رمز حالة HTTP صالحًا (بين 100 و599). تتم مطابقة أسماء ترويسات الاستجابة دون حساسية لحالة الأحرف.
|
||||
</Note>
|
||||
|
||||
#### مشغّل ربط الويب على الخادم
|
||||
|
||||
`httpRouteTriggerSettings` يوفّر دالة تحت `/s/` ويحل مساحة العمل من مضيف الطلب — وهذا يعمل عندما تكون لكل مساحة عمل نطاقها الخاص. مع ذلك، يرسل المزوّدون الخارجيون أحداث كل مستأجر إلى عنوان URL واحد لربط الويب. في هذه الحالة، استخدم `serverWebhookTriggerSettings`: تكون الدالة متاحة عند نقطة نهاية ذات نطاق تسجيل ويتم حل مساحة العمل من الحمولة.
|
||||
|
||||
```ts src/logic-functions/handle-provider-webhook.logic-function.ts
|
||||
import { defineLogicFunction } from 'twenty-sdk/define';
|
||||
import type { RoutePayload } from 'twenty-sdk/logic-function';
|
||||
import { Response } from 'twenty-sdk/logic-function';
|
||||
|
||||
const handler = async (event: RoutePayload) => {
|
||||
// Verify the signature yourself before doing anything (see below).
|
||||
// Return a non-2xx Response to make the provider retry.
|
||||
return { received: true };
|
||||
};
|
||||
|
||||
export default defineLogicFunction({
|
||||
universalIdentifier: 'b3c2f0a1-7d4e-4c9a-9f2b-2e1d6a4c8e10',
|
||||
name: 'handle-provider-webhook',
|
||||
handler,
|
||||
serverWebhookTriggerSettings: {
|
||||
workspaceIdResolver: { source: 'body', path: 'metadata.twentyWorkspaceId' },
|
||||
forwardedRequestHeaders: ['webhook-id', 'webhook-timestamp', 'webhook-signature'],
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
يمكن الوصول إلى الدالة عند:
|
||||
|
||||
```
|
||||
POST https://your-twenty-server.com/webhooks/server/:applicationRegistrationUniversalIdentifier/:logicFunctionUniversalIdentifier
|
||||
```
|
||||
|
||||
كلا المعرّفين هما `universalIdentifier`s من البيان التعريفي الخاص بك — تسجيل التطبيق وهذه الدالة المنطقية. سجّل عنوان URL هذا لدى المزوّد.
|
||||
|
||||
**حل مساحة العمل.** نظرًا لأن نقطة النهاية الواحدة تخدم كل مساحات العمل، يجب أن يضع تكاملك `workspaceId` المستهدف في مكان ما في التسليم، وتخبر `workspaceIdResolver.{ source, path }` المنصّة بمكان قراءته:
|
||||
|
||||
| الحقل | القيم | الملاحظات |
|
||||
| -------- | --------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `المصدر` | `body` \| `query` \| `header` | `body` يقرأ JSON المحلَّل. `query` هو الخيار الأكثر شمولاً — فعادةً ما تتحكم في عنوان URL لرد النداء الذي تسجّله، لذا أضِف `?twentyWorkspaceId=…`. |
|
||||
| `مسار` | مسار بنقطة، على سبيل المثال: `metadata.twentyWorkspaceId` | يقتصر على مقاطع أبجدية رقمية / `_` / `-`؛ يتم رفض مفاتيح النموذج الأولي. |
|
||||
|
||||
يجب أن تكون القيمة المحلولة UUID صالحًا لمساحة عمل **و** يجب أن يكون تطبيقك مثبتًا في تلك المساحة، وإلا فسيتم رفض الطلب قبل تشغيل الدالة.
|
||||
|
||||
<Warning>
|
||||
**التحقق من التوقيع من مسؤوليتك.** المنصّة لا تتحقّق من توقيعات ربط الويب لهذا المشغّل — فهي تكتفي بحل مساحة العمل وتشغيل الدالة الخاصة بك. يجب أن يتحقق معالِجك من التوقيع بنفسه باستخدام `event.rawBody` والرؤوس التي أدرجتها في `forwardedRequestHeaders`، مع المقارنة بسر محفوظ كمتغيّر خادم/تطبيق. تحقّق دائمًا **قبل** أي تأثير جانبي، واستخدم مقارنة بزمن ثابت.
|
||||
</Warning>
|
||||
|
||||
يستخدم معظم المزوّدين HMAC-SHA256 للتوقيع؛ الأجزاء التي تختلف هي اسم الرأس وترميز الملخّص وسلسلة الحمولة الموقّعة. بعض الأمثلة:
|
||||
|
||||
| المزود | الرؤوس المطلوب تمريرها | السلسلة الموقَّعة | الملخّص |
|
||||
| ---------------------------- | ------------------------------------------------------ | ---------------------------- | -------------------------------------------------- |
|
||||
| Svix (Recall, Resend, Clerk) | `webhook-id`, `webhook-timestamp`, `webhook-signature` | `{id}.{timestamp}.{rawBody}` | base64 (السر يكون بصيغة base64 بعد إزالة `whsec_`) |
|
||||
| سترايب | `stripe-signature` | `{timestamp}.{rawBody}` | hex |
|
||||
| جيت هاب | `x-hub-signature-256` | `{rawBody}` | hex (يبدأ بـ `sha256=`) |
|
||||
| Shopify | `x-shopify-hmac-sha256` | `{rawBody}` | base64 |
|
||||
| Slack | `x-slack-signature`, `x-slack-request-timestamp` | `v0:{timestamp}:{rawBody}` | hex (يبدأ بـ `v0=`) |
|
||||
|
||||
```ts
|
||||
import { createHmac, timingSafeEqual } from 'crypto';
|
||||
|
||||
const handler = async (event: RoutePayload) => {
|
||||
const signature = event.headers['x-hub-signature-256'] ?? '';
|
||||
const expected =
|
||||
'sha256=' +
|
||||
createHmac('sha256', process.env.GITHUB_WEBHOOK_SECRET ?? '')
|
||||
.update(event.rawBody ?? '')
|
||||
.digest('hex');
|
||||
|
||||
const a = Buffer.from(signature);
|
||||
const b = Buffer.from(expected);
|
||||
|
||||
if (a.length !== b.length || !timingSafeEqual(a, b)) {
|
||||
return new Response({ error: 'invalid signature' }, { status: 401 });
|
||||
}
|
||||
|
||||
// ...handle the verified event
|
||||
return { received: true };
|
||||
};
|
||||
```
|
||||
|
||||
<Note>
|
||||
تعمل الدالة **بشكل متزامن** وتصبح القيمة التي تعيدها هي استجابة HTTP، لذا يرى المزوّدون رمز الحالة الخاص بك ويمكنهم إعادة المحاولة عند رموز غير 2xx. اجعل المعالِجات سريعة — بعض المزوّدين (مثل Slack) تنتهي مهلة طلباتهم خلال بضع ثوانٍ. نظرًا لأن الدالة تعمل قبل التحقق من التوقيع، قم بحماية نقطة النهاية هذه عبر تحديد المعدل على الحافة الخاصة بك.
|
||||
</Note>
|
||||
|
||||
#### حمولة مُحفِّز حدث قاعدة البيانات
|
||||
|
||||
عندما يستدعي مُحفِّز حدث قاعدة البيانات دالة المنطق الخاصة بك، فإنه يستقبل كائن `DatabaseEventPayload` واحدًا لكل سجل تم تغييره. تجمع الحمولة بين البيانات الوصفية حول مساحة العمل والكائن المصدر وبين الحدث على مستوى السجل.
|
||||
|
||||
@@ -60,6 +60,7 @@ Chcete-li vyvolat logickou funkci spuštěnou trasou z (bezhlavé) front-endové
|
||||
* **cron**: Spouští vaši funkci podle plánu pomocí výrazu CRON.
|
||||
* **databaseEvent**: Spouští se při událostech životního cyklu objektů v pracovním prostoru. Když je operace události `updated`, lze konkrétní sledovaná pole určit v poli `updatedFields`. Pokud zůstane nedefinované nebo prázdné, spustí funkci jakákoli aktualizace.
|
||||
> např. `person.updated`, `*.created`, `company.*`
|
||||
* **serverWebhook**: Přijímá příchozí webhooky od služby třetí strany (Stripe, GitHub, Svix, …) na jediném koncovém bodu v rámci registrace a z payloadu určí cílový pracovní prostor. Viz [spouštěč serverového webhooku](#server-webhook-trigger).
|
||||
|
||||
<Note>
|
||||
Funkci můžete také spustit ručně pomocí CLI:
|
||||
@@ -172,6 +173,90 @@ Z bezpečnostních důvodů jsou hlavičky odpovědi omezeny na seznam povolený
|
||||
Stavový kód musí být platný stavový kód HTTP (mezi 100 a 599). Názvy hlaviček odpovědi se porovnávají bez rozlišení velikosti písmen.
|
||||
</Note>
|
||||
|
||||
#### Serverový spouštěč webhooku
|
||||
|
||||
`httpRouteTriggerSettings` zpřístupňuje funkci pod `/s/` a workspace určuje z hostitele požadavku — což funguje, když má každý workspace svou vlastní doménu. Poskytovatelé třetích stran však doručují události každého tenanta na **jednu** adresu URL webhooku. Pro tento případ použijte `serverWebhookTriggerSettings`: funkce je dostupná na endpointu v rámci registrace a workspace se určuje z payloadu.
|
||||
|
||||
```ts src/logic-functions/handle-provider-webhook.logic-function.ts
|
||||
import { defineLogicFunction } from 'twenty-sdk/define';
|
||||
import type { RoutePayload } from 'twenty-sdk/logic-function';
|
||||
import { Response } from 'twenty-sdk/logic-function';
|
||||
|
||||
const handler = async (event: RoutePayload) => {
|
||||
// Verify the signature yourself before doing anything (see below).
|
||||
// Return a non-2xx Response to make the provider retry.
|
||||
return { received: true };
|
||||
};
|
||||
|
||||
export default defineLogicFunction({
|
||||
universalIdentifier: 'b3c2f0a1-7d4e-4c9a-9f2b-2e1d6a4c8e10',
|
||||
name: 'handle-provider-webhook',
|
||||
handler,
|
||||
serverWebhookTriggerSettings: {
|
||||
workspaceIdResolver: { source: 'body', path: 'metadata.twentyWorkspaceId' },
|
||||
forwardedRequestHeaders: ['webhook-id', 'webhook-timestamp', 'webhook-signature'],
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Funkce je dostupná na:
|
||||
|
||||
```
|
||||
POST https://your-twenty-server.com/webhooks/server/:applicationRegistrationUniversalIdentifier/:logicFunctionUniversalIdentifier
|
||||
```
|
||||
|
||||
Oba identifikátory jsou `universalIdentifier`s z vašeho manifestu — registrace aplikace a této logické funkce. Zaregistrujte tuto adresu URL u poskytovatele.
|
||||
|
||||
**Určení workspace.** Protože jeden endpoint obsluhuje každý workspace, vaše integrace musí vložit cílové `workspaceId` někam do doručovaných dat a `workspaceIdResolver.{ source, path }` říká platformě, odkud ho přečíst:
|
||||
|
||||
| Pole | Hodnoty | Poznámky |
|
||||
| ------- | ------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `zdroj` | `body` \| `query` \| `header` | `body` čte parsovaný JSON. `query` je nejuniverzálnější — obvykle máte pod kontrolou callback URL, kterou registrujete, takže připojte `?twentyWorkspaceId=…`. |
|
||||
| `cesta` | tečková cesta, např. `metadata.twentyWorkspaceId` | Omezeno na alfanumerické / `_` / `-` segmenty; prototypové klíče jsou odmítnuty. |
|
||||
|
||||
Určená hodnota musí být platné UUID workspace **a** vaše aplikace musí být v tomto workspace nainstalovaná, jinak je požadavek odmítnut ještě před spuštěním funkce.
|
||||
|
||||
<Warning>
|
||||
**Ověření podpisu je vaše zodpovědnost.** Platforma pro tento spouštěč neověřuje podpisy webhooků — pouze určí workspace a spustí vaši funkci. Váš handler musí podpis ověřit sám pomocí `event.rawBody` a hlaviček, které jste uvedli v `forwardedRequestHeaders`, porovnáním s tajemstvím uloženým jako serverová/aplikační proměnná. Vždy ověřujte **před** jakýmikoliv vedlejšími efekty a použijte porovnání v konstantním čase.
|
||||
</Warning>
|
||||
|
||||
Většina poskytovatelů podepisuje pomocí HMAC-SHA256; části, které se liší, jsou název hlavičky, kódování digestu a podepsaný řetězec payloadu. Několik příkladů:
|
||||
|
||||
| Poskytovatel | Hlavičky k přeposlání | Podepsaný řetězec | Digest |
|
||||
| ---------------------------- | ------------------------------------------------------ | ---------------------------- | ----------------------------------------------------- |
|
||||
| Svix (Recall, Resend, Clerk) | `webhook-id`, `webhook-timestamp`, `webhook-signature` | `{id}.{timestamp}.{rawBody}` | base64 (tajemství je v base64 po odstranění `whsec_`) |
|
||||
| Stripe | `stripe-signature` | `{timestamp}.{rawBody}` | hex |
|
||||
| GitHub | `x-hub-signature-256` | `{rawBody}` | hex (s prefixem `sha256=`) |
|
||||
| Shopify | `x-shopify-hmac-sha256` | `{rawBody}` | base64 |
|
||||
| Slack | `x-slack-signature`, `x-slack-request-timestamp` | `v0:{timestamp}:{rawBody}` | hex (s prefixem `v0=`) |
|
||||
|
||||
```ts
|
||||
import { createHmac, timingSafeEqual } from 'crypto';
|
||||
|
||||
const handler = async (event: RoutePayload) => {
|
||||
const signature = event.headers['x-hub-signature-256'] ?? '';
|
||||
const expected =
|
||||
'sha256=' +
|
||||
createHmac('sha256', process.env.GITHUB_WEBHOOK_SECRET ?? '')
|
||||
.update(event.rawBody ?? '')
|
||||
.digest('hex');
|
||||
|
||||
const a = Buffer.from(signature);
|
||||
const b = Buffer.from(expected);
|
||||
|
||||
if (a.length !== b.length || !timingSafeEqual(a, b)) {
|
||||
return new Response({ error: 'invalid signature' }, { status: 401 });
|
||||
}
|
||||
|
||||
// ...handle the verified event
|
||||
return { received: true };
|
||||
};
|
||||
```
|
||||
|
||||
<Note>
|
||||
Funkce běží **synchronně** a vámi vrácená hodnota se stává HTTP odpovědí, takže poskytovatelé vidí váš stavový kód a mohou opakovat požadavek při jiném než 2xx kódu. Udržujte handlery rychlé — některým poskytovatelům (např. Slack) vyprší časový limit během několika sekund. Protože funkce běží před tím, než je podpis zkontrolován, chraňte tento endpoint rate limitingem na vaší edge vrstvě.
|
||||
</Note>
|
||||
|
||||
#### Payload spouštěče databázové události
|
||||
|
||||
Když spouštěč databázové události vyvolá vaši logickou funkci, obdrží jeden `DatabaseEventPayload` pro každý změněný záznam. Payload kombinuje metadata o zdrojovém pracovním prostoru a objektu s událostí na úrovni záznamu.
|
||||
|
||||
@@ -60,6 +60,7 @@ Um eine routenausgelöste Logikfunktion von einer (headless) Front-Komponente au
|
||||
* **cron**: Führt Ihre Funktion nach Zeitplan mithilfe eines CRON-Ausdrucks aus.
|
||||
* **databaseEvent**: Wird bei Lebenszyklusereignissen von Workspace-Objekten ausgeführt. Wenn die Ereignisoperation `updated` ist, können bestimmte zu überwachende Felder im Array `updatedFields` angegeben werden. Wenn das Array undefiniert oder leer ist, löst jede Aktualisierung die Funktion aus.
|
||||
> z. B. `person.updated`, `*.created`, `company.*`
|
||||
* **serverWebhook**: Empfängt eingehende Webhooks von einem Drittanbieterdienst (Stripe, GitHub, Svix, …) an einem einzelnen, registrierungsbezogenen Endpunkt und ermittelt den Ziel-Arbeitsbereich aus der Nutzlast. Siehe [Server-Webhook-Trigger](#server-webhook-trigger).
|
||||
|
||||
<Note>
|
||||
Sie können eine Funktion auch manuell über die CLI ausführen:
|
||||
@@ -105,7 +106,7 @@ Der Typ `RoutePayload` hat die folgende Struktur:
|
||||
| `body` | `object \| null` | Geparster Request-Body (JSON) | `{ id: 1 }` -> `{ id: 1 }` |
|
||||
| `rawBody` | `string \| undefined` | Ursprünglicher UTF-8-Request-Body vor dem JSON-Parsing. Nützlich zur Verifizierung von Webhook-Signaturen im HMAC-Stil (z. B. GitHubs `X-Hub-Signature-256`, Stripe). `undefined`, wenn die Laufzeitumgebung es nicht beibehalten hat. | |
|
||||
| `isBase64Encoded` | `boolean` | Gibt an, ob der Body Base64-codiert ist | |
|
||||
| `requestContext.http.method` | `string` | HTTP-Methode (GET, POST, PUT, PATCH, DELETE) | |
|
||||
| `requestContext.http.method` | `Zeichenkette` | HTTP-Methode (GET, POST, PUT, PATCH, DELETE) | |
|
||||
| `requestContext.http.path` | `string` | Rohpfad der Anfrage | |
|
||||
|
||||
|
||||
@@ -171,6 +172,90 @@ Aus Sicherheitsgründen sind Antwort-Header auf eine Allowlist beschränkt. Jede
|
||||
Der Statuscode muss ein gültiger HTTP-Statuscode sein (zwischen 100 und 599). Antwort-Header-Namen werden ohne Beachtung der Groß-/Kleinschreibung verglichen.
|
||||
</Note>
|
||||
|
||||
#### Server-Webhook-Trigger
|
||||
|
||||
`httpRouteTriggerSettings` stellt eine Funktion unter `/s/` bereit und ermittelt den Workspace aus dem Host der Anfrage — was funktioniert, wenn jeder Workspace seine eigene Domain hat. Drittanbieter hingegen liefern die Ereignisse jedes Mandanten an **eine** Webhook-URL. Verwende für diesen Fall `serverWebhookTriggerSettings`: Die Funktion ist über einen registrierungsbezogenen Endpunkt erreichbar und der Workspace wird aus der Nutzlast ermittelt.
|
||||
|
||||
```ts src/logic-functions/handle-provider-webhook.logic-function.ts
|
||||
import { defineLogicFunction } from 'twenty-sdk/define';
|
||||
import type { RoutePayload } from 'twenty-sdk/logic-function';
|
||||
import { Response } from 'twenty-sdk/logic-function';
|
||||
|
||||
const handler = async (event: RoutePayload) => {
|
||||
// Verify the signature yourself before doing anything (see below).
|
||||
// Return a non-2xx Response to make the provider retry.
|
||||
return { received: true };
|
||||
};
|
||||
|
||||
export default defineLogicFunction({
|
||||
universalIdentifier: 'b3c2f0a1-7d4e-4c9a-9f2b-2e1d6a4c8e10',
|
||||
name: 'handle-provider-webhook',
|
||||
handler,
|
||||
serverWebhookTriggerSettings: {
|
||||
workspaceIdResolver: { source: 'body', path: 'metadata.twentyWorkspaceId' },
|
||||
forwardedRequestHeaders: ['webhook-id', 'webhook-timestamp', 'webhook-signature'],
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Die Funktion ist erreichbar unter:
|
||||
|
||||
```
|
||||
POST https://your-twenty-server.com/webhooks/server/:applicationRegistrationUniversalIdentifier/:logicFunctionUniversalIdentifier
|
||||
```
|
||||
|
||||
Beide Bezeichner sind die `universalIdentifier` aus deinem Manifest — der der Anwendungsregistrierung und dieser Logikfunktion. Registriere diese URL beim Provider.
|
||||
|
||||
**Workspace-Auflösung.** Da ein Endpunkt jeden Workspace bedient, muss deine Integration die Ziel-`workspaceId` irgendwo in der Zustellung platzieren, und `workspaceIdResolver.{ source, path }` teilt der Plattform mit, wo sie diese auslesen soll:
|
||||
|
||||
| Feld | Werte | Notizen |
|
||||
| -------- | -------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `quelle` | `body` \| `query` \| `header` | `body` liest das geparste JSON. `query` ist am universellsten — du kontrollierst in der Regel die Callback-URL, die du registrierst, also hänge `?twentyWorkspaceId=…` an. |
|
||||
| `pfad` | Dot-Pfad, z. B. `metadata.twentyWorkspaceId` | Beschränkt auf alphanumerische / `_`- / `-`-Segmente; Prototype-Schlüssel werden abgelehnt. |
|
||||
|
||||
Der aufgelöste Wert muss eine gültige Workspace-UUID sein **und** deine App muss in diesem Workspace installiert sein, andernfalls wird die Anfrage abgelehnt, bevor die Funktion ausgeführt wird.
|
||||
|
||||
<Warning>
|
||||
**Die Signaturüberprüfung liegt in deiner Verantwortung.** Die Plattform überprüft für diesen Trigger keine Webhook-Signaturen — sie ermittelt nur den Workspace und führt deine Funktion aus. Dein Handler muss die Signatur selbst mithilfe von `event.rawBody` und den in `forwardedRequestHeaders` aufgeführten Headern überprüfen und sie mit einem als Server-/Anwendungsvariable gespeicherten Geheimnis vergleichen. Überprüfe immer **vor** jeglicher Nebenwirkung und verwende einen Vergleich in konstanter Zeit.
|
||||
</Warning>
|
||||
|
||||
Die meisten Provider signieren mit HMAC-SHA256; die Teile, die sich unterscheiden, sind der Header-Name, die Digest-Codierung und der signierte Payload-String. Einige Beispiele:
|
||||
|
||||
| Anbieter | Weiterzuleitende Header | Signierte Zeichenkette | Digest |
|
||||
| ---------------------------- | ------------------------------------------------------ | ---------------------------- | --------------------------------------------------------- |
|
||||
| Svix (Recall, Resend, Clerk) | `webhook-id`, `webhook-timestamp`, `webhook-signature` | `{id}.{timestamp}.{rawBody}` | base64 (Geheimnis ist base64 nach Entfernen von `whsec_`) |
|
||||
| Stripe | `stripe-signature` | `{timestamp}.{rawBody}` | hex |
|
||||
| GitHub | `x-hub-signature-256` | `{rawBody}` | hex (mit Präfix `sha256=`) |
|
||||
| Shopify | `x-shopify-hmac-sha256` | `{rawBody}` | base64 |
|
||||
| Slack | `x-slack-signature`, `x-slack-request-timestamp` | `v0:{timestamp}:{rawBody}` | hex (mit Präfix `v0=`) |
|
||||
|
||||
```ts
|
||||
import { createHmac, timingSafeEqual } from 'crypto';
|
||||
|
||||
const handler = async (event: RoutePayload) => {
|
||||
const signature = event.headers['x-hub-signature-256'] ?? '';
|
||||
const expected =
|
||||
'sha256=' +
|
||||
createHmac('sha256', process.env.GITHUB_WEBHOOK_SECRET ?? '')
|
||||
.update(event.rawBody ?? '')
|
||||
.digest('hex');
|
||||
|
||||
const a = Buffer.from(signature);
|
||||
const b = Buffer.from(expected);
|
||||
|
||||
if (a.length !== b.length || !timingSafeEqual(a, b)) {
|
||||
return new Response({ error: 'invalid signature' }, { status: 401 });
|
||||
}
|
||||
|
||||
// ...handle the verified event
|
||||
return { received: true };
|
||||
};
|
||||
```
|
||||
|
||||
<Note>
|
||||
Die Funktion läuft **synchron** und dein Rückgabewert wird zur HTTP-Antwort, sodass Provider deinen Statuscode sehen und bei Nicht-2xx erneut versuchen können. Halte Handler schnell — einige Provider (z. B. Slack) laufen nach wenigen Sekunden in ein Timeout. Da die Funktion ausgeführt wird, bevor die Signatur geprüft wird, solltest du diesen Endpunkt an deinem Edge mit Ratenbegrenzung schützen.
|
||||
</Note>
|
||||
|
||||
#### Datenbank-Event-Trigger-Payload
|
||||
|
||||
Wenn ein Datenbank-Event-Trigger Ihre Logic Function aufruft, erhält sie eine `DatabaseEventPayload` pro geändertem Datensatz. Die Payload kombiniert Metadaten über den Quell-Workspace und das Objekt mit dem Ereignis auf Datensatzebene.
|
||||
|
||||
@@ -60,6 +60,7 @@ Para invocar una función de lógica activada por una ruta desde un componente d
|
||||
* **cron**: Ejecuta tu función en un horario usando una expresión CRON.
|
||||
* **databaseEvent**: Se ejecuta en eventos del ciclo de vida de objetos del espacio de trabajo. Cuando la operación del evento es `updated`, se pueden especificar campos específicos que se deben escuchar en la matriz `updatedFields`. Si se deja sin definir o vacío, cualquier actualización activará la función.
|
||||
> p. ej. `person.updated`, `*.created`, `company.*`
|
||||
* **serverWebhook**: Recibe webhooks entrantes de un servicio de terceros (Stripe, GitHub, Svix, …) en un único endpoint con alcance de registro y resuelve el espacio de trabajo de destino a partir del payload. Consulta [Disparador de webhook del servidor](#server-webhook-trigger).
|
||||
|
||||
<Note>
|
||||
También puedes ejecutar manualmente una función usando la CLI:
|
||||
@@ -172,6 +173,90 @@ Por razones de seguridad, los encabezados de la respuesta están restringidos a
|
||||
El código de estado debe ser un código de estado HTTP válido (entre 100 y 599). Los nombres de los encabezados de respuesta se comparan sin distinguir mayúsculas de minúsculas.
|
||||
</Note>
|
||||
|
||||
#### Disparador de webhook del servidor
|
||||
|
||||
`httpRouteTriggerSettings` expone una función bajo `/s/` y resuelve el espacio de trabajo a partir del host de la solicitud — lo cual funciona cuando cada espacio de trabajo tiene su propio dominio. Los proveedores de terceros, sin embargo, entregan los eventos de cada inquilino a **una** URL de webhook. Para ese caso, usa `serverWebhookTriggerSettings`: la función es accesible en un endpoint con alcance de registro y el espacio de trabajo se resuelve a partir del payload.
|
||||
|
||||
```ts src/logic-functions/handle-provider-webhook.logic-function.ts
|
||||
import { defineLogicFunction } from 'twenty-sdk/define';
|
||||
import type { RoutePayload } from 'twenty-sdk/logic-function';
|
||||
import { Response } from 'twenty-sdk/logic-function';
|
||||
|
||||
const handler = async (event: RoutePayload) => {
|
||||
// Verify the signature yourself before doing anything (see below).
|
||||
// Return a non-2xx Response to make the provider retry.
|
||||
return { received: true };
|
||||
};
|
||||
|
||||
export default defineLogicFunction({
|
||||
universalIdentifier: 'b3c2f0a1-7d4e-4c9a-9f2b-2e1d6a4c8e10',
|
||||
name: 'handle-provider-webhook',
|
||||
handler,
|
||||
serverWebhookTriggerSettings: {
|
||||
workspaceIdResolver: { source: 'body', path: 'metadata.twentyWorkspaceId' },
|
||||
forwardedRequestHeaders: ['webhook-id', 'webhook-timestamp', 'webhook-signature'],
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
La función es accesible en:
|
||||
|
||||
```
|
||||
POST https://your-twenty-server.com/webhooks/server/:applicationRegistrationUniversalIdentifier/:logicFunctionUniversalIdentifier
|
||||
```
|
||||
|
||||
Ambos identificadores son los `universalIdentifier`s de tu manifiesto: el del registro de la aplicación y el de esta función lógica. Registra esa URL con el proveedor.
|
||||
|
||||
**Resolución de espacio de trabajo.** Como un endpoint atiende a cada espacio de trabajo, tu integración debe colocar el `workspaceId` de destino en algún lugar de la entrega, y `workspaceIdResolver.{ source, path }` le indica a la plataforma dónde leerlo:
|
||||
|
||||
| Campo | Valores | Notas |
|
||||
| -------- | ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `source` | `body` \| `query` \| `header` | `body` lee el JSON parseado. `query` es el más universal: normalmente controlas la URL de callback que registras, así que añade `?twentyWorkspaceId=…`. |
|
||||
| `path` | ruta con puntos, p. ej. `metadata.twentyWorkspaceId` | Restringido a segmentos alfanuméricos / `_` / `-`; las claves de prototipo se rechazan. |
|
||||
|
||||
El valor resuelto debe ser un UUID de espacio de trabajo válido **y** tu aplicación debe estar instalada en ese espacio de trabajo; de lo contrario, la solicitud se rechaza antes de que la función se ejecute.
|
||||
|
||||
<Warning>
|
||||
**La verificación de la firma es tu responsabilidad.** La plataforma no verifica las firmas de webhook para este disparador: solo resuelve el espacio de trabajo y ejecuta tu función. Tu manejador debe verificar la firma por sí mismo usando `event.rawBody` y los encabezados que incluiste en `forwardedRequestHeaders`, comparando contra un secreto almacenado como variable de servidor/aplicación. Verifica siempre **antes** de cualquier efecto secundario y usa una comparación en tiempo constante.
|
||||
</Warning>
|
||||
|
||||
La mayoría de los proveedores firman con HMAC-SHA256; las partes que difieren son el nombre del encabezado, la codificación del digest y la cadena firmada del payload. Algunos ejemplos:
|
||||
|
||||
| Proveedor | Encabezados a reenviar | Cadena firmada | Digest |
|
||||
| ---------------------------- | ------------------------------------------------------ | ---------------------------- | --------------------------------------------------------------- |
|
||||
| Svix (Recall, Resend, Clerk) | `webhook-id`, `webhook-timestamp`, `webhook-signature` | `{id}.{timestamp}.{rawBody}` | base64 (el secreto está en base64 después de eliminar `whsec_`) |
|
||||
| Stripe | `stripe-signature` | `{timestamp}.{rawBody}` | hex |
|
||||
| GitHub | `x-hub-signature-256` | `{rawBody}` | hex (con el prefijo `sha256=`) |
|
||||
| Shopify | `x-shopify-hmac-sha256` | `{rawBody}` | base64 |
|
||||
| Slack | `x-slack-signature`, `x-slack-request-timestamp` | `v0:{timestamp}:{rawBody}` | hex (con el prefijo `v0=`) |
|
||||
|
||||
```ts
|
||||
import { createHmac, timingSafeEqual } from 'crypto';
|
||||
|
||||
const handler = async (event: RoutePayload) => {
|
||||
const signature = event.headers['x-hub-signature-256'] ?? '';
|
||||
const expected =
|
||||
'sha256=' +
|
||||
createHmac('sha256', process.env.GITHUB_WEBHOOK_SECRET ?? '')
|
||||
.update(event.rawBody ?? '')
|
||||
.digest('hex');
|
||||
|
||||
const a = Buffer.from(signature);
|
||||
const b = Buffer.from(expected);
|
||||
|
||||
if (a.length !== b.length || !timingSafeEqual(a, b)) {
|
||||
return new Response({ error: 'invalid signature' }, { status: 401 });
|
||||
}
|
||||
|
||||
// ...handle the verified event
|
||||
return { received: true };
|
||||
};
|
||||
```
|
||||
|
||||
<Note>
|
||||
La función se ejecuta **sincrónicamente** y el valor que devuelves se convierte en la respuesta HTTP, por lo que los proveedores ven tu código de estado y pueden reintentar en caso de que no sea 2xx. Mantén los manejadores rápidos: algunos proveedores (p. ej. Slack) agotan el tiempo de espera en pocos segundos. Como la función se ejecuta antes de que se compruebe la firma, protege este endpoint con limitación de tasa en tu edge.
|
||||
</Note>
|
||||
|
||||
#### Payload del disparador de evento de base de datos
|
||||
|
||||
Cuando un disparador de evento de base de datos invoca tu función de lógica, esta recibe un `DatabaseEventPayload` por cada registro modificado. El payload combina metadatos sobre el espacio de trabajo y el objeto de origen con el evento a nivel de registro.
|
||||
|
||||
@@ -60,6 +60,7 @@ Pour appeler une fonction logique déclenchée par une route depuis un composant
|
||||
* **cron** : Exécute votre fonction selon une planification à l’aide d’une expression CRON.
|
||||
* **databaseEvent**: S'exécute lors des événements du cycle de vie des objets de l'espace de travail. Lorsque l'opération de l'événement est `updated`, des champs spécifiques à surveiller peuvent être spécifiés dans le tableau `updatedFields`. S'il est laissé indéfini ou vide, toute mise à jour déclenchera la fonction.
|
||||
> p. ex. `person.updated`, `*.created`, `company.*`
|
||||
* **serverWebhook** : reçoit des webhooks entrants d’un service tiers (Stripe, GitHub, Svix, …) sur un endpoint unique propre à l’inscription et détermine l’espace de travail cible à partir de la charge utile. Voir [déclencheur de webhook serveur](#server-webhook-trigger).
|
||||
|
||||
<Note>
|
||||
Vous pouvez également exécuter manuellement une fonction à l'aide de la CLI :
|
||||
@@ -172,6 +173,90 @@ Pour des raisons de sécurité, les en-têtes de réponse sont restreints à une
|
||||
Le code d’état doit être un code d’état HTTP valide (compris entre 100 et 599). Les noms des en-têtes de réponse sont comparés sans tenir compte de la casse.
|
||||
</Note>
|
||||
|
||||
#### Déclencheur de webhook côté serveur
|
||||
|
||||
`httpRouteTriggerSettings` expose une fonction sous `/s/` et résout l’espace de travail à partir de l’hôte de la requête — ce qui fonctionne lorsque chaque espace de travail a son propre domaine. Les fournisseurs tiers, en revanche, envoient les événements de chaque locataire vers **une** URL de webhook. Dans ce cas, utilisez `serverWebhookTriggerSettings` : la fonction est accessible à un endpoint dont la portée est l’enregistrement, et l’espace de travail est résolu à partir de la charge utile.
|
||||
|
||||
```ts src/logic-functions/handle-provider-webhook.logic-function.ts
|
||||
import { defineLogicFunction } from 'twenty-sdk/define';
|
||||
import type { RoutePayload } from 'twenty-sdk/logic-function';
|
||||
import { Response } from 'twenty-sdk/logic-function';
|
||||
|
||||
const handler = async (event: RoutePayload) => {
|
||||
// Verify the signature yourself before doing anything (see below).
|
||||
// Return a non-2xx Response to make the provider retry.
|
||||
return { received: true };
|
||||
};
|
||||
|
||||
export default defineLogicFunction({
|
||||
universalIdentifier: 'b3c2f0a1-7d4e-4c9a-9f2b-2e1d6a4c8e10',
|
||||
name: 'handle-provider-webhook',
|
||||
handler,
|
||||
serverWebhookTriggerSettings: {
|
||||
workspaceIdResolver: { source: 'body', path: 'metadata.twentyWorkspaceId' },
|
||||
forwardedRequestHeaders: ['webhook-id', 'webhook-timestamp', 'webhook-signature'],
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
La fonction est accessible à l’adresse :
|
||||
|
||||
```
|
||||
POST https://your-twenty-server.com/webhooks/server/:applicationRegistrationUniversalIdentifier/:logicFunctionUniversalIdentifier
|
||||
```
|
||||
|
||||
Les deux identifiants sont les `universalIdentifier`s de votre manifeste — celui de l’enregistrement de l’application et celui de cette fonction logique. Enregistrez cette URL auprès du fournisseur.
|
||||
|
||||
**Résolution de l’espace de travail.** Étant donné qu’un seul endpoint dessert tous les espaces de travail, votre intégration doit placer l’identifiant cible `workspaceId` quelque part dans la requête, et `workspaceIdResolver.{ source, path }` indique à la plateforme où le lire :
|
||||
|
||||
| Champ | Valeurs | Notes |
|
||||
| -------- | ------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| `source` | `body` \| `query` \| `header` | `body` lit le JSON analysé. `query` est le plus universel — vous contrôlez généralement l’URL de rappel que vous enregistrez, donc ajoutez `?twentyWorkspaceId=…`. |
|
||||
| `chemin` | chemin en notation par points, par ex. `metadata.twentyWorkspaceId` | Limité à des segments alphanumériques / `_` / `-` ; les clés du prototype sont rejetées. |
|
||||
|
||||
La valeur résolue doit être un UUID d’espace de travail valide **et** votre application doit être installée dans cet espace de travail, sinon la requête est rejetée avant l’exécution de la fonction.
|
||||
|
||||
<Warning>
|
||||
**La vérification de la signature est de votre responsabilité.** La plateforme ne vérifie pas les signatures de webhook pour ce déclencheur — elle se contente de résoudre l’espace de travail et d’exécuter votre fonction. Votre gestionnaire doit vérifier lui-même la signature en utilisant `event.rawBody` et les en-têtes que vous avez listés dans `forwardedRequestHeaders`, en les comparant à un secret stocké en tant que variable serveur/application. Vérifiez toujours **avant** tout effet de bord et utilisez une comparaison en temps constant.
|
||||
</Warning>
|
||||
|
||||
La plupart des fournisseurs signent avec HMAC-SHA256 ; les éléments qui diffèrent sont le nom de l’en-tête, l’encodage de l’empreinte et la chaîne de la charge utile signée. Quelques exemples :
|
||||
|
||||
| Fournisseur | En-têtes à transférer | Chaîne signée | Empreinte |
|
||||
| ---------------------------- | ------------------------------------------------------ | ---------------------------- | -------------------------------------------------------------- |
|
||||
| Svix (Recall, Resend, Clerk) | `webhook-id`, `webhook-timestamp`, `webhook-signature` | `{id}.{timestamp}.{rawBody}` | base64 (le secret est en base64 après suppression de `whsec_`) |
|
||||
| Stripe | `stripe-signature` | `{timestamp}.{rawBody}` | hexadécimal |
|
||||
| GitHub | `x-hub-signature-256` | `{rawBody}` | hexadécimal (préfixé par `sha256=`) |
|
||||
| Shopify | `x-shopify-hmac-sha256` | `{rawBody}` | base64 |
|
||||
| Slack | `x-slack-signature`, `x-slack-request-timestamp` | `v0:{timestamp}:{rawBody}` | hexadécimal (préfixé par `v0=`) |
|
||||
|
||||
```ts
|
||||
import { createHmac, timingSafeEqual } from 'crypto';
|
||||
|
||||
const handler = async (event: RoutePayload) => {
|
||||
const signature = event.headers['x-hub-signature-256'] ?? '';
|
||||
const expected =
|
||||
'sha256=' +
|
||||
createHmac('sha256', process.env.GITHUB_WEBHOOK_SECRET ?? '')
|
||||
.update(event.rawBody ?? '')
|
||||
.digest('hex');
|
||||
|
||||
const a = Buffer.from(signature);
|
||||
const b = Buffer.from(expected);
|
||||
|
||||
if (a.length !== b.length || !timingSafeEqual(a, b)) {
|
||||
return new Response({ error: 'invalid signature' }, { status: 401 });
|
||||
}
|
||||
|
||||
// ...handle the verified event
|
||||
return { received: true };
|
||||
};
|
||||
```
|
||||
|
||||
<Note>
|
||||
La fonction s’exécute **de manière synchrone** et la valeur que vous retournez devient la réponse HTTP, de sorte que les fournisseurs voient votre code d’état et peuvent réessayer en cas de réponse non-2xx. Gardez les gestionnaires rapides — certains fournisseurs (par ex. Slack) expirent au bout de quelques secondes. Étant donné que la fonction s’exécute avant que la signature ne soit vérifiée, protégez cet endpoint avec une limitation de débit à votre périphérie.
|
||||
</Note>
|
||||
|
||||
#### Charge utile du déclencheur d'événement de base de données
|
||||
|
||||
Lorsqu’un déclencheur d’événement de base de données appelle votre fonction logique, celle-ci reçoit un `DatabaseEventPayload` par enregistrement modifié. La charge utile combine les métadonnées concernant l'espace de travail et l'objet source avec l'événement au niveau de l'enregistrement.
|
||||
|
||||
@@ -60,6 +60,7 @@ Per richiamare, da un componente front-end (headless), una funzione logica attiv
|
||||
* **cron**: Esegue la tua funzione secondo una pianificazione utilizzando un'espressione CRON.
|
||||
* **databaseEvent**: Viene eseguito sugli eventi del ciclo di vita degli oggetti dello spazio di lavoro. Quando l'operazione dell'evento è `updated`, è possibile specificare campi specifici da monitorare nell'array `updatedFields`. Se lasciato non definito o vuoto, qualsiasi aggiornamento attiverà la funzione.
|
||||
> ad es. `person.updated`, `*.created`, `company.*`
|
||||
* **serverWebhook**: Riceve webhook in entrata da un servizio di terze parti (Stripe, GitHub, Svix, …) su un singolo endpoint con ambito di registrazione e risolve lo spazio di lavoro di destinazione a partire dal payload. Vedi [Trigger webhook del server](#server-webhook-trigger).
|
||||
|
||||
<Note>
|
||||
Puoi anche eseguire manualmente una funzione utilizzando la CLI:
|
||||
@@ -171,6 +172,90 @@ Per motivi di sicurezza, le intestazioni di risposta sono limitate a un elenco c
|
||||
Il codice di stato deve essere un codice di stato HTTP valido (compreso tra 100 e 599). I nomi delle intestazioni di risposta vengono confrontati senza distinzione tra maiuscole e minuscole.
|
||||
</Note>
|
||||
|
||||
#### Trigger webhook del server
|
||||
|
||||
`httpRouteTriggerSettings` espone una funzione sotto `/s/` e risolve lo spazio di lavoro dall'host della richiesta — il che funziona quando ogni spazio di lavoro ha il proprio dominio. I provider di terze parti, tuttavia, inviano gli eventi di ogni tenant a **un** URL di webhook. Per quel caso, usa `serverWebhookTriggerSettings`: la funzione è raggiungibile in un endpoint con ambito di registrazione e lo spazio di lavoro viene risolto dal payload.
|
||||
|
||||
```ts src/logic-functions/handle-provider-webhook.logic-function.ts
|
||||
import { defineLogicFunction } from 'twenty-sdk/define';
|
||||
import type { RoutePayload } from 'twenty-sdk/logic-function';
|
||||
import { Response } from 'twenty-sdk/logic-function';
|
||||
|
||||
const handler = async (event: RoutePayload) => {
|
||||
// Verify the signature yourself before doing anything (see below).
|
||||
// Return a non-2xx Response to make the provider retry.
|
||||
return { received: true };
|
||||
};
|
||||
|
||||
export default defineLogicFunction({
|
||||
universalIdentifier: 'b3c2f0a1-7d4e-4c9a-9f2b-2e1d6a4c8e10',
|
||||
name: 'handle-provider-webhook',
|
||||
handler,
|
||||
serverWebhookTriggerSettings: {
|
||||
workspaceIdResolver: { source: 'body', path: 'metadata.twentyWorkspaceId' },
|
||||
forwardedRequestHeaders: ['webhook-id', 'webhook-timestamp', 'webhook-signature'],
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
La funzione è raggiungibile all'indirizzo:
|
||||
|
||||
```
|
||||
POST https://your-twenty-server.com/webhooks/server/:applicationRegistrationUniversalIdentifier/:logicFunctionUniversalIdentifier
|
||||
```
|
||||
|
||||
Entrambi gli identificatori sono gli `universalIdentifier` del tuo manifest — quello della registrazione dell'applicazione e quello di questa funzione logica. Registra quell'URL presso il provider.
|
||||
|
||||
**Risoluzione dello spazio di lavoro.** Poiché un endpoint serve ogni spazio di lavoro, la tua integrazione deve inserire il `workspaceId` di destinazione da qualche parte nella consegna, e `workspaceIdResolver.{ source, path }` indica alla piattaforma dove leggerlo:
|
||||
|
||||
| Campo | Valori | Note |
|
||||
| ---------- | ------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `fonte` | `body` \| `query` \| `header` | `body` legge il JSON analizzato. `query` è il più universale — di solito controlli l'URL di callback che registri, quindi aggiungi `?twentyWorkspaceId=…`. |
|
||||
| `percorso` | percorso con punti, ad es. `metadata.twentyWorkspaceId` | Limitato a segmenti alfanumerici / `_` / `-`; le chiavi del prototipo vengono rifiutate. |
|
||||
|
||||
Il valore risolto deve essere un UUID di spazio di lavoro valido **e** la tua app deve essere installata in quello spazio di lavoro, altrimenti la richiesta viene rifiutata prima che la funzione venga eseguita.
|
||||
|
||||
<Warning>
|
||||
**La verifica della firma è tua responsabilità.** La piattaforma non verifica le firme dei webhook per questo trigger — si limita a risolvere lo spazio di lavoro ed eseguire la tua funzione. Il tuo handler deve verificare la firma autonomamente usando `event.rawBody` e gli header che hai elencato in `forwardedRequestHeaders`, confrontando con un segreto memorizzato come variabile server/applicazione. Verifica sempre **prima** di qualsiasi effetto collaterale e usa un confronto a tempo costante.
|
||||
</Warning>
|
||||
|
||||
La maggior parte dei provider firma con HMAC-SHA256; le parti che differiscono sono il nome dell'header, la codifica del digest e la stringa del payload firmato. Alcuni esempi:
|
||||
|
||||
| Provider | Header da inoltrare | Stringa firmata | Digest |
|
||||
| ---------------------------- | ------------------------------------------------------ | ---------------------------- | ---------------------------------------------------------- |
|
||||
| Svix (Recall, Resend, Clerk) | `webhook-id`, `webhook-timestamp`, `webhook-signature` | `{id}.{timestamp}.{rawBody}` | base64 (il segreto è in base64 dopo aver rimosso `whsec_`) |
|
||||
| Stripe | `stripe-signature` | `{timestamp}.{rawBody}` | esadecimale |
|
||||
| GitHub | `x-hub-signature-256` | `{rawBody}` | esadecimale (prefissato con `sha256=`) |
|
||||
| Shopify | `x-shopify-hmac-sha256` | `{rawBody}` | base64 |
|
||||
| Slack | `x-slack-signature`, `x-slack-request-timestamp` | `v0:{timestamp}:{rawBody}` | esadecimale (prefissato con `v0=`) |
|
||||
|
||||
```ts
|
||||
import { createHmac, timingSafeEqual } from 'crypto';
|
||||
|
||||
const handler = async (event: RoutePayload) => {
|
||||
const signature = event.headers['x-hub-signature-256'] ?? '';
|
||||
const expected =
|
||||
'sha256=' +
|
||||
createHmac('sha256', process.env.GITHUB_WEBHOOK_SECRET ?? '')
|
||||
.update(event.rawBody ?? '')
|
||||
.digest('hex');
|
||||
|
||||
const a = Buffer.from(signature);
|
||||
const b = Buffer.from(expected);
|
||||
|
||||
if (a.length !== b.length || !timingSafeEqual(a, b)) {
|
||||
return new Response({ error: 'invalid signature' }, { status: 401 });
|
||||
}
|
||||
|
||||
// ...handle the verified event
|
||||
return { received: true };
|
||||
};
|
||||
```
|
||||
|
||||
<Note>
|
||||
La funzione viene eseguita **in modo sincrono** e il valore restituito diventa la risposta HTTP, quindi i provider vedono il tuo codice di stato e possono ritentare in caso di codice non 2xx. Mantieni gli handler veloci — alcuni provider (ad es. Slack) vanno in timeout in pochi secondi. Poiché la funzione viene eseguita prima che la firma sia verificata, proteggi questo endpoint con limitazione della frequenza al tuo edge.
|
||||
</Note>
|
||||
|
||||
#### Payload del trigger di evento del database
|
||||
|
||||
Quando un trigger di evento del database invoca la tua funzione logica, questa riceve un `DatabaseEventPayload` per ogni record modificato. Il payload combina i metadati sull’area di lavoro e sull’oggetto di origine con l’evento a livello di record.
|
||||
|
||||
@@ -60,6 +60,7 @@ export default defineLogicFunction({
|
||||
* **cron**: CRON 식을 사용하여 예약된 일정으로 함수를 실행합니다.
|
||||
* **databaseEvent**: 워크스페이스 객체 라이프사이클 이벤트에서 실행됩니다. 이벤트 작업이 `updated`인 경우, 수신할 특정 필드를 `updatedFields` 배열에 지정할 수 있습니다. 정의하지 않거나 비워두면, 어떤 업데이트든 함수가 트리거됩니다.
|
||||
> 예: `person.updated`, `*.created`, `company.*`
|
||||
* **serverWebhook**: 타사 서비스(Stripe, GitHub, Svix, …)로부터 인바운드 웹훅을 수신합니다. 단일 등록 범위 엔드포인트에서 수신하고, 페이로드에서 대상 워크스페이스를 해석합니다. [서버 웹훅 트리거](#server-webhook-trigger)를 참고하세요.
|
||||
|
||||
<Note>
|
||||
CLI를 사용해 함수를 수동으로 실행할 수도 있습니다:
|
||||
@@ -171,6 +172,90 @@ const handler = async (event: RoutePayload) => {
|
||||
상태 코드는 유효한 HTTP 상태 코드(100에서 599 사이)여야 합니다. 응답 헤더 이름은 대소문자를 구분하지 않습니다.
|
||||
</Note>
|
||||
|
||||
#### 서버 웹훅 트리거
|
||||
|
||||
`httpRouteTriggerSettings`는 `/s/` 아래에 함수를 노출하고 요청 호스트에서 워크스페이스를 해석합니다. 이는 각 워크스페이스가 자체 도메인을 가질 때 동작합니다. 그러나 타사 공급자는 모든 테넌트의 이벤트를 **하나의** 웹훅 URL로 전달합니다. 그런 경우에는 `serverWebhookTriggerSettings`를 사용하세요. 이 함수는 등록 범위의 엔드포인트에서 접근 가능하며, 워크스페이스는 페이로드에서 해석됩니다.
|
||||
|
||||
```ts src/logic-functions/handle-provider-webhook.logic-function.ts
|
||||
import { defineLogicFunction } from 'twenty-sdk/define';
|
||||
import type { RoutePayload } from 'twenty-sdk/logic-function';
|
||||
import { Response } from 'twenty-sdk/logic-function';
|
||||
|
||||
const handler = async (event: RoutePayload) => {
|
||||
// Verify the signature yourself before doing anything (see below).
|
||||
// Return a non-2xx Response to make the provider retry.
|
||||
return { received: true };
|
||||
};
|
||||
|
||||
export default defineLogicFunction({
|
||||
universalIdentifier: 'b3c2f0a1-7d4e-4c9a-9f2b-2e1d6a4c8e10',
|
||||
name: 'handle-provider-webhook',
|
||||
handler,
|
||||
serverWebhookTriggerSettings: {
|
||||
workspaceIdResolver: { source: 'body', path: 'metadata.twentyWorkspaceId' },
|
||||
forwardedRequestHeaders: ['webhook-id', 'webhook-timestamp', 'webhook-signature'],
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
함수는 다음 위치에서 접근할 수 있습니다:
|
||||
|
||||
```
|
||||
POST https://your-twenty-server.com/webhooks/server/:applicationRegistrationUniversalIdentifier/:logicFunctionUniversalIdentifier
|
||||
```
|
||||
|
||||
두 식별자는 모두 매니페스트에 있는 `universalIdentifier`입니다. 각각 애플리케이션 등록과 이 로직 함수의 `universalIdentifier`입니다. 해당 URL을 공급자에 등록하세요.
|
||||
|
||||
**워크스페이스 해석.** 하나의 엔드포인트가 모든 워크스페이스를 처리하므로, 통합에서는 대상 `workspaceId`를 전달 데이터 어딘가에 넣어야 하고, `workspaceIdResolver.{ source, path }`는 플랫폼에 그 값을 어디에서 읽어야 하는지 알려 줍니다:
|
||||
|
||||
| 필드 | 값 | 노트 |
|
||||
| -------- | ----------------------------------------- | ------------------------------------------------------------------------------------------------------------ |
|
||||
| `source` | `body` \| `query` \| `header` | `body`는 파싱된 JSON을 읽습니다. `query`가 가장 범용적입니다. 일반적으로 등록하는 콜백 URL을 제어할 수 있으므로, `?twentyWorkspaceId=…`를 추가하면 됩니다. |
|
||||
| `path` | dot-path, 예: `metadata.twentyWorkspaceId` | 영숫자 / `_` / `-` 세그먼트로만 제한되며, prototype 키는 거부됩니다. |
|
||||
|
||||
해석된 값은 유효한 워크스페이스 UUID여야 **하고**, 앱이 해당 워크스페이스에 설치되어 있어야 합니다. 그렇지 않으면 함수가 실행되기 전에 요청이 거부됩니다.
|
||||
|
||||
<Warning>
|
||||
**서명 검증은 사용자 책임입니다.** 이 트리거에 대해 플랫폼은 웹훅 서명을 검증하지 않습니다. 워크스페이스를 해석하고 함수를 실행할 뿐입니다. 핸들러는 `event.rawBody`와 `forwardedRequestHeaders`에 나열한 헤더를 사용해 직접 서명을 검증해야 하며, 서버/애플리케이션 변수로 저장된 시크릿과 비교해야 합니다. 항상 부수 효과가 발생하기 **이전**에 서명을 검증하고, 상수 시간 비교를 사용하세요.
|
||||
</Warning>
|
||||
|
||||
대부분의 공급자는 HMAC-SHA256으로 서명합니다. 서로 다른 부분은 헤더 이름, 다이제스트 인코딩, 그리고 서명 대상 페이로드 문자열입니다. 몇 가지 예시는 다음과 같습니다:
|
||||
|
||||
| 공급자 | 포워딩할 헤더 | 서명 문자열 | 다이제스트 |
|
||||
| ---------------------------- | ------------------------------------------------------ | ---------------------------- | ---------------------------------- |
|
||||
| Svix (Recall, Resend, Clerk) | `webhook-id`, `webhook-timestamp`, `webhook-signature` | `{id}.{timestamp}.{rawBody}` | base64 (`whsec_` 제거 후 시크릿이 base64) |
|
||||
| Stripe | `stripe-signature` | `{timestamp}.{rawBody}` | hex |
|
||||
| GitHub | `x-hub-signature-256` | `{rawBody}` | hex (`sha256=` prefix 포함) |
|
||||
| Shopify | `x-shopify-hmac-sha256` | `{rawBody}` | base64 |
|
||||
| Slack | `x-slack-signature`, `x-slack-request-timestamp` | `v0:{timestamp}:{rawBody}` | hex (`v0=` prefix 포함) |
|
||||
|
||||
```ts
|
||||
import { createHmac, timingSafeEqual } from 'crypto';
|
||||
|
||||
const handler = async (event: RoutePayload) => {
|
||||
const signature = event.headers['x-hub-signature-256'] ?? '';
|
||||
const expected =
|
||||
'sha256=' +
|
||||
createHmac('sha256', process.env.GITHUB_WEBHOOK_SECRET ?? '')
|
||||
.update(event.rawBody ?? '')
|
||||
.digest('hex');
|
||||
|
||||
const a = Buffer.from(signature);
|
||||
const b = Buffer.from(expected);
|
||||
|
||||
if (a.length !== b.length || !timingSafeEqual(a, b)) {
|
||||
return new Response({ error: 'invalid signature' }, { status: 401 });
|
||||
}
|
||||
|
||||
// ...handle the verified event
|
||||
return { received: true };
|
||||
};
|
||||
```
|
||||
|
||||
<Note>
|
||||
함수는 **동기적으로** 실행되며, 반환한 값이 HTTP 응답이 됩니다. 따라서 프로바이더는 상태 코드를 확인하고 2xx가 아닐 경우 재시도할 수 있습니다. 핸들러는 빠르게 유지하세요. 일부 공급자(예: Slack)는 몇 초 안에 타임아웃됩니다. 함수가 서명을 확인하기 전에 실행되므로, 이 엔드포인트는 엣지에서 레이트 리밋으로 보호하세요.
|
||||
</Note>
|
||||
|
||||
#### 데이터베이스 이벤트 트리거 페이로드
|
||||
|
||||
데이터베이스 이벤트 트리거가 로직 함수를 호출하면, 변경된 각 레코드마다 하나의 `DatabaseEventPayload`를 받습니다. 이 페이로드는 소스 워크스페이스와 오브젝트에 대한 메타데이터를 레코드 수준 이벤트와 결합합니다.
|
||||
|
||||
@@ -60,6 +60,7 @@ Para invocar uma função de lógica acionada por rota a partir de um componente
|
||||
* **cron**: Executa sua função em um agendamento usando uma expressão CRON.
|
||||
* **databaseEvent**: Executa em eventos do ciclo de vida de objetos do espaço de trabalho. Quando a operação do evento é `updated`, campos específicos a serem observados podem ser especificados no array `updatedFields`. Se deixar indefinido ou vazio, qualquer atualização acionará a função.
|
||||
> por exemplo, `person.updated`, `*.created`, `company.*`
|
||||
* **serverWebhook**: Recebe webhooks de entrada de um serviço de terceiros (Stripe, GitHub, Svix, …) em um único endpoint com escopo de registro e resolve o workspace de destino a partir do payload. Veja [gatilho de webhook de servidor](#server-webhook-trigger).
|
||||
|
||||
<Note>
|
||||
Você também pode executar manualmente uma função usando a CLI:
|
||||
@@ -171,6 +172,90 @@ Por motivos de segurança, os cabeçalhos de resposta são restringidos a uma li
|
||||
O código de status deve ser um código de status HTTP válido (entre 100 e 599). Os nomes dos cabeçalhos de resposta são comparados sem distinção entre maiúsculas e minúsculas.
|
||||
</Note>
|
||||
|
||||
#### Gatilho de webhook do servidor
|
||||
|
||||
`httpRouteTriggerSettings` expõe uma função em `/s/` e resolve o workspace a partir do host da solicitação — o que funciona quando cada workspace tem seu próprio domínio. Provedores de terceiros, entretanto, entregam os eventos de todos os workspaces para **uma** URL de webhook. Para esse caso, use `serverWebhookTriggerSettings`: a função fica acessível em um endpoint com escopo de registro e o workspace é resolvido a partir do payload.
|
||||
|
||||
```ts src/logic-functions/handle-provider-webhook.logic-function.ts
|
||||
import { defineLogicFunction } from 'twenty-sdk/define';
|
||||
import type { RoutePayload } from 'twenty-sdk/logic-function';
|
||||
import { Response } from 'twenty-sdk/logic-function';
|
||||
|
||||
const handler = async (event: RoutePayload) => {
|
||||
// Verify the signature yourself before doing anything (see below).
|
||||
// Return a non-2xx Response to make the provider retry.
|
||||
return { received: true };
|
||||
};
|
||||
|
||||
export default defineLogicFunction({
|
||||
universalIdentifier: 'b3c2f0a1-7d4e-4c9a-9f2b-2e1d6a4c8e10',
|
||||
name: 'handle-provider-webhook',
|
||||
handler,
|
||||
serverWebhookTriggerSettings: {
|
||||
workspaceIdResolver: { source: 'body', path: 'metadata.twentyWorkspaceId' },
|
||||
forwardedRequestHeaders: ['webhook-id', 'webhook-timestamp', 'webhook-signature'],
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
A função fica acessível em:
|
||||
|
||||
```
|
||||
POST https://your-twenty-server.com/webhooks/server/:applicationRegistrationUniversalIdentifier/:logicFunctionUniversalIdentifier
|
||||
```
|
||||
|
||||
Ambos os identificadores são os `universalIdentifier`s do seu manifesto — o da aplicação registrada e o desta função de lógica. Registre essa URL junto ao provedor.
|
||||
|
||||
**Resolução de workspace.** Como um endpoint atende a todos os workspaces, sua integração deve colocar o `workspaceId` de destino em algum lugar na entrega, e `workspaceIdResolver.{ source, path }` informa à plataforma onde lê-lo:
|
||||
|
||||
| Campo | Valores | Notas |
|
||||
| -------- | ------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `source` | `body` \| `query` \| `header` | `body` lê o JSON já analisado. `query` é o mais universal — você normalmente controla a URL de callback que registra, então acrescente `?twentyWorkspaceId=…`. |
|
||||
| `path` | caminho com pontos, por exemplo, `metadata.twentyWorkspaceId` | Restrito a segmentos alfanuméricos / `_` / `-`; chaves de protótipo são rejeitadas. |
|
||||
|
||||
O valor resolvido deve ser um UUID de workspace válido **e** seu aplicativo deve estar instalado nesse workspace; caso contrário, a solicitação é rejeitada antes que a função seja executada.
|
||||
|
||||
<Warning>
|
||||
**A verificação da assinatura é de sua responsabilidade.** A plataforma não verifica assinaturas de webhook para esse gatilho — ela apenas resolve o workspace e executa sua função. Seu handler deve verificar a assinatura usando `event.rawBody` e os headers que você listou em `forwardedRequestHeaders`, comparando com um segredo armazenado como variável de servidor/aplicação. Sempre verifique **antes** de qualquer efeito colateral e use uma comparação em tempo constante.
|
||||
</Warning>
|
||||
|
||||
A maioria dos provedores assina com HMAC-SHA256; as partes que diferem são o nome do header, a codificação do digest e a string de payload assinada. Alguns exemplos:
|
||||
|
||||
| Provedor | Headers a encaminhar | String assinada | Digest |
|
||||
| ---------------------------- | ------------------------------------------------------ | ---------------------------- | ------------------------------------------------------- |
|
||||
| Svix (Recall, Resend, Clerk) | `webhook-id`, `webhook-timestamp`, `webhook-signature` | `{id}.{timestamp}.{rawBody}` | base64 (o segredo está em base64 após remover `whsec_`) |
|
||||
| Stripe | `stripe-signature` | `{timestamp}.{rawBody}` | hex |
|
||||
| GitHub | `x-hub-signature-256` | `{rawBody}` | hex (prefixado com `sha256=`) |
|
||||
| Shopify | `x-shopify-hmac-sha256` | `{rawBody}` | base64 |
|
||||
| Slack | `x-slack-signature`, `x-slack-request-timestamp` | `v0:{timestamp}:{rawBody}` | hex (prefixado com `v0=`) |
|
||||
|
||||
```ts
|
||||
import { createHmac, timingSafeEqual } from 'crypto';
|
||||
|
||||
const handler = async (event: RoutePayload) => {
|
||||
const signature = event.headers['x-hub-signature-256'] ?? '';
|
||||
const expected =
|
||||
'sha256=' +
|
||||
createHmac('sha256', process.env.GITHUB_WEBHOOK_SECRET ?? '')
|
||||
.update(event.rawBody ?? '')
|
||||
.digest('hex');
|
||||
|
||||
const a = Buffer.from(signature);
|
||||
const b = Buffer.from(expected);
|
||||
|
||||
if (a.length !== b.length || !timingSafeEqual(a, b)) {
|
||||
return new Response({ error: 'invalid signature' }, { status: 401 });
|
||||
}
|
||||
|
||||
// ...handle the verified event
|
||||
return { received: true };
|
||||
};
|
||||
```
|
||||
|
||||
<Note>
|
||||
A função é executada **sincronamente** e o valor que você retorna se torna a resposta HTTP, portanto os provedores veem seu código de status e podem tentar novamente em caso de não 2xx. Mantenha os handlers rápidos — alguns provedores (por exemplo, Slack) atingem timeout em poucos segundos. Como a função é executada antes de a assinatura ser verificada, proteja esse endpoint com rate limiting na sua borda.
|
||||
</Note>
|
||||
|
||||
#### Payload do gatilho de evento do banco de dados
|
||||
|
||||
Quando um gatilho de evento do banco de dados invoca sua função lógica, ela recebe um `DatabaseEventPayload` por registro alterado. O payload combina metadados sobre o workspace e o objeto de origem com o evento em nível de registro.
|
||||
|
||||
@@ -60,6 +60,7 @@ Pentru a apela o funcție logică declanșată de o rută dintr-o componentă fr
|
||||
* **cron**: Rulează funcția pe un program folosind o expresie CRON.
|
||||
* **databaseEvent**: Rulează la evenimentele ciclului de viață ale obiectelor din spațiul de lucru. Când operațiunea evenimentului este `updated`, câmpurile specifice de urmărit pot fi specificate în array-ul `updatedFields`. Dacă este lăsat nedefinit sau gol, orice actualizare va declanșa funcția.
|
||||
> de ex. `person.updated`, `*.created`, `company.*`
|
||||
* **serverWebhook**: Primește webhook-uri de intrare de la un serviciu terț (Stripe, GitHub, Svix, …) la un singur endpoint la nivelul înregistrării și determină spațiul de lucru țintă din payload. Consultați [declanșatorul de webhook de server](#server-webhook-trigger).
|
||||
|
||||
<Note>
|
||||
Puteți, de asemenea, să executați manual o funcție folosind CLI:
|
||||
@@ -172,6 +173,90 @@ Din motive de securitate, anteturile de răspuns sunt limitate la o listă de an
|
||||
Codul de stare trebuie să fie un cod de stare HTTP valid (între 100 și 599). Numele anteturilor de răspuns sunt comparate fără a ține cont de majuscule și minuscule.
|
||||
</Note>
|
||||
|
||||
#### Declanșator webhook de server
|
||||
|
||||
`httpRouteTriggerSettings` expune o funcție sub `/s/` și rezolvă spațiul de lucru din gazda cererii — ceea ce funcționează atunci când fiecare spațiu de lucru are propriul domeniu. Furnizorii terți, însă, livrează evenimentele fiecărui tenant către **un** singur URL de webhook. Pentru acest caz, folosiți `serverWebhookTriggerSettings`: funcția este accesibilă la un endpoint la nivelul înregistrării, iar spațiul de lucru este determinat din payload.
|
||||
|
||||
```ts src/logic-functions/handle-provider-webhook.logic-function.ts
|
||||
import { defineLogicFunction } from 'twenty-sdk/define';
|
||||
import type { RoutePayload } from 'twenty-sdk/logic-function';
|
||||
import { Response } from 'twenty-sdk/logic-function';
|
||||
|
||||
const handler = async (event: RoutePayload) => {
|
||||
// Verify the signature yourself before doing anything (see below).
|
||||
// Return a non-2xx Response to make the provider retry.
|
||||
return { received: true };
|
||||
};
|
||||
|
||||
export default defineLogicFunction({
|
||||
universalIdentifier: 'b3c2f0a1-7d4e-4c9a-9f2b-2e1d6a4c8e10',
|
||||
name: 'handle-provider-webhook',
|
||||
handler,
|
||||
serverWebhookTriggerSettings: {
|
||||
workspaceIdResolver: { source: 'body', path: 'metadata.twentyWorkspaceId' },
|
||||
forwardedRequestHeaders: ['webhook-id', 'webhook-timestamp', 'webhook-signature'],
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Funcția este accesibilă la:
|
||||
|
||||
```
|
||||
POST https://your-twenty-server.com/webhooks/server/:applicationRegistrationUniversalIdentifier/:logicFunctionUniversalIdentifier
|
||||
```
|
||||
|
||||
Ambii identificatori sunt `universalIdentifier` din manifestul dvs. — cel al înregistrării aplicației și al acestei funcții logice. Înregistrați acel URL la furnizor.
|
||||
|
||||
**Rezolvarea spațiului de lucru.** Deoarece un singur endpoint deservește fiecare spațiu de lucru, integrarea dvs. trebuie să plaseze `workspaceId` țintă undeva în livrare, iar `workspaceIdResolver.{ source, path }` îi indică platformei de unde să îl citească:
|
||||
|
||||
| Câmp | Valori | Notițe |
|
||||
| -------- | --------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `source` | `body` \| `query` \| `header` | `body` citește JSON-ul deja parsificat. `query` este cel mai universal — de obicei controlați URL-ul de callback pe care îl înregistrați, așa că adăugați `?twentyWorkspaceId=…`. |
|
||||
| `path` | dot-path, de ex. `metadata.twentyWorkspaceId` | Restricționat la segmente alfanumerice / `_` / `-`; cheile prototype sunt respinse. |
|
||||
|
||||
Valoarea rezolvată trebuie să fie un UUID de spațiu de lucru valid **și** aplicația dvs. trebuie să fie instalată în acel spațiu de lucru, altfel cererea este respinsă înainte ca funcția să ruleze.
|
||||
|
||||
<Warning>
|
||||
**Verificarea semnăturii este responsabilitatea dvs.** Platforma nu verifică semnăturile webhook pentru acest declanșator — doar rezolvă spațiul de lucru și rulează funcția. Handlerul dvs. trebuie să verifice singur semnătura folosind `event.rawBody` și headerele pe care le-ați enumerat în `forwardedRequestHeaders`, comparând cu un secret stocat ca variabilă de server/aplicație. Verificați întotdeauna **înainte** de orice efect secundar și folosiți o comparație în timp constant.
|
||||
</Warning>
|
||||
|
||||
Majoritatea furnizorilor semnează cu HMAC-SHA256; părțile care diferă sunt numele headerului, codificarea digestului și șirul de payload semnat. Câteva exemple:
|
||||
|
||||
| Furnizor | Headere de redirecționat | Șir semnat | Digest |
|
||||
| ---------------------------- | ------------------------------------------------------ | ---------------------------- | -------------------------------------------------------------------- |
|
||||
| Svix (Recall, Resend, Clerk) | `webhook-id`, `webhook-timestamp`, `webhook-signature` | `{id}.{timestamp}.{rawBody}` | base64 (secretul este în base64 după eliminarea prefixului `whsec_`) |
|
||||
| Stripe | `stripe-signature` | `{timestamp}.{rawBody}` | hex |
|
||||
| GitHub | `x-hub-signature-256` | `{rawBody}` | hex (cu prefixul `sha256=`) |
|
||||
| Shopify | `x-shopify-hmac-sha256` | `{rawBody}` | base64 |
|
||||
| Slack | `x-slack-signature`, `x-slack-request-timestamp` | `v0:{timestamp}:{rawBody}` | hex (cu prefixul `v0=`) |
|
||||
|
||||
```ts
|
||||
import { createHmac, timingSafeEqual } from 'crypto';
|
||||
|
||||
const handler = async (event: RoutePayload) => {
|
||||
const signature = event.headers['x-hub-signature-256'] ?? '';
|
||||
const expected =
|
||||
'sha256=' +
|
||||
createHmac('sha256', process.env.GITHUB_WEBHOOK_SECRET ?? '')
|
||||
.update(event.rawBody ?? '')
|
||||
.digest('hex');
|
||||
|
||||
const a = Buffer.from(signature);
|
||||
const b = Buffer.from(expected);
|
||||
|
||||
if (a.length !== b.length || !timingSafeEqual(a, b)) {
|
||||
return new Response({ error: 'invalid signature' }, { status: 401 });
|
||||
}
|
||||
|
||||
// ...handle the verified event
|
||||
return { received: true };
|
||||
};
|
||||
```
|
||||
|
||||
<Note>
|
||||
Funcția rulează **sincron**, iar valoarea returnată devine răspunsul HTTP, astfel încât furnizorii văd codul de stare și pot reîncerca pentru coduri non-2xx. Mențineți handler-ele rapide — unii furnizori (de ex. Slack) expiră după câteva secunde. Deoarece funcția rulează înainte ca semnătura să fie verificată, protejați acest endpoint cu limitare de rată la edge.
|
||||
</Note>
|
||||
|
||||
#### Payload-ul declanșatorului de eveniment al bazei de date
|
||||
|
||||
Când un declanșator de eveniment al bazei de date apelează funcția dvs. logică, aceasta primește un `DatabaseEventPayload` pentru fiecare înregistrare modificată. Payload-ul combină metadatele despre spațiul de lucru și obiectul sursă cu evenimentul la nivel de înregistrare.
|
||||
|
||||
@@ -60,6 +60,7 @@ export default defineLogicFunction({
|
||||
* **cron**: Запускает вашу функцию по расписанию с использованием выражения CRON.
|
||||
* **databaseEvent**: Запускается при событиях жизненного цикла объектов рабочего пространства. Когда операция события — `updated`, можно указать конкретные поля для отслеживания в массиве `updatedFields`. Если оставить не заданным или пустым, любое обновление будет вызывать функцию.
|
||||
> например, `person.updated`, `*.created`, `company.*`
|
||||
* **serverWebhook**: получает входящие вебхуки от стороннего сервиса (Stripe, GitHub, Svix, …) на единственной конечной точке в области регистрации и определяет целевое рабочее пространство из полезной нагрузки. См. [триггер серверного вебхука](#server-webhook-trigger).
|
||||
|
||||
<Note>
|
||||
Вы также можете вручную выполнить функцию с помощью CLI:
|
||||
@@ -171,6 +172,90 @@ const handler = async (event: RoutePayload) => {
|
||||
Код состояния должен быть допустимым кодом состояния HTTP (в диапазоне от 100 до 599). Имена заголовков ответа сравниваются без учета регистра.
|
||||
</Note>
|
||||
|
||||
#### Серверный триггер вебхука
|
||||
|
||||
`httpRouteTriggerSettings` предоставляет функцию по пути `/s/` и определяет рабочее пространство из хоста запроса — это работает, когда у каждого рабочего пространства свой домен. Поставщики сторонних сервисов, однако, отправляют события всех арендаторов на **один** URL вебхука. В этом случае используйте `serverWebhookTriggerSettings`: функция доступна на конечной точке в области регистрации, а рабочее пространство определяется из полезной нагрузки.
|
||||
|
||||
```ts src/logic-functions/handle-provider-webhook.logic-function.ts
|
||||
import { defineLogicFunction } from 'twenty-sdk/define';
|
||||
import type { RoutePayload } from 'twenty-sdk/logic-function';
|
||||
import { Response } from 'twenty-sdk/logic-function';
|
||||
|
||||
const handler = async (event: RoutePayload) => {
|
||||
// Verify the signature yourself before doing anything (see below).
|
||||
// Return a non-2xx Response to make the provider retry.
|
||||
return { received: true };
|
||||
};
|
||||
|
||||
export default defineLogicFunction({
|
||||
universalIdentifier: 'b3c2f0a1-7d4e-4c9a-9f2b-2e1d6a4c8e10',
|
||||
name: 'handle-provider-webhook',
|
||||
handler,
|
||||
serverWebhookTriggerSettings: {
|
||||
workspaceIdResolver: { source: 'body', path: 'metadata.twentyWorkspaceId' },
|
||||
forwardedRequestHeaders: ['webhook-id', 'webhook-timestamp', 'webhook-signature'],
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Функция доступна по адресу:
|
||||
|
||||
```
|
||||
POST https://your-twenty-server.com/webhooks/server/:applicationRegistrationUniversalIdentifier/:logicFunctionUniversalIdentifier
|
||||
```
|
||||
|
||||
Оба идентификатора — это `universalIdentifier` из вашего манифеста: регистрации приложения и этой логической функции. Зарегистрируйте этот URL у поставщика.
|
||||
|
||||
**Определение рабочего пространства.** Поскольку одна конечная точка обслуживает все рабочие пространства, ваша интеграция должна поместить целевой `workspaceId` в передаваемые данные, а `workspaceIdResolver.{ source, path }` указывает платформе, откуда его прочитать:
|
||||
|
||||
| Поле | Значения | Заметки |
|
||||
| -------- | ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| `source` | `body` \| `query` \| `header` | `body` читает разобранный JSON. `query` — самый универсальный вариант: вы обычно контролируете URL обратного вызова, который регистрируете, поэтому добавьте `?twentyWorkspaceId=…`. |
|
||||
| `path` | точечный путь, например `metadata.twentyWorkspaceId` | Ограничено сегментами из буквенно-цифровых символов / `_` / `-`; ключи прототипа отклоняются. |
|
||||
|
||||
Определённое значение должно быть действительным UUID рабочего пространства, и ваше приложение должно быть установлено в этом рабочем пространстве, иначе запрос будет отклонён до запуска функции.
|
||||
|
||||
<Warning>
|
||||
**Проверка подписи — ваша ответственность.** Платформа не проверяет подписи вебхуков для этого триггера — она только определяет рабочее пространство и запускает вашу функцию. Ваш обработчик должен самостоятельно проверить подпись, используя `event.rawBody` и заголовки, перечисленные в `forwardedRequestHeaders`, сравнивая с секретом, хранящимся как серверная/приложенческая переменная. Всегда выполняйте проверку **до** любых побочных эффектов и используйте сравнение с постоянным временем выполнения.
|
||||
</Warning>
|
||||
|
||||
Большинство провайдеров подписывают с помощью HMAC-SHA256; различаются имя заголовка, кодировка дайджеста и строка подписываемой полезной нагрузки. Несколько примеров:
|
||||
|
||||
| Провайдер | Заголовки для пересылки | Подписываемая строка | Дайджест |
|
||||
| ---------------------------- | ------------------------------------------------------ | ---------------------------- | ----------------------------------------------------------------- |
|
||||
| Svix (Recall, Resend, Clerk) | `webhook-id`, `webhook-timestamp`, `webhook-signature` | `{id}.{timestamp}.{rawBody}` | base64 (секрет в формате base64 после удаления префикса `whsec_`) |
|
||||
| Stripe | `stripe-signature` | `{timestamp}.{rawBody}` | hex |
|
||||
| GitHub | `x-hub-signature-256` | `{rawBody}` | hex (с префиксом `sha256=`) |
|
||||
| Shopify | `x-shopify-hmac-sha256` | `{rawBody}` | base64 |
|
||||
| Слэк | `x-slack-signature`, `x-slack-request-timestamp` | `v0:{timestamp}:{rawBody}` | hex (с префиксом `v0=`) |
|
||||
|
||||
```ts
|
||||
import { createHmac, timingSafeEqual } from 'crypto';
|
||||
|
||||
const handler = async (event: RoutePayload) => {
|
||||
const signature = event.headers['x-hub-signature-256'] ?? '';
|
||||
const expected =
|
||||
'sha256=' +
|
||||
createHmac('sha256', process.env.GITHUB_WEBHOOK_SECRET ?? '')
|
||||
.update(event.rawBody ?? '')
|
||||
.digest('hex');
|
||||
|
||||
const a = Buffer.from(signature);
|
||||
const b = Buffer.from(expected);
|
||||
|
||||
if (a.length !== b.length || !timingSafeEqual(a, b)) {
|
||||
return new Response({ error: 'invalid signature' }, { status: 401 });
|
||||
}
|
||||
|
||||
// ...handle the verified event
|
||||
return { received: true };
|
||||
};
|
||||
```
|
||||
|
||||
<Note>
|
||||
Функция выполняется **синхронно**, и возвращаемое вами значение становится HTTP-ответом, поэтому провайдеры видят ваш статус-код и могут повторить запрос при не-2xx коде. Делайте обработчики быстрыми — некоторые провайдеры (например, Slack) прерывают запрос через несколько секунд. Поскольку функция выполняется до проверки подписи, защитите эту конечную точку ограничением частоты на периметре (edge).
|
||||
</Note>
|
||||
|
||||
#### Полезная нагрузка триггера события базы данных
|
||||
|
||||
Когда триггер события базы данных вызывает вашу функцию логики, она получает по одному `DatabaseEventPayload` на каждую изменённую запись. Полезная нагрузка объединяет метаданные о рабочем пространстве-источнике и объекте с событием на уровне записи.
|
||||
|
||||
@@ -60,6 +60,7 @@ Arayüzsüz bir ön uç bileşeninden rota tarafından tetiklenen mantık fonksi
|
||||
* **cron**: Bir CRON ifadesi kullanarak işlevinizi bir zamanlamayla çalıştırır.
|
||||
* **databaseEvent**: Çalışma alanı nesnesi yaşam döngüsü olaylarında çalışır. Olay işlemi `updated` olduğunda, dinlenecek belirli alanlar `updatedFields` dizisinde belirtilebilir. Tanımsız veya boş bırakılırsa, herhangi bir güncelleme işlevi tetikler.
|
||||
> örn. `person.updated`, `*.created`, `company.*`
|
||||
* **serverWebhook**: Üçüncü taraf bir hizmetten (Stripe, GitHub, Svix, …) gelen webhook'ları alır tek bir kayıt kapsamlı uç noktadan ve hedef çalışma alanını payload'dan çözümler. [Sunucu webhook tetikleyicisine](#server-webhook-trigger) bakın.
|
||||
|
||||
<Note>
|
||||
Bir işlevi CLI kullanarak manuel olarak da çalıştırabilirsiniz:
|
||||
@@ -172,6 +173,90 @@ Güvenlik nedenleriyle, yanıt üstbilgileri bir izin listesiyle sınırlandır
|
||||
Durum kodu geçerli bir HTTP durum kodu olmalıdır (100 ile 599 arasında). Yanıt üstbilgisi adları büyük/küçük harfe duyarsız olarak eşleştirilir.
|
||||
</Note>
|
||||
|
||||
#### Sunucu webhook tetikleyicisi
|
||||
|
||||
`httpRouteTriggerSettings`, `/s/` altında bir fonksiyon sunar ve çalışma alanını istek ana bilgisayarından çözümler — bu da her çalışma alanının kendi alan adına sahip olduğu durumda işe yarar. Üçüncü taraf sağlayıcılar ise, her kiracının olaylarını **tek** webhook URL’sine iletir. Bu durum için `serverWebhookTriggerSettings` kullanın: fonksiyona kayıt kapsamlı bir uç noktadan erişilebilir ve çalışma alanı, payload’dan çözümlenir.
|
||||
|
||||
```ts src/logic-functions/handle-provider-webhook.logic-function.ts
|
||||
import { defineLogicFunction } from 'twenty-sdk/define';
|
||||
import type { RoutePayload } from 'twenty-sdk/logic-function';
|
||||
import { Response } from 'twenty-sdk/logic-function';
|
||||
|
||||
const handler = async (event: RoutePayload) => {
|
||||
// Verify the signature yourself before doing anything (see below).
|
||||
// Return a non-2xx Response to make the provider retry.
|
||||
return { received: true };
|
||||
};
|
||||
|
||||
export default defineLogicFunction({
|
||||
universalIdentifier: 'b3c2f0a1-7d4e-4c9a-9f2b-2e1d6a4c8e10',
|
||||
name: 'handle-provider-webhook',
|
||||
handler,
|
||||
serverWebhookTriggerSettings: {
|
||||
workspaceIdResolver: { source: 'body', path: 'metadata.twentyWorkspaceId' },
|
||||
forwardedRequestHeaders: ['webhook-id', 'webhook-timestamp', 'webhook-signature'],
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Fonksiyona şu adresten erişilebilir:
|
||||
|
||||
```
|
||||
POST https://your-twenty-server.com/webhooks/server/:applicationRegistrationUniversalIdentifier/:logicFunctionUniversalIdentifier
|
||||
```
|
||||
|
||||
Her iki tanımlayıcı da manifest’inizdeki `universalIdentifier` değerleridir — uygulama kaydınınki ve bu mantık fonksiyonununki. Bu URL’yi sağlayıcıya kaydedin.
|
||||
|
||||
**Çalışma alanı çözümleme.** Tek bir uç nokta her çalışma alanına hizmet verdiğinden, entegrasyonunuz hedef `workspaceId` değerini teslimatta bir yere koymalı ve `workspaceIdResolver.{ source, path }` platforma bunun nereden okunacağını söyler:
|
||||
|
||||
| Alan | Değerler | Notlar |
|
||||
| -------- | --------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `source` | `body` \| `query` \| `header` | `body`, ayrıştırılmış JSON’u okur. `query` en evrensel seçenektir — genellikle kaydettiğiniz callback URL’sini kontrol edersiniz, bu yüzden `?twentyWorkspaceId=…` ekleyin. |
|
||||
| `path` | nokta-yolu, örn. `metadata.twentyWorkspaceId` | Alfasayısal / `_` / `-` segmentleriyle sınırlandırılmıştır; prototype anahtarları reddedilir. |
|
||||
|
||||
Çözümlenen değer geçerli bir çalışma alanı UUID’si **olmalı** ve uygulamanızın o çalışma alanına yüklü olması gerekir, aksi halde istek, fonksiyon çalışmadan önce reddedilir.
|
||||
|
||||
<Warning>
|
||||
**İmza doğrulama sizin sorumluluğunuzdadır.** Platform bu tetikleyici için webhook imzalarını doğrulamaz — yalnızca çalışma alanını çözümler ve fonksiyonunuzu çalıştırır. İşleyiciniz imzayı, `event.rawBody` ve `forwardedRequestHeaders` içinde listelediğiniz başlıkları kullanarak, sunucu/uygulama değişkeni olarak saklanan bir gizli anahtarla karşılaştırıp kendisi doğrulamalıdır. Her zaman herhangi bir yan etkiden **önce** doğrulayın ve sabit süreli bir karşılaştırma kullanın.
|
||||
</Warning>
|
||||
|
||||
Çoğu sağlayıcı HMAC-SHA256 ile imzalar; farklı olan kısımlar başlık adı, özet kodlaması ve imzalanan payload dizesidir. Birkaç örnek:
|
||||
|
||||
| Sağlayıcı | İletilecek başlıklar | İmzalanmış dize | Özet |
|
||||
| ---------------------------- | ------------------------------------------------------ | ---------------------------- | ---------------------------------------------------------------- |
|
||||
| Svix (Recall, Resend, Clerk) | `webhook-id`, `webhook-timestamp`, `webhook-signature` | `{id}.{timestamp}.{rawBody}` | base64 (gizli anahtar, `whsec_` kaldırıldıktan sonra base64’tür) |
|
||||
| Stripe | `stripe-signature` | `{timestamp}.{rawBody}` | hex |
|
||||
| GitHub | `x-hub-signature-256` | `{rawBody}` | hex (`sha256=` önekiyle) |
|
||||
| Shopify | `x-shopify-hmac-sha256` | `{rawBody}` | base64 |
|
||||
| Slack | `x-slack-signature`, `x-slack-request-timestamp` | `v0:{timestamp}:{rawBody}` | hex (`v0=` önekiyle) |
|
||||
|
||||
```ts
|
||||
import { createHmac, timingSafeEqual } from 'crypto';
|
||||
|
||||
const handler = async (event: RoutePayload) => {
|
||||
const signature = event.headers['x-hub-signature-256'] ?? '';
|
||||
const expected =
|
||||
'sha256=' +
|
||||
createHmac('sha256', process.env.GITHUB_WEBHOOK_SECRET ?? '')
|
||||
.update(event.rawBody ?? '')
|
||||
.digest('hex');
|
||||
|
||||
const a = Buffer.from(signature);
|
||||
const b = Buffer.from(expected);
|
||||
|
||||
if (a.length !== b.length || !timingSafeEqual(a, b)) {
|
||||
return new Response({ error: 'invalid signature' }, { status: 401 });
|
||||
}
|
||||
|
||||
// ...handle the verified event
|
||||
return { received: true };
|
||||
};
|
||||
```
|
||||
|
||||
<Note>
|
||||
Fonksiyon **senkron** olarak çalışır ve döndürdüğünüz değer HTTP yanıtı olur, bu nedenle sağlayıcılar durum kodunuzu görür ve 2xx olmayanlarda yeniden deneyebilir. İşleyicileri hızlı tutun — bazı sağlayıcılar (örn. Slack) birkaç saniye içinde zaman aşımına uğrar. Fonksiyon imza denetlenmeden önce çalıştığı için, bu uç noktayı edge’inizde hız sınırlamasıyla koruyun.
|
||||
</Note>
|
||||
|
||||
#### Veritabanı olay tetikleyicisi yükü
|
||||
|
||||
Bir veritabanı olay tetikleyicisi mantık fonksiyonunuzu çağırdığında, değişen her kayıt için bir `DatabaseEventPayload` alır. Yük, kaynak çalışma alanı ve nesne hakkındaki üstveriyi, kayıt düzeyindeki olayla birleştirir.
|
||||
|
||||
@@ -60,6 +60,7 @@ export default defineLogicFunction({
|
||||
* **cron**:使用 CRON 表达式按计划运行你的函数。
|
||||
* **databaseEvent**:在工作区对象生命周期事件上运行。 当事件操作为 `updated` 时,可以在 `updatedFields` 数组中指定要监听的特定字段。 如果未定义或为空,任何更新都会触发该函数。
|
||||
> 例如 `person.updated`、`*.created`、`company.*`
|
||||
* **serverWebhook**:从第三方服务(Stripe、GitHub、Svix 等)接收入站 Webhooks 在单个以注册为作用域的端点上,并从有效负载中解析目标工作区。 参见 [服务端 Webhook 触发器](#server-webhook-trigger)。
|
||||
|
||||
<Note>
|
||||
你也可以使用 CLI 手动执行函数:
|
||||
@@ -172,6 +173,90 @@ const handler = async (event: RoutePayload) => {
|
||||
状态码必须是有效的 HTTP 状态码(介于 100 和 599 之间)。 响应头名称的匹配不区分大小写。
|
||||
</Note>
|
||||
|
||||
#### 服务端 Webhook 触发器
|
||||
|
||||
`httpRouteTriggerSettings` 在 `/s/` 下暴露一个函数,并根据请求主机解析 workspace——这在每个 workspace 都有自己域名时有效。 然而,第三方服务商会将每个租户的事件发送到**同一个** webhook URL。 对于这种情况,请使用 `serverWebhookTriggerSettings`:该函数可在注册范围的端点访问,并且 workspace 将从负载中解析。
|
||||
|
||||
```ts src/logic-functions/handle-provider-webhook.logic-function.ts
|
||||
import { defineLogicFunction } from 'twenty-sdk/define';
|
||||
import type { RoutePayload } from 'twenty-sdk/logic-function';
|
||||
import { Response } from 'twenty-sdk/logic-function';
|
||||
|
||||
const handler = async (event: RoutePayload) => {
|
||||
// Verify the signature yourself before doing anything (see below).
|
||||
// Return a non-2xx Response to make the provider retry.
|
||||
return { received: true };
|
||||
};
|
||||
|
||||
export default defineLogicFunction({
|
||||
universalIdentifier: 'b3c2f0a1-7d4e-4c9a-9f2b-2e1d6a4c8e10',
|
||||
name: 'handle-provider-webhook',
|
||||
handler,
|
||||
serverWebhookTriggerSettings: {
|
||||
workspaceIdResolver: { source: 'body', path: 'metadata.twentyWorkspaceId' },
|
||||
forwardedRequestHeaders: ['webhook-id', 'webhook-timestamp', 'webhook-signature'],
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
该函数可在以下地址访问:
|
||||
|
||||
```
|
||||
POST https://your-twenty-server.com/webhooks/server/:applicationRegistrationUniversalIdentifier/:logicFunctionUniversalIdentifier
|
||||
```
|
||||
|
||||
这两个标识符都是来自清单的 `universalIdentifier`——即应用注册的 universalIdentifier 和此逻辑函数的 universalIdentifier。 在服务商处注册该 URL。
|
||||
|
||||
**Workspace 解析。** 由于一个端点为每个 workspace 提供服务,你的集成必须在传递内容中的某处放入目标 `workspaceId`,而 `workspaceIdResolver.{ source, path }` 用于告知平台从哪里读取它:
|
||||
|
||||
| 字段 | 值 | 备注 |
|
||||
| ---- | ----------------------------------- | ----------------------------------------------------------------------------------- |
|
||||
| `来源` | `body` \| `query` \| `header` | `body` 读取解析后的 JSON。 `query` 是最通用的——通常你可以控制所注册的回调 URL,因此可以追加 `?twentyWorkspaceId=…`。 |
|
||||
| `路径` | 点路径,例如 `metadata.twentyWorkspaceId` | 仅限字母数字 / `_` / `-` 片段;原型键将被拒绝。 |
|
||||
|
||||
解析得到的值必须是有效的 workspace UUID,**并且** 你的应用必须已安装在该 workspace 中,否则请求会在函数运行前被拒绝。
|
||||
|
||||
<Warning>
|
||||
**签名验证由你负责。** 平台不会为此触发器验证 webhook 签名——它只会解析 workspace 并运行你的函数。 你的处理程序必须使用 `event.rawBody` 以及你在 `forwardedRequestHeaders` 中列出的请求头自行验证签名,并与作为服务器/应用变量存储的密钥进行比对。 始终在产生任何副作用**之前**进行验证,并使用常量时间比较。
|
||||
</Warning>
|
||||
|
||||
大多数服务商使用 HMAC-SHA256 进行签名;不同之处在于请求头名称、摘要编码方式以及被签名的负载字符串。 例如:
|
||||
|
||||
| 提供商 | 要转发的请求头 | 签名字符串 | 摘要 |
|
||||
| ------------------------- | ------------------------------------------------------ | ---------------------------- | ---------------------------------- |
|
||||
| Svix(Recall、Resend、Clerk) | `webhook-id`, `webhook-timestamp`, `webhook-signature` | `{id}.{timestamp}.{rawBody}` | base64(密钥在去掉 `whsec_` 前缀后为 base64) |
|
||||
| Stripe | `stripe-signature` | `{timestamp}.{rawBody}` | hex |
|
||||
| GitHub | `x-hub-signature-256` | `{rawBody}` | hex(前缀为 `sha256=`) |
|
||||
| Shopify | `x-shopify-hmac-sha256` | `{rawBody}` | base64 |
|
||||
| Slack | `x-slack-signature`, `x-slack-request-timestamp` | `v0:{timestamp}:{rawBody}` | hex(前缀为 `v0=`) |
|
||||
|
||||
```ts
|
||||
import { createHmac, timingSafeEqual } from 'crypto';
|
||||
|
||||
const handler = async (event: RoutePayload) => {
|
||||
const signature = event.headers['x-hub-signature-256'] ?? '';
|
||||
const expected =
|
||||
'sha256=' +
|
||||
createHmac('sha256', process.env.GITHUB_WEBHOOK_SECRET ?? '')
|
||||
.update(event.rawBody ?? '')
|
||||
.digest('hex');
|
||||
|
||||
const a = Buffer.from(signature);
|
||||
const b = Buffer.from(expected);
|
||||
|
||||
if (a.length !== b.length || !timingSafeEqual(a, b)) {
|
||||
return new Response({ error: 'invalid signature' }, { status: 401 });
|
||||
}
|
||||
|
||||
// ...handle the verified event
|
||||
return { received: true };
|
||||
};
|
||||
```
|
||||
|
||||
<Note>
|
||||
该函数**同步**运行,你返回的值会成为 HTTP 响应,因此服务商可以看到你的状态码,并在非 2xx 时重试。 保持处理程序足够快速——某些服务商(例如 Slack)会在几秒内超时。 由于函数在检查签名前运行,请在边缘通过速率限制来保护此端点。
|
||||
</Note>
|
||||
|
||||
#### 数据库事件触发器有效负载
|
||||
|
||||
当数据库事件触发器调用你的逻辑函数时,每条被更改的记录都会对应一个 `DatabaseEventPayload`。 该负载将关于源工作区和对象的元数据与记录级事件组合在一起。
|
||||
|
||||
Reference in New Issue
Block a user