docs: add events system pages (#4294)
* feat: events docs * fix: make the conditions optional in casl check * Update backend/src/lib/api-docs/constants.ts * Update backend/src/lib/api-docs/constants.ts Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> * Update docs/docs.json * docs: content * fix: pr changes * feat: improve docs * chore: remove recursive * fix: pr changes * fix: change * fix: pr changes * fix: pr changes * fix: change
@@ -13,11 +13,9 @@ const AUTH_REFRESH_INTERVAL = 60 * 1000;
|
|||||||
const HEART_BEAT_INTERVAL = 15 * 1000;
|
const HEART_BEAT_INTERVAL = 15 * 1000;
|
||||||
|
|
||||||
export const sseServiceFactory = (bus: TEventBusService, redis: Redis) => {
|
export const sseServiceFactory = (bus: TEventBusService, redis: Redis) => {
|
||||||
let heartbeatInterval: NodeJS.Timeout | null = null;
|
|
||||||
|
|
||||||
const clients = new Set<EventStreamClient>();
|
const clients = new Set<EventStreamClient>();
|
||||||
|
|
||||||
heartbeatInterval = setInterval(() => {
|
const heartbeatInterval = setInterval(() => {
|
||||||
for (const client of clients) {
|
for (const client of clients) {
|
||||||
if (client.stream.closed) continue;
|
if (client.stream.closed) continue;
|
||||||
void client.ping();
|
void client.ping();
|
||||||
|
|||||||
@@ -66,15 +66,24 @@ export type EventStreamClient = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function createEventStreamClient(redis: Redis, options: IEventStreamClientOpts): EventStreamClient {
|
export function createEventStreamClient(redis: Redis, options: IEventStreamClientOpts): EventStreamClient {
|
||||||
const rules = options.registered.map((r) => ({
|
const rules = options.registered.map((r) => {
|
||||||
subject: options.type,
|
const secretPath = r.conditions?.secretPath;
|
||||||
action: "subscribe",
|
const hasConditions = r.conditions?.environmentSlug || r.conditions?.secretPath;
|
||||||
conditions: {
|
|
||||||
eventType: r.event,
|
return {
|
||||||
secretPath: r.conditions?.secretPath ?? "/",
|
subject: options.type,
|
||||||
environment: r.conditions?.environmentSlug
|
action: "subscribe",
|
||||||
}
|
conditions: {
|
||||||
}));
|
eventType: r.event,
|
||||||
|
...(hasConditions
|
||||||
|
? {
|
||||||
|
environment: r.conditions?.environmentSlug ?? "",
|
||||||
|
secretPath: { $glob: secretPath }
|
||||||
|
}
|
||||||
|
: {})
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
const id = `sse-${nanoid()}`;
|
const id = `sse-${nanoid()}`;
|
||||||
const control = new AbortController();
|
const control = new AbortController();
|
||||||
|
|||||||
@@ -70,7 +70,8 @@ export enum ApiDocsTags {
|
|||||||
SecretScanning = "Secret Scanning",
|
SecretScanning = "Secret Scanning",
|
||||||
OidcSso = "OIDC SSO",
|
OidcSso = "OIDC SSO",
|
||||||
SamlSso = "SAML SSO",
|
SamlSso = "SAML SSO",
|
||||||
LdapSso = "LDAP SSO"
|
LdapSso = "LDAP SSO",
|
||||||
|
Events = "Event Subscriptions"
|
||||||
}
|
}
|
||||||
|
|
||||||
export const GROUPS = {
|
export const GROUPS = {
|
||||||
@@ -2872,3 +2873,10 @@ export const LdapSso = {
|
|||||||
caCert: "The CA certificate to use when verifying the LDAP server certificate."
|
caCert: "The CA certificate to use when verifying the LDAP server certificate."
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const EventSubscriptions = {
|
||||||
|
SUBSCRIBE_PROJECT_EVENTS: {
|
||||||
|
projectId: "The ID of the project to subscribe to events for.",
|
||||||
|
register: "List of events you want to subscribe to"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { ActionProjectType, ProjectType } from "@app/db/schemas";
|
|||||||
import { getServerSentEventsHeaders } from "@app/ee/services/event/event-sse-stream";
|
import { getServerSentEventsHeaders } from "@app/ee/services/event/event-sse-stream";
|
||||||
import { EventRegisterSchema } from "@app/ee/services/event/types";
|
import { EventRegisterSchema } from "@app/ee/services/event/types";
|
||||||
import { ProjectPermissionSecretActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
import { ProjectPermissionSecretActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||||
|
import { ApiDocsTags, EventSubscriptions } from "@app/lib/api-docs";
|
||||||
import { BadRequestError, ForbiddenRequestError, RateLimitError } from "@app/lib/errors";
|
import { BadRequestError, ForbiddenRequestError, RateLimitError } from "@app/lib/errors";
|
||||||
import { readLimit } from "@app/server/config/rateLimiter";
|
import { readLimit } from "@app/server/config/rateLimiter";
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
@@ -20,10 +21,14 @@ export const registerEventRouter = async (server: FastifyZodProvider) => {
|
|||||||
rateLimit: readLimit
|
rateLimit: readLimit
|
||||||
},
|
},
|
||||||
schema: {
|
schema: {
|
||||||
|
hide: false,
|
||||||
|
tags: [ApiDocsTags.Events],
|
||||||
|
description: "Subscribe to project events",
|
||||||
body: z.object({
|
body: z.object({
|
||||||
projectId: z.string().trim(),
|
projectId: z.string().trim().describe(EventSubscriptions.SUBSCRIBE_PROJECT_EVENTS.projectId),
|
||||||
register: z.array(EventRegisterSchema).max(10)
|
register: z.array(EventRegisterSchema).min(1).max(10)
|
||||||
})
|
}),
|
||||||
|
produces: ["text/event-stream"]
|
||||||
},
|
},
|
||||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
handler: async (req, reply) => {
|
handler: async (req, reply) => {
|
||||||
@@ -75,13 +80,15 @@ export const registerEventRouter = async (server: FastifyZodProvider) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
req.body.register.forEach((r) => {
|
req.body.register.forEach((r) => {
|
||||||
|
const fields = {
|
||||||
|
environment: r.conditions?.environmentSlug ?? "",
|
||||||
|
secretPath: r.conditions?.secretPath ?? "/",
|
||||||
|
eventType: r.event
|
||||||
|
};
|
||||||
|
|
||||||
const allowed = info.permission.can(
|
const allowed = info.permission.can(
|
||||||
ProjectPermissionSecretActions.Subscribe,
|
ProjectPermissionSecretActions.Subscribe,
|
||||||
subject(ProjectPermissionSub.Secrets, {
|
subject(ProjectPermissionSub.Secrets, fields)
|
||||||
environment: r.conditions?.environmentSlug ?? "",
|
|
||||||
secretPath: r.conditions?.secretPath ?? "/",
|
|
||||||
eventType: r.event
|
|
||||||
})
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!allowed) {
|
if (!allowed) {
|
||||||
@@ -89,9 +96,9 @@ export const registerEventRouter = async (server: FastifyZodProvider) => {
|
|||||||
name: "PermissionDenied",
|
name: "PermissionDenied",
|
||||||
message: `You are not allowed to subscribe on secrets`,
|
message: `You are not allowed to subscribe on secrets`,
|
||||||
details: {
|
details: {
|
||||||
event: r.event,
|
event: fields.eventType,
|
||||||
environmentSlug: r.conditions?.environmentSlug,
|
environmentSlug: fields.environment,
|
||||||
secretPath: r.conditions?.secretPath ?? "/"
|
secretPath: fields.secretPath
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Project Events"
|
||||||
|
openapi: "POST /api/v1/events/subscribe/project-events"
|
||||||
|
---
|
||||||
@@ -41,6 +41,8 @@
|
|||||||
"group": "Platform Reference",
|
"group": "Platform Reference",
|
||||||
"pages": [
|
"pages": [
|
||||||
"documentation/platform/organization",
|
"documentation/platform/organization",
|
||||||
|
"documentation/platform/event-subscriptions",
|
||||||
|
"documentation/platform/folder",
|
||||||
{
|
{
|
||||||
"group": "Projects",
|
"group": "Projects",
|
||||||
"pages": [
|
"pages": [
|
||||||
@@ -764,6 +766,10 @@
|
|||||||
"group": "Admin",
|
"group": "Admin",
|
||||||
"pages": ["api-reference/endpoints/admin/bootstrap-instance"]
|
"pages": ["api-reference/endpoints/admin/bootstrap-instance"]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"group": "Events",
|
||||||
|
"pages": ["api-reference/endpoints/events/project-events"]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"group": "Identities",
|
"group": "Identities",
|
||||||
"pages": [
|
"pages": [
|
||||||
|
|||||||
@@ -0,0 +1,118 @@
|
|||||||
|
---
|
||||||
|
title: "Event Subscriptions"
|
||||||
|
sidebarTitle: "Events"
|
||||||
|
description: "Subscribe to events in Infisical for real-time updates"
|
||||||
|
---
|
||||||
|
|
||||||
|
<Info>
|
||||||
|
**Note:** Event Subscriptions is a paid feature. - **Infisical Cloud users:** Event Subscriptions is available under
|
||||||
|
the **Enterprise Tier**. - **Self-Hosted Infisical:** Please contact [[email protected]](mailto:[email protected])
|
||||||
|
to purchase an enterprise license.
|
||||||
|
</Info>
|
||||||
|
|
||||||
|
Event Subscriptions in Infisical allow you to receive real-time notifications when specific actions occur within your account or organization. These notifications include changes to secrets, users, teams, and many more **coming soon**.
|
||||||
|
|
||||||
|
## How It Works
|
||||||
|
|
||||||
|
- Server receives message over pubsub connection indicating changes have occurred
|
||||||
|
- Server processes the change notification
|
||||||
|
- Updated data is synchronized across all connected Infisical instances
|
||||||
|
- Client applications receive real-time updates through [Server-Sent Events (SSE)](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events)
|
||||||
|
- All servers maintain consistent state without manual intervention
|
||||||
|
|
||||||
|
This ensures your infrastructure stays up-to-date automatically, without requiring restarts or manual synchronization.
|
||||||
|
|
||||||
|
<Note>
|
||||||
|
Event Subscriptions are designed for real-time communication and do not include persistence or replay
|
||||||
|
capabilities—events are delivered once and are not stored for future retrieval.
|
||||||
|
</Note>
|
||||||
|
|
||||||
|
## Supported Resources
|
||||||
|
|
||||||
|
You can currently subscribe to notifications for the following resources and event types:
|
||||||
|
|
||||||
|
- **Secrets**
|
||||||
|
- `secret:created`: Triggered when a secret is created
|
||||||
|
- `secret:updated`: Triggered when a secret is updated
|
||||||
|
- `secret:deleted`: Triggered when a secret is deleted
|
||||||
|
|
||||||
|
## Permissions Setup
|
||||||
|
|
||||||
|
To receive events on a supported resource, the identity must have `Subscribe` action permission on that resource.
|
||||||
|
|
||||||
|
Follow these steps to set up the necessary permissions:
|
||||||
|
|
||||||
|
<Steps>
|
||||||
|
<Step title="Select a project and copy the Project ID">
|
||||||
|

|
||||||
|
|
||||||
|
On your project page, open **Project Settings** from the sidebar.
|
||||||
|
|
||||||
|
In the Project name section, click **Copy Project ID** to copy your Project ID, or extract it from the URL:
|
||||||
|
`https://app.infisical.com/project/<your_project_id>/settings`
|
||||||
|
|
||||||
|
</Step>
|
||||||
|
|
||||||
|
<Step title="Navigate to Access Management and open Project Roles">
|
||||||
|
  Navigate to **Access Management**, then select **Project Roles**.
|
||||||
|
</Step>
|
||||||
|
|
||||||
|
<Step title="Select an existing role or create a new one">
|
||||||
|
 You can either edit an existing role or create a new role
|
||||||
|
for event subscriptions.
|
||||||
|
</Step>
|
||||||
|
|
||||||
|
<Step title="Assign policies to the role">
|
||||||
|
 Select the specific resources that the role should have access
|
||||||
|
to. 
|
||||||
|
</Step>
|
||||||
|
|
||||||
|
<Step title="Enable the Subscribe action in permissions">
|
||||||
|

|
||||||
|
|
||||||
|
Ensure the **Subscribe** action is selected for the relevant resources and events.
|
||||||
|
|
||||||
|
## Conditions
|
||||||
|
|
||||||
|
By default, the role will have access to all events for the selected resources in this project.
|
||||||
|
|
||||||
|
<AccordionGroup>
|
||||||
|
<Accordion title="Full Access">
|
||||||
|

|
||||||
|
</Accordion>
|
||||||
|
<Accordion title="Path Prefix">
|
||||||
|

|
||||||
|
</Accordion>
|
||||||
|
<Accordion title="Environment">
|
||||||
|

|
||||||
|
</Accordion>
|
||||||
|
</AccordionGroup>
|
||||||
|
</Step>
|
||||||
|
</Steps>
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
Currently, events are only available via [API](/api-reference/endpoints/events) but will soon be available in our SDKs, Kubernetes Operator, and more.
|
||||||
|
|
||||||
|
### API Usage
|
||||||
|
|
||||||
|
You need an auth token to use this API. To get an authentication token, follow the authentication guide for one of our supported auth methods from the [machine identities documentation](/documentation/platform/identities/machine-identities#authentication-methods).
|
||||||
|
|
||||||
|
#### Creating a Subscription
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
**Request Parameters:**
|
||||||
|
|
||||||
|
- `projectId`: Project whose events you want to subscribe to
|
||||||
|
- `register`: List of event filters
|
||||||
|
- `conditions`: Conditions to filter events on
|
||||||
|
- `environmentSlug`: Project environment
|
||||||
|
- `secretPath`: Path of the secrets
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
The subscribe endpoint responds with a `text/event-stream` content type to initiate SSE streaming.
|
||||||
|
|
||||||
|
For more specific details, please refer to our [API Reference](/api-reference/endpoints/events).
|
||||||
|
After Width: | Height: | Size: 96 KiB |
|
After Width: | Height: | Size: 60 KiB |
|
After Width: | Height: | Size: 96 KiB |
|
After Width: | Height: | Size: 476 KiB |
|
After Width: | Height: | Size: 747 KiB |
|
After Width: | Height: | Size: 910 KiB |
|
After Width: | Height: | Size: 531 KiB |
|
After Width: | Height: | Size: 401 KiB |
|
After Width: | Height: | Size: 484 KiB |
|
After Width: | Height: | Size: 397 KiB |
|
After Width: | Height: | Size: 734 KiB |
|
After Width: | Height: | Size: 441 KiB |
|
After Width: | Height: | Size: 735 KiB |
|
After Width: | Height: | Size: 697 KiB |
|
After Width: | Height: | Size: 704 KiB |
|
After Width: | Height: | Size: 879 KiB |
|
After Width: | Height: | Size: 273 KiB |
|
After Width: | Height: | Size: 168 KiB |
|
After Width: | Height: | Size: 858 KiB |
|
After Width: | Height: | Size: 866 KiB |
|
After Width: | Height: | Size: 895 KiB |
|
After Width: | Height: | Size: 834 KiB |
|
After Width: | Height: | Size: 678 KiB |