@agent-native/core 0.149.5 → 0.151.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/corpus/README.md +1 -1
- package/corpus/templates/analytics/.agents/skills/delegate-to-agent/SKILL.md +28 -0
- package/corpus/templates/analytics/.agents/skills/frontend-design/SKILL.md +5 -0
- package/corpus/templates/analytics/app/hooks/use-navigation-state.ts +83 -45
- package/corpus/templates/analytics/changelog/2026-08-11-analytics-dashboards-keep-the-selected-tab-when-reopened.md +6 -0
- package/corpus/templates/assets/.agents/skills/delegate-to-agent/SKILL.md +28 -0
- package/corpus/templates/assets/.agents/skills/frontend-design/SKILL.md +5 -0
- package/corpus/templates/brain/.agents/skills/delegate-to-agent/SKILL.md +28 -0
- package/corpus/templates/brain/.agents/skills/frontend-design/SKILL.md +5 -0
- package/corpus/templates/calendar/.agents/skills/delegate-to-agent/SKILL.md +28 -0
- package/corpus/templates/calendar/.agents/skills/frontend-design/SKILL.md +5 -0
- package/corpus/templates/chat/.agents/skills/delegate-to-agent/SKILL.md +28 -0
- package/corpus/templates/chat/.agents/skills/frontend-design/SKILL.md +5 -0
- package/corpus/templates/clips/.agents/skills/delegate-to-agent/SKILL.md +28 -0
- package/corpus/templates/clips/.agents/skills/frontend-design/SKILL.md +5 -0
- package/corpus/templates/clips/changelog/2026-08-11-clips-recovers-from-transient-updates-and-avoids-macos-priva.md +6 -0
- package/corpus/templates/clips/desktop/src/app.tsx +32 -176
- package/corpus/templates/clips/desktop/src/lib/updater.ts +52 -37
- package/corpus/templates/clips/desktop/src/styles.css +1 -166
- package/corpus/templates/clips/desktop/src-tauri/Cargo.toml +2 -0
- package/corpus/templates/clips/desktop/src-tauri/Info.plist +4 -0
- package/corpus/templates/clips/desktop/src-tauri/src/eventkit.rs +28 -1
- package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +2 -0
- package/corpus/templates/clips/desktop/src-tauri/src/native_speech.rs +48 -1
- package/corpus/templates/clips/desktop/src-tauri/src/permission_status.rs +3 -0
- package/corpus/templates/clips/desktop/src-tauri/src/whisper_speech.rs +5 -0
- package/corpus/templates/clips/server/plugins/auth.ts +3 -0
- package/corpus/templates/content/.agents/skills/delegate-to-agent/SKILL.md +28 -0
- package/corpus/templates/content/.agents/skills/frontend-design/SKILL.md +5 -0
- package/corpus/templates/content/actions/_document-discovery-query.ts +127 -0
- package/corpus/templates/content/actions/list-documents.ts +189 -198
- package/corpus/templates/content/actions/search-documents.ts +95 -24
- package/corpus/templates/content/app/components/editor/CommentsSidebar.tsx +2 -1
- package/corpus/templates/content/app/hooks/use-documents.ts +84 -6
- package/corpus/templates/content/shared/api.ts +10 -0
- package/corpus/templates/design/.agents/skills/delegate-to-agent/SKILL.md +28 -0
- package/corpus/templates/design/.agents/skills/frontend-design/SKILL.md +5 -0
- package/corpus/templates/design/app/pages/DesignSystemSetup.tsx +5 -1
- package/corpus/templates/design/changelog/2026-08-11-design-agent-prompts-copy-successfully-in-the-desktop-app.md +6 -0
- package/corpus/templates/dispatch/.agents/skills/delegate-to-agent/SKILL.md +28 -0
- package/corpus/templates/dispatch/.agents/skills/frontend-design/SKILL.md +5 -0
- package/corpus/templates/dispatch/app/global.css +2 -2
- package/corpus/templates/dispatch/changelog/2026-08-10-desktop-workspace-sign-in.md +6 -0
- package/corpus/templates/dispatch/changelog/2026-08-11-dispatch-no-longer-shows-the-generic-chat-starter-among-defa.md +6 -0
- package/corpus/templates/dispatch/changelog/2026-08-11-integrations-and-scheduled-work-open-as-first-class-electron-pages.md +6 -0
- package/corpus/templates/dispatch/changelog/2026-08-11-sidebar-text-is-slightly-easier-to-read-in-light-and-dark-mo.md +6 -0
- package/corpus/templates/dispatch/server/lib/identity-sso.ts +57 -31
- package/corpus/templates/dispatch/server/plugins/feature-flags.ts +5 -0
- package/corpus/templates/dispatch/server/plugins/identity-sso.ts +82 -33
- package/corpus/templates/dispatch/server/plugins/org-apps-directory.ts +7 -15
- package/corpus/templates/dispatch/server/plugins/setup-dispatch.ts +9 -1
- package/corpus/templates/dispatch/shared/feature-flags.ts +15 -0
- package/corpus/templates/factory/.agents/skills/delegate-to-agent/SKILL.md +28 -0
- package/corpus/templates/factory/.agents/skills/frontend-design/SKILL.md +5 -0
- package/corpus/templates/forms/.agents/skills/delegate-to-agent/SKILL.md +28 -0
- package/corpus/templates/forms/.agents/skills/form-responses/SKILL.md +8 -7
- package/corpus/templates/forms/.agents/skills/frontend-design/SKILL.md +5 -0
- package/corpus/templates/forms/app/hooks/use-forms.ts +8 -1
- package/corpus/templates/forms/changelog/2026-08-11-fixed-public-forms-so-conditional-questions-stay-hidden-unti.md +6 -0
- package/corpus/templates/forms/server/handlers/submissions.ts +2 -1
- package/corpus/templates/forms/server/lib/public-form-ssr.ts +37 -2
- package/corpus/templates/forms/shared/page-url.ts +54 -0
- package/corpus/templates/forms/shared/types.ts +4 -4
- package/corpus/templates/macros/.agents/skills/frontend-design/SKILL.md +5 -0
- package/corpus/templates/mail/.agents/skills/delegate-to-agent/SKILL.md +28 -0
- package/corpus/templates/mail/.agents/skills/frontend-design/SKILL.md +5 -0
- package/corpus/templates/plan/.agents/skills/delegate-to-agent/SKILL.md +28 -0
- package/corpus/templates/plan/.agents/skills/frontend-design/SKILL.md +5 -0
- package/corpus/templates/slides/.agents/skills/delegate-to-agent/SKILL.md +28 -0
- package/corpus/templates/slides/.agents/skills/frontend-design/SKILL.md +5 -0
- package/corpus/templates/slides/actions/add-slide.ts +0 -27
- package/corpus/templates/slides/actions/import-file.ts +28 -9
- package/corpus/templates/slides/actions/view-screen.ts +4 -4
- package/corpus/templates/slides/app/components/design-system/DesignSystemSetup.tsx +8 -1
- package/corpus/templates/slides/app/components/editor/AiEditingMarker.tsx +25 -0
- package/corpus/templates/slides/app/components/editor/EditorSidebar.tsx +43 -48
- package/corpus/templates/slides/app/components/editor/EditorToolbar.tsx +9 -6
- package/corpus/templates/slides/app/components/editor/GeneratingSlidePreview.tsx +1 -9
- package/corpus/templates/slides/app/components/editor/ImageDropPromptPopover.tsx +2 -0
- package/corpus/templates/slides/app/components/editor/ImageOverlay.tsx +0 -26
- package/corpus/templates/slides/app/components/editor/SlideEditor.tsx +904 -217
- package/corpus/templates/slides/app/components/editor/canvas-interactions/index.ts +1 -0
- package/corpus/templates/slides/app/components/editor/canvas-interactions/slides-canvas-adapter.ts +11 -0
- package/corpus/templates/slides/app/components/editor/inline-edit-session.ts +16 -0
- package/corpus/templates/slides/app/components/editor/slide-object-interactions.ts +22 -6
- package/corpus/templates/slides/app/components/editor/style-clipboard.ts +61 -0
- package/corpus/templates/slides/app/components/layout/Layout.tsx +26 -2
- package/corpus/templates/slides/app/components/ui/context-menu.tsx +1 -0
- package/corpus/templates/slides/app/context/DeckContext.tsx +75 -25
- package/corpus/templates/slides/app/global.css +1 -1
- package/corpus/templates/slides/app/i18n/en-US.ts +6 -1
- package/corpus/templates/slides/app/lib/create-deck-generation.ts +1 -1
- package/corpus/templates/slides/app/lib/image-drop-to-agent.ts +12 -12
- package/corpus/templates/slides/app/lib/slide-agent-context.ts +52 -0
- package/corpus/templates/slides/app/lib/slide-image-replacement.ts +61 -0
- package/corpus/templates/slides/app/pages/DeckEditor.tsx +55 -116
- package/corpus/templates/slides/app/pages/Index.tsx +28 -3
- package/corpus/templates/slides/changelog/2026-08-11-agent-chat-now-follows-the-current-slide-and-selection.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-11-deck-titles-now-use-slide-content-instead-of-placeholder-filenames.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-11-history-now-opens-from-the-deck-overflow-menu.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-11-images-can-be-dropped-directly-onto-slides-and-resized-or-re.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-11-new-decks-now-transition-directly-to-a-full-page-editor-load.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-11-selected-slide-layers-can-be-nudged-precisely-with-arrow-key.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-11-slides-can-copy-and-paste-element-styles-with-cmd-option-c-v.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-11-slides-shows-one-compact-ai-marker-and-keeps-your-current-sl.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-11-text-edits-preserve-slide-layout-when-no-changes-are-made.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-11-the-style-toolbar-now-uses-the-full-available-width.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-11-undo-and-redo-now-move-one-visible-slide-state-at-a-time.md +6 -0
- package/corpus/templates/slides/server/lib/chat-attachments.ts +6 -2
- package/corpus/templates/slides/shared/deck-title.ts +56 -0
- package/corpus/templates/tasks/.agents/skills/delegate-to-agent/SKILL.md +28 -0
- package/corpus/templates/tasks/.agents/skills/frontend-design/SKILL.md +5 -0
- package/dist/agent/run-store.js +6 -1
- package/dist/agent/thread-data-builder.js +19 -44
- package/dist/agent/types.d.ts +13 -0
- package/dist/catalog.json +1 -0
- package/dist/cli/create.js +1 -1
- package/dist/client/AssistantChat.js +58 -10
- package/dist/client/FileStorageSetupCard.d.ts +6 -0
- package/dist/client/FileStorageSetupCard.js +18 -0
- package/dist/client/NewWorkspaceAppFlow.js +10 -0
- package/dist/client/agent-chat-adapter.js +87 -11
- package/dist/client/agent-chat.d.ts +1 -1
- package/dist/client/agent-chat.js +8 -3
- package/dist/client/chat/attachment-adapters.js +10 -0
- package/dist/client/chat/message-components.d.ts +4 -1
- package/dist/client/chat/message-components.js +37 -13
- package/dist/client/chat/tool-call-display.js +14 -0
- package/dist/client/chat-first/apps-rail.d.ts +1 -1
- package/dist/client/chat-first/apps-rail.js +6 -6
- package/dist/client/chat-first/copy.js +1 -0
- package/dist/client/chat-first/types.d.ts +1 -0
- package/dist/client/extensions/EmbeddedExtension.js +1 -1
- package/dist/client/extensions/ExtensionViewer.js +1 -1
- package/dist/client/extensions/InlineExtensionFrame.js +1 -1
- package/dist/client/onboarding/FirstRunOnboarding.js +17 -2
- package/dist/client/settings/FileStorageSettingsForm.d.ts +2 -0
- package/dist/client/settings/FileStorageSettingsForm.js +113 -0
- package/dist/client/settings/SettingsPanel.js +2 -1
- package/dist/client/sse-event-processor.js +21 -0
- package/dist/extensions/actions.js +2 -2
- package/dist/extensions/html-shell.js +3 -1
- package/dist/file-upload/pre-upload-attachments.d.ts +15 -10
- package/dist/file-upload/pre-upload-attachments.js +68 -24
- package/dist/file-upload/registry.d.ts +4 -5
- package/dist/file-upload/registry.js +7 -14
- package/dist/file-upload/s3.d.ts +9 -0
- package/dist/file-upload/s3.js +168 -0
- package/dist/file-upload/types.d.ts +3 -3
- package/dist/file-upload/types.js +2 -2
- package/dist/onboarding/app-profile.js +23 -1
- package/dist/onboarding/default-steps.js +69 -0
- package/dist/onboarding/types.d.ts +2 -0
- package/dist/progress/routes.d.ts +1 -1
- package/dist/provider-api/actions/custom-provider-registration.d.ts +11 -11
- package/dist/provider-api/actions/provider-api.d.ts +8 -8
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/secrets/routes.d.ts +6 -6
- package/dist/server/agent-chat/browser-team-tools.js +20 -0
- package/dist/server/agent-chat-plugin.js +12 -3
- package/dist/server/auth.js +45 -21
- package/dist/server/better-auth-instance.d.ts +2 -0
- package/dist/server/better-auth-instance.js +2 -2
- package/dist/server/core-routes-plugin.d.ts +1 -0
- package/dist/server/core-routes-plugin.js +26 -16
- package/dist/server/identity-sso-store.d.ts +3 -0
- package/dist/server/identity-sso-store.js +45 -0
- package/dist/server/identity-sso.d.ts +20 -10
- package/dist/server/identity-sso.js +79 -31
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.js +1 -1
- package/dist/templates/chat/.agents/skills/delegate-to-agent/SKILL.md +28 -0
- package/dist/templates/chat/.agents/skills/frontend-design/SKILL.md +5 -0
- package/dist/templates/default/.agents/skills/delegate-to-agent/SKILL.md +28 -0
- package/dist/templates/default/.agents/skills/frontend-design/SKILL.md +5 -0
- package/dist/templates/workspace-core/.agents/skills/authentication/SKILL.md +24 -1
- package/dist/templates/workspace-core/.agents/skills/delegate-to-agent/SKILL.md +28 -0
- package/dist/templates/workspace-core/.agents/skills/extensions/SKILL.md +6 -2
- package/dist/templates/workspace-core/.agents/skills/extensions/references/api.md +1 -1
- package/dist/templates/workspace-core/.agents/skills/frontend-design/SKILL.md +5 -0
- package/dist/templates/workspace-core/.agents/skills/generative-ui/SKILL.md +3 -1
- package/docs/content/authentication.mdx +27 -0
- package/docs/content/cross-app-sso.mdx +38 -8
- package/docs/content/extensions.mdx +15 -15
- package/docs/content/file-uploads.mdx +10 -12
- package/docs/content/generative-ui.mdx +6 -1
- package/package.json +2 -2
- package/src/templates/chat/.agents/skills/delegate-to-agent/SKILL.md +28 -0
- package/src/templates/chat/.agents/skills/frontend-design/SKILL.md +5 -0
- package/src/templates/default/.agents/skills/delegate-to-agent/SKILL.md +28 -0
- package/src/templates/default/.agents/skills/frontend-design/SKILL.md +5 -0
- package/src/templates/workspace-core/.agents/skills/authentication/SKILL.md +24 -1
- package/src/templates/workspace-core/.agents/skills/delegate-to-agent/SKILL.md +28 -0
- package/src/templates/workspace-core/.agents/skills/extensions/SKILL.md +6 -2
- package/src/templates/workspace-core/.agents/skills/extensions/references/api.md +1 -1
- package/src/templates/workspace-core/.agents/skills/frontend-design/SKILL.md +5 -0
- package/src/templates/workspace-core/.agents/skills/generative-ui/SKILL.md +3 -1
|
@@ -30,10 +30,14 @@ export interface PreUploadAttachmentsResult {
|
|
|
30
30
|
/** Uploaded non-image files (PDF, generic binary). Parallel to `uploaded`
|
|
31
31
|
* but for the file/document attachment type. */
|
|
32
32
|
uploadedFiles: PreUploadedFileAttachment[];
|
|
33
|
-
/** True if at least one image
|
|
34
|
-
* file-upload provider is configured.
|
|
35
|
-
*
|
|
33
|
+
/** True if at least one image or file could not be uploaded because no
|
|
34
|
+
* file-upload provider is configured. The agent uses this to render the
|
|
35
|
+
* storage setup card. */
|
|
36
36
|
providerMissing: boolean;
|
|
37
|
+
/** True if at least one configured provider failed while uploading. */
|
|
38
|
+
uploadFailed: boolean;
|
|
39
|
+
/** The first provider error, bounded for safe inclusion in the chat hint. */
|
|
40
|
+
uploadError?: string;
|
|
37
41
|
/** A pre-formatted block to inject into the user message text so the agent
|
|
38
42
|
* has each hosted URL inline. Null when nothing was uploaded or no provider
|
|
39
43
|
* is configured. */
|
|
@@ -41,18 +45,18 @@ export interface PreUploadAttachmentsResult {
|
|
|
41
45
|
}
|
|
42
46
|
/**
|
|
43
47
|
* Returns true when a file-upload provider is currently configured.
|
|
44
|
-
* Used to decide whether
|
|
48
|
+
* Used to decide whether an attachment can be uploaded before the agent turn.
|
|
45
49
|
*/
|
|
46
50
|
export declare function isFileUploadProviderConfigured(): boolean;
|
|
47
51
|
/**
|
|
48
52
|
* Pre-upload chat image attachments through the active file-upload provider
|
|
49
53
|
* (Builder.io by default) so the agent can embed hosted URLs in HTML, slide
|
|
50
|
-
* content, and outbound messages. Keeps the original
|
|
51
|
-
*
|
|
54
|
+
* content, and outbound messages. Keeps the original data URL in memory for
|
|
55
|
+
* the current multimodal turn and adds a hosted `url`.
|
|
52
56
|
*
|
|
53
57
|
* Safe to call when no provider is configured: it returns the attachments
|
|
54
|
-
* untouched with `providerMissing: true` so callers can surface
|
|
55
|
-
*
|
|
58
|
+
* untouched with `providerMissing: true` so callers can surface the storage
|
|
59
|
+
* setup card to the agent without persisting the binary payload.
|
|
56
60
|
*/
|
|
57
61
|
export declare function preUploadImageAttachments(opts: {
|
|
58
62
|
attachments: AgentChatAttachment[] | undefined;
|
|
@@ -66,8 +70,9 @@ export declare function preUploadImageAttachments(opts: {
|
|
|
66
70
|
* file-reading still work; callers that persist the attachment can drop the
|
|
67
71
|
* data when a URL exists.
|
|
68
72
|
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
73
|
+
* When no provider is configured, returns untouched in-memory attachments with
|
|
74
|
+
* `providerMissing: true`. The caller may still use the bytes for this turn,
|
|
75
|
+
* but must not persist them as SQL/base64 attachment data.
|
|
71
76
|
*/
|
|
72
77
|
export declare function preUploadAttachments(opts: {
|
|
73
78
|
attachments: AgentChatAttachment[] | undefined;
|
|
@@ -33,7 +33,7 @@ function escapeXmlAttr(value) {
|
|
|
33
33
|
}
|
|
34
34
|
/**
|
|
35
35
|
* Returns true when a file-upload provider is currently configured.
|
|
36
|
-
* Used to decide whether
|
|
36
|
+
* Used to decide whether an attachment can be uploaded before the agent turn.
|
|
37
37
|
*/
|
|
38
38
|
export function isFileUploadProviderConfigured() {
|
|
39
39
|
return getActiveFileUploadProvider() !== null;
|
|
@@ -41,12 +41,12 @@ export function isFileUploadProviderConfigured() {
|
|
|
41
41
|
/**
|
|
42
42
|
* Pre-upload chat image attachments through the active file-upload provider
|
|
43
43
|
* (Builder.io by default) so the agent can embed hosted URLs in HTML, slide
|
|
44
|
-
* content, and outbound messages. Keeps the original
|
|
45
|
-
*
|
|
44
|
+
* content, and outbound messages. Keeps the original data URL in memory for
|
|
45
|
+
* the current multimodal turn and adds a hosted `url`.
|
|
46
46
|
*
|
|
47
47
|
* Safe to call when no provider is configured: it returns the attachments
|
|
48
|
-
* untouched with `providerMissing: true` so callers can surface
|
|
49
|
-
*
|
|
48
|
+
* untouched with `providerMissing: true` so callers can surface the storage
|
|
49
|
+
* setup card to the agent without persisting the binary payload.
|
|
50
50
|
*/
|
|
51
51
|
export async function preUploadImageAttachments(opts) {
|
|
52
52
|
return preUploadAttachments({ ...opts, includeFiles: false });
|
|
@@ -59,8 +59,9 @@ export async function preUploadImageAttachments(opts) {
|
|
|
59
59
|
* file-reading still work; callers that persist the attachment can drop the
|
|
60
60
|
* data when a URL exists.
|
|
61
61
|
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
62
|
+
* When no provider is configured, returns untouched in-memory attachments with
|
|
63
|
+
* `providerMissing: true`. The caller may still use the bytes for this turn,
|
|
64
|
+
* but must not persist them as SQL/base64 attachment data.
|
|
64
65
|
*/
|
|
65
66
|
export async function preUploadAttachments(opts) {
|
|
66
67
|
const list = Array.isArray(opts.attachments) ? opts.attachments : [];
|
|
@@ -68,12 +69,15 @@ export async function preUploadAttachments(opts) {
|
|
|
68
69
|
const uploaded = [];
|
|
69
70
|
const uploadedFiles = [];
|
|
70
71
|
let providerMissing = false;
|
|
72
|
+
let uploadFailed = false;
|
|
73
|
+
let uploadError;
|
|
71
74
|
if (list.length === 0) {
|
|
72
75
|
return {
|
|
73
76
|
attachments: list,
|
|
74
77
|
uploaded,
|
|
75
78
|
uploadedFiles,
|
|
76
79
|
providerMissing: false,
|
|
80
|
+
uploadFailed: false,
|
|
77
81
|
injectedText: null,
|
|
78
82
|
};
|
|
79
83
|
}
|
|
@@ -82,11 +86,9 @@ export async function preUploadAttachments(opts) {
|
|
|
82
86
|
const isFile = att.type === "file" || att.type === "document";
|
|
83
87
|
if (!isImage && !(includeFiles && isFile))
|
|
84
88
|
continue;
|
|
85
|
-
if (typeof att.
|
|
86
|
-
continue;
|
|
87
|
-
if (att.url) {
|
|
89
|
+
if (typeof att.url === "string" && att.url.trim()) {
|
|
88
90
|
// Already pre-uploaded earlier in the pipeline — reuse it.
|
|
89
|
-
const isReferenceOnlySvg = isSvgAttachment(att);
|
|
91
|
+
const isReferenceOnlySvg = att.referenceOnly === true || isSvgAttachment(att);
|
|
90
92
|
if (isReferenceOnlySvg) {
|
|
91
93
|
markReferenceOnlySvgAttachment(att, att.contentType);
|
|
92
94
|
}
|
|
@@ -110,7 +112,21 @@ export async function preUploadAttachments(opts) {
|
|
|
110
112
|
}
|
|
111
113
|
continue;
|
|
112
114
|
}
|
|
113
|
-
|
|
115
|
+
let data = att.data;
|
|
116
|
+
if (typeof data !== "string" &&
|
|
117
|
+
includeFiles &&
|
|
118
|
+
isFile &&
|
|
119
|
+
typeof att.text === "string" &&
|
|
120
|
+
att.text.length > 0) {
|
|
121
|
+
// Text attachments are already decoded by the client. Upload the text
|
|
122
|
+
// bytes too so a later turn has the same durable object URL as binary
|
|
123
|
+
// attachments instead of a SQL-only scratch copy.
|
|
124
|
+
const encoded = Buffer.from(att.text, "utf8").toString("base64");
|
|
125
|
+
data = `data:${normalizeContentType(att.contentType) || "text/plain"};base64,${encoded}`;
|
|
126
|
+
}
|
|
127
|
+
if (typeof data !== "string")
|
|
128
|
+
continue;
|
|
129
|
+
const match = data.match(FILE_DATA_URL_RE);
|
|
114
130
|
if (!match)
|
|
115
131
|
continue;
|
|
116
132
|
const dataUrlMimeType = normalizeContentType(match[1]);
|
|
@@ -134,8 +150,8 @@ export async function preUploadAttachments(opts) {
|
|
|
134
150
|
ownerEmail: opts.ownerEmail || undefined,
|
|
135
151
|
});
|
|
136
152
|
if (!result) {
|
|
137
|
-
|
|
138
|
-
|
|
153
|
+
providerMissing = true;
|
|
154
|
+
att.storageRequired = true;
|
|
139
155
|
continue;
|
|
140
156
|
}
|
|
141
157
|
att.url = result.url;
|
|
@@ -168,8 +184,12 @@ export async function preUploadAttachments(opts) {
|
|
|
168
184
|
}
|
|
169
185
|
}
|
|
170
186
|
catch (err) {
|
|
171
|
-
// Real upload failure (network, API). Keep the
|
|
172
|
-
//
|
|
187
|
+
// Real upload failure (network, API). Keep the bytes in memory for the
|
|
188
|
+
// current turn, but never treat the failure as a durable upload.
|
|
189
|
+
att.storageRequired = true;
|
|
190
|
+
att.storageUploadFailed = true;
|
|
191
|
+
uploadFailed = true;
|
|
192
|
+
uploadError ??= (err instanceof Error ? err.message : String(err)).slice(0, 500);
|
|
173
193
|
console.warn("[agent-native] pre-upload of chat attachment failed:", err instanceof Error ? err.message : String(err));
|
|
174
194
|
}
|
|
175
195
|
}
|
|
@@ -199,21 +219,43 @@ export async function preUploadAttachments(opts) {
|
|
|
199
219
|
lines.push(`<chat-file-attachment ${attrs.join(" ")} />`);
|
|
200
220
|
}
|
|
201
221
|
const hasReferenceOnlySvg = uploadedFiles.some((file) => file.referenceOnly && isSvgAttachment(file));
|
|
202
|
-
|
|
222
|
+
const linesWithMetadata = [
|
|
203
223
|
hasReferenceOnlySvg
|
|
204
224
|
? '<chat-attachments note="The user attached these files. Image attachment URLs may be used for embedding. File attachment URLs are references; SVG files are unsanitized vector source and must not be inlined as HTML or embedded in outbound content unless the target app sanitizes or stores them safely.">'
|
|
205
225
|
: '<chat-attachments note="The user attached these files. Image attachment URLs may be used for embedding in HTML, slide content, or outbound messages. File attachment URLs are references for reading or attaching in target apps.">',
|
|
206
226
|
...lines,
|
|
207
227
|
"</chat-attachments>",
|
|
208
|
-
]
|
|
228
|
+
];
|
|
229
|
+
if (providerMissing || uploadFailed) {
|
|
230
|
+
const failureLines = [
|
|
231
|
+
providerMissing
|
|
232
|
+
? "One or more attachments could not be stored because no durable object-storage provider is configured."
|
|
233
|
+
: null,
|
|
234
|
+
uploadFailed
|
|
235
|
+
? `A configured object-storage provider failed to upload an attachment${uploadError ? `: ${escapeXmlAttr(uploadError)}` : "."}`
|
|
236
|
+
: null,
|
|
237
|
+
].filter((line) => Boolean(line));
|
|
238
|
+
linesWithMetadata.push("<chat-file-attachment-upload-error>", ...failureLines, ...(providerMissing
|
|
239
|
+
? [
|
|
240
|
+
"Call `connect-file-storage` now so the user can connect Builder or configure custom storage keys, then continue using the hosted references.",
|
|
241
|
+
]
|
|
242
|
+
: [
|
|
243
|
+
"Retry the upload or inspect the configured storage provider. Do not claim the attachment is durably available until it succeeds.",
|
|
244
|
+
]), "</chat-file-attachment-upload-error>");
|
|
245
|
+
}
|
|
246
|
+
injectedText = linesWithMetadata.join("\n");
|
|
209
247
|
}
|
|
210
|
-
else if (providerMissing) {
|
|
248
|
+
else if (providerMissing || uploadFailed) {
|
|
211
249
|
injectedText = [
|
|
212
|
-
"<chat-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
250
|
+
"<chat-file-attachment-upload-error>",
|
|
251
|
+
providerMissing
|
|
252
|
+
? "The user attached one or more images or files, but durable object storage is not configured for this app."
|
|
253
|
+
: `The user attached one or more images or files, but the configured storage provider failed to upload them${uploadError ? `: ${escapeXmlAttr(uploadError)}` : "."}`,
|
|
254
|
+
providerMissing
|
|
255
|
+
? "Call `connect-file-storage` now to render the inline storage setup card. The user can connect Builder for managed storage or open the same card's custom-key setup for S3-compatible object storage."
|
|
256
|
+
: "Retry the upload or inspect the configured storage provider. Do not claim the attachment is durably available until it succeeds.",
|
|
257
|
+
"Do not persist the base64 contents in SQL. Until storage succeeds, use the attachment only for this turn.",
|
|
258
|
+
"</chat-file-attachment-upload-error>",
|
|
217
259
|
].join("\n");
|
|
218
260
|
}
|
|
219
261
|
return {
|
|
@@ -221,6 +263,8 @@ export async function preUploadAttachments(opts) {
|
|
|
221
263
|
uploaded,
|
|
222
264
|
uploadedFiles,
|
|
223
265
|
providerMissing,
|
|
266
|
+
uploadFailed,
|
|
267
|
+
...(uploadError ? { uploadError } : {}),
|
|
224
268
|
injectedText,
|
|
225
269
|
};
|
|
226
270
|
}
|
|
@@ -9,15 +9,14 @@ export declare function listFileUploadProviders(): FileUploadProvider[];
|
|
|
9
9
|
/**
|
|
10
10
|
* Returns the first configured provider, checking user-registered ones first
|
|
11
11
|
* and falling back to the built-in Builder.io provider when its env is set.
|
|
12
|
-
* Returns `null` when nothing is configured
|
|
13
|
-
* SQL
|
|
12
|
+
* Returns `null` when nothing is configured. Callers must fail closed rather
|
|
13
|
+
* than persisting the original binary payload in SQL.
|
|
14
14
|
*/
|
|
15
15
|
export declare function getActiveFileUploadProvider(): FileUploadProvider | null;
|
|
16
16
|
export declare function getActiveFileUploadProviderForRequest(): Promise<FileUploadProvider | null>;
|
|
17
17
|
/**
|
|
18
18
|
* Upload a file via the active provider, or `null` if no provider is
|
|
19
|
-
* configured.
|
|
20
|
-
*
|
|
21
|
-
* storing files in SQL is not optimal for production.
|
|
19
|
+
* configured. `null` is an explicit storage-setup state: callers must not
|
|
20
|
+
* turn the input into a base64 SQL fallback.
|
|
22
21
|
*/
|
|
23
22
|
export declare function uploadFile(input: FileUploadInput): Promise<FileUploadResult | null>;
|
|
@@ -18,8 +18,8 @@ export function listFileUploadProviders() {
|
|
|
18
18
|
/**
|
|
19
19
|
* Returns the first configured provider, checking user-registered ones first
|
|
20
20
|
* and falling back to the built-in Builder.io provider when its env is set.
|
|
21
|
-
* Returns `null` when nothing is configured
|
|
22
|
-
* SQL
|
|
21
|
+
* Returns `null` when nothing is configured. Callers must fail closed rather
|
|
22
|
+
* than persisting the original binary payload in SQL.
|
|
23
23
|
*/
|
|
24
24
|
export function getActiveFileUploadProvider() {
|
|
25
25
|
for (const provider of providers.values()) {
|
|
@@ -36,14 +36,8 @@ export async function getActiveFileUploadProviderForRequest() {
|
|
|
36
36
|
if (provider.isConfigured())
|
|
37
37
|
return provider;
|
|
38
38
|
if (provider.isConfiguredForRequest) {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
return provider;
|
|
42
|
-
}
|
|
43
|
-
catch {
|
|
44
|
-
// Treat failed scoped credential lookups as unavailable. The upload
|
|
45
|
-
// call will surface real provider errors after a provider is selected.
|
|
46
|
-
}
|
|
39
|
+
if (await provider.isConfiguredForRequest())
|
|
40
|
+
return provider;
|
|
47
41
|
}
|
|
48
42
|
}
|
|
49
43
|
if (builderFileUploadProvider.isConfigured()) {
|
|
@@ -62,9 +56,8 @@ export async function getActiveFileUploadProviderForRequest() {
|
|
|
62
56
|
}
|
|
63
57
|
/**
|
|
64
58
|
* Upload a file via the active provider, or `null` if no provider is
|
|
65
|
-
* configured.
|
|
66
|
-
*
|
|
67
|
-
* storing files in SQL is not optimal for production.
|
|
59
|
+
* configured. `null` is an explicit storage-setup state: callers must not
|
|
60
|
+
* turn the input into a base64 SQL fallback.
|
|
68
61
|
*/
|
|
69
62
|
export async function uploadFile(input) {
|
|
70
63
|
const provider = await getActiveFileUploadProviderForRequest();
|
|
@@ -85,7 +78,7 @@ export async function uploadFile(input) {
|
|
|
85
78
|
}
|
|
86
79
|
catch (err) {
|
|
87
80
|
// DB unavailable or credential store not ready — can't resolve key.
|
|
88
|
-
//
|
|
81
|
+
// Return an unavailable-provider state below; never fall back to SQL.
|
|
89
82
|
console.warn("[agent-native] Builder credential check failed:", err instanceof Error ? err.message : String(err));
|
|
90
83
|
}
|
|
91
84
|
if (builderKey) {
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Framework-owned S3-compatible object storage provider.
|
|
3
|
+
*
|
|
4
|
+
* The onboarding form writes these keys to scoped secrets. A public base URL
|
|
5
|
+
* is required because chat attachments need stable URLs that remain usable
|
|
6
|
+
* after the request and across later turns in the thread.
|
|
7
|
+
*/
|
|
8
|
+
import type { FileUploadProvider } from "./types.js";
|
|
9
|
+
export declare const s3FileUploadProvider: FileUploadProvider;
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Framework-owned S3-compatible object storage provider.
|
|
3
|
+
*
|
|
4
|
+
* The onboarding form writes these keys to scoped secrets. A public base URL
|
|
5
|
+
* is required because chat attachments need stable URLs that remain usable
|
|
6
|
+
* after the request and across later turns in the thread.
|
|
7
|
+
*/
|
|
8
|
+
import { resolveSecret } from "../server/credential-provider.js";
|
|
9
|
+
function cleanValue(value) {
|
|
10
|
+
const cleaned = value?.trim();
|
|
11
|
+
return cleaned ? cleaned : undefined;
|
|
12
|
+
}
|
|
13
|
+
function buildConfig(values) {
|
|
14
|
+
const bucket = cleanValue(values.bucket);
|
|
15
|
+
const accessKeyId = cleanValue(values.accessKeyId);
|
|
16
|
+
const secretAccessKey = cleanValue(values.secretAccessKey);
|
|
17
|
+
const endpoint = cleanValue(values.endpoint)?.replace(/\/+$/, "");
|
|
18
|
+
const publicBaseUrl = cleanValue(values.publicBaseUrl)?.replace(/\/+$/, "");
|
|
19
|
+
if (!bucket ||
|
|
20
|
+
!accessKeyId ||
|
|
21
|
+
!secretAccessKey ||
|
|
22
|
+
!endpoint ||
|
|
23
|
+
!publicBaseUrl) {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
if (!URL.canParse(endpoint) || !URL.canParse(publicBaseUrl)) {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
const endpointUrl = new URL(endpoint);
|
|
30
|
+
const publicUrl = new URL(publicBaseUrl);
|
|
31
|
+
if (!["http:", "https:"].includes(endpointUrl.protocol) ||
|
|
32
|
+
!["http:", "https:"].includes(publicUrl.protocol)) {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
return {
|
|
36
|
+
region: cleanValue(values.region) ?? "auto",
|
|
37
|
+
bucket,
|
|
38
|
+
accessKeyId,
|
|
39
|
+
secretAccessKey,
|
|
40
|
+
endpoint,
|
|
41
|
+
publicBaseUrl,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
function readEnvConfig() {
|
|
45
|
+
const env = process.env;
|
|
46
|
+
return buildConfig({
|
|
47
|
+
bucket: env.S3_BUCKET || env.R2_BUCKET,
|
|
48
|
+
accessKeyId: env.S3_ACCESS_KEY_ID || env.R2_ACCESS_KEY_ID,
|
|
49
|
+
secretAccessKey: env.S3_SECRET_ACCESS_KEY || env.R2_SECRET_ACCESS_KEY,
|
|
50
|
+
endpoint: env.S3_ENDPOINT || env.R2_ENDPOINT,
|
|
51
|
+
region: env.S3_REGION || env.R2_REGION,
|
|
52
|
+
publicBaseUrl: env.S3_PUBLIC_BASE_URL || env.R2_PUBLIC_BASE_URL,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
async function resolveStorageSecret(primary, fallback) {
|
|
56
|
+
const primaryValue = cleanValue(await resolveSecret(primary));
|
|
57
|
+
return primaryValue ?? cleanValue(await resolveSecret(fallback));
|
|
58
|
+
}
|
|
59
|
+
async function readRequestConfig() {
|
|
60
|
+
const scopedConfig = buildConfig({
|
|
61
|
+
bucket: await resolveStorageSecret("S3_BUCKET", "R2_BUCKET"),
|
|
62
|
+
accessKeyId: await resolveStorageSecret("S3_ACCESS_KEY_ID", "R2_ACCESS_KEY_ID"),
|
|
63
|
+
secretAccessKey: await resolveStorageSecret("S3_SECRET_ACCESS_KEY", "R2_SECRET_ACCESS_KEY"),
|
|
64
|
+
endpoint: await resolveStorageSecret("S3_ENDPOINT", "R2_ENDPOINT"),
|
|
65
|
+
region: await resolveStorageSecret("S3_REGION", "R2_REGION"),
|
|
66
|
+
publicBaseUrl: await resolveStorageSecret("S3_PUBLIC_BASE_URL", "R2_PUBLIC_BASE_URL"),
|
|
67
|
+
});
|
|
68
|
+
return scopedConfig ?? readEnvConfig();
|
|
69
|
+
}
|
|
70
|
+
async function hmac(key, message) {
|
|
71
|
+
const cryptoKey = await crypto.subtle.importKey("raw", key, { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
|
|
72
|
+
return crypto.subtle.sign("HMAC", cryptoKey, new TextEncoder().encode(message));
|
|
73
|
+
}
|
|
74
|
+
async function sha256(data) {
|
|
75
|
+
const buffer = new ArrayBuffer(data.byteLength);
|
|
76
|
+
new Uint8Array(buffer).set(data);
|
|
77
|
+
return toHex(await crypto.subtle.digest("SHA-256", buffer));
|
|
78
|
+
}
|
|
79
|
+
function toHex(buffer) {
|
|
80
|
+
return Array.from(new Uint8Array(buffer), (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
81
|
+
}
|
|
82
|
+
async function signingKey(secret, dateStamp, region) {
|
|
83
|
+
const dateKey = await hmac(new TextEncoder().encode(`AWS4${secret}`).buffer, dateStamp);
|
|
84
|
+
const regionKey = await hmac(dateKey, region);
|
|
85
|
+
const serviceKey = await hmac(regionKey, "s3");
|
|
86
|
+
return hmac(serviceKey, "aws4_request");
|
|
87
|
+
}
|
|
88
|
+
function encodePathSegment(value) {
|
|
89
|
+
return encodeURIComponent(value).replace(/[!'()*]/g, (char) => `%${char.charCodeAt(0).toString(16).toUpperCase()}`);
|
|
90
|
+
}
|
|
91
|
+
function objectPath(config, key) {
|
|
92
|
+
return `/${config.bucket}/${key.split("/").map(encodePathSegment).join("/")}`;
|
|
93
|
+
}
|
|
94
|
+
async function putObject(config, key, data, contentType) {
|
|
95
|
+
const now = new Date();
|
|
96
|
+
const amzDate = now
|
|
97
|
+
.toISOString()
|
|
98
|
+
.replace(/[:-]|\.\d{3}/g, "")
|
|
99
|
+
.slice(0, 15) + "Z";
|
|
100
|
+
const dateStamp = amzDate.slice(0, 8);
|
|
101
|
+
const credentialScope = `${dateStamp}/${config.region}/s3/aws4_request`;
|
|
102
|
+
const host = new URL(config.endpoint).host;
|
|
103
|
+
const canonicalUri = objectPath(config, key);
|
|
104
|
+
const payloadHash = await sha256(data);
|
|
105
|
+
const headers = {
|
|
106
|
+
host,
|
|
107
|
+
"content-type": contentType,
|
|
108
|
+
"x-amz-content-sha256": payloadHash,
|
|
109
|
+
"x-amz-date": amzDate,
|
|
110
|
+
};
|
|
111
|
+
const signedHeaderKeys = Object.keys(headers).sort();
|
|
112
|
+
const signedHeaders = signedHeaderKeys.join(";");
|
|
113
|
+
const canonicalHeaders = signedHeaderKeys
|
|
114
|
+
.map((header) => `${header}:${headers[header]}`)
|
|
115
|
+
.join("\n") + "\n";
|
|
116
|
+
const canonicalRequest = [
|
|
117
|
+
"PUT",
|
|
118
|
+
canonicalUri,
|
|
119
|
+
"",
|
|
120
|
+
canonicalHeaders,
|
|
121
|
+
signedHeaders,
|
|
122
|
+
payloadHash,
|
|
123
|
+
].join("\n");
|
|
124
|
+
const requestHash = await sha256(new TextEncoder().encode(canonicalRequest));
|
|
125
|
+
const stringToSign = [
|
|
126
|
+
"AWS4-HMAC-SHA256",
|
|
127
|
+
amzDate,
|
|
128
|
+
credentialScope,
|
|
129
|
+
requestHash,
|
|
130
|
+
].join("\n");
|
|
131
|
+
const signature = toHex(await hmac(await signingKey(config.secretAccessKey, dateStamp, config.region), stringToSign));
|
|
132
|
+
const authorization = `AWS4-HMAC-SHA256 Credential=${config.accessKeyId}/${credentialScope}, ` +
|
|
133
|
+
`SignedHeaders=${signedHeaders}, Signature=${signature}`;
|
|
134
|
+
const response = await fetch(`${config.endpoint}${canonicalUri}`, {
|
|
135
|
+
method: "PUT",
|
|
136
|
+
headers: {
|
|
137
|
+
...headers,
|
|
138
|
+
Authorization: authorization,
|
|
139
|
+
"Content-Length": String(data.byteLength),
|
|
140
|
+
},
|
|
141
|
+
body: data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength),
|
|
142
|
+
});
|
|
143
|
+
if (!response.ok) {
|
|
144
|
+
const detail = await response.text();
|
|
145
|
+
throw new Error(`S3 PutObject failed (${response.status}): ${detail || response.statusText}`);
|
|
146
|
+
}
|
|
147
|
+
return `${config.publicBaseUrl}/${key.split("/").map(encodePathSegment).join("/")}`;
|
|
148
|
+
}
|
|
149
|
+
function safeFilename(filename) {
|
|
150
|
+
const basename = filename?.split(/[\\/]/).pop()?.trim() || "attachment";
|
|
151
|
+
return (basename.replace(/[^a-zA-Z0-9._-]/g, "_").slice(0, 160) || "attachment");
|
|
152
|
+
}
|
|
153
|
+
export const s3FileUploadProvider = {
|
|
154
|
+
id: "s3",
|
|
155
|
+
name: "S3-compatible object storage",
|
|
156
|
+
isConfigured: () => readEnvConfig() !== null,
|
|
157
|
+
isConfiguredForRequest: async () => (await readRequestConfig()) !== null,
|
|
158
|
+
upload: async ({ data, filename, mimeType }) => {
|
|
159
|
+
const config = await readRequestConfig();
|
|
160
|
+
if (!config) {
|
|
161
|
+
throw new Error("S3 object storage requires a bucket, endpoint, credentials, and public base URL");
|
|
162
|
+
}
|
|
163
|
+
const key = `uploads/${Date.now()}-${Math.random().toString(36).slice(2, 10)}-${safeFilename(filename)}`;
|
|
164
|
+
const bytes = data instanceof Uint8Array ? data : new Uint8Array(data);
|
|
165
|
+
const url = await putObject(config, key, bytes, mimeType || "application/octet-stream");
|
|
166
|
+
return { url, provider: "s3" };
|
|
167
|
+
},
|
|
168
|
+
};
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Templates call `uploadFile()` to upload a file and get back a URL.
|
|
5
5
|
* The framework dispatches to whichever provider is registered (Builder.io
|
|
6
|
-
* built-in, or a user-supplied one). If no provider is active,
|
|
7
|
-
*
|
|
6
|
+
* built-in, or a user-supplied one). If no provider is active, uploads fail
|
|
7
|
+
* closed so binary payloads never get persisted as base64 in SQL.
|
|
8
8
|
*/
|
|
9
9
|
export interface FileUploadInput {
|
|
10
10
|
/** File contents. */
|
|
@@ -13,7 +13,7 @@ export interface FileUploadInput {
|
|
|
13
13
|
filename?: string;
|
|
14
14
|
/** MIME type, e.g. "image/png". */
|
|
15
15
|
mimeType?: string;
|
|
16
|
-
/**
|
|
16
|
+
/** Owner email used for request-scoped provider credentials. */
|
|
17
17
|
ownerEmail?: string;
|
|
18
18
|
/** Builder.io upload hint: return one stable URL while compression finishes asynchronously. */
|
|
19
19
|
stableUrl?: boolean;
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Templates call `uploadFile()` to upload a file and get back a URL.
|
|
5
5
|
* The framework dispatches to whichever provider is registered (Builder.io
|
|
6
|
-
* built-in, or a user-supplied one). If no provider is active,
|
|
7
|
-
*
|
|
6
|
+
* built-in, or a user-supplied one). If no provider is active, uploads fail
|
|
7
|
+
* closed so binary payloads never get persisted as base64 in SQL.
|
|
8
8
|
*/
|
|
9
9
|
export {};
|
|
@@ -14,6 +14,15 @@ const DESIGN_SYSTEM_INTELLIGENCE_CAPABILITY = {
|
|
|
14
14
|
keySummary: "Builder Design System Intelligence",
|
|
15
15
|
why: "Uses your brand and design-system guidance to keep generated work on brand.",
|
|
16
16
|
};
|
|
17
|
+
const FILE_UPLOAD_STORAGE_CAPABILITY = {
|
|
18
|
+
id: "file-storage",
|
|
19
|
+
label: "File uploads and storage",
|
|
20
|
+
required: false,
|
|
21
|
+
suggested: true,
|
|
22
|
+
builderIncluded: true,
|
|
23
|
+
keySummary: "Builder file storage or an S3-compatible bucket, credentials, and public base URL",
|
|
24
|
+
why: "Uploaded images and files need durable object storage so the agent can reuse them throughout a thread.",
|
|
25
|
+
};
|
|
17
26
|
const PROFILES = {
|
|
18
27
|
analytics: {
|
|
19
28
|
appId: "analytics",
|
|
@@ -381,8 +390,21 @@ export function resolveOnboardingAppId(explicit) {
|
|
|
381
390
|
export function getOnboardingAppProfile(appId) {
|
|
382
391
|
const resolvedId = resolveOnboardingAppId(appId);
|
|
383
392
|
const profile = PROFILES[resolvedId] ?? FALLBACK_PROFILE;
|
|
393
|
+
const capabilities = profile.capabilities.some((capability) => capability.id === FILE_UPLOAD_STORAGE_CAPABILITY.id)
|
|
394
|
+
? profile.capabilities.map((capability) => capability.id === FILE_UPLOAD_STORAGE_CAPABILITY.id
|
|
395
|
+
? {
|
|
396
|
+
...FILE_UPLOAD_STORAGE_CAPABILITY,
|
|
397
|
+
// Assets and other domain-specific profiles can still make
|
|
398
|
+
// storage required while sharing the framework-wide contract.
|
|
399
|
+
required: capability.required,
|
|
400
|
+
builderIncluded: capability.builderIncluded,
|
|
401
|
+
}
|
|
402
|
+
: { ...capability })
|
|
403
|
+
: profile.capabilities.flatMap((capability, index) => index === 0
|
|
404
|
+
? [{ ...capability }, { ...FILE_UPLOAD_STORAGE_CAPABILITY }]
|
|
405
|
+
: [{ ...capability }]);
|
|
384
406
|
return {
|
|
385
407
|
...profile,
|
|
386
|
-
capabilities
|
|
408
|
+
capabilities,
|
|
387
409
|
};
|
|
388
410
|
}
|
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { PROVIDER_ENV_META, PROVIDER_ENV_VARS, } from "../agent/engine/provider-env-vars.js";
|
|
9
9
|
import { detectEngineFromUserSecrets, isAgentEngineSettingConfigured, } from "../agent/engine/registry.js";
|
|
10
|
+
import { getActiveFileUploadProviderForRequest, registerFileUploadProvider, } from "../file-upload/registry.js";
|
|
11
|
+
import { s3FileUploadProvider } from "../file-upload/s3.js";
|
|
10
12
|
import { canUseDeployCredentialFallbackForRequest, readDeployCredentialEnv, resolveSecret, } from "../server/credential-provider.js";
|
|
11
13
|
import { getSetting } from "../settings/store.js";
|
|
12
14
|
import { registerOnboardingStep } from "./registry.js";
|
|
@@ -374,13 +376,80 @@ const githubRepositoryStep = {
|
|
|
374
376
|
readDeployCredentialEnv("GH_TOKEN"));
|
|
375
377
|
},
|
|
376
378
|
};
|
|
379
|
+
const fileStorageStep = {
|
|
380
|
+
id: "file-storage",
|
|
381
|
+
order: 15,
|
|
382
|
+
required: false,
|
|
383
|
+
title: "File uploads and storage",
|
|
384
|
+
description: "Keep uploaded images and files available throughout the thread with Builder or S3-compatible object storage.",
|
|
385
|
+
methods: [
|
|
386
|
+
{
|
|
387
|
+
id: "builder",
|
|
388
|
+
kind: "builder-cli-auth",
|
|
389
|
+
label: "Connect Builder",
|
|
390
|
+
description: "Builder includes managed file storage alongside its AI connection.",
|
|
391
|
+
primary: true,
|
|
392
|
+
badge: "free",
|
|
393
|
+
payload: { scope: "llm" },
|
|
394
|
+
},
|
|
395
|
+
{
|
|
396
|
+
id: "s3",
|
|
397
|
+
kind: "form",
|
|
398
|
+
label: "Use custom storage keys",
|
|
399
|
+
description: "Connect AWS S3, Cloudflare R2, MinIO, or another S3-compatible bucket with a stable public base URL.",
|
|
400
|
+
payload: {
|
|
401
|
+
writeScope: "workspace",
|
|
402
|
+
saveTo: "scoped-secrets",
|
|
403
|
+
secretDescription: "S3-compatible object storage for file uploads",
|
|
404
|
+
fields: [
|
|
405
|
+
{
|
|
406
|
+
key: "S3_ENDPOINT",
|
|
407
|
+
label: "Endpoint URL",
|
|
408
|
+
placeholder: "https://s3.us-east-1.amazonaws.com",
|
|
409
|
+
},
|
|
410
|
+
{
|
|
411
|
+
key: "S3_BUCKET",
|
|
412
|
+
label: "Bucket name",
|
|
413
|
+
placeholder: "my-uploads-bucket",
|
|
414
|
+
},
|
|
415
|
+
{
|
|
416
|
+
key: "S3_ACCESS_KEY_ID",
|
|
417
|
+
label: "Access key ID",
|
|
418
|
+
placeholder: "AKIA...",
|
|
419
|
+
},
|
|
420
|
+
{
|
|
421
|
+
key: "S3_SECRET_ACCESS_KEY",
|
|
422
|
+
label: "Secret access key",
|
|
423
|
+
secret: true,
|
|
424
|
+
},
|
|
425
|
+
{
|
|
426
|
+
key: "S3_REGION",
|
|
427
|
+
label: "Region",
|
|
428
|
+
placeholder: "auto",
|
|
429
|
+
},
|
|
430
|
+
{
|
|
431
|
+
key: "S3_PUBLIC_BASE_URL",
|
|
432
|
+
label: "Public base URL",
|
|
433
|
+
placeholder: "https://cdn.example.com",
|
|
434
|
+
},
|
|
435
|
+
],
|
|
436
|
+
},
|
|
437
|
+
},
|
|
438
|
+
],
|
|
439
|
+
isComplete: async () => (await getActiveFileUploadProviderForRequest()) !== null,
|
|
440
|
+
};
|
|
377
441
|
let registered = false;
|
|
378
442
|
/** Idempotent. Safe to call from every plugin-mount call. */
|
|
379
443
|
export function registerDefaultOnboardingSteps() {
|
|
380
444
|
if (registered)
|
|
381
445
|
return;
|
|
382
446
|
registered = true;
|
|
447
|
+
// The framework provides a generic S3/R2 implementation for the custom-key
|
|
448
|
+
// onboarding path. Templates may replace the same provider id with a
|
|
449
|
+
// domain-specific implementation after this default plugin mounts.
|
|
450
|
+
registerFileUploadProvider(s3FileUploadProvider);
|
|
383
451
|
registerOnboardingStep(llmStep);
|
|
452
|
+
registerOnboardingStep(fileStorageStep);
|
|
384
453
|
registerOnboardingStep(databaseStep);
|
|
385
454
|
registerOnboardingStep(authStep);
|
|
386
455
|
registerOnboardingStep(emailStep);
|
|
@@ -91,6 +91,8 @@ export interface OnboardingCapability {
|
|
|
91
91
|
label: string;
|
|
92
92
|
/** Whether this capability blocks the app's normal setup. */
|
|
93
93
|
required: boolean;
|
|
94
|
+
/** Show this optional capability as a recommended BYOK setup. */
|
|
95
|
+
suggested?: boolean;
|
|
94
96
|
/** Whether Builder's managed connection covers this capability. */
|
|
95
97
|
builderIncluded: boolean;
|
|
96
98
|
/** Compact description of the key or connection the BYOK path needs. */
|