Cogover Learning

API reference @cogover/sdk: Script và HTTP route

Trang này thuộc API reference của @cogover/sdk.

defineScript

function defineScript<
  Input = unknown,
  Output = unknown,
  Schema extends object = EffectiveWorkspaceObjects,
>(handler: ScriptHandler<Input, Output, Schema>): SandboxHandler;

Các type public liên quan:

type SandboxHandler = (inputJson: string) => Promise<string | null>;
 
type ScriptHandler<Input, Output, Schema extends object = EffectiveWorkspaceObjects> =
  (context: ScriptContext<Input, Schema>) =>
    Output | ScriptResponse<unknown> | null |
    Promise<Output | ScriptResponse<unknown> | null>;
 
interface ScriptContext<Input, Schema extends object = EffectiveWorkspaceObjects> {
  readonly input: Input;
  readonly request: ScriptRequest<Input>;
  readonly invocation: InvocationContext;
  readonly data: DataApi<Schema>;
  readonly schema: SchemaApi<Schema>;
  readonly log: ScriptLogger;
  readonly response: ResponseApi;
  readonly state: ProjectState;
  readonly locks: DistributedLocks;
  readonly push: PushApi<Schema>;
  readonly notifications: NotificationsApi;
  readonly email: EmailApi<Schema>;
  readonly jobs: JobsApi;
  readonly secrets: SecretsApi;
  readonly crypto: CryptoApi;
  readonly org: OrgApi;
  readonly limits: LimitsApi;
  readonly processes: ProcessesApi;
  readonly agents: AgentsApi;
}

defineScript ném ValidationError nếu handler không phải function. Handler kết quả ném lỗi nếu input không phải JSON hợp lệ hoặc output không serialize được sang JSON. Trả null tạo kết quả null; trả undefined là không hợp lệ.

Hàm wrapper parse input JSON, cung cấp input, request, invocation, response, data, schema, log, state, locks, push, notifications, email, jobs, secrets, crypto, org, limits, processes, agents, chờ kết quả async và serialize output thành JSON. limits đọc ngân sách của lần thực thi (xem Đọc giới hạn trong code), còn processes và agents khởi chạy Process và AI Agent (xem Process và AI Agent).

HTTP router và request context

ScriptContext<TInput, TSchema> cung cấp input, request, invocation, response, data, schema, log, state, locks, push, notifications, email, jobs, secrets, crypto, org, limits, processes và agents cho cả script handler lẫn route handler. input giữ input invocation hiện có. Nên dùng request.body cho dữ liệu nghiệp vụ: body không chứa invocation metadata và các field transport/xác thực đã được Cogover loại bỏ.

type RequestMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
type ScriptQuery = Readonly<Record<string, string | readonly string[]>>;
type ScriptHeaders = Readonly<Record<string, string>>;
type ScriptPathParams = Readonly<Record<string, string>>;
 
interface ScriptRequest<TBody = unknown> {
  readonly method: RequestMethod;
  readonly path: string;   // Tương đối với /api/v1/ts-projects/{projectSlug}
  readonly params: ScriptPathParams;
  readonly query: ScriptQuery;
  readonly headers: ScriptHeaders;
  readonly body: TBody;
  readonly rawBody?: string;      // Chỉ có với lời gọi webhook
  readonly contentType?: string;  // Chỉ có với lời gọi webhook
}

Request descriptor và các map metadata được freeze; body không bị deep-freeze. Method, path, query và header trong allowlist đến từ Cogover, không lấy từ field nghiệp vụ. Query lặp tên trở thành readonly array. Tên header được đổi sang chữ thường; credential như authorization và cookie không bao giờ lộ vào script. Path param được percent-decode sau khi route an toàn đã khớp. Kiểu TypeScript không validate input nghiệp vụ lúc chạy.

rawBody và contentType chỉ có mặt với lời gọi đi vào project qua inbound webhook (xem Inbound webhook): rawBody là request body đúng như đã nhận, dưới dạng chuỗi UTF-8, và contentType là header Content-Type của request. Với mọi lời gọi khác, cả hai field đều vắng mặt.

Danh tính invocation và workspace hiện tại

context.invocation là snapshot đồng bộ, bất biến được tạo từ metadata mà Cogover đã xác thực và nạp sẵn cho lần thực thi hiện tại. Đọc property này không gửi yêu cầu tới Data API.

type InvocationContext = UserInvocationContext | SystemInvocationContext | InboundInvocationContext;
 
interface CurrentWorkspace {
  readonly id: string;
  readonly name?: string;
  readonly domain?: string;
  readonly language?: string;
  readonly timezone?: string;
  readonly dateFormat?: string;
  readonly timeFormat?: string;
  readonly numberFormat?: string;
}
 
interface CurrentWorkspaceRole {
  readonly id: string;
  readonly name: string;
}
 
interface CurrentWorkspaceMembership {
  readonly personnelId?: string;
  readonly language?: string;
  readonly timezone?: string;
  readonly isSuperAdmin: boolean;                  // Có role Super Admin của workspace
  readonly roles: readonly CurrentWorkspaceRole[]; // Sắp xếp theo tên
}
 
interface CurrentUser {
  readonly accountId: string;
  readonly email?: string;
  readonly firstName?: string;
  readonly lastName?: string;
  readonly avatar?: string;
  readonly language?: string;
  readonly timezone?: string;
  readonly membership: CurrentWorkspaceMembership;
}
 
interface UserInvocationContext {
  readonly identity: "user";
  readonly workspace: CurrentWorkspace;
  readonly user: CurrentUser;
}
 
interface SystemInvocationContext {
  readonly identity: "system";
  readonly workspace: CurrentWorkspace;
  readonly user: null;
}
 
interface InboundInvocationContext {
  readonly identity: "inbound";
  readonly workspace: CurrentWorkspace;
  readonly user: null;
  readonly inbound: {
    readonly id: string;             // Nằm trong URL webhook
    readonly name: string;
    readonly mode: "KEY" | "HMAC";   // Cách lời gọi đã được xác thực
  };
}

Mọi field đều readonly. Các field profile/workspace optional có thể không tồn tại, vì vậy script không được giả định chúng luôn có giá trị.

identity là "user" với lời gọi của người dùng Workspace đã xác thực, "system" với lần thực thi không có người dùng, ví dụ job chạy theo lịch, và "inbound" với lời gọi webhook được xác thực bằng một inbound access của project (xem Inbound webhook). Invocation inbound không có user; inbound định danh inbound access đã xác thực lời gọi.

export default defineScript(({ invocation }) => {
  if (invocation.identity === "system") {
    return {workspaceId: invocation.workspace.id, user: null};
  }
  if (invocation.identity === "inbound") {
    return {workspaceId: invocation.workspace.id, inbound: invocation.inbound.name};
  }
  return {
    workspaceId: invocation.workspace.id,
    accountId: invocation.user.accountId,
    personnelId: invocation.user.membership.personnelId,
  };
});

Cogover bỏ qua invocation metadata do caller tự gửi và dựng snapshot từ request context đã xác thực. Raw authentication data, token, inbound key và credential không được đưa vào. Handler gọi bên ngoài Cogover không có invocation snapshot đã xác thực.

Role của người dùng hiện tại trong Workspace

user.membership mô tả tư cách thành viên của người gọi trong Workspace hiện tại:

  • isSuperAdmin là true khi một trong các role của người dùng là role quản trị cao nhất (Super Admin) của Workspace, ngược lại là false.
  • roles liệt kê các role được gán cho người dùng trong Workspace hiện tại, sắp xếp theo tên. Mỗi CurrentWorkspaceRole chỉ có id và name; danh sách rỗng khi người dùng không có role nào. Quyền của role không được cung cấp.

So sánh role theo id: role có thể được đổi tên, và cùng một tên role có id khác nhau ở mỗi Workspace. Quyền truy cập record và field vẫn do Cogover kiểm soát theo danh tính của người dùng, vì vậy hãy dùng các field này cho quy tắc nghiệp vụ, chẳng hạn ai được duyệt đơn hàng, chứ không thay thế cho quy tắc bảo mật.

const APPROVER_ROLE_ID = "RO6P2N1K9X4A";
 
export default defineScript(({ invocation }) => {
  const user = invocation.user;
  const canApprove = user !== null && (user.membership.isSuperAdmin
    || user.membership.roles.some((role) => role.id === APPROVER_ROLE_ID));
  return {canApprove};
});

Invocation HTTP phản ánh role của người dùng tại thời điểm gửi request. Record trigger và background job có thể nhận thay đổi role chậm tối đa khoảng một phút. Khi phát triển local bằng Cogover Dev CLI, các giá trị được lấy lúc development session bắt đầu; hãy khởi động lại cogover-dev để nhận thay đổi role. Invocation system hoặc inbound có user: null nên không có membership.

createRouter<TSchema = EffectiveWorkspaceObjects>(): ScriptRouter<TSchema>

Tạo router độc lập. TSchema extends object mặc định là schema workspace hiệu lực, giữ suy luận WorkspaceObjects qua module augmentation cho data và schema API. Route phân biệt hoa/thường theo method và path tĩnh/động; route tĩnh được ưu tiên hơn route động. Khi nhiều route động cùng method khớp một path, ví dụ /orders/:id và /:collection/new với /orders/new, route được đăng ký trước sẽ xử lý. / ứng với URL project canonical không có route suffix. /index là route riêng, phải đăng ký rõ ràng. defineScript() vẫn dùng cho /; gọi đường dẫn con sẽ ném NotFoundError trước khi handler của script chạy.

router.addRoute<TInput = unknown, TOutput = unknown>(method: RequestMethod, path: string, handler: ScriptHandler<TInput, TOutput, TSchema>): ScriptRouter<TSchema>

Đăng ký handler và trả lại chính router để gọi nối tiếp. Hỗ trợ GET, POST, PUT, PATCH, DELETE. Path là / hoặc các segment tĩnh/động như /orders/:orderId. Segment tĩnh bắt đầu bằng chữ ASCII hoặc số và có thể chứa chữ, số, _, -, dài tối đa 100 ký tự. Tên param bắt đầu bằng chữ ASCII, có thể chứa chữ, số, _, dài tối đa 64 ký tự và không được lặp trong cùng route. Toàn bộ path dài tối đa 1.024 ký tự. Dấu / cuối đường dẫn con, segment rỗng, ., .., percent escape trong template, ?, # và wildcard bị từ chối. Route trùng hoặc mơ hồ về cấu trúc trong cùng method, handler sai hoặc vượt 256 route đều ném ValidationError.

Handler là hàm ScriptHandler thông thường, không phải hàm đã bọc bằng defineScript(). Handler nhận cùng data/schema/log API, input và request context. Có thể đăng ký cùng một handler cho nhiều path.

router.get/post/put/patch/delete(path, handler): ScriptRouter<TSchema>

Cách viết ngắn theo từng method của addRoute(), cùng validation, giá trị trả về và lỗi.

router.toHandler(): SandboxHandler

Trả về handler để default-export, chụp lại danh sách route hiện tại. Đăng ký thêm sau đó không thay đổi handler đã tạo. Handler parse input, chọn đúng một route, chờ kết quả và serialize như defineScript(). null giữ nguyên null; kết quả undefined ném ValidationError. Route không tồn tại ném NotFoundError (HTTP 404), kể cả path chỉ được đăng ký cho method khác: router không trả 405 khi sai method. Invocation dùng method chưa hỗ trợ có CogoverApiError với code: "METHOD_NOT_ALLOWED" (HTTP 405). Metadata invocation sai ném ValidationError. Khi gọi trực tiếp mà không có request metadata của Cogover, handler mặc định là POST /.

Route kế thừa xác thực, quyền và giới hạn runtime của project. Đăng ký route không cấp thêm quyền hay tạo ranh giới bảo mật riêng. Idempotency replay có phạm vi theo method/path cùng phiên bản project và người gọi; dùng key mới cho mỗi thao tác nghiệp vụ khác nhau.

HTTP response API

Trả về giá trị JSON bình thường vẫn giữ hành vi tương thích 200 application/json. Dùng helper response trong context khi cần điều khiển status, header, content type hoặc dạng body. Chỉ response do các helper này tạo mới làm được việc đó: giá trị được trả về luôn được gửi như dữ liệu JSON với HTTP 200, kể cả khi có hình dạng giống envelope response nội bộ của Cogover, ví dụ request body được route trả lại nguyên vẹn.

router.post("/orders", ({ response }) => response.json({ id: "ORD-1" }, {
  status: 201,
  headers: { location: "/orders/ORD-1", "x-request-state": "created" },
}));
 
router.get("/files/:fileId", ({ response }) =>
  response.bytes(new Uint8Array([0, 1, 255]), { contentType: "application/octet-stream" }));
type ResponseHeaderValue = string | readonly string[];
 
interface ResponseInit {
  readonly status?: number;
  readonly headers?: Readonly<Record<string, ResponseHeaderValue>>;
}
 
interface BinaryResponseInit extends ResponseInit {
  readonly contentType?: string;
}
 
interface ResponseApi {
  json<T>(body: T, init?: ResponseInit): ScriptResponse<T>;
  text(body: string, init?: ResponseInit): ScriptResponse<string>;
  bytes(body: Uint8Array, init?: BinaryResponseInit): ScriptResponse<Uint8Array>;
  empty(init?: ResponseInit): ScriptResponse<null>;
  redirect(location: string, status?: 301 | 302 | 303 | 307 | 308): ScriptResponse<null>;
}
 
response.json(body, init?)
response.text(body, init?)
response.bytes(body: Uint8Array, { status?, headers?, contentType? }?)
response.empty(init?)                 // mặc định status 204
response.redirect(location, status?) // mặc định status 302

Status phải là số nguyên từ 200 đến 599. Giá trị header là một string hoặc readonly string array. Hop-by-hop header, header đặt credential và header dành riêng cho Cogover bị từ chối. Tên/giá trị header không được chứa newline; tối đa 50 header. Tên header dài tối đa 128 ký tự và mỗi giá trị tối đa 8.192 ký tự. Content type mặc định lần lượt là application/json; charset=utf-8, text/plain; charset=utf-8 và application/octet-stream. Redirect chỉ nhận 301, 302, 303, 307 hoặc 308; giá trị khác ném ValidationError. Helper trả ScriptResponse opaque; code ứng dụng không được tự tạo transport envelope. Redirect location phải khác rỗng, dài tối đa 4.096 ký tự và không chứa newline. response.json() không nhận undefined; response.text() yêu cầu string và response.bytes() yêu cầu Uint8Array.

Logging

context.log implement ScriptLogger:

interface ScriptLogger {
  debug(message: string, details?: unknown): void;
  info(message: string, details?: unknown): void;
  warn(message: string, details?: unknown): void;
  error(message: string, details?: unknown): void;
}

Mỗi lời gọi ghi một dòng vào standard output của lần thực thi, với mọi level: [LEVEL] message, sau đó là một dấu cách và details dạng JSON khi có details, ví dụ [WARN] Stock is low {"sku":"A-1","left":2}. details là string được ghi trong dấu ngoặc kép. Chuỗi JSON bị cắt ở 8.192 ký tự và khi đó kết thúc bằng …[truncated]. Giá trị JSON không biểu diễn được sẽ được ghi ở dạng dễ đọc thay vì gây lỗi: tham chiếu lặp lại tới một object bao ngoài thành "[Circular]", bigint thành chuỗi số thập phân, Error thành {"name":…,"message":…} không kèm stack, Map thành object và Set thành mảng. Khi hoàn toàn không serialize được details, ví dụ vì toJSON() ném lỗi, dòng log kết thúc bằng [unserializable]; lời gọi log không bao giờ ném lỗi. Không đưa credential, token, dữ liệu cá nhân hoặc secret khác vào bất kỳ đối số nào.

© 2026 Cogover LLC