Infrastructure build to support third-party app integrations
This commit is contained in:
132
backend/db/integrations.dao.ts
Normal file
132
backend/db/integrations.dao.ts
Normal file
@@ -0,0 +1,132 @@
|
||||
import { query } from './index.js';
|
||||
import { decryptSecret, encryptSecret } from '../lib/crypto.js';
|
||||
import type { IntegrationRow, ProviderSlug } from '../types/integration.js';
|
||||
|
||||
type IntegrationRecord = {
|
||||
id: number;
|
||||
provider: ProviderSlug;
|
||||
external_workspace_id: string | null;
|
||||
display_name: string | null;
|
||||
api_key_hash: string | null;
|
||||
access_token_ciphertext: string | null;
|
||||
refresh_token_ciphertext: string | null;
|
||||
signing_secret_ciphertext: string | null;
|
||||
token_expires_at: Date | null;
|
||||
scopes: string[] | null;
|
||||
};
|
||||
|
||||
const PUBLIC_COLUMNS = `
|
||||
id, provider, external_workspace_id, display_name, token_expires_at, scopes
|
||||
`;
|
||||
|
||||
/**
|
||||
* Ciphertext columns are dropped here rather than in the query so that every
|
||||
* caller path converges on a value that structurally cannot carry a secret.
|
||||
*/
|
||||
const toPublicRow = (record: IntegrationRecord): IntegrationRow => ({
|
||||
id: record.id,
|
||||
provider: record.provider,
|
||||
externalWorkspaceId: record.external_workspace_id,
|
||||
displayName: record.display_name,
|
||||
scopes: record.scopes ?? [],
|
||||
tokenExpiresAt: record.token_expires_at,
|
||||
});
|
||||
|
||||
export const findByApiKeyHash = async (
|
||||
hash: string
|
||||
): Promise<IntegrationRow | null> => {
|
||||
const { rows } = await query<IntegrationRecord>(
|
||||
`SELECT ${PUBLIC_COLUMNS} FROM integrations WHERE api_key_hash = $1`,
|
||||
[hash]
|
||||
);
|
||||
return rows[0] ? toPublicRow(rows[0]) : null;
|
||||
};
|
||||
|
||||
export const findByProviderWorkspace = async (
|
||||
provider: ProviderSlug,
|
||||
externalWorkspaceId: string
|
||||
): Promise<IntegrationRow | null> => {
|
||||
const { rows } = await query<IntegrationRecord>(
|
||||
`SELECT ${PUBLIC_COLUMNS} FROM integrations
|
||||
WHERE provider = $1 AND external_workspace_id = $2`,
|
||||
[provider, externalWorkspaceId]
|
||||
);
|
||||
return rows[0] ? toPublicRow(rows[0]) : null;
|
||||
};
|
||||
|
||||
export const upsertInstall = async (input: {
|
||||
provider: ProviderSlug;
|
||||
externalWorkspaceId: string;
|
||||
displayName?: string;
|
||||
apiKeyHash?: string;
|
||||
signingSecret?: string;
|
||||
scopes?: string[];
|
||||
}): Promise<IntegrationRow> => {
|
||||
const { rows } = await query<IntegrationRecord>(
|
||||
`INSERT INTO integrations
|
||||
(provider, external_workspace_id, display_name, api_key_hash,
|
||||
signing_secret_ciphertext, scopes)
|
||||
VALUES ($1, $2, $3, $4, $5, $6)
|
||||
ON CONFLICT (provider, external_workspace_id) DO UPDATE SET
|
||||
display_name = EXCLUDED.display_name,
|
||||
api_key_hash = COALESCE(EXCLUDED.api_key_hash, integrations.api_key_hash),
|
||||
signing_secret_ciphertext = COALESCE(
|
||||
EXCLUDED.signing_secret_ciphertext, integrations.signing_secret_ciphertext
|
||||
),
|
||||
scopes = EXCLUDED.scopes,
|
||||
updated_at = NOW()
|
||||
RETURNING ${PUBLIC_COLUMNS}`,
|
||||
[
|
||||
input.provider,
|
||||
input.externalWorkspaceId,
|
||||
input.displayName ?? null,
|
||||
input.apiKeyHash ?? null,
|
||||
input.signingSecret ? encryptSecret(input.signingSecret) : null,
|
||||
input.scopes ?? [],
|
||||
]
|
||||
);
|
||||
return toPublicRow(rows[0]);
|
||||
};
|
||||
|
||||
export const updateTokens = async (input: {
|
||||
id: number;
|
||||
accessToken: string;
|
||||
refreshToken?: string;
|
||||
expiresAt?: Date;
|
||||
}): Promise<void> => {
|
||||
await query(
|
||||
`UPDATE integrations SET
|
||||
access_token_ciphertext = $2,
|
||||
refresh_token_ciphertext = COALESCE($3, refresh_token_ciphertext),
|
||||
token_expires_at = $4,
|
||||
updated_at = NOW()
|
||||
WHERE id = $1`,
|
||||
[
|
||||
input.id,
|
||||
encryptSecret(input.accessToken),
|
||||
input.refreshToken ? encryptSecret(input.refreshToken) : null,
|
||||
input.expiresAt ?? null,
|
||||
]
|
||||
);
|
||||
};
|
||||
|
||||
const readSecret = async (
|
||||
id: number,
|
||||
column: 'access_token_ciphertext' | 'refresh_token_ciphertext' | 'signing_secret_ciphertext'
|
||||
): Promise<string | null> => {
|
||||
const { rows } = await query<Record<string, string | null>>(
|
||||
`SELECT ${column} AS value FROM integrations WHERE id = $1`,
|
||||
[id]
|
||||
);
|
||||
const value = rows[0]?.value;
|
||||
return value ? decryptSecret(value) : null;
|
||||
};
|
||||
|
||||
export const getAccessToken = (id: number): Promise<string | null> =>
|
||||
readSecret(id, 'access_token_ciphertext');
|
||||
|
||||
export const getRefreshToken = (id: number): Promise<string | null> =>
|
||||
readSecret(id, 'refresh_token_ciphertext');
|
||||
|
||||
export const getSigningSecret = (id: number): Promise<string | null> =>
|
||||
readSecret(id, 'signing_secret_ciphertext');
|
||||
Reference in New Issue
Block a user