Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
5 changes: 5 additions & 0 deletions apps/api/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -426,6 +426,10 @@ async fn app_with_env(env: &'static crate::env::RuntimeConfig) -> Router {
rate_limit::rate_limit,
));

let scim_routes = match subscription_config.clone() {
Some(config) => anlg_api_subscription::scim_router(config),
None => Router::new(),
};
let subscription_routes = match subscription_config {
Some(config) => {
let router = anlg_api_subscription::router(config);
Expand Down Expand Up @@ -458,6 +462,7 @@ async fn app_with_env(env: &'static crate::env::RuntimeConfig) -> Router {
.nest("/sync", sync_routes)
.merge(integration_routes)
.merge(integration_management_routes)
.nest("/scim/v2", scim_routes)
.merge(auth_routes)
.layer(
CorsLayer::new()
Expand Down
40 changes: 40 additions & 0 deletions apps/desktop/src/enterprise-capture/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ import { beforeEach, describe, expect, it, vi } from "vitest";

import {
acknowledgeSessionDelivery,
cancelScheduledCapture,
EnterpriseCaptureClientError,
listScheduledCaptures,
listSessionDeliveries,
} from "./client";

Expand Down Expand Up @@ -105,6 +107,44 @@ describe("enterprise capture client", () => {
});
});

it("lists and cancels upcoming scheduled captures", async () => {
const scheduled = {
calendarEventId: "evt-1",
title: "Standup",
startsAt: "2026-08-21T15:00:00Z",
status: "pending",
jobId: "cal-evt-1",
};
vi.mocked(fetch).mockResolvedValueOnce(jsonResponse([scheduled]));

await expect(
listScheduledCaptures({
serverUrl: "https://capture.example.test/control/",
accessToken: "access-token",
workspaceId: "workspace 1",
}),
).resolves.toEqual([scheduled]);
expect(vi.mocked(fetch).mock.calls[0]?.[0]).toBe(
"https://capture.example.test/control/v1/workspaces/workspace%201/scheduled-captures",
);

vi.mocked(fetch).mockResolvedValueOnce(
jsonResponse({ ...scheduled, status: "canceled" }),
);
await expect(
cancelScheduledCapture({
serverUrl: "https://capture.example.test",
accessToken: "access-token",
workspaceId: "workspace-1",
calendarEventId: "evt/1",
}),
).resolves.toMatchObject({ status: "canceled" });
expect(String(vi.mocked(fetch).mock.calls[1]?.[0])).toContain(
"/scheduled-captures/evt%2F1",
);
expect(vi.mocked(fetch).mock.calls[1]?.[1]?.method).toBe("DELETE");
});

it("rejects an oversized response without a declared content length", async () => {
const chunk = new Uint8Array(1024 * 1024);
let chunksRead = 0;
Expand Down
69 changes: 68 additions & 1 deletion apps/desktop/src/enterprise-capture/client.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { fetch } from "@tauri-apps/plugin-http";

import type { DeliveryItem, DeliveryPage } from "./types";
import type { DeliveryItem, DeliveryPage, ScheduledCapture } from "./types";

const REQUEST_TIMEOUT_MS = 30_000;
const MAX_PAGE_BYTES = 24 * 1024 * 1024;
Expand Down Expand Up @@ -66,6 +66,45 @@ export async function acknowledgeSessionDelivery(input: {
}
}

export async function listScheduledCaptures(input: {
serverUrl: string;
accessToken: string;
workspaceId: string;
}): Promise<ScheduledCapture[]> {
const body = await request(
endpoint(
input.serverUrl,
`v1/workspaces/${encodeURIComponent(input.workspaceId)}/scheduled-captures`,
),
input.accessToken,
);
if (!Array.isArray(body)) {
throw new EnterpriseCaptureClientError(
"invalid_response",
"The capture server returned an invalid scheduled capture list.",
);
}
return body.map(parseScheduledCapture);
}

export async function cancelScheduledCapture(input: {
serverUrl: string;
accessToken: string;
workspaceId: string;
calendarEventId: string;
}): Promise<ScheduledCapture> {
return parseScheduledCapture(
await request(
endpoint(
input.serverUrl,
`v1/workspaces/${encodeURIComponent(input.workspaceId)}/scheduled-captures/${encodeURIComponent(input.calendarEventId)}`,
),
input.accessToken,
{ method: "DELETE" },
),
);
}

async function request(
url: URL,
accessToken: string,
Expand Down Expand Up @@ -215,6 +254,34 @@ function parseDeliveryItem(value: unknown): DeliveryItem {
};
}

function parseScheduledCapture(value: unknown): ScheduledCapture {
if (
!isObject(value) ||
typeof value.calendarEventId !== "string" ||
value.calendarEventId.length === 0 ||
typeof value.title !== "string" ||
typeof value.startsAt !== "string" ||
!Number.isFinite(Date.parse(value.startsAt)) ||
(value.status !== "pending" &&
value.status !== "skipped" &&
value.status !== "canceled" &&
value.status !== "dispatched") ||
(value.jobId !== null && typeof value.jobId !== "string")
) {
throw new EnterpriseCaptureClientError(
"invalid_response",
"The capture server returned an invalid scheduled capture.",
);
}
return {
calendarEventId: value.calendarEventId,
title: value.title,
startsAt: value.startsAt,
status: value.status,
jobId: value.jobId,
};
}

function integer(value: unknown, _field: string, allowZero = false): number {
if (
typeof value !== "number" ||
Expand Down
8 changes: 8 additions & 0 deletions apps/desktop/src/enterprise-capture/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,11 @@ export type PendingCompletion = {
sessionId: string;
revision: number;
};

export type ScheduledCapture = {
calendarEventId: string;
title: string;
startsAt: string;
status: "pending" | "skipped" | "canceled" | "dispatched";
jobId: string | null;
};
Loading
Loading