fix(integration): thread requester identity through unfurl for per-user authorization

This commit is contained in:
Philipinho
2026-08-08 23:19:23 +01:00
parent c29249fe13
commit 21665fc0e7
2 changed files with 24 additions and 0 deletions
@@ -66,8 +66,24 @@ export type UnfurlOpts = {
match: RegExpMatchArray;
patternType: string;
settings?: Record<string, any>;
// 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;
@@ -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<string, any>) ?? {},
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;
}