diff --git a/apps/server/src/core/integration/registry/integration-provider.interface.ts b/apps/server/src/core/integration/registry/integration-provider.interface.ts index 3c70db367..9d78d4a3d 100644 --- a/apps/server/src/core/integration/registry/integration-provider.interface.ts +++ b/apps/server/src/core/integration/registry/integration-provider.interface.ts @@ -66,8 +66,24 @@ export type UnfurlOpts = { match: RegExpMatchArray; patternType: string; settings?: Record; + // The requesting Docmost user and integration. Providers backed by a shared + // (workspace) connection MUST authorize the requester against the target + // resource before returning content: the shared bot token is not itself + // proof that the requester may see it. + userId: string; + integrationId: string; }; +// Thrown by a provider's unfurl() when the requesting user is not authorized +// to view the linked resource. UnfurlService turns it into a null result +// (no card) rather than logging it as an error. +export class UnfurlForbiddenError extends Error { + constructor(message = 'Not authorized to unfurl this link') { + super(message); + this.name = 'UnfurlForbiddenError'; + } +} + export type LinkDescription = { title: string; description?: string; diff --git a/apps/server/src/core/integration/unfurl/unfurl.service.ts b/apps/server/src/core/integration/unfurl/unfurl.service.ts index 63a675cb9..1dc91bb81 100644 --- a/apps/server/src/core/integration/unfurl/unfurl.service.ts +++ b/apps/server/src/core/integration/unfurl/unfurl.service.ts @@ -6,6 +6,7 @@ import { OAuthService } from '../oauth/oauth.service'; import { UnfurlResult, UnfurlNeedsConnection, + UnfurlForbiddenError, IntegrationProvider, } from '../registry/integration-provider.interface'; import { RedisService } from '@nestjs-labs/nestjs-ioredis'; @@ -89,6 +90,8 @@ export class UnfurlService { match, patternType, settings: (integration.settings as Record) ?? {}, + userId, + integrationId: integration.id, }); await this.redis.set( @@ -100,6 +103,11 @@ export class UnfurlService { return unfurlResult; } catch (err) { + // Not-authorized is an expected outcome (no card), not an error. + if (err instanceof UnfurlForbiddenError) { + this.logger.debug(`Unfurl not authorized for ${url}`); + return null; + } this.logger.error(`Unfurl failed for ${url}: ${(err as Error).message}`); return null; }