Phase 1 — Hooks
extensionManager.scanAndLoadHooks() chạy trước khi Fastify server tồn tại. Toàn bộ filter(...), action(...), schedule(...) trong hooks/index.ts đăng ký lên emitter ở bước này.
Khai báo trong backend/extensions/helpdesk/package.json:
{ "name": "@codihaus/odp-extension-helpdesk", "main": "dist/index.js", "odp-extension": { "id": "helpdesk", "type": "bundle", "entries": [ { "type": "hook", "name": "helpdesk-hooks", "source": "src/hooks/index.ts" }, { "type": "endpoint", "name": "helpdesk", "source": "src/endpoints/index.ts" } ], "permissions": { "module": "helpdesk", "displayName": "Helpdesk", "actions": [ { "key": "read", "displayName": "View Helpdesk", "riskLevel": "low" }, { "key": "create", "displayName": "Create Helpdesk Records", "riskLevel": "low" }, { "key": "update", "displayName": "Edit Helpdesk Records", "riskLevel": "medium" }, { "key": "delete", "displayName": "Delete Helpdesk Records", "riskLevel": "medium" }, { "key": "manage-settings", "displayName": "Manage Settings", "riskLevel": "high" }, { "key": "manage-automation", "displayName": "Manage Automation", "riskLevel": "high" }, { "key": "manage-inboxes", "displayName": "Manage Inboxes", "riskLevel": "high" } ] } }}permissions.actions ở đây chính là danh sách action mà validateAppAccess sẽ đối chiếu — xem Endpoints, routes & guards. Khai báo permission ngay trong package.json để ODP tự sinh policy option trong Admin console, không cần migration riêng.
ODP boot theo thứ tự cố định (chi tiết đầy đủ ở skill odp-extension-dev → references/extension-loading.md):
Phase 1 — Hooks
extensionManager.scanAndLoadHooks() chạy trước khi Fastify server tồn tại. Toàn bộ filter(...), action(...), schedule(...) trong hooks/index.ts đăng ký lên emitter ở bước này.
Phase 2 — Endpoints
createServer() dựng Fastify xong mới gọi extensionManager.loadEndpoints() — lúc này endpoints/index.ts mới chạy, route mount vào router.
Route mount tại /helpdesk/* (bundle entry name = helpdesk, khớp name trong odp-extension.entries).
src/endpoints/index.ts chỉ làm một việc: build AppContext rồi giao cho registerRoutes:
import { defineEndpoint } from '@codihaus/odp-extensions-sdk';import type { AppContext } from './context.js';import { registerRoutes } from './routes.js';
export default defineEndpoint((router, { services, database, logger, env, getSchema }) => { const ctx: AppContext = { services, database, getSchema, log: logger as any, env: env as Record<string, unknown>, };
registerRoutes(router, ctx);});context.ts định nghĩa type dùng xuyên suốt mọi controller/service:
export interface AppContext { services: any; database: any; getSchema: () => Promise<any>; log: { info: (obj: unknown, msg?: string) => void; warn: (obj: unknown, msg?: string) => void; error: (obj: unknown, msg?: string) => void; }; env: Record<string, unknown>; // Optional so existing call sites (that build ctx without cache) still typecheck; // inbound services that need it guard for undefined. cache?: ExtensionCache; // Emitter for cross-module action events (e.g. notify.trigger → Power Notify). emitter?: { emitAction: (event: string, payload: any, context: any) => void };}hooks/index.ts build một AppContext tương tự nhưng có thêm cache (isolated store, dùng cho OAuth token cache + IMAP poison-message backoff) và emitter. Hai đầu vào (hook + endpoint) build ctx độc lập — không chia sẻ instance — nhưng cùng shape để các service dùng chung (sla.ts, automation/engine.ts) chạy được từ cả hai phía.
Mọi truy cập DB collection đi qua createItemsService(ctx, collection, accountability) — xem chi tiết cơ chế ép admin:true ở Master/Satellite & Trust model.
// package.json scripts"scripts": { "build": "node node_modules/@codihaus/odp-extensions-sdk/dist/cli.js build", "dev": "node node_modules/@codihaus/odp-extensions-sdk/dist/cli.js build --watch"}node_modules/@codihaus/odp-extensions-sdk không tồn tại cục bộ trong backend/extensions/helpdesk/ — Node resolution đi ngược lên cây thư mục và tìm thấy ở backend/node_modules/@codihaus/odp-extensions-sdk/dist/cli.js (shamefully-hoist). Chạy npm run build trong thư mục extension thực chất gọi CLI đã hoist lên backend/node_modules.
Build output là một file duy nhất dist/index.js (ESM, self-contained) — đây là file ODP require/import lúc boot (theo main trong package.json).
registerRoutes gắn guard cho từng routecreateItemsService và ranh giới quyền