feat: fix all the file storage logic

pull/718/head
Travis Fischer 2025-07-02 08:55:41 -05:00
rodzic 762953e8c3
commit a513194a2f
26 zmienionych plików z 602 dodań i 376 usunięć

Wyświetl plik

@ -35,8 +35,8 @@
"@agentic/platform-hono": "workspace:*",
"@agentic/platform-types": "workspace:*",
"@agentic/platform-validators": "workspace:*",
"@aws-sdk/client-s3": "^3.840.0",
"@aws-sdk/s3-request-presigner": "^3.840.0",
"@aws-sdk/client-s3": "^3.726.1",
"@aws-sdk/s3-request-presigner": "^3.726.1",
"@dicebear/collection": "catalog:",
"@dicebear/core": "catalog:",
"@fisch0920/drizzle-orm": "catalog:",
@ -50,6 +50,7 @@
"file-type": "^21.0.0",
"hono": "catalog:",
"ky": "catalog:",
"mrmime": "^2.0.1",
"octokit": "catalog:",
"p-all": "catalog:",
"postgres": "catalog:",

Wyświetl plik

@ -19,7 +19,7 @@ import {
openapiErrorResponse409,
openapiErrorResponses
} from '@/lib/openapi-utils'
import { uploadFileToStorage } from '@/lib/storage'
import { uploadFileUrlToStorage } from '@/lib/storage'
import { createDeploymentQuerySchema } from './schemas'
@ -154,9 +154,9 @@ export function registerV1CreateDeployment(
const agenticProjectConfig = await resolveAgenticProjectConfig(body, {
label: `deployment "${deploymentIdentifier}"`,
logger,
uploadFileToStorage: async (source) => {
return uploadFileToStorage(source, {
projectIdentifier
uploadFileUrlToStorage: async (source) => {
return uploadFileUrlToStorage(source, {
prefix: projectIdentifier
})
}
})

Wyświetl plik

@ -0,0 +1,7 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`Storage > uploadFileUrlToStorage data-uri 1`] = `"https://storage.agentic.so/@dev/test/ef4238fba78887e0974cd48809a66e284cdd78ce92d6b2d485c25a552fb39631.svg"`;
exports[`Storage > uploadFileUrlToStorage data-uri 2 1`] = `"https://storage.agentic.so/@dev/test/efbc1f0409b730d93b01c918be9e024bf7777801cfd252221c39e36c08c1b4fb"`;
exports[`Storage > uploadFileUrlToStorage url 1`] = `"https://storage.agentic.so/@dev/test/6da6ef895be2a42606b99e5e3b9c25687c92c81986a9718e07f32e574d41cf7a.svg"`;

Wyświetl plik

@ -5,6 +5,7 @@ import { and, db, eq, type RawAccount, type RawUser, schema } from '@/db'
import { createAvatar } from '../create-avatar'
import { getUniqueNamespace } from '../ensure-unique-namespace'
import { uploadFileUrlToStorage } from '../storage'
/**
* After a user completes an authentication flow, we'll have partial account info
@ -51,9 +52,6 @@ export async function upsertOrLinkUserAccount({
}): Promise<RawUser> {
const { provider, accountId } = partialAccount
// Set a default profile image if one isn't provided
partialUser.image ??= createAvatar(partialUser.email)
const [existingAccount, existingUser] = await Promise.all([
db.query.accounts.findFirst({
where: and(
@ -70,6 +68,16 @@ export async function upsertOrLinkUserAccount({
})
])
async function resolveUserProfileImage({ prefix }: { prefix: string }) {
// Set a default profile image if one isn't provided
partialUser.image = await uploadFileUrlToStorage(
partialUser.image ?? createAvatar(partialUser.email),
{
prefix
}
)
}
if (existingAccount && existingUser) {
// Happy path case: the user is just logging in with an existing account
// that's already linked to a user.
@ -98,10 +106,19 @@ export async function upsertOrLinkUserAccount({
// the one in the account we're linking, we should throw an error unless it's
// a "trusted" provider.
if (provider === 'password' && existingUser.email !== partialUser.email) {
await db
await resolveUserProfileImage({ prefix: existingUser.username })
const [user] = await db
.update(schema.users)
.set(partialUser)
.where(eq(schema.users.id, existingUser.id))
.returning()
assert(
user,
500,
`Error updating existing user during ${provider} authentication`
)
return user
}
return existingUser
@ -124,6 +141,8 @@ export async function upsertOrLinkUserAccount({
{ label: 'Username' }
)
await resolveUserProfileImage({ prefix: username })
// This is a user's first time signing up with the platform, so create both
// a new user and linked account.
return db.transaction(async (tx) => {

Wyświetl plik

@ -1,13 +1,14 @@
import { describe, expect, it } from 'vitest'
import { describe, expect, test } from 'vitest'
import {
deleteStorageObject,
getStorageObject,
putStorageObject
putStorageObject,
uploadFileUrlToStorage
} from './storage'
describe('Storage', () => {
it('putObject, getObject, deleteObject', async () => {
test('putObject, getObject, deleteObject', async () => {
if (!process.env.ACCESS_KEY_ID) {
// TODO: ignore on CI
expect(true).toEqual(true)
@ -27,4 +28,43 @@ describe('Storage', () => {
const res = await deleteStorageObject('test.txt')
expect(res.$metadata.httpStatusCode).toEqual(204)
})
test('uploadFileUrlToStorage url', async () => {
const url = await uploadFileUrlToStorage(
'https://agentic.so/agentic-icon-circle-light.svg',
{
prefix: '@dev/test'
}
)
expect(url).toBeTruthy()
expect(new URL(url).origin).toEqual('https://storage.agentic.so')
expect(url).toMatchSnapshot()
})
test('uploadFileUrlToStorage data-uri', async () => {
const url = await uploadFileUrlToStorage(
'data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22400%22%20height%3D%22400%22%20viewBox%3D%220%200%20124%20124%22%20fill%3D%22none%22%3E%3Crect%20width%3D%22124%22%20height%3D%22124%22%20rx%3D%2224%22%20fill%3D%22%23F97316%22%2F%3E%3Cpath%20d%3D%22M19.375%2036.7818V100.625C19.375%20102.834%2021.1659%20104.625%2023.375%20104.625H87.2181C90.7818%20104.625%2092.5664%20100.316%2090.0466%2097.7966L26.2034%2033.9534C23.6836%2031.4336%2019.375%2033.2182%2019.375%2036.7818Z%22%20fill%3D%22white%22%2F%3E%3Ccircle%20cx%3D%2263.2109%22%20cy%3D%2237.5391%22%20r%3D%2218.1641%22%20fill%3D%22black%22%2F%3E%3Crect%20opacity%3D%220.4%22%20x%3D%2281.1328%22%20y%3D%2280.7198%22%20width%3D%2217.5687%22%20height%3D%2217.3876%22%20rx%3D%224%22%20transform%3D%22rotate(-45%2081.1328%2080.7198)%22%20fill%3D%22%23FDBA74%22%2F%3E%3C%2Fsvg%3E',
{
prefix: '@dev/test'
}
)
expect(url).toBeTruthy()
expect(new URL(url).origin).toEqual('https://storage.agentic.so')
expect(url).toMatchSnapshot()
})
test('uploadFileUrlToStorage data-uri 2', async () => {
const url = await uploadFileUrlToStorage(
'data:application/octet-stream;base64,IyBUZXN0IEV2ZXJ5dGhpbmcgT3BlbkFQSQoKVGhpcyBpcyB0ZXN0aW5nICoqcmVhZG1lIHJlbmRlcmluZyoqLgoKIyMgTWlzYwoKLSBbIF0gSXRlbSAxCi0gWyBdIEl0ZW0gMgotIFt4XSBJdGVtIDMKCi0tLQoKLSBfaXRhbGljXwotICoqYm9sZCoqCi0gW2xpbmtdKGh0dHBzOi8vd3d3Lmdvb2dsZS5jb20pCi0gYGNvZGVgCgojIyBDb2RlCgpgYGB0cwpjb25zdCBhID0gMQoKZXhwb3J0IGZ1bmN0aW9uIGZvbygpIHsKICBjb25zb2xlLmxvZygnaGVsbG8gd29ybGQnKQp9CmBgYAoKIyMgSW1hZ2VzCgohW0ltYWdlXShodHRwczovL3BsYWNlaG9sZC5jby82MDB4NDAwKQo=',
{
prefix: '@dev/test'
}
)
expect(url).toBeTruthy()
expect(new URL(url).origin).toEqual('https://storage.agentic.so')
expect(url).toMatchSnapshot()
})
})

Wyświetl plik

@ -11,6 +11,7 @@ import {
import { getSignedUrl } from '@aws-sdk/s3-request-presigner'
import { fileTypeFromBuffer } from 'file-type'
import ky from 'ky'
import { lookup as lookupMimeType } from 'mrmime'
import { env } from './env'
@ -25,7 +26,9 @@ export const storageClient = new S3Client({
credentials: {
accessKeyId: env.S3_ACCESS_KEY_ID,
secretAccessKey: env.S3_ACCESS_KEY_SECRET
}
},
requestChecksumCalculation: 'WHEN_REQUIRED',
responseChecksumValidation: 'WHEN_REQUIRED'
})
// This ensures that buckets are created automatically if they don't exist on
@ -71,13 +74,17 @@ export async function deleteStorageObject(
}
export function getStorageObjectInternalUrl(key: string) {
return `${env.AGENTIC_STORAGE_BASE_URL}/${Bucket}/${key}`
return `${env.S3_ENDPOINT}/${Bucket}/${key}`
}
export function getStorageObjectPublicUrl(key: string) {
return `${env.AGENTIC_STORAGE_BASE_URL}/${key}`
}
// TODO: Signed uploads don't seem to be working; getting a signature mismatch
// error, and no idea why. Switched to using non-presigned URL uploads for now,
// but this will be necessary in the future, for instance, for the web client
// to upload files.
export async function getStorageSignedUploadUrl(
key: string,
{
@ -93,21 +100,21 @@ export async function getStorageSignedUploadUrl(
)
}
export async function uploadFileToStorage(
input: string,
export async function uploadFileUrlToStorage(
inputUrl: string,
{
projectIdentifier
prefix
}: {
projectIdentifier: string
prefix: string
}
): Promise<string> {
let source: URL
try {
source = new URL(input)
source = new URL(inputUrl)
} catch {
// Not a URL
throw new Error(`Invalid source file URL: ${input}`)
throw new Error(`Invalid source file URL: ${inputUrl}`)
}
if (source.hostname === 'storage.agentic.so') {
@ -117,29 +124,71 @@ export async function uploadFileToStorage(
const sourceBuffer = await ky.get(source).arrayBuffer()
const [hash, fileType] = await Promise.all([
const [hash, inferredFileType] = await Promise.all([
sha256(sourceBuffer),
fileTypeFromBuffer(sourceBuffer)
])
const filename = fileType ? `${hash}.${fileType.ext}` : hash
const key = `${projectIdentifier}/${filename}`
const maybeSourceExt = source.pathname.split('.').at(-1)
const maybeSourceExt2 = maybeSourceExt
? /^[a-z]+$/i.test(maybeSourceExt)
? maybeSourceExt
: undefined
: undefined
const maybeSourceMime =
source.protocol === 'data:'
? source.pathname.split(',')[0]?.split(';')[0]
: undefined
const sourceExt0 =
maybeSourceExt2 ??
(maybeSourceMime && maybeSourceMime !== 'application/octet-stream'
? maybeSourceMime.split('/')[1]?.split('+')[0]
: undefined)
const sourceExt = sourceExt0 === 'markdown' ? 'md' : sourceExt0
const fileType =
inferredFileType ??
(sourceExt
? {
ext: sourceExt,
mime: lookupMimeType(sourceExt)
}
: undefined)
const filename = fileType?.ext ? `${hash}.${fileType.ext}` : hash
const key = `${prefix}/${filename}`
const publicObjectUrl = getStorageObjectPublicUrl(key)
// console.log('uploading to r2', {
// key,
// source,
// sourceExt,
// maybeSourceMime,
// maybeSourceExt,
// maybeSourceExt2,
// inputUrl,
// fileType,
// publicObjectUrl
// })
try {
// Check if the object already exists.
await ky.head(publicObjectUrl)
} catch {
const signedUploadUrl = await getStorageSignedUploadUrl(key)
const body = Buffer.from(sourceBuffer)
// Object doesn't exist yet, so upload it.
await ky.post(signedUploadUrl, {
body: sourceBuffer,
headers: {
'Content-Type': fileType?.mime ?? 'application/octet-stream'
}
})
try {
await putStorageObject(key, body, {
ContentType: fileType?.mime ?? 'application/octet-stream'
})
} catch (err: any) {
const error = await err.response.text()
// eslint-disable-next-line no-console
console.error('error uploading to r2', err.message, error)
throw err
}
}
return publicObjectUrl

Wyświetl plik

@ -16,8 +16,7 @@ export async function deployProjects(
projects,
async (project) => {
const config = await loadAgenticConfig({
cwd: project,
agenticApiClient: client
cwd: project
})
const deployment = await client.createDeployment(config)
console.log(`Deployed ${project} => ${deployment.identifier}`)

Wyświetl plik

@ -2,11 +2,13 @@
import type { Project } from '@agentic/platform-types'
import { assert, omit, sanitizeSearchParams } from '@agentic/platform-core'
import ky from 'ky'
import { ChevronsUpDownIcon, ExternalLinkIcon } from 'lucide-react'
import Link from 'next/link'
import { useRouter, useSearchParams } from 'next/navigation'
import plur from 'plur'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useAsync } from 'react-use'
import { useAgentic } from '@/components/agentic-provider'
import { CodeBlock } from '@/components/code-block'
@ -189,6 +191,14 @@ export function MarketplacePublicProjectDetail({
return tab as MarketplacePublicProjectDetailTab
}, [searchParams, deployment])
const { value: readme } = useAsync(async () => {
if (tab === 'readme' && deployment?.readme?.trim()) {
return ky.get(deployment.readme).text()
}
return undefined
}, [deployment, tab])
return (
<PageContainer>
<section>
@ -332,12 +342,9 @@ export function MarketplacePublicProjectDetail({
</TabsContent>
)}
{deployment?.readme?.trim() && tab === 'readme' && (
{tab === 'readme' && readme && (
<TabsContent value='readme' className='flex flex-col gap-4'>
<SSRMarkdown
markdown={deployment.readme}
className='items-start!'
/>
<SSRMarkdown markdown={readme} className='items-start!' />
</TabsContent>
)}

Wyświetl plik

@ -47,13 +47,16 @@ Examples: `1.0.0`, `0.0.1`, `5.0.1`, etc.
</ResponseField>
<ResponseField name='readme' type='string'>
Optional embedded markdown readme documenting the project. Supports
GitHub-flavored markdown.
Optional markdown readme documenting the project (supports GitHub-flavored markdown).
A string which may be either: a URL to a remote markdown file (eg, `https://example.com/readme.md`), a local file path (eg, `./readme.md`), or a data-uri string (eg, `data:text/markdown;base64,SGVsbG8gV29ybGQ=`).
</ResponseField>
<ResponseField name='iconUrl' type='string'>
Optional logo image URL to use for the project. Logos should have a square
aspect ratio.
Optional logo image to use for the project. Logos should have a square aspect ratio.
A string which may be either: a URL to a remote image (eg, `https://example.com/logo.png`), a local file path (eg, `./logo.png`), or a data-uri string (eg, `data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA...`).
</ResponseField>

Wyświetl plik

@ -0,0 +1,9 @@
<svg width="96" height="96" viewBox="0 0 96 96" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="48" cy="48" r="48" fill="#888888"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M26.2922 73.2067C22.6248 73.255 18.9825 73.2065 15.3653 73.0611C26.752 55.048 38.1402 37.0309 49.5299 19.0097C51.1635 17.3293 52.9847 17.1107 54.9933 18.3541C57.491 20.4558 58.2438 23.0539 57.2515 26.1485C55.4913 29.1352 53.6945 32.0975 51.8609 35.0357C44.1878 47.1766 36.5149 59.3176 28.8417 71.4585C28.2861 72.4763 27.4363 73.059 26.2922 73.2067Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M55.4302 37.3668C55.7566 37.5224 56.0237 37.7651 56.2315 38.0952C64.3606 49.8615 72.5437 61.5897 80.7804 73.2796C77.1867 73.3768 73.5931 73.3768 69.9993 73.2796C68.9793 73.049 68.1295 72.5391 67.4497 71.7499C62.3379 64.2943 57.1902 56.864 52.0065 49.4591C51.4627 48.6715 50.9772 47.8459 50.5496 46.9824C50.3708 45.8664 50.6137 44.8466 51.278 43.9229C52.6986 41.7519 54.0827 39.5666 55.4302 37.3668Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M63.8803 73.061C59.7288 73.2064 55.5523 73.255 51.3509 73.2067C50.2417 71.5381 49.1005 69.887 47.9271 68.2532C46.8451 69.884 45.8009 71.5351 44.7948 73.2067C40.6904 73.255 36.611 73.2064 32.5567 73.061C36.3849 67.0221 40.2457 61.0003 44.1392 54.9953C45.7173 53.3419 47.5871 52.9292 49.7483 53.757C50.3223 54.0317 50.8322 54.396 51.278 54.8497C55.5109 60.9027 59.7117 66.9732 63.8803 73.061Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M15.3652 73.0611C18.9825 73.2065 22.6247 73.255 26.2921 73.2067C22.625 73.4004 18.9342 73.4004 15.2195 73.2067C15.2376 73.1183 15.2861 73.0698 15.3652 73.0611Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M32.5568 73.0611C36.6111 73.2065 40.6904 73.255 44.7948 73.2067C40.6907 73.4004 36.5629 73.4004 32.4111 73.2067C32.4292 73.1183 32.4777 73.0698 32.5568 73.0611Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M63.8803 73.0611C63.9594 73.0698 64.0079 73.1183 64.026 73.2067C59.7771 73.4005 55.552 73.4005 51.3509 73.2067C55.5523 73.255 59.7289 73.2065 63.8803 73.0611Z" fill="white"/>
</svg>

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 2.1 KiB

Wyświetl plik

@ -372,13 +372,13 @@ export class AgenticApiClient {
*
* @example
* ```ts
* const publicObjectUrl = await client.uploadFileToStorage(
* const publicObjectUrl = await client.uploadFileUrlToStorage(
* new URL('https://example.com/image.png'),
* { projectIdentifier: '@username/my-project' }
* )
* ```
*/
async uploadFileToStorage(
async uploadFileUrlToStorage(
source: string | ArrayBuffer | URL,
{
projectIdentifier

Wyświetl plik

@ -15,6 +15,7 @@ import { registerSignoutCommand } from './commands/signout'
import { registerSignupCommand } from './commands/signup'
import { registerWhoAmICommand } from './commands/whoami'
import { AuthStore } from './lib/auth-store'
import { agenticApiBaseUrl } from './lib/env'
import { initExitHooks } from './lib/exit-hooks'
import { createErrorHandler } from './lib/handle-error'
@ -23,7 +24,7 @@ async function main() {
// Initialize the API client
const client = new AgenticApiClient({
apiBaseUrl: process.env.AGENTIC_API_BASE_URL,
apiBaseUrl: agenticApiBaseUrl,
onUpdateAuth: (update) => {
if (update) {
AuthStore.setAuth(update)

Wyświetl plik

@ -8,14 +8,13 @@ import type { Context } from '../types'
export function registerDebugCommand({
program,
logger,
handleError,
client
handleError
}: Context) {
const command = new Command('debug')
.description('Prints config for a local project.')
.option(
'-c, --cwd <dir>',
'The directory to load the Agentic project config from (defaults to cwd). This directory must contain an "agentic.config.{ts,js,json}" project file.'
'The directory to load the Agentic project config from (defaults to the cwd). This directory must contain an "agentic.config.{ts,js,json}" project file.'
)
.action(async (opts) => {
try {
@ -27,8 +26,7 @@ export function registerDebugCommand({
// client.
const config = await oraPromise(
loadAgenticConfig({
cwd: opts.cwd,
agenticApiClient: client
cwd: opts.cwd
}),
{
text: `Loading Agentic config from ${opts.cwd ?? process.cwd()}`,

Wyświetl plik

@ -16,7 +16,7 @@ export function registerDeployCommand({
.description('Creates a new deployment.')
.option(
'-c, --cwd <dir>',
'The directory to load the Agentic project config from (defaults to cwd). This directory must contain an "agentic.config.{ts,js,json}" project file.'
'The directory to load the Agentic project config from (defaults to the cwd). This directory must contain an "agentic.config.{ts,js,json}" project file.'
)
.option('-d, --debug', 'Print out the parsed agentic config and return.')
// TODO
@ -33,8 +33,7 @@ export function registerDeployCommand({
// client.
const config = await oraPromise(
loadAgenticConfig({
cwd: opts.cwd,
agenticApiClient: client
cwd: opts.cwd
}),
{
text: `Loading Agentic config from ${opts.cwd ?? process.cwd()}`,

Wyświetl plik

@ -2,11 +2,13 @@ import type { AuthSession } from '@agentic/platform-types'
import { assert } from '@agentic/platform-core'
import Conf from 'conf'
const keyAuthSession = 'authSession'
import { agenticApiBaseUrl } from './env'
const keyAuthSession = `authSession:${agenticApiBaseUrl}`
export const AuthStore = {
store: new Conf<{
authSession: AuthSession
[keyAuthSession]: AuthSession
}>({ projectName: 'agentic' }),
isAuthenticated() {

Wyświetl plik

@ -0,0 +1,2 @@
export const agenticApiBaseUrl =
process.env.AGENTIC_API_BASE_URL || 'https://api.agentic.so'

Wyświetl plik

@ -19,8 +19,7 @@ export async function resolveDeployment({
}): Promise<Deployment> {
if (!deploymentIdentifier) {
const config = await loadAgenticConfig({
cwd,
agenticApiClient: client
cwd
})
// TODO: re-add team support

Wyświetl plik

@ -33,6 +33,7 @@
"@agentic/platform-types": "workspace:*",
"@agentic/platform-validators": "workspace:*",
"@modelcontextprotocol/sdk": "catalog:",
"mrmime": "^2.0.1",
"semver": "catalog:",
"unconfig": "catalog:"
},

Wyświetl plik

@ -268,7 +268,7 @@ exports[`loadAgenticConfig > everything-openapi 1`] = `
"slug": "pro",
},
],
"readme": "data:application/octet-stream;base64,IyBUZXN0IEV2ZXJ5dGhpbmcgT3BlbkFQSQoKVGhpcyBpcyB0ZXN0aW5nICoqcmVhZG1lIHJlbmRlcmluZyoqLgoKIyMgTWlzYwoKLSBbIF0gSXRlbSAxCi0gWyBdIEl0ZW0gMgotIFt4XSBJdGVtIDMKCi0tLQoKLSBfaXRhbGljXwotICoqYm9sZCoqCi0gW2xpbmtdKGh0dHBzOi8vd3d3Lmdvb2dsZS5jb20pCi0gYGNvZGVgCgojIyBDb2RlCgpgYGB0cwpjb25zdCBhID0gMQoKZXhwb3J0IGZ1bmN0aW9uIGZvbygpIHsKICBjb25zb2xlLmxvZygnaGVsbG8gd29ybGQnKQp9CmBgYAoKIyMgSW1hZ2VzCgohW0ltYWdlXShodHRwczovL3BsYWNlaG9sZC5jby82MDB4NDAwKQo=",
"readme": "data:text/markdown;base64,IyBUZXN0IEV2ZXJ5dGhpbmcgT3BlbkFQSQoKVGhpcyBpcyB0ZXN0aW5nICoqcmVhZG1lIHJlbmRlcmluZyoqLgoKIyMgTWlzYwoKLSBbIF0gSXRlbSAxCi0gWyBdIEl0ZW0gMgotIFt4XSBJdGVtIDMKCi0tLQoKLSBfaXRhbGljXwotICoqYm9sZCoqCi0gW2xpbmtdKGh0dHBzOi8vd3d3Lmdvb2dsZS5jb20pCi0gYGNvZGVgCgojIyBDb2RlCgpgYGB0cwpjb25zdCBhID0gMQoKZXhwb3J0IGZ1bmN0aW9uIGZvbygpIHsKICBjb25zb2xlLmxvZygnaGVsbG8gd29ybGQnKQp9CmBgYAoKIyMgSW1hZ2VzCgohW0ltYWdlXShodHRwczovL3BsYWNlaG9sZC5jby82MDB4NDAwKQo=",
"slug": "test-everything-openapi",
"toolConfigs": [
{
@ -414,7 +414,7 @@ exports[`loadAgenticConfig > metadata-0 1`] = `
"limit": 1000,
"mode": "approximate",
},
"icon": "data:application/octet-stream;base64,PHN2ZyB3aWR0aD0iOTYiIGhlaWdodD0iOTYiIHZpZXdCb3g9IjAgMCA5NiA5NiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPGNpcmNsZSBjeD0iNDgiIGN5PSI0OCIgcj0iNDgiIGZpbGw9IiM4ODg4ODgiLz4KPHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik0yNi4yOTIyIDczLjIwNjdDMjIuNjI0OCA3My4yNTUgMTguOTgyNSA3My4yMDY1IDE1LjM2NTMgNzMuMDYxMUMyNi43NTIgNTUuMDQ4IDM4LjE0MDIgMzcuMDMwOSA0OS41Mjk5IDE5LjAwOTdDNTEuMTYzNSAxNy4zMjkzIDUyLjk4NDcgMTcuMTEwNyA1NC45OTMzIDE4LjM1NDFDNTcuNDkxIDIwLjQ1NTggNTguMjQzOCAyMy4wNTM5IDU3LjI1MTUgMjYuMTQ4NUM1NS40OTEzIDI5LjEzNTIgNTMuNjk0NSAzMi4wOTc1IDUxLjg2MDkgMzUuMDM1N0M0NC4xODc4IDQ3LjE3NjYgMzYuNTE0OSA1OS4zMTc2IDI4Ljg0MTcgNzEuNDU4NUMyOC4yODYxIDcyLjQ3NjMgMjcuNDM2MyA3My4wNTkgMjYuMjkyMiA3My4yMDY3WiIgZmlsbD0id2hpdGUiLz4KPHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik01NS40MzAyIDM3LjM2NjhDNTUuNzU2NiAzNy41MjI0IDU2LjAyMzcgMzcuNzY1MSA1Ni4yMzE1IDM4LjA5NTJDNjQuMzYwNiA0OS44NjE1IDcyLjU0MzcgNjEuNTg5NyA4MC43ODA0IDczLjI3OTZDNzcuMTg2NyA3My4zNzY4IDczLjU5MzEgNzMuMzc2OCA2OS45OTkzIDczLjI3OTZDNjguOTc5MyA3My4wNDkgNjguMTI5NSA3Mi41MzkxIDY3LjQ0OTcgNzEuNzQ5OUM2Mi4zMzc5IDY0LjI5NDMgNTcuMTkwMiA1Ni44NjQgNTIuMDA2NSA0OS40NTkxQzUxLjQ2MjcgNDguNjcxNSA1MC45NzcyIDQ3Ljg0NTkgNTAuNTQ5NiA0Ni45ODI0QzUwLjM3MDggNDUuODY2NCA1MC42MTM3IDQ0Ljg0NjYgNTEuMjc4IDQzLjkyMjlDNTIuNjk4NiA0MS43NTE5IDU0LjA4MjcgMzkuNTY2NiA1NS40MzAyIDM3LjM2NjhaIiBmaWxsPSJ3aGl0ZSIvPgo8cGF0aCBmaWxsLXJ1bGU9ImV2ZW5vZGQiIGNsaXAtcnVsZT0iZXZlbm9kZCIgZD0iTTYzLjg4MDMgNzMuMDYxQzU5LjcyODggNzMuMjA2NCA1NS41NTIzIDczLjI1NSA1MS4zNTA5IDczLjIwNjdDNTAuMjQxNyA3MS41MzgxIDQ5LjEwMDUgNjkuODg3IDQ3LjkyNzEgNjguMjUzMkM0Ni44NDUxIDY5Ljg4NCA0NS44MDA5IDcxLjUzNTEgNDQuNzk0OCA3My4yMDY3QzQwLjY5MDQgNzMuMjU1IDM2LjYxMSA3My4yMDY0IDMyLjU1NjcgNzMuMDYxQzM2LjM4NDkgNjcuMDIyMSA0MC4yNDU3IDYxLjAwMDMgNDQuMTM5MiA1NC45OTUzQzQ1LjcxNzMgNTMuMzQxOSA0Ny41ODcxIDUyLjkyOTIgNDkuNzQ4MyA1My43NTdDNTAuMzIyMyA1NC4wMzE3IDUwLjgzMjIgNTQuMzk2IDUxLjI3OCA1NC44NDk3QzU1LjUxMDkgNjAuOTAyNyA1OS43MTE3IDY2Ljk3MzIgNjMuODgwMyA3My4wNjFaIiBmaWxsPSJ3aGl0ZSIvPgo8cGF0aCBmaWxsLXJ1bGU9ImV2ZW5vZGQiIGNsaXAtcnVsZT0iZXZlbm9kZCIgZD0iTTE1LjM2NTIgNzMuMDYxMUMxOC45ODI1IDczLjIwNjUgMjIuNjI0NyA3My4yNTUgMjYuMjkyMSA3My4yMDY3QzIyLjYyNSA3My40MDA0IDE4LjkzNDIgNzMuNDAwNCAxNS4yMTk1IDczLjIwNjdDMTUuMjM3NiA3My4xMTgzIDE1LjI4NjEgNzMuMDY5OCAxNS4zNjUyIDczLjA2MTFaIiBmaWxsPSJ3aGl0ZSIvPgo8cGF0aCBmaWxsLXJ1bGU9ImV2ZW5vZGQiIGNsaXAtcnVsZT0iZXZlbm9kZCIgZD0iTTMyLjU1NjggNzMuMDYxMUMzNi42MTExIDczLjIwNjUgNDAuNjkwNCA3My4yNTUgNDQuNzk0OCA3My4yMDY3QzQwLjY5MDcgNzMuNDAwNCAzNi41NjI5IDczLjQwMDQgMzIuNDExMSA3My4yMDY3QzMyLjQyOTIgNzMuMTE4MyAzMi40Nzc3IDczLjA2OTggMzIuNTU2OCA3My4wNjExWiIgZmlsbD0id2hpdGUiLz4KPHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik02My44ODAzIDczLjA2MTFDNjMuOTU5NCA3My4wNjk4IDY0LjAwNzkgNzMuMTE4MyA2NC4wMjYgNzMuMjA2N0M1OS43NzcxIDczLjQwMDUgNTUuNTUyIDczLjQwMDUgNTEuMzUwOSA3My4yMDY3QzU1LjU1MjMgNzMuMjU1IDU5LjcyODkgNzMuMjA2NSA2My44ODAzIDczLjA2MTFaIiBmaWxsPSJ3aGl0ZSIvPgo8L3N2Zz4K",
"icon": "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iOTYiIGhlaWdodD0iOTYiIHZpZXdCb3g9IjAgMCA5NiA5NiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPGNpcmNsZSBjeD0iNDgiIGN5PSI0OCIgcj0iNDgiIGZpbGw9IiM4ODg4ODgiLz4KPHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik0yNi4yOTIyIDczLjIwNjdDMjIuNjI0OCA3My4yNTUgMTguOTgyNSA3My4yMDY1IDE1LjM2NTMgNzMuMDYxMUMyNi43NTIgNTUuMDQ4IDM4LjE0MDIgMzcuMDMwOSA0OS41Mjk5IDE5LjAwOTdDNTEuMTYzNSAxNy4zMjkzIDUyLjk4NDcgMTcuMTEwNyA1NC45OTMzIDE4LjM1NDFDNTcuNDkxIDIwLjQ1NTggNTguMjQzOCAyMy4wNTM5IDU3LjI1MTUgMjYuMTQ4NUM1NS40OTEzIDI5LjEzNTIgNTMuNjk0NSAzMi4wOTc1IDUxLjg2MDkgMzUuMDM1N0M0NC4xODc4IDQ3LjE3NjYgMzYuNTE0OSA1OS4zMTc2IDI4Ljg0MTcgNzEuNDU4NUMyOC4yODYxIDcyLjQ3NjMgMjcuNDM2MyA3My4wNTkgMjYuMjkyMiA3My4yMDY3WiIgZmlsbD0id2hpdGUiLz4KPHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik01NS40MzAyIDM3LjM2NjhDNTUuNzU2NiAzNy41MjI0IDU2LjAyMzcgMzcuNzY1MSA1Ni4yMzE1IDM4LjA5NTJDNjQuMzYwNiA0OS44NjE1IDcyLjU0MzcgNjEuNTg5NyA4MC43ODA0IDczLjI3OTZDNzcuMTg2NyA3My4zNzY4IDczLjU5MzEgNzMuMzc2OCA2OS45OTkzIDczLjI3OTZDNjguOTc5MyA3My4wNDkgNjguMTI5NSA3Mi41MzkxIDY3LjQ0OTcgNzEuNzQ5OUM2Mi4zMzc5IDY0LjI5NDMgNTcuMTkwMiA1Ni44NjQgNTIuMDA2NSA0OS40NTkxQzUxLjQ2MjcgNDguNjcxNSA1MC45NzcyIDQ3Ljg0NTkgNTAuNTQ5NiA0Ni45ODI0QzUwLjM3MDggNDUuODY2NCA1MC42MTM3IDQ0Ljg0NjYgNTEuMjc4IDQzLjkyMjlDNTIuNjk4NiA0MS43NTE5IDU0LjA4MjcgMzkuNTY2NiA1NS40MzAyIDM3LjM2NjhaIiBmaWxsPSJ3aGl0ZSIvPgo8cGF0aCBmaWxsLXJ1bGU9ImV2ZW5vZGQiIGNsaXAtcnVsZT0iZXZlbm9kZCIgZD0iTTYzLjg4MDMgNzMuMDYxQzU5LjcyODggNzMuMjA2NCA1NS41NTIzIDczLjI1NSA1MS4zNTA5IDczLjIwNjdDNTAuMjQxNyA3MS41MzgxIDQ5LjEwMDUgNjkuODg3IDQ3LjkyNzEgNjguMjUzMkM0Ni44NDUxIDY5Ljg4NCA0NS44MDA5IDcxLjUzNTEgNDQuNzk0OCA3My4yMDY3QzQwLjY5MDQgNzMuMjU1IDM2LjYxMSA3My4yMDY0IDMyLjU1NjcgNzMuMDYxQzM2LjM4NDkgNjcuMDIyMSA0MC4yNDU3IDYxLjAwMDMgNDQuMTM5MiA1NC45OTUzQzQ1LjcxNzMgNTMuMzQxOSA0Ny41ODcxIDUyLjkyOTIgNDkuNzQ4MyA1My43NTdDNTAuMzIyMyA1NC4wMzE3IDUwLjgzMjIgNTQuMzk2IDUxLjI3OCA1NC44NDk3QzU1LjUxMDkgNjAuOTAyNyA1OS43MTE3IDY2Ljk3MzIgNjMuODgwMyA3My4wNjFaIiBmaWxsPSJ3aGl0ZSIvPgo8cGF0aCBmaWxsLXJ1bGU9ImV2ZW5vZGQiIGNsaXAtcnVsZT0iZXZlbm9kZCIgZD0iTTE1LjM2NTIgNzMuMDYxMUMxOC45ODI1IDczLjIwNjUgMjIuNjI0NyA3My4yNTUgMjYuMjkyMSA3My4yMDY3QzIyLjYyNSA3My40MDA0IDE4LjkzNDIgNzMuNDAwNCAxNS4yMTk1IDczLjIwNjdDMTUuMjM3NiA3My4xMTgzIDE1LjI4NjEgNzMuMDY5OCAxNS4zNjUyIDczLjA2MTFaIiBmaWxsPSJ3aGl0ZSIvPgo8cGF0aCBmaWxsLXJ1bGU9ImV2ZW5vZGQiIGNsaXAtcnVsZT0iZXZlbm9kZCIgZD0iTTMyLjU1NjggNzMuMDYxMUMzNi42MTExIDczLjIwNjUgNDAuNjkwNCA3My4yNTUgNDQuNzk0OCA3My4yMDY3QzQwLjY5MDcgNzMuNDAwNCAzNi41NjI5IDczLjQwMDQgMzIuNDExMSA3My4yMDY3QzMyLjQyOTIgNzMuMTE4MyAzMi40Nzc3IDczLjA2OTggMzIuNTU2OCA3My4wNjExWiIgZmlsbD0id2hpdGUiLz4KPHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik02My44ODAzIDczLjA2MTFDNjMuOTU5NCA3My4wNjk4IDY0LjAwNzkgNzMuMTE4MyA2NC4wMjYgNzMuMjA2N0M1OS43NzcxIDczLjQwMDUgNTUuNTUyIDczLjQwMDUgNTEuMzUwOSA3My4yMDY3QzU1LjU1MjMgNzMuMjU1IDU5LjcyODkgNzMuMjA2NSA2My44ODAzIDczLjA2MTFaIiBmaWxsPSJ3aGl0ZSIvPgo8L3N2Zz4K",
"name": "Test Metadata 0",
"origin": {
"location": "external",
@ -443,7 +443,7 @@ exports[`loadAgenticConfig > metadata-0 1`] = `
"slug": "free",
},
],
"readme": "data:application/octet-stream;base64,IyBUZXN0IE1ldGFkYXRhCgpUaGlzIGlzIHRlc3RpbmcgKipyZWFkbWUgcmVuZGVyaW5nKiouCgojIyBNaXNjCgotIFsgXSBJdGVtIDEKLSBbIF0gSXRlbSAyCi0gW3hdIEl0ZW0gMwoKLS0tCgotIF9pdGFsaWNfCi0gKipib2xkKioKLSBbbGlua10oaHR0cHM6Ly93d3cuZ29vZ2xlLmNvbSkKLSBgY29kZWAKCiMjIENvZGUKCmBgYHRzCmNvbnN0IGEgPSAxCgpleHBvcnQgZnVuY3Rpb24gZm9vKCkgewogIGNvbnNvbGUubG9nKCdoZWxsbyB3b3JsZCcpCn0KYGBgCgojIyBJbWFnZXMKCiFbSW1hZ2VdKGh0dHBzOi8vcGxhY2Vob2xkLmNvLzYwMHg0MDApCg==",
"readme": "data:text/markdown;base64,IyBUZXN0IE1ldGFkYXRhCgpUaGlzIGlzIHRlc3RpbmcgKipyZWFkbWUgcmVuZGVyaW5nKiouCgojIyBNaXNjCgotIFsgXSBJdGVtIDEKLSBbIF0gSXRlbSAyCi0gW3hdIEl0ZW0gMwoKLS0tCgotIF9pdGFsaWNfCi0gKipib2xkKioKLSBbbGlua10oaHR0cHM6Ly93d3cuZ29vZ2xlLmNvbSkKLSBgY29kZWAKCiMjIENvZGUKCmBgYHRzCmNvbnN0IGEgPSAxCgpleHBvcnQgZnVuY3Rpb24gZm9vKCkgewogIGNvbnNvbGUubG9nKCdoZWxsbyB3b3JsZCcpCn0KYGBgCgojIyBJbWFnZXMKCiFbSW1hZ2VdKGh0dHBzOi8vcGxhY2Vob2xkLmNvLzYwMHg0MDApCg==",
"slug": "test-metadata-0",
"toolConfigs": [],
}

Wyświetl plik

@ -5,7 +5,7 @@ import type {
ResolvedAgenticProjectConfig
} from '@agentic/platform-types'
import type { UploadFileToStorageFn } from './types'
import type { UploadFileUrlToStorageFn } from './types'
import {
parseAgenticProjectConfig,
parseResolvedAgenticProjectConfig
@ -22,7 +22,7 @@ export async function resolveAgenticProjectConfig(
logger?: Logger
cwd?: string
label?: string
uploadFileToStorage: UploadFileToStorageFn
uploadFileUrlToStorage: UploadFileUrlToStorageFn
}
): Promise<ResolvedAgenticProjectConfig> {
const config = parseAgenticProjectConfig(inputConfig)

Wyświetl plik

@ -1,13 +1,13 @@
import type { UploadFileToStorageFn } from './types'
import type { UploadFileUrlToStorageFn } from './types'
export async function resolveMetadataFile(
input: string | undefined,
{
label,
uploadFileToStorage
uploadFileUrlToStorage
}: {
label: string
uploadFileToStorage: UploadFileToStorageFn
uploadFileUrlToStorage: UploadFileUrlToStorageFn
}
): Promise<string | undefined> {
if (!input) return
@ -15,7 +15,7 @@ export async function resolveMetadataFile(
try {
const source = new URL(input)
return uploadFileToStorage(source.toString())
return uploadFileUrlToStorage(source.toString())
} catch {
throw new Error(`Invalid "${label}" must be a public URL: "${input}"`)
}

Wyświetl plik

@ -3,28 +3,28 @@ import type {
ResolvedAgenticProjectConfig
} from '@agentic/platform-types'
import type { UploadFileToStorageFn } from './types'
import type { UploadFileUrlToStorageFn } from './types'
import { resolveMetadataFile } from './resolve-metadata-file'
export async function resolveMetadataFiles(
{ readme, icon }: Pick<AgenticProjectConfig, 'readme' | 'icon'>,
{
uploadFileToStorage
uploadFileUrlToStorage
}: {
uploadFileToStorage: UploadFileToStorageFn
uploadFileUrlToStorage: UploadFileUrlToStorageFn
}
): Promise<Pick<ResolvedAgenticProjectConfig, 'readme' | 'iconUrl'>> {
if (readme) {
readme = await resolveMetadataFile(readme, {
label: 'readme',
uploadFileToStorage
uploadFileUrlToStorage
})
}
if (icon) {
icon = await resolveMetadataFile(icon, {
label: 'icon',
uploadFileToStorage
uploadFileUrlToStorage
})
}

Wyświetl plik

@ -1 +1 @@
export type UploadFileToStorageFn = (source: string) => Promise<string>
export type UploadFileUrlToStorageFn = (source: string) => Promise<string>

Wyświetl plik

@ -1,6 +1,8 @@
import { readFile } from 'node:fs/promises'
import path from 'node:path'
import { lookup as lookupMimeType } from 'mrmime'
export async function validateMetadataFile(
input: string | undefined,
{
@ -25,7 +27,10 @@ export async function validateMetadataFile(
// Not a URL; check if it's a local file path.
const buffer = await readFile(path.resolve(cwd, input))
return `data:application/octet-stream;base64,${buffer.toString('base64')}`
const mime =
lookupMimeType(path.extname(input)) ?? 'application/octet-stream'
return `data:${mime};base64,${buffer.toString('base64')}`
} catch {
throw new Error(
`Invalid "${label}" (must be a URL or a path to a local file): "${input}"`

Plik diff jest za duży Load Diff

Wyświetl plik

@ -135,3 +135,4 @@
- basic account page on website
- edit name, profile photo, etc
- **public project detail page metadata**
- upload user images to agentic blob storage