nolo-cli 0.1.13 → 0.1.15
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/README.md +9 -2
- package/agent-runtime/hostAdapter.ts +53 -0
- package/agent-runtime/index.ts +28 -0
- package/agent-runtime/localLoop.ts +62 -0
- package/agent-runtime/runtimeDecision.ts +70 -0
- package/agent-runtime/types.ts +87 -0
- package/agentRunCommand.ts +104 -0
- package/agentRuntimeCommands.ts +139 -22
- package/agentRuntimeLocal.ts +7 -0
- package/ai/agent/_executeModel.ts +118 -0
- package/ai/agent/agentSlice.ts +544 -1
- package/ai/agent/appWorkingMemory.ts +126 -0
- package/ai/agent/avatarUtils.ts +24 -0
- package/ai/agent/buildEditingContext.ts +373 -0
- package/ai/agent/buildSystemPrompt.ts +532 -0
- package/ai/agent/cleanAgentMessages.ts +140 -0
- package/ai/agent/cliChatClient.ts +119 -0
- package/ai/agent/contextCompiler.ts +107 -0
- package/ai/agent/contextLayerContract.ts +44 -0
- package/ai/agent/createAgentSchema.ts +234 -0
- package/ai/agent/executeToolCall.ts +58 -0
- package/ai/agent/fetchAgentContexts.ts +42 -0
- package/ai/agent/generatePrompt.ts +3 -0
- package/ai/agent/getFullChatContextKeys.ts +168 -0
- package/ai/agent/hooks/fetchPublicAgents.ts +133 -0
- package/ai/agent/hooks/useAgentConfig.ts +61 -0
- package/ai/agent/hooks/useAgentDialog.ts +35 -0
- package/ai/agent/hooks/useAgentFormValidation.ts +202 -0
- package/ai/agent/hooks/usePublicAgents.ts +473 -0
- package/ai/agent/persistMessageWithFixedId.ts +37 -0
- package/ai/agent/planSlice.ts +259 -0
- package/ai/agent/referenceUtils.ts +229 -0
- package/ai/agent/runAgentBackground.ts +238 -0
- package/ai/agent/runAgentClientLoop.ts +138 -0
- package/ai/agent/runtimeGuidance.ts +97 -0
- package/ai/agent/runtimeServerBase.ts +37 -0
- package/ai/agent/server/fetchPublicAgents.ts +128 -0
- package/ai/agent/startParallelAgentStreams.ts +424 -0
- package/ai/agent/startupProtocol.ts +53 -0
- package/ai/agent/streamAgentChatTurn.ts +1299 -0
- package/ai/agent/streamAgentChatTurnUtils.ts +738 -0
- package/ai/agent/types.ts +71 -0
- package/ai/agent/utils/imageOutput.ts +39 -0
- package/ai/agent/utils/publicImageAgentMode.ts +26 -0
- package/ai/agent/utils/sortUtils.ts +250 -0
- package/ai/agent/web/referencePickerUtils.ts +146 -0
- package/ai/ai.locale.ts +1083 -0
- package/ai/chat/accumulateToolCallChunks.ts +95 -0
- package/ai/chat/fetchUtils.native.ts +276 -0
- package/ai/chat/fetchUtils.ts +153 -0
- package/ai/chat/inlineImageUrlsForCustomProvider.ts +117 -0
- package/ai/chat/parseApiError.ts +64 -0
- package/ai/chat/parseMultilineSSE.ts +95 -0
- package/ai/chat/sendOpenAICompletionsRequest.native.ts +682 -0
- package/ai/chat/sendOpenAICompletionsRequest.ts +712 -0
- package/ai/chat/sendOpenAIResponseRequest.ts +512 -0
- package/ai/chat/shouldUseServerProxy.ts +18 -0
- package/ai/chat/sseClient.native.ts +91 -0
- package/ai/chat/sseClient.ts +67 -0
- package/ai/chat/streamReader.native.ts +31 -0
- package/ai/chat/streamReader.ts +62 -0
- package/ai/chat/updateTotalUsage.ts +72 -0
- package/ai/context/buildReferenceContext.ts +437 -0
- package/ai/context/calculateContextUsage.ts +133 -0
- package/ai/context/retention.ts +165 -0
- package/ai/context/tokenUtils.ts +78 -0
- package/ai/index.ts +1 -1
- package/ai/llm/agentCapabilities.ts +74 -0
- package/ai/llm/calculateGeminiImageTokens.ts +57 -0
- package/ai/llm/deepinfra.ts +28 -0
- package/ai/llm/fireworks.ts +68 -0
- package/ai/llm/generateRequestBody.ts +165 -0
- package/ai/llm/getModelContextWindow.ts +84 -0
- package/ai/llm/getNoloKey.ts +37 -0
- package/ai/llm/getPricing.ts +232 -0
- package/ai/llm/hooks/useModelPricing.ts +75 -0
- package/ai/llm/imagePricing.ts +66 -0
- package/ai/llm/isResponseAPIModel.ts +13 -0
- package/ai/llm/kimi.ts +18 -0
- package/ai/llm/mimo.ts +71 -0
- package/ai/llm/mistral.ts +22 -0
- package/ai/llm/modelAvatar.ts +427 -0
- package/ai/llm/models.ts +45 -0
- package/ai/llm/openrouterModels.ts +141 -0
- package/ai/llm/providers.ts +307 -0
- package/ai/llm/reasoningModels.ts +28 -0
- package/ai/llm/types.ts +59 -0
- package/ai/llm/usageRequestOptions.ts +59 -0
- package/ai/memory/capture.ts +148 -0
- package/ai/memory/consolidate.ts +104 -0
- package/ai/memory/delete.ts +147 -0
- package/ai/memory/overlay.ts +84 -0
- package/ai/memory/query.ts +38 -0
- package/ai/memory/queryShared.ts +160 -0
- package/ai/memory/rank.ts +105 -0
- package/ai/memory/recentRelationshipRecap.ts +247 -0
- package/ai/memory/remember.ts +167 -0
- package/ai/memory/runtime.ts +76 -0
- package/ai/memory/store.ts +20 -0
- package/ai/memory/storeShared.ts +76 -0
- package/ai/memory/types.ts +46 -0
- package/ai/memory/understanding.ts +349 -0
- package/ai/memory/understandingGreeting.ts +264 -0
- package/ai/messages/type.ts +20 -0
- package/ai/policy/personalizationDialog.ts +333 -0
- package/ai/policy/runtimePolicy.ts +440 -0
- package/ai/policy/selfUpdateFields.ts +48 -0
- package/ai/policy/types.ts +64 -0
- package/ai/skills/referenceRuntime.ts +274 -0
- package/ai/skills/skillDiagnostics.ts +251 -0
- package/ai/skills/skillDocBuilder.ts +139 -0
- package/ai/skills/skillDocProtocol.ts +434 -0
- package/ai/skills/skillReferenceSummary.ts +63 -0
- package/ai/skills/skillSummaryMarker.ts +26 -0
- package/ai/token/calculatePrice.ts +546 -0
- package/ai/token/db.ts +98 -0
- package/ai/token/externalToolCost.ts +321 -0
- package/ai/token/hooks/useRecords.ts +65 -0
- package/ai/token/missingUsageEstimate.ts +42 -0
- package/ai/token/modelUsageQuery.ts +252 -0
- package/ai/token/normalizeUsage.ts +84 -0
- package/ai/token/openaiImageGenerationUsage.ts +56 -0
- package/ai/token/prepareTokenUsageData.ts +88 -0
- package/ai/token/query.ts +88 -0
- package/ai/token/queryUserTokens.ts +59 -0
- package/ai/token/resolveBillingTarget.ts +52 -0
- package/ai/token/saveTokenRecord.ts +53 -0
- package/ai/token/serverDialogProjection.ts +78 -0
- package/ai/token/serverTokenWriter.ts +143 -0
- package/ai/token/stats.ts +21 -0
- package/ai/token/tokenThunks.ts +24 -0
- package/ai/token/types.ts +93 -0
- package/ai/tools/agent/agentTools.ts +176 -0
- package/ai/tools/agent/agentUpdateShared.ts +311 -0
- package/ai/tools/agent/callAgentTool.ts +139 -0
- package/ai/tools/agent/createAgentTool.ts +512 -0
- package/ai/tools/agent/createDialogTool.ts +69 -0
- package/ai/tools/agent/createSkillAgentTool.ts +62 -0
- package/ai/tools/agent/parallelBudget.ts +221 -0
- package/ai/tools/agent/presets/appBuilderPreset.ts +147 -0
- package/ai/tools/agent/runLlmTool.ts +96 -0
- package/ai/tools/agent/runStreamingAgentTool.ts +73 -0
- package/ai/tools/agent/skillAgentArgs.ts +106 -0
- package/ai/tools/agent/skillAgentPreset.ts +89 -0
- package/ai/tools/agent/streamParallelAgentsTool.ts +122 -0
- package/ai/tools/agent/updateAgentTool.ts +96 -0
- package/ai/tools/agent/updateSelfTool.ts +113 -0
- package/ai/tools/amazonProductScraperTool.ts +86 -0
- package/ai/tools/apifyActorClient.ts +45 -0
- package/ai/tools/appEditGuard.ts +372 -0
- package/ai/tools/appReadSnapshot.ts +153 -0
- package/ai/tools/appTools.ts +1549 -0
- package/ai/tools/applyEditTool.ts +256 -0
- package/ai/tools/applyLineEditsTool.ts +312 -0
- package/ai/tools/browserTools/click.ts +33 -0
- package/ai/tools/browserTools/closeSession.ts +29 -0
- package/ai/tools/browserTools/common.ts +27 -0
- package/ai/tools/browserTools/openSession.ts +48 -0
- package/ai/tools/browserTools/readContent.ts +38 -0
- package/ai/tools/browserTools/selectOption.ts +46 -0
- package/ai/tools/browserTools/typeText.ts +42 -0
- package/ai/tools/category/createCategoryTool.ts +66 -0
- package/ai/tools/category/queryContentsByCategoryTool.ts +69 -0
- package/ai/tools/category/updateContentCategoryTool.ts +75 -0
- package/ai/tools/cfBrowserTools.ts +319 -0
- package/ai/tools/cfSpeechToTextTool.ts +49 -0
- package/ai/tools/checkEnvTool.ts +65 -0
- package/ai/tools/cloudflareCrawlTool.ts +289 -0
- package/ai/tools/codeSearchTool.ts +111 -0
- package/ai/tools/codeTools.ts +101 -0
- package/ai/tools/createDocTool.ts +132 -0
- package/ai/tools/createPlanTool.ts +999 -0
- package/ai/tools/createSkillDocTool.ts +155 -0
- package/ai/tools/createWorkflowTool.ts +154 -0
- package/ai/tools/deepseekOcrTool.ts +34 -0
- package/ai/tools/delayTool.ts +31 -0
- package/ai/tools/deleteSpacesTool.ts +325 -0
- package/ai/tools/deleteSpacesToolModel.ts +159 -0
- package/ai/tools/devReloadUtils.ts +29 -0
- package/ai/tools/dialogMessageSearch.ts +137 -0
- package/ai/tools/doctorSkillTool.ts +72 -0
- package/ai/tools/ecommerceScraperTool.ts +86 -0
- package/ai/tools/emailTools.ts +549 -0
- package/ai/tools/evalSkillTool.ts +92 -0
- package/ai/tools/exaSearchTool.ts +64 -0
- package/ai/tools/execBashTool.ts +379 -0
- package/ai/tools/executeSqlTool.ts +192 -0
- package/ai/tools/fetchWebpageSupport.ts +309 -0
- package/ai/tools/fetchWebpageTool.ts +84 -0
- package/ai/tools/geminiImagePreviewTool.ts +361 -0
- package/ai/tools/generateDocxTool.ts +215 -0
- package/ai/tools/googleSearchScraperTool.ts +106 -0
- package/ai/tools/importDataTool.ts +133 -0
- package/ai/tools/importSkillTool.ts +162 -0
- package/ai/tools/index.ts +1927 -0
- package/ai/tools/listFilesTool.ts +82 -0
- package/ai/tools/listUserSpacesTool.ts +113 -0
- package/ai/tools/modelUsageTools.ts +199 -0
- package/ai/tools/olmOcrTool.ts +34 -0
- package/ai/tools/openaiImageTool.ts +267 -0
- package/ai/tools/prepareTools.ts +23 -0
- package/ai/tools/readDocTool.ts +84 -0
- package/ai/tools/readFileTool.ts +211 -0
- package/ai/tools/readTool.ts +163 -0
- package/ai/tools/readXPostTool.ts +233 -0
- package/ai/tools/rememberMemoryTool.ts +84 -0
- package/ai/tools/remotionVideoTool.ts +151 -0
- package/ai/tools/searchDialogMessagesTool.ts +222 -0
- package/ai/tools/searchRepoTool.ts +115 -0
- package/ai/tools/searchWorkspaceTool.ts +259 -0
- package/ai/tools/skillFollowup.ts +86 -0
- package/ai/tools/surfWeatherTool.ts +169 -0
- package/ai/tools/table/addTableRowTool.ts +217 -0
- package/ai/tools/table/createTableTool.ts +315 -0
- package/ai/tools/table/rowTools.ts +366 -0
- package/ai/tools/table/schemaTools.ts +244 -0
- package/ai/tools/table/shareTableTool.ts +148 -0
- package/ai/tools/table/toolShared.ts +129 -0
- package/ai/tools/toolApiClient.ts +198 -0
- package/ai/tools/toolNameAliases.ts +57 -0
- package/ai/tools/toolResultError.ts +42 -0
- package/ai/tools/toolRunSlice.ts +303 -0
- package/ai/tools/toolSchemaCompatibility.ts +53 -0
- package/ai/tools/toolVisibility.ts +4 -0
- package/ai/tools/types.ts +20 -0
- package/ai/tools/uiAskChoiceTool.ts +104 -0
- package/ai/tools/updateContentTitleTool.ts +84 -0
- package/ai/tools/updateDocTool.ts +105 -0
- package/ai/tools/updateUserPreferenceProfileTool.ts +145 -0
- package/ai/tools/whisperTool.ts +77 -0
- package/ai/tools/writeFileTool.ts +210 -0
- package/ai/tools/youtubeScraperTool.ts +116 -0
- package/ai/tools/ziweiChartTool.ts +678 -0
- package/ai/types.ts +55 -0
- package/ai/workflow/workflowExecutor.ts +323 -0
- package/ai/workflow/workflowSlice.ts +73 -0
- package/ai/workflow/workflowTypes.ts +106 -0
- package/client/agentRun.test.ts +240 -0
- package/client/agentRun.ts +182 -19
- package/client/compactDialog.test.ts +238 -0
- package/client/localRuntimeAdapter.test.ts +135 -0
- package/client/localRuntimeAdapter.ts +244 -0
- package/client/profileConfig.test.ts +40 -0
- package/client/streamingOutput.test.ts +22 -0
- package/client/streamingOutput.ts +38 -0
- package/commandRegistry.ts +11 -2
- package/connector-experimental/index.ts +5 -0
- package/database/actions/cacheMergedUserData.ts +64 -0
- package/database/actions/common.ts +242 -0
- package/database/actions/deleteFile.ts +40 -0
- package/database/actions/fetchUserData.ts +16 -0
- package/database/actions/fileContent.ts +125 -0
- package/database/actions/patch.ts +155 -0
- package/database/actions/read.ts +337 -0
- package/database/actions/readAndWait.ts +224 -0
- package/database/actions/readRequestManager.ts +120 -0
- package/database/actions/remove.ts +94 -0
- package/database/actions/replication.ts +366 -0
- package/database/actions/upload.ts +174 -0
- package/database/actions/upsert.ts +56 -0
- package/database/actions/write.ts +126 -0
- package/database/client/db.native.ts +73 -0
- package/database/client/db.ts +51 -0
- package/database/client/fetchUserData.ts +61 -0
- package/database/client/handleError.ts +19 -0
- package/database/client/queryRequest.ts +21 -0
- package/database/config.ts +21 -0
- package/database/dbActionThunks.ts +1 -0
- package/database/dbSlice.ts +149 -0
- package/database/email.ts +42 -0
- package/database/fileRing.ts +51 -0
- package/database/fileSharding.ts +70 -0
- package/database/fileStorage.native.ts +92 -0
- package/database/fileStorage.ts +232 -0
- package/database/fileUrl.ts +34 -0
- package/database/hooks/useUserData.ts +489 -0
- package/database/index.ts +1 -0
- package/database/keys.ts +765 -0
- package/database/queryPrefixes.ts +14 -0
- package/database/requests.ts +443 -0
- package/database/runtimeServerContext.ts +35 -0
- package/database/server/MemoryDB.ts +76 -0
- package/database/server/actorAccess.ts +76 -0
- package/database/server/agentDelegation.ts +124 -0
- package/database/server/coreDataOwnership.ts +13 -0
- package/database/server/coreDataProxy.ts +76 -0
- package/database/server/cybotReadonly.ts +18 -0
- package/database/server/dataHandlers.ts +111 -0
- package/database/server/db.ts +118 -0
- package/database/server/dbPath.ts +20 -0
- package/database/server/delete.ts +499 -0
- package/database/server/emailRepository.ts +1480 -0
- package/database/server/ensureDbOpen.ts +12 -0
- package/database/server/fileRead.ts +337 -0
- package/database/server/fileService.ts +436 -0
- package/database/server/handleTransaction.ts +86 -0
- package/database/server/patch.ts +282 -0
- package/database/server/query.ts +138 -0
- package/database/server/read.ts +325 -0
- package/database/server/resourceAccess.ts +211 -0
- package/database/server/routes.ts +110 -0
- package/database/server/spaceMemberAuthority.ts +67 -0
- package/database/server/upload.ts +159 -0
- package/database/server/write.ts +494 -0
- package/database/server/writeAuthority.ts +133 -0
- package/database/sqliteDb.ts +46 -0
- package/database/table/deleteTable.ts +120 -0
- package/database/tenantPlacement.ts +57 -0
- package/database/tombstones.ts +52 -0
- package/database/userDataLoadDecision.ts +17 -0
- package/database/userDataMerge.ts +95 -0
- package/database/userPreferenceRegister.ts +108 -0
- package/database/utils/dbPath.ts +47 -0
- package/database/utils/ulid.native.ts +6 -0
- package/database/utils/ulid.ts +1 -0
- package/index.ts +37 -19
- package/localRuntimeDb.ts +28 -0
- package/package.json +17 -4
- package/runtimeModeArgs.ts +33 -0
- package/tui/readlineWorkspace.ts +1 -0
- package/tui/session.ts +22 -0
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
// 文件路径: packages/database/server/read.ts
|
|
2
|
+
|
|
3
|
+
import serverDb, { ensureServerDbOpen } from "./db";
|
|
4
|
+
import { DB_PREFIX, shareKey } from "database/keys";
|
|
5
|
+
import { parseToken, verifyToken } from "auth/token";
|
|
6
|
+
import { assertSessionTokenVersion } from "auth/sessionTokenVersion";
|
|
7
|
+
import { isTombstoneRecord } from "database/tombstones";
|
|
8
|
+
import { userActor } from "auth/actor";
|
|
9
|
+
import { authorizeActorRecordAccess } from "./actorAccess";
|
|
10
|
+
|
|
11
|
+
const isLevelNotFoundError = (error: any): boolean => {
|
|
12
|
+
const code = error?.code;
|
|
13
|
+
return (
|
|
14
|
+
error?.notFound === true ||
|
|
15
|
+
error?.name === "NotFoundError" ||
|
|
16
|
+
code === "LEVEL_NOT_FOUND" ||
|
|
17
|
+
code === "LEVEL_NOT_FOUND_ERROR"
|
|
18
|
+
);
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const PROFILE_KEY_SUFFIX = "-profile";
|
|
22
|
+
|
|
23
|
+
const isPublicProfileKey = (dbKey: string): boolean =>
|
|
24
|
+
dbKey.endsWith(PROFILE_KEY_SUFFIX) && dbKey.length > PROFILE_KEY_SUFFIX.length;
|
|
25
|
+
|
|
26
|
+
const isPublicAgentKey = (dbKey: string): boolean =>
|
|
27
|
+
dbKey.startsWith("agent-pub-") || dbKey.startsWith("cybot-pub-");
|
|
28
|
+
|
|
29
|
+
const getUserIdFromProfileKey = (dbKey: string): string =>
|
|
30
|
+
dbKey.slice(0, -PROFILE_KEY_SUFFIX.length);
|
|
31
|
+
|
|
32
|
+
const getAuthorizedUserId = async (req: any): Promise<string | null> => {
|
|
33
|
+
const authHeader = req.headers?.get?.("authorization");
|
|
34
|
+
const token = typeof authHeader === "string" ? authHeader.split(" ")[1] : "";
|
|
35
|
+
if (!token) return null;
|
|
36
|
+
|
|
37
|
+
const parsed = parseToken(token);
|
|
38
|
+
const userId = parsed?.userId;
|
|
39
|
+
if (!userId || typeof userId !== "string") return null;
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
const user = await serverDb.get(`${DB_PREFIX.USER}${userId}`);
|
|
43
|
+
if (!user?.publicKey || user?.isDisabled) return null;
|
|
44
|
+
|
|
45
|
+
const payload = verifyToken(token, user.publicKey);
|
|
46
|
+
assertSessionTokenVersion(payload, user);
|
|
47
|
+
if (!payload || payload.userId !== userId) return null;
|
|
48
|
+
|
|
49
|
+
const now = Date.now();
|
|
50
|
+
if (payload.exp != null) {
|
|
51
|
+
const expMs =
|
|
52
|
+
typeof payload.exp === "number"
|
|
53
|
+
? payload.exp * 1000
|
|
54
|
+
: new Date(payload.exp).getTime();
|
|
55
|
+
if (Number.isFinite(expMs) && now > expMs) return null;
|
|
56
|
+
}
|
|
57
|
+
if (payload.nbf != null) {
|
|
58
|
+
const nbfMs =
|
|
59
|
+
typeof payload.nbf === "number"
|
|
60
|
+
? payload.nbf * 1000
|
|
61
|
+
: new Date(payload.nbf).getTime();
|
|
62
|
+
if (Number.isFinite(nbfMs) && now < nbfMs) return null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return userId;
|
|
66
|
+
} catch {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const purchaseRecordKey = (buyerId: string, shareToken: string) =>
|
|
72
|
+
`doc-purchase-${buyerId}-${shareToken}`;
|
|
73
|
+
|
|
74
|
+
const extractShareTokenFromDbKey = (dbKey: string): string | null =>
|
|
75
|
+
dbKey.startsWith("share-") ? dbKey.slice("share-".length) : null;
|
|
76
|
+
|
|
77
|
+
const isCommunityShare = (share: any): boolean =>
|
|
78
|
+
share?.meta?.visibility === "community";
|
|
79
|
+
|
|
80
|
+
const isShareOwner = (share: any, userId: string | null): boolean =>
|
|
81
|
+
Boolean(userId && share?.meta?.authorId === userId);
|
|
82
|
+
|
|
83
|
+
const isPaidShare = (share: any): boolean =>
|
|
84
|
+
typeof share?.meta?.price === "number" && share.meta.price > 0;
|
|
85
|
+
|
|
86
|
+
const redactSharePayload = (share: any) => ({
|
|
87
|
+
...share,
|
|
88
|
+
data: {},
|
|
89
|
+
meta: {
|
|
90
|
+
...(share?.meta ?? {}),
|
|
91
|
+
previewLocked: true,
|
|
92
|
+
requiresPurchase: true,
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
const hasPurchasedShare = async (
|
|
97
|
+
userId: string | null,
|
|
98
|
+
shareToken: string | null
|
|
99
|
+
): Promise<boolean> => {
|
|
100
|
+
if (!userId || !shareToken) return false;
|
|
101
|
+
try {
|
|
102
|
+
await serverDb.get(purchaseRecordKey(userId, shareToken));
|
|
103
|
+
return true;
|
|
104
|
+
} catch {
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const toPublicProfile = (profile: any, user: any) => ({
|
|
110
|
+
nickname:
|
|
111
|
+
typeof profile?.nickname === "string" && profile.nickname.trim()
|
|
112
|
+
? profile.nickname
|
|
113
|
+
: typeof user?.username === "string"
|
|
114
|
+
? user.username
|
|
115
|
+
: "",
|
|
116
|
+
username:
|
|
117
|
+
typeof user?.username === "string"
|
|
118
|
+
? user.username
|
|
119
|
+
: typeof profile?.username === "string"
|
|
120
|
+
? profile.username
|
|
121
|
+
: "",
|
|
122
|
+
avatar: typeof profile?.avatar === "string" ? profile.avatar : "",
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
const toPublicAgent = (agent: any) => {
|
|
126
|
+
const { apiKey, customProviderUrl, endpointKey, ...publicAgent } = agent ?? {};
|
|
127
|
+
return publicAgent;
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const isAgentOwner = (agent: any, userId: string | null): boolean =>
|
|
131
|
+
Boolean(
|
|
132
|
+
userId &&
|
|
133
|
+
(agent?.ownerId === userId ||
|
|
134
|
+
agent?.userId === userId ||
|
|
135
|
+
agent?.tenantId === userId)
|
|
136
|
+
);
|
|
137
|
+
export const handleReadSingle = async (req: any) => {
|
|
138
|
+
const corsHeaders = {
|
|
139
|
+
"Access-Control-Allow-Origin": "*",
|
|
140
|
+
"Content-Type": "application/json",
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
if (!req.params?.id) {
|
|
144
|
+
return new Response(JSON.stringify({ error: "need id" }), {
|
|
145
|
+
status: 400,
|
|
146
|
+
headers: corsHeaders,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const dbKey = req.params.id;
|
|
151
|
+
const isShareDataKey = dbKey.startsWith("share-") || shareKey.isShareKey(dbKey);
|
|
152
|
+
|
|
153
|
+
try {
|
|
154
|
+
// ⭐ 确保 LevelDB 已打开,避免 LEVEL_DATABASE_NOT_OPEN
|
|
155
|
+
await ensureServerDbOpen();
|
|
156
|
+
|
|
157
|
+
const authedUserId = await getAuthorizedUserId(req);
|
|
158
|
+
const isPublicAgentDataKey = isPublicAgentKey(dbKey);
|
|
159
|
+
// NOTE: `space.visibility = "public"` is currently a product/discovery signal for
|
|
160
|
+
// logged-in flows, but it does not automatically make raw `/api/v1/db/read/:id`
|
|
161
|
+
// anonymous-readable. Today only public profiles and community shares bypass auth
|
|
162
|
+
// here. If we later want guest-readable public spaces / skill pages, that needs an
|
|
163
|
+
// explicit allowlist rule in this handler instead of assuming space visibility is enough.
|
|
164
|
+
if (!authedUserId && !isPublicProfileKey(dbKey) && !isShareDataKey && !isPublicAgentDataKey) {
|
|
165
|
+
return new Response(
|
|
166
|
+
JSON.stringify({
|
|
167
|
+
error: "Unauthorized",
|
|
168
|
+
message: "Authentication required",
|
|
169
|
+
}),
|
|
170
|
+
{ status: 401, headers: corsHeaders }
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (!authedUserId && isPublicProfileKey(dbKey)) {
|
|
175
|
+
const inviterId = getUserIdFromProfileKey(dbKey);
|
|
176
|
+
|
|
177
|
+
let profile: any = null;
|
|
178
|
+
try {
|
|
179
|
+
profile = await serverDb.get(dbKey);
|
|
180
|
+
} catch (error: any) {
|
|
181
|
+
if (!isLevelNotFoundError(error)) throw error;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
let user: any = null;
|
|
185
|
+
try {
|
|
186
|
+
user = await serverDb.get(`${DB_PREFIX.USER}${inviterId}`);
|
|
187
|
+
} catch (error: any) {
|
|
188
|
+
if (!isLevelNotFoundError(error)) throw error;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (user?.isDisabled) {
|
|
192
|
+
return new Response(
|
|
193
|
+
JSON.stringify({
|
|
194
|
+
error: "Not Found",
|
|
195
|
+
message: `Resource with id ${dbKey} not found`,
|
|
196
|
+
}),
|
|
197
|
+
{ status: 404, headers: corsHeaders }
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (!profile && !user) {
|
|
202
|
+
return new Response(
|
|
203
|
+
JSON.stringify({
|
|
204
|
+
error: "Not Found",
|
|
205
|
+
message: `Resource with id ${dbKey} not found`,
|
|
206
|
+
}),
|
|
207
|
+
{ status: 404, headers: corsHeaders }
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return new Response(JSON.stringify(toPublicProfile(profile, user)), {
|
|
212
|
+
status: 200,
|
|
213
|
+
headers: corsHeaders,
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const result = await serverDb.get(dbKey);
|
|
218
|
+
if (
|
|
219
|
+
result === null ||
|
|
220
|
+
typeof result === "undefined" ||
|
|
221
|
+
isTombstoneRecord(result)
|
|
222
|
+
) {
|
|
223
|
+
return new Response(
|
|
224
|
+
JSON.stringify({
|
|
225
|
+
error: "Not Found",
|
|
226
|
+
message: `Resource with id ${dbKey} not found`,
|
|
227
|
+
}),
|
|
228
|
+
{ status: 404, headers: corsHeaders }
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (!authedUserId && isPublicAgentDataKey && result?.isPublic !== true) {
|
|
233
|
+
return new Response(
|
|
234
|
+
JSON.stringify({
|
|
235
|
+
error: "Not Found",
|
|
236
|
+
message: `Resource with id ${dbKey} not found`,
|
|
237
|
+
}),
|
|
238
|
+
{ status: 404, headers: corsHeaders }
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (isShareDataKey) {
|
|
243
|
+
const owner = isShareOwner(result, authedUserId);
|
|
244
|
+
if (!isCommunityShare(result) && !owner) {
|
|
245
|
+
return new Response(
|
|
246
|
+
JSON.stringify({
|
|
247
|
+
error: "Not Found",
|
|
248
|
+
message: `Resource with id ${dbKey} not found`,
|
|
249
|
+
}),
|
|
250
|
+
{ status: 404, headers: corsHeaders }
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (isPaidShare(result) && !owner) {
|
|
255
|
+
const shareToken = extractShareTokenFromDbKey(dbKey);
|
|
256
|
+
const purchased = await hasPurchasedShare(authedUserId, shareToken);
|
|
257
|
+
if (!purchased) {
|
|
258
|
+
return new Response(JSON.stringify(redactSharePayload(result)), {
|
|
259
|
+
status: 200,
|
|
260
|
+
headers: corsHeaders,
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (!authedUserId && isPublicAgentDataKey) {
|
|
267
|
+
return new Response(JSON.stringify(toPublicAgent(result)), {
|
|
268
|
+
status: 200,
|
|
269
|
+
headers: corsHeaders,
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (!authedUserId && isShareDataKey && isCommunityShare(result)) {
|
|
274
|
+
return new Response(JSON.stringify({ ...result }), {
|
|
275
|
+
status: 200,
|
|
276
|
+
headers: corsHeaders,
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const access = await authorizeActorRecordAccess({
|
|
281
|
+
action: "read",
|
|
282
|
+
actor: userActor(authedUserId),
|
|
283
|
+
dbKey,
|
|
284
|
+
record: result,
|
|
285
|
+
});
|
|
286
|
+
if (!access.allowed) {
|
|
287
|
+
return new Response(
|
|
288
|
+
JSON.stringify({
|
|
289
|
+
error: "Forbidden",
|
|
290
|
+
message: "You do not have access to this resource",
|
|
291
|
+
}),
|
|
292
|
+
{ status: 403, headers: corsHeaders }
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (isPublicAgentDataKey && !isAgentOwner(result, authedUserId)) {
|
|
297
|
+
return new Response(JSON.stringify(toPublicAgent(result)), {
|
|
298
|
+
status: 200,
|
|
299
|
+
headers: corsHeaders,
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
return new Response(JSON.stringify({ ...result }), {
|
|
303
|
+
status: 200,
|
|
304
|
+
headers: corsHeaders,
|
|
305
|
+
});
|
|
306
|
+
} catch (error: any) {
|
|
307
|
+
if (isLevelNotFoundError(error)) {
|
|
308
|
+
return new Response(
|
|
309
|
+
JSON.stringify({
|
|
310
|
+
error: "Not Found",
|
|
311
|
+
message: `Resource with id ${dbKey} not found`,
|
|
312
|
+
}),
|
|
313
|
+
{ status: 404, headers: corsHeaders }
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
console.error("Database fetch error:", error);
|
|
317
|
+
return new Response(
|
|
318
|
+
JSON.stringify({
|
|
319
|
+
error: "Internal Server Error",
|
|
320
|
+
message: "Failed to fetch data",
|
|
321
|
+
}),
|
|
322
|
+
{ status: 500, headers: corsHeaders }
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
};
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { isSystemAdmin } from "core/init";
|
|
2
|
+
import { shareKey } from "database/keys";
|
|
3
|
+
import serverDb from "./db";
|
|
4
|
+
import { resolveKeyOwnerId } from "./writeAuthority";
|
|
5
|
+
|
|
6
|
+
export type AccessAction = "read" | "write" | "delete" | "manage";
|
|
7
|
+
|
|
8
|
+
type AccessInput = {
|
|
9
|
+
action: AccessAction;
|
|
10
|
+
actionUserId?: string | null;
|
|
11
|
+
dbKey: string;
|
|
12
|
+
record?: any;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export type AccessResult = {
|
|
16
|
+
allowed: boolean;
|
|
17
|
+
reason?: string;
|
|
18
|
+
keyOwnerId?: string | null;
|
|
19
|
+
recordOwnerId?: string | null;
|
|
20
|
+
spaceId?: string | null;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const SPACE_PREFIX = "space-";
|
|
24
|
+
const SPACE_MEMBER_PREFIX = "space-member-";
|
|
25
|
+
|
|
26
|
+
const normalizeSpaceId = (spaceId?: unknown): string | null => {
|
|
27
|
+
if (typeof spaceId !== "string") return null;
|
|
28
|
+
const trimmed = spaceId.trim();
|
|
29
|
+
if (!trimmed) return null;
|
|
30
|
+
return trimmed.startsWith(SPACE_PREFIX)
|
|
31
|
+
? trimmed.slice(SPACE_PREFIX.length)
|
|
32
|
+
: trimmed;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const isShareRecordKey = (dbKey: unknown): boolean => {
|
|
36
|
+
const value = String(dbKey || "");
|
|
37
|
+
return value.startsWith("share-") || shareKey.isShareKey(value);
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const getRecordOwnerId = (record: any): string | null =>
|
|
41
|
+
(typeof record?.ownerId === "string" && record.ownerId) ||
|
|
42
|
+
(typeof record?.userId === "string" && record.userId) ||
|
|
43
|
+
(typeof record?.tenantId === "string" && record.tenantId) ||
|
|
44
|
+
(isShareRecordKey(record?.dbKey) && typeof record?.meta?.authorId === "string"
|
|
45
|
+
? record.meta.authorId
|
|
46
|
+
: null) ||
|
|
47
|
+
null;
|
|
48
|
+
|
|
49
|
+
const getSpaceIdFromRecord = (record: any): string | null =>
|
|
50
|
+
normalizeSpaceId(record?.spaceId);
|
|
51
|
+
|
|
52
|
+
const getSpaceIdFromKey = (dbKey: string): string | null => {
|
|
53
|
+
if (!dbKey.startsWith(SPACE_PREFIX) || dbKey.startsWith(SPACE_MEMBER_PREFIX)) {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
return normalizeSpaceId(dbKey);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const isAgentOwnedByUser = (agentKey: unknown, userId: string): boolean =>
|
|
60
|
+
typeof agentKey === "string" &&
|
|
61
|
+
Boolean(userId) &&
|
|
62
|
+
resolveKeyOwnerId(agentKey) === userId;
|
|
63
|
+
|
|
64
|
+
const isPublicAgentRecord = (dbKey: string, record: any): boolean =>
|
|
65
|
+
(dbKey.startsWith("agent-pub-") || dbKey.startsWith("cybot-pub-")) &&
|
|
66
|
+
record?.isPublic === true;
|
|
67
|
+
|
|
68
|
+
const isCommunityShareRecord = (dbKey: string, record: any): boolean =>
|
|
69
|
+
(dbKey.startsWith("share-") || shareKey.isShareKey(dbKey)) &&
|
|
70
|
+
record?.meta?.visibility === "community";
|
|
71
|
+
|
|
72
|
+
const hasSpaceMembership = async (
|
|
73
|
+
userId: string,
|
|
74
|
+
spaceId: string | null
|
|
75
|
+
): Promise<boolean> => {
|
|
76
|
+
const normalized = normalizeSpaceId(spaceId);
|
|
77
|
+
if (!userId || !normalized) return false;
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
const membership = await serverDb.get(
|
|
81
|
+
`${SPACE_MEMBER_PREFIX}${userId}-${normalized}`
|
|
82
|
+
);
|
|
83
|
+
if (membership) return true;
|
|
84
|
+
} catch {
|
|
85
|
+
// Fall through to the space record. Some older data only has members[].
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
const space = await serverDb.get(`${SPACE_PREFIX}${normalized}`);
|
|
90
|
+
return (
|
|
91
|
+
space?.ownerId === userId ||
|
|
92
|
+
(Array.isArray(space?.members) && space.members.includes(userId))
|
|
93
|
+
);
|
|
94
|
+
} catch {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const getContentKeyCandidates = (dbKey: string, record: any): string[] => {
|
|
100
|
+
const candidates = [
|
|
101
|
+
dbKey,
|
|
102
|
+
typeof record?.dbKey === "string" ? record.dbKey : "",
|
|
103
|
+
typeof record?.contentKey === "string" ? record.contentKey : "",
|
|
104
|
+
typeof record?.key === "string" ? record.key : "",
|
|
105
|
+
].filter(Boolean);
|
|
106
|
+
|
|
107
|
+
return [...new Set(candidates)];
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const isRecordLinkedFromMemberSpace = async (
|
|
111
|
+
userId: string,
|
|
112
|
+
dbKey: string,
|
|
113
|
+
record: any
|
|
114
|
+
): Promise<string | null> => {
|
|
115
|
+
if (!userId || !dbKey) return null;
|
|
116
|
+
|
|
117
|
+
const contentKeys = getContentKeyCandidates(dbKey, record);
|
|
118
|
+
const prefix = `${SPACE_MEMBER_PREFIX}${userId}-`;
|
|
119
|
+
|
|
120
|
+
try {
|
|
121
|
+
for await (const [, membership] of serverDb.iterator({
|
|
122
|
+
gte: prefix,
|
|
123
|
+
lte: prefix + "\uffff",
|
|
124
|
+
})) {
|
|
125
|
+
const spaceId = normalizeSpaceId(membership?.spaceId);
|
|
126
|
+
if (!spaceId) continue;
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
const space = await serverDb.get(`${SPACE_PREFIX}${spaceId}`);
|
|
130
|
+
const contents = space?.contents;
|
|
131
|
+
if (!contents || typeof contents !== "object") continue;
|
|
132
|
+
|
|
133
|
+
if (contentKeys.some((contentKey) => Boolean(contents[contentKey]))) {
|
|
134
|
+
return spaceId;
|
|
135
|
+
}
|
|
136
|
+
} catch {
|
|
137
|
+
// Ignore stale membership pointers.
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
} catch {
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return null;
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
export async function authorizeRecordAccess({
|
|
148
|
+
action,
|
|
149
|
+
actionUserId,
|
|
150
|
+
dbKey,
|
|
151
|
+
record,
|
|
152
|
+
}: AccessInput): Promise<AccessResult> {
|
|
153
|
+
if (!actionUserId) {
|
|
154
|
+
return { allowed: false, reason: "missing_actor" };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (isSystemAdmin(actionUserId)) {
|
|
158
|
+
return { allowed: true };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (action === "read") {
|
|
162
|
+
if (isPublicAgentRecord(dbKey, record)) return { allowed: true };
|
|
163
|
+
if (isCommunityShareRecord(dbKey, record)) return { allowed: true };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const keyOwnerId = resolveKeyOwnerId(dbKey);
|
|
167
|
+
const recordOwnerId = getRecordOwnerId({ ...record, dbKey });
|
|
168
|
+
const spaceId = getSpaceIdFromRecord(record) ?? getSpaceIdFromKey(dbKey);
|
|
169
|
+
|
|
170
|
+
if (keyOwnerId && recordOwnerId && keyOwnerId !== recordOwnerId) {
|
|
171
|
+
return {
|
|
172
|
+
allowed: false,
|
|
173
|
+
reason: "owner_mismatch",
|
|
174
|
+
keyOwnerId,
|
|
175
|
+
recordOwnerId,
|
|
176
|
+
spaceId,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (keyOwnerId === actionUserId || recordOwnerId === actionUserId) {
|
|
181
|
+
return { allowed: true, keyOwnerId, recordOwnerId, spaceId };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (
|
|
185
|
+
record?.ownerType === "agent" &&
|
|
186
|
+
isAgentOwnedByUser(recordOwnerId, actionUserId)
|
|
187
|
+
) {
|
|
188
|
+
return { allowed: true, keyOwnerId, recordOwnerId, spaceId };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (await hasSpaceMembership(actionUserId, spaceId)) {
|
|
192
|
+
return { allowed: true, keyOwnerId, recordOwnerId, spaceId };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const linkedSpaceId = await isRecordLinkedFromMemberSpace(
|
|
196
|
+
actionUserId,
|
|
197
|
+
dbKey,
|
|
198
|
+
record
|
|
199
|
+
);
|
|
200
|
+
if (linkedSpaceId) {
|
|
201
|
+
return { allowed: true, keyOwnerId, recordOwnerId, spaceId: linkedSpaceId };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return {
|
|
205
|
+
allowed: false,
|
|
206
|
+
reason: "not_owner_or_member",
|
|
207
|
+
keyOwnerId,
|
|
208
|
+
recordOwnerId,
|
|
209
|
+
spaceId,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { API_ENDPOINTS } from "../config";
|
|
2
|
+
import { handleQuery } from "./query";
|
|
3
|
+
import { handleWrite } from "./write";
|
|
4
|
+
import { handlePatch } from "./patch";
|
|
5
|
+
import { maybeProxyDatabaseRequestToCore } from "./coreDataProxy";
|
|
6
|
+
import { createPageServer } from "render/page/server/createPage";
|
|
7
|
+
import { handleToken } from "auth/server/token";
|
|
8
|
+
import { logger } from "auth/server/shared";
|
|
9
|
+
|
|
10
|
+
export const databaseRequest = async (req, res, url) => {
|
|
11
|
+
const pathname = url.pathname;
|
|
12
|
+
const getIdFromPath = (prefix) => {
|
|
13
|
+
const start = pathname.indexOf(prefix) + prefix.length;
|
|
14
|
+
const end =
|
|
15
|
+
pathname.indexOf("/", start) !== -1
|
|
16
|
+
? pathname.indexOf("/", start)
|
|
17
|
+
: undefined;
|
|
18
|
+
return pathname.slice(start, end);
|
|
19
|
+
};
|
|
20
|
+
if (pathname.startsWith(API_ENDPOINTS.DATABASE)) {
|
|
21
|
+
const operation = pathname
|
|
22
|
+
.substr(API_ENDPOINTS.DATABASE.length)
|
|
23
|
+
.split("/")[1];
|
|
24
|
+
|
|
25
|
+
switch (operation) {
|
|
26
|
+
case "write": {
|
|
27
|
+
const proxied = await maybeProxyDatabaseRequestToCore({
|
|
28
|
+
req,
|
|
29
|
+
pathname,
|
|
30
|
+
search: url.search,
|
|
31
|
+
coreBaseUrl: process.env.NOLO_SERVER_CORE_BASE_URL,
|
|
32
|
+
});
|
|
33
|
+
if (proxied) {
|
|
34
|
+
return proxied;
|
|
35
|
+
}
|
|
36
|
+
return handleWrite(req, res);
|
|
37
|
+
}
|
|
38
|
+
case "patch": {
|
|
39
|
+
const proxied = await maybeProxyDatabaseRequestToCore({
|
|
40
|
+
req,
|
|
41
|
+
pathname,
|
|
42
|
+
search: url.search,
|
|
43
|
+
coreBaseUrl: process.env.NOLO_SERVER_CORE_BASE_URL,
|
|
44
|
+
});
|
|
45
|
+
if (proxied) {
|
|
46
|
+
return proxied;
|
|
47
|
+
}
|
|
48
|
+
req.params = { id: getIdFromPath("/api/v1/db/patch/") };
|
|
49
|
+
return handlePatch(req, res);
|
|
50
|
+
}
|
|
51
|
+
case "query":
|
|
52
|
+
req.params = { userId: getIdFromPath("/api/v1/db/query/") };
|
|
53
|
+
return handleQuery(req, res);
|
|
54
|
+
case "page": {
|
|
55
|
+
// /api/v1/db/page/create
|
|
56
|
+
const subOperation = pathname.split("/")[5]; // page 后面的操作
|
|
57
|
+
if (subOperation === "create") {
|
|
58
|
+
try {
|
|
59
|
+
// 鉴权
|
|
60
|
+
const user = await handleToken(req, res);
|
|
61
|
+
const userId = user?.userId;
|
|
62
|
+
if (!userId) {
|
|
63
|
+
return res.status(401).json({
|
|
64
|
+
success: false,
|
|
65
|
+
message: "Unauthorized: userId not found",
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const { title, content, slateData, spaceId, categoryId, tags } =
|
|
70
|
+
req.body || {};
|
|
71
|
+
|
|
72
|
+
const result = await createPageServer({
|
|
73
|
+
userId,
|
|
74
|
+
title,
|
|
75
|
+
content,
|
|
76
|
+
slateData,
|
|
77
|
+
spaceId,
|
|
78
|
+
categoryId,
|
|
79
|
+
tags,
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
logger.info({
|
|
83
|
+
event: "page_created",
|
|
84
|
+
userId,
|
|
85
|
+
dbKey: result.dbKey,
|
|
86
|
+
title: result.title,
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
return res.status(200).json(result);
|
|
90
|
+
} catch (error: any) {
|
|
91
|
+
logger.error({
|
|
92
|
+
event: "create_page_failed",
|
|
93
|
+
error: error.message,
|
|
94
|
+
});
|
|
95
|
+
return res.status(500).json({
|
|
96
|
+
success: false,
|
|
97
|
+
message: error.message || "Failed to create page",
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return new Response("Unknown page operation", { status: 404 });
|
|
102
|
+
}
|
|
103
|
+
default:
|
|
104
|
+
return new Response(
|
|
105
|
+
JSON.stringify({ error: "Not Found", message: `Unknown database operation: ${operation}` }),
|
|
106
|
+
{ status: 404, headers: { "Content-Type": "application/json" } }
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import serverDb from "./db";
|
|
2
|
+
|
|
3
|
+
const SPACE_MEMBER_PREFIX = "space-member-";
|
|
4
|
+
const SPACE_PREFIX = "space-";
|
|
5
|
+
|
|
6
|
+
export const parseSpaceMemberKey = (
|
|
7
|
+
dbKey: string
|
|
8
|
+
): { memberUserId: string; spaceId: string } | null => {
|
|
9
|
+
if (typeof dbKey !== "string" || !dbKey.startsWith(SPACE_MEMBER_PREFIX)) {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const rest = dbKey.slice(SPACE_MEMBER_PREFIX.length);
|
|
14
|
+
const lastDash = rest.lastIndexOf("-");
|
|
15
|
+
if (lastDash <= 0 || lastDash === rest.length - 1) {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return {
|
|
20
|
+
memberUserId: rest.slice(0, lastDash),
|
|
21
|
+
spaceId: rest.slice(lastDash + 1),
|
|
22
|
+
};
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const loadSpaceRecord = async (spaceId: string): Promise<any | null> => {
|
|
26
|
+
try {
|
|
27
|
+
return await serverDb.get(`${SPACE_PREFIX}${spaceId}`);
|
|
28
|
+
} catch {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export const canWriteSpaceMemberRecord = async ({
|
|
34
|
+
dbKey,
|
|
35
|
+
actionUserId,
|
|
36
|
+
}: {
|
|
37
|
+
dbKey: string;
|
|
38
|
+
actionUserId?: string | null;
|
|
39
|
+
}): Promise<boolean> => {
|
|
40
|
+
const parsed = parseSpaceMemberKey(dbKey);
|
|
41
|
+
if (!parsed || !actionUserId) {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const space = await loadSpaceRecord(parsed.spaceId);
|
|
46
|
+
return Array.isArray(space?.members) && space.members.includes(actionUserId);
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export const canDeleteSpaceMemberRecord = async ({
|
|
50
|
+
dbKey,
|
|
51
|
+
actionUserId,
|
|
52
|
+
}: {
|
|
53
|
+
dbKey: string;
|
|
54
|
+
actionUserId?: string | null;
|
|
55
|
+
}): Promise<boolean> => {
|
|
56
|
+
const parsed = parseSpaceMemberKey(dbKey);
|
|
57
|
+
if (!parsed || !actionUserId) {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (parsed.memberUserId === actionUserId) {
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const space = await loadSpaceRecord(parsed.spaceId);
|
|
66
|
+
return typeof space?.ownerId === "string" && space.ownerId === actionUserId;
|
|
67
|
+
};
|