mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-22 16:51:28 +02:00
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work. > - Connection intents need secure provider implementations to complete setup. > - Some providers use managed OAuth or external credential brokers. > - Those tokens must stay out of durable Paperclip state and fail closed when refresh fails. > - This pull request adds managed connector backends and the required storage contract. > - The benefit is safer provider setup with governed credential lifecycles. ## Linked Issues or Issue Description Refs #11965 This is stack 8 of 11. It depends on stack 7 and replaces another reviewable part of #11965. ## What Changed - Add managed Google Workspace and external connector backends. - Add Vercel Connect support without storing provider bearer tokens. - Add replay-safe migration 0232 and its generated snapshot. - Fail closed and clear stale token bindings when organization OAuth refresh needs reauthorization. ## Verification - `pnpm --filter @paperclipai/server typecheck` - `pnpm --filter @paperclipai/server exec vitest run src/__tests__/tool-access-service.test.ts` - Result: 194 tests passed. - `pnpm --filter @paperclipai/db check:migrations` - `pnpm build` - `pnpm exec vitest run --project @paperclipai/server server/src/services/remote-url-credentials.test.ts` (5 passed, including URL userinfo vault extraction) ## Risks - Broker metadata errors can block provider setup. - OAuth refresh failure disables the shared organization connection until reauthorization. - Migration 0232 is generated, ordered after 0231, and safe to replay. > I checked `ROADMAP.md`. This stack continues the existing app connection work from #11965 and does not duplicate another planned item. ## Model Used OpenAI Codex, GPT-5. The runtime model ID and context window were not exposed. The model used reasoning, tool use, and code execution. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have linked the public source pull request with `Refs #` - [x] I have not referenced internal or instance-local Paperclip issues or links - [x] My branch name describes the change and contains no internal Paperclip ticket id or instance-derived details - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] All Paperclip CI gates are green - [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge
71 lines
2.4 KiB
TypeScript
71 lines
2.4 KiB
TypeScript
const REDACTED_URL_VALUE = "REDACTED";
|
|
|
|
const SENSITIVE_URL_FIELD_PATTERN =
|
|
/^(?:code|state|nonce|key|[A-Za-z0-9_-]*(?:api[-_]?key|access[-_]?token|auth(?:[-_]?token)?|token|authorization|bearer|secret|passwd|password|credential|jwt|private[-_]?key|cookie|connectionstring)[A-Za-z0-9_-]*)$/i;
|
|
|
|
export const REMOTE_URL_SECRET_CONFIG_PATH = "remote.url";
|
|
|
|
export interface SplitRemoteUrlCredential {
|
|
publicUrl: string;
|
|
secretUrl: string | null;
|
|
secretParameterNames: string[];
|
|
}
|
|
|
|
/**
|
|
* Separate credential-bearing query values from the URL that is safe to store
|
|
* on a tool connection. The complete URL is written to the encrypted vault;
|
|
* only its public origin, path, and non-sensitive query configuration remain
|
|
* in connection config and API responses.
|
|
*/
|
|
export function splitRemoteUrlCredential(value: string): SplitRemoteUrlCredential {
|
|
const input = value.trim();
|
|
const url = new URL(input);
|
|
const hasUserInfo = url.username.length > 0 || url.password.length > 0;
|
|
if (hasUserInfo) {
|
|
url.username = "";
|
|
url.password = "";
|
|
}
|
|
const secretParameterNames: string[] = [];
|
|
for (const key of [...url.searchParams.keys()]) {
|
|
if (!SENSITIVE_URL_FIELD_PATTERN.test(key)) continue;
|
|
secretParameterNames.push(key);
|
|
url.searchParams.delete(key);
|
|
}
|
|
return {
|
|
publicUrl: url.toString(),
|
|
secretUrl: hasUserInfo || secretParameterNames.length > 0 ? input : null,
|
|
secretParameterNames,
|
|
};
|
|
}
|
|
|
|
/** Preserve useful endpoint context in logs without retaining credentials. */
|
|
export function redactRemoteUrlCredential(value: string): string {
|
|
const input = value.trim();
|
|
try {
|
|
const url = new URL(input);
|
|
if (url.username || url.password) {
|
|
url.username = REDACTED_URL_VALUE;
|
|
url.password = "";
|
|
}
|
|
for (const key of [...url.searchParams.keys()]) {
|
|
if (SENSITIVE_URL_FIELD_PATTERN.test(key)) url.searchParams.set(key, REDACTED_URL_VALUE);
|
|
}
|
|
return url.toString();
|
|
} catch {
|
|
return "configured remote MCP endpoint";
|
|
}
|
|
}
|
|
|
|
/**
|
|
* A vault value may be replaced independently of connection config. Keep that
|
|
* replacement constrained to the public endpoint the operator originally
|
|
* reviewed; only credential-bearing query values may differ.
|
|
*/
|
|
export function remoteUrlCredentialMatchesPublicUrl(publicValue: string, secretValue: string): boolean {
|
|
try {
|
|
return splitRemoteUrlCredential(secretValue).publicUrl === new URL(publicValue).toString();
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|