@promptbook/cli 0.113.0-4 → 0.113.0-8
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/apps/agents-server/src/app/admin/update/AdvancedOriginRepositoryPanel.tsx +76 -0
- package/apps/agents-server/src/app/admin/update/CurrentDeploymentCard.tsx +160 -0
- package/apps/agents-server/src/app/admin/update/PendingCommitsCard.tsx +86 -0
- package/apps/agents-server/src/app/admin/update/TargetEnvironmentCard.tsx +331 -0
- package/apps/agents-server/src/app/admin/update/UpdateClient.tsx +20 -873
- package/apps/agents-server/src/app/admin/update/UpdateJobCard.tsx +329 -0
- package/apps/agents-server/src/app/admin/update/UpdateOverview.ts +74 -0
- package/apps/agents-server/src/app/admin/update/buildDeploymentTimeBehindLabel.ts +29 -0
- package/apps/agents-server/src/app/admin/update/formatHumanReadableTimestamp.ts +18 -0
- package/apps/agents-server/src/app/admin/update/getUpdateJobFailureMessage.ts +13 -0
- package/apps/agents-server/src/app/admin/update/getUpdateJobSuccessMessage.ts +13 -0
- package/apps/agents-server/src/app/admin/update/useUpdateClientState.ts +441 -0
- package/apps/agents-server/src/app/agents/[agentName]/api/book/route.ts +13 -1
- package/apps/agents-server/src/app/agents/[agentName]/export-as-transpiled-code/AgentCodePageClient.tsx +14 -559
- package/apps/agents-server/src/app/agents/[agentName]/export-as-transpiled-code/AgentCodePageHeader.tsx +84 -0
- package/apps/agents-server/src/app/agents/[agentName]/export-as-transpiled-code/AgentCodePageSection.tsx +52 -0
- package/apps/agents-server/src/app/agents/[agentName]/export-as-transpiled-code/GeneratedCodePreview.tsx +138 -0
- package/apps/agents-server/src/app/agents/[agentName]/export-as-transpiled-code/GeneratedHarnessSection.tsx +98 -0
- package/apps/agents-server/src/app/agents/[agentName]/export-as-transpiled-code/SourceBookSection.tsx +66 -0
- package/apps/agents-server/src/app/agents/[agentName]/export-as-transpiled-code/TranspiledCodeErrorBanner.tsx +30 -0
- package/apps/agents-server/src/app/agents/[agentName]/export-as-transpiled-code/TranspiledCodeExportWarningBanner.tsx +62 -0
- package/apps/agents-server/src/app/agents/[agentName]/export-as-transpiled-code/Transpiler.ts +9 -0
- package/apps/agents-server/src/app/agents/[agentName]/export-as-transpiled-code/useAgentCodeExportState.ts +478 -0
- package/apps/agents-server/src/app/system/utilities/mocked-chats/cloneMockedChatPreset.ts +17 -0
- package/apps/agents-server/src/app/system/utilities/mocked-chats/confirmDeleteMockedChat.ts +16 -0
- package/apps/agents-server/src/app/system/utilities/mocked-chats/createDraftWithUpdatedMessage.ts +81 -0
- package/apps/agents-server/src/app/system/utilities/mocked-chats/createDraftWithUpdatedParticipant.ts +146 -0
- package/apps/agents-server/src/app/system/utilities/mocked-chats/createDraftWithUpdatedSettings.ts +19 -0
- package/apps/agents-server/src/app/system/utilities/mocked-chats/createDuplicatedDraft.ts +66 -0
- package/apps/agents-server/src/app/system/utilities/mocked-chats/findMockedChatById.ts +51 -0
- package/apps/agents-server/src/app/system/utilities/mocked-chats/persistMockedChats.ts +51 -0
- package/apps/agents-server/src/app/system/utilities/mocked-chats/useMockedChatsEditorState.ts +16 -426
- package/apps/agents-server/src/components/Homepage/buildFreeGraphBoxLayout.ts +446 -0
- package/apps/agents-server/src/components/Homepage/buildGraphLayoutNodes.ts +415 -157
- package/apps/agents-server/src/components/Homepage/useAgentsGraphCanvasState.ts +2 -1
- package/apps/agents-server/src/database/sqlite/$provideLocalSqliteSupabase.ts +3 -1541
- package/apps/agents-server/src/database/sqlite/localSqliteSupabase/LocalSqliteQueryBuilder.ts +623 -0
- package/apps/agents-server/src/database/sqlite/localSqliteSupabase/LocalSqliteSupabaseClient.ts +64 -0
- package/apps/agents-server/src/database/sqlite/localSqliteSupabase/ensureTable.ts +93 -0
- package/apps/agents-server/src/database/sqlite/localSqliteSupabase/localSqliteFilters.ts +202 -0
- package/apps/agents-server/src/database/sqlite/localSqliteSupabase/localSqliteMutationPayload.ts +131 -0
- package/apps/agents-server/src/database/sqlite/localSqliteSupabase/localSqliteRowOperations.ts +102 -0
- package/apps/agents-server/src/database/sqlite/localSqliteSupabase/localSqliteSql.ts +59 -0
- package/apps/agents-server/src/database/sqlite/localSqliteSupabase/localSqliteTableSchema.ts +329 -0
- package/apps/agents-server/src/database/sqlite/localSqliteSupabase/localSqliteTypes.ts +84 -0
- package/apps/agents-server/src/database/sqlite/localSqliteSupabase/localSqliteValueCodec.ts +63 -0
- package/apps/agents-server/src/database/sqlite/localSqliteSupabase/normalizeSqliteError.ts +22 -0
- package/apps/agents-server/src/search/createDefaultServerSearchProviders/loadLocalOrganizationSearchDataset.ts +12 -1
- package/apps/agents-server/src/tools/send_email.ts +1 -2
- package/apps/agents-server/src/utils/agentOrganization/loadAgentOrganizationState.ts +13 -1
- package/apps/agents-server/src/utils/backup/createServerBackupZipStream.ts +7 -1382
- package/apps/agents-server/src/utils/backup/serverBackup/appendAgentBackupEntriesToZip.ts +27 -0
- package/apps/agents-server/src/utils/backup/serverBackup/appendConversationBackupEntriesToZip.ts +140 -0
- package/apps/agents-server/src/utils/backup/serverBackup/appendFileBackupEntriesToZip.ts +147 -0
- package/apps/agents-server/src/utils/backup/serverBackup/appendMessageBackupEntriesToZip.ts +65 -0
- package/apps/agents-server/src/utils/backup/serverBackup/appendMetadataBackupEntriesToZip.ts +39 -0
- package/apps/agents-server/src/utils/backup/serverBackup/appendSectionEntriesToZip.ts +92 -0
- package/apps/agents-server/src/utils/backup/serverBackup/appendUserBackupEntriesToZip.ts +80 -0
- package/apps/agents-server/src/utils/backup/serverBackup/createRedactedWalletBackupRecord.ts +34 -0
- package/apps/agents-server/src/utils/backup/serverBackup/createServerBackupManifest.ts +82 -0
- package/apps/agents-server/src/utils/backup/serverBackup/downloadBackupBinaryContent.ts +39 -0
- package/apps/agents-server/src/utils/backup/serverBackup/resolveFeedbackThreadMessages.ts +53 -0
- package/apps/agents-server/src/utils/backup/serverBackup/serverBackupAttachments.ts +151 -0
- package/apps/agents-server/src/utils/backup/serverBackup/serverBackupContext.ts +167 -0
- package/apps/agents-server/src/utils/backup/serverBackup/serverBackupFilenames.ts +161 -0
- package/apps/agents-server/src/utils/backup/serverBackup/serverBackupPreviews.ts +97 -0
- package/apps/agents-server/src/utils/backup/serverBackup/serverBackupRowUtilities.ts +158 -0
- package/apps/agents-server/src/utils/backup/serverBackup/serverBackupTypes.ts +59 -0
- package/apps/agents-server/src/utils/createLocalAgentSourceImporter.ts +159 -0
- package/apps/agents-server/src/utils/createMissingImportedAgentFallback.ts +60 -0
- package/apps/agents-server/src/utils/customDomainRouting.ts +157 -12
- package/apps/agents-server/src/utils/externalChatRunner/processExternalUserChatJob.ts +1 -1
- package/apps/agents-server/src/utils/importAgentWithFallback.ts +1 -58
- package/apps/agents-server/src/utils/localAgentRouteReferences.ts +167 -0
- package/apps/agents-server/src/utils/localChatRunner/processLocalUserChatJob.ts +1 -1
- package/apps/agents-server/src/utils/managementApi/managementApiAgents.ts +17 -3
- package/apps/agents-server/src/utils/messages/sendMessage.ts +5 -3
- package/apps/agents-server/src/utils/resolveAgentStateFromSource.ts +7 -1
- package/apps/agents-server/src/utils/resolveInheritedAgentSource.ts +54 -5
- package/apps/agents-server/src/utils/resolveServerAgentContext.ts +12 -1
- package/apps/agents-server/src/utils/resolveStoredAgentState.ts +2 -1
- package/apps/agents-server/src/utils/speech-to-text/SpeechToTextFailoverRecognition/SpeechToTextFailoverRecognitionProviderRuntime.ts +0 -1
- package/apps/agents-server/src/utils/speech-to-text/SpeechToTextFailoverRecognition.ts +34 -2
- package/apps/agents-server/src/utils/userChat/userChatMessageLifecycle.ts +39 -9
- package/apps/agents-server/tests/e2e/support/ChatHistoryNavigationSupport.ts +5 -0
- package/esm/index.es.js +875 -60
- package/esm/index.es.js.map +1 -1
- package/esm/scripts/find-refactor-candidates/find-refactor-candidates.d.ts +7 -0
- package/esm/scripts/find-refactor-candidates/selectMostImportantRefactorCandidates.d.ts +12 -0
- package/esm/scripts/run-codex-prompts/common/resolveCoderAgent.d.ts +20 -0
- package/esm/scripts/run-codex-prompts/ui/CoderRunUiState.d.ts +5 -0
- package/esm/scripts/run-codex-prompts/ui/buildCoderRunAgentVisual.d.ts +29 -0
- package/esm/scripts/run-codex-prompts/ui/buildCoderRunUiFrame.d.ts +4 -0
- package/esm/src/avatars/renderAvatarVisualAsciiArt.d.ts +75 -0
- package/esm/src/book-components/Chat/Chat/insertDictationChunk.d.ts +1 -1
- package/esm/src/book-components/Chat/Chat/insertDictationChunk.test.d.ts +1 -0
- package/esm/src/book-components/Chat/Chat/learnDictationDictionary.test.d.ts +1 -0
- package/esm/src/book-components/Chat/Chat/refineFinalDictationChunk.test.d.ts +1 -0
- package/esm/src/cli/cli-commands/common/createPositiveIntegerOptionParser.d.ts +10 -0
- package/esm/src/utils/ascii-art/$detectTerminalAnsiColorDepth.d.ts +15 -0
- package/esm/src/utils/ascii-art/convertImageDataToAsciiArt.d.ts +80 -0
- package/esm/src/version.d.ts +1 -1
- package/package.json +1 -1
- package/src/avatars/avatarAnimationScheduler.ts +2 -1
- package/src/avatars/renderAvatarVisualAsciiArt.ts +138 -0
- package/src/avatars/visuals/octopus3d3AvatarVisual.ts +6 -2
- package/src/avatars/visuals/octopus3d4AvatarVisual.ts +14 -18
- package/src/book-components/Chat/Chat/insertDictationChunk.ts +3 -3
- package/src/book-components/Chat/Chat/learnDictationDictionary.ts +1 -1
- package/src/book-components/Chat/Chat/useChatInputAreaDictation.ts +133 -26
- package/src/cli/cli-commands/agents-server/buildAgentsServer.ts +3 -1
- package/src/cli/cli-commands/coder/boilerplateTemplates.ts +14 -21
- package/src/cli/cli-commands/coder/find-refactor-candidates.ts +9 -2
- package/src/cli/cli-commands/coder/initializeCoderProjectConfiguration.ts +1 -4
- package/src/cli/cli-commands/coder/run.ts +3 -27
- package/src/cli/cli-commands/common/createPositiveIntegerOptionParser.ts +31 -0
- package/src/commitments/TEAM/TEAM.ts +5 -5
- package/src/other/templates/getTemplatesPipelineCollection.ts +770 -1169
- package/src/utils/ascii-art/$detectTerminalAnsiColorDepth.ts +45 -0
- package/src/utils/ascii-art/convertImageDataToAsciiArt.ts +396 -0
- package/src/version.ts +2 -2
- package/src/versions.txt +2 -0
- package/umd/index.umd.js +875 -60
- package/umd/index.umd.js.map +1 -1
- package/umd/scripts/find-refactor-candidates/find-refactor-candidates.d.ts +7 -0
- package/umd/scripts/find-refactor-candidates/selectMostImportantRefactorCandidates.d.ts +12 -0
- package/umd/scripts/run-codex-prompts/common/resolveCoderAgent.d.ts +20 -0
- package/umd/scripts/run-codex-prompts/ui/CoderRunUiState.d.ts +5 -0
- package/umd/scripts/run-codex-prompts/ui/buildCoderRunAgentVisual.d.ts +29 -0
- package/umd/scripts/run-codex-prompts/ui/buildCoderRunUiFrame.d.ts +4 -0
- package/umd/src/avatars/renderAvatarVisualAsciiArt.d.ts +75 -0
- package/umd/src/book-components/Chat/Chat/insertDictationChunk.d.ts +1 -1
- package/umd/src/book-components/Chat/Chat/insertDictationChunk.test.d.ts +1 -0
- package/umd/src/book-components/Chat/Chat/learnDictationDictionary.test.d.ts +1 -0
- package/umd/src/book-components/Chat/Chat/refineFinalDictationChunk.test.d.ts +1 -0
- package/umd/src/cli/cli-commands/common/createPositiveIntegerOptionParser.d.ts +10 -0
- package/umd/src/utils/ascii-art/$detectTerminalAnsiColorDepth.d.ts +15 -0
- package/umd/src/utils/ascii-art/convertImageDataToAsciiArt.d.ts +80 -0
- package/umd/src/version.d.ts +1 -1
- package/esm/scripts/run-codex-prompts/common/resolveAgentSystemMessage.d.ts +0 -6
- package/umd/scripts/run-codex-prompts/common/resolveAgentSystemMessage.d.ts +0 -6
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
import JSZip from 'jszip';
|
|
2
|
-
import { $getTableName } from '../../database/$getTableName';
|
|
3
2
|
import { $provideSupabaseForServer } from '../../database/$provideSupabaseForServer';
|
|
4
|
-
import type { AgentsServerDatabase } from '../../database/schema';
|
|
5
3
|
import { appendBooksBackupEntriesToZip, type BooksBackupZipStream } from './createBooksBackupZipStream';
|
|
6
|
-
import {
|
|
4
|
+
import { appendSectionEntriesToZip } from './serverBackup/appendSectionEntriesToZip';
|
|
5
|
+
import { createServerBackupContext } from './serverBackup/serverBackupContext';
|
|
6
|
+
import { createServerBackupManifest } from './serverBackup/createServerBackupManifest';
|
|
7
7
|
import {
|
|
8
8
|
DEFAULT_SERVER_BACKUP_SECTION_KEYS,
|
|
9
9
|
SERVER_BACKUP_SECTION_DEFINITION_BY_KEY,
|
|
10
|
-
type ServerBackupSectionDefinition,
|
|
11
|
-
type ServerBackupSectionKey,
|
|
12
10
|
normalizeServerBackupSectionKeys,
|
|
13
11
|
} from './serverBackupSections';
|
|
14
12
|
|
|
@@ -17,152 +15,6 @@ import {
|
|
|
17
15
|
*/
|
|
18
16
|
const SERVER_BACKUP_ROOT_PREFIX = 'promptbook-server-backup-';
|
|
19
17
|
|
|
20
|
-
/**
|
|
21
|
-
* Stable archive format identifier for the user-facing backup layout.
|
|
22
|
-
*/
|
|
23
|
-
const SERVER_BACKUP_FORMAT = 'promptbook-server-backup/v2';
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Filename used for the flattened metadata and limits export.
|
|
27
|
-
*/
|
|
28
|
-
const METADATA_AND_LIMITS_FILENAME = 'metadata-and-limits.json';
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Filename used when a requested section intentionally exports no data.
|
|
32
|
-
*/
|
|
33
|
-
const EXCLUDED_SECTION_FILENAME = 'excluded.json';
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* JSON file extension used by the per-entity exports.
|
|
37
|
-
*/
|
|
38
|
-
const JSON_FILE_EXTENSION = '.json';
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Typed row alias for `Agent`.
|
|
42
|
-
*/
|
|
43
|
-
type AgentRow = AgentsServerDatabase['public']['Tables']['Agent']['Row'];
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Typed row alias for `User`.
|
|
47
|
-
*/
|
|
48
|
-
type UserRow = AgentsServerDatabase['public']['Tables']['User']['Row'];
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Typed row alias for `UserChat`.
|
|
52
|
-
*/
|
|
53
|
-
type UserChatRow = AgentsServerDatabase['public']['Tables']['UserChat']['Row'];
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Typed row alias for `ChatFeedback`.
|
|
57
|
-
*/
|
|
58
|
-
type ChatFeedbackRow = AgentsServerDatabase['public']['Tables']['ChatFeedback']['Row'];
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Typed row alias for `Wallet`.
|
|
62
|
-
*/
|
|
63
|
-
type WalletRow = AgentsServerDatabase['public']['Tables']['Wallet']['Row'];
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* JSON payload persisted for legacy table-backed exports.
|
|
67
|
-
*/
|
|
68
|
-
type BackupTableFilePayload = {
|
|
69
|
-
/**
|
|
70
|
-
* Logical entity name from the generated schema types.
|
|
71
|
-
*/
|
|
72
|
-
readonly entity: keyof AgentsServerDatabase['public']['Tables'];
|
|
73
|
-
/**
|
|
74
|
-
* Physical table name used by the current server installation.
|
|
75
|
-
*/
|
|
76
|
-
readonly databaseTable: string;
|
|
77
|
-
/**
|
|
78
|
-
* Total number of exported rows.
|
|
79
|
-
*/
|
|
80
|
-
readonly rowCount: number;
|
|
81
|
-
/**
|
|
82
|
-
* Ordered snapshot rows.
|
|
83
|
-
*/
|
|
84
|
-
readonly rows: ReadonlyArray<Record<string, unknown>>;
|
|
85
|
-
};
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* High-level manifest written into every server backup ZIP.
|
|
89
|
-
*/
|
|
90
|
-
type ServerBackupManifest = {
|
|
91
|
-
/**
|
|
92
|
-
* Stable backup archive format identifier.
|
|
93
|
-
*/
|
|
94
|
-
readonly format: typeof SERVER_BACKUP_FORMAT;
|
|
95
|
-
/**
|
|
96
|
-
* ISO timestamp when the archive was generated.
|
|
97
|
-
*/
|
|
98
|
-
readonly generatedAt: string;
|
|
99
|
-
/**
|
|
100
|
-
* Whether the archive includes every default exportable section.
|
|
101
|
-
*/
|
|
102
|
-
readonly isFullBackup: boolean;
|
|
103
|
-
/**
|
|
104
|
-
* Ordered list of exported section keys.
|
|
105
|
-
*/
|
|
106
|
-
readonly selectedSections: ReadonlyArray<ServerBackupSectionKey>;
|
|
107
|
-
/**
|
|
108
|
-
* Human-readable section details for restore/debug tooling.
|
|
109
|
-
*/
|
|
110
|
-
readonly sections: ReadonlyArray<{
|
|
111
|
-
readonly key: ServerBackupSectionKey;
|
|
112
|
-
readonly label: string;
|
|
113
|
-
readonly description: string;
|
|
114
|
-
readonly directoryName: string;
|
|
115
|
-
readonly selectionKind: ServerBackupSectionDefinition['selectionKind'];
|
|
116
|
-
readonly includesBooks: boolean;
|
|
117
|
-
}>;
|
|
118
|
-
};
|
|
119
|
-
|
|
120
|
-
/**
|
|
121
|
-
* Shared preview persisted for referenced users inside backup metadata files.
|
|
122
|
-
*/
|
|
123
|
-
type BackupUserPreview = {
|
|
124
|
-
readonly id: number;
|
|
125
|
-
readonly username: string;
|
|
126
|
-
readonly isAdmin: boolean;
|
|
127
|
-
readonly profileImageUrl: string | null;
|
|
128
|
-
};
|
|
129
|
-
|
|
130
|
-
/**
|
|
131
|
-
* Shared preview persisted for referenced agents inside backup metadata files.
|
|
132
|
-
*/
|
|
133
|
-
type BackupAgentPreview = {
|
|
134
|
-
readonly id: number;
|
|
135
|
-
readonly agentName: string;
|
|
136
|
-
readonly permanentId: string | null;
|
|
137
|
-
};
|
|
138
|
-
|
|
139
|
-
/**
|
|
140
|
-
* Attachment reference stored alongside exported files/media metadata.
|
|
141
|
-
*/
|
|
142
|
-
type BackupAttachmentReference = {
|
|
143
|
-
readonly source: 'user-chat' | 'chat-feedback';
|
|
144
|
-
readonly messageIndex: number;
|
|
145
|
-
readonly messageId: string | null;
|
|
146
|
-
readonly attachmentName: string | null;
|
|
147
|
-
readonly sender: unknown;
|
|
148
|
-
readonly chatId?: string;
|
|
149
|
-
readonly chatTitle?: string | null;
|
|
150
|
-
readonly feedbackId?: number;
|
|
151
|
-
readonly user?: BackupUserPreview | null;
|
|
152
|
-
readonly agent?: BackupAgentPreview | null;
|
|
153
|
-
};
|
|
154
|
-
|
|
155
|
-
/**
|
|
156
|
-
* Shared context with lazy-loaded table snapshots reused across multiple sections.
|
|
157
|
-
*/
|
|
158
|
-
type ServerBackupContext = {
|
|
159
|
-
readonly supabase: ReturnType<typeof $provideSupabaseForServer>;
|
|
160
|
-
userRowsPromise?: Promise<Array<UserRow>>;
|
|
161
|
-
agentRowsPromise?: Promise<Array<AgentRow>>;
|
|
162
|
-
userChatRowsPromise?: Promise<Array<UserChatRow>>;
|
|
163
|
-
chatFeedbackRowsPromise?: Promise<Array<ChatFeedbackRow>>;
|
|
164
|
-
};
|
|
165
|
-
|
|
166
18
|
/**
|
|
167
19
|
* Builds one ZIP archive containing the selected server entities plus the books backup tree when requested.
|
|
168
20
|
*
|
|
@@ -200,24 +52,11 @@ export async function createServerBackupZipStream(
|
|
|
200
52
|
|
|
201
53
|
await Promise.all([appendBooksPromise, appendSectionsPromise]);
|
|
202
54
|
|
|
203
|
-
const manifest
|
|
204
|
-
format: SERVER_BACKUP_FORMAT,
|
|
55
|
+
const manifest = createServerBackupManifest({
|
|
205
56
|
generatedAt,
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
(sectionKey, index) => selectedSectionKeys[index] === sectionKey,
|
|
210
|
-
),
|
|
211
|
-
selectedSections: selectedSectionKeys,
|
|
212
|
-
sections: selectedSectionDefinitions.map(({ key, label, description, directoryName, selectionKind, includesBooks }) => ({
|
|
213
|
-
key,
|
|
214
|
-
label,
|
|
215
|
-
description,
|
|
216
|
-
directoryName,
|
|
217
|
-
selectionKind,
|
|
218
|
-
includesBooks,
|
|
219
|
-
})),
|
|
220
|
-
};
|
|
57
|
+
selectedSectionKeys,
|
|
58
|
+
selectedSectionDefinitions,
|
|
59
|
+
});
|
|
221
60
|
|
|
222
61
|
zip.file(`${backupRootFolderName}/manifest.json`, `${JSON.stringify(manifest, null, 2)}\n`);
|
|
223
62
|
|
|
@@ -232,1217 +71,3 @@ export async function createServerBackupZipStream(
|
|
|
232
71
|
stream,
|
|
233
72
|
};
|
|
234
73
|
}
|
|
235
|
-
|
|
236
|
-
/**
|
|
237
|
-
* Creates the shared backup context with lazy table loaders.
|
|
238
|
-
*
|
|
239
|
-
* @param supabase - Server-side Supabase client.
|
|
240
|
-
* @returns Reusable backup context.
|
|
241
|
-
*/
|
|
242
|
-
function createServerBackupContext(
|
|
243
|
-
supabase: ReturnType<typeof $provideSupabaseForServer>,
|
|
244
|
-
): ServerBackupContext {
|
|
245
|
-
return {
|
|
246
|
-
supabase,
|
|
247
|
-
};
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
/**
|
|
251
|
-
* Appends one selected section into the ZIP archive.
|
|
252
|
-
*
|
|
253
|
-
* @param options - Section append options.
|
|
254
|
-
*/
|
|
255
|
-
async function appendSectionEntriesToZip(options: {
|
|
256
|
-
zip: JSZip;
|
|
257
|
-
backupRootFolderName: string;
|
|
258
|
-
sectionDefinition: ServerBackupSectionDefinition;
|
|
259
|
-
context: ServerBackupContext;
|
|
260
|
-
}): Promise<void> {
|
|
261
|
-
const { zip, backupRootFolderName, sectionDefinition, context } = options;
|
|
262
|
-
const sectionRootPath = `${backupRootFolderName}/data/${sectionDefinition.directoryName}`;
|
|
263
|
-
zip.folder(sectionRootPath);
|
|
264
|
-
|
|
265
|
-
switch (sectionDefinition.key) {
|
|
266
|
-
case 'metadata':
|
|
267
|
-
await appendMetadataBackupEntriesToZip(zip, sectionRootPath, context);
|
|
268
|
-
return;
|
|
269
|
-
case 'agents':
|
|
270
|
-
await appendAgentBackupEntriesToZip(zip, sectionRootPath, sectionDefinition, context);
|
|
271
|
-
return;
|
|
272
|
-
case 'conversations':
|
|
273
|
-
await appendConversationBackupEntriesToZip(zip, sectionRootPath, context);
|
|
274
|
-
return;
|
|
275
|
-
case 'users':
|
|
276
|
-
await appendUserBackupEntriesToZip(zip, sectionRootPath, context);
|
|
277
|
-
return;
|
|
278
|
-
case 'files':
|
|
279
|
-
await appendFileBackupEntriesToZip(zip, sectionRootPath, context);
|
|
280
|
-
return;
|
|
281
|
-
case 'messages':
|
|
282
|
-
await appendMessageBackupEntriesToZip(zip, sectionRootPath, context);
|
|
283
|
-
return;
|
|
284
|
-
case 'security':
|
|
285
|
-
case 'caches':
|
|
286
|
-
appendExcludedSectionNoteToZip(zip, sectionRootPath, sectionDefinition);
|
|
287
|
-
return;
|
|
288
|
-
default: {
|
|
289
|
-
const exhaustiveSectionKey: never = sectionDefinition.key;
|
|
290
|
-
throw new Error(`Unsupported backup section ${String(exhaustiveSectionKey)}.`);
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
/**
|
|
296
|
-
* Writes the flattened metadata and limits file.
|
|
297
|
-
*
|
|
298
|
-
* @param zip - ZIP archive being assembled.
|
|
299
|
-
* @param sectionRootPath - Root path for the metadata section.
|
|
300
|
-
* @param context - Shared backup context.
|
|
301
|
-
*/
|
|
302
|
-
async function appendMetadataBackupEntriesToZip(
|
|
303
|
-
zip: JSZip,
|
|
304
|
-
sectionRootPath: string,
|
|
305
|
-
context: ServerBackupContext,
|
|
306
|
-
): Promise<void> {
|
|
307
|
-
const [metadataRows, serverLimitRows] = await Promise.all([
|
|
308
|
-
loadTableRows(context.supabase, 'Metadata'),
|
|
309
|
-
loadTableRows(context.supabase, 'ServerLimit'),
|
|
310
|
-
]);
|
|
311
|
-
|
|
312
|
-
const keyValueEntries = [
|
|
313
|
-
...metadataRows.map((row) => [row.key, row.value] as const),
|
|
314
|
-
...serverLimitRows.map((row) => [row.key, row.value] as const),
|
|
315
|
-
].sort(([leftKey], [rightKey]) => leftKey.localeCompare(rightKey));
|
|
316
|
-
|
|
317
|
-
zip.file(
|
|
318
|
-
`${sectionRootPath}/${METADATA_AND_LIMITS_FILENAME}`,
|
|
319
|
-
`${JSON.stringify(Object.fromEntries(keyValueEntries), null, 2)}\n`,
|
|
320
|
-
);
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
/**
|
|
324
|
-
* Writes the legacy table-backed agent export while keeping the books tree unchanged.
|
|
325
|
-
*
|
|
326
|
-
* @param zip - ZIP archive being assembled.
|
|
327
|
-
* @param sectionRootPath - Root path for the agents section.
|
|
328
|
-
* @param sectionDefinition - Agents section definition.
|
|
329
|
-
* @param context - Shared backup context.
|
|
330
|
-
*/
|
|
331
|
-
async function appendAgentBackupEntriesToZip(
|
|
332
|
-
zip: JSZip,
|
|
333
|
-
sectionRootPath: string,
|
|
334
|
-
sectionDefinition: ServerBackupSectionDefinition,
|
|
335
|
-
context: ServerBackupContext,
|
|
336
|
-
): Promise<void> {
|
|
337
|
-
const tableKeys = sectionDefinition.tables || [];
|
|
338
|
-
|
|
339
|
-
for (const tableKey of tableKeys) {
|
|
340
|
-
const tablePayload = await loadBackupTableFilePayload(context.supabase, tableKey);
|
|
341
|
-
zip.file(`${sectionRootPath}/${tableKey}.json`, `${JSON.stringify(tablePayload, null, 2)}\n`);
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
/**
|
|
346
|
-
* Writes one JSON chat export per conversation and per feedback thread.
|
|
347
|
-
*
|
|
348
|
-
* @param zip - ZIP archive being assembled.
|
|
349
|
-
* @param sectionRootPath - Root path for the conversations section.
|
|
350
|
-
* @param context - Shared backup context.
|
|
351
|
-
*/
|
|
352
|
-
async function appendConversationBackupEntriesToZip(
|
|
353
|
-
zip: JSZip,
|
|
354
|
-
sectionRootPath: string,
|
|
355
|
-
context: ServerBackupContext,
|
|
356
|
-
): Promise<void> {
|
|
357
|
-
const [userChats, users, agents, feedbackRows] = await Promise.all([
|
|
358
|
-
loadUserChatRows(context),
|
|
359
|
-
loadUserRows(context),
|
|
360
|
-
loadAgentRows(context),
|
|
361
|
-
loadChatFeedbackRows(context),
|
|
362
|
-
]);
|
|
363
|
-
|
|
364
|
-
const chatsRootPath = `${sectionRootPath}/chats`;
|
|
365
|
-
const feedbackRootPath = `${sectionRootPath}/feedback`;
|
|
366
|
-
const usedChatFilenames = new Set<string>();
|
|
367
|
-
const usedFeedbackFilenames = new Set<string>();
|
|
368
|
-
const userPreviewById = createUserPreviewById(users);
|
|
369
|
-
const agentPreviewByPermanentId = createAgentPreviewByPermanentId(agents);
|
|
370
|
-
const agentPreviewByName = createAgentPreviewByName(agents);
|
|
371
|
-
|
|
372
|
-
zip.folder(chatsRootPath);
|
|
373
|
-
zip.folder(feedbackRootPath);
|
|
374
|
-
|
|
375
|
-
for (const chat of userChats) {
|
|
376
|
-
const userPreview = userPreviewById.get(chat.userId) || null;
|
|
377
|
-
const agentPreview = agentPreviewByPermanentId.get(chat.agentPermanentId) || {
|
|
378
|
-
id: 0,
|
|
379
|
-
agentName: chat.agentPermanentId,
|
|
380
|
-
permanentId: chat.agentPermanentId,
|
|
381
|
-
};
|
|
382
|
-
const chatMessages = resolveSerializableArray(chat.messages);
|
|
383
|
-
const preferredFilename = ensureJsonFilename(
|
|
384
|
-
createBackupStem(
|
|
385
|
-
[userPreview?.username, agentPreview.agentName, normalizeOptionalText(chat.title), 'conversation'],
|
|
386
|
-
`chat-${chat.id}`,
|
|
387
|
-
),
|
|
388
|
-
);
|
|
389
|
-
const filename = createUniqueBackupFilename(
|
|
390
|
-
usedChatFilenames,
|
|
391
|
-
preferredFilename,
|
|
392
|
-
`chat-${chat.id}`,
|
|
393
|
-
);
|
|
394
|
-
const metadataFilename = createJsonMetadataFilename(filename);
|
|
395
|
-
|
|
396
|
-
zip.file(`${chatsRootPath}/${filename}`, `${JSON.stringify(chatMessages, null, 2)}\n`);
|
|
397
|
-
zip.file(
|
|
398
|
-
`${chatsRootPath}/${metadataFilename}`,
|
|
399
|
-
`${JSON.stringify(
|
|
400
|
-
{
|
|
401
|
-
kind: 'user-chat',
|
|
402
|
-
chatId: chat.id,
|
|
403
|
-
title: chat.title,
|
|
404
|
-
createdAt: chat.createdAt,
|
|
405
|
-
updatedAt: chat.updatedAt,
|
|
406
|
-
lastMessageAt: chat.lastMessageAt,
|
|
407
|
-
source: chat.source,
|
|
408
|
-
draftMessage: chat.draftMessage,
|
|
409
|
-
messageCount: chatMessages.length,
|
|
410
|
-
user: userPreview,
|
|
411
|
-
agent: agentPreview,
|
|
412
|
-
},
|
|
413
|
-
null,
|
|
414
|
-
2,
|
|
415
|
-
)}\n`,
|
|
416
|
-
);
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
for (const feedback of feedbackRows) {
|
|
420
|
-
const { messages, rawChatThreadText } = resolveFeedbackThreadMessages(feedback.chatThread);
|
|
421
|
-
const agentPreview = agentPreviewByName.get(feedback.agentName) || {
|
|
422
|
-
id: 0,
|
|
423
|
-
agentName: feedback.agentName,
|
|
424
|
-
permanentId: null,
|
|
425
|
-
};
|
|
426
|
-
const preferredFilename = ensureJsonFilename(
|
|
427
|
-
createBackupStem(
|
|
428
|
-
[feedback.agentName, normalizeOptionalText(feedback.textRating), `feedback ${feedback.id}`],
|
|
429
|
-
`feedback-${feedback.id}`,
|
|
430
|
-
),
|
|
431
|
-
);
|
|
432
|
-
const filename = createUniqueBackupFilename(
|
|
433
|
-
usedFeedbackFilenames,
|
|
434
|
-
preferredFilename,
|
|
435
|
-
`feedback-${feedback.id}`,
|
|
436
|
-
);
|
|
437
|
-
const metadataFilename = createJsonMetadataFilename(filename);
|
|
438
|
-
|
|
439
|
-
zip.file(`${feedbackRootPath}/${filename}`, `${JSON.stringify(messages, null, 2)}\n`);
|
|
440
|
-
zip.file(
|
|
441
|
-
`${feedbackRootPath}/${metadataFilename}`,
|
|
442
|
-
`${JSON.stringify(
|
|
443
|
-
{
|
|
444
|
-
kind: 'chat-feedback',
|
|
445
|
-
feedbackId: feedback.id,
|
|
446
|
-
createdAt: feedback.createdAt,
|
|
447
|
-
agent: agentPreview,
|
|
448
|
-
rating: feedback.rating,
|
|
449
|
-
textRating: feedback.textRating,
|
|
450
|
-
userNote: feedback.userNote,
|
|
451
|
-
expectedAnswer: feedback.expectedAnswer,
|
|
452
|
-
promptbookEngineVersion: feedback.promptbookEngineVersion,
|
|
453
|
-
url: feedback.url,
|
|
454
|
-
language: feedback.language,
|
|
455
|
-
platform: feedback.platform,
|
|
456
|
-
messageCount: messages.length,
|
|
457
|
-
...(rawChatThreadText ? { rawChatThreadText } : {}),
|
|
458
|
-
},
|
|
459
|
-
null,
|
|
460
|
-
2,
|
|
461
|
-
)}\n`,
|
|
462
|
-
);
|
|
463
|
-
}
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
/**
|
|
467
|
-
* Writes one JSON file per user with related memories, structured data, and redacted wallet entries.
|
|
468
|
-
*
|
|
469
|
-
* @param zip - ZIP archive being assembled.
|
|
470
|
-
* @param sectionRootPath - Root path for the users section.
|
|
471
|
-
* @param context - Shared backup context.
|
|
472
|
-
*/
|
|
473
|
-
async function appendUserBackupEntriesToZip(
|
|
474
|
-
zip: JSZip,
|
|
475
|
-
sectionRootPath: string,
|
|
476
|
-
context: ServerBackupContext,
|
|
477
|
-
): Promise<void> {
|
|
478
|
-
const [users, agents, memoryRows, userDataRows, walletRows] = await Promise.all([
|
|
479
|
-
loadUserRows(context),
|
|
480
|
-
loadAgentRows(context),
|
|
481
|
-
loadTableRows(context.supabase, 'UserMemory'),
|
|
482
|
-
loadTableRows(context.supabase, 'UserData'),
|
|
483
|
-
loadTableRows(context.supabase, 'Wallet'),
|
|
484
|
-
]);
|
|
485
|
-
const usedUserFilenames = new Set<string>();
|
|
486
|
-
const agentPreviewByPermanentId = createAgentPreviewByPermanentId(agents);
|
|
487
|
-
const memoryRowsByUserId = groupRowsBy(memoryRows, (row) => row.userId);
|
|
488
|
-
const userDataRowsByUserId = groupRowsBy(userDataRows, (row) => row.userId);
|
|
489
|
-
const walletRowsByUserId = groupRowsBy(walletRows, (row) => row.userId);
|
|
490
|
-
|
|
491
|
-
for (const user of users) {
|
|
492
|
-
const filename = createUniqueBackupFilename(
|
|
493
|
-
usedUserFilenames,
|
|
494
|
-
ensureJsonFilename(createBackupStem([user.username], `user-${user.id}`)),
|
|
495
|
-
`user-${user.id}`,
|
|
496
|
-
);
|
|
497
|
-
const userMemoryRows = memoryRowsByUserId.get(user.id) || [];
|
|
498
|
-
const scopedUserDataRows = userDataRowsByUserId.get(user.id) || [];
|
|
499
|
-
const scopedWalletRows = walletRowsByUserId.get(user.id) || [];
|
|
500
|
-
|
|
501
|
-
zip.file(
|
|
502
|
-
`${sectionRootPath}/${filename}`,
|
|
503
|
-
`${JSON.stringify(
|
|
504
|
-
{
|
|
505
|
-
user: {
|
|
506
|
-
id: user.id,
|
|
507
|
-
createdAt: user.createdAt,
|
|
508
|
-
updatedAt: user.updatedAt,
|
|
509
|
-
username: user.username,
|
|
510
|
-
isAdmin: user.isAdmin,
|
|
511
|
-
profileImageUrl: user.profileImageUrl,
|
|
512
|
-
},
|
|
513
|
-
memories: userMemoryRows.map((row) => ({
|
|
514
|
-
id: row.id,
|
|
515
|
-
createdAt: row.createdAt,
|
|
516
|
-
updatedAt: row.updatedAt,
|
|
517
|
-
content: row.content,
|
|
518
|
-
isGlobal: row.isGlobal,
|
|
519
|
-
deletedAt: row.deletedAt,
|
|
520
|
-
agent: row.agentPermanentId ? agentPreviewByPermanentId.get(row.agentPermanentId) || null : null,
|
|
521
|
-
})),
|
|
522
|
-
data: scopedUserDataRows.map((row) => ({
|
|
523
|
-
id: row.id,
|
|
524
|
-
createdAt: row.createdAt,
|
|
525
|
-
updatedAt: row.updatedAt,
|
|
526
|
-
key: row.key,
|
|
527
|
-
value: row.value,
|
|
528
|
-
})),
|
|
529
|
-
wallet: scopedWalletRows.map((row) => createRedactedWalletBackupRecord(row, agentPreviewByPermanentId)),
|
|
530
|
-
},
|
|
531
|
-
null,
|
|
532
|
-
2,
|
|
533
|
-
)}\n`,
|
|
534
|
-
);
|
|
535
|
-
}
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
/**
|
|
539
|
-
* Writes uploaded files and generated images together with sidecar restore metadata.
|
|
540
|
-
*
|
|
541
|
-
* @param zip - ZIP archive being assembled.
|
|
542
|
-
* @param sectionRootPath - Root path for the files section.
|
|
543
|
-
* @param context - Shared backup context.
|
|
544
|
-
*/
|
|
545
|
-
async function appendFileBackupEntriesToZip(
|
|
546
|
-
zip: JSZip,
|
|
547
|
-
sectionRootPath: string,
|
|
548
|
-
context: ServerBackupContext,
|
|
549
|
-
): Promise<void> {
|
|
550
|
-
const [fileRows, imageRows, users, agents, userChats, feedbackRows] = await Promise.all([
|
|
551
|
-
loadTableRows(context.supabase, 'File'),
|
|
552
|
-
loadTableRows(context.supabase, 'Image'),
|
|
553
|
-
loadUserRows(context),
|
|
554
|
-
loadAgentRows(context),
|
|
555
|
-
loadUserChatRows(context),
|
|
556
|
-
loadChatFeedbackRows(context),
|
|
557
|
-
]);
|
|
558
|
-
const uploadsRootPath = `${sectionRootPath}/uploads`;
|
|
559
|
-
const imagesRootPath = `${sectionRootPath}/images`;
|
|
560
|
-
const usedUploadFilenames = new Set<string>();
|
|
561
|
-
const usedImageFilenames = new Set<string>();
|
|
562
|
-
const userPreviewById = createUserPreviewById(users);
|
|
563
|
-
const agentPreviewById = createAgentPreviewById(agents);
|
|
564
|
-
const agentPreviewByPermanentId = createAgentPreviewByPermanentId(agents);
|
|
565
|
-
const agentPreviewByName = createAgentPreviewByName(agents);
|
|
566
|
-
const attachmentReferencesByUrl = createAttachmentReferencesByUrl({
|
|
567
|
-
userChats,
|
|
568
|
-
feedbackRows,
|
|
569
|
-
userPreviewById,
|
|
570
|
-
agentPreviewByPermanentId,
|
|
571
|
-
agentPreviewByName,
|
|
572
|
-
});
|
|
573
|
-
|
|
574
|
-
zip.folder(uploadsRootPath);
|
|
575
|
-
zip.folder(imagesRootPath);
|
|
576
|
-
|
|
577
|
-
for (const fileRow of fileRows) {
|
|
578
|
-
const contentUrl = normalizeOptionalText(fileRow.storageUrl) || normalizeOptionalText(fileRow.shortUrl);
|
|
579
|
-
const filename = createUniqueBackupFilename(
|
|
580
|
-
usedUploadFilenames,
|
|
581
|
-
resolveBinaryBackupFilename(fileRow.fileName, contentUrl, `file-${fileRow.id}`),
|
|
582
|
-
`file-${fileRow.id}`,
|
|
583
|
-
);
|
|
584
|
-
const downloadResult = await downloadBackupBinaryContent(contentUrl);
|
|
585
|
-
const attachedToMessages = deduplicateSerializableObjects(
|
|
586
|
-
resolveAttachmentReferencesForUrls(
|
|
587
|
-
attachmentReferencesByUrl,
|
|
588
|
-
[fileRow.storageUrl, fileRow.shortUrl],
|
|
589
|
-
),
|
|
590
|
-
);
|
|
591
|
-
|
|
592
|
-
if (downloadResult.content) {
|
|
593
|
-
zip.file(`${uploadsRootPath}/${filename}`, downloadResult.content);
|
|
594
|
-
}
|
|
595
|
-
|
|
596
|
-
zip.file(
|
|
597
|
-
`${uploadsRootPath}/${createBinaryMetadataFilename(filename)}`,
|
|
598
|
-
`${JSON.stringify(
|
|
599
|
-
{
|
|
600
|
-
kind: 'uploaded-file',
|
|
601
|
-
id: fileRow.id,
|
|
602
|
-
createdAt: fileRow.createdAt,
|
|
603
|
-
originalFileName: resolvePathBasename(fileRow.fileName, `file-${fileRow.id}`),
|
|
604
|
-
fileType: fileRow.fileType,
|
|
605
|
-
fileSize: fileRow.fileSize,
|
|
606
|
-
purpose: fileRow.purpose,
|
|
607
|
-
status: fileRow.status,
|
|
608
|
-
uploadedBy: typeof fileRow.userId === 'number' ? userPreviewById.get(fileRow.userId) || null : null,
|
|
609
|
-
agent: typeof fileRow.agentId === 'number' ? agentPreviewById.get(fileRow.agentId) || null : null,
|
|
610
|
-
attachedToMessages,
|
|
611
|
-
contentUrl,
|
|
612
|
-
contentIncluded: Boolean(downloadResult.content),
|
|
613
|
-
...(downloadResult.error ? { contentDownloadError: downloadResult.error } : {}),
|
|
614
|
-
},
|
|
615
|
-
null,
|
|
616
|
-
2,
|
|
617
|
-
)}\n`,
|
|
618
|
-
);
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
for (const imageRow of imageRows) {
|
|
622
|
-
const filename = createUniqueBackupFilename(
|
|
623
|
-
usedImageFilenames,
|
|
624
|
-
resolveBinaryBackupFilename(imageRow.filename, imageRow.cdnUrl, `image-${imageRow.id}`),
|
|
625
|
-
`image-${imageRow.id}`,
|
|
626
|
-
);
|
|
627
|
-
const downloadResult = await downloadBackupBinaryContent(imageRow.cdnUrl);
|
|
628
|
-
|
|
629
|
-
if (downloadResult.content) {
|
|
630
|
-
zip.file(`${imagesRootPath}/${filename}`, downloadResult.content);
|
|
631
|
-
}
|
|
632
|
-
|
|
633
|
-
zip.file(
|
|
634
|
-
`${imagesRootPath}/${createBinaryMetadataFilename(filename)}`,
|
|
635
|
-
`${JSON.stringify(
|
|
636
|
-
{
|
|
637
|
-
kind: 'generated-image',
|
|
638
|
-
id: imageRow.id,
|
|
639
|
-
createdAt: imageRow.createdAt,
|
|
640
|
-
updatedAt: imageRow.updatedAt,
|
|
641
|
-
filename: imageRow.filename,
|
|
642
|
-
prompt: imageRow.prompt,
|
|
643
|
-
purpose: imageRow.purpose,
|
|
644
|
-
agent: typeof imageRow.agentId === 'number' ? agentPreviewById.get(imageRow.agentId) || null : null,
|
|
645
|
-
contentUrl: imageRow.cdnUrl,
|
|
646
|
-
contentIncluded: Boolean(downloadResult.content),
|
|
647
|
-
...(downloadResult.error ? { contentDownloadError: downloadResult.error } : {}),
|
|
648
|
-
},
|
|
649
|
-
null,
|
|
650
|
-
2,
|
|
651
|
-
)}\n`,
|
|
652
|
-
);
|
|
653
|
-
}
|
|
654
|
-
}
|
|
655
|
-
|
|
656
|
-
/**
|
|
657
|
-
* Writes one JSON file per system message with delivery history.
|
|
658
|
-
*
|
|
659
|
-
* @param zip - ZIP archive being assembled.
|
|
660
|
-
* @param sectionRootPath - Root path for the messages section.
|
|
661
|
-
* @param context - Shared backup context.
|
|
662
|
-
*/
|
|
663
|
-
async function appendMessageBackupEntriesToZip(
|
|
664
|
-
zip: JSZip,
|
|
665
|
-
sectionRootPath: string,
|
|
666
|
-
context: ServerBackupContext,
|
|
667
|
-
): Promise<void> {
|
|
668
|
-
const [messageRows, sendAttemptRows] = await Promise.all([
|
|
669
|
-
loadTableRows(context.supabase, 'Message'),
|
|
670
|
-
loadTableRows(context.supabase, 'MessageSendAttempt'),
|
|
671
|
-
]);
|
|
672
|
-
const usedMessageFilenames = new Set<string>();
|
|
673
|
-
const sendAttemptsByMessageId = groupRowsBy(sendAttemptRows, (row) => row.messageId);
|
|
674
|
-
|
|
675
|
-
for (const messageRow of messageRows) {
|
|
676
|
-
const filename = createUniqueBackupFilename(
|
|
677
|
-
usedMessageFilenames,
|
|
678
|
-
ensureJsonFilename(
|
|
679
|
-
createBackupStem(
|
|
680
|
-
[messageRow.channel, messageRow.direction, messageRow.createdAt],
|
|
681
|
-
`message-${messageRow.id}`,
|
|
682
|
-
),
|
|
683
|
-
),
|
|
684
|
-
`message-${messageRow.id}`,
|
|
685
|
-
);
|
|
686
|
-
const sendAttempts = sendAttemptsByMessageId.get(messageRow.id) || [];
|
|
687
|
-
|
|
688
|
-
zip.file(
|
|
689
|
-
`${sectionRootPath}/${filename}`,
|
|
690
|
-
`${JSON.stringify(
|
|
691
|
-
{
|
|
692
|
-
id: messageRow.id,
|
|
693
|
-
createdAt: messageRow.createdAt,
|
|
694
|
-
channel: messageRow.channel,
|
|
695
|
-
direction: messageRow.direction,
|
|
696
|
-
sender: messageRow.sender,
|
|
697
|
-
recipients: messageRow.recipients,
|
|
698
|
-
content: messageRow.content,
|
|
699
|
-
threadId: messageRow.threadId,
|
|
700
|
-
metadata: messageRow.metadata,
|
|
701
|
-
sendAttempts: sendAttempts.map((row) => ({
|
|
702
|
-
id: row.id,
|
|
703
|
-
createdAt: row.createdAt,
|
|
704
|
-
providerName: row.providerName,
|
|
705
|
-
isSuccessful: row.isSuccessful,
|
|
706
|
-
})),
|
|
707
|
-
},
|
|
708
|
-
null,
|
|
709
|
-
2,
|
|
710
|
-
)}\n`,
|
|
711
|
-
);
|
|
712
|
-
}
|
|
713
|
-
}
|
|
714
|
-
|
|
715
|
-
/**
|
|
716
|
-
* Writes the explanatory note for a section that is intentionally excluded from the archive.
|
|
717
|
-
*
|
|
718
|
-
* @param zip - ZIP archive being assembled.
|
|
719
|
-
* @param sectionRootPath - Root path for the excluded section.
|
|
720
|
-
* @param sectionDefinition - Excluded section definition.
|
|
721
|
-
*/
|
|
722
|
-
function appendExcludedSectionNoteToZip(
|
|
723
|
-
zip: JSZip,
|
|
724
|
-
sectionRootPath: string,
|
|
725
|
-
sectionDefinition: ServerBackupSectionDefinition,
|
|
726
|
-
): void {
|
|
727
|
-
zip.file(
|
|
728
|
-
`${sectionRootPath}/${EXCLUDED_SECTION_FILENAME}`,
|
|
729
|
-
`${JSON.stringify(
|
|
730
|
-
{
|
|
731
|
-
key: sectionDefinition.key,
|
|
732
|
-
label: sectionDefinition.label,
|
|
733
|
-
included: false,
|
|
734
|
-
reason: sectionDefinition.description,
|
|
735
|
-
},
|
|
736
|
-
null,
|
|
737
|
-
2,
|
|
738
|
-
)}\n`,
|
|
739
|
-
);
|
|
740
|
-
}
|
|
741
|
-
|
|
742
|
-
/**
|
|
743
|
-
* Reads one logical table and converts it into a deterministic JSON backup payload.
|
|
744
|
-
*
|
|
745
|
-
* @param supabase - Server-side Supabase client.
|
|
746
|
-
* @param tableKey - Logical table key from the generated schema.
|
|
747
|
-
* @returns Serializable table payload for the archive.
|
|
748
|
-
*/
|
|
749
|
-
async function loadBackupTableFilePayload(
|
|
750
|
-
supabase: ReturnType<typeof $provideSupabaseForServer>,
|
|
751
|
-
tableKey: keyof AgentsServerDatabase['public']['Tables'],
|
|
752
|
-
): Promise<BackupTableFilePayload> {
|
|
753
|
-
const tableName = await $getTableName(tableKey);
|
|
754
|
-
const result = await supabase.from(tableName).select('*');
|
|
755
|
-
|
|
756
|
-
if (result.error) {
|
|
757
|
-
throw new Error(`Unable to load backup table ${tableKey}: ${result.error.message}`);
|
|
758
|
-
}
|
|
759
|
-
|
|
760
|
-
const rows = sortBackupRows(
|
|
761
|
-
(((result.data || []) as unknown as Array<Record<string, unknown>>).map((row) => ({ ...row }))),
|
|
762
|
-
);
|
|
763
|
-
|
|
764
|
-
return {
|
|
765
|
-
entity: tableKey,
|
|
766
|
-
databaseTable: tableName,
|
|
767
|
-
rowCount: rows.length,
|
|
768
|
-
rows,
|
|
769
|
-
};
|
|
770
|
-
}
|
|
771
|
-
|
|
772
|
-
/**
|
|
773
|
-
* Reads one logical table into typed rows.
|
|
774
|
-
*
|
|
775
|
-
* @param supabase - Server-side Supabase client.
|
|
776
|
-
* @param tableKey - Logical table key from the generated schema.
|
|
777
|
-
* @returns Ordered table rows.
|
|
778
|
-
*/
|
|
779
|
-
async function loadTableRows<TableKey extends keyof AgentsServerDatabase['public']['Tables']>(
|
|
780
|
-
supabase: ReturnType<typeof $provideSupabaseForServer>,
|
|
781
|
-
tableKey: TableKey,
|
|
782
|
-
): Promise<Array<AgentsServerDatabase['public']['Tables'][TableKey]['Row']>> {
|
|
783
|
-
const tableName = await $getTableName(tableKey);
|
|
784
|
-
const result = await supabase.from(tableName).select('*');
|
|
785
|
-
|
|
786
|
-
if (result.error) {
|
|
787
|
-
throw new Error(`Unable to load backup table ${String(tableKey)}: ${result.error.message}`);
|
|
788
|
-
}
|
|
789
|
-
|
|
790
|
-
return sortBackupRows((result.data || []) as unknown as Array<Record<string, unknown>>) as Array<
|
|
791
|
-
AgentsServerDatabase['public']['Tables'][TableKey]['Row']
|
|
792
|
-
>;
|
|
793
|
-
}
|
|
794
|
-
|
|
795
|
-
/**
|
|
796
|
-
* Lazily loads users once for the backup run.
|
|
797
|
-
*
|
|
798
|
-
* @param context - Shared backup context.
|
|
799
|
-
* @returns Cached user rows.
|
|
800
|
-
*/
|
|
801
|
-
function loadUserRows(context: ServerBackupContext): Promise<Array<UserRow>> {
|
|
802
|
-
context.userRowsPromise = context.userRowsPromise || loadTableRows(context.supabase, 'User');
|
|
803
|
-
return context.userRowsPromise;
|
|
804
|
-
}
|
|
805
|
-
|
|
806
|
-
/**
|
|
807
|
-
* Lazily loads agents once for the backup run.
|
|
808
|
-
*
|
|
809
|
-
* @param context - Shared backup context.
|
|
810
|
-
* @returns Cached agent rows.
|
|
811
|
-
*/
|
|
812
|
-
function loadAgentRows(context: ServerBackupContext): Promise<Array<AgentRow>> {
|
|
813
|
-
context.agentRowsPromise = context.agentRowsPromise || loadTableRows(context.supabase, 'Agent');
|
|
814
|
-
return context.agentRowsPromise;
|
|
815
|
-
}
|
|
816
|
-
|
|
817
|
-
/**
|
|
818
|
-
* Lazily loads user chats once for the backup run.
|
|
819
|
-
*
|
|
820
|
-
* @param context - Shared backup context.
|
|
821
|
-
* @returns Cached user-chat rows.
|
|
822
|
-
*/
|
|
823
|
-
function loadUserChatRows(context: ServerBackupContext): Promise<Array<UserChatRow>> {
|
|
824
|
-
context.userChatRowsPromise = context.userChatRowsPromise || loadTableRows(context.supabase, 'UserChat');
|
|
825
|
-
return context.userChatRowsPromise;
|
|
826
|
-
}
|
|
827
|
-
|
|
828
|
-
/**
|
|
829
|
-
* Lazily loads feedback rows once for the backup run.
|
|
830
|
-
*
|
|
831
|
-
* @param context - Shared backup context.
|
|
832
|
-
* @returns Cached feedback rows.
|
|
833
|
-
*/
|
|
834
|
-
function loadChatFeedbackRows(context: ServerBackupContext): Promise<Array<ChatFeedbackRow>> {
|
|
835
|
-
context.chatFeedbackRowsPromise = context.chatFeedbackRowsPromise || loadTableRows(context.supabase, 'ChatFeedback');
|
|
836
|
-
return context.chatFeedbackRowsPromise;
|
|
837
|
-
}
|
|
838
|
-
|
|
839
|
-
/**
|
|
840
|
-
* Builds the user preview map reused across metadata sidecars.
|
|
841
|
-
*
|
|
842
|
-
* @param users - Full user rows.
|
|
843
|
-
* @returns User preview map keyed by numeric id.
|
|
844
|
-
*/
|
|
845
|
-
function createUserPreviewById(users: ReadonlyArray<UserRow>): Map<number, BackupUserPreview> {
|
|
846
|
-
return new Map(
|
|
847
|
-
users.map((user) => [
|
|
848
|
-
user.id,
|
|
849
|
-
{
|
|
850
|
-
id: user.id,
|
|
851
|
-
username: user.username,
|
|
852
|
-
isAdmin: user.isAdmin,
|
|
853
|
-
profileImageUrl: user.profileImageUrl,
|
|
854
|
-
} satisfies BackupUserPreview,
|
|
855
|
-
] as const),
|
|
856
|
-
);
|
|
857
|
-
}
|
|
858
|
-
|
|
859
|
-
/**
|
|
860
|
-
* Builds the agent preview map keyed by numeric id.
|
|
861
|
-
*
|
|
862
|
-
* @param agents - Full agent rows.
|
|
863
|
-
* @returns Agent preview map keyed by numeric id.
|
|
864
|
-
*/
|
|
865
|
-
function createAgentPreviewById(agents: ReadonlyArray<AgentRow>): Map<number, BackupAgentPreview> {
|
|
866
|
-
return new Map(
|
|
867
|
-
agents.map((agent) => [
|
|
868
|
-
agent.id,
|
|
869
|
-
{
|
|
870
|
-
id: agent.id,
|
|
871
|
-
agentName: agent.agentName,
|
|
872
|
-
permanentId: agent.permanentId,
|
|
873
|
-
} satisfies BackupAgentPreview,
|
|
874
|
-
] as const),
|
|
875
|
-
);
|
|
876
|
-
}
|
|
877
|
-
|
|
878
|
-
/**
|
|
879
|
-
* Builds the agent preview map keyed by permanent id.
|
|
880
|
-
*
|
|
881
|
-
* @param agents - Full agent rows.
|
|
882
|
-
* @returns Agent preview map keyed by permanent id.
|
|
883
|
-
*/
|
|
884
|
-
function createAgentPreviewByPermanentId(agents: ReadonlyArray<AgentRow>): Map<string, BackupAgentPreview> {
|
|
885
|
-
return new Map(
|
|
886
|
-
agents.flatMap((agent) =>
|
|
887
|
-
agent.permanentId
|
|
888
|
-
? [
|
|
889
|
-
[
|
|
890
|
-
agent.permanentId,
|
|
891
|
-
{
|
|
892
|
-
id: agent.id,
|
|
893
|
-
agentName: agent.agentName,
|
|
894
|
-
permanentId: agent.permanentId,
|
|
895
|
-
} satisfies BackupAgentPreview,
|
|
896
|
-
] as const,
|
|
897
|
-
]
|
|
898
|
-
: [],
|
|
899
|
-
),
|
|
900
|
-
);
|
|
901
|
-
}
|
|
902
|
-
|
|
903
|
-
/**
|
|
904
|
-
* Builds the first-agent-by-name preview map used by feedback sidecars.
|
|
905
|
-
*
|
|
906
|
-
* @param agents - Full agent rows.
|
|
907
|
-
* @returns Agent preview map keyed by agent name.
|
|
908
|
-
*/
|
|
909
|
-
function createAgentPreviewByName(agents: ReadonlyArray<AgentRow>): Map<string, BackupAgentPreview> {
|
|
910
|
-
const agentPreviewByName = new Map<string, BackupAgentPreview>();
|
|
911
|
-
|
|
912
|
-
for (const agent of agents) {
|
|
913
|
-
if (agentPreviewByName.has(agent.agentName)) {
|
|
914
|
-
continue;
|
|
915
|
-
}
|
|
916
|
-
|
|
917
|
-
agentPreviewByName.set(agent.agentName, {
|
|
918
|
-
id: agent.id,
|
|
919
|
-
agentName: agent.agentName,
|
|
920
|
-
permanentId: agent.permanentId,
|
|
921
|
-
});
|
|
922
|
-
}
|
|
923
|
-
|
|
924
|
-
return agentPreviewByName;
|
|
925
|
-
}
|
|
926
|
-
|
|
927
|
-
/**
|
|
928
|
-
* Builds one map of attachment URLs to the user-facing messages that reference them.
|
|
929
|
-
*
|
|
930
|
-
* @param options - Attachment map source data.
|
|
931
|
-
* @returns Attachment references keyed by URL.
|
|
932
|
-
*/
|
|
933
|
-
function createAttachmentReferencesByUrl(options: {
|
|
934
|
-
userChats: ReadonlyArray<UserChatRow>;
|
|
935
|
-
feedbackRows: ReadonlyArray<ChatFeedbackRow>;
|
|
936
|
-
userPreviewById: ReadonlyMap<number, BackupUserPreview>;
|
|
937
|
-
agentPreviewByPermanentId: ReadonlyMap<string, BackupAgentPreview>;
|
|
938
|
-
agentPreviewByName: ReadonlyMap<string, BackupAgentPreview>;
|
|
939
|
-
}): Map<string, Array<BackupAttachmentReference>> {
|
|
940
|
-
const attachmentReferencesByUrl = new Map<string, Array<BackupAttachmentReference>>();
|
|
941
|
-
|
|
942
|
-
const appendReference = (url: string, reference: BackupAttachmentReference): void => {
|
|
943
|
-
const references = attachmentReferencesByUrl.get(url) || [];
|
|
944
|
-
references.push(reference);
|
|
945
|
-
attachmentReferencesByUrl.set(url, references);
|
|
946
|
-
};
|
|
947
|
-
|
|
948
|
-
for (const chat of options.userChats) {
|
|
949
|
-
const userPreview = options.userPreviewById.get(chat.userId) || null;
|
|
950
|
-
const agentPreview = options.agentPreviewByPermanentId.get(chat.agentPermanentId) || null;
|
|
951
|
-
|
|
952
|
-
for (const [messageIndex, message] of resolveSerializableArray(chat.messages).entries()) {
|
|
953
|
-
for (const attachment of resolveMessageAttachments(message)) {
|
|
954
|
-
appendReference(attachment.url, {
|
|
955
|
-
source: 'user-chat',
|
|
956
|
-
chatId: chat.id,
|
|
957
|
-
chatTitle: chat.title,
|
|
958
|
-
messageIndex,
|
|
959
|
-
messageId: normalizeOptionalText((message as { id?: unknown }).id),
|
|
960
|
-
attachmentName: attachment.name,
|
|
961
|
-
sender: (message as { sender?: unknown }).sender ?? null,
|
|
962
|
-
user: userPreview,
|
|
963
|
-
agent: agentPreview,
|
|
964
|
-
});
|
|
965
|
-
}
|
|
966
|
-
}
|
|
967
|
-
}
|
|
968
|
-
|
|
969
|
-
for (const feedbackRow of options.feedbackRows) {
|
|
970
|
-
const agentPreview = options.agentPreviewByName.get(feedbackRow.agentName) || null;
|
|
971
|
-
const { messages } = resolveFeedbackThreadMessages(feedbackRow.chatThread);
|
|
972
|
-
|
|
973
|
-
for (const [messageIndex, message] of messages.entries()) {
|
|
974
|
-
for (const attachment of resolveMessageAttachments(message)) {
|
|
975
|
-
appendReference(attachment.url, {
|
|
976
|
-
source: 'chat-feedback',
|
|
977
|
-
feedbackId: feedbackRow.id,
|
|
978
|
-
messageIndex,
|
|
979
|
-
messageId: normalizeOptionalText((message as { id?: unknown }).id),
|
|
980
|
-
attachmentName: attachment.name,
|
|
981
|
-
sender: (message as { sender?: unknown }).sender ?? null,
|
|
982
|
-
agent: agentPreview,
|
|
983
|
-
});
|
|
984
|
-
}
|
|
985
|
-
}
|
|
986
|
-
}
|
|
987
|
-
|
|
988
|
-
return attachmentReferencesByUrl;
|
|
989
|
-
}
|
|
990
|
-
|
|
991
|
-
/**
|
|
992
|
-
* Resolves attachment references for the provided URLs.
|
|
993
|
-
*
|
|
994
|
-
* @param attachmentReferencesByUrl - Attachment reference map keyed by URL.
|
|
995
|
-
* @param candidateUrls - Possible URLs for one backed-up file.
|
|
996
|
-
* @returns Flattened reference list.
|
|
997
|
-
*/
|
|
998
|
-
function resolveAttachmentReferencesForUrls(
|
|
999
|
-
attachmentReferencesByUrl: ReadonlyMap<string, ReadonlyArray<BackupAttachmentReference>>,
|
|
1000
|
-
candidateUrls: ReadonlyArray<string | null>,
|
|
1001
|
-
): Array<BackupAttachmentReference> {
|
|
1002
|
-
const references: Array<BackupAttachmentReference> = [];
|
|
1003
|
-
|
|
1004
|
-
for (const candidateUrl of candidateUrls) {
|
|
1005
|
-
const normalizedUrl = normalizeOptionalText(candidateUrl);
|
|
1006
|
-
if (!normalizedUrl) {
|
|
1007
|
-
continue;
|
|
1008
|
-
}
|
|
1009
|
-
|
|
1010
|
-
const matches = attachmentReferencesByUrl.get(normalizedUrl) || [];
|
|
1011
|
-
references.push(...matches);
|
|
1012
|
-
}
|
|
1013
|
-
|
|
1014
|
-
return references;
|
|
1015
|
-
}
|
|
1016
|
-
|
|
1017
|
-
/**
|
|
1018
|
-
* Parses the persisted feedback chat thread into the same array shape used by chat JSON exports.
|
|
1019
|
-
*
|
|
1020
|
-
* @param rawChatThread - Raw persisted feedback thread.
|
|
1021
|
-
* @returns Parsed chat messages together with an optional raw fallback string.
|
|
1022
|
-
*/
|
|
1023
|
-
function resolveFeedbackThreadMessages(rawChatThread: unknown): {
|
|
1024
|
-
messages: Array<Record<string, unknown>>;
|
|
1025
|
-
rawChatThreadText: string | null;
|
|
1026
|
-
} {
|
|
1027
|
-
if (Array.isArray(rawChatThread)) {
|
|
1028
|
-
return {
|
|
1029
|
-
messages: rawChatThread.filter(isRecord),
|
|
1030
|
-
rawChatThreadText: null,
|
|
1031
|
-
};
|
|
1032
|
-
}
|
|
1033
|
-
|
|
1034
|
-
if (typeof rawChatThread !== 'string') {
|
|
1035
|
-
return {
|
|
1036
|
-
messages: [],
|
|
1037
|
-
rawChatThreadText: null,
|
|
1038
|
-
};
|
|
1039
|
-
}
|
|
1040
|
-
|
|
1041
|
-
const normalizedChatThreadText = rawChatThread.trim();
|
|
1042
|
-
if (normalizedChatThreadText.length === 0) {
|
|
1043
|
-
return {
|
|
1044
|
-
messages: [],
|
|
1045
|
-
rawChatThreadText: null,
|
|
1046
|
-
};
|
|
1047
|
-
}
|
|
1048
|
-
|
|
1049
|
-
try {
|
|
1050
|
-
const parsed = JSON.parse(normalizedChatThreadText);
|
|
1051
|
-
if (Array.isArray(parsed)) {
|
|
1052
|
-
return {
|
|
1053
|
-
messages: parsed.filter(isRecord),
|
|
1054
|
-
rawChatThreadText: null,
|
|
1055
|
-
};
|
|
1056
|
-
}
|
|
1057
|
-
} catch {
|
|
1058
|
-
// Keep the original text below so the backup still preserves the source thread payload.
|
|
1059
|
-
}
|
|
1060
|
-
|
|
1061
|
-
return {
|
|
1062
|
-
messages: [],
|
|
1063
|
-
rawChatThreadText: normalizedChatThreadText,
|
|
1064
|
-
};
|
|
1065
|
-
}
|
|
1066
|
-
|
|
1067
|
-
/**
|
|
1068
|
-
* Extracts normalized attachment previews from one serialized chat message.
|
|
1069
|
-
*
|
|
1070
|
-
* @param message - One serialized chat message candidate.
|
|
1071
|
-
* @returns Normalized attachment previews with required URLs.
|
|
1072
|
-
*/
|
|
1073
|
-
function resolveMessageAttachments(message: Record<string, unknown>): Array<{
|
|
1074
|
-
name: string | null;
|
|
1075
|
-
url: string;
|
|
1076
|
-
}> {
|
|
1077
|
-
const rawAttachments = message.attachments;
|
|
1078
|
-
if (!Array.isArray(rawAttachments)) {
|
|
1079
|
-
return [];
|
|
1080
|
-
}
|
|
1081
|
-
|
|
1082
|
-
return rawAttachments.flatMap((attachment) => {
|
|
1083
|
-
if (!attachment || typeof attachment !== 'object' || Array.isArray(attachment)) {
|
|
1084
|
-
return [];
|
|
1085
|
-
}
|
|
1086
|
-
|
|
1087
|
-
const url = normalizeOptionalText((attachment as { url?: unknown }).url);
|
|
1088
|
-
if (!url) {
|
|
1089
|
-
return [];
|
|
1090
|
-
}
|
|
1091
|
-
|
|
1092
|
-
return [
|
|
1093
|
-
{
|
|
1094
|
-
name: normalizeOptionalText((attachment as { name?: unknown }).name),
|
|
1095
|
-
url,
|
|
1096
|
-
},
|
|
1097
|
-
];
|
|
1098
|
-
});
|
|
1099
|
-
}
|
|
1100
|
-
|
|
1101
|
-
/**
|
|
1102
|
-
* Creates the redacted wallet representation written into per-user backups.
|
|
1103
|
-
*
|
|
1104
|
-
* @param walletRow - Persisted wallet row.
|
|
1105
|
-
* @param agentPreviewByPermanentId - Agent preview lookup for scoped records.
|
|
1106
|
-
* @returns Redacted wallet record safe for backup export.
|
|
1107
|
-
*/
|
|
1108
|
-
function createRedactedWalletBackupRecord(
|
|
1109
|
-
walletRow: WalletRow,
|
|
1110
|
-
agentPreviewByPermanentId: ReadonlyMap<string, BackupAgentPreview>,
|
|
1111
|
-
): Record<string, unknown> {
|
|
1112
|
-
return {
|
|
1113
|
-
id: walletRow.id,
|
|
1114
|
-
createdAt: walletRow.createdAt,
|
|
1115
|
-
updatedAt: walletRow.updatedAt,
|
|
1116
|
-
isUserScoped: walletRow.isUserScoped,
|
|
1117
|
-
isGlobal: walletRow.isGlobal,
|
|
1118
|
-
deletedAt: walletRow.deletedAt,
|
|
1119
|
-
recordType: walletRow.recordType,
|
|
1120
|
-
service: walletRow.service,
|
|
1121
|
-
key: walletRow.key,
|
|
1122
|
-
jsonSchema: walletRow.jsonSchema,
|
|
1123
|
-
username: walletRow.username,
|
|
1124
|
-
agent: walletRow.agentPermanentId ? agentPreviewByPermanentId.get(walletRow.agentPermanentId) || null : null,
|
|
1125
|
-
hasPassword: Boolean(normalizeOptionalText(walletRow.password)),
|
|
1126
|
-
hasSecret: Boolean(normalizeOptionalText(walletRow.secret)),
|
|
1127
|
-
hasCookies: Boolean(normalizeOptionalText(walletRow.cookies)),
|
|
1128
|
-
};
|
|
1129
|
-
}
|
|
1130
|
-
|
|
1131
|
-
/**
|
|
1132
|
-
* Downloads one binary file for backup inclusion.
|
|
1133
|
-
*
|
|
1134
|
-
* @param url - Public binary URL to fetch.
|
|
1135
|
-
* @returns Download result with either binary content or an explanatory error.
|
|
1136
|
-
*/
|
|
1137
|
-
async function downloadBackupBinaryContent(url: string | null): Promise<{
|
|
1138
|
-
content: ArrayBuffer | null;
|
|
1139
|
-
error: string | null;
|
|
1140
|
-
}> {
|
|
1141
|
-
if (!url) {
|
|
1142
|
-
return {
|
|
1143
|
-
content: null,
|
|
1144
|
-
error: 'Missing content URL.',
|
|
1145
|
-
};
|
|
1146
|
-
}
|
|
1147
|
-
|
|
1148
|
-
try {
|
|
1149
|
-
const response = await fetch(url);
|
|
1150
|
-
if (!response.ok) {
|
|
1151
|
-
return {
|
|
1152
|
-
content: null,
|
|
1153
|
-
error: `Failed to download content (${response.status} ${response.statusText}).`,
|
|
1154
|
-
};
|
|
1155
|
-
}
|
|
1156
|
-
|
|
1157
|
-
return {
|
|
1158
|
-
content: await response.arrayBuffer(),
|
|
1159
|
-
error: null,
|
|
1160
|
-
};
|
|
1161
|
-
} catch (error) {
|
|
1162
|
-
return {
|
|
1163
|
-
content: null,
|
|
1164
|
-
error: error instanceof Error ? error.message : 'Unknown download error.',
|
|
1165
|
-
};
|
|
1166
|
-
}
|
|
1167
|
-
}
|
|
1168
|
-
|
|
1169
|
-
/**
|
|
1170
|
-
* Resolves the preferred binary backup filename while keeping the original file extension when available.
|
|
1171
|
-
*
|
|
1172
|
-
* @param fileName - Stored filename/path.
|
|
1173
|
-
* @param fallbackUrl - Public file URL used when the filename is empty.
|
|
1174
|
-
* @param fallbackStem - Deterministic fallback stem.
|
|
1175
|
-
* @returns Safe filename for the ZIP archive.
|
|
1176
|
-
*/
|
|
1177
|
-
function resolveBinaryBackupFilename(
|
|
1178
|
-
fileName: string | null | undefined,
|
|
1179
|
-
fallbackUrl: string | null | undefined,
|
|
1180
|
-
fallbackStem: string,
|
|
1181
|
-
): string {
|
|
1182
|
-
const rawBaseName =
|
|
1183
|
-
resolvePathBasename(fileName, '') ||
|
|
1184
|
-
resolvePathBasename(fallbackUrl, '') ||
|
|
1185
|
-
fallbackStem;
|
|
1186
|
-
|
|
1187
|
-
return sanitizeBackupPathSegment(rawBaseName, fallbackStem);
|
|
1188
|
-
}
|
|
1189
|
-
|
|
1190
|
-
/**
|
|
1191
|
-
* Creates one human-readable backup filename stem from optional labels.
|
|
1192
|
-
*
|
|
1193
|
-
* @param labels - Preferred labels ordered by importance.
|
|
1194
|
-
* @param fallbackStem - Deterministic fallback stem.
|
|
1195
|
-
* @returns Safe filename stem without extension.
|
|
1196
|
-
*/
|
|
1197
|
-
function createBackupStem(
|
|
1198
|
-
labels: ReadonlyArray<string | null | undefined>,
|
|
1199
|
-
fallbackStem: string,
|
|
1200
|
-
): string {
|
|
1201
|
-
const normalizedLabels = labels
|
|
1202
|
-
.map((label) => normalizeOptionalText(label))
|
|
1203
|
-
.filter((label): label is string => Boolean(label));
|
|
1204
|
-
|
|
1205
|
-
return sanitizeBackupPathSegment(
|
|
1206
|
-
normalizedLabels.length > 0 ? normalizedLabels.join(' -- ') : fallbackStem,
|
|
1207
|
-
fallbackStem,
|
|
1208
|
-
);
|
|
1209
|
-
}
|
|
1210
|
-
|
|
1211
|
-
/**
|
|
1212
|
-
* Ensures one filename ends with `.json`.
|
|
1213
|
-
*
|
|
1214
|
-
* @param filenameStem - Filename stem or full filename.
|
|
1215
|
-
* @returns JSON filename.
|
|
1216
|
-
*/
|
|
1217
|
-
function ensureJsonFilename(filenameStem: string): string {
|
|
1218
|
-
return filenameStem.endsWith(JSON_FILE_EXTENSION) ? filenameStem : `${filenameStem}${JSON_FILE_EXTENSION}`;
|
|
1219
|
-
}
|
|
1220
|
-
|
|
1221
|
-
/**
|
|
1222
|
-
* Creates the metadata sidecar filename for a JSON export file.
|
|
1223
|
-
*
|
|
1224
|
-
* @param jsonFilename - Main JSON filename.
|
|
1225
|
-
* @returns Sidecar metadata filename.
|
|
1226
|
-
*/
|
|
1227
|
-
function createJsonMetadataFilename(jsonFilename: string): string {
|
|
1228
|
-
return jsonFilename.endsWith(JSON_FILE_EXTENSION)
|
|
1229
|
-
? `${jsonFilename.slice(0, -JSON_FILE_EXTENSION.length)}.metadata.json`
|
|
1230
|
-
: `${jsonFilename}.metadata.json`;
|
|
1231
|
-
}
|
|
1232
|
-
|
|
1233
|
-
/**
|
|
1234
|
-
* Creates the metadata sidecar filename for a binary export file.
|
|
1235
|
-
*
|
|
1236
|
-
* @param filename - Main binary filename.
|
|
1237
|
-
* @returns Sidecar metadata filename.
|
|
1238
|
-
*/
|
|
1239
|
-
function createBinaryMetadataFilename(filename: string): string {
|
|
1240
|
-
return `${filename}.metadata.json`;
|
|
1241
|
-
}
|
|
1242
|
-
|
|
1243
|
-
/**
|
|
1244
|
-
* Creates a unique filename inside one ZIP folder with deterministic suffixes.
|
|
1245
|
-
*
|
|
1246
|
-
* @param usedFilenames - Already allocated filenames in the folder.
|
|
1247
|
-
* @param filename - Preferred filename candidate.
|
|
1248
|
-
* @param suffixBase - Deterministic suffix base used on collisions.
|
|
1249
|
-
* @returns Unique filename for the ZIP folder.
|
|
1250
|
-
*/
|
|
1251
|
-
function createUniqueBackupFilename(
|
|
1252
|
-
usedFilenames: Set<string>,
|
|
1253
|
-
filename: string,
|
|
1254
|
-
suffixBase: string,
|
|
1255
|
-
): string {
|
|
1256
|
-
if (!usedFilenames.has(filename)) {
|
|
1257
|
-
usedFilenames.add(filename);
|
|
1258
|
-
return filename;
|
|
1259
|
-
}
|
|
1260
|
-
|
|
1261
|
-
const extensionIndex = filename.lastIndexOf('.');
|
|
1262
|
-
const hasExtension = extensionIndex > 0;
|
|
1263
|
-
const filenameBase = hasExtension ? filename.slice(0, extensionIndex) : filename;
|
|
1264
|
-
const extension = hasExtension ? filename.slice(extensionIndex) : '';
|
|
1265
|
-
|
|
1266
|
-
for (let suffixIndex = 0; suffixIndex < Number.MAX_SAFE_INTEGER; suffixIndex += 1) {
|
|
1267
|
-
const suffix = suffixIndex === 0 ? `--${suffixBase}` : `--${suffixBase}-${suffixIndex + 1}`;
|
|
1268
|
-
const nextFilename = `${filenameBase}${suffix}${extension}`;
|
|
1269
|
-
|
|
1270
|
-
if (!usedFilenames.has(nextFilename)) {
|
|
1271
|
-
usedFilenames.add(nextFilename);
|
|
1272
|
-
return nextFilename;
|
|
1273
|
-
}
|
|
1274
|
-
}
|
|
1275
|
-
|
|
1276
|
-
throw new Error(`Unable to allocate unique backup filename for ${suffixBase}.`);
|
|
1277
|
-
}
|
|
1278
|
-
|
|
1279
|
-
/**
|
|
1280
|
-
* Resolves the basename from a stored path or URL.
|
|
1281
|
-
*
|
|
1282
|
-
* @param value - Stored path or URL.
|
|
1283
|
-
* @param fallback - Fallback filename when no basename exists.
|
|
1284
|
-
* @returns Basename without parent directories or query strings.
|
|
1285
|
-
*/
|
|
1286
|
-
function resolvePathBasename(value: string | null | undefined, fallback: string): string {
|
|
1287
|
-
const normalizedValue = normalizeOptionalText(value);
|
|
1288
|
-
if (!normalizedValue) {
|
|
1289
|
-
return fallback;
|
|
1290
|
-
}
|
|
1291
|
-
|
|
1292
|
-
try {
|
|
1293
|
-
const url = new URL(normalizedValue);
|
|
1294
|
-
const urlPathSegments = url.pathname.split('/').filter(Boolean);
|
|
1295
|
-
const urlBaseName = urlPathSegments[urlPathSegments.length - 1];
|
|
1296
|
-
if (urlBaseName) {
|
|
1297
|
-
return decodeURIComponent(urlBaseName);
|
|
1298
|
-
}
|
|
1299
|
-
} catch {
|
|
1300
|
-
// Continue with plain path parsing below.
|
|
1301
|
-
}
|
|
1302
|
-
|
|
1303
|
-
const pathSegments = normalizedValue.split(/[\\/]/).filter(Boolean);
|
|
1304
|
-
return pathSegments[pathSegments.length - 1] || fallback;
|
|
1305
|
-
}
|
|
1306
|
-
|
|
1307
|
-
/**
|
|
1308
|
-
* Groups rows by a computed key.
|
|
1309
|
-
*
|
|
1310
|
-
* @param rows - Rows to group.
|
|
1311
|
-
* @param getKey - Key selector.
|
|
1312
|
-
* @returns Grouped rows keyed by the computed value.
|
|
1313
|
-
*/
|
|
1314
|
-
function groupRowsBy<Row, Key extends string | number>(
|
|
1315
|
-
rows: ReadonlyArray<Row>,
|
|
1316
|
-
getKey: (row: Row) => Key,
|
|
1317
|
-
): Map<Key, Array<Row>> {
|
|
1318
|
-
const rowsByKey = new Map<Key, Array<Row>>();
|
|
1319
|
-
|
|
1320
|
-
for (const row of rows) {
|
|
1321
|
-
const key = getKey(row);
|
|
1322
|
-
const groupedRows = rowsByKey.get(key) || [];
|
|
1323
|
-
groupedRows.push(row);
|
|
1324
|
-
rowsByKey.set(key, groupedRows);
|
|
1325
|
-
}
|
|
1326
|
-
|
|
1327
|
-
return rowsByKey;
|
|
1328
|
-
}
|
|
1329
|
-
|
|
1330
|
-
/**
|
|
1331
|
-
* Converts one unknown persisted message array into serializable objects.
|
|
1332
|
-
*
|
|
1333
|
-
* @param value - Raw message array candidate.
|
|
1334
|
-
* @returns Serializable message array.
|
|
1335
|
-
*/
|
|
1336
|
-
function resolveSerializableArray(value: unknown): Array<Record<string, unknown>> {
|
|
1337
|
-
if (!Array.isArray(value)) {
|
|
1338
|
-
return [];
|
|
1339
|
-
}
|
|
1340
|
-
|
|
1341
|
-
return value.filter(isRecord);
|
|
1342
|
-
}
|
|
1343
|
-
|
|
1344
|
-
/**
|
|
1345
|
-
* Type guard for plain objects used throughout the backup builders.
|
|
1346
|
-
*
|
|
1347
|
-
* @param value - Unknown value.
|
|
1348
|
-
* @returns `true` when the value is a non-array object.
|
|
1349
|
-
*/
|
|
1350
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
1351
|
-
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
1352
|
-
}
|
|
1353
|
-
|
|
1354
|
-
/**
|
|
1355
|
-
* Deduplicates JSON-serializable objects using their serialized representation.
|
|
1356
|
-
*
|
|
1357
|
-
* @param values - Serializable objects.
|
|
1358
|
-
* @returns Deduplicated object list in first-seen order.
|
|
1359
|
-
*/
|
|
1360
|
-
function deduplicateSerializableObjects<Value extends object>(values: ReadonlyArray<Value>): Array<Value> {
|
|
1361
|
-
const seenSerializedValues = new Set<string>();
|
|
1362
|
-
const deduplicatedValues: Array<Value> = [];
|
|
1363
|
-
|
|
1364
|
-
for (const value of values) {
|
|
1365
|
-
const serializedValue = JSON.stringify(value);
|
|
1366
|
-
if (seenSerializedValues.has(serializedValue)) {
|
|
1367
|
-
continue;
|
|
1368
|
-
}
|
|
1369
|
-
|
|
1370
|
-
seenSerializedValues.add(serializedValue);
|
|
1371
|
-
deduplicatedValues.push(value);
|
|
1372
|
-
}
|
|
1373
|
-
|
|
1374
|
-
return deduplicatedValues;
|
|
1375
|
-
}
|
|
1376
|
-
|
|
1377
|
-
/**
|
|
1378
|
-
* Normalizes one optional text field to a trimmed string or `null`.
|
|
1379
|
-
*
|
|
1380
|
-
* @param value - Raw value.
|
|
1381
|
-
* @returns Trimmed text or `null`.
|
|
1382
|
-
*/
|
|
1383
|
-
function normalizeOptionalText(value: unknown): string | null {
|
|
1384
|
-
if (typeof value !== 'string') {
|
|
1385
|
-
return null;
|
|
1386
|
-
}
|
|
1387
|
-
|
|
1388
|
-
const normalizedValue = value.trim();
|
|
1389
|
-
return normalizedValue.length > 0 ? normalizedValue : null;
|
|
1390
|
-
}
|
|
1391
|
-
|
|
1392
|
-
/**
|
|
1393
|
-
* Sorts exported rows so JSON files remain stable and easier to diff.
|
|
1394
|
-
*
|
|
1395
|
-
* @param rows - Raw table rows fetched from Supabase.
|
|
1396
|
-
* @returns Sorted shallow-cloned row list.
|
|
1397
|
-
*/
|
|
1398
|
-
function sortBackupRows(rows: ReadonlyArray<Record<string, unknown>>): Array<Record<string, unknown>> {
|
|
1399
|
-
return [...rows].sort(compareBackupRows);
|
|
1400
|
-
}
|
|
1401
|
-
|
|
1402
|
-
/**
|
|
1403
|
-
* Compares two generic backup rows using the most common stable identifier fields.
|
|
1404
|
-
*
|
|
1405
|
-
* @param left - First row.
|
|
1406
|
-
* @param right - Second row.
|
|
1407
|
-
* @returns Stable ordering for backup JSON output.
|
|
1408
|
-
*/
|
|
1409
|
-
function compareBackupRows(left: Record<string, unknown>, right: Record<string, unknown>): number {
|
|
1410
|
-
const comparableKeys = ['sortOrder', 'id', 'key', 'username', 'agentName', 'permanentId', 'messageHash', 'createdAt'];
|
|
1411
|
-
|
|
1412
|
-
for (const comparableKey of comparableKeys) {
|
|
1413
|
-
const comparison = compareComparableValues(left[comparableKey], right[comparableKey]);
|
|
1414
|
-
|
|
1415
|
-
if (comparison !== 0) {
|
|
1416
|
-
return comparison;
|
|
1417
|
-
}
|
|
1418
|
-
}
|
|
1419
|
-
|
|
1420
|
-
return JSON.stringify(left).localeCompare(JSON.stringify(right));
|
|
1421
|
-
}
|
|
1422
|
-
|
|
1423
|
-
/**
|
|
1424
|
-
* Compares one optional pair of identifier values.
|
|
1425
|
-
*
|
|
1426
|
-
* @param left - First value.
|
|
1427
|
-
* @param right - Second value.
|
|
1428
|
-
* @returns Comparison result compatible with `Array.sort`.
|
|
1429
|
-
*/
|
|
1430
|
-
function compareComparableValues(left: unknown, right: unknown): number {
|
|
1431
|
-
if (left === right) {
|
|
1432
|
-
return 0;
|
|
1433
|
-
}
|
|
1434
|
-
|
|
1435
|
-
if (typeof left === 'number' && typeof right === 'number') {
|
|
1436
|
-
return left - right;
|
|
1437
|
-
}
|
|
1438
|
-
|
|
1439
|
-
if (left === undefined || left === null) {
|
|
1440
|
-
return 1;
|
|
1441
|
-
}
|
|
1442
|
-
|
|
1443
|
-
if (right === undefined || right === null) {
|
|
1444
|
-
return -1;
|
|
1445
|
-
}
|
|
1446
|
-
|
|
1447
|
-
return String(left).localeCompare(String(right));
|
|
1448
|
-
}
|