@ouija-dev/plugin-plane 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,19 @@
1
+ import type { PlaneApiClient } from './api-client.js';
2
+ import type { PluginLogger } from '@ouija-dev/types';
3
+ export interface AgentUserRecord {
4
+ memberId: string;
5
+ email: string;
6
+ displayName: string;
7
+ }
8
+ /**
9
+ * Ensure an agent bot user exists in the given Plane workspace.
10
+ *
11
+ * - If the email is already a member: no-op (returns existing record).
12
+ * - If not: sends a workspace invitation.
13
+ *
14
+ * This is best-effort. If the invitation fails (e.g. Plane is in SSO-only
15
+ * mode or email is blocked) we log a warning but do NOT prevent the plugin
16
+ * from starting. The bot can still move cards; assignment will fail gracefully.
17
+ */
18
+ export declare function ensureAgentUser(client: PlaneApiClient, workspaceSlug: string, agentEmail: string, logger: PluginLogger): Promise<AgentUserRecord | null>;
19
+ //# sourceMappingURL=agent-user.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agent-user.d.ts","sourceRoot":"","sources":["../src/agent-user.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AACtD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAErD,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;CACrB;AAED;;;;;;;;;GASG;AACH,wBAAsB,eAAe,CACnC,MAAM,EAAE,cAAc,EACtB,aAAa,EAAE,MAAM,EACrB,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,YAAY,GACnB,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC,CAuBjC"}
@@ -0,0 +1,41 @@
1
+ // ---- Agent user registration and management on Plane ----
2
+ // Agents need to be real Plane users so they can be assigned to issues,
3
+ // appear in the board UI, and post comments attributed to their identity.
4
+ //
5
+ // This module handles creating / verifying agent bot users during plugin start.
6
+ /**
7
+ * Ensure an agent bot user exists in the given Plane workspace.
8
+ *
9
+ * - If the email is already a member: no-op (returns existing record).
10
+ * - If not: sends a workspace invitation.
11
+ *
12
+ * This is best-effort. If the invitation fails (e.g. Plane is in SSO-only
13
+ * mode or email is blocked) we log a warning but do NOT prevent the plugin
14
+ * from starting. The bot can still move cards; assignment will fail gracefully.
15
+ */
16
+ export async function ensureAgentUser(client, workspaceSlug, agentEmail, logger) {
17
+ try {
18
+ const result = await client.createMember(workspaceSlug, agentEmail, 10);
19
+ logger.info('Agent user provisioned on Plane', {
20
+ workspaceSlug,
21
+ email: agentEmail,
22
+ memberId: result.id,
23
+ });
24
+ return {
25
+ memberId: result.id,
26
+ email: result.email,
27
+ displayName: agentEmail,
28
+ };
29
+ }
30
+ catch (err) {
31
+ // A 400 often means "already a member" — treat as success by logging a warning.
32
+ // Callers should not block startup on this.
33
+ logger.warn('Could not provision agent user on Plane (non-fatal)', {
34
+ workspaceSlug,
35
+ email: agentEmail,
36
+ error: String(err),
37
+ });
38
+ return null;
39
+ }
40
+ }
41
+ //# sourceMappingURL=agent-user.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agent-user.js","sourceRoot":"","sources":["../src/agent-user.ts"],"names":[],"mappings":"AAAA,4DAA4D;AAC5D,wEAAwE;AACxE,0EAA0E;AAC1E,EAAE;AACF,gFAAgF;AAWhF;;;;;;;;;GASG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CACnC,MAAsB,EACtB,aAAqB,EACrB,UAAkB,EAClB,MAAoB;IAEpB,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,YAAY,CAAC,aAAa,EAAE,UAAU,EAAE,EAAE,CAAC,CAAC;QACxE,MAAM,CAAC,IAAI,CAAC,iCAAiC,EAAE;YAC7C,aAAa;YACb,KAAK,EAAE,UAAU;YACjB,QAAQ,EAAE,MAAM,CAAC,EAAE;SACpB,CAAC,CAAC;QACH,OAAO;YACL,QAAQ,EAAE,MAAM,CAAC,EAAE;YACnB,KAAK,EAAE,MAAM,CAAC,KAAK;YACnB,WAAW,EAAE,UAAU;SACxB,CAAC;IACJ,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,gFAAgF;QAChF,4CAA4C;QAC5C,MAAM,CAAC,IAAI,CAAC,qDAAqD,EAAE;YACjE,aAAa;YACb,KAAK,EAAE,UAAU;YACjB,KAAK,EAAE,MAAM,CAAC,GAAG,CAAC;SACnB,CAAC,CAAC;QACH,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC"}
@@ -0,0 +1,104 @@
1
+ export interface PlaneIssue {
2
+ id: string;
3
+ name: string;
4
+ description_html: string;
5
+ state: string;
6
+ project: string;
7
+ workspace: string;
8
+ label_details: Array<{
9
+ id: string;
10
+ name: string;
11
+ }>;
12
+ assignee_details: Array<{
13
+ id: string;
14
+ email: string;
15
+ display_name: string;
16
+ }>;
17
+ created_at: string;
18
+ updated_at: string;
19
+ }
20
+ export interface PlaneState {
21
+ id: string;
22
+ name: string;
23
+ color: string;
24
+ group: 'backlog' | 'unstarted' | 'started' | 'completed' | 'cancelled';
25
+ sequence: number;
26
+ }
27
+ export interface PlaneMember {
28
+ id: string;
29
+ email: string;
30
+ display_name: string;
31
+ role: number;
32
+ }
33
+ export interface PlaneComment {
34
+ id: string;
35
+ comment_html: string;
36
+ actor: string;
37
+ created_at: string;
38
+ }
39
+ export declare class PlaneRateLimitError extends Error {
40
+ readonly retryAfter: number;
41
+ constructor(retryAfter: number);
42
+ }
43
+ export declare class PlaneApiError extends Error {
44
+ readonly statusCode: number;
45
+ readonly body: string;
46
+ constructor(statusCode: number, body: string, path: string);
47
+ }
48
+ export declare class PlaneApiClient {
49
+ private readonly baseUrl;
50
+ private readonly apiToken;
51
+ constructor(baseUrl: string, apiToken: string);
52
+ private request;
53
+ private get;
54
+ private patch;
55
+ private post;
56
+ /**
57
+ * Fetch a single issue by ID.
58
+ * GET /api/v1/workspaces/:workspaceSlug/projects/:projectId/issues/:issueId/
59
+ */
60
+ getIssue(workspaceSlug: string, projectId: string, issueId: string): Promise<PlaneIssue>;
61
+ /**
62
+ * Update issue fields (e.g. state for column moves).
63
+ * PATCH /api/v1/workspaces/:workspaceSlug/projects/:projectId/issues/:issueId/
64
+ */
65
+ updateIssue(workspaceSlug: string, projectId: string, issueId: string, data: Partial<Pick<PlaneIssue, 'state'>> & Record<string, unknown>): Promise<PlaneIssue>;
66
+ /**
67
+ * Add a comment to an issue.
68
+ * POST /api/v1/workspaces/:workspaceSlug/projects/:projectId/issues/:issueId/comments/
69
+ */
70
+ addComment(workspaceSlug: string, projectId: string, issueId: string, body: string): Promise<PlaneComment>;
71
+ /**
72
+ * Fetch all states (columns) for a project.
73
+ * GET /api/v1/workspaces/:workspaceSlug/projects/:projectId/states/
74
+ */
75
+ getStates(workspaceSlug: string, projectId: string): Promise<PlaneState[]>;
76
+ /**
77
+ * List all members of a workspace.
78
+ * GET /api/v1/workspaces/:workspaceSlug/members/
79
+ */
80
+ getMembers(workspaceSlug: string): Promise<PlaneMember[]>;
81
+ /**
82
+ * Invite a member (e.g. agent bot user) to the workspace.
83
+ * POST /api/v1/workspaces/:workspaceSlug/invitations/
84
+ *
85
+ * Role codes: 5=Guest, 10=Member, 15=Viewer, 20=Admin
86
+ */
87
+ createMember(workspaceSlug: string, email: string, role?: 5 | 10 | 15 | 20): Promise<{
88
+ id: string;
89
+ email: string;
90
+ role: number;
91
+ }>;
92
+ /**
93
+ * Assign a member to an issue.
94
+ * Plane uses assignees as an array of member IDs on the issue resource.
95
+ * We fetch the current assignees and PATCH with the new list.
96
+ */
97
+ assignMember(workspaceSlug: string, projectId: string, issueId: string, memberId: string): Promise<PlaneIssue>;
98
+ /**
99
+ * Lightweight connectivity check: fetch workspace details.
100
+ * GET /api/v1/workspaces/:workspaceSlug/
101
+ */
102
+ ping(_workspaceSlug: string): Promise<void>;
103
+ }
104
+ //# sourceMappingURL=api-client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"api-client.d.ts","sourceRoot":"","sources":["../src/api-client.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,149 @@
1
+ // ---- Plane REST API v1 client ----
2
+ // All requests use the X-Api-Key header for authentication.
3
+ // Base URL: https://<plane-host>/api/v1
4
+ // ---- Rate limit error ----
5
+ export class PlaneRateLimitError extends Error {
6
+ retryAfter;
7
+ constructor(retryAfter) {
8
+ super(`Plane API rate limit exceeded. Retry after ${retryAfter}s`);
9
+ this.name = 'PlaneRateLimitError';
10
+ this.retryAfter = retryAfter;
11
+ }
12
+ }
13
+ // ---- API error ----
14
+ export class PlaneApiError extends Error {
15
+ statusCode;
16
+ body;
17
+ constructor(statusCode, body, path) {
18
+ super(`Plane API error ${statusCode} on ${path}: ${body}`);
19
+ this.name = 'PlaneApiError';
20
+ this.statusCode = statusCode;
21
+ this.body = body;
22
+ }
23
+ }
24
+ // ---- Client ----
25
+ export class PlaneApiClient {
26
+ baseUrl;
27
+ apiToken;
28
+ constructor(baseUrl, apiToken) {
29
+ // Normalise: strip trailing slash so path construction is consistent.
30
+ this.baseUrl = baseUrl.replace(/\/$/, '');
31
+ this.apiToken = apiToken;
32
+ }
33
+ // ---- Private helpers ----
34
+ async request(method, path, body) {
35
+ const url = `${this.baseUrl}/api/v1${path}`;
36
+ const headers = {
37
+ 'X-Api-Key': this.apiToken,
38
+ 'Content-Type': 'application/json',
39
+ };
40
+ const init = {
41
+ method,
42
+ headers,
43
+ ...(body !== undefined ? { body: JSON.stringify(body) } : {}),
44
+ };
45
+ const response = await fetch(url, init);
46
+ // Rate limit: surface the retry-after header.
47
+ if (response.status === 429) {
48
+ const retryAfter = Number(response.headers.get('Retry-After') ?? '60');
49
+ throw new PlaneRateLimitError(retryAfter);
50
+ }
51
+ const text = await response.text();
52
+ if (!response.ok) {
53
+ throw new PlaneApiError(response.status, text, path);
54
+ }
55
+ // 204 No Content — return empty object cast to T.
56
+ if (response.status === 204 || text.trim() === '') {
57
+ return {};
58
+ }
59
+ return JSON.parse(text);
60
+ }
61
+ get(path) {
62
+ return this.request('GET', path);
63
+ }
64
+ patch(path, body) {
65
+ return this.request('PATCH', path, body);
66
+ }
67
+ post(path, body) {
68
+ return this.request('POST', path, body);
69
+ }
70
+ // ---- Issue operations ----
71
+ /**
72
+ * Fetch a single issue by ID.
73
+ * GET /api/v1/workspaces/:workspaceSlug/projects/:projectId/issues/:issueId/
74
+ */
75
+ async getIssue(workspaceSlug, projectId, issueId) {
76
+ return this.get(`/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/`);
77
+ }
78
+ /**
79
+ * Update issue fields (e.g. state for column moves).
80
+ * PATCH /api/v1/workspaces/:workspaceSlug/projects/:projectId/issues/:issueId/
81
+ */
82
+ async updateIssue(workspaceSlug, projectId, issueId, data) {
83
+ return this.patch(`/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/`, data);
84
+ }
85
+ /**
86
+ * Add a comment to an issue.
87
+ * POST /api/v1/workspaces/:workspaceSlug/projects/:projectId/issues/:issueId/comments/
88
+ */
89
+ async addComment(workspaceSlug, projectId, issueId, body) {
90
+ return this.post(`/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/comments/`, { comment_html: body });
91
+ }
92
+ /**
93
+ * Fetch all states (columns) for a project.
94
+ * GET /api/v1/workspaces/:workspaceSlug/projects/:projectId/states/
95
+ */
96
+ async getStates(workspaceSlug, projectId) {
97
+ const response = await this.get(`/workspaces/${workspaceSlug}/projects/${projectId}/states/`);
98
+ // Plane may return a paginated envelope or a plain array depending on version.
99
+ if (Array.isArray(response)) {
100
+ return response;
101
+ }
102
+ return response.results;
103
+ }
104
+ /**
105
+ * List all members of a workspace.
106
+ * GET /api/v1/workspaces/:workspaceSlug/members/
107
+ */
108
+ async getMembers(workspaceSlug) {
109
+ const response = await this.get(`/workspaces/${workspaceSlug}/members/`);
110
+ if (Array.isArray(response)) {
111
+ return response;
112
+ }
113
+ return response.results;
114
+ }
115
+ /**
116
+ * Invite a member (e.g. agent bot user) to the workspace.
117
+ * POST /api/v1/workspaces/:workspaceSlug/invitations/
118
+ *
119
+ * Role codes: 5=Guest, 10=Member, 15=Viewer, 20=Admin
120
+ */
121
+ async createMember(workspaceSlug, email, role = 10) {
122
+ return this.post(`/workspaces/${workspaceSlug}/invitations/`, { email, role });
123
+ }
124
+ /**
125
+ * Assign a member to an issue.
126
+ * Plane uses assignees as an array of member IDs on the issue resource.
127
+ * We fetch the current assignees and PATCH with the new list.
128
+ */
129
+ async assignMember(workspaceSlug, projectId, issueId, memberId) {
130
+ // Fetch current issue to get existing assignees.
131
+ const issue = await this.getIssue(workspaceSlug, projectId, issueId);
132
+ const existing = issue.assignee_details.map((a) => a.id);
133
+ if (existing.includes(memberId)) {
134
+ // Already assigned — no-op.
135
+ return issue;
136
+ }
137
+ return this.patch(`/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/`, { assignees: [...existing, memberId] });
138
+ }
139
+ /**
140
+ * Lightweight connectivity check: fetch workspace details.
141
+ * GET /api/v1/workspaces/:workspaceSlug/
142
+ */
143
+ async ping(_workspaceSlug) {
144
+ // /users/me/ is the lightest authenticated endpoint.
145
+ // /workspaces/:slug/ requires session auth on some Plane versions.
146
+ await this.get(`/users/me/`);
147
+ }
148
+ }
149
+ //# sourceMappingURL=api-client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"api-client.js","sourceRoot":"","sources":["../src/api-client.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,30 @@
1
+ export declare const planeConfigSchema: {
2
+ readonly type: "object";
3
+ readonly properties: {
4
+ readonly baseUrl: {
5
+ readonly type: "string";
6
+ readonly format: "uri";
7
+ };
8
+ readonly apiToken: {
9
+ readonly type: "string";
10
+ readonly minLength: 1;
11
+ };
12
+ readonly workspaceSlug: {
13
+ readonly type: "string";
14
+ readonly minLength: 1;
15
+ };
16
+ readonly webhookSecret: {
17
+ readonly type: "string";
18
+ readonly minLength: 1;
19
+ };
20
+ };
21
+ readonly required: readonly ["baseUrl", "apiToken", "workspaceSlug", "webhookSecret"];
22
+ readonly additionalProperties: false;
23
+ };
24
+ export interface PlaneConfig {
25
+ baseUrl: string;
26
+ apiToken: string;
27
+ workspaceSlug: string;
28
+ webhookSecret: string;
29
+ }
30
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,iBAAiB;;;;;;;;;;;;;;;;;;;;;;CAUpB,CAAC;AAIX,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,MAAM,CAAC;IACtB,aAAa,EAAE,MAAM,CAAC;CACvB"}
package/dist/config.js ADDED
@@ -0,0 +1,15 @@
1
+ // ---- Plane plugin configuration schema ----
2
+ // Defined as a const object so TypeScript preserves literal types.
3
+ // Use with Ajv for runtime validation via @ouija-dev/plugin-sdk.
4
+ export const planeConfigSchema = {
5
+ type: 'object',
6
+ properties: {
7
+ baseUrl: { type: 'string', format: 'uri' },
8
+ apiToken: { type: 'string', minLength: 1 },
9
+ workspaceSlug: { type: 'string', minLength: 1 },
10
+ webhookSecret: { type: 'string', minLength: 1 },
11
+ },
12
+ required: ['baseUrl', 'apiToken', 'workspaceSlug', 'webhookSecret'],
13
+ additionalProperties: false,
14
+ };
15
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,8CAA8C;AAC9C,mEAAmE;AACnE,iEAAiE;AAEjE,MAAM,CAAC,MAAM,iBAAiB,GAAG;IAC/B,IAAI,EAAE,QAAQ;IACd,UAAU,EAAE;QACV,OAAO,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE;QAC1C,QAAQ,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,SAAS,EAAE,CAAC,EAAE;QAC1C,aAAa,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,SAAS,EAAE,CAAC,EAAE;QAC/C,aAAa,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,SAAS,EAAE,CAAC,EAAE;KAChD;IACD,QAAQ,EAAE,CAAC,SAAS,EAAE,UAAU,EAAE,eAAe,EAAE,eAAe,CAAC;IACnE,oBAAoB,EAAE,KAAK;CACnB,CAAC"}
@@ -0,0 +1,69 @@
1
+ import type { FastifyInstance } from 'fastify';
2
+ import type { KanbanPlugin, KanbanCard, KanbanColumn, PluginManifest, PluginContext, PluginHealth } from '@ouija-dev/types';
3
+ import type { CardId, ColumnId, BoardId } from '@ouija-dev/types';
4
+ import type { PlaneConfig } from './config.js';
5
+ import type { PluginFactory } from '@ouija-dev/plugin-sdk';
6
+ export declare class PlanePlugin implements KanbanPlugin<PlaneConfig> {
7
+ readonly manifest: PluginManifest;
8
+ private config;
9
+ private client;
10
+ private context;
11
+ init(context: PluginContext<PlaneConfig>): Promise<void>;
12
+ start(): Promise<void>;
13
+ stop(): Promise<void>;
14
+ healthCheck(): Promise<PluginHealth>;
15
+ /**
16
+ * Register the Plane webhook ingress route on the Fastify server.
17
+ *
18
+ * Route: POST /hooks/plane/:secret
19
+ *
20
+ * Security (per spec §5.5):
21
+ * 1. Path secret — cheap first-pass filter
22
+ * 2. HMAC-SHA256 signature (X-Plane-Signature header)
23
+ * 3. Timestamp freshness — reject events older than 5 minutes
24
+ *
25
+ * Always responds 200 (even on auth failure) to prevent path enumeration.
26
+ */
27
+ registerRoutes(server: FastifyInstance): Promise<void>;
28
+ /**
29
+ * Fetch a single card (Plane issue) by its ID.
30
+ *
31
+ * The cardId must be in the form "<projectId>/<issueId>" so the plugin
32
+ * knows which project the issue belongs to, or a plain issue UUID when
33
+ * the default project from the plugin's workspace context is used.
34
+ *
35
+ * Convention adopted: cardId format is "<projectId>/<issueId>".
36
+ * This avoids storing project context separately and keeps card IDs
37
+ * self-contained.
38
+ */
39
+ getCard(id: CardId): Promise<KanbanCard>;
40
+ /**
41
+ * Move a card to a different column by updating the issue's state.
42
+ */
43
+ moveCard(id: CardId, toColumnId: ColumnId): Promise<void>;
44
+ /**
45
+ * Add a comment to a card (Plane issue).
46
+ */
47
+ addComment(id: CardId, body: string): Promise<void>;
48
+ /**
49
+ * Assign a user (member ID) to a card.
50
+ */
51
+ assignUser(id: CardId, userId: string): Promise<void>;
52
+ /** Expose getMembers for agent registry provisioning. */
53
+ getMembers(workspaceSlug?: string): Promise<import('./api-client.js').PlaneMember[]>;
54
+ /** Expose inviteMember for agent registry provisioning. */
55
+ inviteMember(workspaceSlug: string | undefined, email: string, role?: 5 | 10 | 15 | 20): Promise<{
56
+ id: string;
57
+ email: string;
58
+ role: number;
59
+ }>;
60
+ /**
61
+ * Return all columns (states) for a given board (project).
62
+ * The boardId is the Plane project UUID.
63
+ */
64
+ getColumns(board: BoardId): Promise<KanbanColumn[]>;
65
+ }
66
+ declare const planePluginFactory: PluginFactory<PlaneConfig>;
67
+ export default planePluginFactory;
68
+ export { planePluginFactory as PluginFactory };
69
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,eAAe,EAAgC,MAAM,SAAS,CAAC;AAC7E,OAAO,KAAK,EACV,YAAY,EACZ,UAAU,EACV,YAAY,EACZ,cAAc,EACd,aAAa,EACb,YAAY,EACb,MAAM,kBAAkB,CAAC;AAE1B,OAAO,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAGlE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAG/C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAmB3D,qBAAa,WAAY,YAAW,YAAY,CAAC,WAAW,CAAC;IAC3D,QAAQ,CAAC,QAAQ,iBAAY;IAE7B,OAAO,CAAC,MAAM,CAAe;IAC7B,OAAO,CAAC,MAAM,CAAkB;IAChC,OAAO,CAAC,OAAO,CAA8B;IAIvC,IAAI,CAAC,OAAO,EAAE,aAAa,CAAC,WAAW,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IAUxD,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAOtB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAIrB,WAAW,IAAI,OAAO,CAAC,YAAY,CAAC;IAe1C;;;;;;;;;;;OAWG;IACG,cAAc,CAAC,MAAM,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC;IAyE5D;;;;;;;;;;OAUG;IACG,OAAO,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC;IAoB9C;;OAEG;IACG,QAAQ,CAAC,EAAE,EAAE,MAAM,EAAE,UAAU,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC;IAS/D;;OAEG;IACG,UAAU,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAOzD;;OAEG;IACG,UAAU,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAO3D,yDAAyD;IACnD,UAAU,CAAC,aAAa,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,iBAAiB,EAAE,WAAW,EAAE,CAAC;IAK1F,2DAA2D;IACrD,YAAY,CAChB,aAAa,EAAE,MAAM,GAAG,SAAS,EACjC,KAAK,EAAE,MAAM,EACb,IAAI,GAAE,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAO,GAC1B,OAAO,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;IAKvD;;;OAGG;IACG,UAAU,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC;CAY1D;AAsCD,QAAA,MAAM,kBAAkB,EAAE,aAAa,CAAC,WAAW,CAKlD,CAAC;AAEF,eAAe,kBAAkB,CAAC;AAGlC,OAAO,EAAE,kBAAkB,IAAI,aAAa,EAAE,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,244 @@
1
+ // ---- Plane kanban plugin ----
2
+ // Implements KanbanPlugin<PlaneConfig> against the Plane REST API v1.
3
+ //
4
+ // Responsibilities:
5
+ // - Lifecycle: init, start, stop, healthCheck
6
+ // - Route: POST /hooks/plane/:secret (HMAC + path-secret verification)
7
+ // - KanbanPlugin: getCard, moveCard, addComment, assignUser, getColumns
8
+ import { cardId, columnId, boardId } from '@ouija-dev/types';
9
+ import { planeConfigSchema } from './config.js';
10
+ import { PlaneApiClient } from './api-client.js';
11
+ import { normalizeWebhook, verifyPlaneSignature, isWebhookFresh } from './webhook-handler.js';
12
+ // ---- Plugin manifest (static — safe to read before init) ----
13
+ const manifest = {
14
+ name: '@ouija-dev/plugin-plane',
15
+ version: '0.1.0',
16
+ type: 'kanban',
17
+ coreApiVersion: '>=1.0.0 <2.0.0',
18
+ configSchema: planeConfigSchema,
19
+ dependencies: [],
20
+ events: {
21
+ produces: ['kanban.card.moved', 'kanban.card.assigned'],
22
+ consumes: [],
23
+ },
24
+ };
25
+ // ---- PlanePlugin ----
26
+ export class PlanePlugin {
27
+ manifest = manifest;
28
+ config;
29
+ client;
30
+ context;
31
+ // ---- Lifecycle ----
32
+ async init(context) {
33
+ this.context = context;
34
+ this.config = context.config;
35
+ this.client = new PlaneApiClient(this.config.baseUrl, this.config.apiToken);
36
+ context.logger.info('PlanePlugin initialised', {
37
+ baseUrl: this.config.baseUrl,
38
+ workspaceSlug: this.config.workspaceSlug,
39
+ });
40
+ }
41
+ async start() {
42
+ // Verify connectivity. This will throw and prevent the plugin starting
43
+ // if the API is unreachable, which is the correct behaviour.
44
+ await this.client.ping(this.config.workspaceSlug);
45
+ this.context.logger.info('PlanePlugin started — API connectivity verified');
46
+ }
47
+ async stop() {
48
+ this.context.logger.info('PlanePlugin stopped');
49
+ }
50
+ async healthCheck() {
51
+ try {
52
+ await this.client.ping(this.config.workspaceSlug);
53
+ return { healthy: true, message: 'Plane API reachable' };
54
+ }
55
+ catch (err) {
56
+ return {
57
+ healthy: false,
58
+ message: `Plane API unreachable: ${String(err)}`,
59
+ details: { error: String(err) },
60
+ };
61
+ }
62
+ }
63
+ // ---- Route registration ----
64
+ /**
65
+ * Register the Plane webhook ingress route on the Fastify server.
66
+ *
67
+ * Route: POST /hooks/plane/:secret
68
+ *
69
+ * Security (per spec §5.5):
70
+ * 1. Path secret — cheap first-pass filter
71
+ * 2. HMAC-SHA256 signature (X-Plane-Signature header)
72
+ * 3. Timestamp freshness — reject events older than 5 minutes
73
+ *
74
+ * Always responds 200 (even on auth failure) to prevent path enumeration.
75
+ */
76
+ async registerRoutes(server) {
77
+ const plugin = this;
78
+ server.post('/hooks/plane/:secret', {
79
+ config: {
80
+ // Fastify 5: disable built-in body parsing so we can read raw body for HMAC.
81
+ rawBody: true,
82
+ },
83
+ }, async (request, reply) => {
84
+ const { secret } = request.params;
85
+ // 1. Path secret check.
86
+ if (secret !== plugin.config.webhookSecret) {
87
+ plugin.context.logger.warn('Plane webhook: invalid path secret', {
88
+ ip: request.ip,
89
+ });
90
+ // Return 200 to prevent enumeration.
91
+ return reply.status(200).send({ ok: false });
92
+ }
93
+ // 2. HMAC signature verification.
94
+ const sigHeader = request.headers['x-plane-signature'];
95
+ const rawBody =
96
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
97
+ request.rawBody ?? JSON.stringify(request.body);
98
+ if (!verifyPlaneSignature(plugin.config.webhookSecret, rawBody, sigHeader)) {
99
+ plugin.context.logger.warn('Plane webhook: invalid HMAC signature', {
100
+ ip: request.ip,
101
+ });
102
+ return reply.status(200).send({ ok: false });
103
+ }
104
+ // 3. Timestamp freshness.
105
+ const body = request.body;
106
+ const timestamp = typeof body?.['timestamp'] === 'string' ? body['timestamp'] : undefined;
107
+ if (!isWebhookFresh(timestamp)) {
108
+ plugin.context.logger.warn('Plane webhook: stale timestamp', {
109
+ timestamp,
110
+ ip: request.ip,
111
+ });
112
+ return reply.status(200).send({ ok: false });
113
+ }
114
+ // 4. Normalize and publish.
115
+ const event = normalizeWebhook(body, manifest.name, plugin.config.baseUrl);
116
+ if (event !== null) {
117
+ await plugin.context.publishEvent(event.topic, event.payload);
118
+ plugin.context.logger.info('Plane webhook processed', {
119
+ topic: event.topic,
120
+ eventId: event.id,
121
+ });
122
+ }
123
+ else {
124
+ plugin.context.logger.debug('Plane webhook: no-op event (field not tracked)', {
125
+ event: body?.['event'],
126
+ });
127
+ }
128
+ return reply.status(200).send({ ok: true });
129
+ });
130
+ }
131
+ // ---- KanbanPlugin methods ----
132
+ /**
133
+ * Fetch a single card (Plane issue) by its ID.
134
+ *
135
+ * The cardId must be in the form "<projectId>/<issueId>" so the plugin
136
+ * knows which project the issue belongs to, or a plain issue UUID when
137
+ * the default project from the plugin's workspace context is used.
138
+ *
139
+ * Convention adopted: cardId format is "<projectId>/<issueId>".
140
+ * This avoids storing project context separately and keeps card IDs
141
+ * self-contained.
142
+ */
143
+ async getCard(id) {
144
+ const { workspaceSlug } = this.config;
145
+ const { projectId, issueId } = splitCardId(id);
146
+ const issue = await this.client.getIssue(workspaceSlug, projectId, issueId);
147
+ return {
148
+ id: cardId(`${issue.project}/${issue.id}`),
149
+ title: issue.name,
150
+ description: issue.description_html,
151
+ columnId: columnId(typeof issue.state === 'string' ? issue.state : issue.state?.id ?? issue.state),
152
+ boardId: boardId(issue.project),
153
+ labels: (issue.label_details ?? issue['labels'] ?? []).map((l) => l.name),
154
+ assignees: (issue.assignee_details ?? issue['assignees'] ?? []).map((a) => a.id),
155
+ url: issueUrl(this.config.baseUrl, workspaceSlug, issue.project, issue.id),
156
+ createdAt: issue.created_at,
157
+ updatedAt: issue.updated_at,
158
+ };
159
+ }
160
+ /**
161
+ * Move a card to a different column by updating the issue's state.
162
+ */
163
+ async moveCard(id, toColumnId) {
164
+ const { workspaceSlug } = this.config;
165
+ const { projectId, issueId } = splitCardId(id);
166
+ await this.client.updateIssue(workspaceSlug, projectId, issueId, {
167
+ state: toColumnId,
168
+ });
169
+ }
170
+ /**
171
+ * Add a comment to a card (Plane issue).
172
+ */
173
+ async addComment(id, body) {
174
+ const { workspaceSlug } = this.config;
175
+ const { projectId, issueId } = splitCardId(id);
176
+ await this.client.addComment(workspaceSlug, projectId, issueId, body);
177
+ }
178
+ /**
179
+ * Assign a user (member ID) to a card.
180
+ */
181
+ async assignUser(id, userId) {
182
+ const { workspaceSlug } = this.config;
183
+ const { projectId, issueId } = splitCardId(id);
184
+ await this.client.assignMember(workspaceSlug, projectId, issueId, userId);
185
+ }
186
+ /** Expose getMembers for agent registry provisioning. */
187
+ async getMembers(workspaceSlug) {
188
+ const slug = workspaceSlug ?? this.config.workspaceSlug;
189
+ return this.client.getMembers(slug);
190
+ }
191
+ /** Expose inviteMember for agent registry provisioning. */
192
+ async inviteMember(workspaceSlug, email, role = 10) {
193
+ const slug = workspaceSlug ?? this.config.workspaceSlug;
194
+ return this.client.createMember(slug, email, role);
195
+ }
196
+ /**
197
+ * Return all columns (states) for a given board (project).
198
+ * The boardId is the Plane project UUID.
199
+ */
200
+ async getColumns(board) {
201
+ const { workspaceSlug } = this.config;
202
+ const states = await this.client.getStates(workspaceSlug, board);
203
+ return states
204
+ .sort((a, b) => a.sequence - b.sequence)
205
+ .map((s, index) => ({
206
+ id: columnId(s.id),
207
+ name: s.name,
208
+ position: index,
209
+ }));
210
+ }
211
+ }
212
+ // ---- Helpers ----
213
+ /**
214
+ * Split a composite card ID "<projectId>/<issueId>" into its parts.
215
+ * Falls back to treating the whole string as the issueId if no slash present
216
+ * (to handle legacy or direct-issue-ID callers).
217
+ */
218
+ function splitCardId(id) {
219
+ const raw = id;
220
+ const slashIdx = raw.indexOf('/');
221
+ if (slashIdx === -1) {
222
+ // Single UUID — assume it's the issue ID. Project must be inferred elsewhere.
223
+ // In practice, callers should always pass "<projectId>/<issueId>".
224
+ throw new Error(`PlanePlugin: cardId "${raw}" must be in "<projectId>/<issueId>" format`);
225
+ }
226
+ return {
227
+ projectId: raw.slice(0, slashIdx),
228
+ issueId: raw.slice(slashIdx + 1),
229
+ };
230
+ }
231
+ function issueUrl(baseUrl, workspaceSlug, projectId, issueId) {
232
+ return `${baseUrl.replace(/\/$/, '')}/${workspaceSlug}/projects/${projectId}/issues/${issueId}`;
233
+ }
234
+ // ---- PluginFactory (default export for plugin-loader) ----
235
+ const planePluginFactory = {
236
+ manifest,
237
+ create() {
238
+ return new PlanePlugin();
239
+ },
240
+ };
241
+ export default planePluginFactory;
242
+ // Named export for consumers who prefer it.
243
+ export { planePluginFactory as PluginFactory };
244
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,68 @@
1
+ import type { OuijaEvent, KanbanCard } from '@ouija-dev/types';
2
+ interface PlaneWebhookIssueData {
3
+ id: string;
4
+ name: string;
5
+ description_html: string;
6
+ state: string | {
7
+ id: string;
8
+ name: string;
9
+ color?: string;
10
+ group?: string;
11
+ };
12
+ project: string;
13
+ workspace: string;
14
+ labels?: Array<{
15
+ id: string;
16
+ name: string;
17
+ }>;
18
+ assignees?: Array<{
19
+ id: string;
20
+ email?: string;
21
+ display_name?: string;
22
+ }>;
23
+ label_details?: Array<{
24
+ id: string;
25
+ name: string;
26
+ }>;
27
+ assignee_details?: Array<{
28
+ id: string;
29
+ email: string;
30
+ display_name: string;
31
+ }>;
32
+ created_at: string;
33
+ updated_at: string;
34
+ }
35
+ declare function toKanbanCard(data: PlaneWebhookIssueData, workspaceSlug: string, baseUrl: string): KanbanCard;
36
+ export type NormalizedWebhookEvent = OuijaEvent<'kanban.card.moved'> | OuijaEvent<'kanban.card.assigned'> | null;
37
+ /**
38
+ * Parse and normalize a raw Plane webhook payload.
39
+ *
40
+ * Returns:
41
+ * - OuijaEvent<'kanban.card.moved'> when field === "state"
42
+ * - OuijaEvent<'kanban.card.assigned'> when field === "assignees"
43
+ * - null for all other activity types or on any parse error
44
+ *
45
+ * This function never throws — all errors are swallowed and return null.
46
+ * Callers must not rely on thrown errors for control flow.
47
+ */
48
+ export declare function normalizeWebhook(raw: unknown, sourcePlugin?: string, baseUrl?: string): NormalizedWebhookEvent;
49
+ /**
50
+ * Verify the Plane webhook HMAC-SHA256 signature.
51
+ *
52
+ * Plane sends: X-Plane-Signature: sha256=<hex-digest>
53
+ *
54
+ * @param secret The webhook secret configured in Plane
55
+ * @param rawBody The raw request body bytes (Buffer or string)
56
+ * @param sigHeader The value of X-Plane-Signature header
57
+ * @returns true if valid, false if invalid or missing
58
+ */
59
+ export declare function verifyPlaneSignature(secret: string, rawBody: Buffer | string, sigHeader: string | undefined): boolean;
60
+ /**
61
+ * Reject webhooks older than maxAgeMs (default 5 minutes).
62
+ * Plane includes a top-level `timestamp` field in ISO 8601 format.
63
+ *
64
+ * @returns true if the timestamp is within the allowed window, false otherwise
65
+ */
66
+ export declare function isWebhookFresh(timestamp: string | undefined, nowMs?: number, maxAgeMs?: number): boolean;
67
+ export { toKanbanCard };
68
+ //# sourceMappingURL=webhook-handler.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webhook-handler.d.ts","sourceRoot":"","sources":["../src/webhook-handler.ts"],"names":[],"mappings":"AAkBA,OAAO,KAAK,EACV,UAAU,EAGV,UAAU,EACX,MAAM,kBAAkB,CAAC;AA4B1B,UAAU,qBAAqB;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,gBAAgB,EAAE,MAAM,CAAC;IAGzB,KAAK,EAAE,MAAM,GAAG;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAC7E,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAElB,MAAM,CAAC,EAAE,KAAK,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC7C,SAAS,CAAC,EAAE,KAAK,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,YAAY,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAEzE,aAAa,CAAC,EAAE,KAAK,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACpD,gBAAgB,CAAC,EAAE,KAAK,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,YAAY,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC9E,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;CACpB;AA8CD,iBAAS,YAAY,CACnB,IAAI,EAAE,qBAAqB,EAC3B,aAAa,EAAE,MAAM,EACrB,OAAO,EAAE,MAAM,GACd,UAAU,CAgBZ;AAID,MAAM,MAAM,sBAAsB,GAC9B,UAAU,CAAC,mBAAmB,CAAC,GAC/B,UAAU,CAAC,sBAAsB,CAAC,GAClC,IAAI,CAAC;AAIT;;;;;;;;;;GAUG;AACH,wBAAgB,gBAAgB,CAC9B,GAAG,EAAE,OAAO,EACZ,YAAY,SAA4B,EACxC,OAAO,SAAK,GACX,sBAAsB,CAqFxB;AAID;;;;;;;;;GASG;AACH,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,MAAM,GAAG,MAAM,EACxB,SAAS,EAAE,MAAM,GAAG,SAAS,GAC5B,OAAO,CAsBT;AAID;;;;;GAKG;AACH,wBAAgB,cAAc,CAC5B,SAAS,EAAE,MAAM,GAAG,SAAS,EAC7B,KAAK,SAAa,EAClB,QAAQ,SAAgB,GACvB,OAAO,CAOT;AAGD,OAAO,EAAE,YAAY,EAAE,CAAC"}
@@ -0,0 +1,196 @@
1
+ // ---- Plane webhook normalization ----
2
+ // Converts raw Plane webhook payloads into typed OuijaEvents.
3
+ //
4
+ // Plane Community Edition webhook format:
5
+ // event: "issue" (not "issue_activity")
6
+ // action: "created" | "updated" | "deleted"
7
+ // data: full issue snapshot (state is an object {id, name, group}, not a string)
8
+ // activity: the specific change
9
+ // .field: "state_id" | "assignees" | ... (note: "state_id", not "state")
10
+ // .actor: {id, first_name, last_name, email, display_name}
11
+ //
12
+ // We only care about:
13
+ // field === "state_id" or field === "state" → kanban.card.moved
14
+ // field === "assignees" → kanban.card.assigned
15
+ // Everything else returns null.
16
+ import { createHmac, timingSafeEqual } from 'node:crypto';
17
+ import { cardId, columnId } from '@ouija-dev/types';
18
+ import { boardId as mkBoardId } from '@ouija-dev/types';
19
+ // ---- Type guard ----
20
+ function isPlaneWebhookPayload(raw) {
21
+ if (typeof raw !== 'object' || raw === null)
22
+ return false;
23
+ const p = raw;
24
+ // Must have event, data, activity, workspace_id
25
+ if (typeof p['event'] !== 'string')
26
+ return false;
27
+ if (typeof p['data'] !== 'object' || p['data'] === null)
28
+ return false;
29
+ if (typeof p['activity'] !== 'object' || p['activity'] === null)
30
+ return false;
31
+ if (typeof p['workspace_id'] !== 'string')
32
+ return false;
33
+ const data = p['data'];
34
+ if (typeof data['id'] !== 'string')
35
+ return false;
36
+ // state can be a string (UUID) or an object { id, name, ... }
37
+ if (typeof data['state'] !== 'string' && (typeof data['state'] !== 'object' || data['state'] === null))
38
+ return false;
39
+ const activity = p['activity'];
40
+ if (typeof activity['field'] !== 'string')
41
+ return false;
42
+ return true;
43
+ }
44
+ // ---- Normalize issue data → KanbanCard ----
45
+ /** Extract state ID from either string or object form. */
46
+ function extractStateId(state) {
47
+ return typeof state === 'string' ? state : state.id;
48
+ }
49
+ function toKanbanCard(data, workspaceSlug, baseUrl) {
50
+ const labels = data.labels ?? data.label_details ?? [];
51
+ const assignees = data.assignees ?? data.assignee_details ?? [];
52
+ return {
53
+ id: cardId(data.id),
54
+ title: data.name,
55
+ description: data.description_html,
56
+ columnId: columnId(extractStateId(data.state)),
57
+ boardId: mkBoardId(data.project),
58
+ labels: labels.map((l) => l.name),
59
+ assignees: assignees.map((a) => a.id),
60
+ url: `${baseUrl.replace(/\/$/, '')}/${workspaceSlug}/projects/${data.project}/issues/${data.id}`,
61
+ createdAt: data.created_at,
62
+ updatedAt: data.updated_at,
63
+ };
64
+ }
65
+ // ---- Main normalization function ----
66
+ /**
67
+ * Parse and normalize a raw Plane webhook payload.
68
+ *
69
+ * Returns:
70
+ * - OuijaEvent<'kanban.card.moved'> when field === "state"
71
+ * - OuijaEvent<'kanban.card.assigned'> when field === "assignees"
72
+ * - null for all other activity types or on any parse error
73
+ *
74
+ * This function never throws — all errors are swallowed and return null.
75
+ * Callers must not rely on thrown errors for control flow.
76
+ */
77
+ export function normalizeWebhook(raw, sourcePlugin = '@ouija-dev/plugin-plane', baseUrl = '') {
78
+ try {
79
+ if (!isPlaneWebhookPayload(raw)) {
80
+ return null;
81
+ }
82
+ const payload = raw;
83
+ const { data, activity } = payload;
84
+ // Accept both "issue" (community edition) and "issue_activity" (older versions).
85
+ if (payload.event !== 'issue' && payload.event !== 'issue_activity') {
86
+ return null;
87
+ }
88
+ const workspaceSlug = data.workspace ?? '';
89
+ const eventId = activity.id ?? payload.webhook_id ?? crypto.randomUUID();
90
+ const timestamp = activity.created_at ?? payload.timestamp ?? new Date().toISOString();
91
+ const correlationId = payload.activity_id ?? payload.webhook_id ?? eventId;
92
+ // Actor can be in "actor" (community) or "actor_detail" (older)
93
+ const actor = activity.actor ?? activity.actor_detail;
94
+ // "state_id" (community edition) or "state" (older) → card moved
95
+ if (activity.field === 'state_id' || activity.field === 'state') {
96
+ const fromColumnId = activity.old_identifier ?? activity.old_value;
97
+ const toColumnId = activity.new_identifier ?? activity.new_value ?? extractStateId(data.state);
98
+ if (!fromColumnId || !toColumnId) {
99
+ return null;
100
+ }
101
+ const movedBy = actor?.email ?? actor?.display_name ?? actor?.id ?? 'unknown';
102
+ const cardMovedPayload = {
103
+ cardId: cardId(data.id),
104
+ fromColumnId: columnId(fromColumnId),
105
+ toColumnId: columnId(toColumnId),
106
+ movedBy,
107
+ };
108
+ const event = {
109
+ id: eventId,
110
+ topic: 'kanban.card.moved',
111
+ payload: cardMovedPayload,
112
+ timestamp,
113
+ sourcePlugin,
114
+ correlationId,
115
+ };
116
+ return event;
117
+ }
118
+ if (activity.field === 'assignees') {
119
+ const assigneeId = activity.new_identifier ?? activity.new_value;
120
+ if (!assigneeId) {
121
+ return null;
122
+ }
123
+ const assignedBy = actor?.email ?? actor?.display_name ?? actor?.id ?? 'unknown';
124
+ const cardAssignedPayload = {
125
+ cardId: cardId(data.id),
126
+ assigneeId,
127
+ assignedBy,
128
+ };
129
+ const event = {
130
+ id: eventId,
131
+ topic: 'kanban.card.assigned',
132
+ payload: cardAssignedPayload,
133
+ timestamp,
134
+ sourcePlugin,
135
+ correlationId,
136
+ };
137
+ return event;
138
+ }
139
+ // Unrecognised activity field (comment, name change, etc.) — ignore.
140
+ return null;
141
+ }
142
+ catch {
143
+ // Never propagate parse errors to callers.
144
+ return null;
145
+ }
146
+ }
147
+ // ---- HMAC signature verification ----
148
+ /**
149
+ * Verify the Plane webhook HMAC-SHA256 signature.
150
+ *
151
+ * Plane sends: X-Plane-Signature: sha256=<hex-digest>
152
+ *
153
+ * @param secret The webhook secret configured in Plane
154
+ * @param rawBody The raw request body bytes (Buffer or string)
155
+ * @param sigHeader The value of X-Plane-Signature header
156
+ * @returns true if valid, false if invalid or missing
157
+ */
158
+ export function verifyPlaneSignature(secret, rawBody, sigHeader) {
159
+ if (!sigHeader)
160
+ return false;
161
+ const prefix = 'sha256=';
162
+ if (!sigHeader.startsWith(prefix))
163
+ return false;
164
+ const receivedHex = sigHeader.slice(prefix.length);
165
+ const bodyBuffer = typeof rawBody === 'string' ? Buffer.from(rawBody, 'utf8') : rawBody;
166
+ const expected = createHmac('sha256', secret).update(bodyBuffer).digest('hex');
167
+ // Constant-time comparison to prevent timing attacks.
168
+ try {
169
+ const expectedBuf = Buffer.from(expected, 'hex');
170
+ const receivedBuf = Buffer.from(receivedHex, 'hex');
171
+ if (expectedBuf.length !== receivedBuf.length)
172
+ return false;
173
+ return timingSafeEqual(expectedBuf, receivedBuf);
174
+ }
175
+ catch {
176
+ return false;
177
+ }
178
+ }
179
+ // ---- Timestamp freshness check ----
180
+ /**
181
+ * Reject webhooks older than maxAgeMs (default 5 minutes).
182
+ * Plane includes a top-level `timestamp` field in ISO 8601 format.
183
+ *
184
+ * @returns true if the timestamp is within the allowed window, false otherwise
185
+ */
186
+ export function isWebhookFresh(timestamp, nowMs = Date.now(), maxAgeMs = 5 * 60 * 1000) {
187
+ if (!timestamp)
188
+ return false;
189
+ const ts = Date.parse(timestamp);
190
+ if (Number.isNaN(ts))
191
+ return false;
192
+ return Math.abs(nowMs - ts) <= maxAgeMs;
193
+ }
194
+ // Re-export KanbanCard helper for consumers.
195
+ export { toKanbanCard };
196
+ //# sourceMappingURL=webhook-handler.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webhook-handler.js","sourceRoot":"","sources":["../src/webhook-handler.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "@ouija-dev/plugin-plane",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "files": ["dist/", "README.md"],
8
+ "publishConfig": { "access": "public" },
9
+ "repository": { "type": "git", "url": "https://github.com/muhammadkh4n/ouija.git" },
10
+ "exports": {
11
+ ".": {
12
+ "types": "./dist/index.d.ts",
13
+ "default": "./dist/index.js"
14
+ },
15
+ "./webhook-handler": {
16
+ "types": "./dist/webhook-handler.d.ts",
17
+ "default": "./dist/webhook-handler.js"
18
+ }
19
+ },
20
+ "scripts": {
21
+ "build": "tsc",
22
+ "typecheck": "tsc --noEmit",
23
+ "test": "vitest run"
24
+ },
25
+ "dependencies": {
26
+ "@ouija-dev/types": "*",
27
+ "@ouija-dev/plugin-sdk": "*"
28
+ },
29
+ "devDependencies": {
30
+ "fastify": "^5.8.4",
31
+ "typescript": "^5.5.0",
32
+ "vitest": "^3.0.0",
33
+ "@types/node": "^22.0.0"
34
+ }
35
+ }