refactor!: rename chat providers to channels

This commit is contained in:
Peter Steinberger
2026-01-13 06:16:43 +00:00
parent 0cd632ba84
commit 90342a4f3a
393 changed files with 8004 additions and 6737 deletions

View File

@@ -1,6 +1,6 @@
import { requirePairingAdapter } from "../providers/plugins/pairing.js";
import type { PairingProvider } from "./pairing-store.js";
import { requirePairingAdapter } from "../channels/plugins/pairing.js";
import type { PairingChannel } from "./pairing-store.js";
export function resolvePairingIdLabel(provider: PairingProvider): string {
return requirePairingAdapter(provider).idLabel;
export function resolvePairingIdLabel(channel: PairingChannel): string {
return requirePairingAdapter(channel).idLabel;
}

View File

@@ -5,39 +5,39 @@ import { buildPairingReply } from "./pairing-messages.js";
describe("buildPairingReply", () => {
const cases = [
{
provider: "discord",
channel: "discord",
idLine: "Your Discord user id: 1",
code: "ABC123",
},
{
provider: "slack",
channel: "slack",
idLine: "Your Slack user id: U1",
code: "DEF456",
},
{
provider: "signal",
channel: "signal",
idLine: "Your Signal number: +15550001111",
code: "GHI789",
},
{
provider: "imessage",
channel: "imessage",
idLine: "Your iMessage sender id: +15550002222",
code: "JKL012",
},
{
provider: "whatsapp",
channel: "whatsapp",
idLine: "Your WhatsApp phone number: +15550003333",
code: "MNO345",
},
] as const;
for (const testCase of cases) {
it(`formats pairing reply for ${testCase.provider}`, () => {
it(`formats pairing reply for ${testCase.channel}`, () => {
const text = buildPairingReply(testCase);
expect(text).toContain(testCase.idLine);
expect(text).toContain(`Pairing code: ${testCase.code}`);
expect(text).toContain(
`clawdbot pairing approve ${testCase.provider} <code>`,
`clawdbot pairing approve ${testCase.channel} <code>`,
);
});
}

View File

@@ -1,11 +1,11 @@
import type { PairingProvider } from "./pairing-store.js";
import type { PairingChannel } from "./pairing-store.js";
export function buildPairingReply(params: {
provider: PairingProvider;
channel: PairingChannel;
idLine: string;
code: string;
}): string {
const { provider, idLine, code } = params;
const { channel, idLine, code } = params;
return [
"Clawdbot: access not configured.",
"",
@@ -14,6 +14,6 @@ export function buildPairingReply(params: {
`Pairing code: ${code}`,
"",
"Ask the bot owner to approve with:",
`clawdbot pairing approve ${provider} <code>`,
`clawdbot pairing approve ${channel} <code>`,
].join("\n");
}

View File

@@ -7,8 +7,8 @@ import { describe, expect, it, vi } from "vitest";
import { resolveOAuthDir } from "../config/paths.js";
import {
listProviderPairingRequests,
upsertProviderPairingRequest,
listChannelPairingRequests,
upsertChannelPairingRequest,
} from "./pairing-store.js";
async function withTempStateDir<T>(fn: (stateDir: string) => Promise<T>) {
@@ -27,19 +27,19 @@ async function withTempStateDir<T>(fn: (stateDir: string) => Promise<T>) {
describe("pairing store", () => {
it("reuses pending code and reports created=false", async () => {
await withTempStateDir(async () => {
const first = await upsertProviderPairingRequest({
provider: "discord",
const first = await upsertChannelPairingRequest({
channel: "discord",
id: "u1",
});
const second = await upsertProviderPairingRequest({
provider: "discord",
const second = await upsertChannelPairingRequest({
channel: "discord",
id: "u1",
});
expect(first.created).toBe(true);
expect(second.created).toBe(false);
expect(second.code).toBe(first.code);
const list = await listProviderPairingRequests("discord");
const list = await listChannelPairingRequests("discord");
expect(list).toHaveLength(1);
expect(list[0]?.code).toBe(first.code);
});
@@ -47,8 +47,8 @@ describe("pairing store", () => {
it("expires pending requests after TTL", async () => {
await withTempStateDir(async (stateDir) => {
const created = await upsertProviderPairingRequest({
provider: "signal",
const created = await upsertChannelPairingRequest({
channel: "signal",
id: "+15550001111",
});
expect(created.created).toBe(true);
@@ -71,11 +71,11 @@ describe("pairing store", () => {
"utf8",
);
const list = await listProviderPairingRequests("signal");
const list = await listChannelPairingRequests("signal");
expect(list).toHaveLength(0);
const next = await upsertProviderPairingRequest({
provider: "signal",
const next = await upsertChannelPairingRequest({
channel: "signal",
id: "+15550001111",
});
expect(next.created).toBe(true);
@@ -87,8 +87,8 @@ describe("pairing store", () => {
const spy = vi.spyOn(crypto, "randomInt");
try {
spy.mockReturnValue(0);
const first = await upsertProviderPairingRequest({
provider: "telegram",
const first = await upsertChannelPairingRequest({
channel: "telegram",
id: "123",
});
expect(first.code).toBe("AAAAAAAA");
@@ -96,8 +96,8 @@ describe("pairing store", () => {
const sequence = Array(8).fill(0).concat(Array(8).fill(1));
let idx = 0;
spy.mockImplementation(() => sequence[idx++] ?? 1);
const second = await upsertProviderPairingRequest({
provider: "telegram",
const second = await upsertChannelPairingRequest({
channel: "telegram",
id: "456",
});
expect(second.code).toBe("BBBBBBBB");
@@ -111,20 +111,20 @@ describe("pairing store", () => {
await withTempStateDir(async () => {
const ids = ["+15550000001", "+15550000002", "+15550000003"];
for (const id of ids) {
const created = await upsertProviderPairingRequest({
provider: "whatsapp",
const created = await upsertChannelPairingRequest({
channel: "whatsapp",
id,
});
expect(created.created).toBe(true);
}
const blocked = await upsertProviderPairingRequest({
provider: "whatsapp",
const blocked = await upsertChannelPairingRequest({
channel: "whatsapp",
id: "+15550000004",
});
expect(blocked.created).toBe(false);
const list = await listProviderPairingRequests("whatsapp");
const list = await listChannelPairingRequests("whatsapp");
const listIds = list.map((entry) => entry.id);
expect(listIds).toHaveLength(3);
expect(listIds).toContain("+15550000001");

View File

@@ -4,10 +4,9 @@ import os from "node:os";
import path from "node:path";
import lockfile from "proper-lockfile";
import { requirePairingAdapter } from "../channels/plugins/pairing.js";
import type { ChannelId } from "../channels/plugins/types.js";
import { resolveOAuthDir, resolveStateDir } from "../config/paths.js";
import { requirePairingAdapter } from "../providers/plugins/pairing.js";
import type { ProviderId } from "../providers/plugins/types.js";
const PAIRING_CODE_LENGTH = 8;
const PAIRING_CODE_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
@@ -24,7 +23,7 @@ const PAIRING_STORE_LOCK_OPTIONS = {
stale: 30_000,
} as const;
export type PairingProvider = ProviderId;
export type PairingChannel = ChannelId;
export type PairingRequest = {
id: string;
@@ -50,17 +49,17 @@ function resolveCredentialsDir(env: NodeJS.ProcessEnv = process.env): string {
}
function resolvePairingPath(
provider: PairingProvider,
channel: PairingChannel,
env: NodeJS.ProcessEnv = process.env,
): string {
return path.join(resolveCredentialsDir(env), `${provider}-pairing.json`);
return path.join(resolveCredentialsDir(env), `${channel}-pairing.json`);
}
function resolveAllowFromPath(
provider: PairingProvider,
channel: PairingChannel,
env: NodeJS.ProcessEnv = process.env,
): string {
return path.join(resolveCredentialsDir(env), `${provider}-allowFrom.json`);
return path.join(resolveCredentialsDir(env), `${channel}-allowFrom.json`);
}
function safeParseJson<T>(raw: string): T | null {
@@ -194,8 +193,8 @@ function normalizeId(value: string | number): string {
return String(value).trim();
}
function normalizeAllowEntry(provider: PairingProvider, entry: string): string {
const adapter = requirePairingAdapter(provider);
function normalizeAllowEntry(channel: PairingChannel, entry: string): string {
const adapter = requirePairingAdapter(channel);
const trimmed = entry.trim();
if (!trimmed) return "";
if (trimmed === "*") return "";
@@ -205,30 +204,30 @@ function normalizeAllowEntry(provider: PairingProvider, entry: string): string {
return String(normalized).trim();
}
export async function readProviderAllowFromStore(
provider: PairingProvider,
export async function readChannelAllowFromStore(
channel: PairingChannel,
env: NodeJS.ProcessEnv = process.env,
): Promise<string[]> {
requirePairingAdapter(provider);
const filePath = resolveAllowFromPath(provider, env);
requirePairingAdapter(channel);
const filePath = resolveAllowFromPath(channel, env);
const { value } = await readJsonFile<AllowFromStore>(filePath, {
version: 1,
allowFrom: [],
});
const list = Array.isArray(value.allowFrom) ? value.allowFrom : [];
return list
.map((v) => normalizeAllowEntry(provider, String(v)))
.map((v) => normalizeAllowEntry(channel, String(v)))
.filter(Boolean);
}
export async function addProviderAllowFromStoreEntry(params: {
provider: PairingProvider;
export async function addChannelAllowFromStoreEntry(params: {
channel: PairingChannel;
entry: string | number;
env?: NodeJS.ProcessEnv;
}): Promise<{ changed: boolean; allowFrom: string[] }> {
requirePairingAdapter(params.provider);
requirePairingAdapter(params.channel);
const env = params.env ?? process.env;
const filePath = resolveAllowFromPath(params.provider, env);
const filePath = resolveAllowFromPath(params.channel, env);
return await withFileLock(
filePath,
{ version: 1, allowFrom: [] } satisfies AllowFromStore,
@@ -238,10 +237,10 @@ export async function addProviderAllowFromStoreEntry(params: {
allowFrom: [],
});
const current = (Array.isArray(value.allowFrom) ? value.allowFrom : [])
.map((v) => normalizeAllowEntry(params.provider, String(v)))
.map((v) => normalizeAllowEntry(params.channel, String(v)))
.filter(Boolean);
const normalized = normalizeAllowEntry(
params.provider,
params.channel,
normalizeId(params.entry),
);
if (!normalized) return { changed: false, allowFrom: current };
@@ -257,12 +256,12 @@ export async function addProviderAllowFromStoreEntry(params: {
);
}
export async function listProviderPairingRequests(
provider: PairingProvider,
export async function listChannelPairingRequests(
channel: PairingChannel,
env: NodeJS.ProcessEnv = process.env,
): Promise<PairingRequest[]> {
requirePairingAdapter(provider);
const filePath = resolvePairingPath(provider, env);
requirePairingAdapter(channel);
const filePath = resolvePairingPath(channel, env);
return await withFileLock(
filePath,
{ version: 1, requests: [] } satisfies PairingStore,
@@ -299,15 +298,15 @@ export async function listProviderPairingRequests(
);
}
export async function upsertProviderPairingRequest(params: {
provider: PairingProvider;
export async function upsertChannelPairingRequest(params: {
channel: PairingChannel;
id: string | number;
meta?: Record<string, string | undefined | null>;
env?: NodeJS.ProcessEnv;
}): Promise<{ code: string; created: boolean }> {
requirePairingAdapter(params.provider);
requirePairingAdapter(params.channel);
const env = params.env ?? process.env;
const filePath = resolvePairingPath(params.provider, env);
const filePath = resolvePairingPath(params.channel, env);
return await withFileLock(
filePath,
{ version: 1, requests: [] } satisfies PairingStore,
@@ -398,17 +397,17 @@ export async function upsertProviderPairingRequest(params: {
);
}
export async function approveProviderPairingCode(params: {
provider: PairingProvider;
export async function approveChannelPairingCode(params: {
channel: PairingChannel;
code: string;
env?: NodeJS.ProcessEnv;
}): Promise<{ id: string; entry?: PairingRequest } | null> {
requirePairingAdapter(params.provider);
requirePairingAdapter(params.channel);
const env = params.env ?? process.env;
const code = params.code.trim().toUpperCase();
if (!code) return null;
const filePath = resolvePairingPath(params.provider, env);
const filePath = resolvePairingPath(params.channel, env);
return await withFileLock(
filePath,
{ version: 1, requests: [] } satisfies PairingStore,
@@ -439,8 +438,8 @@ export async function approveProviderPairingCode(params: {
version: 1,
requests: pruned,
} satisfies PairingStore);
await addProviderAllowFromStoreEntry({
provider: params.provider,
await addChannelAllowFromStoreEntry({
channel: params.channel,
entry: entry.id,
env,
});