@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/dist/index.mjs
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { plan, planRelations, shareCommentMessage, shareCommentMessageRelations, shareCommentThread, shareCommentThreadKind, shareCommentThreadRelations, shareCommentThreadStatus, shareDailyUsage, shareDailyUsageRelations, shareKind, uploadIntent, user, userRelations, walkthrough, walkthroughFile, walkthroughFileRelations, walkthroughRelations } from "./schema.mjs";
|
|
2
|
+
import { and, createDb, db, eq, gte, runDatabaseBatches, sql, withDatabase } from "./db.mjs";
|
|
3
|
+
import { getAuthenticatedShareUser, getRepositoryMetadata, handleSharingApiRequest, parseGitPullRequestUrl, parseShareUpload, shareUserFromAuthValue } from "./api.mjs";
|
|
4
|
+
import { SharingRoot, getSessionUserId, planDataView, shareCommentMessageDataView, shareCommentThreadDataView, shareStatsDataView, shareStatsDayDataView, uploadIntentDataView, uploadIntentSecretHeader, walkthroughDataView } from "./views.mjs";
|
|
5
|
+
import { createSharingFateServer, shareCommentAnchorSchema, shareCommentTargetSchema } from "./fate.mjs";
|
|
6
|
+
export { SharingRoot, and, createDb, createSharingFateServer, db, eq, getAuthenticatedShareUser, getRepositoryMetadata, getSessionUserId, gte, handleSharingApiRequest, parseGitPullRequestUrl, parseShareUpload, plan, planDataView, planRelations, runDatabaseBatches, shareCommentAnchorSchema, shareCommentMessage, shareCommentMessageDataView, shareCommentMessageRelations, shareCommentTargetSchema, shareCommentThread, shareCommentThreadDataView, shareCommentThreadKind, shareCommentThreadRelations, shareCommentThreadStatus, shareDailyUsage, shareDailyUsageRelations, shareKind, shareStatsDataView, shareStatsDayDataView, shareUserFromAuthValue, sql, uploadIntent, uploadIntentDataView, uploadIntentSecretHeader, user, userRelations, walkthrough, walkthroughDataView, walkthroughFile, walkthroughFileRelations, walkthroughRelations, withDatabase };
|
package/dist/react.mjs
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { PlanReviewSurface, ReviewSurface } from "@nkzw/codiff-core/react";
|
|
2
|
+
import { jsx } from "react/jsx-runtime";
|
|
3
|
+
//#region react.tsx
|
|
4
|
+
const asRecord = (value) => value && typeof value === "object" ? value : null;
|
|
5
|
+
const submittedShareCommentMessage = (value) => {
|
|
6
|
+
const message = asRecord(value);
|
|
7
|
+
if (!message || typeof message.authorName !== "string" || typeof message.body !== "string" || typeof message.canEdit !== "boolean" || typeof message.createdAt !== "string" || typeof message.id !== "string" || typeof message.threadId !== "string") throw new Error("Unable to load the submitted walkthrough comment.");
|
|
8
|
+
return {
|
|
9
|
+
authorImage: typeof message.authorImage === "string" ? message.authorImage : null,
|
|
10
|
+
authorName: message.authorName,
|
|
11
|
+
authorUsername: typeof message.authorUsername === "string" ? message.authorUsername : null,
|
|
12
|
+
body: message.body,
|
|
13
|
+
canEdit: message.canEdit,
|
|
14
|
+
createdAt: message.createdAt,
|
|
15
|
+
id: message.id,
|
|
16
|
+
threadId: message.threadId
|
|
17
|
+
};
|
|
18
|
+
};
|
|
19
|
+
const toSubmittedReviewComment = (comment, message, threadId, canResolveThread) => ({
|
|
20
|
+
...comment,
|
|
21
|
+
author: {
|
|
22
|
+
...message.authorImage ? { avatarUrl: message.authorImage } : {},
|
|
23
|
+
login: message.authorUsername ?? message.authorName,
|
|
24
|
+
name: message.authorName
|
|
25
|
+
},
|
|
26
|
+
body: message.body,
|
|
27
|
+
...message.canEdit ? {
|
|
28
|
+
canDelete: true,
|
|
29
|
+
canEdit: true
|
|
30
|
+
} : {},
|
|
31
|
+
...canResolveThread ? { canResolveThread: true } : {},
|
|
32
|
+
id: message.id,
|
|
33
|
+
submittedAt: message.createdAt,
|
|
34
|
+
threadId
|
|
35
|
+
});
|
|
36
|
+
const resolveSubmittedShareReply = ({ canResolveThread, comment, result }) => toSubmittedReviewComment(comment, submittedShareCommentMessage(result), comment.threadId, canResolveThread);
|
|
37
|
+
const resolveSubmittedShareThread = ({ canResolveThread, comment, result }) => {
|
|
38
|
+
const thread = asRecord(result);
|
|
39
|
+
const messages = thread ? asRecord(thread.messages) : null;
|
|
40
|
+
const firstItem = messages && Array.isArray(messages.items) ? asRecord(messages.items[0]) : null;
|
|
41
|
+
if (!thread || typeof thread.id !== "string" || !firstItem) throw new Error("Unable to load the submitted walkthrough comment.");
|
|
42
|
+
return toSubmittedReviewComment(comment, submittedShareCommentMessage(firstItem.node), thread.id, canResolveThread);
|
|
43
|
+
};
|
|
44
|
+
function SharedPlanApp({ providerLabel, ...props }) {
|
|
45
|
+
return /* @__PURE__ */ jsx(PlanReviewSurface, {
|
|
46
|
+
...props,
|
|
47
|
+
signInLabel: `Sign in with ${providerLabel} to comment`
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
function SharedWalkthroughApp({ commenting, gitIdentity, onDeleteShare, providerLabel, settingsBar, snapshot }) {
|
|
51
|
+
return /* @__PURE__ */ jsx(ReviewSurface, {
|
|
52
|
+
commenting,
|
|
53
|
+
gitIdentity,
|
|
54
|
+
onDeleteShare,
|
|
55
|
+
providerLabel,
|
|
56
|
+
settingsBar,
|
|
57
|
+
signInLabel: `Sign in with ${providerLabel} to comment`,
|
|
58
|
+
snapshot
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
//#endregion
|
|
62
|
+
export { SharedPlanApp, SharedWalkthroughApp, resolveSubmittedShareReply, resolveSubmittedShareThread };
|
package/dist/schema.mjs
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import { relations, sql } from "drizzle-orm";
|
|
2
|
+
import { check, index, integer, primaryKey, sqliteTable, text, uniqueIndex } from "drizzle-orm/sqlite-core";
|
|
3
|
+
//#region schema.ts
|
|
4
|
+
const idColumn = () => text().notNull().$defaultFn(() => crypto.randomUUID());
|
|
5
|
+
const timestampColumn = () => integer({ mode: "timestamp" }).notNull().default(sql`(unixepoch())`);
|
|
6
|
+
const nullableTimestampColumn = () => integer({ mode: "timestamp" });
|
|
7
|
+
const shareCommentThreadKind = [
|
|
8
|
+
"plan",
|
|
9
|
+
"walkthrough-diff",
|
|
10
|
+
"walkthrough-general"
|
|
11
|
+
];
|
|
12
|
+
const shareCommentThreadStatus = ["open", "resolved"];
|
|
13
|
+
const shareKind = ["plan", "walkthrough"];
|
|
14
|
+
const user = sqliteTable("user", {
|
|
15
|
+
banExpires: nullableTimestampColumn(),
|
|
16
|
+
banned: integer({ mode: "boolean" }),
|
|
17
|
+
banReason: text(),
|
|
18
|
+
createdAt: timestampColumn(),
|
|
19
|
+
displayUsername: text(),
|
|
20
|
+
email: text().notNull(),
|
|
21
|
+
emailVerified: integer({ mode: "boolean" }).notNull().default(false),
|
|
22
|
+
id: idColumn().primaryKey(),
|
|
23
|
+
image: text(),
|
|
24
|
+
name: text().notNull(),
|
|
25
|
+
password: text(),
|
|
26
|
+
role: text().notNull().default("user"),
|
|
27
|
+
updatedAt: timestampColumn(),
|
|
28
|
+
username: text()
|
|
29
|
+
}, (table) => [
|
|
30
|
+
uniqueIndex("user_email_key").on(table.email),
|
|
31
|
+
uniqueIndex("user_username_key").on(table.username),
|
|
32
|
+
index("user_id_idx").on(table.id)
|
|
33
|
+
]);
|
|
34
|
+
const walkthrough = sqliteTable("Walkthrough", {
|
|
35
|
+
branch: text(),
|
|
36
|
+
byteSize: integer().notNull(),
|
|
37
|
+
codiffVersion: text().notNull(),
|
|
38
|
+
commentsImportedAt: nullableTimestampColumn(),
|
|
39
|
+
createdAt: timestampColumn(),
|
|
40
|
+
description: text(),
|
|
41
|
+
filesIndexedAt: nullableTimestampColumn(),
|
|
42
|
+
id: idColumn().primaryKey(),
|
|
43
|
+
objectKey: text().notNull(),
|
|
44
|
+
pullRequestNumber: integer(),
|
|
45
|
+
pullRequestTitle: text(),
|
|
46
|
+
pullRequestUrl: text(),
|
|
47
|
+
repositoryHost: text(),
|
|
48
|
+
repositoryName: text(),
|
|
49
|
+
repositoryOwner: text(),
|
|
50
|
+
repositoryUrl: text(),
|
|
51
|
+
schemaVersion: integer().notNull(),
|
|
52
|
+
sha256: text().notNull(),
|
|
53
|
+
sharedByEmail: text(),
|
|
54
|
+
sharedByName: text(),
|
|
55
|
+
sharedByUserId: text().references(() => user.id, { onDelete: "set null" }),
|
|
56
|
+
slug: text().notNull(),
|
|
57
|
+
sourceType: text().notNull(),
|
|
58
|
+
title: text().notNull(),
|
|
59
|
+
updatedAt: timestampColumn()
|
|
60
|
+
}, (table) => [
|
|
61
|
+
uniqueIndex("Walkthrough_slug_key").on(table.slug),
|
|
62
|
+
index("Walkthrough_sharedByEmail_idx").on(table.sharedByEmail),
|
|
63
|
+
index("Walkthrough_sharedByUserId_idx").on(table.sharedByUserId),
|
|
64
|
+
index("Walkthrough_repository_idx").on(table.repositoryOwner, table.repositoryName)
|
|
65
|
+
]);
|
|
66
|
+
const walkthroughFile = sqliteTable("WalkthroughFile", {
|
|
67
|
+
path: text().notNull(),
|
|
68
|
+
walkthroughId: text().notNull().references(() => walkthrough.id, { onDelete: "cascade" })
|
|
69
|
+
}, (table) => [primaryKey({
|
|
70
|
+
columns: [table.walkthroughId, table.path],
|
|
71
|
+
name: "WalkthroughFile_walkthroughId_path_pkey"
|
|
72
|
+
})]);
|
|
73
|
+
const plan = sqliteTable("Plan", {
|
|
74
|
+
agent: text(),
|
|
75
|
+
byteSize: integer().notNull(),
|
|
76
|
+
codiffVersion: text().notNull(),
|
|
77
|
+
commentsImportedAt: nullableTimestampColumn(),
|
|
78
|
+
createdAt: timestampColumn(),
|
|
79
|
+
id: idColumn().primaryKey(),
|
|
80
|
+
objectKey: text().notNull(),
|
|
81
|
+
schemaVersion: integer().notNull(),
|
|
82
|
+
sessionId: text(),
|
|
83
|
+
sha256: text().notNull(),
|
|
84
|
+
sharedByEmail: text(),
|
|
85
|
+
sharedByName: text(),
|
|
86
|
+
sharedByUserId: text().references(() => user.id, { onDelete: "set null" }),
|
|
87
|
+
slug: text().notNull(),
|
|
88
|
+
sourceFileName: text().notNull(),
|
|
89
|
+
title: text().notNull(),
|
|
90
|
+
updatedAt: timestampColumn()
|
|
91
|
+
}, (table) => [
|
|
92
|
+
uniqueIndex("Plan_slug_key").on(table.slug),
|
|
93
|
+
index("Plan_sharedByEmail_idx").on(table.sharedByEmail),
|
|
94
|
+
index("Plan_sharedByUserId_idx").on(table.sharedByUserId)
|
|
95
|
+
]);
|
|
96
|
+
const uploadIntent = sqliteTable("UploadIntent", {
|
|
97
|
+
claimedAt: nullableTimestampColumn(),
|
|
98
|
+
code: text().notNull(),
|
|
99
|
+
createdAt: timestampColumn(),
|
|
100
|
+
expiresAt: integer({ mode: "timestamp" }).notNull(),
|
|
101
|
+
id: idColumn().primaryKey(),
|
|
102
|
+
secretHash: text().notNull(),
|
|
103
|
+
sharedByUserId: text().references(() => user.id, { onDelete: "set null" }),
|
|
104
|
+
shareKind: text({ enum: shareKind }).notNull().default("walkthrough"),
|
|
105
|
+
status: text({ enum: [
|
|
106
|
+
"pending",
|
|
107
|
+
"claimed",
|
|
108
|
+
"uploaded"
|
|
109
|
+
] }).notNull().default("pending"),
|
|
110
|
+
updatedAt: timestampColumn(),
|
|
111
|
+
uploadTokenHash: text(),
|
|
112
|
+
walkthroughSlug: text()
|
|
113
|
+
}, (table) => [uniqueIndex("UploadIntent_code_key").on(table.code), index("UploadIntent_sharedByUserId_idx").on(table.sharedByUserId)]);
|
|
114
|
+
const shareCommentThread = sqliteTable("ShareCommentThread", {
|
|
115
|
+
anchorJson: text(),
|
|
116
|
+
createdAt: timestampColumn(),
|
|
117
|
+
filePath: text(),
|
|
118
|
+
id: idColumn().primaryKey(),
|
|
119
|
+
kind: text({ enum: shareCommentThreadKind }).notNull(),
|
|
120
|
+
lineNumber: integer(),
|
|
121
|
+
planId: text().references(() => plan.id, { onDelete: "cascade" }),
|
|
122
|
+
resolvedAt: nullableTimestampColumn(),
|
|
123
|
+
sectionId: text(),
|
|
124
|
+
side: text({ enum: ["additions", "deletions"] }),
|
|
125
|
+
sourceThreadId: text(),
|
|
126
|
+
startLineNumber: integer(),
|
|
127
|
+
startSide: text({ enum: ["additions", "deletions"] }),
|
|
128
|
+
status: text({ enum: shareCommentThreadStatus }).notNull().default("open"),
|
|
129
|
+
updatedAt: timestampColumn(),
|
|
130
|
+
walkthroughId: text().references(() => walkthrough.id, { onDelete: "cascade" })
|
|
131
|
+
}, (table) => [
|
|
132
|
+
check("ShareCommentThread_parent_check", sql`(${table.planId} is not null and ${table.walkthroughId} is null) or (${table.planId} is null and ${table.walkthroughId} is not null)`),
|
|
133
|
+
index("ShareCommentThread_planId_createdAt_idx").on(table.planId, table.createdAt),
|
|
134
|
+
index("ShareCommentThread_walkthroughId_createdAt_idx").on(table.walkthroughId, table.createdAt),
|
|
135
|
+
uniqueIndex("ShareCommentThread_planId_sourceThreadId_key").on(table.planId, table.sourceThreadId),
|
|
136
|
+
uniqueIndex("ShareCommentThread_walkthroughId_sourceThreadId_key").on(table.walkthroughId, table.sourceThreadId)
|
|
137
|
+
]);
|
|
138
|
+
const shareCommentMessage = sqliteTable("ShareCommentMessage", {
|
|
139
|
+
authorImage: text(),
|
|
140
|
+
authorName: text().notNull(),
|
|
141
|
+
authorUserId: text().references(() => user.id, { onDelete: "set null" }),
|
|
142
|
+
authorUsername: text(),
|
|
143
|
+
body: text().notNull(),
|
|
144
|
+
createdAt: timestampColumn(),
|
|
145
|
+
id: idColumn().primaryKey(),
|
|
146
|
+
sourceMessageId: text(),
|
|
147
|
+
threadId: text().notNull().references(() => shareCommentThread.id, { onDelete: "cascade" }),
|
|
148
|
+
updatedAt: timestampColumn()
|
|
149
|
+
}, (table) => [
|
|
150
|
+
index("ShareCommentMessage_authorUserId_idx").on(table.authorUserId),
|
|
151
|
+
index("ShareCommentMessage_threadId_createdAt_idx").on(table.threadId, table.createdAt),
|
|
152
|
+
uniqueIndex("ShareCommentMessage_threadId_sourceMessageId_key").on(table.threadId, table.sourceMessageId)
|
|
153
|
+
]);
|
|
154
|
+
const shareDailyUsage = sqliteTable("ShareDailyUsage", {
|
|
155
|
+
date: text().notNull(),
|
|
156
|
+
planCount: integer().notNull().default(0),
|
|
157
|
+
userId: text().notNull().references(() => user.id, { onDelete: "cascade" }),
|
|
158
|
+
walkthroughCount: integer().notNull().default(0)
|
|
159
|
+
}, (table) => [primaryKey({
|
|
160
|
+
columns: [table.userId, table.date],
|
|
161
|
+
name: "ShareDailyUsage_userId_date_pkey"
|
|
162
|
+
})]);
|
|
163
|
+
const userRelations = relations(user, ({ many }) => ({
|
|
164
|
+
shareCommentMessages: many(shareCommentMessage),
|
|
165
|
+
shareDailyUsage: many(shareDailyUsage)
|
|
166
|
+
}));
|
|
167
|
+
const walkthroughRelations = relations(walkthrough, ({ many }) => ({
|
|
168
|
+
commentThreads: many(shareCommentThread),
|
|
169
|
+
files: many(walkthroughFile)
|
|
170
|
+
}));
|
|
171
|
+
const walkthroughFileRelations = relations(walkthroughFile, ({ one }) => ({ walkthrough: one(walkthrough, {
|
|
172
|
+
fields: [walkthroughFile.walkthroughId],
|
|
173
|
+
references: [walkthrough.id]
|
|
174
|
+
}) }));
|
|
175
|
+
const planRelations = relations(plan, ({ many }) => ({ commentThreads: many(shareCommentThread) }));
|
|
176
|
+
const shareCommentThreadRelations = relations(shareCommentThread, ({ many, one }) => ({
|
|
177
|
+
messages: many(shareCommentMessage),
|
|
178
|
+
plan: one(plan, {
|
|
179
|
+
fields: [shareCommentThread.planId],
|
|
180
|
+
references: [plan.id]
|
|
181
|
+
}),
|
|
182
|
+
walkthrough: one(walkthrough, {
|
|
183
|
+
fields: [shareCommentThread.walkthroughId],
|
|
184
|
+
references: [walkthrough.id]
|
|
185
|
+
})
|
|
186
|
+
}));
|
|
187
|
+
const shareCommentMessageRelations = relations(shareCommentMessage, ({ one }) => ({
|
|
188
|
+
author: one(user, {
|
|
189
|
+
fields: [shareCommentMessage.authorUserId],
|
|
190
|
+
references: [user.id]
|
|
191
|
+
}),
|
|
192
|
+
thread: one(shareCommentThread, {
|
|
193
|
+
fields: [shareCommentMessage.threadId],
|
|
194
|
+
references: [shareCommentThread.id]
|
|
195
|
+
})
|
|
196
|
+
}));
|
|
197
|
+
const shareDailyUsageRelations = relations(shareDailyUsage, ({ one }) => ({ user: one(user, {
|
|
198
|
+
fields: [shareDailyUsage.userId],
|
|
199
|
+
references: [user.id]
|
|
200
|
+
}) }));
|
|
201
|
+
const schema = {
|
|
202
|
+
plan,
|
|
203
|
+
planRelations,
|
|
204
|
+
shareCommentMessage,
|
|
205
|
+
shareCommentMessageRelations,
|
|
206
|
+
shareCommentThread,
|
|
207
|
+
shareCommentThreadRelations,
|
|
208
|
+
shareDailyUsage,
|
|
209
|
+
shareDailyUsageRelations,
|
|
210
|
+
uploadIntent,
|
|
211
|
+
user,
|
|
212
|
+
userRelations,
|
|
213
|
+
walkthrough,
|
|
214
|
+
walkthroughFile,
|
|
215
|
+
walkthroughFileRelations,
|
|
216
|
+
walkthroughRelations
|
|
217
|
+
};
|
|
218
|
+
//#endregion
|
|
219
|
+
export { schema as default, plan, planRelations, shareCommentMessage, shareCommentMessageRelations, shareCommentThread, shareCommentThreadKind, shareCommentThreadRelations, shareCommentThreadStatus, shareDailyUsage, shareDailyUsageRelations, shareKind, uploadIntent, user, userRelations, walkthrough, walkthroughFile, walkthroughFileRelations, walkthroughRelations };
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
//#region upload-intent.ts
|
|
2
|
+
const encoder = new TextEncoder();
|
|
3
|
+
const tokenVersion = "v1";
|
|
4
|
+
const base64UrlEncode = (value) => btoa(String.fromCharCode(...value)).replaceAll("+", "-").replaceAll("/", "_").replaceAll("=", "");
|
|
5
|
+
const base64UrlDecode = (value) => {
|
|
6
|
+
const padding = "=".repeat((4 - value.length % 4) % 4);
|
|
7
|
+
const decoded = atob(value.replaceAll("-", "+").replaceAll("_", "/") + padding);
|
|
8
|
+
return Uint8Array.from(decoded, (character) => character.charCodeAt(0));
|
|
9
|
+
};
|
|
10
|
+
const signingKey = (env) => {
|
|
11
|
+
const secret = env.BETTER_AUTH_SECRET?.trim();
|
|
12
|
+
if (!secret) throw new Error("Codiff sharing requires BETTER_AUTH_SECRET.");
|
|
13
|
+
return crypto.subtle.importKey("raw", encoder.encode(secret), {
|
|
14
|
+
hash: "SHA-256",
|
|
15
|
+
name: "HMAC"
|
|
16
|
+
}, false, ["sign", "verify"]);
|
|
17
|
+
};
|
|
18
|
+
const signedValue = (code, payload) => `${code}.${payload}`;
|
|
19
|
+
const createUploadIntentSecret = async (env, code, expiresAt) => {
|
|
20
|
+
const nonce = /* @__PURE__ */ new Uint8Array(24);
|
|
21
|
+
crypto.getRandomValues(nonce);
|
|
22
|
+
const payload = [
|
|
23
|
+
tokenVersion,
|
|
24
|
+
expiresAt.getTime().toString(36),
|
|
25
|
+
base64UrlEncode(nonce)
|
|
26
|
+
].join(".");
|
|
27
|
+
const signature = await crypto.subtle.sign("HMAC", await signingKey(env), encoder.encode(signedValue(code, payload)));
|
|
28
|
+
return `${payload}.${base64UrlEncode(new Uint8Array(signature))}`;
|
|
29
|
+
};
|
|
30
|
+
const hashUploadIntentSecret = async (secret) => {
|
|
31
|
+
const digest = await crypto.subtle.digest("SHA-256", encoder.encode(secret));
|
|
32
|
+
return [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
33
|
+
};
|
|
34
|
+
const verifyUploadIntentSecret = async (env, code, secret) => {
|
|
35
|
+
const parts = secret.split(".");
|
|
36
|
+
if (parts.length !== 4 || parts[0] !== tokenVersion) return null;
|
|
37
|
+
const [version, encodedExpiresAt, nonce, encodedSignature] = parts;
|
|
38
|
+
const expiresAtMs = Number.parseInt(encodedExpiresAt, 36);
|
|
39
|
+
if (version !== tokenVersion || !Number.isSafeInteger(expiresAtMs) || expiresAtMs <= 0 || !nonce || !encodedSignature) return null;
|
|
40
|
+
try {
|
|
41
|
+
const payload = `${version}.${encodedExpiresAt}.${nonce}`;
|
|
42
|
+
return await crypto.subtle.verify("HMAC", await signingKey(env), base64UrlDecode(encodedSignature), encoder.encode(signedValue(code, payload))) ? { expiresAt: new Date(expiresAtMs) } : null;
|
|
43
|
+
} catch {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
//#endregion
|
|
48
|
+
export { createUploadIntentSecret, hashUploadIntentSecret, verifyUploadIntentSecret };
|
package/dist/views.mjs
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { FateRequestError, computed, dataView, field, list } from "@nkzw/fate/server";
|
|
2
|
+
//#region views.ts
|
|
3
|
+
const uploadIntentSecretHeader = "x-codiff-upload-secret";
|
|
4
|
+
const isRecord = (value) => value !== null && typeof value === "object";
|
|
5
|
+
const getSessionUserId = (value) => {
|
|
6
|
+
const result = isRecord(value) && isRecord(value.response) ? value.response : value;
|
|
7
|
+
const sessionUser = isRecord(result) ? result.user : null;
|
|
8
|
+
return isRecord(sessionUser) && typeof sessionUser.id === "string" && sessionUser.id ? sessionUser.id : null;
|
|
9
|
+
};
|
|
10
|
+
const sessionUserIds = /* @__PURE__ */ new WeakMap();
|
|
11
|
+
const readContextSessionUserId = (context) => {
|
|
12
|
+
if (!context.auth) return Promise.resolve(null);
|
|
13
|
+
const existing = sessionUserIds.get(context.request);
|
|
14
|
+
if (existing) return existing;
|
|
15
|
+
const result = context.auth.api.getSession({
|
|
16
|
+
asResponse: false,
|
|
17
|
+
headers: context.request.headers
|
|
18
|
+
}).then(getSessionUserId);
|
|
19
|
+
sessionUserIds.set(context.request, result);
|
|
20
|
+
return result;
|
|
21
|
+
};
|
|
22
|
+
const canManageShare = () => computed({
|
|
23
|
+
resolve: async (_record, values, context) => Boolean(values.sharedByUserId && context?.auth && await readContextSessionUserId(context) === values.sharedByUserId),
|
|
24
|
+
select: { sharedByUserId: field("sharedByUserId") }
|
|
25
|
+
});
|
|
26
|
+
const canEditShareComment = () => computed({
|
|
27
|
+
resolve: async (_record, values, context) => Boolean(values.authorUserId && context?.auth && await readContextSessionUserId(context) === values.authorUserId),
|
|
28
|
+
select: { authorUserId: field("authorUserId") }
|
|
29
|
+
});
|
|
30
|
+
const sha256 = async (value) => {
|
|
31
|
+
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(value));
|
|
32
|
+
return [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
33
|
+
};
|
|
34
|
+
const protectedUploadIntentField = (fieldName) => computed({
|
|
35
|
+
resolve: async (_intent, values, context) => {
|
|
36
|
+
const secret = context?.request.headers.get(uploadIntentSecretHeader);
|
|
37
|
+
if (!secret || values.secretHash !== await sha256(secret)) throw new FateRequestError("NOT_FOUND", "Upload intent not found.");
|
|
38
|
+
return values.value;
|
|
39
|
+
},
|
|
40
|
+
select: {
|
|
41
|
+
secretHash: field("secretHash"),
|
|
42
|
+
value: field(fieldName)
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
const uploadIntentDataView = dataView("UploadIntent")({
|
|
46
|
+
expiresAt: protectedUploadIntentField("expiresAt"),
|
|
47
|
+
id: true,
|
|
48
|
+
shareKind: protectedUploadIntentField("shareKind"),
|
|
49
|
+
status: protectedUploadIntentField("status"),
|
|
50
|
+
walkthroughSlug: protectedUploadIntentField("walkthroughSlug")
|
|
51
|
+
});
|
|
52
|
+
const shareCommentMessageDataView = dataView("ShareCommentMessage")({
|
|
53
|
+
authorImage: true,
|
|
54
|
+
authorName: true,
|
|
55
|
+
authorUsername: true,
|
|
56
|
+
body: true,
|
|
57
|
+
canEdit: canEditShareComment(),
|
|
58
|
+
createdAt: true,
|
|
59
|
+
id: true,
|
|
60
|
+
threadId: true,
|
|
61
|
+
updatedAt: true
|
|
62
|
+
});
|
|
63
|
+
const shareCommentThreadDataView = dataView("ShareCommentThread")({
|
|
64
|
+
anchorJson: true,
|
|
65
|
+
createdAt: true,
|
|
66
|
+
filePath: true,
|
|
67
|
+
id: true,
|
|
68
|
+
kind: true,
|
|
69
|
+
lineNumber: true,
|
|
70
|
+
messages: list(shareCommentMessageDataView, { orderBy: [{ createdAt: "asc" }, { id: "asc" }] }),
|
|
71
|
+
planId: true,
|
|
72
|
+
resolvedAt: true,
|
|
73
|
+
sectionId: true,
|
|
74
|
+
side: true,
|
|
75
|
+
startLineNumber: true,
|
|
76
|
+
startSide: true,
|
|
77
|
+
status: true,
|
|
78
|
+
updatedAt: true,
|
|
79
|
+
walkthroughId: true
|
|
80
|
+
});
|
|
81
|
+
const walkthroughDataView = dataView("Walkthrough")({
|
|
82
|
+
branch: true,
|
|
83
|
+
byteSize: true,
|
|
84
|
+
canDelete: canManageShare(),
|
|
85
|
+
canResolveComments: canManageShare(),
|
|
86
|
+
codiffVersion: true,
|
|
87
|
+
commentThreads: list(shareCommentThreadDataView, { orderBy: [{ createdAt: "asc" }, { id: "asc" }] }),
|
|
88
|
+
createdAt: true,
|
|
89
|
+
description: true,
|
|
90
|
+
id: true,
|
|
91
|
+
pullRequestNumber: true,
|
|
92
|
+
pullRequestTitle: true,
|
|
93
|
+
pullRequestUrl: true,
|
|
94
|
+
repositoryHost: true,
|
|
95
|
+
repositoryName: true,
|
|
96
|
+
repositoryOwner: true,
|
|
97
|
+
repositoryUrl: true,
|
|
98
|
+
schemaVersion: true,
|
|
99
|
+
sha256: true,
|
|
100
|
+
slug: true,
|
|
101
|
+
sourceType: true,
|
|
102
|
+
title: true,
|
|
103
|
+
updatedAt: true
|
|
104
|
+
});
|
|
105
|
+
const planDataView = dataView("Plan")({
|
|
106
|
+
agent: true,
|
|
107
|
+
byteSize: true,
|
|
108
|
+
canDelete: canManageShare(),
|
|
109
|
+
canResolveComments: canManageShare(),
|
|
110
|
+
codiffVersion: true,
|
|
111
|
+
commentThreads: list(shareCommentThreadDataView, { orderBy: [{ createdAt: "asc" }, { id: "asc" }] }),
|
|
112
|
+
createdAt: true,
|
|
113
|
+
id: true,
|
|
114
|
+
schemaVersion: true,
|
|
115
|
+
slug: true,
|
|
116
|
+
sourceFileName: true,
|
|
117
|
+
title: true,
|
|
118
|
+
updatedAt: true
|
|
119
|
+
});
|
|
120
|
+
const shareStatsDayDataView = dataView("ShareStatsDay")({
|
|
121
|
+
date: true,
|
|
122
|
+
id: true,
|
|
123
|
+
mergeRequestWalkthroughs: true,
|
|
124
|
+
plans: true,
|
|
125
|
+
walkthroughs: true
|
|
126
|
+
});
|
|
127
|
+
const shareStatsDataView = dataView("ShareStats")({
|
|
128
|
+
days: list(shareStatsDayDataView),
|
|
129
|
+
id: true,
|
|
130
|
+
maxDailyShares: true,
|
|
131
|
+
totalMergeRequestWalkthroughs: true,
|
|
132
|
+
totalPlans: true,
|
|
133
|
+
totalWalkthroughs: true
|
|
134
|
+
});
|
|
135
|
+
const SharingRoot = {
|
|
136
|
+
planBySlug: planDataView,
|
|
137
|
+
sharingStats: shareStatsDataView,
|
|
138
|
+
uploadIntentByCode: uploadIntentDataView,
|
|
139
|
+
walkthroughBySlug: walkthroughDataView
|
|
140
|
+
};
|
|
141
|
+
//#endregion
|
|
142
|
+
export { SharingRoot, getSessionUserId, planDataView, shareCommentMessageDataView, shareCommentThreadDataView, shareStatsDataView, shareStatsDayDataView, uploadIntentDataView, uploadIntentSecretHeader, walkthroughDataView };
|
package/fate.test.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { createLiveEventBus } from '@nkzw/fate/server';
|
|
2
|
+
import { expect, test } from 'vite-plus/test';
|
|
3
|
+
import { createSharingFateServer } from './fate.ts';
|
|
4
|
+
|
|
5
|
+
const fateServer = createSharingFateServer({
|
|
6
|
+
live: createLiveEventBus(),
|
|
7
|
+
providerLabel: 'GitHub',
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
test('enables live subscriptions for every shared service entity', () => {
|
|
11
|
+
expect(fateServer.manifest.live).toEqual(fateServer.manifest.types);
|
|
12
|
+
expect(Object.keys(fateServer.manifest.live)).toEqual([
|
|
13
|
+
'Plan',
|
|
14
|
+
'ShareCommentMessage',
|
|
15
|
+
'ShareCommentThread',
|
|
16
|
+
'ShareStats',
|
|
17
|
+
'ShareStatsDay',
|
|
18
|
+
'UploadIntent',
|
|
19
|
+
'Walkthrough',
|
|
20
|
+
]);
|
|
21
|
+
});
|