birdclaw 0.5.1 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +44 -1
- package/README.md +50 -5
- package/package.json +3 -2
- package/scripts/browser-perf.mjs +1 -0
- package/scripts/start-test-server.mjs +16 -3
- package/src/cli.ts +376 -13
- package/src/components/AccountSwitcher.tsx +186 -0
- package/src/components/AppNav.tsx +27 -7
- package/src/components/AvatarChip.tsx +9 -3
- package/src/components/DmWorkspace.tsx +18 -8
- package/src/components/LinkPreviewCard.tsx +40 -18
- package/src/components/MarkdownViewer.tsx +452 -0
- package/src/components/SyncNowButton.tsx +57 -25
- package/src/components/ThemeSlider.tsx +55 -50
- package/src/components/TimelineCard.tsx +225 -93
- package/src/components/TimelineRouteFrame.tsx +22 -8
- package/src/components/TweetMediaGrid.tsx +87 -38
- package/src/components/TweetRichText.tsx +15 -11
- package/src/components/account-selection.ts +64 -0
- package/src/components/useTimelineRouteData.ts +23 -7
- package/src/lib/account-sync-job.ts +654 -0
- package/src/lib/actions-transport.ts +216 -146
- package/src/lib/api-client.ts +128 -53
- package/src/lib/archive-finder.ts +78 -63
- package/src/lib/archive-import.ts +1364 -1300
- package/src/lib/authored-live.ts +261 -204
- package/src/lib/avatar-cache.ts +159 -44
- package/src/lib/backup.ts +1532 -951
- package/src/lib/bird-actions.ts +139 -57
- package/src/lib/bird-command.ts +101 -28
- package/src/lib/bird.ts +549 -194
- package/src/lib/blocklist.ts +40 -23
- package/src/lib/blocks-write.ts +129 -80
- package/src/lib/blocks.ts +165 -97
- package/src/lib/bookmark-sync-job.ts +250 -160
- package/src/lib/conversation-surface.ts +79 -48
- package/src/lib/db.ts +33 -3
- package/src/lib/dms-live.ts +720 -66
- package/src/lib/effect-runtime.ts +45 -0
- package/src/lib/follow-graph.ts +224 -180
- package/src/lib/http-effect.ts +222 -0
- package/src/lib/inbox.ts +74 -43
- package/src/lib/link-index.ts +88 -76
- package/src/lib/link-insights.ts +24 -0
- package/src/lib/link-preview-metadata.ts +472 -52
- package/src/lib/media-fetch.ts +286 -213
- package/src/lib/mention-threads-live.ts +352 -288
- package/src/lib/mentions-live.ts +390 -342
- package/src/lib/moderation-target.ts +102 -65
- package/src/lib/moderation-write.ts +77 -18
- package/src/lib/mutes-write.ts +129 -80
- package/src/lib/mutes.ts +8 -1
- package/src/lib/openai.ts +84 -53
- package/src/lib/period-digest.ts +953 -0
- package/src/lib/profile-affiliation-hydration.ts +93 -54
- package/src/lib/profile-hydration.ts +124 -72
- package/src/lib/profile-replies.ts +60 -43
- package/src/lib/profile-resolver.ts +402 -294
- package/src/lib/queries.ts +969 -199
- package/src/lib/research.ts +165 -120
- package/src/lib/sqlite.ts +1 -0
- package/src/lib/timeline-collections-live.ts +204 -167
- package/src/lib/timeline-live.ts +60 -39
- package/src/lib/tweet-lookup.ts +30 -19
- package/src/lib/types.ts +38 -1
- package/src/lib/ui.ts +30 -7
- package/src/lib/url-expansion.ts +226 -55
- package/src/lib/url-safety.ts +220 -0
- package/src/lib/web-sync.ts +216 -148
- package/src/lib/whois.ts +166 -147
- package/src/lib/x-web.ts +102 -71
- package/src/lib/xurl.ts +681 -411
- package/src/routeTree.gen.ts +42 -0
- package/src/routes/__root.tsx +25 -5
- package/src/routes/api/action.tsx +127 -78
- package/src/routes/api/avatar.tsx +39 -30
- package/src/routes/api/blocks.tsx +26 -23
- package/src/routes/api/conversation.tsx +25 -14
- package/src/routes/api/inbox.tsx +27 -21
- package/src/routes/api/link-insights.tsx +31 -25
- package/src/routes/api/link-preview.tsx +25 -21
- package/src/routes/api/period-digest.tsx +123 -0
- package/src/routes/api/profile-hydrate.tsx +31 -28
- package/src/routes/api/query.tsx +79 -55
- package/src/routes/api/status.tsx +18 -10
- package/src/routes/api/sync.tsx +75 -29
- package/src/routes/dms.tsx +95 -28
- package/src/routes/inbox.tsx +32 -19
- package/src/routes/today.tsx +441 -0
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
const PRIVATE_IPV4_RANGES = [
|
|
2
|
+
["0.0.0.0", 8],
|
|
3
|
+
["10.0.0.0", 8],
|
|
4
|
+
["100.64.0.0", 10],
|
|
5
|
+
["127.0.0.0", 8],
|
|
6
|
+
["169.254.0.0", 16],
|
|
7
|
+
["172.16.0.0", 12],
|
|
8
|
+
["192.0.0.0", 24],
|
|
9
|
+
["192.0.2.0", 24],
|
|
10
|
+
["192.168.0.0", 16],
|
|
11
|
+
["198.18.0.0", 15],
|
|
12
|
+
["198.51.100.0", 24],
|
|
13
|
+
["203.0.113.0", 24],
|
|
14
|
+
["224.0.0.0", 4],
|
|
15
|
+
["240.0.0.0", 4],
|
|
16
|
+
] satisfies Array<[string, number]>;
|
|
17
|
+
|
|
18
|
+
function ipv4ToNumber(value: string) {
|
|
19
|
+
const parts = value.split(".").map((part) => Number(part));
|
|
20
|
+
if (parts.length !== 4 || parts.some((part) => !Number.isInteger(part))) {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
if (parts.some((part) => part < 0 || part > 255)) return null;
|
|
24
|
+
return (
|
|
25
|
+
(parts[0] ?? 0) * 256 ** 3 +
|
|
26
|
+
(parts[1] ?? 0) * 256 ** 2 +
|
|
27
|
+
(parts[2] ?? 0) * 256 +
|
|
28
|
+
(parts[3] ?? 0)
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function isIpv4InRange(address: string, range: string, prefix: number) {
|
|
33
|
+
const addressNumber = ipv4ToNumber(address);
|
|
34
|
+
const rangeNumber = ipv4ToNumber(range);
|
|
35
|
+
if (addressNumber === null || rangeNumber === null) return false;
|
|
36
|
+
const mask = prefix === 0 ? 0 : (0xffffffff << (32 - prefix)) >>> 0;
|
|
37
|
+
return (addressNumber & mask) === (rangeNumber & mask);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function isPrivateIpv4(address: string) {
|
|
41
|
+
return PRIVATE_IPV4_RANGES.some(([range, prefix]) =>
|
|
42
|
+
isIpv4InRange(address, range, prefix),
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function normalizeIpv6(address: string) {
|
|
47
|
+
return address.toLowerCase().replace(/^\[|\]$/g, "");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function parseIpv6Parts(address: string) {
|
|
51
|
+
let normalized = normalizeIpv6(address);
|
|
52
|
+
if (!normalized.includes(":")) return null;
|
|
53
|
+
|
|
54
|
+
if (normalized.includes(".")) {
|
|
55
|
+
const lastColon = normalized.lastIndexOf(":");
|
|
56
|
+
const ipv4 = normalized.slice(lastColon + 1);
|
|
57
|
+
const addressNumber = ipv4ToNumber(ipv4);
|
|
58
|
+
if (addressNumber === null) return null;
|
|
59
|
+
normalized = `${normalized.slice(0, lastColon + 1)}${(
|
|
60
|
+
(addressNumber >>> 16) &
|
|
61
|
+
0xffff
|
|
62
|
+
).toString(16)}:${(addressNumber & 0xffff).toString(16)}`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const halves = normalized.split("::");
|
|
66
|
+
if (halves.length > 2) return null;
|
|
67
|
+
const parseGroups = (value: string) =>
|
|
68
|
+
value === ""
|
|
69
|
+
? []
|
|
70
|
+
: value.split(":").map((part) => Number.parseInt(part, 16));
|
|
71
|
+
const left = parseGroups(halves[0] ?? "");
|
|
72
|
+
const right = halves.length === 2 ? parseGroups(halves[1] ?? "") : [];
|
|
73
|
+
const missingGroups = 8 - left.length - right.length;
|
|
74
|
+
if (
|
|
75
|
+
(halves.length === 1 && missingGroups !== 0) ||
|
|
76
|
+
(halves.length === 2 && missingGroups < 0) ||
|
|
77
|
+
![...left, ...right].every(
|
|
78
|
+
(part) => Number.isInteger(part) && part >= 0 && part <= 0xffff,
|
|
79
|
+
)
|
|
80
|
+
) {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
return [...left, ...Array.from({ length: missingGroups }, () => 0), ...right];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function ipv4FromHexPair(parts: string[]) {
|
|
87
|
+
if (parts.length !== 2) return null;
|
|
88
|
+
const high = Number.parseInt(parts[0] ?? "", 16);
|
|
89
|
+
const low = Number.parseInt(parts[1] ?? "", 16);
|
|
90
|
+
if (
|
|
91
|
+
![high, low].every(
|
|
92
|
+
(part) => Number.isInteger(part) && part >= 0 && part <= 0xffff,
|
|
93
|
+
)
|
|
94
|
+
) {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
return [(high >> 8) & 255, high & 255, (low >> 8) & 255, low & 255].join(".");
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function ipv4FromIpv6Parts(parts: number[]) {
|
|
101
|
+
const tailIpv4 = () =>
|
|
102
|
+
[
|
|
103
|
+
(parts[6] >> 8) & 255,
|
|
104
|
+
parts[6] & 255,
|
|
105
|
+
(parts[7] >> 8) & 255,
|
|
106
|
+
parts[7] & 255,
|
|
107
|
+
].join(".");
|
|
108
|
+
const hasZeroPrefix = (length: number) =>
|
|
109
|
+
parts.slice(0, length).every((part) => part === 0);
|
|
110
|
+
if (hasZeroPrefix(5) && parts[5] === 0xffff) return tailIpv4();
|
|
111
|
+
if (hasZeroPrefix(4) && parts[4] === 0xffff && parts[5] === 0) {
|
|
112
|
+
return tailIpv4();
|
|
113
|
+
}
|
|
114
|
+
if (hasZeroPrefix(6)) return tailIpv4();
|
|
115
|
+
if (parts[0] === 0x64 && parts[1] === 0xff9b && parts[2] === 0) {
|
|
116
|
+
return tailIpv4();
|
|
117
|
+
}
|
|
118
|
+
if (parts[0] === 0x64 && parts[1] === 0xff9b && parts[2] === 1) {
|
|
119
|
+
return tailIpv4();
|
|
120
|
+
}
|
|
121
|
+
if (parts[0] === 0x2002) {
|
|
122
|
+
return [
|
|
123
|
+
(parts[1] >> 8) & 255,
|
|
124
|
+
parts[1] & 255,
|
|
125
|
+
(parts[2] >> 8) & 255,
|
|
126
|
+
parts[2] & 255,
|
|
127
|
+
].join(".");
|
|
128
|
+
}
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function ipv4FromIpv6Suffix(address: string) {
|
|
133
|
+
const normalized = normalizeIpv6(address);
|
|
134
|
+
const parts = parseIpv6Parts(normalized);
|
|
135
|
+
if (parts) return ipv4FromIpv6Parts(parts);
|
|
136
|
+
const prefix = ["::ffff:", "64:ff9b::", "64:ff9b:1::", "::"].find((value) =>
|
|
137
|
+
normalized.startsWith(value),
|
|
138
|
+
);
|
|
139
|
+
if (!prefix) return null;
|
|
140
|
+
const suffix = normalized.slice(prefix.length);
|
|
141
|
+
if (ipv4ToNumber(suffix) !== null) return suffix;
|
|
142
|
+
return ipv4FromHexPair(suffix.split(":"));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function isPrivateIpv6(address: string) {
|
|
146
|
+
const normalized = normalizeIpv6(address);
|
|
147
|
+
const parts = parseIpv6Parts(normalized);
|
|
148
|
+
const mappedIpv4 = parts
|
|
149
|
+
? ipv4FromIpv6Parts(parts)
|
|
150
|
+
: ipv4FromIpv6Suffix(normalized);
|
|
151
|
+
if (mappedIpv4) return isPrivateIpv4(mappedIpv4);
|
|
152
|
+
if (parts) {
|
|
153
|
+
const first = parts[0] ?? 0;
|
|
154
|
+
return (
|
|
155
|
+
parts.every((part) => part === 0) ||
|
|
156
|
+
parts.slice(0, 7).every((part) => part === 0) ||
|
|
157
|
+
(first & 0xfe00) === 0xfc00 ||
|
|
158
|
+
(first & 0xffc0) === 0xfe80 ||
|
|
159
|
+
(first & 0xffc0) === 0xfec0 ||
|
|
160
|
+
(first & 0xff00) === 0xff00
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function isBlockedAddress(address: string) {
|
|
167
|
+
const normalized = address.replace(/^\[|\]$/g, "");
|
|
168
|
+
if (ipv4ToNumber(normalized) !== null) return isPrivateIpv4(normalized);
|
|
169
|
+
if (normalized.includes(":")) return isPrivateIpv6(normalized);
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function isLocalHostname(hostname: string) {
|
|
174
|
+
const normalized = hostname.toLowerCase().replace(/\.$/, "");
|
|
175
|
+
return (
|
|
176
|
+
normalized === "localhost" ||
|
|
177
|
+
normalized.endsWith(".localhost") ||
|
|
178
|
+
normalized.endsWith(".local") ||
|
|
179
|
+
normalized.endsWith(".internal") ||
|
|
180
|
+
normalized.endsWith(".test")
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export function assertSafePreviewUrl(url: string) {
|
|
185
|
+
const parsed = new URL(url);
|
|
186
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
187
|
+
throw new Error("Link preview URL must use http or https");
|
|
188
|
+
}
|
|
189
|
+
if (parsed.username || parsed.password) {
|
|
190
|
+
throw new Error("Link preview URL must not include credentials");
|
|
191
|
+
}
|
|
192
|
+
if (isLocalHostname(parsed.hostname) || isBlockedAddress(parsed.hostname)) {
|
|
193
|
+
throw new Error("Link preview URL points to a private host");
|
|
194
|
+
}
|
|
195
|
+
return parsed;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export function safeHttpUrl(value: string | null | undefined) {
|
|
199
|
+
if (!value) return null;
|
|
200
|
+
try {
|
|
201
|
+
const parsed = new URL(value);
|
|
202
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
return parsed.toString();
|
|
206
|
+
} catch {
|
|
207
|
+
return null;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export const __test__ = {
|
|
212
|
+
assertSafePreviewUrl,
|
|
213
|
+
ipv4FromIpv6Suffix,
|
|
214
|
+
ipv4ToNumber,
|
|
215
|
+
isBlockedAddress,
|
|
216
|
+
isIpv4InRange,
|
|
217
|
+
isPrivateIpv6,
|
|
218
|
+
parseIpv6Parts,
|
|
219
|
+
safeHttpUrl,
|
|
220
|
+
};
|