@takosjp/yurucommu-core 3.0.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/LICENSE +16 -0
- package/README.md +82 -0
- package/migrations/0001_init.sql +495 -0
- package/migrations/0002_social_remote_actor_edges.sql +92 -0
- package/migrations/0003_activity_remote_object_edges.sql +68 -0
- package/migrations/0004_blocklist.sql +26 -0
- package/migrations/0005_story_community_scope.sql +13 -0
- package/migrations/0006_dm_community_read_status.sql +19 -0
- package/migrations/0007_moderation_reports.sql +22 -0
- package/migrations/0008_actor_fields_aka.sql +18 -0
- package/migrations/0009_object_tags.sql +13 -0
- package/migrations/0010_object_recipients_drop_actor_fk.sql +34 -0
- package/migrations/0011_drop_remote_actor_fks.sql +205 -0
- package/migrations/0012_objects_content_fts.sql +39 -0
- package/migrations/0013_efficiency_indexes.sql +13 -0
- package/migrations/0014_inbox_actor_created_idx.sql +15 -0
- package/migrations/0015_community_bans.sql +16 -0
- package/migrations/0016_namespace_takos_oidc_subject.sql +19 -0
- package/migrations/0017_mobile_push_registrations.sql +22 -0
- package/migrations/README.md +122 -0
- package/package.json +75 -0
- package/packages/api/LICENSE +16 -0
- package/packages/api/package.json +30 -0
- package/packages/api/src/index.ts +4 -0
- package/packages/api/src/lib/api/account.ts +20 -0
- package/packages/api/src/lib/api/actors.ts +149 -0
- package/packages/api/src/lib/api/auth.ts +46 -0
- package/packages/api/src/lib/api/communities.ts +329 -0
- package/packages/api/src/lib/api/dm.test.ts +67 -0
- package/packages/api/src/lib/api/dm.ts +236 -0
- package/packages/api/src/lib/api/fetch.ts +111 -0
- package/packages/api/src/lib/api/follow.ts +30 -0
- package/packages/api/src/lib/api/media.ts +100 -0
- package/packages/api/src/lib/api/moderation.ts +98 -0
- package/packages/api/src/lib/api/normalize.ts +71 -0
- package/packages/api/src/lib/api/notifications.test.ts +63 -0
- package/packages/api/src/lib/api/notifications.ts +61 -0
- package/packages/api/src/lib/api/posts.test.ts +110 -0
- package/packages/api/src/lib/api/posts.ts +181 -0
- package/packages/api/src/lib/api/recommendations.ts +22 -0
- package/packages/api/src/lib/api/search.ts +88 -0
- package/packages/api/src/lib/api/stories.ts +80 -0
- package/packages/api/src/lib/api.ts +15 -0
- package/packages/api/src/lib/fetch-with-timeout.ts +42 -0
- package/packages/api/src/lib/transport.ts +40 -0
- package/packages/api/src/social-server.ts +47 -0
- package/packages/api/src/types/index.ts +185 -0
- package/scripts/apply-takosumi-migrations.ts +621 -0
- package/src/backend/federation-helpers.ts +36 -0
- package/src/backend/index.ts +872 -0
- package/src/backend/lib/account-migration.ts +106 -0
- package/src/backend/lib/activitypub-actor-cache.ts +238 -0
- package/src/backend/lib/activitypub-helpers.ts +131 -0
- package/src/backend/lib/activitypub-validators.ts +323 -0
- package/src/backend/lib/ap-context.ts +16 -0
- package/src/backend/lib/ap-ids.ts +101 -0
- package/src/backend/lib/ap-response.ts +30 -0
- package/src/backend/lib/ap-signing.ts +87 -0
- package/src/backend/lib/ap-verify.ts +670 -0
- package/src/backend/lib/auth-lockout.ts +230 -0
- package/src/backend/lib/backend-paths.ts +34 -0
- package/src/backend/lib/base64.ts +30 -0
- package/src/backend/lib/blocklist-purge.ts +109 -0
- package/src/backend/lib/blocklist.ts +279 -0
- package/src/backend/lib/chunk.ts +33 -0
- package/src/backend/lib/client-ip.ts +169 -0
- package/src/backend/lib/community-visibility.ts +230 -0
- package/src/backend/lib/crypto.ts +424 -0
- package/src/backend/lib/delivery/circuit.ts +265 -0
- package/src/backend/lib/delivery/metrics.ts +30 -0
- package/src/backend/lib/delivery/planner.ts +190 -0
- package/src/backend/lib/delivery/queue-batching.ts +626 -0
- package/src/backend/lib/delivery/queue-delivery.ts +641 -0
- package/src/backend/lib/delivery/queue.ts +576 -0
- package/src/backend/lib/delivery/transformers.ts +56 -0
- package/src/backend/lib/delivery/types.ts +139 -0
- package/src/backend/lib/errors.ts +114 -0
- package/src/backend/lib/federation-fetch.ts +296 -0
- package/src/backend/lib/feed-cursor.ts +57 -0
- package/src/backend/lib/feed-exclude.ts +48 -0
- package/src/backend/lib/hex.ts +8 -0
- package/src/backend/lib/log-mask.ts +213 -0
- package/src/backend/lib/logger.ts +285 -0
- package/src/backend/lib/mobile-contract.ts +137 -0
- package/src/backend/lib/oauth-providers.ts +324 -0
- package/src/backend/lib/oauth-utils.ts +148 -0
- package/src/backend/lib/oidc-id-token.ts +151 -0
- package/src/backend/lib/parse-helpers.ts +31 -0
- package/src/backend/lib/post-visibility.ts +190 -0
- package/src/backend/lib/session-actor.ts +61 -0
- package/src/backend/lib/ssrf.ts +428 -0
- package/src/backend/lib/strip-image-metadata.ts +191 -0
- package/src/backend/middleware/bearer-auth.ts +70 -0
- package/src/backend/middleware/body-limit.ts +212 -0
- package/src/backend/middleware/cache.ts +429 -0
- package/src/backend/middleware/csrf.ts +130 -0
- package/src/backend/middleware/error-handler.ts +77 -0
- package/src/backend/middleware/rate-limit.ts +308 -0
- package/src/backend/public.ts +21 -0
- package/src/backend/routes/account-teardown.ts +430 -0
- package/src/backend/routes/activitypub/handlers/actor-inbox-handlers.ts +354 -0
- package/src/backend/routes/activitypub/handlers/inbound-timestamp.ts +29 -0
- package/src/backend/routes/activitypub/handlers/inbox-content-handlers.ts +1634 -0
- package/src/backend/routes/activitypub/handlers/inbox-follow-handlers.ts +547 -0
- package/src/backend/routes/activitypub/handlers/inbox-interaction-handlers.ts +497 -0
- package/src/backend/routes/activitypub/handlers/inbox-shared-helpers.ts +262 -0
- package/src/backend/routes/activitypub/handlers/user-inbox-handlers.ts +35 -0
- package/src/backend/routes/activitypub/inbox-types.ts +74 -0
- package/src/backend/routes/activitypub/inbox.ts +1191 -0
- package/src/backend/routes/activitypub/outbox.ts +0 -0
- package/src/backend/routes/activitypub/query-helpers.ts +227 -0
- package/src/backend/routes/activitypub.ts +616 -0
- package/src/backend/routes/actors-helpers.ts +487 -0
- package/src/backend/routes/actors.ts +1311 -0
- package/src/backend/routes/apps.ts +313 -0
- package/src/backend/routes/auth-helpers.ts +566 -0
- package/src/backend/routes/auth.ts +615 -0
- package/src/backend/routes/communities/membership-invites.ts +208 -0
- package/src/backend/routes/communities/membership-join.ts +335 -0
- package/src/backend/routes/communities/membership-members.ts +539 -0
- package/src/backend/routes/communities/membership-requests.ts +296 -0
- package/src/backend/routes/communities/membership-shared.ts +364 -0
- package/src/backend/routes/communities/messages.ts +479 -0
- package/src/backend/routes/communities/routes.ts +624 -0
- package/src/backend/routes/communities.ts +21 -0
- package/src/backend/routes/dm/contacts.ts +525 -0
- package/src/backend/routes/dm/conversations-helpers.ts +197 -0
- package/src/backend/routes/dm/conversations.ts +25 -0
- package/src/backend/routes/dm/messages.ts +658 -0
- package/src/backend/routes/dm/query-helpers.ts +85 -0
- package/src/backend/routes/dm/read-archive.ts +228 -0
- package/src/backend/routes/dm/requests.ts +222 -0
- package/src/backend/routes/dm/typing.ts +81 -0
- package/src/backend/routes/dm.ts +15 -0
- package/src/backend/routes/follow-helpers.ts +370 -0
- package/src/backend/routes/follow.ts +588 -0
- package/src/backend/routes/media.ts +692 -0
- package/src/backend/routes/mobile.ts +159 -0
- package/src/backend/routes/moderation.ts +373 -0
- package/src/backend/routes/notifications.ts +757 -0
- package/src/backend/routes/posts/delete-cascade.ts +330 -0
- package/src/backend/routes/posts/interactions.ts +795 -0
- package/src/backend/routes/posts/post-helpers.ts +847 -0
- package/src/backend/routes/posts/queries.ts +537 -0
- package/src/backend/routes/posts/routes.ts +865 -0
- package/src/backend/routes/posts/transformers.ts +161 -0
- package/src/backend/routes/posts.ts +17 -0
- package/src/backend/routes/recommendations.ts +88 -0
- package/src/backend/routes/search.ts +730 -0
- package/src/backend/routes/stories/interactions.ts +576 -0
- package/src/backend/routes/stories/query-helpers.ts +482 -0
- package/src/backend/routes/stories/routes.ts +906 -0
- package/src/backend/routes/stories.ts +13 -0
- package/src/backend/routes/takos-tools/dm.ts +249 -0
- package/src/backend/routes/takos-tools/follows.ts +225 -0
- package/src/backend/routes/takos-tools/posts.ts +292 -0
- package/src/backend/routes/takos-tools/search.ts +228 -0
- package/src/backend/routes/takos-tools/timeline.ts +132 -0
- package/src/backend/routes/takos-tools/types.ts +10 -0
- package/src/backend/routes/takos-tools-response.ts +178 -0
- package/src/backend/routes/takos-tools.ts +153 -0
- package/src/backend/routes/timeline.ts +755 -0
- package/src/backend/runtime/bun.ts +620 -0
- package/src/backend/runtime/cloudflare.ts +202 -0
- package/src/backend/runtime/compat-bun/types.ts +44 -0
- package/src/backend/runtime/memory-kv.ts +104 -0
- package/src/backend/runtime/shared.ts +142 -0
- package/src/backend/runtime/types.ts +205 -0
- package/src/backend/server.ts +636 -0
- package/src/backend/types.ts +143 -0
- package/src/db/index.ts +97 -0
- package/src/db/schema/actors.ts +129 -0
- package/src/db/schema/communities.ts +133 -0
- package/src/db/schema/date-utils.ts +17 -0
- package/src/db/schema/index.ts +17 -0
- package/src/db/schema/messaging.ts +241 -0
- package/src/db/schema/mobile.ts +37 -0
- package/src/db/schema/posts.ts +150 -0
- package/src/db/schema/relations.ts +266 -0
- package/src/db/schema/reports.ts +33 -0
- package/src/db/schema/social.ts +106 -0
- package/src/db/schema/stories.ts +70 -0
- package/src/db/schema.ts +15 -0
- package/src/plugin/public.ts +7 -0
- package/src/runtime/site-worker.ts +10 -0
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cloudflare Workers Runtime Adapters
|
|
3
|
+
*
|
|
4
|
+
* These adapters wrap Cloudflare-specific APIs to conform to the runtime interfaces.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type {
|
|
8
|
+
D1Database,
|
|
9
|
+
Fetcher,
|
|
10
|
+
KVNamespace,
|
|
11
|
+
R2Bucket,
|
|
12
|
+
R2Object,
|
|
13
|
+
} from "@cloudflare/workers-types";
|
|
14
|
+
import { getDb } from "../../db/index.ts";
|
|
15
|
+
import type {
|
|
16
|
+
IKeyValueStore,
|
|
17
|
+
IObjectStorage,
|
|
18
|
+
IStaticAssets,
|
|
19
|
+
ListObjectsResult,
|
|
20
|
+
ObjectMetadata,
|
|
21
|
+
StorageObject,
|
|
22
|
+
} from "./types.ts";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Cloudflare R2 Storage Adapter
|
|
26
|
+
*/
|
|
27
|
+
class CloudflareStorage implements IObjectStorage {
|
|
28
|
+
constructor(private bucket: R2Bucket) {}
|
|
29
|
+
|
|
30
|
+
async put(
|
|
31
|
+
key: string,
|
|
32
|
+
value: ReadableStream | ArrayBuffer | string,
|
|
33
|
+
options?: {
|
|
34
|
+
httpMetadata?: ObjectMetadata["httpMetadata"];
|
|
35
|
+
customMetadata?: Record<string, string>;
|
|
36
|
+
},
|
|
37
|
+
): Promise<void> {
|
|
38
|
+
await this.bucket.put(key, value as Parameters<R2Bucket["put"]>[1], {
|
|
39
|
+
httpMetadata: options?.httpMetadata,
|
|
40
|
+
customMetadata: options?.customMetadata,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async get(key: string): Promise<StorageObject | null> {
|
|
45
|
+
const obj = await this.bucket.get(key);
|
|
46
|
+
if (!obj) return null;
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
key,
|
|
50
|
+
body: obj.body as unknown as ReadableStream,
|
|
51
|
+
bodyUsed: obj.bodyUsed,
|
|
52
|
+
httpEtag: obj.httpEtag,
|
|
53
|
+
arrayBuffer: () => obj.arrayBuffer(),
|
|
54
|
+
text: () => obj.text(),
|
|
55
|
+
json: <T>() => obj.json<T>(),
|
|
56
|
+
httpMetadata: obj.httpMetadata,
|
|
57
|
+
customMetadata: obj.customMetadata,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async delete(key: string | string[]): Promise<void> {
|
|
62
|
+
await this.bucket.delete(key);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async list(options?: {
|
|
66
|
+
prefix?: string;
|
|
67
|
+
limit?: number;
|
|
68
|
+
cursor?: string;
|
|
69
|
+
delimiter?: string;
|
|
70
|
+
}): Promise<ListObjectsResult> {
|
|
71
|
+
const result = await this.bucket.list(options);
|
|
72
|
+
return {
|
|
73
|
+
objects: result.objects.map((obj: R2Object) => ({
|
|
74
|
+
key: obj.key,
|
|
75
|
+
size: obj.size,
|
|
76
|
+
uploaded: obj.uploaded,
|
|
77
|
+
etag: obj.etag,
|
|
78
|
+
httpMetadata: obj.httpMetadata,
|
|
79
|
+
})),
|
|
80
|
+
truncated: result.truncated,
|
|
81
|
+
cursor: result.truncated ? result.cursor : undefined,
|
|
82
|
+
delimitedPrefixes: result.delimitedPrefixes,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async head(key: string): Promise<ObjectMetadata | null> {
|
|
87
|
+
const obj = await this.bucket.head(key);
|
|
88
|
+
if (!obj) return null;
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
contentType: obj.httpMetadata?.contentType,
|
|
92
|
+
contentLength: obj.size,
|
|
93
|
+
etag: obj.etag,
|
|
94
|
+
httpMetadata: obj.httpMetadata,
|
|
95
|
+
customMetadata: obj.customMetadata,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Cloudflare KV Adapter
|
|
102
|
+
*/
|
|
103
|
+
class CloudflareKV implements IKeyValueStore {
|
|
104
|
+
constructor(private kv: KVNamespace) {}
|
|
105
|
+
|
|
106
|
+
get(key: string, options?: { type?: "text" }): Promise<string | null>;
|
|
107
|
+
get<T = unknown>(key: string, options: { type: "json" }): Promise<T | null>;
|
|
108
|
+
get(
|
|
109
|
+
key: string,
|
|
110
|
+
options: { type: "arrayBuffer" },
|
|
111
|
+
): Promise<ArrayBuffer | null>;
|
|
112
|
+
async get(
|
|
113
|
+
key: string,
|
|
114
|
+
options?: { type?: "text" | "json" | "arrayBuffer" },
|
|
115
|
+
): Promise<string | ArrayBuffer | unknown | null> {
|
|
116
|
+
const type = options?.type ?? "text";
|
|
117
|
+
if (type === "json") return this.kv.get(key, { type: "json" });
|
|
118
|
+
if (type === "arrayBuffer") {
|
|
119
|
+
return this.kv.get(key, { type: "arrayBuffer" });
|
|
120
|
+
}
|
|
121
|
+
return this.kv.get(key, { type: "text" });
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async put(
|
|
125
|
+
key: string,
|
|
126
|
+
value: string | ArrayBuffer | ReadableStream,
|
|
127
|
+
options?: {
|
|
128
|
+
expirationTtl?: number;
|
|
129
|
+
expiration?: number;
|
|
130
|
+
metadata?: Record<string, unknown>;
|
|
131
|
+
},
|
|
132
|
+
): Promise<void> {
|
|
133
|
+
await this.kv.put(key, value as Parameters<KVNamespace["put"]>[1], {
|
|
134
|
+
expirationTtl: options?.expirationTtl,
|
|
135
|
+
expiration: options?.expiration,
|
|
136
|
+
metadata: options?.metadata,
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async delete(key: string): Promise<void> {
|
|
141
|
+
await this.kv.delete(key);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async list(options?: {
|
|
145
|
+
prefix?: string;
|
|
146
|
+
limit?: number;
|
|
147
|
+
cursor?: string;
|
|
148
|
+
}): Promise<{
|
|
149
|
+
keys: Array<{ name: string; expiration?: number; metadata?: unknown }>;
|
|
150
|
+
list_complete: boolean;
|
|
151
|
+
cursor?: string;
|
|
152
|
+
}> {
|
|
153
|
+
const result = await this.kv.list(options);
|
|
154
|
+
return {
|
|
155
|
+
keys: result.keys,
|
|
156
|
+
list_complete: result.list_complete,
|
|
157
|
+
cursor: result.list_complete ? undefined : result.cursor,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Cloudflare Static Assets Adapter
|
|
164
|
+
*/
|
|
165
|
+
class CloudflareAssets implements IStaticAssets {
|
|
166
|
+
constructor(private assets: Fetcher) {}
|
|
167
|
+
|
|
168
|
+
fetch(request: Request): Promise<Response> {
|
|
169
|
+
return this.assets.fetch(request as never) as unknown as Promise<Response>;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Wrap a native Cloudflare Workers binding env into the app's runtime
|
|
175
|
+
* `Env` shape. The Hono app and all helper functions speak the runtime
|
|
176
|
+
* `I*` contracts, so every binding flows through this adapter before
|
|
177
|
+
* reaching app code. Pre-creates the drizzle wrapper as `DB_INSTANCE`.
|
|
178
|
+
*/
|
|
179
|
+
export function wrapCloudflareBindings<
|
|
180
|
+
T extends {
|
|
181
|
+
DB: D1Database;
|
|
182
|
+
MEDIA?: R2Bucket;
|
|
183
|
+
KV: KVNamespace;
|
|
184
|
+
ASSETS?: Fetcher;
|
|
185
|
+
},
|
|
186
|
+
>(
|
|
187
|
+
bindings: T,
|
|
188
|
+
): Omit<T, "DB" | "MEDIA" | "KV" | "ASSETS"> & {
|
|
189
|
+
DB_INSTANCE: ReturnType<typeof getDb>;
|
|
190
|
+
MEDIA?: IObjectStorage;
|
|
191
|
+
KV: IKeyValueStore;
|
|
192
|
+
ASSETS?: IStaticAssets;
|
|
193
|
+
} {
|
|
194
|
+
const { DB, MEDIA, KV, ASSETS, ...rest } = bindings;
|
|
195
|
+
return {
|
|
196
|
+
...rest,
|
|
197
|
+
DB_INSTANCE: getDb(DB),
|
|
198
|
+
MEDIA: MEDIA ? new CloudflareStorage(MEDIA) : undefined,
|
|
199
|
+
KV: new CloudflareKV(KV),
|
|
200
|
+
ASSETS: ASSETS ? new CloudflareAssets(ASSETS) : undefined,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export type BunSQLiteRunResult = {
|
|
2
|
+
changes: number;
|
|
3
|
+
lastInsertRowid: number;
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
export interface BunSQLiteStatement {
|
|
7
|
+
get(...values: unknown[]): unknown;
|
|
8
|
+
all(...values: unknown[]): unknown[];
|
|
9
|
+
run(...values: unknown[]): BunSQLiteRunResult;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface BunSQLiteDatabase {
|
|
13
|
+
exec(query: string): void;
|
|
14
|
+
prepare(query: string): BunSQLiteStatement;
|
|
15
|
+
transaction(callback: () => void): () => void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface BunSQLiteDatabaseConstructor {
|
|
19
|
+
new (filename: string): BunSQLiteDatabase;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type BunFile = BodyInit & {
|
|
23
|
+
readonly size: number;
|
|
24
|
+
exists(): Promise<boolean>;
|
|
25
|
+
arrayBuffer(): Promise<ArrayBuffer>;
|
|
26
|
+
text(): Promise<string>;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
type BunWriteData =
|
|
30
|
+
Blob | string | ArrayBufferLike | ArrayBufferView<ArrayBufferLike>;
|
|
31
|
+
|
|
32
|
+
export interface BunRuntime {
|
|
33
|
+
file(path: string): BunFile;
|
|
34
|
+
write(path: string, data: BunWriteData): Promise<number>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function loadBunSqlite(
|
|
38
|
+
runtimeRequire: (specifier: string) => unknown,
|
|
39
|
+
): BunSQLiteDatabaseConstructor {
|
|
40
|
+
const sqliteModule = runtimeRequire("bun:sqlite") as {
|
|
41
|
+
Database: BunSQLiteDatabaseConstructor;
|
|
42
|
+
};
|
|
43
|
+
return sqliteModule.Database;
|
|
44
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import type { IKeyValueStore } from "./types.ts";
|
|
2
|
+
import {
|
|
3
|
+
DEFAULT_LIST_LIMIT,
|
|
4
|
+
nowSeconds,
|
|
5
|
+
paginateList,
|
|
6
|
+
readStream,
|
|
7
|
+
resolveExpiration,
|
|
8
|
+
} from "./shared.ts";
|
|
9
|
+
|
|
10
|
+
export class MemoryKV implements IKeyValueStore {
|
|
11
|
+
private store = new Map<
|
|
12
|
+
string,
|
|
13
|
+
{ value: string; expiration?: number; metadata?: Record<string, unknown> }
|
|
14
|
+
>();
|
|
15
|
+
|
|
16
|
+
get(key: string, options?: { type?: "text" }): Promise<string | null>;
|
|
17
|
+
get<T = unknown>(key: string, options: { type: "json" }): Promise<T | null>;
|
|
18
|
+
get(
|
|
19
|
+
key: string,
|
|
20
|
+
options: { type: "arrayBuffer" },
|
|
21
|
+
): Promise<ArrayBuffer | null>;
|
|
22
|
+
async get(
|
|
23
|
+
key: string,
|
|
24
|
+
options?: { type?: "text" | "json" | "arrayBuffer" },
|
|
25
|
+
): Promise<string | ArrayBuffer | unknown | null> {
|
|
26
|
+
const entry = this.store.get(key);
|
|
27
|
+
if (!entry) return null;
|
|
28
|
+
|
|
29
|
+
if (entry.expiration && entry.expiration < nowSeconds()) {
|
|
30
|
+
this.store.delete(key);
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const type = options?.type ?? "text";
|
|
35
|
+
if (type === "json") return JSON.parse(entry.value);
|
|
36
|
+
if (type === "arrayBuffer") {
|
|
37
|
+
return new TextEncoder().encode(entry.value).buffer;
|
|
38
|
+
}
|
|
39
|
+
return entry.value;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async put(
|
|
43
|
+
key: string,
|
|
44
|
+
value: string | ArrayBuffer | ReadableStream,
|
|
45
|
+
options?: {
|
|
46
|
+
expirationTtl?: number;
|
|
47
|
+
expiration?: number;
|
|
48
|
+
metadata?: Record<string, unknown>;
|
|
49
|
+
},
|
|
50
|
+
): Promise<void> {
|
|
51
|
+
let strValue: string;
|
|
52
|
+
if (typeof value === "string") {
|
|
53
|
+
strValue = value;
|
|
54
|
+
} else if (value instanceof ArrayBuffer) {
|
|
55
|
+
strValue = new TextDecoder().decode(value);
|
|
56
|
+
} else {
|
|
57
|
+
strValue = new TextDecoder().decode(await readStream(value));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
this.store.set(key, {
|
|
61
|
+
value: strValue,
|
|
62
|
+
expiration: resolveExpiration(options),
|
|
63
|
+
metadata: options?.metadata,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async delete(key: string): Promise<void> {
|
|
68
|
+
this.store.delete(key);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async list(options?: {
|
|
72
|
+
prefix?: string;
|
|
73
|
+
limit?: number;
|
|
74
|
+
cursor?: string;
|
|
75
|
+
}): Promise<{
|
|
76
|
+
keys: Array<{ name: string; expiration?: number; metadata?: unknown }>;
|
|
77
|
+
list_complete: boolean;
|
|
78
|
+
cursor?: string;
|
|
79
|
+
}> {
|
|
80
|
+
const keys: Array<{
|
|
81
|
+
name: string;
|
|
82
|
+
expiration?: number;
|
|
83
|
+
metadata?: unknown;
|
|
84
|
+
}> = [];
|
|
85
|
+
const now = nowSeconds();
|
|
86
|
+
|
|
87
|
+
for (const [name, entry] of this.store.entries()) {
|
|
88
|
+
if (entry.expiration && entry.expiration < now) continue;
|
|
89
|
+
if (options?.prefix && !name.startsWith(options.prefix)) continue;
|
|
90
|
+
|
|
91
|
+
keys.push({
|
|
92
|
+
name,
|
|
93
|
+
expiration: entry.expiration,
|
|
94
|
+
metadata: entry.metadata,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const { items, complete, cursor } = paginateList(
|
|
99
|
+
keys,
|
|
100
|
+
options?.limit ?? DEFAULT_LIST_LIMIT,
|
|
101
|
+
);
|
|
102
|
+
return { keys: items, list_complete: complete, cursor };
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
|
|
3
|
+
export const DEFAULT_LIST_LIMIT = 1000;
|
|
4
|
+
|
|
5
|
+
const FALLBACK_MIME = "application/octet-stream";
|
|
6
|
+
|
|
7
|
+
const MIME_TYPES: Record<string, string> = {
|
|
8
|
+
".html": "text/html",
|
|
9
|
+
".css": "text/css",
|
|
10
|
+
".js": "application/javascript",
|
|
11
|
+
".json": "application/json",
|
|
12
|
+
".png": "image/png",
|
|
13
|
+
".jpg": "image/jpeg",
|
|
14
|
+
".jpeg": "image/jpeg",
|
|
15
|
+
".gif": "image/gif",
|
|
16
|
+
".svg": "image/svg+xml",
|
|
17
|
+
".webp": "image/webp",
|
|
18
|
+
".ico": "image/x-icon",
|
|
19
|
+
".woff": "font/woff",
|
|
20
|
+
".woff2": "font/woff2",
|
|
21
|
+
".ttf": "font/ttf",
|
|
22
|
+
".mp4": "video/mp4",
|
|
23
|
+
".webm": "video/webm",
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export function getMimeType(ext: string): string {
|
|
27
|
+
return MIME_TYPES[ext] || FALLBACK_MIME;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function nowSeconds(): number {
|
|
31
|
+
return Date.now() / 1000;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function hasNulByte(value: string): boolean {
|
|
35
|
+
return value.includes("\0");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function isPathWithinBasePath(
|
|
39
|
+
basePath: string,
|
|
40
|
+
candidatePath: string,
|
|
41
|
+
): boolean {
|
|
42
|
+
const relative = path.relative(basePath, candidatePath);
|
|
43
|
+
return (
|
|
44
|
+
relative === "" ||
|
|
45
|
+
(!relative.startsWith("..") && !path.isAbsolute(relative))
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function resolvePathWithinBasePath(
|
|
50
|
+
basePath: string,
|
|
51
|
+
key: string,
|
|
52
|
+
): string {
|
|
53
|
+
if (hasNulByte(key)) {
|
|
54
|
+
throw new Error("Invalid path");
|
|
55
|
+
}
|
|
56
|
+
const resolvedPath = path.resolve(basePath, key);
|
|
57
|
+
if (!isPathWithinBasePath(basePath, resolvedPath)) {
|
|
58
|
+
throw new Error("Path escapes base directory");
|
|
59
|
+
}
|
|
60
|
+
return resolvedPath;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function isNotFoundError(error: unknown): boolean {
|
|
64
|
+
return (
|
|
65
|
+
typeof error === "object" &&
|
|
66
|
+
error !== null &&
|
|
67
|
+
"code" in error &&
|
|
68
|
+
(error as { code?: unknown }).code === "ENOENT"
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export async function assertPathChainWithinBasePath(
|
|
73
|
+
basePath: string,
|
|
74
|
+
targetPath: string,
|
|
75
|
+
realpath: (path: string) => Promise<string>,
|
|
76
|
+
): Promise<void> {
|
|
77
|
+
let currentPath = targetPath;
|
|
78
|
+
|
|
79
|
+
while (true) {
|
|
80
|
+
try {
|
|
81
|
+
const realCurrentPath = await realpath(currentPath);
|
|
82
|
+
if (!isPathWithinBasePath(basePath, realCurrentPath)) {
|
|
83
|
+
throw new Error("Path escapes base directory");
|
|
84
|
+
}
|
|
85
|
+
return;
|
|
86
|
+
} catch (error) {
|
|
87
|
+
if (!isNotFoundError(error)) {
|
|
88
|
+
throw error;
|
|
89
|
+
}
|
|
90
|
+
const parentPath = path.dirname(currentPath);
|
|
91
|
+
if (parentPath === currentPath) {
|
|
92
|
+
throw error;
|
|
93
|
+
}
|
|
94
|
+
currentPath = parentPath;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export async function readStream(
|
|
100
|
+
stream: ReadableStream<Uint8Array>,
|
|
101
|
+
): Promise<Uint8Array> {
|
|
102
|
+
const reader = stream.getReader();
|
|
103
|
+
const chunks: Uint8Array[] = [];
|
|
104
|
+
|
|
105
|
+
while (true) {
|
|
106
|
+
const { done, value } = await reader.read();
|
|
107
|
+
if (done) break;
|
|
108
|
+
chunks.push(value);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
|
|
112
|
+
const combined = new Uint8Array(totalLength);
|
|
113
|
+
let offset = 0;
|
|
114
|
+
for (const chunk of chunks) {
|
|
115
|
+
combined.set(chunk, offset);
|
|
116
|
+
offset += chunk.length;
|
|
117
|
+
}
|
|
118
|
+
return combined;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function resolveExpiration(options?: {
|
|
122
|
+
expiration?: number;
|
|
123
|
+
expirationTtl?: number;
|
|
124
|
+
}): number | undefined {
|
|
125
|
+
if (options?.expiration) return options.expiration;
|
|
126
|
+
if (options?.expirationTtl) {
|
|
127
|
+
return Math.floor(nowSeconds()) + options.expirationTtl;
|
|
128
|
+
}
|
|
129
|
+
return undefined;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function paginateList<T>(
|
|
133
|
+
items: T[],
|
|
134
|
+
limit: number,
|
|
135
|
+
): { items: T[]; complete: boolean; cursor?: string } {
|
|
136
|
+
const complete = items.length <= limit;
|
|
137
|
+
return {
|
|
138
|
+
items: items.slice(0, limit),
|
|
139
|
+
complete,
|
|
140
|
+
cursor: complete ? undefined : String(limit),
|
|
141
|
+
};
|
|
142
|
+
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime Abstraction Types
|
|
3
|
+
*
|
|
4
|
+
* These interfaces abstract away Cloudflare Workers-specific APIs
|
|
5
|
+
* to allow the application to run on different runtimes.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Shared metadata for query/run results
|
|
10
|
+
*/
|
|
11
|
+
export interface ResultMeta {
|
|
12
|
+
changes?: number;
|
|
13
|
+
last_row_id?: number;
|
|
14
|
+
duration?: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Database query result
|
|
19
|
+
*/
|
|
20
|
+
export interface QueryResult<T = unknown> {
|
|
21
|
+
results: T[];
|
|
22
|
+
success: boolean;
|
|
23
|
+
meta?: ResultMeta;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Single row result
|
|
28
|
+
*/
|
|
29
|
+
export type FirstResult<T> = T | null;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Run result (for INSERT/UPDATE/DELETE)
|
|
33
|
+
*/
|
|
34
|
+
export interface RunResult {
|
|
35
|
+
success: boolean;
|
|
36
|
+
meta?: Pick<ResultMeta, "changes" | "last_row_id">;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Prepared statement interface
|
|
41
|
+
*/
|
|
42
|
+
export interface PreparedStatement {
|
|
43
|
+
bind(...values: unknown[]): PreparedStatement;
|
|
44
|
+
first<T = unknown>(colName?: string): Promise<FirstResult<T>>;
|
|
45
|
+
all<T = unknown>(): Promise<QueryResult<T>>;
|
|
46
|
+
run(): Promise<RunResult>;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Database interface - abstracts D1Database
|
|
51
|
+
*/
|
|
52
|
+
export interface IDatabase {
|
|
53
|
+
prepare(query: string): PreparedStatement;
|
|
54
|
+
exec(query: string): Promise<void>;
|
|
55
|
+
batch<T = unknown>(
|
|
56
|
+
statements: PreparedStatement[],
|
|
57
|
+
): Promise<QueryResult<T>[]>;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Object storage metadata
|
|
62
|
+
*/
|
|
63
|
+
export interface ObjectMetadata {
|
|
64
|
+
contentType?: string;
|
|
65
|
+
contentLength?: number;
|
|
66
|
+
etag?: string;
|
|
67
|
+
httpMetadata?: {
|
|
68
|
+
contentType?: string;
|
|
69
|
+
cacheControl?: string;
|
|
70
|
+
contentDisposition?: string;
|
|
71
|
+
contentEncoding?: string;
|
|
72
|
+
contentLanguage?: string;
|
|
73
|
+
};
|
|
74
|
+
customMetadata?: Record<string, string>;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Storage object interface
|
|
79
|
+
*/
|
|
80
|
+
export interface StorageObject {
|
|
81
|
+
key: string;
|
|
82
|
+
body: ReadableStream | null;
|
|
83
|
+
bodyUsed: boolean;
|
|
84
|
+
/**
|
|
85
|
+
* HTTP `ETag` value to send to clients. Optional because not every
|
|
86
|
+
* backend (e.g. the in-memory test stub) tracks an etag.
|
|
87
|
+
*/
|
|
88
|
+
httpEtag?: string;
|
|
89
|
+
arrayBuffer(): Promise<ArrayBuffer>;
|
|
90
|
+
text(): Promise<string>;
|
|
91
|
+
json<T = unknown>(): Promise<T>;
|
|
92
|
+
httpMetadata?: ObjectMetadata["httpMetadata"];
|
|
93
|
+
customMetadata?: Record<string, string>;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* List objects result
|
|
98
|
+
*/
|
|
99
|
+
export interface ListObjectsResult {
|
|
100
|
+
objects: Array<{
|
|
101
|
+
key: string;
|
|
102
|
+
size: number;
|
|
103
|
+
uploaded: Date;
|
|
104
|
+
etag?: string;
|
|
105
|
+
httpMetadata?: ObjectMetadata["httpMetadata"];
|
|
106
|
+
}>;
|
|
107
|
+
truncated: boolean;
|
|
108
|
+
cursor?: string;
|
|
109
|
+
delimitedPrefixes?: string[];
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Object storage interface - abstracts R2Bucket
|
|
114
|
+
*/
|
|
115
|
+
export interface IObjectStorage {
|
|
116
|
+
put(
|
|
117
|
+
key: string,
|
|
118
|
+
value: ReadableStream | ArrayBuffer | string,
|
|
119
|
+
options?: {
|
|
120
|
+
httpMetadata?: ObjectMetadata["httpMetadata"];
|
|
121
|
+
customMetadata?: Record<string, string>;
|
|
122
|
+
},
|
|
123
|
+
): Promise<void>;
|
|
124
|
+
|
|
125
|
+
get(key: string): Promise<StorageObject | null>;
|
|
126
|
+
|
|
127
|
+
delete(key: string | string[]): Promise<void>;
|
|
128
|
+
|
|
129
|
+
list(options?: {
|
|
130
|
+
prefix?: string;
|
|
131
|
+
limit?: number;
|
|
132
|
+
cursor?: string;
|
|
133
|
+
delimiter?: string;
|
|
134
|
+
}): Promise<ListObjectsResult>;
|
|
135
|
+
|
|
136
|
+
head(key: string): Promise<ObjectMetadata | null>;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Key-value store interface - abstracts KVNamespace
|
|
141
|
+
*/
|
|
142
|
+
export interface IKeyValueStore {
|
|
143
|
+
get(key: string, options?: { type?: "text" }): Promise<string | null>;
|
|
144
|
+
get<T = unknown>(key: string, options: { type: "json" }): Promise<T | null>;
|
|
145
|
+
get(
|
|
146
|
+
key: string,
|
|
147
|
+
options: { type: "arrayBuffer" },
|
|
148
|
+
): Promise<ArrayBuffer | null>;
|
|
149
|
+
|
|
150
|
+
put(
|
|
151
|
+
key: string,
|
|
152
|
+
value: string | ArrayBuffer | ReadableStream,
|
|
153
|
+
options?: {
|
|
154
|
+
expirationTtl?: number;
|
|
155
|
+
expiration?: number;
|
|
156
|
+
metadata?: Record<string, unknown>;
|
|
157
|
+
},
|
|
158
|
+
): Promise<void>;
|
|
159
|
+
|
|
160
|
+
delete(key: string): Promise<void>;
|
|
161
|
+
|
|
162
|
+
list(options?: {
|
|
163
|
+
prefix?: string;
|
|
164
|
+
limit?: number;
|
|
165
|
+
cursor?: string;
|
|
166
|
+
}): Promise<{
|
|
167
|
+
keys: Array<{ name: string; expiration?: number; metadata?: unknown }>;
|
|
168
|
+
list_complete: boolean;
|
|
169
|
+
cursor?: string;
|
|
170
|
+
}>;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Static assets fetcher interface
|
|
175
|
+
*/
|
|
176
|
+
export interface IStaticAssets {
|
|
177
|
+
fetch(request: Request): Promise<Response>;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Runtime environment - for non-Cloudflare runtimes
|
|
182
|
+
*/
|
|
183
|
+
export interface RuntimeEnv {
|
|
184
|
+
db: IDatabase;
|
|
185
|
+
storage?: IObjectStorage;
|
|
186
|
+
kv?: IKeyValueStore;
|
|
187
|
+
assets?: IStaticAssets;
|
|
188
|
+
|
|
189
|
+
// Environment variables
|
|
190
|
+
APP_URL: string;
|
|
191
|
+
AUTH_PASSWORD_HASH?: string;
|
|
192
|
+
GOOGLE_CLIENT_ID?: string;
|
|
193
|
+
GOOGLE_CLIENT_SECRET?: string;
|
|
194
|
+
X_CLIENT_ID?: string;
|
|
195
|
+
X_CLIENT_SECRET?: string;
|
|
196
|
+
OIDC_ISSUER_URL?: string;
|
|
197
|
+
OIDC_CLIENT_ID?: string;
|
|
198
|
+
OIDC_CLIENT_SECRET?: string;
|
|
199
|
+
OAUTH_ISSUER_URL?: string;
|
|
200
|
+
TAKOSUMI_ACCOUNTS_ISSUER_URL?: string;
|
|
201
|
+
TAKOSUMI_ACCOUNTS_CLIENT_ID?: string;
|
|
202
|
+
TAKOSUMI_ACCOUNTS_CLIENT_SECRET?: string;
|
|
203
|
+
TAKOS_URL?: string;
|
|
204
|
+
AUTH_MODE?: string;
|
|
205
|
+
}
|