mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-06 05:56:19 +01:00
merge: reconcile published native prerequisite and current master
Preserve canonical startup ownership, scoped evidence, current Storybook IDs, and the Codex form controls while carrying Claude effort and immutable source bindings. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
commit
48ffca9438
415 files changed
+22501
-15340
No files matched your search
@@ -1,4 +1,4 @@
|
||||
import { slackRegistrationSchema, slackSetupActionSchema } from "@paperclipai/shared";
|
||||
import { slackTestMessageSchema, slackRegistrationSchema, slackSetupActionSchema, slackManagedProvisionSchema } from "@paperclipai/shared";
|
||||
import { isCrossSiteOAuthCallbackNavigation, oauthCallbackInterstitialHtml } from "../lib/oauth-browser-return.js";
|
||||
import {
|
||||
Router,
|
||||
@@ -164,6 +164,10 @@ export function chatChannelRoutes(db: Db, options: ChatChannelRouteOptions) {
|
||||
async (req, res) => {
|
||||
const companyId = req.params.companyId as string;
|
||||
await assertConnectionManager(req, companyId);
|
||||
if (req.body.provider === "slack" && !req.body.slackSetupMethod) {
|
||||
req.body.slackSetupMethod = service.slackRegistration.managerGrants.defaultMethod();
|
||||
}
|
||||
if (req.body.slackSetupMethod === "managed" && service.slackRegistration.managerGrants.defaultMethod() !== "managed") throw forbidden("Managed Slack setup is unavailable");
|
||||
res
|
||||
.status(201)
|
||||
.json(await service.create(companyId, req.body, actorUserId(req)));
|
||||
@@ -182,6 +186,50 @@ export function chatChannelRoutes(db: Db, options: ChatChannelRouteOptions) {
|
||||
return { userId: actor.actorId, sessionId: actor.sessionId,
|
||||
bypassPermissionCheck: req.actor.source === "local_implicit" || req.actor.isInstanceAdmin === true };
|
||||
};
|
||||
router.get("/companies/:companyId/chat-slack/setup-options", async (req, res) => {
|
||||
const companyId = req.params.companyId as string;
|
||||
await assertConnectionManager(req, companyId);
|
||||
res.set("Cache-Control", "no-store");
|
||||
res.json(await service.slackRegistration.managerGrants.choices(companyId, slackActor(req)));
|
||||
});
|
||||
router.post("/chat-endpoints/:endpointId/slack/managed/authorize", validate(slackSetupActionSchema), async (req, res) => {
|
||||
if (!(await assertEndpointManagementAccess(req, res))) return;
|
||||
res.set("Cache-Control", "no-store");
|
||||
res.json(await service.slackRegistration.managerGrants.authorize(endpointId(req), slackActor(req)));
|
||||
});
|
||||
router.post("/chat-endpoints/:endpointId/slack/managed/provision", validate(slackManagedProvisionSchema), async (req, res) => {
|
||||
if (!(await assertEndpointManagementAccess(req, res))) return;
|
||||
res.set("Cache-Control", "no-store");
|
||||
res.json(await service.slackRegistration.managed.provision(endpointId(req), slackActor(req), req.body));
|
||||
});
|
||||
router.delete("/companies/:companyId/chat-slack/manager-grants/:grantId", async (req, res) => {
|
||||
const companyId = req.params.companyId as string;
|
||||
await assertConnectionManager(req, companyId);
|
||||
await service.slackRegistration.managerGrants.revoke(req.params.grantId as string, companyId, slackActor(req));
|
||||
res.sendStatus(204);
|
||||
});
|
||||
router.get("/chat-slack/managed/oauth/callback", async (req, res) => {
|
||||
res.set("Cache-Control", "no-store"); res.set("Referrer-Policy", "no-referrer");
|
||||
const actor = slackActor(req);
|
||||
const state = typeof req.query.state === "string" ? req.query.state : "";
|
||||
const expiredId = await service.slackRegistration.managerGrants.expiredReturn(state, actor);
|
||||
if (expiredId) { res.redirect(303, await service.slackRegistration.returnPath(expiredId)); return; }
|
||||
const pending = await service.slackRegistration.managerGrants.pending(state, actor);
|
||||
await assertConnectionManager(req, pending.current.endpoint.companyId);
|
||||
if (req.get("accept")?.includes("text/html") && isCrossSiteOAuthCallbackNavigation(req)) {
|
||||
res.type("html").send(oauthCallbackInterstitialHtml()); return;
|
||||
}
|
||||
const id = await service.slackRegistration.managerGrants.complete(state,
|
||||
typeof req.query.claim_id === "string" ? req.query.claim_id : null,
|
||||
typeof req.query.error === "string" ? req.query.error : null, actor,
|
||||
typeof req.query.code === "string" ? req.query.code : null);
|
||||
res.redirect(303, await service.slackRegistration.returnPath(id));
|
||||
});
|
||||
router.post("/chat-endpoints/:endpointId/slack/test-message", validate(slackTestMessageSchema), async (req, res) => {
|
||||
if (!(await assertEndpointManagementAccess(req, res))) return;
|
||||
res.set("Cache-Control", "no-store");
|
||||
res.json(await service.sendSlackTestMessage(endpointId(req), slackActor(req), req.body));
|
||||
});
|
||||
router.post("/chat-endpoints/:endpointId/slack/registration", validate(slackRegistrationSchema), async (req, res) => {
|
||||
if (!(await assertEndpointManagementAccess(req, res))) return;
|
||||
res.set("Cache-Control", "no-store");
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { updateFastResponseSchema } from "@paperclipai/shared";
|
||||
import { slackRegistrationSchema, slackSetupActionSchema, slackInstallAuthorizationSchema, slackRegistrationStateSchema, slackAppConfigurationSchema, slackAvatarStateSchema, slackAccountStateSchema } from "@paperclipai/shared";
|
||||
import { slackTestMessageSchema, slackTestMessageResultSchema, slackManagerAuthorizationSchema, slackSetupOptionsSchema, slackManagedProvisionSchema, slackManagedResultSchema, slackRegistrationSchema, slackSetupActionSchema, slackInstallAuthorizationSchema, slackRegistrationStateSchema, slackAppConfigurationSchema, slackAvatarStateSchema, slackAccountStateSchema } from "@paperclipai/shared";
|
||||
import { experimentalApiMetadata } from "./experimental-api-metadata.js";
|
||||
import {
|
||||
experimentalApiPaths,
|
||||
@@ -803,8 +803,9 @@ const chatAdapterCapabilitiesResponseSchema = z
|
||||
const chatEndpointSetupResponseSchema = z
|
||||
.object({
|
||||
step: z.enum(["choose_agent", "provider_setup", "test", "complete"]),
|
||||
slackSetupMethod: z.enum(["automatic", "manual", "existing"]).optional(),
|
||||
slackSetupMethod: z.enum(["managed", "automatic", "manual", "existing"]).optional(),
|
||||
slackRegistration: slackRegistrationStateSchema.optional(),
|
||||
slackManagerError: z.string().optional(),
|
||||
slackAvatar: slackAvatarStateSchema.optional(),
|
||||
slackAccount: slackAccountStateSchema.optional(),
|
||||
slackOAuthCallbackUri: z.string().nullable().optional(),
|
||||
@@ -2563,6 +2564,41 @@ registry.registerPath({
|
||||
},
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "get", path: "/api/companies/{companyId}/chat-slack/setup-options", tags: ["chat-channels"],
|
||||
summary: "Discover Slack setup methods and the current user's authorized workspaces",
|
||||
description: "Managed setup is advertised only for approved Cloud deployments with the broker profile enabled. Self-hosted discovery makes no hosted-service requests. Grants are personal and company scoped.",
|
||||
request: { params: z.object({ companyId: z.string().uuid() }) }, responses: { 200: r.ok(slackSetupOptionsSchema), 401: r.unauthorized, 403: r.forbidden },
|
||||
});
|
||||
for (const action of ["authorize", "provision"] as const) registry.registerPath({
|
||||
method: "post", path: `/api/chat-endpoints/{endpointId}/slack/managed/${action}`, tags: ["chat-channels"],
|
||||
summary: action === "authorize" ? "Authorize a personal Slack manager workspace grant" : "Provision the saved managed Slack app",
|
||||
description: "Requires the selected managed method, company membership, connection-management permission and Cloud availability. Authorization state is actor/session bound and single use. Provisioning accepts a personal grant ID and idempotent request ID; no provider credentials or arbitrary manifests are accepted. Explicit reconnect uses reconnect=true with a fresh request ID to reinstall the saved app. Repeating that reconnect request returns its saved result without reinstalling a healthy connection.",
|
||||
request: { params: z.object({ endpointId: z.string().uuid() }), body: jsonBody(action === "authorize" ? slackSetupActionSchema : slackManagedProvisionSchema) },
|
||||
responses: { 200: r.ok(action === "authorize" ? slackManagerAuthorizationSchema : slackManagedResultSchema), 400: r.badRequest, 401: r.unauthorized, 403: r.forbidden, 404: r.notFound, 409: r.conflict, 422: r.unprocessable },
|
||||
});
|
||||
registry.registerPath({
|
||||
method: "delete", path: "/api/companies/{companyId}/chat-slack/manager-grants/{grantId}", tags: ["chat-channels"],
|
||||
summary: "Revoke the current user's workspace grant for future managed provisioning",
|
||||
description: "Removes local manager credentials and invalidates pending manager authorization. Installed bots remain connected; this does not uninstall Slack apps.",
|
||||
request: { params: z.object({ companyId: z.string().uuid(), grantId: z.string().uuid() }) }, responses: { 204: { description: "Personal manager grant revoked" }, 401: r.unauthorized, 403: r.forbidden },
|
||||
});
|
||||
registry.registerPath({
|
||||
method: "get", path: "/api/chat-slack/managed/oauth/callback", tags: ["chat-channels"],
|
||||
summary: "Redeem a sealed manager authorization and return to Slack setup",
|
||||
description: "Authenticated, same-origin continuation with company, actor, session, endpoint revision and configured-origin checks. The broker handoff is single use; credentials are never returned to the browser. Explicit local development qualification accepts a Slack code instead of a broker claim; production ignores code.",
|
||||
request: { query: z.object({ state: z.string(), claim_id: z.string().optional(), code: z.string().optional(), error: z.string().optional() }) },
|
||||
responses: { 200: { description: "Same-origin continuation page" }, 303: { description: "Return to saved setup" }, 400: r.badRequest, 401: r.unauthorized, 403: r.forbidden, 409: r.conflict },
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "post", path: "/api/chat-endpoints/{endpointId}/slack/test-message", tags: ["chat-channels"],
|
||||
summary: "Connect the current user's Slack identity and send a test DM",
|
||||
description: "Requires a board connection manager and active company membership. Reuses only the actor's verified identity in the same workspace; otherwise returns paginated account choices and sends a private connection challenge. The account holder must confirm it while signed in before linking or test delivery. Revoked and conflicting links cannot be overwritten. The request ID prevents duplicate or ambiguous sends from being replayed.",
|
||||
request: { params: z.object({ endpointId: z.string().uuid() }), body: jsonBody(slackTestMessageSchema) },
|
||||
responses: { 200: r.ok(slackTestMessageResultSchema), 400: r.badRequest, 401: r.unauthorized, 403: r.forbidden, 404: r.notFound, 409: r.conflict, 422: r.unprocessable, 429: { description: "Wait before sending another test message" } },
|
||||
});
|
||||
|
||||
for (const action of ["registration", "install", "resume"] as const) {
|
||||
registry.registerPath({
|
||||
method: "post", path: `/api/chat-endpoints/{endpointId}/slack/${action}`, tags: ["chat-channels"],
|
||||
|
||||
Reference in new issue
Block a user