Skip to content

Commit a6aa525

Browse files
ralyodioclaude
andcommitted
feat(social): bridge sh1pt → crawlproof.com connected accounts
Two new packages so sh1pt can post via accounts a user has already connected on crawlproof.com, instead of requiring a separate per-platform credential set in the local sh1pt vault. packages/crawlproof-social — @profullstack/crawlproof-social Typed HTTP client for crawlproof's /api/sp/v1/{accounts,posts} API. Reusable outside sh1pt; createCrawlproofSocialClient({token}) → .listAccounts(), .post({accountId, text, subreddit?, title?}). packages/social/crawlproof — @profullstack/sh1pt-social-crawlproof Standard defineSocial adapter that wraps the SDK. setup() prompts for a CRAWLPROOF_API_TOKEN, lists the user's accounts from crawlproof.com, lets them pick one. post() forwards to the picked account via the v1 API. This is additive — the existing 43 sh1pt-social-* adapters keep working unchanged. Users who want one place to manage social connections use the crawlproof adapter; users who want local-creds-only keep using the per-platform adapters. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 95119b7 commit a6aa525

7 files changed

Lines changed: 419 additions & 0 deletions

File tree

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"name": "@profullstack/crawlproof-social",
3+
"version": "0.1.0",
4+
"description": "Typed HTTP client for the Crawlproof social posting API (/api/sp/v1). Use it from sh1pt, Node scripts, or CI to post via accounts a user has connected on crawlproof.com.",
5+
"type": "module",
6+
"main": "./src/index.ts",
7+
"scripts": {
8+
"build": "tsc -p tsconfig.json",
9+
"typecheck": "tsc -p tsconfig.json --noEmit",
10+
"prepublishOnly": "pnpm build"
11+
},
12+
"license": "MIT",
13+
"repository": {
14+
"type": "git",
15+
"url": "git+https://github.com/profullstack/sh1pt.git",
16+
"directory": "packages/crawlproof-social"
17+
},
18+
"homepage": "https://sh1pt.com",
19+
"bugs": "https://github.com/profullstack/sh1pt/issues",
20+
"files": [
21+
"dist"
22+
]
23+
}
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
// @profullstack/crawlproof-social
2+
//
3+
// Typed HTTP client for crawlproof.com's social posting API:
4+
// GET /api/sp/v1/accounts — list a user's connected social accounts
5+
// POST /api/sp/v1/posts — publish a post via one of those accounts
6+
//
7+
// Auth: Bearer token issued at https://crawlproof.com/social/api-tokens.
8+
//
9+
// Usage:
10+
// import { createCrawlproofSocialClient } from '@profullstack/crawlproof-social';
11+
// const client = createCrawlproofSocialClient({ token: process.env.CRAWLPROOF_API_TOKEN! });
12+
// const accounts = await client.listAccounts();
13+
// const result = await client.post({ accountId: accounts[0].id, text: 'hello world' });
14+
15+
export type Platform =
16+
| 'bluesky'
17+
| 'reddit'
18+
| 'mastodon'
19+
| 'linkedin'
20+
| 'x'
21+
| 'facebook_page'
22+
| 'threads'
23+
| 'discord'
24+
| 'telegram'
25+
// Schema-supported, posting-not-yet-implemented:
26+
| 'pinterest'
27+
| 'instagram'
28+
| 'instagram_business'
29+
| 'tiktok'
30+
| 'youtube'
31+
| 'tumblr'
32+
| 'snapchat';
33+
34+
export type SocialAccount = {
35+
id: string;
36+
platform: Platform;
37+
handle: string;
38+
status: 'active' | 'token_expired' | 'suspended_by_platform' | 'user_disabled' | 'flagged';
39+
instance_url: string | null;
40+
last_post_at: string | null;
41+
created_at: string;
42+
};
43+
44+
export type PostInput = {
45+
accountId: string;
46+
text: string;
47+
// Reddit-only — required when posting to a reddit account.
48+
subreddit?: string;
49+
title?: string;
50+
};
51+
52+
export type PostResult = {
53+
postId: string; // sp_post.id on crawlproof
54+
platformPostId: string; // platform-native id
55+
webUrl: string;
56+
};
57+
58+
export type CrawlproofSocialClient = {
59+
listAccounts(): Promise<SocialAccount[]>;
60+
post(input: PostInput): Promise<PostResult>;
61+
};
62+
63+
export type ClientOptions = {
64+
token: string;
65+
// Defaults to https://crawlproof.com. Override for self-hosted or
66+
// staging environments.
67+
baseUrl?: string;
68+
// Override the global fetch (e.g. inject undici, an instrumented
69+
// fetch). Defaults to globalThis.fetch.
70+
fetch?: typeof fetch;
71+
};
72+
73+
const DEFAULT_BASE_URL = 'https://crawlproof.com';
74+
75+
export class CrawlproofSocialError extends Error {
76+
status: number;
77+
body: unknown;
78+
constructor(status: number, message: string, body: unknown) {
79+
super(message);
80+
this.name = 'CrawlproofSocialError';
81+
this.status = status;
82+
this.body = body;
83+
}
84+
}
85+
86+
export function createCrawlproofSocialClient(
87+
options: ClientOptions,
88+
): CrawlproofSocialClient {
89+
if (!options.token) {
90+
throw new Error('crawlproof-social: token is required');
91+
}
92+
const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, '');
93+
const fetchImpl = options.fetch ?? globalThis.fetch;
94+
if (typeof fetchImpl !== 'function') {
95+
throw new Error('crawlproof-social: no fetch implementation available');
96+
}
97+
const headers = {
98+
authorization: `Bearer ${options.token}`,
99+
'content-type': 'application/json',
100+
};
101+
102+
async function call<T>(path: string, init: RequestInit = {}): Promise<T> {
103+
const res = await fetchImpl(`${baseUrl}${path}`, {
104+
...init,
105+
headers: { ...headers, ...(init.headers ?? {}) },
106+
});
107+
let body: unknown = null;
108+
try {
109+
body = await res.json();
110+
} catch {
111+
// non-JSON response; fall through with body = null
112+
}
113+
if (!res.ok) {
114+
const message =
115+
body && typeof body === 'object' && 'error' in body
116+
? String((body as { error: unknown }).error)
117+
: `${res.status} ${res.statusText}`;
118+
throw new CrawlproofSocialError(res.status, message, body);
119+
}
120+
return body as T;
121+
}
122+
123+
return {
124+
async listAccounts(): Promise<SocialAccount[]> {
125+
const r = await call<{ accounts: SocialAccount[] }>('/api/sp/v1/accounts', {
126+
method: 'GET',
127+
});
128+
return r.accounts;
129+
},
130+
async post(input: PostInput): Promise<PostResult> {
131+
const r = await call<{
132+
post_id: string;
133+
platform_post_id: string;
134+
web_url: string;
135+
}>('/api/sp/v1/posts', {
136+
method: 'POST',
137+
body: JSON.stringify({
138+
account_id: input.accountId,
139+
text: input.text,
140+
subreddit: input.subreddit,
141+
title: input.title,
142+
}),
143+
});
144+
return {
145+
postId: r.post_id,
146+
platformPostId: r.platform_post_id,
147+
webUrl: r.web_url,
148+
};
149+
},
150+
};
151+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"extends": "../../tsconfig.base.json",
3+
"compilerOptions": { "outDir": "dist", "rootDir": "src" },
4+
"include": ["src/**/*"]
5+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
{
2+
"name": "@profullstack/sh1pt-social-crawlproof",
3+
"version": "0.1.0",
4+
"description": "sh1pt social adapter that posts via accounts a user has connected on crawlproof.com (instead of asking for per-platform local credentials).",
5+
"type": "module",
6+
"main": "./src/index.ts",
7+
"scripts": {
8+
"build": "tsc -p tsconfig.json",
9+
"typecheck": "tsc -p tsconfig.json --noEmit",
10+
"prepublishOnly": "pnpm build"
11+
},
12+
"dependencies": {
13+
"@profullstack/sh1pt-core": "workspace:*",
14+
"@profullstack/crawlproof-social": "workspace:*"
15+
},
16+
"license": "MIT",
17+
"repository": {
18+
"type": "git",
19+
"url": "git+https://github.com/profullstack/sh1pt.git",
20+
"directory": "packages/social/crawlproof"
21+
},
22+
"homepage": "https://sh1pt.com",
23+
"bugs": "https://github.com/profullstack/sh1pt/issues",
24+
"files": [
25+
"dist"
26+
]
27+
}

0 commit comments

Comments
 (0)