@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.
Files changed (185) hide show
  1. package/LICENSE +16 -0
  2. package/README.md +82 -0
  3. package/migrations/0001_init.sql +495 -0
  4. package/migrations/0002_social_remote_actor_edges.sql +92 -0
  5. package/migrations/0003_activity_remote_object_edges.sql +68 -0
  6. package/migrations/0004_blocklist.sql +26 -0
  7. package/migrations/0005_story_community_scope.sql +13 -0
  8. package/migrations/0006_dm_community_read_status.sql +19 -0
  9. package/migrations/0007_moderation_reports.sql +22 -0
  10. package/migrations/0008_actor_fields_aka.sql +18 -0
  11. package/migrations/0009_object_tags.sql +13 -0
  12. package/migrations/0010_object_recipients_drop_actor_fk.sql +34 -0
  13. package/migrations/0011_drop_remote_actor_fks.sql +205 -0
  14. package/migrations/0012_objects_content_fts.sql +39 -0
  15. package/migrations/0013_efficiency_indexes.sql +13 -0
  16. package/migrations/0014_inbox_actor_created_idx.sql +15 -0
  17. package/migrations/0015_community_bans.sql +16 -0
  18. package/migrations/0016_namespace_takos_oidc_subject.sql +19 -0
  19. package/migrations/0017_mobile_push_registrations.sql +22 -0
  20. package/migrations/README.md +122 -0
  21. package/package.json +75 -0
  22. package/packages/api/LICENSE +16 -0
  23. package/packages/api/package.json +30 -0
  24. package/packages/api/src/index.ts +4 -0
  25. package/packages/api/src/lib/api/account.ts +20 -0
  26. package/packages/api/src/lib/api/actors.ts +149 -0
  27. package/packages/api/src/lib/api/auth.ts +46 -0
  28. package/packages/api/src/lib/api/communities.ts +329 -0
  29. package/packages/api/src/lib/api/dm.test.ts +67 -0
  30. package/packages/api/src/lib/api/dm.ts +236 -0
  31. package/packages/api/src/lib/api/fetch.ts +111 -0
  32. package/packages/api/src/lib/api/follow.ts +30 -0
  33. package/packages/api/src/lib/api/media.ts +100 -0
  34. package/packages/api/src/lib/api/moderation.ts +98 -0
  35. package/packages/api/src/lib/api/normalize.ts +71 -0
  36. package/packages/api/src/lib/api/notifications.test.ts +63 -0
  37. package/packages/api/src/lib/api/notifications.ts +61 -0
  38. package/packages/api/src/lib/api/posts.test.ts +110 -0
  39. package/packages/api/src/lib/api/posts.ts +181 -0
  40. package/packages/api/src/lib/api/recommendations.ts +22 -0
  41. package/packages/api/src/lib/api/search.ts +88 -0
  42. package/packages/api/src/lib/api/stories.ts +80 -0
  43. package/packages/api/src/lib/api.ts +15 -0
  44. package/packages/api/src/lib/fetch-with-timeout.ts +42 -0
  45. package/packages/api/src/lib/transport.ts +40 -0
  46. package/packages/api/src/social-server.ts +47 -0
  47. package/packages/api/src/types/index.ts +185 -0
  48. package/scripts/apply-takosumi-migrations.ts +621 -0
  49. package/src/backend/federation-helpers.ts +36 -0
  50. package/src/backend/index.ts +872 -0
  51. package/src/backend/lib/account-migration.ts +106 -0
  52. package/src/backend/lib/activitypub-actor-cache.ts +238 -0
  53. package/src/backend/lib/activitypub-helpers.ts +131 -0
  54. package/src/backend/lib/activitypub-validators.ts +323 -0
  55. package/src/backend/lib/ap-context.ts +16 -0
  56. package/src/backend/lib/ap-ids.ts +101 -0
  57. package/src/backend/lib/ap-response.ts +30 -0
  58. package/src/backend/lib/ap-signing.ts +87 -0
  59. package/src/backend/lib/ap-verify.ts +670 -0
  60. package/src/backend/lib/auth-lockout.ts +230 -0
  61. package/src/backend/lib/backend-paths.ts +34 -0
  62. package/src/backend/lib/base64.ts +30 -0
  63. package/src/backend/lib/blocklist-purge.ts +109 -0
  64. package/src/backend/lib/blocklist.ts +279 -0
  65. package/src/backend/lib/chunk.ts +33 -0
  66. package/src/backend/lib/client-ip.ts +169 -0
  67. package/src/backend/lib/community-visibility.ts +230 -0
  68. package/src/backend/lib/crypto.ts +424 -0
  69. package/src/backend/lib/delivery/circuit.ts +265 -0
  70. package/src/backend/lib/delivery/metrics.ts +30 -0
  71. package/src/backend/lib/delivery/planner.ts +190 -0
  72. package/src/backend/lib/delivery/queue-batching.ts +626 -0
  73. package/src/backend/lib/delivery/queue-delivery.ts +641 -0
  74. package/src/backend/lib/delivery/queue.ts +576 -0
  75. package/src/backend/lib/delivery/transformers.ts +56 -0
  76. package/src/backend/lib/delivery/types.ts +139 -0
  77. package/src/backend/lib/errors.ts +114 -0
  78. package/src/backend/lib/federation-fetch.ts +296 -0
  79. package/src/backend/lib/feed-cursor.ts +57 -0
  80. package/src/backend/lib/feed-exclude.ts +48 -0
  81. package/src/backend/lib/hex.ts +8 -0
  82. package/src/backend/lib/log-mask.ts +213 -0
  83. package/src/backend/lib/logger.ts +285 -0
  84. package/src/backend/lib/mobile-contract.ts +137 -0
  85. package/src/backend/lib/oauth-providers.ts +324 -0
  86. package/src/backend/lib/oauth-utils.ts +148 -0
  87. package/src/backend/lib/oidc-id-token.ts +151 -0
  88. package/src/backend/lib/parse-helpers.ts +31 -0
  89. package/src/backend/lib/post-visibility.ts +190 -0
  90. package/src/backend/lib/session-actor.ts +61 -0
  91. package/src/backend/lib/ssrf.ts +428 -0
  92. package/src/backend/lib/strip-image-metadata.ts +191 -0
  93. package/src/backend/middleware/bearer-auth.ts +70 -0
  94. package/src/backend/middleware/body-limit.ts +212 -0
  95. package/src/backend/middleware/cache.ts +429 -0
  96. package/src/backend/middleware/csrf.ts +130 -0
  97. package/src/backend/middleware/error-handler.ts +77 -0
  98. package/src/backend/middleware/rate-limit.ts +308 -0
  99. package/src/backend/public.ts +21 -0
  100. package/src/backend/routes/account-teardown.ts +430 -0
  101. package/src/backend/routes/activitypub/handlers/actor-inbox-handlers.ts +354 -0
  102. package/src/backend/routes/activitypub/handlers/inbound-timestamp.ts +29 -0
  103. package/src/backend/routes/activitypub/handlers/inbox-content-handlers.ts +1634 -0
  104. package/src/backend/routes/activitypub/handlers/inbox-follow-handlers.ts +547 -0
  105. package/src/backend/routes/activitypub/handlers/inbox-interaction-handlers.ts +497 -0
  106. package/src/backend/routes/activitypub/handlers/inbox-shared-helpers.ts +262 -0
  107. package/src/backend/routes/activitypub/handlers/user-inbox-handlers.ts +35 -0
  108. package/src/backend/routes/activitypub/inbox-types.ts +74 -0
  109. package/src/backend/routes/activitypub/inbox.ts +1191 -0
  110. package/src/backend/routes/activitypub/outbox.ts +0 -0
  111. package/src/backend/routes/activitypub/query-helpers.ts +227 -0
  112. package/src/backend/routes/activitypub.ts +616 -0
  113. package/src/backend/routes/actors-helpers.ts +487 -0
  114. package/src/backend/routes/actors.ts +1311 -0
  115. package/src/backend/routes/apps.ts +313 -0
  116. package/src/backend/routes/auth-helpers.ts +566 -0
  117. package/src/backend/routes/auth.ts +615 -0
  118. package/src/backend/routes/communities/membership-invites.ts +208 -0
  119. package/src/backend/routes/communities/membership-join.ts +335 -0
  120. package/src/backend/routes/communities/membership-members.ts +539 -0
  121. package/src/backend/routes/communities/membership-requests.ts +296 -0
  122. package/src/backend/routes/communities/membership-shared.ts +364 -0
  123. package/src/backend/routes/communities/messages.ts +479 -0
  124. package/src/backend/routes/communities/routes.ts +624 -0
  125. package/src/backend/routes/communities.ts +21 -0
  126. package/src/backend/routes/dm/contacts.ts +525 -0
  127. package/src/backend/routes/dm/conversations-helpers.ts +197 -0
  128. package/src/backend/routes/dm/conversations.ts +25 -0
  129. package/src/backend/routes/dm/messages.ts +658 -0
  130. package/src/backend/routes/dm/query-helpers.ts +85 -0
  131. package/src/backend/routes/dm/read-archive.ts +228 -0
  132. package/src/backend/routes/dm/requests.ts +222 -0
  133. package/src/backend/routes/dm/typing.ts +81 -0
  134. package/src/backend/routes/dm.ts +15 -0
  135. package/src/backend/routes/follow-helpers.ts +370 -0
  136. package/src/backend/routes/follow.ts +588 -0
  137. package/src/backend/routes/media.ts +692 -0
  138. package/src/backend/routes/mobile.ts +159 -0
  139. package/src/backend/routes/moderation.ts +373 -0
  140. package/src/backend/routes/notifications.ts +757 -0
  141. package/src/backend/routes/posts/delete-cascade.ts +330 -0
  142. package/src/backend/routes/posts/interactions.ts +795 -0
  143. package/src/backend/routes/posts/post-helpers.ts +847 -0
  144. package/src/backend/routes/posts/queries.ts +537 -0
  145. package/src/backend/routes/posts/routes.ts +865 -0
  146. package/src/backend/routes/posts/transformers.ts +161 -0
  147. package/src/backend/routes/posts.ts +17 -0
  148. package/src/backend/routes/recommendations.ts +88 -0
  149. package/src/backend/routes/search.ts +730 -0
  150. package/src/backend/routes/stories/interactions.ts +576 -0
  151. package/src/backend/routes/stories/query-helpers.ts +482 -0
  152. package/src/backend/routes/stories/routes.ts +906 -0
  153. package/src/backend/routes/stories.ts +13 -0
  154. package/src/backend/routes/takos-tools/dm.ts +249 -0
  155. package/src/backend/routes/takos-tools/follows.ts +225 -0
  156. package/src/backend/routes/takos-tools/posts.ts +292 -0
  157. package/src/backend/routes/takos-tools/search.ts +228 -0
  158. package/src/backend/routes/takos-tools/timeline.ts +132 -0
  159. package/src/backend/routes/takos-tools/types.ts +10 -0
  160. package/src/backend/routes/takos-tools-response.ts +178 -0
  161. package/src/backend/routes/takos-tools.ts +153 -0
  162. package/src/backend/routes/timeline.ts +755 -0
  163. package/src/backend/runtime/bun.ts +620 -0
  164. package/src/backend/runtime/cloudflare.ts +202 -0
  165. package/src/backend/runtime/compat-bun/types.ts +44 -0
  166. package/src/backend/runtime/memory-kv.ts +104 -0
  167. package/src/backend/runtime/shared.ts +142 -0
  168. package/src/backend/runtime/types.ts +205 -0
  169. package/src/backend/server.ts +636 -0
  170. package/src/backend/types.ts +143 -0
  171. package/src/db/index.ts +97 -0
  172. package/src/db/schema/actors.ts +129 -0
  173. package/src/db/schema/communities.ts +133 -0
  174. package/src/db/schema/date-utils.ts +17 -0
  175. package/src/db/schema/index.ts +17 -0
  176. package/src/db/schema/messaging.ts +241 -0
  177. package/src/db/schema/mobile.ts +37 -0
  178. package/src/db/schema/posts.ts +150 -0
  179. package/src/db/schema/relations.ts +266 -0
  180. package/src/db/schema/reports.ts +33 -0
  181. package/src/db/schema/social.ts +106 -0
  182. package/src/db/schema/stories.ts +70 -0
  183. package/src/db/schema.ts +15 -0
  184. package/src/plugin/public.ts +7 -0
  185. 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
+ }