pilothub 0.0.1 → 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -0
- package/README.md +36 -129
- package/dist/browserAuth.d.ts +20 -0
- package/dist/browserAuth.js +156 -0
- package/dist/browserAuth.js.map +1 -0
- package/dist/browserAuth.test.d.ts +1 -0
- package/dist/browserAuth.test.js +83 -0
- package/dist/browserAuth.test.js.map +1 -0
- package/dist/cli/buildInfo.d.ts +3 -0
- package/dist/cli/buildInfo.js +103 -0
- package/dist/cli/buildInfo.js.map +1 -0
- package/dist/cli/commands/auth.d.ts +9 -0
- package/dist/cli/commands/auth.js +75 -0
- package/dist/cli/commands/auth.js.map +1 -0
- package/dist/cli/commands/delete.d.ts +11 -0
- package/dist/cli/commands/delete.js +67 -0
- package/dist/cli/commands/delete.js.map +1 -0
- package/dist/cli/commands/delete.test.d.ts +1 -0
- package/dist/cli/commands/delete.test.js +52 -0
- package/dist/cli/commands/delete.test.js.map +1 -0
- package/dist/cli/commands/publish.d.ts +9 -0
- package/dist/cli/commands/publish.js +87 -0
- package/dist/cli/commands/publish.js.map +1 -0
- package/dist/cli/commands/publish.test.d.ts +1 -0
- package/dist/cli/commands/publish.test.js +104 -0
- package/dist/cli/commands/publish.test.js.map +1 -0
- package/dist/cli/commands/skills.d.ts +23 -0
- package/dist/cli/commands/skills.js +298 -0
- package/dist/cli/commands/skills.js.map +1 -0
- package/dist/cli/commands/skills.test.d.ts +1 -0
- package/dist/cli/commands/skills.test.js +156 -0
- package/dist/cli/commands/skills.test.js.map +1 -0
- package/dist/cli/commands/star.d.ts +8 -0
- package/dist/cli/commands/star.js +38 -0
- package/dist/cli/commands/star.js.map +1 -0
- package/dist/cli/commands/sync.d.ts +3 -0
- package/dist/cli/commands/sync.js +160 -0
- package/dist/cli/commands/sync.js.map +1 -0
- package/dist/cli/commands/sync.test.d.ts +1 -0
- package/dist/cli/commands/sync.test.js +277 -0
- package/dist/cli/commands/sync.test.js.map +1 -0
- package/dist/cli/commands/syncHelpers.d.ts +76 -0
- package/dist/cli/commands/syncHelpers.js +349 -0
- package/dist/cli/commands/syncHelpers.js.map +1 -0
- package/dist/cli/commands/syncHelpers.test.d.ts +1 -0
- package/dist/cli/commands/syncHelpers.test.js +22 -0
- package/dist/cli/commands/syncHelpers.test.js.map +1 -0
- package/dist/cli/commands/syncTypes.d.ts +24 -0
- package/dist/cli/commands/syncTypes.js +2 -0
- package/dist/cli/commands/syncTypes.js.map +1 -0
- package/dist/cli/commands/unstar.d.ts +8 -0
- package/dist/cli/commands/unstar.js +38 -0
- package/dist/cli/commands/unstar.js.map +1 -0
- package/dist/cli/helpStyle.d.ts +13 -0
- package/dist/cli/helpStyle.js +38 -0
- package/dist/cli/helpStyle.js.map +1 -0
- package/dist/cli/pilotbotConfig.d.ts +6 -0
- package/dist/cli/pilotbotConfig.js +110 -0
- package/dist/cli/pilotbotConfig.js.map +1 -0
- package/dist/cli/pilotbotConfig.test.d.ts +1 -0
- package/dist/cli/pilotbotConfig.test.js +133 -0
- package/dist/cli/pilotbotConfig.test.js.map +1 -0
- package/dist/cli/registry.d.ts +7 -0
- package/dist/cli/registry.js +42 -0
- package/dist/cli/registry.js.map +1 -0
- package/dist/cli/registry.test.d.ts +1 -0
- package/dist/cli/registry.test.js +48 -0
- package/dist/cli/registry.test.js.map +1 -0
- package/dist/cli/scanSkills.d.ts +7 -0
- package/dist/cli/scanSkills.js +75 -0
- package/dist/cli/scanSkills.js.map +1 -0
- package/dist/cli/scanSkills.test.d.ts +1 -0
- package/dist/cli/scanSkills.test.js +60 -0
- package/dist/cli/scanSkills.test.js.map +1 -0
- package/dist/cli/slug.d.ts +2 -0
- package/dist/cli/slug.js +16 -0
- package/dist/cli/slug.js.map +1 -0
- package/dist/cli/types.d.ts +15 -0
- package/dist/cli/types.js +2 -0
- package/dist/cli/types.js.map +1 -0
- package/dist/cli/ui.d.ts +7 -0
- package/dist/cli/ui.js +72 -0
- package/dist/cli/ui.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +268 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.d.ts +4 -0
- package/dist/config.js +38 -0
- package/dist/config.js.map +1 -0
- package/dist/discovery.d.ts +5 -0
- package/dist/discovery.js +21 -0
- package/dist/discovery.js.map +1 -0
- package/dist/discovery.test.d.ts +1 -0
- package/dist/discovery.test.js +46 -0
- package/dist/discovery.test.js.map +1 -0
- package/dist/http.d.ts +32 -0
- package/dist/http.js +261 -0
- package/dist/http.js.map +1 -0
- package/dist/http.test.d.ts +1 -0
- package/dist/http.test.js +135 -0
- package/dist/http.test.js.map +1 -0
- package/dist/schema/ark.js.map +1 -0
- package/dist/schema/index.js.map +1 -0
- package/dist/schema/routes.js.map +1 -0
- package/{packages/schema/dist → dist/schema}/schemas.d.ts +0 -39
- package/{packages/schema/dist → dist/schema}/schemas.js +0 -22
- package/dist/schema/schemas.js.map +1 -0
- package/dist/schema/textFiles.js.map +1 -0
- package/dist/schema/textFiles.test.d.ts +1 -0
- package/dist/schema/textFiles.test.js +20 -0
- package/dist/schema/textFiles.test.js.map +1 -0
- package/dist/skills.d.ts +43 -0
- package/dist/skills.js +163 -0
- package/dist/skills.js.map +1 -0
- package/dist/skills.test.d.ts +1 -0
- package/dist/skills.test.js +144 -0
- package/dist/skills.test.js.map +1 -0
- package/dist/types.d.ts +7 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +27 -70
- package/.env.local.example +0 -19
- package/.github/workflows/ci.yml +0 -40
- package/.oxlintrc.json +0 -3
- package/AGENTS.md +0 -45
- package/CHANGELOG.md +0 -138
- package/DEPRECATIONS.md +0 -7
- package/biome.json +0 -41
- package/convex/_generated/api.d.ts +0 -153
- package/convex/_generated/api.js +0 -23
- package/convex/_generated/dataModel.d.ts +0 -60
- package/convex/_generated/server.d.ts +0 -143
- package/convex/_generated/server.js +0 -93
- package/convex/auth.config.ts +0 -8
- package/convex/auth.ts +0 -19
- package/convex/comments.ts +0 -88
- package/convex/crons.ts +0 -34
- package/convex/devSeed.ts +0 -459
- package/convex/devSeedExtra.ts +0 -541
- package/convex/downloads.ts +0 -78
- package/convex/githubBackups.ts +0 -170
- package/convex/githubBackupsNode.ts +0 -183
- package/convex/githubImport.ts +0 -317
- package/convex/githubSoulBackups.ts +0 -170
- package/convex/githubSoulBackupsNode.ts +0 -186
- package/convex/http.ts +0 -194
- package/convex/httpApi.handlers.test.ts +0 -488
- package/convex/httpApi.test.ts +0 -70
- package/convex/httpApi.ts +0 -305
- package/convex/httpApiV1.handlers.test.ts +0 -584
- package/convex/httpApiV1.ts +0 -1172
- package/convex/leaderboards.ts +0 -39
- package/convex/lib/access.ts +0 -36
- package/convex/lib/apiTokenAuth.ts +0 -36
- package/convex/lib/badges.ts +0 -50
- package/convex/lib/changelog.test.ts +0 -34
- package/convex/lib/changelog.ts +0 -278
- package/convex/lib/embeddings.ts +0 -38
- package/convex/lib/githubBackup.ts +0 -443
- package/convex/lib/githubImport.test.ts +0 -247
- package/convex/lib/githubImport.ts +0 -425
- package/convex/lib/githubSoulBackup.ts +0 -443
- package/convex/lib/leaderboards.ts +0 -103
- package/convex/lib/moderation.ts +0 -42
- package/convex/lib/public.ts +0 -89
- package/convex/lib/searchText.test.ts +0 -46
- package/convex/lib/searchText.ts +0 -27
- package/convex/lib/skillBackfill.test.ts +0 -34
- package/convex/lib/skillBackfill.ts +0 -67
- package/convex/lib/skillPublish.test.ts +0 -28
- package/convex/lib/skillPublish.ts +0 -284
- package/convex/lib/skillStats.ts +0 -80
- package/convex/lib/skills.test.ts +0 -197
- package/convex/lib/skills.ts +0 -273
- package/convex/lib/soulChangelog.ts +0 -273
- package/convex/lib/soulPublish.ts +0 -236
- package/convex/lib/tokens.test.ts +0 -33
- package/convex/lib/tokens.ts +0 -51
- package/convex/lib/webhooks.test.ts +0 -91
- package/convex/lib/webhooks.ts +0 -112
- package/convex/maintenance.test.ts +0 -270
- package/convex/maintenance.ts +0 -840
- package/convex/rateLimits.ts +0 -50
- package/convex/schema.ts +0 -472
- package/convex/search.test.ts +0 -12
- package/convex/search.ts +0 -254
- package/convex/seed.test.ts +0 -37
- package/convex/seed.ts +0 -254
- package/convex/seedSouls.ts +0 -111
- package/convex/skillStatEvents.ts +0 -568
- package/convex/skills.ts +0 -1606
- package/convex/soulComments.ts +0 -88
- package/convex/soulDownloads.ts +0 -14
- package/convex/soulStars.ts +0 -71
- package/convex/souls.ts +0 -570
- package/convex/stars.ts +0 -108
- package/convex/statsMaintenance.ts +0 -205
- package/convex/telemetry.ts +0 -434
- package/convex/tokens.ts +0 -88
- package/convex/tsconfig.json +0 -7
- package/convex/uploads.ts +0 -20
- package/convex/users.ts +0 -122
- package/convex/webhooks.ts +0 -50
- package/convex.json +0 -3
- package/docs/README.md +0 -32
- package/docs/api.md +0 -51
- package/docs/architecture.md +0 -61
- package/docs/auth.md +0 -54
- package/docs/cli.md +0 -117
- package/docs/deploy.md +0 -78
- package/docs/diffing.md +0 -84
- package/docs/github-import.md +0 -171
- package/docs/http-api.md +0 -187
- package/docs/manual-testing.md +0 -64
- package/docs/mintlify.md +0 -43
- package/docs/quickstart.md +0 -120
- package/docs/skill-format.md +0 -58
- package/docs/soul-format.md +0 -37
- package/docs/spec.md +0 -177
- package/docs/telemetry.md +0 -91
- package/docs/troubleshooting.md +0 -49
- package/docs/webhook.md +0 -51
- package/e2e/menu-smoke.pw.test.ts +0 -49
- package/e2e/pilothub.e2e.test.ts +0 -494
- package/e2e/search-exact.pw.test.ts +0 -97
- package/packages/pilothub/LICENSE +0 -22
- package/packages/pilothub/README.md +0 -57
- package/packages/pilothub/package.json +0 -41
- package/packages/pilothub/src/browserAuth.test.ts +0 -96
- package/packages/pilothub/src/browserAuth.ts +0 -174
- package/packages/pilothub/src/cli/buildInfo.ts +0 -94
- package/packages/pilothub/src/cli/commands/auth.ts +0 -97
- package/packages/pilothub/src/cli/commands/delete.test.ts +0 -73
- package/packages/pilothub/src/cli/commands/delete.ts +0 -83
- package/packages/pilothub/src/cli/commands/publish.test.ts +0 -122
- package/packages/pilothub/src/cli/commands/publish.ts +0 -108
- package/packages/pilothub/src/cli/commands/skills.test.ts +0 -191
- package/packages/pilothub/src/cli/commands/skills.ts +0 -380
- package/packages/pilothub/src/cli/commands/star.ts +0 -46
- package/packages/pilothub/src/cli/commands/sync.test.ts +0 -310
- package/packages/pilothub/src/cli/commands/sync.ts +0 -200
- package/packages/pilothub/src/cli/commands/syncHelpers.test.ts +0 -26
- package/packages/pilothub/src/cli/commands/syncHelpers.ts +0 -427
- package/packages/pilothub/src/cli/commands/syncTypes.ts +0 -27
- package/packages/pilothub/src/cli/commands/unstar.ts +0 -48
- package/packages/pilothub/src/cli/helpStyle.ts +0 -45
- package/packages/pilothub/src/cli/pilotbotConfig.test.ts +0 -159
- package/packages/pilothub/src/cli/pilotbotConfig.ts +0 -147
- package/packages/pilothub/src/cli/registry.test.ts +0 -63
- package/packages/pilothub/src/cli/registry.ts +0 -43
- package/packages/pilothub/src/cli/scanSkills.test.ts +0 -64
- package/packages/pilothub/src/cli/scanSkills.ts +0 -84
- package/packages/pilothub/src/cli/slug.ts +0 -16
- package/packages/pilothub/src/cli/types.ts +0 -12
- package/packages/pilothub/src/cli/ui.ts +0 -75
- package/packages/pilothub/src/cli.ts +0 -311
- package/packages/pilothub/src/config.ts +0 -36
- package/packages/pilothub/src/discovery.test.ts +0 -75
- package/packages/pilothub/src/discovery.ts +0 -19
- package/packages/pilothub/src/http.test.ts +0 -156
- package/packages/pilothub/src/http.ts +0 -301
- package/packages/pilothub/src/schema/ark.ts +0 -29
- package/packages/pilothub/src/schema/index.ts +0 -5
- package/packages/pilothub/src/schema/routes.ts +0 -22
- package/packages/pilothub/src/schema/schemas.ts +0 -260
- package/packages/pilothub/src/schema/textFiles.test.ts +0 -23
- package/packages/pilothub/src/schema/textFiles.ts +0 -66
- package/packages/pilothub/src/skills.test.ts +0 -191
- package/packages/pilothub/src/skills.ts +0 -172
- package/packages/pilothub/src/types.ts +0 -10
- package/packages/pilothub/tsconfig.json +0 -14
- package/packages/schema/README.md +0 -3
- package/packages/schema/dist/ark.js.map +0 -1
- package/packages/schema/dist/index.js.map +0 -1
- package/packages/schema/dist/routes.js.map +0 -1
- package/packages/schema/dist/schemas.js.map +0 -1
- package/packages/schema/dist/textFiles.js.map +0 -1
- package/packages/schema/package.json +0 -26
- package/packages/schema/src/ark.ts +0 -29
- package/packages/schema/src/index.ts +0 -5
- package/packages/schema/src/routes.ts +0 -22
- package/packages/schema/src/schemas.test.ts +0 -123
- package/packages/schema/src/schemas.ts +0 -287
- package/packages/schema/src/textFiles.test.ts +0 -23
- package/packages/schema/src/textFiles.ts +0 -66
- package/packages/schema/tsconfig.json +0 -15
- package/pilothub +0 -46
- package/playwright.config.ts +0 -33
- package/public/.well-known/pilothub.json +0 -6
- package/public/api/v1/openapi.json +0 -379
- package/public/favicon.ico +0 -0
- package/public/logo192.png +0 -0
- package/public/logo512.png +0 -0
- package/public/manifest.json +0 -25
- package/public/og.png +0 -0
- package/public/og.svg +0 -98
- package/public/pilot-logo.png +0 -0
- package/public/pilot-mark.png +0 -0
- package/public/robots.txt +0 -3
- package/public/tanstack-circle-logo.png +0 -0
- package/public/tanstack-word-logo-white.svg +0 -1
- package/scripts/check-peer-deps.ts +0 -56
- package/scripts/docs-list.ts +0 -148
- package/scripts/run-playwright-local.sh +0 -14
- package/server/og/fetchSkillOgMeta.ts +0 -27
- package/server/og/fetchSoulOgMeta.ts +0 -27
- package/server/og/ogAssets.ts +0 -80
- package/server/og/skillOgSvg.test.ts +0 -59
- package/server/og/skillOgSvg.ts +0 -258
- package/server/og/soulOgSvg.ts +0 -209
- package/server/routes/og/skill.png.ts +0 -103
- package/server/routes/og/soul.png.ts +0 -111
- package/src/__tests__/skill-detail-page.test.tsx +0 -86
- package/src/__tests__/skills-index.test.tsx +0 -145
- package/src/__tests__/upload.route.test.tsx +0 -228
- package/src/components/AppProviders.tsx +0 -19
- package/src/components/ClientOnly.tsx +0 -18
- package/src/components/Footer.tsx +0 -29
- package/src/components/Header.tsx +0 -295
- package/src/components/InstallSwitcher.tsx +0 -53
- package/src/components/SkillCard.tsx +0 -36
- package/src/components/SkillDetailPage.tsx +0 -817
- package/src/components/SkillDiffCard.tsx +0 -485
- package/src/components/SoulCard.tsx +0 -19
- package/src/components/SoulDetailPage.tsx +0 -263
- package/src/components/UserBootstrap.tsx +0 -18
- package/src/components/ui/dropdown-menu.tsx +0 -67
- package/src/components/ui/toggle-group.tsx +0 -35
- package/src/convex/client.ts +0 -3
- package/src/lib/badges.ts +0 -29
- package/src/lib/diffing.test.ts +0 -163
- package/src/lib/diffing.ts +0 -106
- package/src/lib/gravatar.test.ts +0 -9
- package/src/lib/gravatar.ts +0 -158
- package/src/lib/og.test.ts +0 -142
- package/src/lib/og.ts +0 -156
- package/src/lib/publicUser.ts +0 -39
- package/src/lib/roles.ts +0 -19
- package/src/lib/site.test.ts +0 -130
- package/src/lib/site.ts +0 -84
- package/src/lib/theme-transition.test.ts +0 -134
- package/src/lib/theme-transition.ts +0 -134
- package/src/lib/theme.test.tsx +0 -88
- package/src/lib/theme.ts +0 -43
- package/src/lib/uploadFiles.jsdom.test.ts +0 -33
- package/src/lib/uploadFiles.test.ts +0 -123
- package/src/lib/uploadFiles.ts +0 -245
- package/src/lib/uploadUtils.test.ts +0 -78
- package/src/lib/uploadUtils.ts +0 -93
- package/src/lib/useAuthStatus.ts +0 -12
- package/src/lib/utils.test.ts +0 -9
- package/src/lib/utils.ts +0 -6
- package/src/logo.svg +0 -12
- package/src/routeTree.gen.ts +0 -345
- package/src/router.tsx +0 -17
- package/src/routes/$owner/$slug.tsx +0 -55
- package/src/routes/__root.tsx +0 -136
- package/src/routes/admin.tsx +0 -11
- package/src/routes/cli/auth.tsx +0 -168
- package/src/routes/dashboard.tsx +0 -97
- package/src/routes/import.tsx +0 -415
- package/src/routes/index.tsx +0 -252
- package/src/routes/management.tsx +0 -529
- package/src/routes/settings.tsx +0 -203
- package/src/routes/skills/index.tsx +0 -422
- package/src/routes/souls/$slug.tsx +0 -55
- package/src/routes/souls/index.tsx +0 -243
- package/src/routes/stars.tsx +0 -68
- package/src/routes/u/$handle.tsx +0 -307
- package/src/routes/upload/utils.ts +0 -81
- package/src/routes/upload.tsx +0 -499
- package/src/styles.css +0 -2718
- package/tsconfig.json +0 -24
- package/tsconfig.oxlint.json +0 -16
- package/vercel.json +0 -8
- package/vite.config.ts +0 -48
- package/vitest.config.ts +0 -47
- package/vitest.e2e.config.ts +0 -11
- package/vitest.setup.ts +0 -1
- /package/{packages/pilothub/bin → bin}/pilothub.js +0 -0
- /package/{packages/schema/dist → dist/schema}/ark.d.ts +0 -0
- /package/{packages/schema/dist → dist/schema}/ark.js +0 -0
- /package/{packages/schema/dist → dist/schema}/index.d.ts +0 -0
- /package/{packages/schema/dist → dist/schema}/index.js +0 -0
- /package/{packages/schema/dist → dist/schema}/routes.d.ts +0 -0
- /package/{packages/schema/dist → dist/schema}/routes.js +0 -0
- /package/{packages/schema/dist → dist/schema}/textFiles.d.ts +0 -0
- /package/{packages/schema/dist → dist/schema}/textFiles.js +0 -0
package/convex/httpApiV1.ts
DELETED
|
@@ -1,1172 +0,0 @@
|
|
|
1
|
-
import { CliPublishRequestSchema, parseArk } from 'pilothub-schema'
|
|
2
|
-
import { api, internal } from './_generated/api'
|
|
3
|
-
import type { Doc, Id } from './_generated/dataModel'
|
|
4
|
-
import type { ActionCtx } from './_generated/server'
|
|
5
|
-
import { httpAction } from './_generated/server'
|
|
6
|
-
import { requireApiTokenUser } from './lib/apiTokenAuth'
|
|
7
|
-
import { hashToken } from './lib/tokens'
|
|
8
|
-
import { publishVersionForUser } from './skills'
|
|
9
|
-
import { publishSoulVersionForUser } from './souls'
|
|
10
|
-
|
|
11
|
-
const RATE_LIMIT_WINDOW_MS = 60_000
|
|
12
|
-
const RATE_LIMITS = {
|
|
13
|
-
read: { ip: 120, key: 600 },
|
|
14
|
-
write: { ip: 30, key: 120 },
|
|
15
|
-
} as const
|
|
16
|
-
const MAX_RAW_FILE_BYTES = 200 * 1024
|
|
17
|
-
|
|
18
|
-
type SearchSkillEntry = {
|
|
19
|
-
score: number
|
|
20
|
-
skill: {
|
|
21
|
-
slug?: string
|
|
22
|
-
displayName?: string
|
|
23
|
-
summary?: string | null
|
|
24
|
-
updatedAt?: number
|
|
25
|
-
} | null
|
|
26
|
-
version: { version?: string; createdAt?: number } | null
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
type ListSkillsResult = {
|
|
30
|
-
items: Array<{
|
|
31
|
-
skill: {
|
|
32
|
-
_id: Id<'skills'>
|
|
33
|
-
slug: string
|
|
34
|
-
displayName: string
|
|
35
|
-
summary?: string
|
|
36
|
-
tags: Record<string, Id<'skillVersions'>>
|
|
37
|
-
stats: unknown
|
|
38
|
-
createdAt: number
|
|
39
|
-
updatedAt: number
|
|
40
|
-
latestVersionId?: Id<'skillVersions'>
|
|
41
|
-
}
|
|
42
|
-
latestVersion: { version: string; createdAt: number; changelog: string } | null
|
|
43
|
-
}>
|
|
44
|
-
nextCursor: string | null
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
type SkillFile = Doc<'skillVersions'>['files'][number]
|
|
48
|
-
type SoulFile = Doc<'soulVersions'>['files'][number]
|
|
49
|
-
|
|
50
|
-
type GetBySlugResult = {
|
|
51
|
-
skill: {
|
|
52
|
-
_id: Id<'skills'>
|
|
53
|
-
slug: string
|
|
54
|
-
displayName: string
|
|
55
|
-
summary?: string
|
|
56
|
-
tags: Record<string, Id<'skillVersions'>>
|
|
57
|
-
stats: unknown
|
|
58
|
-
createdAt: number
|
|
59
|
-
updatedAt: number
|
|
60
|
-
} | null
|
|
61
|
-
latestVersion: Doc<'skillVersions'> | null
|
|
62
|
-
owner: { _id: Id<'users'>; handle?: string; displayName?: string; image?: string } | null
|
|
63
|
-
} | null
|
|
64
|
-
|
|
65
|
-
type ListVersionsResult = {
|
|
66
|
-
items: Array<{
|
|
67
|
-
version: string
|
|
68
|
-
createdAt: number
|
|
69
|
-
changelog: string
|
|
70
|
-
changelogSource?: 'auto' | 'user'
|
|
71
|
-
files: Array<{
|
|
72
|
-
path: string
|
|
73
|
-
size: number
|
|
74
|
-
storageId: Id<'_storage'>
|
|
75
|
-
sha256: string
|
|
76
|
-
contentType?: string
|
|
77
|
-
}>
|
|
78
|
-
softDeletedAt?: number
|
|
79
|
-
}>
|
|
80
|
-
nextCursor: string | null
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
type ListSoulsResult = {
|
|
84
|
-
items: Array<{
|
|
85
|
-
soul: {
|
|
86
|
-
_id: Id<'souls'>
|
|
87
|
-
slug: string
|
|
88
|
-
displayName: string
|
|
89
|
-
summary?: string
|
|
90
|
-
tags: Record<string, Id<'soulVersions'>>
|
|
91
|
-
stats: unknown
|
|
92
|
-
createdAt: number
|
|
93
|
-
updatedAt: number
|
|
94
|
-
latestVersionId?: Id<'soulVersions'>
|
|
95
|
-
}
|
|
96
|
-
latestVersion: { version: string; createdAt: number; changelog: string } | null
|
|
97
|
-
}>
|
|
98
|
-
nextCursor: string | null
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
type GetSoulBySlugResult = {
|
|
102
|
-
soul: {
|
|
103
|
-
_id: Id<'souls'>
|
|
104
|
-
slug: string
|
|
105
|
-
displayName: string
|
|
106
|
-
summary?: string
|
|
107
|
-
tags: Record<string, Id<'soulVersions'>>
|
|
108
|
-
stats: unknown
|
|
109
|
-
createdAt: number
|
|
110
|
-
updatedAt: number
|
|
111
|
-
} | null
|
|
112
|
-
latestVersion: Doc<'soulVersions'> | null
|
|
113
|
-
owner: { handle?: string; displayName?: string; image?: string } | null
|
|
114
|
-
} | null
|
|
115
|
-
|
|
116
|
-
type ListSoulVersionsResult = {
|
|
117
|
-
items: Array<{
|
|
118
|
-
version: string
|
|
119
|
-
createdAt: number
|
|
120
|
-
changelog: string
|
|
121
|
-
changelogSource?: 'auto' | 'user'
|
|
122
|
-
files: Array<{
|
|
123
|
-
path: string
|
|
124
|
-
size: number
|
|
125
|
-
storageId: Id<'_storage'>
|
|
126
|
-
sha256: string
|
|
127
|
-
contentType?: string
|
|
128
|
-
}>
|
|
129
|
-
softDeletedAt?: number
|
|
130
|
-
}>
|
|
131
|
-
nextCursor: string | null
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
async function searchSkillsV1Handler(ctx: ActionCtx, request: Request) {
|
|
135
|
-
const rate = await applyRateLimit(ctx, request, 'read')
|
|
136
|
-
if (!rate.ok) return rate.response
|
|
137
|
-
|
|
138
|
-
const url = new URL(request.url)
|
|
139
|
-
const query = url.searchParams.get('q')?.trim() ?? ''
|
|
140
|
-
const limit = toOptionalNumber(url.searchParams.get('limit'))
|
|
141
|
-
const highlightedOnly = url.searchParams.get('highlightedOnly') === 'true'
|
|
142
|
-
|
|
143
|
-
if (!query) return json({ results: [] }, 200, rate.headers)
|
|
144
|
-
|
|
145
|
-
const results = (await ctx.runAction(api.search.searchSkills, {
|
|
146
|
-
query,
|
|
147
|
-
limit,
|
|
148
|
-
highlightedOnly: highlightedOnly || undefined,
|
|
149
|
-
})) as SearchSkillEntry[]
|
|
150
|
-
|
|
151
|
-
return json(
|
|
152
|
-
{
|
|
153
|
-
results: results.map((result) => ({
|
|
154
|
-
score: result.score,
|
|
155
|
-
slug: result.skill?.slug,
|
|
156
|
-
displayName: result.skill?.displayName,
|
|
157
|
-
summary: result.skill?.summary ?? null,
|
|
158
|
-
version: result.version?.version ?? null,
|
|
159
|
-
updatedAt: result.skill?.updatedAt,
|
|
160
|
-
})),
|
|
161
|
-
},
|
|
162
|
-
200,
|
|
163
|
-
rate.headers,
|
|
164
|
-
)
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
export const searchSkillsV1Http = httpAction(searchSkillsV1Handler)
|
|
168
|
-
|
|
169
|
-
async function resolveSkillVersionV1Handler(ctx: ActionCtx, request: Request) {
|
|
170
|
-
const rate = await applyRateLimit(ctx, request, 'read')
|
|
171
|
-
if (!rate.ok) return rate.response
|
|
172
|
-
|
|
173
|
-
const url = new URL(request.url)
|
|
174
|
-
const slug = url.searchParams.get('slug')?.trim().toLowerCase()
|
|
175
|
-
const hash = url.searchParams.get('hash')?.trim().toLowerCase()
|
|
176
|
-
if (!slug || !hash) return text('Missing slug or hash', 400, rate.headers)
|
|
177
|
-
if (!/^[a-f0-9]{64}$/.test(hash)) return text('Invalid hash', 400, rate.headers)
|
|
178
|
-
|
|
179
|
-
const resolved = await ctx.runQuery(api.skills.resolveVersionByHash, { slug, hash })
|
|
180
|
-
if (!resolved) return text('Skill not found', 404, rate.headers)
|
|
181
|
-
|
|
182
|
-
return json(
|
|
183
|
-
{ slug, match: resolved.match, latestVersion: resolved.latestVersion },
|
|
184
|
-
200,
|
|
185
|
-
rate.headers,
|
|
186
|
-
)
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
export const resolveSkillVersionV1Http = httpAction(resolveSkillVersionV1Handler)
|
|
190
|
-
|
|
191
|
-
async function listSkillsV1Handler(ctx: ActionCtx, request: Request) {
|
|
192
|
-
const rate = await applyRateLimit(ctx, request, 'read')
|
|
193
|
-
if (!rate.ok) return rate.response
|
|
194
|
-
|
|
195
|
-
const url = new URL(request.url)
|
|
196
|
-
const limit = toOptionalNumber(url.searchParams.get('limit'))
|
|
197
|
-
const rawCursor = url.searchParams.get('cursor')?.trim() || undefined
|
|
198
|
-
const sort = parseListSort(url.searchParams.get('sort'))
|
|
199
|
-
const cursor = sort === 'updated' ? rawCursor : undefined
|
|
200
|
-
|
|
201
|
-
const result = (await ctx.runQuery(api.skills.listPublicPage, {
|
|
202
|
-
limit,
|
|
203
|
-
cursor,
|
|
204
|
-
sort,
|
|
205
|
-
})) as ListSkillsResult
|
|
206
|
-
|
|
207
|
-
const items = await Promise.all(
|
|
208
|
-
result.items.map(async (item) => {
|
|
209
|
-
const tags = await resolveTags(ctx, item.skill.tags)
|
|
210
|
-
return {
|
|
211
|
-
slug: item.skill.slug,
|
|
212
|
-
displayName: item.skill.displayName,
|
|
213
|
-
summary: item.skill.summary ?? null,
|
|
214
|
-
tags,
|
|
215
|
-
stats: item.skill.stats,
|
|
216
|
-
createdAt: item.skill.createdAt,
|
|
217
|
-
updatedAt: item.skill.updatedAt,
|
|
218
|
-
latestVersion: item.latestVersion
|
|
219
|
-
? {
|
|
220
|
-
version: item.latestVersion.version,
|
|
221
|
-
createdAt: item.latestVersion.createdAt,
|
|
222
|
-
changelog: item.latestVersion.changelog,
|
|
223
|
-
}
|
|
224
|
-
: null,
|
|
225
|
-
}
|
|
226
|
-
}),
|
|
227
|
-
)
|
|
228
|
-
|
|
229
|
-
return json({ items, nextCursor: result.nextCursor ?? null }, 200, rate.headers)
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
export const listSkillsV1Http = httpAction(listSkillsV1Handler)
|
|
233
|
-
|
|
234
|
-
async function skillsGetRouterV1Handler(ctx: ActionCtx, request: Request) {
|
|
235
|
-
const rate = await applyRateLimit(ctx, request, 'read')
|
|
236
|
-
if (!rate.ok) return rate.response
|
|
237
|
-
|
|
238
|
-
const segments = getPathSegments(request, '/api/v1/skills/')
|
|
239
|
-
if (segments.length === 0) return text('Missing slug', 400, rate.headers)
|
|
240
|
-
const slug = segments[0]?.trim().toLowerCase() ?? ''
|
|
241
|
-
const second = segments[1]
|
|
242
|
-
const third = segments[2]
|
|
243
|
-
|
|
244
|
-
if (segments.length === 1) {
|
|
245
|
-
const result = (await ctx.runQuery(api.skills.getBySlug, { slug })) as GetBySlugResult
|
|
246
|
-
if (!result?.skill) return text('Skill not found', 404, rate.headers)
|
|
247
|
-
|
|
248
|
-
const tags = await resolveTags(ctx, result.skill.tags)
|
|
249
|
-
return json(
|
|
250
|
-
{
|
|
251
|
-
skill: {
|
|
252
|
-
slug: result.skill.slug,
|
|
253
|
-
displayName: result.skill.displayName,
|
|
254
|
-
summary: result.skill.summary ?? null,
|
|
255
|
-
tags,
|
|
256
|
-
stats: result.skill.stats,
|
|
257
|
-
createdAt: result.skill.createdAt,
|
|
258
|
-
updatedAt: result.skill.updatedAt,
|
|
259
|
-
},
|
|
260
|
-
latestVersion: result.latestVersion
|
|
261
|
-
? {
|
|
262
|
-
version: result.latestVersion.version,
|
|
263
|
-
createdAt: result.latestVersion.createdAt,
|
|
264
|
-
changelog: result.latestVersion.changelog,
|
|
265
|
-
}
|
|
266
|
-
: null,
|
|
267
|
-
owner: result.owner
|
|
268
|
-
? {
|
|
269
|
-
handle: result.owner.handle ?? null,
|
|
270
|
-
userId: result.owner._id,
|
|
271
|
-
displayName: result.owner.displayName ?? null,
|
|
272
|
-
image: result.owner.image ?? null,
|
|
273
|
-
}
|
|
274
|
-
: null,
|
|
275
|
-
},
|
|
276
|
-
200,
|
|
277
|
-
rate.headers,
|
|
278
|
-
)
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
if (second === 'versions' && segments.length === 2) {
|
|
282
|
-
const skill = await ctx.runQuery(internal.skills.getSkillBySlugInternal, { slug })
|
|
283
|
-
if (!skill || skill.softDeletedAt) return text('Skill not found', 404, rate.headers)
|
|
284
|
-
|
|
285
|
-
const url = new URL(request.url)
|
|
286
|
-
const limit = toOptionalNumber(url.searchParams.get('limit'))
|
|
287
|
-
const cursor = url.searchParams.get('cursor')?.trim() || undefined
|
|
288
|
-
const result = (await ctx.runQuery(api.skills.listVersionsPage, {
|
|
289
|
-
skillId: skill._id,
|
|
290
|
-
limit,
|
|
291
|
-
cursor,
|
|
292
|
-
})) as ListVersionsResult
|
|
293
|
-
|
|
294
|
-
const items = result.items
|
|
295
|
-
.filter((version) => !version.softDeletedAt)
|
|
296
|
-
.map((version) => ({
|
|
297
|
-
version: version.version,
|
|
298
|
-
createdAt: version.createdAt,
|
|
299
|
-
changelog: version.changelog,
|
|
300
|
-
changelogSource: version.changelogSource ?? null,
|
|
301
|
-
}))
|
|
302
|
-
|
|
303
|
-
return json({ items, nextCursor: result.nextCursor ?? null }, 200, rate.headers)
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
if (second === 'versions' && third && segments.length === 3) {
|
|
307
|
-
const skill = await ctx.runQuery(internal.skills.getSkillBySlugInternal, { slug })
|
|
308
|
-
if (!skill || skill.softDeletedAt) return text('Skill not found', 404, rate.headers)
|
|
309
|
-
|
|
310
|
-
const version = await ctx.runQuery(api.skills.getVersionBySkillAndVersion, {
|
|
311
|
-
skillId: skill._id,
|
|
312
|
-
version: third,
|
|
313
|
-
})
|
|
314
|
-
if (!version) return text('Version not found', 404, rate.headers)
|
|
315
|
-
if (version.softDeletedAt) return text('Version not available', 410, rate.headers)
|
|
316
|
-
|
|
317
|
-
return json(
|
|
318
|
-
{
|
|
319
|
-
skill: { slug: skill.slug, displayName: skill.displayName },
|
|
320
|
-
version: {
|
|
321
|
-
version: version.version,
|
|
322
|
-
createdAt: version.createdAt,
|
|
323
|
-
changelog: version.changelog,
|
|
324
|
-
changelogSource: version.changelogSource ?? null,
|
|
325
|
-
files: version.files.map((file: SkillFile) => ({
|
|
326
|
-
path: file.path,
|
|
327
|
-
size: file.size,
|
|
328
|
-
sha256: file.sha256,
|
|
329
|
-
contentType: file.contentType ?? null,
|
|
330
|
-
})),
|
|
331
|
-
},
|
|
332
|
-
},
|
|
333
|
-
200,
|
|
334
|
-
rate.headers,
|
|
335
|
-
)
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
if (second === 'file' && segments.length === 2) {
|
|
339
|
-
const url = new URL(request.url)
|
|
340
|
-
const path = url.searchParams.get('path')?.trim()
|
|
341
|
-
if (!path) return text('Missing path', 400, rate.headers)
|
|
342
|
-
const versionParam = url.searchParams.get('version')?.trim()
|
|
343
|
-
const tagParam = url.searchParams.get('tag')?.trim()
|
|
344
|
-
|
|
345
|
-
const skillResult = (await ctx.runQuery(api.skills.getBySlug, {
|
|
346
|
-
slug,
|
|
347
|
-
})) as GetBySlugResult
|
|
348
|
-
if (!skillResult?.skill) return text('Skill not found', 404, rate.headers)
|
|
349
|
-
|
|
350
|
-
let version = skillResult.latestVersion
|
|
351
|
-
if (versionParam) {
|
|
352
|
-
version = await ctx.runQuery(api.skills.getVersionBySkillAndVersion, {
|
|
353
|
-
skillId: skillResult.skill._id,
|
|
354
|
-
version: versionParam,
|
|
355
|
-
})
|
|
356
|
-
} else if (tagParam) {
|
|
357
|
-
const versionId = skillResult.skill.tags[tagParam]
|
|
358
|
-
if (versionId) {
|
|
359
|
-
version = await ctx.runQuery(api.skills.getVersionById, { versionId })
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
if (!version) return text('Version not found', 404, rate.headers)
|
|
364
|
-
if (version.softDeletedAt) return text('Version not available', 410, rate.headers)
|
|
365
|
-
|
|
366
|
-
const normalized = path.trim()
|
|
367
|
-
const normalizedLower = normalized.toLowerCase()
|
|
368
|
-
const file =
|
|
369
|
-
version.files.find((entry) => entry.path === normalized) ??
|
|
370
|
-
version.files.find((entry) => entry.path.toLowerCase() === normalizedLower)
|
|
371
|
-
if (!file) return text('File not found', 404, rate.headers)
|
|
372
|
-
if (file.size > MAX_RAW_FILE_BYTES) return text('File exceeds 200KB limit', 413, rate.headers)
|
|
373
|
-
|
|
374
|
-
const blob = await ctx.storage.get(file.storageId)
|
|
375
|
-
if (!blob) return text('File missing in storage', 410, rate.headers)
|
|
376
|
-
const textContent = await blob.text()
|
|
377
|
-
|
|
378
|
-
const isSvg =
|
|
379
|
-
file.contentType?.toLowerCase().includes('svg') ||
|
|
380
|
-
file.path.toLowerCase().endsWith('.svg')
|
|
381
|
-
|
|
382
|
-
const headers = mergeHeaders(rate.headers, {
|
|
383
|
-
'Content-Type': file.contentType
|
|
384
|
-
? `${file.contentType}; charset=utf-8`
|
|
385
|
-
: 'text/plain; charset=utf-8',
|
|
386
|
-
'Cache-Control': 'private, max-age=60',
|
|
387
|
-
ETag: file.sha256,
|
|
388
|
-
'X-Content-SHA256': file.sha256,
|
|
389
|
-
'X-Content-Size': String(file.size),
|
|
390
|
-
'X-Content-Type-Options': 'nosniff',
|
|
391
|
-
'X-Frame-Options': 'DENY',
|
|
392
|
-
// For any text response that a browser might try to render, lock it down.
|
|
393
|
-
// In particular, this prevents SVG <foreignObject> script execution from
|
|
394
|
-
// reading localStorage tokens on this origin.
|
|
395
|
-
'Content-Security-Policy': "default-src 'none'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'",
|
|
396
|
-
...(isSvg ? { 'Content-Disposition': 'attachment' } : {}),
|
|
397
|
-
})
|
|
398
|
-
return new Response(textContent, { status: 200, headers })
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
return text('Not found', 404, rate.headers)
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
export const skillsGetRouterV1Http = httpAction(skillsGetRouterV1Handler)
|
|
405
|
-
|
|
406
|
-
async function publishSkillV1Handler(ctx: ActionCtx, request: Request) {
|
|
407
|
-
const rate = await applyRateLimit(ctx, request, 'write')
|
|
408
|
-
if (!rate.ok) return rate.response
|
|
409
|
-
|
|
410
|
-
try {
|
|
411
|
-
if (!parseBearerToken(request)) return text('Unauthorized', 401, rate.headers)
|
|
412
|
-
} catch {
|
|
413
|
-
return text('Unauthorized', 401, rate.headers)
|
|
414
|
-
}
|
|
415
|
-
const { userId } = await requireApiTokenUser(ctx, request)
|
|
416
|
-
|
|
417
|
-
const contentType = request.headers.get('content-type') ?? ''
|
|
418
|
-
try {
|
|
419
|
-
if (contentType.includes('application/json')) {
|
|
420
|
-
const body = await request.json()
|
|
421
|
-
const payload = parsePublishBody(body)
|
|
422
|
-
const result = await publishVersionForUser(ctx, userId, payload)
|
|
423
|
-
return json({ ok: true, ...result }, 200, rate.headers)
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
if (contentType.includes('multipart/form-data')) {
|
|
427
|
-
const payload = await parseMultipartPublish(ctx, request)
|
|
428
|
-
const result = await publishVersionForUser(ctx, userId, payload)
|
|
429
|
-
return json({ ok: true, ...result }, 200, rate.headers)
|
|
430
|
-
}
|
|
431
|
-
} catch (error) {
|
|
432
|
-
const message = error instanceof Error ? error.message : 'Publish failed'
|
|
433
|
-
return text(message, 400, rate.headers)
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
return text('Unsupported content type', 415, rate.headers)
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
export const publishSkillV1Http = httpAction(publishSkillV1Handler)
|
|
440
|
-
|
|
441
|
-
type FileLike = {
|
|
442
|
-
name: string
|
|
443
|
-
size: number
|
|
444
|
-
type: string
|
|
445
|
-
arrayBuffer: () => Promise<ArrayBuffer>
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
type FileLikeEntry = FormDataEntryValue & FileLike
|
|
449
|
-
|
|
450
|
-
function toFileLike(entry: FormDataEntryValue): FileLikeEntry | null {
|
|
451
|
-
if (typeof entry === 'string') return null
|
|
452
|
-
const candidate = entry as Partial<FileLike>
|
|
453
|
-
if (typeof candidate.name !== 'string') return null
|
|
454
|
-
if (typeof candidate.size !== 'number') return null
|
|
455
|
-
if (typeof candidate.arrayBuffer !== 'function') return null
|
|
456
|
-
return entry as FileLikeEntry
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
async function skillsPostRouterV1Handler(ctx: ActionCtx, request: Request) {
|
|
460
|
-
const rate = await applyRateLimit(ctx, request, 'write')
|
|
461
|
-
if (!rate.ok) return rate.response
|
|
462
|
-
|
|
463
|
-
const segments = getPathSegments(request, '/api/v1/skills/')
|
|
464
|
-
if (segments.length !== 2 || segments[1] !== 'undelete') {
|
|
465
|
-
return text('Not found', 404, rate.headers)
|
|
466
|
-
}
|
|
467
|
-
const slug = segments[0]?.trim().toLowerCase() ?? ''
|
|
468
|
-
try {
|
|
469
|
-
const { userId } = await requireApiTokenUser(ctx, request)
|
|
470
|
-
await ctx.runMutation(internal.skills.setSkillSoftDeletedInternal, {
|
|
471
|
-
userId,
|
|
472
|
-
slug,
|
|
473
|
-
deleted: false,
|
|
474
|
-
})
|
|
475
|
-
return json({ ok: true }, 200, rate.headers)
|
|
476
|
-
} catch {
|
|
477
|
-
return text('Unauthorized', 401, rate.headers)
|
|
478
|
-
}
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
export const skillsPostRouterV1Http = httpAction(skillsPostRouterV1Handler)
|
|
482
|
-
|
|
483
|
-
async function skillsDeleteRouterV1Handler(ctx: ActionCtx, request: Request) {
|
|
484
|
-
const rate = await applyRateLimit(ctx, request, 'write')
|
|
485
|
-
if (!rate.ok) return rate.response
|
|
486
|
-
|
|
487
|
-
const segments = getPathSegments(request, '/api/v1/skills/')
|
|
488
|
-
if (segments.length !== 1) return text('Not found', 404, rate.headers)
|
|
489
|
-
const slug = segments[0]?.trim().toLowerCase() ?? ''
|
|
490
|
-
try {
|
|
491
|
-
const { userId } = await requireApiTokenUser(ctx, request)
|
|
492
|
-
await ctx.runMutation(internal.skills.setSkillSoftDeletedInternal, {
|
|
493
|
-
userId,
|
|
494
|
-
slug,
|
|
495
|
-
deleted: true,
|
|
496
|
-
})
|
|
497
|
-
return json({ ok: true }, 200, rate.headers)
|
|
498
|
-
} catch {
|
|
499
|
-
return text('Unauthorized', 401, rate.headers)
|
|
500
|
-
}
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
export const skillsDeleteRouterV1Http = httpAction(skillsDeleteRouterV1Handler)
|
|
504
|
-
|
|
505
|
-
async function whoamiV1Handler(ctx: ActionCtx, request: Request) {
|
|
506
|
-
const rate = await applyRateLimit(ctx, request, 'read')
|
|
507
|
-
if (!rate.ok) return rate.response
|
|
508
|
-
|
|
509
|
-
try {
|
|
510
|
-
const { user } = await requireApiTokenUser(ctx, request)
|
|
511
|
-
return json(
|
|
512
|
-
{
|
|
513
|
-
user: {
|
|
514
|
-
handle: user.handle ?? null,
|
|
515
|
-
displayName: user.displayName ?? null,
|
|
516
|
-
image: user.image ?? null,
|
|
517
|
-
},
|
|
518
|
-
},
|
|
519
|
-
200,
|
|
520
|
-
rate.headers,
|
|
521
|
-
)
|
|
522
|
-
} catch {
|
|
523
|
-
return text('Unauthorized', 401, rate.headers)
|
|
524
|
-
}
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
export const whoamiV1Http = httpAction(whoamiV1Handler)
|
|
528
|
-
|
|
529
|
-
async function parseMultipartPublish(
|
|
530
|
-
ctx: ActionCtx,
|
|
531
|
-
request: Request,
|
|
532
|
-
): Promise<{
|
|
533
|
-
slug: string
|
|
534
|
-
displayName: string
|
|
535
|
-
version: string
|
|
536
|
-
changelog: string
|
|
537
|
-
tags?: string[]
|
|
538
|
-
forkOf?: { slug: string; version?: string }
|
|
539
|
-
files: Array<{
|
|
540
|
-
path: string
|
|
541
|
-
size: number
|
|
542
|
-
storageId: Id<'_storage'>
|
|
543
|
-
sha256: string
|
|
544
|
-
contentType?: string
|
|
545
|
-
}>
|
|
546
|
-
}> {
|
|
547
|
-
const form = await request.formData()
|
|
548
|
-
const payloadRaw = form.get('payload')
|
|
549
|
-
if (!payloadRaw || typeof payloadRaw !== 'string') {
|
|
550
|
-
throw new Error('Missing payload')
|
|
551
|
-
}
|
|
552
|
-
let payload: Record<string, unknown>
|
|
553
|
-
try {
|
|
554
|
-
payload = JSON.parse(payloadRaw) as Record<string, unknown>
|
|
555
|
-
} catch {
|
|
556
|
-
throw new Error('Invalid JSON payload')
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
const files: Array<{
|
|
560
|
-
path: string
|
|
561
|
-
size: number
|
|
562
|
-
storageId: Id<'_storage'>
|
|
563
|
-
sha256: string
|
|
564
|
-
contentType?: string
|
|
565
|
-
}> = []
|
|
566
|
-
|
|
567
|
-
for (const entry of form.getAll('files')) {
|
|
568
|
-
const file = toFileLike(entry)
|
|
569
|
-
if (!file) continue
|
|
570
|
-
const path = file.name
|
|
571
|
-
const size = file.size
|
|
572
|
-
const contentType = file.type || undefined
|
|
573
|
-
const buffer = new Uint8Array(await file.arrayBuffer())
|
|
574
|
-
const sha256 = await sha256Hex(buffer)
|
|
575
|
-
const storageId = await ctx.storage.store(file as Blob)
|
|
576
|
-
files.push({ path, size, storageId, sha256, contentType })
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
const forkOf = payload.forkOf && typeof payload.forkOf === 'object' ? payload.forkOf : undefined
|
|
580
|
-
const body = {
|
|
581
|
-
slug: payload.slug,
|
|
582
|
-
displayName: payload.displayName,
|
|
583
|
-
version: payload.version,
|
|
584
|
-
changelog: typeof payload.changelog === 'string' ? payload.changelog : '',
|
|
585
|
-
tags: Array.isArray(payload.tags) ? payload.tags : undefined,
|
|
586
|
-
...(payload.source ? { source: payload.source } : {}),
|
|
587
|
-
files,
|
|
588
|
-
...(forkOf ? { forkOf } : {}),
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
return parsePublishBody(body)
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
function parsePublishBody(body: unknown) {
|
|
595
|
-
const parsed = parseArk(CliPublishRequestSchema, body, 'Publish payload')
|
|
596
|
-
if (parsed.files.length === 0) throw new Error('files required')
|
|
597
|
-
const tags = parsed.tags && parsed.tags.length > 0 ? parsed.tags : undefined
|
|
598
|
-
return {
|
|
599
|
-
slug: parsed.slug,
|
|
600
|
-
displayName: parsed.displayName,
|
|
601
|
-
version: parsed.version,
|
|
602
|
-
changelog: parsed.changelog,
|
|
603
|
-
tags,
|
|
604
|
-
source: parsed.source ?? undefined,
|
|
605
|
-
forkOf: parsed.forkOf
|
|
606
|
-
? {
|
|
607
|
-
slug: parsed.forkOf.slug,
|
|
608
|
-
version: parsed.forkOf.version ?? undefined,
|
|
609
|
-
}
|
|
610
|
-
: undefined,
|
|
611
|
-
files: parsed.files.map((file) => ({
|
|
612
|
-
...file,
|
|
613
|
-
storageId: file.storageId as Id<'_storage'>,
|
|
614
|
-
})),
|
|
615
|
-
}
|
|
616
|
-
}
|
|
617
|
-
|
|
618
|
-
async function resolveSoulTags(
|
|
619
|
-
ctx: ActionCtx,
|
|
620
|
-
tags: Record<string, Id<'soulVersions'>>,
|
|
621
|
-
): Promise<Record<string, string>> {
|
|
622
|
-
const resolved: Record<string, string> = {}
|
|
623
|
-
for (const [tag, versionId] of Object.entries(tags)) {
|
|
624
|
-
const version = await ctx.runQuery(api.souls.getVersionById, { versionId })
|
|
625
|
-
if (version && !version.softDeletedAt) {
|
|
626
|
-
resolved[tag] = version.version
|
|
627
|
-
}
|
|
628
|
-
}
|
|
629
|
-
return resolved
|
|
630
|
-
}
|
|
631
|
-
|
|
632
|
-
async function resolveTags(
|
|
633
|
-
ctx: ActionCtx,
|
|
634
|
-
tags: Record<string, Id<'skillVersions'>>,
|
|
635
|
-
): Promise<Record<string, string>> {
|
|
636
|
-
const resolved: Record<string, string> = {}
|
|
637
|
-
for (const [tag, versionId] of Object.entries(tags)) {
|
|
638
|
-
const version = await ctx.runQuery(api.skills.getVersionById, { versionId })
|
|
639
|
-
if (version && !version.softDeletedAt) {
|
|
640
|
-
resolved[tag] = version.version
|
|
641
|
-
}
|
|
642
|
-
}
|
|
643
|
-
return resolved
|
|
644
|
-
}
|
|
645
|
-
|
|
646
|
-
async function applyRateLimit(
|
|
647
|
-
ctx: ActionCtx,
|
|
648
|
-
request: Request,
|
|
649
|
-
kind: 'read' | 'write',
|
|
650
|
-
): Promise<{ ok: true; headers: HeadersInit } | { ok: false; response: Response }> {
|
|
651
|
-
const ip = getClientIp(request) ?? 'unknown'
|
|
652
|
-
const ipResult = await checkRateLimit(ctx, `ip:${ip}`, RATE_LIMITS[kind].ip)
|
|
653
|
-
const token = parseBearerToken(request)
|
|
654
|
-
const keyResult = token
|
|
655
|
-
? await checkRateLimit(ctx, `key:${await hashToken(token)}`, RATE_LIMITS[kind].key)
|
|
656
|
-
: null
|
|
657
|
-
|
|
658
|
-
const chosen = pickMostRestrictive(ipResult, keyResult)
|
|
659
|
-
const headers = rateHeaders(chosen)
|
|
660
|
-
|
|
661
|
-
if (!ipResult.allowed || (keyResult && !keyResult.allowed)) {
|
|
662
|
-
return {
|
|
663
|
-
ok: false,
|
|
664
|
-
response: text('Rate limit exceeded', 429, headers),
|
|
665
|
-
}
|
|
666
|
-
}
|
|
667
|
-
|
|
668
|
-
return { ok: true, headers }
|
|
669
|
-
}
|
|
670
|
-
|
|
671
|
-
type RateLimitResult = {
|
|
672
|
-
allowed: boolean
|
|
673
|
-
remaining: number
|
|
674
|
-
limit: number
|
|
675
|
-
resetAt: number
|
|
676
|
-
}
|
|
677
|
-
|
|
678
|
-
async function checkRateLimit(
|
|
679
|
-
ctx: ActionCtx,
|
|
680
|
-
key: string,
|
|
681
|
-
limit: number,
|
|
682
|
-
): Promise<RateLimitResult> {
|
|
683
|
-
return (await ctx.runMutation(internal.rateLimits.checkRateLimitInternal, {
|
|
684
|
-
key,
|
|
685
|
-
limit,
|
|
686
|
-
windowMs: RATE_LIMIT_WINDOW_MS,
|
|
687
|
-
})) as RateLimitResult
|
|
688
|
-
}
|
|
689
|
-
|
|
690
|
-
function pickMostRestrictive(primary: RateLimitResult, secondary: RateLimitResult | null) {
|
|
691
|
-
if (!secondary) return primary
|
|
692
|
-
if (!primary.allowed) return primary
|
|
693
|
-
if (!secondary.allowed) return secondary
|
|
694
|
-
return secondary.remaining < primary.remaining ? secondary : primary
|
|
695
|
-
}
|
|
696
|
-
|
|
697
|
-
function rateHeaders(result: RateLimitResult): HeadersInit {
|
|
698
|
-
const resetSeconds = Math.ceil(result.resetAt / 1000)
|
|
699
|
-
return {
|
|
700
|
-
'X-RateLimit-Limit': String(result.limit),
|
|
701
|
-
'X-RateLimit-Remaining': String(result.remaining),
|
|
702
|
-
'X-RateLimit-Reset': String(resetSeconds),
|
|
703
|
-
...(result.allowed ? {} : { 'Retry-After': String(resetSeconds) }),
|
|
704
|
-
}
|
|
705
|
-
}
|
|
706
|
-
|
|
707
|
-
function getClientIp(request: Request) {
|
|
708
|
-
const header =
|
|
709
|
-
request.headers.get('cf-connecting-ip') ??
|
|
710
|
-
request.headers.get('x-real-ip') ??
|
|
711
|
-
request.headers.get('x-forwarded-for') ??
|
|
712
|
-
request.headers.get('fly-client-ip')
|
|
713
|
-
if (!header) return null
|
|
714
|
-
if (header.includes(',')) return header.split(',')[0]?.trim() || null
|
|
715
|
-
return header.trim()
|
|
716
|
-
}
|
|
717
|
-
|
|
718
|
-
function parseBearerToken(request: Request) {
|
|
719
|
-
const header = request.headers.get('authorization') ?? request.headers.get('Authorization')
|
|
720
|
-
if (!header) return null
|
|
721
|
-
const trimmed = header.trim()
|
|
722
|
-
if (!trimmed.toLowerCase().startsWith('bearer ')) return null
|
|
723
|
-
const token = trimmed.slice(7).trim()
|
|
724
|
-
return token || null
|
|
725
|
-
}
|
|
726
|
-
|
|
727
|
-
function json(value: unknown, status = 200, headers?: HeadersInit) {
|
|
728
|
-
return new Response(JSON.stringify(value), {
|
|
729
|
-
status,
|
|
730
|
-
headers: mergeHeaders(
|
|
731
|
-
{
|
|
732
|
-
'Content-Type': 'application/json',
|
|
733
|
-
'Cache-Control': 'no-store',
|
|
734
|
-
},
|
|
735
|
-
headers,
|
|
736
|
-
),
|
|
737
|
-
})
|
|
738
|
-
}
|
|
739
|
-
|
|
740
|
-
function text(value: string, status: number, headers?: HeadersInit) {
|
|
741
|
-
return new Response(value, {
|
|
742
|
-
status,
|
|
743
|
-
headers: mergeHeaders(
|
|
744
|
-
{
|
|
745
|
-
'Content-Type': 'text/plain; charset=utf-8',
|
|
746
|
-
'Cache-Control': 'no-store',
|
|
747
|
-
},
|
|
748
|
-
headers,
|
|
749
|
-
),
|
|
750
|
-
})
|
|
751
|
-
}
|
|
752
|
-
|
|
753
|
-
function mergeHeaders(base: HeadersInit, extra?: HeadersInit) {
|
|
754
|
-
return { ...(base as Record<string, string>), ...(extra as Record<string, string>) }
|
|
755
|
-
}
|
|
756
|
-
|
|
757
|
-
function getPathSegments(request: Request, prefix: string) {
|
|
758
|
-
const pathname = new URL(request.url).pathname
|
|
759
|
-
if (!pathname.startsWith(prefix)) return []
|
|
760
|
-
const rest = pathname.slice(prefix.length)
|
|
761
|
-
return rest
|
|
762
|
-
.split('/')
|
|
763
|
-
.map((segment) => segment.trim())
|
|
764
|
-
.filter(Boolean)
|
|
765
|
-
.map((segment) => decodeURIComponent(segment))
|
|
766
|
-
}
|
|
767
|
-
|
|
768
|
-
function toOptionalNumber(value: string | null) {
|
|
769
|
-
if (!value) return undefined
|
|
770
|
-
const parsed = Number.parseInt(value, 10)
|
|
771
|
-
return Number.isFinite(parsed) ? parsed : undefined
|
|
772
|
-
}
|
|
773
|
-
|
|
774
|
-
type SkillListSort =
|
|
775
|
-
| 'updated'
|
|
776
|
-
| 'downloads'
|
|
777
|
-
| 'stars'
|
|
778
|
-
| 'installsCurrent'
|
|
779
|
-
| 'installsAllTime'
|
|
780
|
-
| 'trending'
|
|
781
|
-
|
|
782
|
-
function parseListSort(value: string | null): SkillListSort {
|
|
783
|
-
const normalized = value?.trim().toLowerCase()
|
|
784
|
-
if (normalized === 'downloads') return 'downloads'
|
|
785
|
-
if (normalized === 'stars' || normalized === 'rating') return 'stars'
|
|
786
|
-
if (
|
|
787
|
-
normalized === 'installs' ||
|
|
788
|
-
normalized === 'install' ||
|
|
789
|
-
normalized === 'installscurrent' ||
|
|
790
|
-
normalized === 'installs-current'
|
|
791
|
-
) {
|
|
792
|
-
return 'installsCurrent'
|
|
793
|
-
}
|
|
794
|
-
if (normalized === 'installsalltime' || normalized === 'installs-all-time') {
|
|
795
|
-
return 'installsAllTime'
|
|
796
|
-
}
|
|
797
|
-
if (normalized === 'trending') return 'trending'
|
|
798
|
-
return 'updated'
|
|
799
|
-
}
|
|
800
|
-
|
|
801
|
-
async function sha256Hex(bytes: Uint8Array) {
|
|
802
|
-
const data = new Uint8Array(bytes)
|
|
803
|
-
const digest = await crypto.subtle.digest('SHA-256', data)
|
|
804
|
-
return toHex(new Uint8Array(digest))
|
|
805
|
-
}
|
|
806
|
-
|
|
807
|
-
function toHex(bytes: Uint8Array) {
|
|
808
|
-
let out = ''
|
|
809
|
-
for (const byte of bytes) out += byte.toString(16).padStart(2, '0')
|
|
810
|
-
return out
|
|
811
|
-
}
|
|
812
|
-
|
|
813
|
-
async function listSoulsV1Handler(ctx: ActionCtx, request: Request) {
|
|
814
|
-
const rate = await applyRateLimit(ctx, request, 'read')
|
|
815
|
-
if (!rate.ok) return rate.response
|
|
816
|
-
|
|
817
|
-
const url = new URL(request.url)
|
|
818
|
-
const limit = toOptionalNumber(url.searchParams.get('limit'))
|
|
819
|
-
const cursor = url.searchParams.get('cursor')?.trim() || undefined
|
|
820
|
-
|
|
821
|
-
const result = (await ctx.runQuery(api.souls.listPublicPage, {
|
|
822
|
-
limit,
|
|
823
|
-
cursor,
|
|
824
|
-
})) as ListSoulsResult
|
|
825
|
-
|
|
826
|
-
const items = await Promise.all(
|
|
827
|
-
result.items.map(async (item) => {
|
|
828
|
-
const tags = await resolveSoulTags(ctx, item.soul.tags)
|
|
829
|
-
return {
|
|
830
|
-
slug: item.soul.slug,
|
|
831
|
-
displayName: item.soul.displayName,
|
|
832
|
-
summary: item.soul.summary ?? null,
|
|
833
|
-
tags,
|
|
834
|
-
stats: item.soul.stats,
|
|
835
|
-
createdAt: item.soul.createdAt,
|
|
836
|
-
updatedAt: item.soul.updatedAt,
|
|
837
|
-
latestVersion: item.latestVersion
|
|
838
|
-
? {
|
|
839
|
-
version: item.latestVersion.version,
|
|
840
|
-
createdAt: item.latestVersion.createdAt,
|
|
841
|
-
changelog: item.latestVersion.changelog,
|
|
842
|
-
}
|
|
843
|
-
: null,
|
|
844
|
-
}
|
|
845
|
-
}),
|
|
846
|
-
)
|
|
847
|
-
|
|
848
|
-
return json({ items, nextCursor: result.nextCursor ?? null }, 200, rate.headers)
|
|
849
|
-
}
|
|
850
|
-
|
|
851
|
-
export const listSoulsV1Http = httpAction(listSoulsV1Handler)
|
|
852
|
-
|
|
853
|
-
async function soulsGetRouterV1Handler(ctx: ActionCtx, request: Request) {
|
|
854
|
-
const rate = await applyRateLimit(ctx, request, 'read')
|
|
855
|
-
if (!rate.ok) return rate.response
|
|
856
|
-
|
|
857
|
-
const segments = getPathSegments(request, '/api/v1/souls/')
|
|
858
|
-
if (segments.length === 0) return text('Missing slug', 400, rate.headers)
|
|
859
|
-
const slug = segments[0]?.trim().toLowerCase() ?? ''
|
|
860
|
-
const second = segments[1]
|
|
861
|
-
const third = segments[2]
|
|
862
|
-
|
|
863
|
-
if (segments.length === 1) {
|
|
864
|
-
const result = (await ctx.runQuery(api.souls.getBySlug, { slug })) as GetSoulBySlugResult
|
|
865
|
-
if (!result?.soul) return text('Soul not found', 404, rate.headers)
|
|
866
|
-
|
|
867
|
-
const tags = await resolveSoulTags(ctx, result.soul.tags)
|
|
868
|
-
return json(
|
|
869
|
-
{
|
|
870
|
-
soul: {
|
|
871
|
-
slug: result.soul.slug,
|
|
872
|
-
displayName: result.soul.displayName,
|
|
873
|
-
summary: result.soul.summary ?? null,
|
|
874
|
-
tags,
|
|
875
|
-
stats: result.soul.stats,
|
|
876
|
-
createdAt: result.soul.createdAt,
|
|
877
|
-
updatedAt: result.soul.updatedAt,
|
|
878
|
-
},
|
|
879
|
-
latestVersion: result.latestVersion
|
|
880
|
-
? {
|
|
881
|
-
version: result.latestVersion.version,
|
|
882
|
-
createdAt: result.latestVersion.createdAt,
|
|
883
|
-
changelog: result.latestVersion.changelog,
|
|
884
|
-
}
|
|
885
|
-
: null,
|
|
886
|
-
owner: result.owner
|
|
887
|
-
? {
|
|
888
|
-
handle: result.owner.handle ?? null,
|
|
889
|
-
displayName: result.owner.displayName ?? null,
|
|
890
|
-
image: result.owner.image ?? null,
|
|
891
|
-
}
|
|
892
|
-
: null,
|
|
893
|
-
},
|
|
894
|
-
200,
|
|
895
|
-
rate.headers,
|
|
896
|
-
)
|
|
897
|
-
}
|
|
898
|
-
|
|
899
|
-
if (second === 'versions' && segments.length === 2) {
|
|
900
|
-
const soul = await ctx.runQuery(internal.souls.getSoulBySlugInternal, { slug })
|
|
901
|
-
if (!soul || soul.softDeletedAt) return text('Soul not found', 404, rate.headers)
|
|
902
|
-
|
|
903
|
-
const url = new URL(request.url)
|
|
904
|
-
const limit = toOptionalNumber(url.searchParams.get('limit'))
|
|
905
|
-
const cursor = url.searchParams.get('cursor')?.trim() || undefined
|
|
906
|
-
const result = (await ctx.runQuery(api.souls.listVersionsPage, {
|
|
907
|
-
soulId: soul._id,
|
|
908
|
-
limit,
|
|
909
|
-
cursor,
|
|
910
|
-
})) as ListSoulVersionsResult
|
|
911
|
-
|
|
912
|
-
const items = result.items
|
|
913
|
-
.filter((version) => !version.softDeletedAt)
|
|
914
|
-
.map((version) => ({
|
|
915
|
-
version: version.version,
|
|
916
|
-
createdAt: version.createdAt,
|
|
917
|
-
changelog: version.changelog,
|
|
918
|
-
changelogSource: version.changelogSource ?? null,
|
|
919
|
-
}))
|
|
920
|
-
|
|
921
|
-
return json({ items, nextCursor: result.nextCursor ?? null }, 200, rate.headers)
|
|
922
|
-
}
|
|
923
|
-
|
|
924
|
-
if (second === 'versions' && third && segments.length === 3) {
|
|
925
|
-
const soul = await ctx.runQuery(internal.souls.getSoulBySlugInternal, { slug })
|
|
926
|
-
if (!soul || soul.softDeletedAt) return text('Soul not found', 404, rate.headers)
|
|
927
|
-
|
|
928
|
-
const version = await ctx.runQuery(api.souls.getVersionBySoulAndVersion, {
|
|
929
|
-
soulId: soul._id,
|
|
930
|
-
version: third,
|
|
931
|
-
})
|
|
932
|
-
if (!version) return text('Version not found', 404, rate.headers)
|
|
933
|
-
if (version.softDeletedAt) return text('Version not available', 410, rate.headers)
|
|
934
|
-
|
|
935
|
-
return json(
|
|
936
|
-
{
|
|
937
|
-
soul: { slug: soul.slug, displayName: soul.displayName },
|
|
938
|
-
version: {
|
|
939
|
-
version: version.version,
|
|
940
|
-
createdAt: version.createdAt,
|
|
941
|
-
changelog: version.changelog,
|
|
942
|
-
changelogSource: version.changelogSource ?? null,
|
|
943
|
-
files: version.files.map((file: SoulFile) => ({
|
|
944
|
-
path: file.path,
|
|
945
|
-
size: file.size,
|
|
946
|
-
sha256: file.sha256,
|
|
947
|
-
contentType: file.contentType ?? null,
|
|
948
|
-
})),
|
|
949
|
-
},
|
|
950
|
-
},
|
|
951
|
-
200,
|
|
952
|
-
rate.headers,
|
|
953
|
-
)
|
|
954
|
-
}
|
|
955
|
-
|
|
956
|
-
if (second === 'file' && segments.length === 2) {
|
|
957
|
-
const url = new URL(request.url)
|
|
958
|
-
const path = url.searchParams.get('path')?.trim()
|
|
959
|
-
if (!path) return text('Missing path', 400, rate.headers)
|
|
960
|
-
const versionParam = url.searchParams.get('version')?.trim()
|
|
961
|
-
const tagParam = url.searchParams.get('tag')?.trim()
|
|
962
|
-
|
|
963
|
-
const soulResult = (await ctx.runQuery(api.souls.getBySlug, {
|
|
964
|
-
slug,
|
|
965
|
-
})) as GetSoulBySlugResult
|
|
966
|
-
if (!soulResult?.soul) return text('Soul not found', 404, rate.headers)
|
|
967
|
-
|
|
968
|
-
let version = soulResult.latestVersion
|
|
969
|
-
if (versionParam) {
|
|
970
|
-
version = await ctx.runQuery(api.souls.getVersionBySoulAndVersion, {
|
|
971
|
-
soulId: soulResult.soul._id,
|
|
972
|
-
version: versionParam,
|
|
973
|
-
})
|
|
974
|
-
} else if (tagParam) {
|
|
975
|
-
const versionId = soulResult.soul.tags[tagParam]
|
|
976
|
-
if (versionId) {
|
|
977
|
-
version = await ctx.runQuery(api.souls.getVersionById, { versionId })
|
|
978
|
-
}
|
|
979
|
-
}
|
|
980
|
-
|
|
981
|
-
if (!version) return text('Version not found', 404, rate.headers)
|
|
982
|
-
if (version.softDeletedAt) return text('Version not available', 410, rate.headers)
|
|
983
|
-
|
|
984
|
-
const normalized = path.trim()
|
|
985
|
-
const normalizedLower = normalized.toLowerCase()
|
|
986
|
-
const file =
|
|
987
|
-
version.files.find((entry) => entry.path === normalized) ??
|
|
988
|
-
version.files.find((entry) => entry.path.toLowerCase() === normalizedLower)
|
|
989
|
-
if (!file) return text('File not found', 404, rate.headers)
|
|
990
|
-
if (file.size > MAX_RAW_FILE_BYTES) return text('File exceeds 200KB limit', 413, rate.headers)
|
|
991
|
-
|
|
992
|
-
const blob = await ctx.storage.get(file.storageId)
|
|
993
|
-
if (!blob) return text('File missing in storage', 410, rate.headers)
|
|
994
|
-
const textContent = await blob.text()
|
|
995
|
-
|
|
996
|
-
void ctx.runMutation(api.soulDownloads.increment, { soulId: soulResult.soul._id })
|
|
997
|
-
|
|
998
|
-
const isSvg =
|
|
999
|
-
file.contentType?.toLowerCase().includes('svg') ||
|
|
1000
|
-
file.path.toLowerCase().endsWith('.svg')
|
|
1001
|
-
|
|
1002
|
-
const headers = mergeHeaders(rate.headers, {
|
|
1003
|
-
'Content-Type': file.contentType
|
|
1004
|
-
? `${file.contentType}; charset=utf-8`
|
|
1005
|
-
: 'text/plain; charset=utf-8',
|
|
1006
|
-
'Cache-Control': 'private, max-age=60',
|
|
1007
|
-
ETag: file.sha256,
|
|
1008
|
-
'X-Content-SHA256': file.sha256,
|
|
1009
|
-
'X-Content-Size': String(file.size),
|
|
1010
|
-
'X-Content-Type-Options': 'nosniff',
|
|
1011
|
-
'X-Frame-Options': 'DENY',
|
|
1012
|
-
// For any text response that a browser might try to render, lock it down.
|
|
1013
|
-
// In particular, this prevents SVG <foreignObject> script execution from
|
|
1014
|
-
// reading localStorage tokens on this origin.
|
|
1015
|
-
'Content-Security-Policy': "default-src 'none'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'",
|
|
1016
|
-
...(isSvg ? { 'Content-Disposition': 'attachment' } : {}),
|
|
1017
|
-
})
|
|
1018
|
-
return new Response(textContent, { status: 200, headers })
|
|
1019
|
-
}
|
|
1020
|
-
|
|
1021
|
-
return text('Not found', 404, rate.headers)
|
|
1022
|
-
}
|
|
1023
|
-
|
|
1024
|
-
export const soulsGetRouterV1Http = httpAction(soulsGetRouterV1Handler)
|
|
1025
|
-
|
|
1026
|
-
async function publishSoulV1Handler(ctx: ActionCtx, request: Request) {
|
|
1027
|
-
const rate = await applyRateLimit(ctx, request, 'write')
|
|
1028
|
-
if (!rate.ok) return rate.response
|
|
1029
|
-
|
|
1030
|
-
try {
|
|
1031
|
-
if (!parseBearerToken(request)) return text('Unauthorized', 401, rate.headers)
|
|
1032
|
-
} catch {
|
|
1033
|
-
return text('Unauthorized', 401, rate.headers)
|
|
1034
|
-
}
|
|
1035
|
-
const { userId } = await requireApiTokenUser(ctx, request)
|
|
1036
|
-
|
|
1037
|
-
const contentType = request.headers.get('content-type') ?? ''
|
|
1038
|
-
try {
|
|
1039
|
-
if (contentType.includes('application/json')) {
|
|
1040
|
-
const body = await request.json()
|
|
1041
|
-
const payload = parsePublishBody(body)
|
|
1042
|
-
const result = await publishSoulVersionForUser(ctx, userId, payload)
|
|
1043
|
-
return json({ ok: true, ...result }, 200, rate.headers)
|
|
1044
|
-
}
|
|
1045
|
-
|
|
1046
|
-
if (contentType.includes('multipart/form-data')) {
|
|
1047
|
-
const payload = await parseMultipartPublish(ctx, request)
|
|
1048
|
-
const result = await publishSoulVersionForUser(ctx, userId, payload)
|
|
1049
|
-
return json({ ok: true, ...result }, 200, rate.headers)
|
|
1050
|
-
}
|
|
1051
|
-
} catch (error) {
|
|
1052
|
-
const message = error instanceof Error ? error.message : 'Publish failed'
|
|
1053
|
-
return text(message, 400, rate.headers)
|
|
1054
|
-
}
|
|
1055
|
-
|
|
1056
|
-
return text('Unsupported content type', 415, rate.headers)
|
|
1057
|
-
}
|
|
1058
|
-
|
|
1059
|
-
export const publishSoulV1Http = httpAction(publishSoulV1Handler)
|
|
1060
|
-
|
|
1061
|
-
async function soulsPostRouterV1Handler(ctx: ActionCtx, request: Request) {
|
|
1062
|
-
const rate = await applyRateLimit(ctx, request, 'write')
|
|
1063
|
-
if (!rate.ok) return rate.response
|
|
1064
|
-
|
|
1065
|
-
const segments = getPathSegments(request, '/api/v1/souls/')
|
|
1066
|
-
if (segments.length !== 2 || segments[1] !== 'undelete') {
|
|
1067
|
-
return text('Not found', 404, rate.headers)
|
|
1068
|
-
}
|
|
1069
|
-
const slug = segments[0]?.trim().toLowerCase() ?? ''
|
|
1070
|
-
try {
|
|
1071
|
-
const { userId } = await requireApiTokenUser(ctx, request)
|
|
1072
|
-
await ctx.runMutation(internal.souls.setSoulSoftDeletedInternal, {
|
|
1073
|
-
userId,
|
|
1074
|
-
slug,
|
|
1075
|
-
deleted: false,
|
|
1076
|
-
})
|
|
1077
|
-
return json({ ok: true }, 200, rate.headers)
|
|
1078
|
-
} catch {
|
|
1079
|
-
return text('Unauthorized', 401, rate.headers)
|
|
1080
|
-
}
|
|
1081
|
-
}
|
|
1082
|
-
|
|
1083
|
-
export const soulsPostRouterV1Http = httpAction(soulsPostRouterV1Handler)
|
|
1084
|
-
|
|
1085
|
-
async function soulsDeleteRouterV1Handler(ctx: ActionCtx, request: Request) {
|
|
1086
|
-
const rate = await applyRateLimit(ctx, request, 'write')
|
|
1087
|
-
if (!rate.ok) return rate.response
|
|
1088
|
-
|
|
1089
|
-
const segments = getPathSegments(request, '/api/v1/souls/')
|
|
1090
|
-
if (segments.length !== 1) return text('Not found', 404, rate.headers)
|
|
1091
|
-
const slug = segments[0]?.trim().toLowerCase() ?? ''
|
|
1092
|
-
try {
|
|
1093
|
-
const { userId } = await requireApiTokenUser(ctx, request)
|
|
1094
|
-
await ctx.runMutation(internal.souls.setSoulSoftDeletedInternal, {
|
|
1095
|
-
userId,
|
|
1096
|
-
slug,
|
|
1097
|
-
deleted: true,
|
|
1098
|
-
})
|
|
1099
|
-
return json({ ok: true }, 200, rate.headers)
|
|
1100
|
-
} catch {
|
|
1101
|
-
return text('Unauthorized', 401, rate.headers)
|
|
1102
|
-
}
|
|
1103
|
-
}
|
|
1104
|
-
|
|
1105
|
-
export const soulsDeleteRouterV1Http = httpAction(soulsDeleteRouterV1Handler)
|
|
1106
|
-
|
|
1107
|
-
async function starsPostRouterV1Handler(ctx: ActionCtx, request: Request) {
|
|
1108
|
-
const rate = await applyRateLimit(ctx, request, 'write')
|
|
1109
|
-
if (!rate.ok) return rate.response
|
|
1110
|
-
|
|
1111
|
-
const segments = getPathSegments(request, '/api/v1/stars/')
|
|
1112
|
-
if (segments.length !== 1) return text('Not found', 404, rate.headers)
|
|
1113
|
-
const slug = segments[0]?.trim().toLowerCase() ?? ''
|
|
1114
|
-
|
|
1115
|
-
try {
|
|
1116
|
-
const { userId } = await requireApiTokenUser(ctx, request)
|
|
1117
|
-
const skill = await ctx.runQuery(internal.skills.getSkillBySlugInternal, { slug })
|
|
1118
|
-
if (!skill) return text('Skill not found', 404, rate.headers)
|
|
1119
|
-
|
|
1120
|
-
const result = await ctx.runMutation(internal.stars.addStarInternal, {
|
|
1121
|
-
userId,
|
|
1122
|
-
skillId: skill._id,
|
|
1123
|
-
})
|
|
1124
|
-
return json(result, 200, rate.headers)
|
|
1125
|
-
} catch {
|
|
1126
|
-
return text('Unauthorized', 401, rate.headers)
|
|
1127
|
-
}
|
|
1128
|
-
}
|
|
1129
|
-
|
|
1130
|
-
export const starsPostRouterV1Http = httpAction(starsPostRouterV1Handler)
|
|
1131
|
-
|
|
1132
|
-
async function starsDeleteRouterV1Handler(ctx: ActionCtx, request: Request) {
|
|
1133
|
-
const rate = await applyRateLimit(ctx, request, 'write')
|
|
1134
|
-
if (!rate.ok) return rate.response
|
|
1135
|
-
|
|
1136
|
-
const segments = getPathSegments(request, '/api/v1/stars/')
|
|
1137
|
-
if (segments.length !== 1) return text('Not found', 404, rate.headers)
|
|
1138
|
-
const slug = segments[0]?.trim().toLowerCase() ?? ''
|
|
1139
|
-
|
|
1140
|
-
try {
|
|
1141
|
-
const { userId } = await requireApiTokenUser(ctx, request)
|
|
1142
|
-
const skill = await ctx.runQuery(internal.skills.getSkillBySlugInternal, { slug })
|
|
1143
|
-
if (!skill) return text('Skill not found', 404, rate.headers)
|
|
1144
|
-
|
|
1145
|
-
const result = await ctx.runMutation(internal.stars.removeStarInternal, {
|
|
1146
|
-
userId,
|
|
1147
|
-
skillId: skill._id,
|
|
1148
|
-
})
|
|
1149
|
-
return json(result, 200, rate.headers)
|
|
1150
|
-
} catch {
|
|
1151
|
-
return text('Unauthorized', 401, rate.headers)
|
|
1152
|
-
}
|
|
1153
|
-
}
|
|
1154
|
-
|
|
1155
|
-
export const starsDeleteRouterV1Http = httpAction(starsDeleteRouterV1Handler)
|
|
1156
|
-
export const __handlers = {
|
|
1157
|
-
searchSkillsV1Handler,
|
|
1158
|
-
resolveSkillVersionV1Handler,
|
|
1159
|
-
listSkillsV1Handler,
|
|
1160
|
-
skillsGetRouterV1Handler,
|
|
1161
|
-
publishSkillV1Handler,
|
|
1162
|
-
skillsPostRouterV1Handler,
|
|
1163
|
-
skillsDeleteRouterV1Handler,
|
|
1164
|
-
listSoulsV1Handler,
|
|
1165
|
-
soulsGetRouterV1Handler,
|
|
1166
|
-
publishSoulV1Handler,
|
|
1167
|
-
soulsPostRouterV1Handler,
|
|
1168
|
-
soulsDeleteRouterV1Handler,
|
|
1169
|
-
starsPostRouterV1Handler,
|
|
1170
|
-
starsDeleteRouterV1Handler,
|
|
1171
|
-
whoamiV1Handler,
|
|
1172
|
-
}
|