@nkzw/codiff-service 1.9.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 +21 -0
- package/api.test.ts +44 -0
- package/api.ts +707 -0
- package/db.ts +45 -0
- package/dist/api.mjs +403 -0
- package/dist/db.mjs +26 -0
- package/dist/fate.mjs +700 -0
- package/dist/index.mjs +6 -0
- package/dist/react.mjs +62 -0
- package/dist/schema.mjs +219 -0
- package/dist/upload-intent.mjs +48 -0
- package/dist/views.mjs +142 -0
- package/fate.test.ts +21 -0
- package/fate.ts +1258 -0
- package/index.ts +5 -0
- package/package.json +79 -0
- package/react.test.ts +70 -0
- package/react.tsx +151 -0
- package/schema.ts +320 -0
- package/upload-intent.test.ts +24 -0
- package/upload-intent.ts +92 -0
- package/views.ts +269 -0
- package/vite.config.ts +8 -0
package/db.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
2
|
+
import { and, eq, gte, sql } from 'drizzle-orm';
|
|
3
|
+
import { drizzle, type DrizzleD1Database } from 'drizzle-orm/d1';
|
|
4
|
+
import schema from './schema.ts';
|
|
5
|
+
|
|
6
|
+
export type Database = DrizzleD1Database<typeof schema>;
|
|
7
|
+
export type D1Binding = Parameters<typeof drizzle>[0];
|
|
8
|
+
export type DatabaseBatchStatement = Parameters<Database['batch']>[0][number];
|
|
9
|
+
|
|
10
|
+
const databaseBatchSize = 500;
|
|
11
|
+
const store = new AsyncLocalStorage<Database>();
|
|
12
|
+
|
|
13
|
+
export const createDb = (binding: D1Binding): Database => drizzle(binding, { schema });
|
|
14
|
+
|
|
15
|
+
export const withDatabase = <Value>(binding: D1Binding, callback: () => Value): Value =>
|
|
16
|
+
store.run(createDb(binding), callback);
|
|
17
|
+
|
|
18
|
+
export const runDatabaseBatches = async (
|
|
19
|
+
database: Database,
|
|
20
|
+
statements: ReadonlyArray<DatabaseBatchStatement>,
|
|
21
|
+
) => {
|
|
22
|
+
for (let index = 0; index < statements.length; index += databaseBatchSize) {
|
|
23
|
+
const chunk = statements.slice(index, index + databaseBatchSize);
|
|
24
|
+
const first = chunk[0];
|
|
25
|
+
if (first) {
|
|
26
|
+
await database.batch([first, ...chunk.slice(1)]);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const getDb = (): Database => {
|
|
32
|
+
const database = store.getStore();
|
|
33
|
+
if (!database) {
|
|
34
|
+
throw new Error('Codiff sharing database is unavailable.');
|
|
35
|
+
}
|
|
36
|
+
return database;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export const db = new Proxy({} as Database, {
|
|
40
|
+
get(_target, property, receiver) {
|
|
41
|
+
return Reflect.get(getDb(), property, receiver);
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
export { and, eq, gte, sql };
|
package/dist/api.mjs
ADDED
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
import { plan, shareDailyUsage, uploadIntent, user, walkthrough } from "./schema.mjs";
|
|
2
|
+
import { db, eq, sql, withDatabase } from "./db.mjs";
|
|
3
|
+
import { createUploadIntentSecret, hashUploadIntentSecret, verifyUploadIntentSecret } from "./upload-intent.mjs";
|
|
4
|
+
import { parsePlanShareUpload } from "@nkzw/codiff-core/share";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
//#region api.ts
|
|
7
|
+
const boundedString = (maximum) => z.string().max(maximum);
|
|
8
|
+
const nonEmptyString = (maximum) => z.string().min(1).max(maximum);
|
|
9
|
+
const reviewAuthorSchema = z.object({
|
|
10
|
+
avatarUrl: boundedString(2048).optional(),
|
|
11
|
+
login: nonEmptyString(200),
|
|
12
|
+
name: nonEmptyString(200).optional()
|
|
13
|
+
}).passthrough();
|
|
14
|
+
const reviewCommentSchema = z.object({
|
|
15
|
+
anchor: z.enum(["file", "line"]).optional(),
|
|
16
|
+
author: reviewAuthorSchema,
|
|
17
|
+
body: boundedString(128 * 1024),
|
|
18
|
+
filePath: nonEmptyString(4096),
|
|
19
|
+
id: nonEmptyString(256),
|
|
20
|
+
isThreadResolved: z.boolean().optional(),
|
|
21
|
+
lineNumber: z.number().int().positive().optional(),
|
|
22
|
+
sectionId: nonEmptyString(4096).optional(),
|
|
23
|
+
side: z.enum(["additions", "deletions"]).optional(),
|
|
24
|
+
startLineNumber: z.number().int().positive().optional(),
|
|
25
|
+
startSide: z.enum(["additions", "deletions"]).optional(),
|
|
26
|
+
submittedAt: boundedString(64).optional(),
|
|
27
|
+
threadId: nonEmptyString(256).optional()
|
|
28
|
+
}).passthrough().refine((comment) => comment.anchor === "file" || comment.lineNumber != null && comment.side != null && (comment.startLineNumber == null || comment.startSide != null), "Invalid walkthrough review comment.");
|
|
29
|
+
const generalCommentSchema = z.object({
|
|
30
|
+
author: reviewAuthorSchema,
|
|
31
|
+
body: boundedString(128 * 1024),
|
|
32
|
+
id: nonEmptyString(256),
|
|
33
|
+
submittedAt: boundedString(64).optional()
|
|
34
|
+
}).passthrough();
|
|
35
|
+
const generalCommentThreadSchema = z.object({
|
|
36
|
+
comments: z.array(generalCommentSchema).max(1e3),
|
|
37
|
+
id: nonEmptyString(256),
|
|
38
|
+
isResolved: z.boolean().optional()
|
|
39
|
+
}).passthrough();
|
|
40
|
+
const walkthroughFileSchema = z.object({ path: nonEmptyString(4096) }).passthrough();
|
|
41
|
+
const walkthroughSourceSchema = z.object({
|
|
42
|
+
host: boundedString(512).optional(),
|
|
43
|
+
number: z.number().int().positive().optional(),
|
|
44
|
+
projectPath: boundedString(4096).optional(),
|
|
45
|
+
title: boundedString(300).optional(),
|
|
46
|
+
type: nonEmptyString(64),
|
|
47
|
+
url: boundedString(4096).optional()
|
|
48
|
+
}).passthrough();
|
|
49
|
+
const walkthroughManifestSchema = z.object({
|
|
50
|
+
branch: boundedString(1024).optional(),
|
|
51
|
+
codiffVersion: nonEmptyString(100),
|
|
52
|
+
exportedAt: boundedString(64),
|
|
53
|
+
files: z.array(walkthroughFileSchema).max(1e5),
|
|
54
|
+
kind: z.literal("codiff-walkthrough-share"),
|
|
55
|
+
repository: z.object({
|
|
56
|
+
generalComments: z.array(generalCommentThreadSchema).max(1e3).optional(),
|
|
57
|
+
source: walkthroughSourceSchema.optional(),
|
|
58
|
+
title: boundedString(300).optional()
|
|
59
|
+
}).passthrough(),
|
|
60
|
+
reviewComments: z.array(reviewCommentSchema).max(1e4).optional(),
|
|
61
|
+
version: z.literal(1),
|
|
62
|
+
walkthrough: z.object({
|
|
63
|
+
summary: boundedString(128 * 1024).optional(),
|
|
64
|
+
title: nonEmptyString(300)
|
|
65
|
+
}).passthrough()
|
|
66
|
+
}).passthrough();
|
|
67
|
+
const uploaderSchema = z.object({
|
|
68
|
+
email: boundedString(320).optional(),
|
|
69
|
+
name: boundedString(200).optional()
|
|
70
|
+
}).passthrough();
|
|
71
|
+
const walkthroughUploadSchema = z.object({
|
|
72
|
+
snapshot: walkthroughManifestSchema,
|
|
73
|
+
uploader: uploaderSchema.optional()
|
|
74
|
+
}).passthrough();
|
|
75
|
+
const json = (value, init) => new Response(JSON.stringify(value), {
|
|
76
|
+
...init,
|
|
77
|
+
headers: {
|
|
78
|
+
"content-type": "application/json; charset=utf-8",
|
|
79
|
+
...init?.headers ?? {}
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
const notFound = () => json({ error: "not-found" }, { status: 404 });
|
|
83
|
+
const badRequest = (message) => json({ error: message }, { status: 400 });
|
|
84
|
+
const unauthorized = () => json({ error: "authentication-required" }, { status: 401 });
|
|
85
|
+
const maxUploadByteSize = 25 * 1024 * 1024;
|
|
86
|
+
const textEncoder = new TextEncoder();
|
|
87
|
+
const isRecord = (value) => value !== null && typeof value === "object";
|
|
88
|
+
const isBetterAuthApi = (value) => isRecord(value) && isRecord(value.api) && typeof value.api.getSession === "function";
|
|
89
|
+
const stringValue = (value) => typeof value === "string" && value ? value : null;
|
|
90
|
+
const shareUserFromAuthValue = (value) => {
|
|
91
|
+
const result = isRecord(value) && isRecord(value.response) ? value.response : value;
|
|
92
|
+
const userValue = isRecord(result) ? result.user : null;
|
|
93
|
+
if (!isRecord(userValue)) return null;
|
|
94
|
+
const id = stringValue(userValue.id);
|
|
95
|
+
const name = stringValue(userValue.name) ?? stringValue(userValue.username);
|
|
96
|
+
if (!id || !name) return null;
|
|
97
|
+
return {
|
|
98
|
+
id,
|
|
99
|
+
image: stringValue(userValue.image),
|
|
100
|
+
name,
|
|
101
|
+
username: stringValue(userValue.displayUsername) ?? stringValue(userValue.username)
|
|
102
|
+
};
|
|
103
|
+
};
|
|
104
|
+
const shareUserFromRow = (row) => row ? {
|
|
105
|
+
id: row.id,
|
|
106
|
+
image: row.image,
|
|
107
|
+
name: row.name,
|
|
108
|
+
username: row.displayUsername ?? row.username
|
|
109
|
+
} : null;
|
|
110
|
+
const getAuthenticatedShareUser = async (request, auth) => {
|
|
111
|
+
if (!isBetterAuthApi(auth)) return null;
|
|
112
|
+
try {
|
|
113
|
+
return shareUserFromAuthValue(await auth.api.getSession({
|
|
114
|
+
asResponse: false,
|
|
115
|
+
headers: request.headers,
|
|
116
|
+
returnHeaders: true
|
|
117
|
+
}));
|
|
118
|
+
} catch {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
const getShareUserById = async (userId) => {
|
|
123
|
+
if (!userId) return null;
|
|
124
|
+
const [record] = await db.select().from(user).where(eq(user.id, userId)).limit(1);
|
|
125
|
+
return shareUserFromRow(record);
|
|
126
|
+
};
|
|
127
|
+
const randomToken = (bytes = 24) => {
|
|
128
|
+
const value = new Uint8Array(bytes);
|
|
129
|
+
crypto.getRandomValues(value);
|
|
130
|
+
return btoa(String.fromCharCode(...value)).replaceAll("+", "-").replaceAll("/", "_").replaceAll("=", "");
|
|
131
|
+
};
|
|
132
|
+
const randomCode = () => randomToken(8).slice(0, 10).toUpperCase();
|
|
133
|
+
const publicOrigin = (request, env) => {
|
|
134
|
+
const url = new URL(request.url);
|
|
135
|
+
return (url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "[::1]" ? url.origin : env.PUBLIC_ORIGIN || url.origin).replace(/\/+$/, "");
|
|
136
|
+
};
|
|
137
|
+
const sha256 = async (value) => {
|
|
138
|
+
const digest = await crypto.subtle.digest("SHA-256", textEncoder.encode(value));
|
|
139
|
+
return [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
140
|
+
};
|
|
141
|
+
const cleanTitleValue = (value) => typeof value === "string" ? value.replaceAll(/\s+/g, " ").trim().slice(0, 300) || null : null;
|
|
142
|
+
const cleanIdentityValue = (value, maxLength) => typeof value === "string" ? value.trim().slice(0, maxLength) || null : null;
|
|
143
|
+
const sanitizeWalkthrough = (snapshot) => {
|
|
144
|
+
const repository = snapshot.repository;
|
|
145
|
+
const commitTitle = repository.source?.type === "commit" ? cleanTitleValue(repository.title) ?? cleanTitleValue(snapshot.walkthrough.title) : null;
|
|
146
|
+
const sanitizedRepository = {
|
|
147
|
+
...repository,
|
|
148
|
+
root: commitTitle ?? "Shared Codiff review",
|
|
149
|
+
...commitTitle ? { title: commitTitle } : {}
|
|
150
|
+
};
|
|
151
|
+
if (!commitTitle) delete sanitizedRepository.title;
|
|
152
|
+
return {
|
|
153
|
+
...snapshot,
|
|
154
|
+
repository: sanitizedRepository
|
|
155
|
+
};
|
|
156
|
+
};
|
|
157
|
+
const parseShareUpload = (body) => {
|
|
158
|
+
const payload = JSON.parse(body);
|
|
159
|
+
const snapshotValue = (isRecord(payload) ? payload : null)?.snapshot ?? payload;
|
|
160
|
+
if (!isRecord(snapshotValue) || snapshotValue.version !== 1) throw new Error("unsupported-manifest");
|
|
161
|
+
if (snapshotValue.kind === "codiff-walkthrough-share") {
|
|
162
|
+
const envelope = walkthroughUploadSchema.safeParse(payload);
|
|
163
|
+
const parsed = envelope.success ? envelope.data : { snapshot: walkthroughManifestSchema.parse(payload) };
|
|
164
|
+
return {
|
|
165
|
+
kind: "walkthrough",
|
|
166
|
+
sharedByEmail: cleanIdentityValue(parsed.uploader?.email, 320),
|
|
167
|
+
sharedByName: cleanIdentityValue(parsed.uploader?.name, 200),
|
|
168
|
+
snapshot: sanitizeWalkthrough(parsed.snapshot)
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
if (snapshotValue.kind !== "codiff-plan-share") throw new Error("unsupported-manifest");
|
|
172
|
+
const parsed = parsePlanShareUpload(payload);
|
|
173
|
+
return {
|
|
174
|
+
kind: "plan",
|
|
175
|
+
sharedByEmail: cleanIdentityValue(parsed.uploader?.email, 320),
|
|
176
|
+
sharedByName: cleanIdentityValue(parsed.uploader?.name, 200),
|
|
177
|
+
snapshot: parsed.snapshot
|
|
178
|
+
};
|
|
179
|
+
};
|
|
180
|
+
const parseGitPullRequestUrl = (value) => {
|
|
181
|
+
if (!value) return null;
|
|
182
|
+
try {
|
|
183
|
+
const url = new URL(value);
|
|
184
|
+
const gitLab = /^\/(.+)\/-\/merge_requests\/(\d+)(?:\/.*)?$/.exec(url.pathname);
|
|
185
|
+
if (gitLab) return {
|
|
186
|
+
host: url.host,
|
|
187
|
+
number: Number(gitLab[2]),
|
|
188
|
+
projectPath: decodeURIComponent(gitLab[1])
|
|
189
|
+
};
|
|
190
|
+
const gitHub = /^\/([^/]+)\/([^/]+)\/pull\/(\d+)(?:\/.*)?$/.exec(url.pathname);
|
|
191
|
+
if (gitHub) return {
|
|
192
|
+
host: url.host,
|
|
193
|
+
number: Number(gitHub[3]),
|
|
194
|
+
projectPath: `${decodeURIComponent(gitHub[1])}/${decodeURIComponent(gitHub[2])}`
|
|
195
|
+
};
|
|
196
|
+
} catch {
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
return null;
|
|
200
|
+
};
|
|
201
|
+
const getRepositoryMetadata = (source) => {
|
|
202
|
+
if (!source || source.type !== "pull-request") return null;
|
|
203
|
+
const parsed = parseGitPullRequestUrl(source.url);
|
|
204
|
+
const projectPath = source.projectPath || parsed?.projectPath;
|
|
205
|
+
const host = source.host || parsed?.host;
|
|
206
|
+
if (!projectPath || !host) return null;
|
|
207
|
+
const segments = projectPath.split("/");
|
|
208
|
+
return {
|
|
209
|
+
host,
|
|
210
|
+
name: segments.at(-1) ?? projectPath,
|
|
211
|
+
number: source.number ?? parsed?.number ?? null,
|
|
212
|
+
owner: segments.slice(0, -1).join("/") || null,
|
|
213
|
+
url: `https://${host}/${projectPath}`
|
|
214
|
+
};
|
|
215
|
+
};
|
|
216
|
+
const utcDateKey = (date = /* @__PURE__ */ new Date()) => date.toISOString().slice(0, 10);
|
|
217
|
+
const quotaResponse = () => {
|
|
218
|
+
const now = /* @__PURE__ */ new Date();
|
|
219
|
+
const resetAt = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate() + 1));
|
|
220
|
+
return json({
|
|
221
|
+
error: "share-quota-exceeded",
|
|
222
|
+
resetAt: resetAt.toISOString()
|
|
223
|
+
}, {
|
|
224
|
+
headers: { "retry-after": String(Math.max(1, Math.ceil((+resetAt - +now) / 1e3))) },
|
|
225
|
+
status: 429
|
|
226
|
+
});
|
|
227
|
+
};
|
|
228
|
+
const hasAvailableQuota = async (userId, kind) => {
|
|
229
|
+
const [usage] = await db.select().from(shareDailyUsage).where(sql`${shareDailyUsage.userId} = ${userId} and ${shareDailyUsage.date} = ${utcDateKey()}`).limit(1);
|
|
230
|
+
if (!usage) return true;
|
|
231
|
+
return usage.planCount + usage.walkthroughCount < 100 && (kind === "plan" ? usage.planCount < 50 : kind === "walkthrough" ? usage.walkthroughCount < 50 : true);
|
|
232
|
+
};
|
|
233
|
+
const createUploadIntent = async (request, env, options) => {
|
|
234
|
+
const code = randomCode();
|
|
235
|
+
const now = /* @__PURE__ */ new Date();
|
|
236
|
+
const expiresAt = new Date(now.getTime() + 600 * 1e3);
|
|
237
|
+
const secret = await createUploadIntentSecret(env, code, expiresAt);
|
|
238
|
+
const tokenHash = await hashUploadIntentSecret(secret);
|
|
239
|
+
const shareUser = await getAuthenticatedShareUser(request, options.auth);
|
|
240
|
+
if (shareUser && options.enforceDailyQuota && !await hasAvailableQuota(shareUser.id)) return quotaResponse();
|
|
241
|
+
if (shareUser) await db.insert(uploadIntent).values({
|
|
242
|
+
claimedAt: now,
|
|
243
|
+
code,
|
|
244
|
+
expiresAt,
|
|
245
|
+
secretHash: tokenHash,
|
|
246
|
+
sharedByUserId: shareUser.id,
|
|
247
|
+
status: "claimed",
|
|
248
|
+
updatedAt: now,
|
|
249
|
+
uploadTokenHash: tokenHash
|
|
250
|
+
});
|
|
251
|
+
const origin = publicOrigin(request, env);
|
|
252
|
+
return json({
|
|
253
|
+
claimUrl: `${origin}/connect/${code}?secret=${secret}`,
|
|
254
|
+
code,
|
|
255
|
+
pollUrl: `${origin}/api/upload-intents/${code}?secret=${secret}`,
|
|
256
|
+
secret,
|
|
257
|
+
status: shareUser ? "claimed" : "pending"
|
|
258
|
+
});
|
|
259
|
+
};
|
|
260
|
+
const readUploadIntent = async (request, env, code) => {
|
|
261
|
+
const secret = new URL(request.url).searchParams.get("secret");
|
|
262
|
+
if (!secret) return badRequest("missing-secret");
|
|
263
|
+
const [intent] = await db.select().from(uploadIntent).where(eq(uploadIntent.code, code)).limit(1);
|
|
264
|
+
const tokenHash = await hashUploadIntentSecret(secret);
|
|
265
|
+
if (intent && intent.secretHash !== tokenHash) return notFound();
|
|
266
|
+
const pending = intent ? null : await verifyUploadIntentSecret(env, code, secret);
|
|
267
|
+
if (!intent && !pending) return notFound();
|
|
268
|
+
if ((intent?.expiresAt ?? pending.expiresAt).getTime() < Date.now() && intent?.status !== "uploaded") return json({ status: "expired" }, { status: 410 });
|
|
269
|
+
if (!intent) return json({ status: "pending" });
|
|
270
|
+
return json({
|
|
271
|
+
status: intent.status,
|
|
272
|
+
uploadToken: intent.status === "claimed" ? secret : void 0,
|
|
273
|
+
url: intent.status === "uploaded" && intent.walkthroughSlug ? `${publicOrigin(request, env)}/${intent.shareKind === "plan" ? "p" : "w"}/${intent.walkthroughSlug}` : void 0
|
|
274
|
+
});
|
|
275
|
+
};
|
|
276
|
+
const isQuotaError = (error) => error instanceof Error && error.message.toLowerCase().includes("share-quota-exceeded");
|
|
277
|
+
const uploadShare = async (request, env, options) => {
|
|
278
|
+
const code = request.headers.get("x-codiff-upload-code");
|
|
279
|
+
const authorization = request.headers.get("authorization");
|
|
280
|
+
const token = authorization?.startsWith("Bearer ") ? authorization.slice(7) : null;
|
|
281
|
+
if (!code || !token) return unauthorized();
|
|
282
|
+
const [intent] = await db.select().from(uploadIntent).where(eq(uploadIntent.code, code)).limit(1);
|
|
283
|
+
if (!intent || intent.status !== "claimed" || !intent.sharedByUserId || intent.expiresAt.getTime() < Date.now() || intent.uploadTokenHash !== await sha256(token)) return unauthorized();
|
|
284
|
+
const body = await request.text();
|
|
285
|
+
if (textEncoder.encode(body).byteLength > maxUploadByteSize) return json({ error: "manifest-too-large" }, { status: 413 });
|
|
286
|
+
let upload;
|
|
287
|
+
try {
|
|
288
|
+
upload = parseShareUpload(body);
|
|
289
|
+
} catch (error) {
|
|
290
|
+
return badRequest(error instanceof Error && error.message === "unsupported-manifest" ? error.message : "invalid-manifest");
|
|
291
|
+
}
|
|
292
|
+
if (options.enforceDailyQuota && !await hasAvailableQuota(intent.sharedByUserId, upload.kind)) return quotaResponse();
|
|
293
|
+
const canonical = JSON.stringify(upload.snapshot);
|
|
294
|
+
const digest = await sha256(canonical);
|
|
295
|
+
const slug = randomToken(18);
|
|
296
|
+
const objectKey = `${upload.kind === "plan" ? "plans" : "walkthroughs"}/${slug}/manifest.json`;
|
|
297
|
+
const shareUser = await getShareUserById(intent.sharedByUserId);
|
|
298
|
+
if (!shareUser) return unauthorized();
|
|
299
|
+
const now = /* @__PURE__ */ new Date();
|
|
300
|
+
await env.WALKTHROUGH_BUCKET.put(objectKey, canonical, { httpMetadata: { contentType: "application/json; charset=utf-8" } });
|
|
301
|
+
try {
|
|
302
|
+
if (upload.kind === "walkthrough") {
|
|
303
|
+
const source = upload.snapshot.repository.source;
|
|
304
|
+
const repository = getRepositoryMetadata(source);
|
|
305
|
+
const commitTitle = source?.type === "commit" ? cleanTitleValue(upload.snapshot.repository.title) : null;
|
|
306
|
+
const walkthroughId = crypto.randomUUID();
|
|
307
|
+
await db.batch([db.insert(walkthrough).values({
|
|
308
|
+
branch: upload.snapshot.branch ?? null,
|
|
309
|
+
byteSize: textEncoder.encode(canonical).byteLength,
|
|
310
|
+
codiffVersion: upload.snapshot.codiffVersion,
|
|
311
|
+
description: upload.snapshot.walkthrough.summary ?? null,
|
|
312
|
+
id: walkthroughId,
|
|
313
|
+
objectKey,
|
|
314
|
+
pullRequestNumber: repository?.number ?? source?.number ?? null,
|
|
315
|
+
pullRequestTitle: source?.title ?? null,
|
|
316
|
+
pullRequestUrl: source?.type === "pull-request" ? source.url ?? null : null,
|
|
317
|
+
repositoryHost: repository?.host ?? null,
|
|
318
|
+
repositoryName: repository?.name ?? null,
|
|
319
|
+
repositoryOwner: repository?.owner ?? null,
|
|
320
|
+
repositoryUrl: repository?.url ?? null,
|
|
321
|
+
schemaVersion: upload.snapshot.version,
|
|
322
|
+
sha256: digest,
|
|
323
|
+
sharedByEmail: null,
|
|
324
|
+
sharedByName: null,
|
|
325
|
+
sharedByUserId: shareUser.id,
|
|
326
|
+
slug,
|
|
327
|
+
sourceType: source?.type ?? "unknown",
|
|
328
|
+
title: commitTitle ?? upload.snapshot.walkthrough.title,
|
|
329
|
+
updatedAt: now
|
|
330
|
+
}), db.update(uploadIntent).set({
|
|
331
|
+
shareKind: upload.kind,
|
|
332
|
+
status: "uploaded",
|
|
333
|
+
updatedAt: now,
|
|
334
|
+
walkthroughSlug: slug
|
|
335
|
+
}).where(eq(uploadIntent.id, intent.id))]);
|
|
336
|
+
} else await db.batch([db.insert(plan).values({
|
|
337
|
+
agent: cleanIdentityValue(upload.snapshot.source?.agent, 40),
|
|
338
|
+
byteSize: textEncoder.encode(canonical).byteLength,
|
|
339
|
+
codiffVersion: upload.snapshot.codiffVersion,
|
|
340
|
+
objectKey,
|
|
341
|
+
schemaVersion: upload.snapshot.version,
|
|
342
|
+
sessionId: cleanIdentityValue(upload.snapshot.source?.sessionId, 200),
|
|
343
|
+
sha256: digest,
|
|
344
|
+
sharedByEmail: null,
|
|
345
|
+
sharedByName: null,
|
|
346
|
+
sharedByUserId: shareUser.id,
|
|
347
|
+
slug,
|
|
348
|
+
sourceFileName: cleanTitleValue(upload.snapshot.document.name) ?? "plan.md",
|
|
349
|
+
title: cleanTitleValue(upload.snapshot.document.title) ?? "Codiff Plan",
|
|
350
|
+
updatedAt: now
|
|
351
|
+
}), db.update(uploadIntent).set({
|
|
352
|
+
shareKind: upload.kind,
|
|
353
|
+
status: "uploaded",
|
|
354
|
+
updatedAt: now,
|
|
355
|
+
walkthroughSlug: slug
|
|
356
|
+
}).where(eq(uploadIntent.id, intent.id))]);
|
|
357
|
+
} catch (error) {
|
|
358
|
+
await env.WALKTHROUGH_BUCKET.delete(objectKey);
|
|
359
|
+
if (isQuotaError(error)) return quotaResponse();
|
|
360
|
+
throw error;
|
|
361
|
+
}
|
|
362
|
+
options.onUploadIntentUpdated?.({
|
|
363
|
+
changed: upload.kind === "plan" ? [
|
|
364
|
+
"shareKind",
|
|
365
|
+
"status",
|
|
366
|
+
"walkthroughSlug"
|
|
367
|
+
] : ["status", "walkthroughSlug"],
|
|
368
|
+
id: intent.id,
|
|
369
|
+
shareKind: upload.kind,
|
|
370
|
+
walkthroughSlug: slug
|
|
371
|
+
});
|
|
372
|
+
return json({
|
|
373
|
+
slug,
|
|
374
|
+
status: "uploaded",
|
|
375
|
+
url: `${publicOrigin(request, env)}/${upload.kind === "plan" ? "p" : "w"}/${slug}`
|
|
376
|
+
});
|
|
377
|
+
};
|
|
378
|
+
const getManifest = async (env, kind, slug) => {
|
|
379
|
+
const [record] = kind === "plan" ? await db.select({ objectKey: plan.objectKey }).from(plan).where(eq(plan.slug, slug)).limit(1) : await db.select({ objectKey: walkthrough.objectKey }).from(walkthrough).where(eq(walkthrough.slug, slug)).limit(1);
|
|
380
|
+
if (!record) return notFound();
|
|
381
|
+
const object = await env.WALKTHROUGH_BUCKET.get(record.objectKey);
|
|
382
|
+
if (!object) return notFound();
|
|
383
|
+
return new Response(object.body, { headers: {
|
|
384
|
+
"cache-control": "private, no-store",
|
|
385
|
+
"content-type": "application/json; charset=utf-8",
|
|
386
|
+
"x-robots-tag": "noindex"
|
|
387
|
+
} });
|
|
388
|
+
};
|
|
389
|
+
const handleSharingApi = async (request, env, options) => {
|
|
390
|
+
const url = new URL(request.url);
|
|
391
|
+
const uploadIntentMatch = /^\/api\/upload-intents\/([^/]+)$/.exec(url.pathname);
|
|
392
|
+
const planManifestMatch = /^\/api\/plans\/([^/]+)\/manifest$/.exec(url.pathname);
|
|
393
|
+
const walkthroughManifestMatch = /^\/api\/walkthroughs\/([^/]+)\/manifest$/.exec(url.pathname);
|
|
394
|
+
if (request.method === "POST" && url.pathname === "/api/upload-intents") return createUploadIntent(request, env, options);
|
|
395
|
+
if (request.method === "GET" && uploadIntentMatch) return readUploadIntent(request, env, uploadIntentMatch[1]);
|
|
396
|
+
if (request.method === "POST" && url.pathname === "/api/uploads") return uploadShare(request, env, options);
|
|
397
|
+
if (request.method === "GET" && planManifestMatch) return getManifest(env, "plan", planManifestMatch[1]);
|
|
398
|
+
if (request.method === "GET" && walkthroughManifestMatch) return getManifest(env, "walkthrough", walkthroughManifestMatch[1]);
|
|
399
|
+
return notFound();
|
|
400
|
+
};
|
|
401
|
+
const handleSharingApiRequest = (request, env, options = {}) => withDatabase(env.DB, () => handleSharingApi(request, env, options));
|
|
402
|
+
//#endregion
|
|
403
|
+
export { getAuthenticatedShareUser, getRepositoryMetadata, handleSharingApiRequest, parseGitPullRequestUrl, parseShareUpload, shareUserFromAuthValue };
|
package/dist/db.mjs
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import schema from "./schema.mjs";
|
|
2
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
3
|
+
import { and, eq, gte, sql } from "drizzle-orm";
|
|
4
|
+
import { drizzle } from "drizzle-orm/d1";
|
|
5
|
+
//#region db.ts
|
|
6
|
+
const databaseBatchSize = 500;
|
|
7
|
+
const store = new AsyncLocalStorage();
|
|
8
|
+
const createDb = (binding) => drizzle(binding, { schema });
|
|
9
|
+
const withDatabase = (binding, callback) => store.run(createDb(binding), callback);
|
|
10
|
+
const runDatabaseBatches = async (database, statements) => {
|
|
11
|
+
for (let index = 0; index < statements.length; index += databaseBatchSize) {
|
|
12
|
+
const chunk = statements.slice(index, index + databaseBatchSize);
|
|
13
|
+
const first = chunk[0];
|
|
14
|
+
if (first) await database.batch([first, ...chunk.slice(1)]);
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
const getDb = () => {
|
|
18
|
+
const database = store.getStore();
|
|
19
|
+
if (!database) throw new Error("Codiff sharing database is unavailable.");
|
|
20
|
+
return database;
|
|
21
|
+
};
|
|
22
|
+
const db = new Proxy({}, { get(_target, property, receiver) {
|
|
23
|
+
return Reflect.get(getDb(), property, receiver);
|
|
24
|
+
} });
|
|
25
|
+
//#endregion
|
|
26
|
+
export { and, createDb, db, eq, gte, runDatabaseBatches, sql, withDatabase };
|