├── .eslintrc.json ├── .gitignore ├── LICENSE ├── README.md ├── app ├── about │ └── page.tsx ├── account │ ├── account-sidebar.tsx │ ├── api-key-item.tsx │ ├── api-keys.tsx │ ├── connection-item.tsx │ ├── connections.tsx │ ├── new-api-key.tsx │ ├── packages.tsx │ ├── page.tsx │ └── section.tsx ├── api │ ├── ai-plugins │ │ ├── lookup │ │ │ └── route.ts │ │ └── search │ │ │ └── route.ts │ ├── api-keys │ │ ├── [apiKeyId] │ │ │ └── route.ts │ │ ├── route.ts │ │ └── types.ts │ ├── connections │ │ ├── [connectionId] │ │ │ └── route.ts │ │ └── route.ts │ ├── export │ │ └── packages │ │ │ ├── export-packages.ts │ │ │ └── route.ts │ ├── opensearch │ │ ├── route.ts │ │ └── suggest │ │ │ └── route.ts │ ├── packages │ │ ├── [packageId] │ │ │ ├── ai-plugin │ │ │ │ └── route.ts │ │ │ ├── delete-package.ts │ │ │ ├── examples │ │ │ │ └── route.ts │ │ │ ├── get-package.ts │ │ │ ├── openapi │ │ │ │ └── route.ts │ │ │ ├── route.ts │ │ │ └── update-package.ts │ │ ├── connected │ │ │ └── route.ts │ │ ├── create-package.ts │ │ ├── list-packages.ts │ │ ├── lookup │ │ │ └── route.ts │ │ ├── paginated-search │ │ │ └── route.ts │ │ ├── route.ts │ │ └── search │ │ │ └── route.ts │ ├── proxy │ │ └── [packageId] │ │ │ └── [...path] │ │ │ └── route.ts │ └── whoami │ │ └── route.ts ├── auth │ ├── complete │ │ └── page.tsx │ └── page.tsx ├── connect │ └── oauth-callback │ │ └── page.tsx ├── docs │ └── page.tsx ├── export │ └── page.tsx ├── favicon.ico ├── favicon.svg ├── globals.css ├── layout.tsx ├── new │ └── page.tsx ├── openapi │ └── page.tsx ├── packages │ ├── [packageId] │ │ ├── connect │ │ │ └── page.tsx │ │ ├── description │ │ │ └── page.tsx │ │ ├── edit │ │ │ └── page.tsx │ │ ├── endpoint │ │ │ └── page.tsx │ │ ├── openapi │ │ │ └── page.tsx │ │ ├── page.tsx │ │ └── versions │ │ │ └── [packageVersion] │ │ │ ├── endpoint │ │ │ └── page.tsx │ │ │ └── page.tsx │ ├── header.tsx │ ├── package-item.tsx │ ├── page.tsx │ ├── pagination.tsx │ ├── search-input.tsx │ └── types.tsx ├── page.tsx ├── privacy │ └── page.tsx ├── sign-out │ └── route.ts └── terms │ └── page.tsx ├── components ├── account-auth.tsx ├── account-header │ ├── account-header.tsx │ ├── index.ts │ ├── menu-items.tsx │ ├── menu.tsx │ └── theme-toggle.tsx ├── account-profile.tsx ├── buttons │ └── default-button.tsx ├── code.tsx ├── connect-package │ └── connect-package-form.tsx ├── edit-package │ └── edit-package-form.tsx ├── error-boundary.tsx ├── icons │ └── logo-github.tsx ├── json-viewer.tsx ├── main-template.tsx ├── markdown │ ├── markdown-dynamic.tsx │ └── markdown.tsx ├── modal.tsx ├── new-package │ └── new-package-form.tsx ├── search-input.tsx ├── select │ ├── index.ts │ ├── option.tsx │ └── select.tsx ├── show-package │ ├── document-authentication.tsx │ ├── document-endpoint-request-example-tabs.tsx │ ├── document-endpoint-request-example.tsx │ ├── document-endpoint-response-example.tsx │ ├── document-endpoint.tsx │ ├── document-endpoints.tsx │ ├── document-schema.tsx │ ├── document-security-schema.tsx │ ├── package-description.tsx │ ├── package-header.tsx │ ├── package-info.tsx │ ├── package-main.tsx │ ├── package-sidebar.tsx │ └── package-versions.tsx ├── tab.tsx ├── text-input.tsx └── textarea-input.tsx ├── helpers ├── ai-plugin │ ├── helpers.ts │ └── types.ts ├── api │ └── package-response.ts ├── openapi │ ├── document.test.ts │ ├── document.ts │ ├── endpoint.ts │ ├── fixtures │ │ ├── clearbit.json │ │ ├── petstore.json │ │ └── reflect.json │ ├── index.ts │ ├── proxy.ts │ ├── request-example.test.ts │ ├── request-example.ts │ ├── response-example.test.ts │ ├── response-example.ts │ ├── response.ts │ ├── schema.ts │ ├── security-scheme.ts │ ├── types.ts │ ├── utils.test.ts │ └── utils.ts └── proxy │ ├── oauth.ts │ └── proxy-request.ts ├── lib ├── assert.ts ├── code-highlighter.ts ├── description.ts ├── format-distance.ts ├── generate-id.ts ├── json-fetch.ts ├── lodash-memoize.ts ├── not-empty.ts ├── response-json.ts ├── shiki │ ├── languages │ │ ├── abap.tmLanguage.json │ │ ├── actionscript-3.tmLanguage.json │ │ ├── ada.tmLanguage.json │ │ ├── apache.tmLanguage.json │ │ ├── apex.tmLanguage.json │ │ ├── apl.tmLanguage.json │ │ ├── applescript.tmLanguage.json │ │ ├── ara.tmLanguage.json │ │ ├── asm.tmLanguage.json │ │ ├── astro.tmLanguage.json │ │ ├── awk.tmLanguage.json │ │ ├── ballerina.tmLanguage.json │ │ ├── bat.tmLanguage.json │ │ ├── berry.tmLanguage.json │ │ ├── bibtex.tmLanguage.json │ │ ├── bicep.tmLanguage.json │ │ ├── blade.tmLanguage.json │ │ ├── c.tmLanguage.json │ │ ├── cadence.tmLanguage.json │ │ ├── clarity.tmLanguage.json │ │ ├── clojure.tmLanguage.json │ │ ├── cmake.tmLanguage.json │ │ ├── cobol.tmLanguage.json │ │ ├── codeql.tmLanguage.json │ │ ├── coffee.tmLanguage.json │ │ ├── cpp-macro.tmLanguage.json │ │ ├── cpp.tmLanguage.json │ │ ├── crystal.tmLanguage.json │ │ ├── csharp.tmLanguage.json │ │ ├── css.tmLanguage.json │ │ ├── cue.tmLanguage.json │ │ ├── d.tmLanguage.json │ │ ├── dart.tmLanguage.json │ │ ├── dax.tmLanguage.json │ │ ├── diff.tmLanguage.json │ │ ├── docker.tmLanguage.json │ │ ├── dream-maker.tmLanguage.json │ │ ├── elixir.tmLanguage.json │ │ ├── elm.tmLanguage.json │ │ ├── erb.tmLanguage.json │ │ ├── erlang.tmLanguage.json │ │ ├── fish.tmLanguage.json │ │ ├── fsharp.tmLanguage.json │ │ ├── gdresource.tmLanguage.json │ │ ├── gdscript.tmLanguage.json │ │ ├── gdshader.tmLanguage.json │ │ ├── gherkin.tmLanguage.json │ │ ├── git-commit.tmLanguage.json │ │ ├── git-rebase.tmLanguage.json │ │ ├── glsl.tmLanguage.json │ │ ├── gnuplot.tmLanguage.json │ │ ├── go.tmLanguage.json │ │ ├── graphql.tmLanguage.json │ │ ├── groovy.tmLanguage.json │ │ ├── hack.tmLanguage.json │ │ ├── haml.tmLanguage.json │ │ ├── handlebars.tmLanguage.json │ │ ├── haskell.tmLanguage.json │ │ ├── hcl.tmLanguage.json │ │ ├── hlsl.tmLanguage.json │ │ ├── html.tmLanguage.json │ │ ├── http.tmLanguage.json │ │ ├── imba.tmLanguage.json │ │ ├── ini.tmLanguage.json │ │ ├── java.tmLanguage.json │ │ ├── javascript.tmLanguage.json │ │ ├── jinja-html.tmLanguage.json │ │ ├── jinja.tmLanguage.json │ │ ├── jison.tmLanguage.json │ │ ├── json.tmLanguage.json │ │ ├── json5.tmLanguage.json │ │ ├── jsonc.tmLanguage.json │ │ ├── jsonnet.tmLanguage.json │ │ ├── jssm.tmLanguage.json │ │ ├── jsx.tmLanguage.json │ │ ├── julia.tmLanguage.json │ │ ├── kotlin.tmLanguage.json │ │ ├── kusto.tmLanguage.json │ │ ├── latex.tmLanguage.json │ │ ├── less.tmLanguage.json │ │ ├── liquid.tmLanguage.json │ │ ├── lisp.tmLanguage.json │ │ ├── logo.tmLanguage.json │ │ ├── lua.tmLanguage.json │ │ ├── make.tmLanguage.json │ │ ├── markdown.tmLanguage.json │ │ ├── marko.tmLanguage.json │ │ ├── matlab.tmLanguage.json │ │ ├── mdx.tmLanguage.json │ │ ├── mermaid.tmLanguage.json │ │ ├── nginx.tmLanguage.json │ │ ├── nim.tmLanguage.json │ │ ├── nix.tmLanguage.json │ │ ├── objective-c.tmLanguage.json │ │ ├── objective-cpp.tmLanguage.json │ │ ├── ocaml.tmLanguage.json │ │ ├── pascal.tmLanguage.json │ │ ├── perl.tmLanguage.json │ │ ├── php-html.tmLanguage.json │ │ ├── php.tmLanguage.json │ │ ├── plsql.tmLanguage.json │ │ ├── postcss.tmLanguage.json │ │ ├── powerquery.tmLanguage.json │ │ ├── powershell.tmLanguage.json │ │ ├── prisma.tmLanguage.json │ │ ├── prolog.tmLanguage.json │ │ ├── proto.tmLanguage.json │ │ ├── pug.tmLanguage.json │ │ ├── puppet.tmLanguage.json │ │ ├── purescript.tmLanguage.json │ │ ├── python.tmLanguage.json │ │ ├── r.tmLanguage.json │ │ ├── raku.tmLanguage.json │ │ ├── razor.tmLanguage.json │ │ ├── reg.tmLanguage.json │ │ ├── rel.tmLanguage.json │ │ ├── riscv.tmLanguage.json │ │ ├── rst.tmLanguage.json │ │ ├── ruby.tmLanguage.json │ │ ├── rust.tmLanguage.json │ │ ├── sas.tmLanguage.json │ │ ├── sass.tmLanguage.json │ │ ├── scala.tmLanguage.json │ │ ├── scheme.tmLanguage.json │ │ ├── scss.tmLanguage.json │ │ ├── shaderlab.tmLanguage.json │ │ ├── shellscript.tmLanguage.json │ │ ├── smalltalk.tmLanguage.json │ │ ├── solidity.tmLanguage.json │ │ ├── sparql.tmLanguage.json │ │ ├── sql.tmLanguage.json │ │ ├── ssh-config.tmLanguage.json │ │ ├── stata.tmLanguage.json │ │ ├── stylus.tmLanguage.json │ │ ├── svelte.tmLanguage.json │ │ ├── swift.tmLanguage.json │ │ ├── system-verilog.tmLanguage.json │ │ ├── tasl.tmLanguage.json │ │ ├── tcl.tmLanguage.json │ │ ├── tex.tmLanguage.json │ │ ├── toml.tmLanguage.json │ │ ├── tsx.tmLanguage.json │ │ ├── turtle.tmLanguage.json │ │ ├── twig.tmLanguage.json │ │ ├── typescript.tmLanguage.json │ │ ├── v.tmLanguage.json │ │ ├── vb.tmLanguage.json │ │ ├── verilog.tmLanguage.json │ │ ├── vhdl.tmLanguage.json │ │ ├── viml.tmLanguage.json │ │ ├── vue-html.tmLanguage.json │ │ ├── vue.tmLanguage.json │ │ ├── wasm.tmLanguage.json │ │ ├── wenyan.tmLanguage.json │ │ ├── wgsl.tmLanguage.json │ │ ├── xml.tmLanguage.json │ │ ├── xsl.tmLanguage.json │ │ ├── yaml.tmLanguage.json │ │ └── zenscript.tmLanguage.json │ └── themes │ │ ├── css-variables.json │ │ ├── dark-plus.json │ │ ├── dracula-soft.json │ │ ├── dracula.json │ │ ├── github-dark-dimmed.json │ │ ├── github-dark.json │ │ ├── github-light.json │ │ ├── hc_light.json │ │ ├── light-plus.json │ │ ├── material-theme-darker.json │ │ ├── material-theme-lighter.json │ │ ├── material-theme-ocean.json │ │ ├── material-theme-palenight.json │ │ ├── material-theme.json │ │ ├── min-dark.json │ │ ├── min-light.json │ │ ├── monokai.json │ │ ├── nord.json │ │ ├── one-dark-pro.json │ │ ├── poimandres.json │ │ ├── rose-pine-dawn.json │ │ ├── rose-pine-moon.json │ │ ├── rose-pine.json │ │ ├── slack-dark.json │ │ ├── slack-ochin.json │ │ ├── solarized-dark.json │ │ ├── solarized-light.json │ │ ├── vitesse-dark.json │ │ └── vitesse-light.json ├── use-debounced-state.ts ├── use-keyboard-shortcut.ts ├── use-local-storage-state.ts ├── use-match-media-state.ts ├── use-message.ts └── use-theme.tsx ├── next.config.js ├── openapi.json ├── package.json ├── pnpm-lock.yaml ├── postcss.config.js ├── prettier.config.js ├── public ├── background-gradient.jpg └── theme.js ├── schema.sql ├── scripts ├── proxy-request.ts └── validate-documents.ts ├── server ├── db │ ├── api-keys │ │ ├── getters.ts │ │ ├── setters.ts │ │ └── types.ts │ ├── db.ts │ ├── packages │ │ ├── getters.ts │ │ ├── setters.ts │ │ └── types.ts │ ├── types.ts │ ├── user-connections │ │ ├── getters.ts │ │ ├── setters.ts │ │ └── types.ts │ └── users │ │ ├── getters.ts │ │ └── setters.ts └── helpers │ ├── api-builder │ ├── api-builder.ts │ ├── index.ts │ └── types.ts │ ├── auth │ ├── index.ts │ ├── session.ts │ └── token.ts │ ├── error.ts │ └── params.ts ├── tailwind.config.js ├── tests └── vitest-global-setup.mjs ├── tsconfig.json └── vite.config.ts /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals", 3 | "plugins": ["@typescript-eslint"], 4 | "rules": { 5 | "import/order": [ 6 | "warn", 7 | { 8 | "newlines-between": "always", 9 | "alphabetize": {"order": "asc"}, 10 | "groups": [ 11 | "builtin", 12 | "external", 13 | "internal", 14 | ["parent", "sibling"], 15 | "index", 16 | "type" 17 | ] 18 | } 19 | ], 20 | 21 | "no-unused-vars": "off", 22 | 23 | "@typescript-eslint/no-unused-vars": [ 24 | "error", 25 | { 26 | "argsIgnorePattern": "^_", 27 | "varsIgnorePattern": "^_", 28 | "caughtErrorsIgnorePattern": "^_" 29 | } 30 | ] 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | .pnpm-debug.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | 38 | .vscode 39 | TODO.md 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Alex MacCaw 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /app/about/page.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link' 2 | 3 | import {MainTemplate} from '@/components/main-template' 4 | 5 | export default async function AboutPage() { 6 | return ( 7 | 8 |
9 |

Our purpose

10 | 11 |

12 | AI is fundamentally changing the way we live, work, and build software. It has 13 | the potential to be the biggest platform shift since the iphone and mobile. 14 |

15 | 16 |

17 | With mobile we learned the painful lesson of the Apple app-store, controlled by 18 | a single monopolistic company, stifling innovation and entrepreneurship. 19 |

20 | 21 |

22 | AI, our new platform, needs it's own app-store. An unrestricted app-store 23 | built upon the open web and the OpenAPI specification. 24 |

25 | 26 |

27 | We engineers currently have a slim chance of creating this app-store layer 28 | before some large corporation does it. We must seize this chance. 29 |

30 | 31 |

32 | That's why we're building{' '} 33 | 34 | openpm.ai 35 | 36 | , an open source package-manager for OpenAPI files. AIs can use consume packages 37 | from openpm in a similar fashion to how ChatGPT plugins work. Ultimately, AIs 38 | can use openpm to discover and interact with the world via APIs. 39 |

40 | 41 |

42 | Everything we release is under the MIT license. We will never charge a 43 | transaction fee for our services. We will never wield editorial control. We will 44 | only remove packages that are scams or illegal under US law. At any point you 45 | can choose to{' '} 46 | 47 | export all of our packages 48 | {' '} 49 | and run them on your own server. 50 |

51 |
52 |
53 | ) 54 | } 55 | -------------------------------------------------------------------------------- /app/account/account-sidebar.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link' 2 | import React from 'react' 3 | 4 | export function AccountSidebar() { 5 | return ( 6 | 33 | ) 34 | } 35 | 36 | const MenuItem = ({children, href}: {children: React.ReactNode; href: string}) => ( 37 | 45 | {children} 46 | 47 | ) 48 | -------------------------------------------------------------------------------- /app/account/api-key-item.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import {useRouter} from 'next/navigation' 4 | 5 | import {jsonFetch} from '@/lib/json-fetch' 6 | import {RedactedApiKey} from '@/server/db/api-keys/types' 7 | 8 | export function ApiKeyItem({apiKey}: {apiKey: RedactedApiKey}) { 9 | const router = useRouter() 10 | 11 | const onRevoke = async () => { 12 | if (!confirm('Are you sure you want to revoke this API key?')) { 13 | return 14 | } 15 | 16 | const {error} = await jsonFetch(`/api/api-keys/${apiKey.id}`, { 17 | method: 'DELETE', 18 | }) 19 | 20 | if (error) { 21 | alert(error.message) 22 | throw new Error(error.message ?? 'Unknown error') 23 | } 24 | 25 | router.refresh() 26 | } 27 | 28 | return ( 29 |
  • 30 | 31 | sk-...{apiKey.keyExcerpt} 32 | 33 | 34 | 41 | 42 |
  • 43 | ) 44 | } 45 | -------------------------------------------------------------------------------- /app/account/api-keys.tsx: -------------------------------------------------------------------------------- 1 | 'use server' 2 | 3 | import {getUnrevokedApiKeys} from '@/server/db/api-keys/getters' 4 | 5 | import {ApiKeyItem} from './api-key-item' 6 | import {NewApiKey} from './new-api-key' 7 | 8 | export async function ApiKeys({userId}: {userId: string}) { 9 | const apiKeys = await getUnrevokedApiKeys({userId}) 10 | 11 | return ( 12 |
    13 | 18 | 19 | 22 |
    23 | ) 24 | } 25 | -------------------------------------------------------------------------------- /app/account/connection-item.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import Link from 'next/link' 4 | import {useRouter} from 'next/navigation' 5 | 6 | import {jsonFetch} from '@/lib/json-fetch' 7 | import {UserConnection} from '@/server/db/user-connections/types' 8 | 9 | export function ConnectionItem({connection}: {connection: UserConnection}) { 10 | const router = useRouter() 11 | 12 | const onRevoke = async () => { 13 | if ( 14 | !confirm( 15 | `Are you sure you want to revoke this connection to ${connection.package_id}?`, 16 | ) 17 | ) { 18 | return 19 | } 20 | 21 | const {error} = await jsonFetch(`/api/connections/${connection.id}`, { 22 | method: 'DELETE', 23 | }) 24 | 25 | if (error) { 26 | alert(error.message) 27 | throw new Error(error.message ?? 'Unknown error') 28 | } 29 | 30 | router.refresh() 31 | } 32 | 33 | return ( 34 |
  • 35 | 40 | {connection.package_id} 41 | 42 | 43 | 44 | 51 | 52 |
  • 53 | ) 54 | } 55 | -------------------------------------------------------------------------------- /app/account/connections.tsx: -------------------------------------------------------------------------------- 1 | 'use server' 2 | 3 | import {getConnectionsForUser} from '@/server/db/user-connections/getters' 4 | 5 | import {ConnectionItem} from './connection-item' 6 | 7 | export async function Connections({userId}: {userId: string}) { 8 | const connections = await getConnectionsForUser(userId) 9 | 10 | return ( 11 |
    12 | 17 | 18 | {connections.length === 0 && ( 19 |

    You don't have any connections yet.

    20 | )} 21 |
    22 | ) 23 | } 24 | -------------------------------------------------------------------------------- /app/account/new-api-key.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import {useRouter} from 'next/navigation' 4 | import React, {useState} from 'react' 5 | 6 | import {Modal} from '@/components/modal' 7 | 8 | import {CreateApiKeyResponse} from '../api/api-keys/types' 9 | 10 | export function NewApiKey() { 11 | const router = useRouter() 12 | const [apiKey, setApiKey] = useState(null) 13 | 14 | const onCreateApiKey = async () => { 15 | const result = await fetch('/api/api-keys', { 16 | method: 'POST', 17 | }) 18 | 19 | if (!result.ok) { 20 | alert('Failed to create API key') 21 | throw new Error('Failed to create API key') 22 | } 23 | 24 | const newApiKey = await result.json() 25 | setApiKey(newApiKey) 26 | 27 | router.refresh() 28 | } 29 | 30 | return ( 31 |
    32 | 35 | 36 | {apiKey && setApiKey(null)} />} 37 |
    38 | ) 39 | } 40 | 41 | function NewApiKeyModal({apiKey, onClose}: {apiKey: string; onClose: () => void}) { 42 | return ( 43 | 44 | {apiKey && ( 45 |
    46 |

    47 | Here is your new API key. Be sure to copy it. This will be the last time that 48 | it is shown. 49 |

    50 | 51 |
    52 |             {apiKey}
    53 |           
    54 | 55 |
    56 | 63 |
    64 |
    65 | )} 66 |
    67 | ) 68 | } 69 | -------------------------------------------------------------------------------- /app/account/packages.tsx: -------------------------------------------------------------------------------- 1 | 'use server' 2 | 3 | import Link from 'next/link' 4 | 5 | import {getAllPackagesForUserId} from '@/server/db/packages/getters' 6 | 7 | export async function Packages({userId}: {userId: string}) { 8 | const packages = await getAllPackagesForUserId(userId) 9 | 10 | return ( 11 |
    12 | 42 | 43 |
    44 | 45 | New package... 46 | 47 |
    48 |
    49 | ) 50 | } 51 | -------------------------------------------------------------------------------- /app/account/page.tsx: -------------------------------------------------------------------------------- 1 | import dynamic from 'next/dynamic' 2 | 3 | import {AccountHeader} from '@/components/account-header' 4 | import {authOrRedirect} from '@/server/helpers/auth' 5 | 6 | import {AccountSidebar} from './account-sidebar' 7 | import {ApiKeys} from './api-keys' 8 | import {Connections} from './connections' 9 | import {Packages} from './packages' 10 | import {Section} from './section' 11 | 12 | const AccountProfile = dynamic(() => import('@/components/account-profile'), { 13 | ssr: false, 14 | }) 15 | 16 | export default async function AccountPage() { 17 | const userId = await authOrRedirect() 18 | 19 | return ( 20 |
    21 |
    22 | 23 |
    24 | 25 |
    26 | 27 | 28 |
    29 |

    Account

    30 | 31 |
    32 |
    33 | 34 |
    35 | 36 |
    37 | 38 |
    39 | 40 |
    41 | 42 |
    43 | 44 |
    45 | 46 |
    47 |
    48 |
    49 |
    50 |
    51 | ) 52 | } 53 | -------------------------------------------------------------------------------- /app/account/section.tsx: -------------------------------------------------------------------------------- 1 | import {ReactNode, Suspense} from 'react' 2 | 3 | export function Section({title, children}: {title: string; children: ReactNode}) { 4 | return ( 5 |
    6 |

    {title}

    7 | 8 | }>{children} 9 |
    10 | ) 11 | } 12 | 13 | function Fallback() { 14 | return ( 15 |
    16 |
    17 |
    18 |
    19 |
    20 | ) 21 | } 22 | 23 | function dasherize(str: string) { 24 | return str.replace(/\s+/g, '-').toLowerCase() 25 | } 26 | -------------------------------------------------------------------------------- /app/api/ai-plugins/lookup/route.ts: -------------------------------------------------------------------------------- 1 | import {NextResponse} from 'next/server' 2 | import {z} from 'zod' 3 | 4 | import {packageToAiPlugin} from '@/helpers/ai-plugin/helpers' 5 | import {getPackagesWithIds} from '@/server/db/packages/getters' 6 | import {withApiBuilder} from '@/server/helpers/api-builder' 7 | import {error} from '@/server/helpers/error' 8 | 9 | const ApiSchema = z.object({ 10 | ids: z.string(), 11 | }) 12 | 13 | type ApiRequestParams = z.infer 14 | 15 | export const GET = withApiBuilder( 16 | ApiSchema, 17 | async (request: Request, {data}) => { 18 | const {ids: idsStr} = data 19 | const ids = idsStr.split(',') 20 | 21 | if (ids.length > 1000) { 22 | return error('Too many ids') 23 | } 24 | 25 | if (ids.length === 0) { 26 | return error('No ids') 27 | } 28 | 29 | const packages = await getPackagesWithIds(ids) 30 | const plugins = packages.map((pkg) => packageToAiPlugin(pkg)) 31 | 32 | return NextResponse.json(plugins, { 33 | headers: { 34 | 'Cache-Control': 's-maxage=30, stale-while-revalidate=59', 35 | }, 36 | }) 37 | }, 38 | ) 39 | -------------------------------------------------------------------------------- /app/api/ai-plugins/search/route.ts: -------------------------------------------------------------------------------- 1 | import {NextResponse} from 'next/server' 2 | import {z} from 'zod' 3 | 4 | import {packageToAiPlugin} from '@/helpers/ai-plugin/helpers' 5 | import {searchPackages} from '@/server/db/packages/getters' 6 | import {withApiBuilder} from '@/server/helpers/api-builder' 7 | 8 | const ApiSchema = z.object({ 9 | query: z.string(), 10 | limit: z.coerce.number().min(1).max(100).default(10), 11 | }) 12 | 13 | type ApiRequestParams = z.infer 14 | 15 | export const GET = withApiBuilder( 16 | ApiSchema, 17 | async (request: Request, {data}) => { 18 | const {query, limit} = data 19 | 20 | const packages = await searchPackages({query, limit}) 21 | const plugins = packages.map((pkg) => packageToAiPlugin(pkg)) 22 | 23 | return NextResponse.json(plugins, { 24 | headers: { 25 | 'Cache-Control': 's-maxage=30, stale-while-revalidate=59', 26 | }, 27 | }) 28 | }, 29 | ) 30 | -------------------------------------------------------------------------------- /app/api/api-keys/[apiKeyId]/route.ts: -------------------------------------------------------------------------------- 1 | import {NextResponse} from 'next/server' 2 | 3 | import {assertString} from '@/lib/assert' 4 | import {getApiKey} from '@/server/db/api-keys/getters' 5 | import {revokeApiKey} from '@/server/db/api-keys/setters' 6 | import {withAuth} from '@/server/helpers/auth' 7 | 8 | export const DELETE = withAuth( 9 | async ( 10 | req: Request, 11 | {userId, params}: {userId: string; params: {apiKeyId: string}}, 12 | ) => { 13 | const {apiKeyId} = params 14 | 15 | assertString(userId, 'Invalid user') 16 | assertString(apiKeyId, 'Invalid API key ID') 17 | 18 | await revokeApiKey({userId, apiKeyId}) 19 | 20 | const apiKey = await getApiKey({userId, apiKeyId}) 21 | 22 | if (req.headers.get('Accept') === 'application/json') { 23 | return NextResponse.json({ 24 | id: apiKey.id, 25 | createdAt: apiKey.createdAt, 26 | revokedAt: apiKey.revokedAt, 27 | }) 28 | } else { 29 | return NextResponse.redirect(new URL(`/account`, req.url)) 30 | } 31 | }, 32 | ) 33 | -------------------------------------------------------------------------------- /app/api/api-keys/route.ts: -------------------------------------------------------------------------------- 1 | import {NextResponse} from 'next/server' 2 | 3 | import {assertString} from '@/lib/assert' 4 | import {createApiKey} from '@/server/db/api-keys/setters' 5 | import {withAuth} from '@/server/helpers/auth' 6 | 7 | export const POST = withAuth(async (req: Request, {userId}: {userId: string}) => { 8 | assertString(userId, 'Invalid user') 9 | 10 | const {id, key} = await createApiKey({userId}) 11 | 12 | return NextResponse.json({ 13 | id, 14 | key, 15 | }) 16 | }) 17 | -------------------------------------------------------------------------------- /app/api/api-keys/types.ts: -------------------------------------------------------------------------------- 1 | export interface CreateApiKeyResponse { 2 | id: string 3 | key: string 4 | } 5 | -------------------------------------------------------------------------------- /app/api/connections/[connectionId]/route.ts: -------------------------------------------------------------------------------- 1 | import {NextResponse} from 'next/server' 2 | 3 | import {assertString} from '@/lib/assert' 4 | import {deleteUserConnection} from '@/server/db/user-connections/setters' 5 | import {withAuth} from '@/server/helpers/auth' 6 | 7 | export const DELETE = withAuth( 8 | async ( 9 | req: Request, 10 | {userId, params}: {userId: string; params: {connectionId: string}}, 11 | ) => { 12 | const {connectionId} = params 13 | 14 | assertString(userId, 'Invalid user') 15 | assertString(connectionId, 'Invalid connection id') 16 | 17 | await deleteUserConnection({userId, connectionId}) 18 | 19 | return NextResponse.json({success: true}) 20 | }, 21 | ) 22 | -------------------------------------------------------------------------------- /app/api/connections/route.ts: -------------------------------------------------------------------------------- 1 | import {NextResponse} from 'next/server' 2 | import {z} from 'zod' 3 | 4 | import {parseJsonSpec} from '@/helpers/openapi' 5 | import {getFullPackageById} from '@/server/db/packages/getters' 6 | import {createUserConnection} from '@/server/db/user-connections/setters' 7 | import {withApiBuilder} from '@/server/helpers/api-builder' 8 | import {withAuth} from '@/server/helpers/auth' 9 | import {error} from '@/server/helpers/error' 10 | 11 | const ApiSchema = z.object({ 12 | package_id: z.string(), 13 | api_key: z.string().nullable(), 14 | }) 15 | 16 | type ApiRequestParams = z.infer 17 | 18 | // Connects a user to a package 19 | 20 | export const POST = withAuth( 21 | withApiBuilder( 22 | ApiSchema, 23 | async (req: Request, {userId, data}) => { 24 | const packageRow = await getFullPackageById(data.package_id) 25 | 26 | if (!packageRow) { 27 | return error('Package does not exist') 28 | } 29 | 30 | const _doc = await parseJsonSpec(packageRow.openapi) 31 | 32 | // Todo - validate the spec against 33 | // the auth provided 34 | 35 | const connection = await createUserConnection({ 36 | packageId: data.package_id, 37 | userId: userId, 38 | apiKey: data.api_key, 39 | }) 40 | 41 | return NextResponse.json({ 42 | id: connection.id, 43 | }) 44 | }, 45 | ), 46 | ) 47 | -------------------------------------------------------------------------------- /app/api/export/packages/export-packages.ts: -------------------------------------------------------------------------------- 1 | import {NextResponse} from 'next/server' 2 | 3 | import {getAllPackages} from '@/server/db/packages/getters' 4 | 5 | async function exportPackages() { 6 | const packages = await getAllPackages() 7 | 8 | // Export packages as JSON 9 | 10 | return NextResponse.json( 11 | packages.map((pkg) => ({ 12 | id: pkg.id, 13 | domain: pkg.domain, 14 | openapi: pkg.openapi, 15 | })), 16 | ) 17 | } 18 | 19 | export default exportPackages 20 | -------------------------------------------------------------------------------- /app/api/export/packages/route.ts: -------------------------------------------------------------------------------- 1 | import exportPackages from './export-packages' 2 | 3 | export {exportPackages as GET} 4 | -------------------------------------------------------------------------------- /app/api/opensearch/route.ts: -------------------------------------------------------------------------------- 1 | export function GET() { 2 | return new Response( 3 | ` 4 | 5 | openpm 6 | Search openpm 7 | UTF-8 8 | UTF-8 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | `, 17 | { 18 | headers: { 19 | 'Content-Type': 'application/opensearchdescription+xml', 20 | }, 21 | }, 22 | ) 23 | } 24 | -------------------------------------------------------------------------------- /app/api/opensearch/suggest/route.ts: -------------------------------------------------------------------------------- 1 | import {searchPackages} from '@/server/db/packages/getters' 2 | import {getParams} from '@/server/helpers/params' 3 | 4 | export async function GET(req: Request) { 5 | const params = getParams(req) 6 | const query = params.get('q') 7 | 8 | const packages = query ? await searchPackages({query, limit: 5}) : [] 9 | 10 | return new Response(JSON.stringify(packages.map((pkg) => pkg.id)), { 11 | headers: {'Content-Type': 'application/x-suggestions+json'}, 12 | }) 13 | } 14 | -------------------------------------------------------------------------------- /app/api/packages/[packageId]/ai-plugin/route.ts: -------------------------------------------------------------------------------- 1 | import {NextResponse} from 'next/server' 2 | 3 | import {packageToAiPlugin} from '@/helpers/ai-plugin/helpers' 4 | import {getFullPackageById} from '@/server/db/packages/getters' 5 | import {error} from '@/server/helpers/error' 6 | 7 | // Retrive a package manifest 8 | export async function GET(req: Request, {params}: {params: {packageId: string}}) { 9 | const pkg = await getFullPackageById(params.packageId) 10 | 11 | if (!pkg) { 12 | return error('Package not found', 'not_found', 404) 13 | } 14 | 15 | return NextResponse.json(packageToAiPlugin(pkg)) 16 | } 17 | -------------------------------------------------------------------------------- /app/api/packages/[packageId]/delete-package.ts: -------------------------------------------------------------------------------- 1 | import {NextResponse} from 'next/server' 2 | 3 | import {getFullPackageById} from '@/server/db/packages/getters' 4 | import {deletePackage} from '@/server/db/packages/setters' 5 | import {withAuth} from '@/server/helpers/auth' 6 | import {error} from '@/server/helpers/error' 7 | 8 | // Retrive a package 9 | 10 | const endpoint = withAuth( 11 | async ( 12 | req: Request, 13 | {params, userId}: {params: {packageId: string}; userId: string}, 14 | ) => { 15 | const packageRow = await getFullPackageById(params.packageId) 16 | 17 | if (!packageRow) { 18 | return error('Package does not exist') 19 | } 20 | 21 | if (!packageRow.acl_write.includes(userId)) { 22 | return error('Unauthorized', 'unauthorized', 403) 23 | } 24 | 25 | await deletePackage(packageRow.id) 26 | 27 | return NextResponse.json({success: true}) 28 | }, 29 | ) 30 | 31 | export default endpoint 32 | -------------------------------------------------------------------------------- /app/api/packages/[packageId]/examples/route.ts: -------------------------------------------------------------------------------- 1 | import {NextResponse} from 'next/server' 2 | 3 | import {parseJsonSpec} from '@/helpers/openapi' 4 | import {highlight} from '@/lib/code-highlighter' 5 | import {getFullPackageById} from '@/server/db/packages/getters' 6 | import {error} from '@/server/helpers/error' 7 | import {getParams} from '@/server/helpers/params' 8 | 9 | export async function GET( 10 | req: Request, 11 | { 12 | params, 13 | }: { 14 | params: {packageId: string} 15 | }, 16 | ) { 17 | const pkg = await getFullPackageById(params.packageId) 18 | 19 | if (!pkg) { 20 | return error('Package not found', 'not_found', 404) 21 | } 22 | 23 | const doc = await parseJsonSpec(pkg.openapi) 24 | 25 | const path = getParams(req).get('path') 26 | 27 | const endpoint = doc.endpoints.find((endpoint) => endpoint.path === path) 28 | 29 | if (!endpoint) { 30 | return error('Endpoint not found', 'not_found', 404) 31 | } 32 | 33 | const requestExample = endpoint.requestExample 34 | const responseExample = endpoint.responseExample 35 | 36 | const [ 37 | requestExampleCurl, 38 | requestExampleJavaScript, 39 | requestExamplePython, 40 | responseExampleJson, 41 | ] = await Promise.all([ 42 | highlight(requestExample.exampleCurl, 'shellscript'), 43 | highlight(requestExample.exampleJavaScript, 'javascript'), 44 | highlight(requestExample.examplePython, 'python'), 45 | responseExample?.exampleJson 46 | ? highlight(responseExample.exampleJson, 'shellscript') 47 | : null, 48 | ]) 49 | 50 | return NextResponse.json( 51 | { 52 | requestExampleCurl, 53 | requestExampleJavaScript, 54 | requestExamplePython, 55 | responseExampleJson, 56 | }, 57 | { 58 | headers: { 59 | 'Cache-Control': 's-maxage=86400, stale-while-revalidate', 60 | }, 61 | }, 62 | ) 63 | } 64 | -------------------------------------------------------------------------------- /app/api/packages/[packageId]/get-package.ts: -------------------------------------------------------------------------------- 1 | import {NextResponse} from 'next/server' 2 | 3 | import {buildPackageResponse} from '@/helpers/api/package-response' 4 | import {getFullPackageById} from '@/server/db/packages/getters' 5 | import {error} from '@/server/helpers/error' 6 | 7 | // Retrive a package 8 | 9 | async function endpoint( 10 | request: Request, 11 | { 12 | params, 13 | }: { 14 | params: {packageId: string} 15 | }, 16 | ) { 17 | const {searchParams} = new URL(request.url) 18 | const proxy = searchParams.get('proxy') === 'true' 19 | const pkg = await getFullPackageById(params.packageId) 20 | 21 | if (!pkg) { 22 | return error('Package not found', 'not_found', 404) 23 | } 24 | 25 | const response = await buildPackageResponse(pkg, {proxy}) 26 | 27 | return NextResponse.json(response) 28 | } 29 | 30 | export default endpoint 31 | -------------------------------------------------------------------------------- /app/api/packages/[packageId]/openapi/route.ts: -------------------------------------------------------------------------------- 1 | import {stringify as yamlStringify} from 'yaml' 2 | 3 | import {parseJsonSpec} from '@/helpers/openapi' 4 | import {getFullPackageById} from '@/server/db/packages/getters' 5 | import {error} from '@/server/helpers/error' 6 | import {getParams} from '@/server/helpers/params' 7 | 8 | // Retrive a package OpenAPI manifest 9 | export async function GET(req: Request, {params}: {params: {packageId: string}}) { 10 | const extension = req.url.split('.').pop() 11 | const format = getParams(req).get('format') ?? extension ?? 'json' 12 | 13 | const pkg = await getFullPackageById(params.packageId) 14 | 15 | if (!pkg) { 16 | return error('Package not found', 'not_found', 404) 17 | } 18 | 19 | const doc = await parseJsonSpec(pkg.openapi) 20 | 21 | if (format === 'yaml') { 22 | return new Response(yamlStringify(doc), { 23 | headers: { 24 | 'Content-Type': 'text/yaml', 25 | }, 26 | }) 27 | } else { 28 | return new Response(JSON.stringify(doc, null, 2), { 29 | headers: { 30 | 'Content-Type': 'application/json', 31 | }, 32 | }) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /app/api/packages/[packageId]/route.ts: -------------------------------------------------------------------------------- 1 | import deletePackage from './delete-package' 2 | import getPackage from './get-package' 3 | import updatePackage from './update-package' 4 | 5 | export {getPackage as GET} 6 | export {updatePackage as PUT} 7 | export {deletePackage as DELETE} 8 | -------------------------------------------------------------------------------- /app/api/packages/[packageId]/update-package.ts: -------------------------------------------------------------------------------- 1 | import {NextResponse} from 'next/server' 2 | import semver from 'semver' 3 | import {z} from 'zod' 4 | 5 | import {parseSpec} from '@/helpers/openapi' 6 | import {OpenApiDocument} from '@/helpers/openapi/document' 7 | import {getFullPackageById} from '@/server/db/packages/getters' 8 | import {updatePackage, updatePackageSpec} from '@/server/db/packages/setters' 9 | import {withApiBuilder} from '@/server/helpers/api-builder' 10 | import {withAuth} from '@/server/helpers/auth' 11 | import {error} from '@/server/helpers/error' 12 | 13 | const ApiSchema = z.object({ 14 | packageId: z.string(), 15 | openapi: z.string().nullable(), 16 | openapi_format: z.enum(['json', 'yaml']).default('json'), 17 | name: z.string().nullable(), 18 | machine_name: z.string().nullable(), 19 | domain: z.string(), 20 | contact_email: z.string().nullable(), 21 | legal_info_url: z.string().nullable(), 22 | logo_url: z.string().nullable(), 23 | description: z.string().nullable(), 24 | machine_description: z.string().nullable(), 25 | oauth_client_id: z.string().nullable(), 26 | oauth_client_secret: z.string().nullable(), 27 | oauth_authorization_url: z.string().nullable(), 28 | oauth_token_url: z.string().nullable(), 29 | }) 30 | 31 | type ApiRequestParams = z.infer 32 | 33 | // Creates a new package version 34 | 35 | const endpoint = withAuth( 36 | withApiBuilder( 37 | ApiSchema, 38 | async (req: Request, {userId, data}) => { 39 | const packageRow = await getFullPackageById(data.packageId) 40 | 41 | if (!packageRow) { 42 | return error('Package does not exist') 43 | } 44 | 45 | if (!packageRow.acl_write.includes(userId)) { 46 | return error('Unauthorized', 'unauthorized', 403) 47 | } 48 | 49 | await updatePackage(data.packageId, data) 50 | 51 | if (data.openapi) { 52 | let doc: OpenApiDocument 53 | 54 | try { 55 | doc = await parseSpec(data.openapi, data.openapi_format) 56 | } catch (err: any) { 57 | return error(`Invalid OpenAPI document: ${err?.message}`) 58 | } 59 | 60 | const version = semver.valid(doc.version) 61 | 62 | if (!version) { 63 | return error('Invalid version') 64 | } 65 | 66 | if (semver.gt(version, packageRow.version)) { 67 | await updatePackageSpec({ 68 | packageId: data.packageId, 69 | openapi: JSON.stringify(doc), 70 | version, 71 | }) 72 | } 73 | } 74 | 75 | return NextResponse.json({ 76 | id: packageRow.id, 77 | }) 78 | }, 79 | ), 80 | ) 81 | 82 | export default endpoint 83 | -------------------------------------------------------------------------------- /app/api/packages/connected/route.ts: -------------------------------------------------------------------------------- 1 | import {NextResponse} from 'next/server' 2 | 3 | import {buildPackageResponse} from '@/helpers/api/package-response' 4 | import {getFullPackagesByIds} from '@/server/db/packages/getters' 5 | import {getConnectionsForUser} from '@/server/db/user-connections/getters' 6 | import {withAuth} from '@/server/helpers/auth' 7 | 8 | // Retrive a list of connected packages for the current user 9 | 10 | export const GET = withAuth(async (request: Request, {userId}) => { 11 | const {searchParams} = new URL(request.url) 12 | const proxy = searchParams.get('proxy') === 'true' 13 | const connections = await getConnectionsForUser(userId) 14 | 15 | if (!connections.length) { 16 | return NextResponse.json([]) 17 | } 18 | 19 | const packageIds = connections.map((conn) => conn.package_id) 20 | 21 | const packages = await getFullPackagesByIds(packageIds) 22 | 23 | const responses = await Promise.all( 24 | packages.map((pkg) => buildPackageResponse(pkg, {proxy})), 25 | ) 26 | 27 | return NextResponse.json(responses) 28 | }) 29 | -------------------------------------------------------------------------------- /app/api/packages/create-package.ts: -------------------------------------------------------------------------------- 1 | import first from 'lodash/first' 2 | import {NextResponse} from 'next/server' 3 | import {z} from 'zod' 4 | 5 | import {parseSpec} from '@/helpers/openapi' 6 | import {OpenApiDocument} from '@/helpers/openapi/document' 7 | import {getFullPackageById} from '@/server/db/packages/getters' 8 | import {createPackage} from '@/server/db/packages/setters' 9 | import {getUserById} from '@/server/db/users/getters' 10 | import {withApiBuilder} from '@/server/helpers/api-builder' 11 | import {withAuth} from '@/server/helpers/auth' 12 | import {error} from '@/server/helpers/error' 13 | 14 | const ApiSchema = z.object({ 15 | // Validate no spaces 16 | id: z.string().regex(/^[a-z0-9-]+$/), 17 | openapi: z.string(), 18 | openapi_format: z.enum(['json', 'yaml']).default('json'), 19 | }) 20 | 21 | type ApiRequestParams = z.infer 22 | 23 | const createPackageEndpoint = withAuth( 24 | withApiBuilder( 25 | ApiSchema, 26 | async (req: Request, {userId, data}) => { 27 | const userRow = await getUserById(userId) 28 | const packageRow = await getFullPackageById(data.id) 29 | 30 | if (packageRow) { 31 | return error('Package already exists') 32 | } 33 | 34 | let doc: OpenApiDocument 35 | 36 | try { 37 | doc = await parseSpec(data.openapi, data.openapi_format) 38 | } catch (err: any) { 39 | return error(`Invalid OpenAPI document: ${err?.message}`) 40 | } 41 | 42 | const version = doc.version 43 | 44 | if (typeof version !== 'string') { 45 | return error('Invalid OpenAPI version') 46 | } 47 | 48 | const domain = doc.domain 49 | 50 | if (!domain) { 51 | return error('Missing OpenAPI domain') 52 | } 53 | 54 | await createPackage({ 55 | id: data.id, 56 | openapi: JSON.stringify(doc), 57 | name: doc.name || data.id, 58 | version, 59 | userId, 60 | domain, 61 | logoUrl: defaultLogo(domain), 62 | description: doc.description, 63 | contactEmail: userRow ? first(userRow.emails) ?? null : null, 64 | }) 65 | 66 | return NextResponse.json({ 67 | id: data.id, 68 | version, 69 | openapi: doc, 70 | }) 71 | }, 72 | ), 73 | ) 74 | 75 | const defaultLogo = (domain: string) => `https://logo.clearbit.com/${domain}` 76 | 77 | export default createPackageEndpoint 78 | -------------------------------------------------------------------------------- /app/api/packages/list-packages.ts: -------------------------------------------------------------------------------- 1 | import {NextResponse} from 'next/server' 2 | import {z} from 'zod' 3 | 4 | import {getPackagesWithPagination} from '@/server/db/packages/getters' 5 | import {withApiBuilder} from '@/server/helpers/api-builder' 6 | 7 | const ApiSchema = z.object({ 8 | limit: z.coerce.number().min(1).max(500).default(100), 9 | page: z.coerce.number().min(1).default(1), 10 | }) 11 | 12 | type ApiRequestParams = z.infer 13 | 14 | // Paginated lists of packages 15 | 16 | const endpoint = withApiBuilder( 17 | ApiSchema, 18 | async (request: Request, {data}) => { 19 | const {limit, page} = data 20 | 21 | const result = await getPackagesWithPagination({page, limit}) 22 | 23 | return NextResponse.json(result, { 24 | headers: { 25 | 'Cache-Control': 's-maxage=30, stale-while-revalidate=59', 26 | }, 27 | }) 28 | }, 29 | ) 30 | 31 | export default endpoint 32 | -------------------------------------------------------------------------------- /app/api/packages/lookup/route.ts: -------------------------------------------------------------------------------- 1 | import {NextResponse} from 'next/server' 2 | import {z} from 'zod' 3 | 4 | import { 5 | getPackagesWithIds, 6 | } from '@/server/db/packages/getters' 7 | import {withApiBuilder} from '@/server/helpers/api-builder' 8 | import { error } from '@/server/helpers/error' 9 | 10 | const ApiSchema = z.object({ 11 | ids: z.string(), 12 | }) 13 | 14 | type ApiRequestParams = z.infer 15 | 16 | export const GET = withApiBuilder( 17 | ApiSchema, 18 | async (request: Request, {data}) => { 19 | const {ids: idsStr} = data 20 | const ids = idsStr.split(',') 21 | 22 | if (ids.length > 1000) { 23 | return error('Too many ids') 24 | } 25 | 26 | if (ids.length === 0) { 27 | return error('No ids') 28 | } 29 | 30 | const packages = await getPackagesWithIds(ids) 31 | 32 | return NextResponse.json(packages) 33 | }, 34 | ) -------------------------------------------------------------------------------- /app/api/packages/paginated-search/route.ts: -------------------------------------------------------------------------------- 1 | import {NextResponse} from 'next/server' 2 | import {z} from 'zod' 3 | 4 | import {searchPackagesWithPagination} from '@/server/db/packages/getters' 5 | import {withApiBuilder} from '@/server/helpers/api-builder' 6 | 7 | const ApiSchema = z.object({ 8 | query: z.string(), 9 | limit: z.coerce.number().min(1).max(500).default(10), 10 | page: z.coerce.number().min(1).default(1), 11 | }) 12 | 13 | type ApiRequestParams = z.infer 14 | 15 | // Paginated lists of packages 16 | 17 | export const GET = withApiBuilder( 18 | ApiSchema, 19 | async (request: Request, {data}) => { 20 | const {query, limit, page} = data 21 | 22 | const result = await searchPackagesWithPagination({query, limit, page}) 23 | 24 | return NextResponse.json(result, { 25 | headers: { 26 | 'Cache-Control': 's-maxage=30, stale-while-revalidate=59', 27 | }, 28 | }) 29 | }, 30 | ) 31 | -------------------------------------------------------------------------------- /app/api/packages/route.ts: -------------------------------------------------------------------------------- 1 | import createPackage from './create-package' 2 | import listPackages from './list-packages' 3 | 4 | export {createPackage as POST} 5 | export {listPackages as GET} 6 | -------------------------------------------------------------------------------- /app/api/packages/search/route.ts: -------------------------------------------------------------------------------- 1 | import {NextResponse} from 'next/server' 2 | import {z} from 'zod' 3 | 4 | import {buildPackageResponse} from '@/helpers/api/package-response' 5 | import {searchPackages} from '@/server/db/packages/getters' 6 | import {withApiBuilder} from '@/server/helpers/api-builder' 7 | 8 | const ApiSchema = z.object({ 9 | query: z.string(), 10 | limit: z.coerce.number().min(1).max(500).default(10), 11 | page: z.coerce.number().min(1).default(1), 12 | proxy: z.coerce.boolean().default(false), 13 | }) 14 | 15 | type ApiRequestParams = z.infer 16 | 17 | // Paginated lists of packages 18 | 19 | export const GET = withApiBuilder( 20 | ApiSchema, 21 | async (request: Request, {data}) => { 22 | const {query, limit, proxy} = data 23 | 24 | const packages = await searchPackages({query, limit}) 25 | 26 | return NextResponse.json( 27 | packages.map((pkg) => buildPackageResponse(pkg, {proxy})), 28 | { 29 | headers: { 30 | 'Cache-Control': 's-maxage=30, stale-while-revalidate=59', 31 | }, 32 | }, 33 | ) 34 | }, 35 | ) 36 | -------------------------------------------------------------------------------- /app/api/proxy/[packageId]/[...path]/route.ts: -------------------------------------------------------------------------------- 1 | import {parseJsonSpec} from '@/helpers/openapi' 2 | import {OpenAPI} from '@/helpers/openapi/types' 3 | import {ProxyRequest} from '@/helpers/proxy/proxy-request' 4 | import {getUnsafePackageById} from '@/server/db/packages/getters' 5 | import {withAuth} from '@/server/helpers/auth' 6 | import {error} from '@/server/helpers/error' 7 | 8 | interface ProxyOptions { 9 | params: {packageId: string; path: string[]} 10 | userId: string 11 | } 12 | 13 | function buildProxy(method: OpenAPI.HttpMethods) { 14 | return withAuth(async (request: Request, {params, userId}: ProxyOptions) => { 15 | const pkg = await getUnsafePackageById(params.packageId) 16 | 17 | if (!pkg) { 18 | return error('Package not found', 'package_not_found', 404) 19 | } 20 | 21 | const document = await parseJsonSpec(pkg.openapi) 22 | 23 | const path = `/${params.path.join('/')}` 24 | 25 | const proxyRequest = new ProxyRequest({ 26 | method, 27 | path, 28 | request, 29 | package: pkg, 30 | document, 31 | userId, 32 | }) 33 | 34 | if (!proxyRequest.endpoint) { 35 | return error('Endpoint not found', 'endpoint_not_found', 404) 36 | } 37 | 38 | return proxyRequest.fetch() 39 | }) 40 | } 41 | 42 | export const GET = buildProxy(OpenAPI.HttpMethods.GET) 43 | export const POST = buildProxy(OpenAPI.HttpMethods.POST) 44 | export const PUT = buildProxy(OpenAPI.HttpMethods.PUT) 45 | export const DELETE = buildProxy(OpenAPI.HttpMethods.DELETE) 46 | export const PATCH = buildProxy(OpenAPI.HttpMethods.PATCH) 47 | -------------------------------------------------------------------------------- /app/api/whoami/route.ts: -------------------------------------------------------------------------------- 1 | import first from 'lodash/first' 2 | import {NextResponse} from 'next/server' 3 | 4 | import {assertString} from '@/lib/assert' 5 | import {getUserById} from '@/server/db/users/getters' 6 | import {withAuth} from '@/server/helpers/auth' 7 | 8 | export const GET = withAuth(async (req: Request, {userId}: {userId: string}) => { 9 | assertString(userId, 'Invalid user') 10 | 11 | const user = await getUserById(userId) 12 | 13 | return NextResponse.json({ 14 | id: userId, 15 | email: first(user?.emails) ?? null, 16 | }) 17 | }) 18 | -------------------------------------------------------------------------------- /app/auth/complete/page.tsx: -------------------------------------------------------------------------------- 1 | 'use server' 2 | 3 | import {redirect} from 'next/navigation' 4 | 5 | import {setUser} from '@/server/db/users/setters' 6 | import {getSessionEmails, authOrRedirect} from '@/server/helpers/auth' 7 | 8 | export default async function CompletePage({ 9 | searchParams, 10 | }: { 11 | searchParams: {redirect?: string} 12 | }) { 13 | const userId = await authOrRedirect() 14 | const emails = (await getSessionEmails()) ?? [] 15 | 16 | await setUser({ 17 | id: userId, 18 | signInDate: new Date(), 19 | emails, 20 | }) 21 | 22 | // If redirect is a relative path 23 | if (searchParams.redirect?.startsWith('/')) { 24 | return redirect(searchParams.redirect) 25 | } 26 | 27 | redirect('/account') 28 | } 29 | -------------------------------------------------------------------------------- /app/auth/page.tsx: -------------------------------------------------------------------------------- 1 | import dynamic from 'next/dynamic' 2 | 3 | import {MainTemplate} from '@/components/main-template' 4 | 5 | const AccountAuth = dynamic(() => import('@/components/account-auth'), { 6 | ssr: false, 7 | }) 8 | 9 | export default function AccountPage({searchParams}: {searchParams: {redirect?: string}}) { 10 | return ( 11 | 12 |
    13 |
    14 | 15 |
    16 |
    17 |
    18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /app/connect/oauth-callback/page.tsx: -------------------------------------------------------------------------------- 1 | 'use server' 2 | 3 | import {redirect} from 'next/navigation' 4 | 5 | import {fetchAccessToken} from '@/helpers/proxy/oauth' 6 | import {assertString} from '@/lib/assert' 7 | import {getUnsafePackageById} from '@/server/db/packages/getters' 8 | import {createUserConnection} from '@/server/db/user-connections/setters' 9 | import {authOrRedirect} from '@/server/helpers/auth' 10 | 11 | export default async function ConnectOAuthCallback({ 12 | searchParams, 13 | }: { 14 | searchParams: {package_id: string; code: string} 15 | }) { 16 | const userId = await authOrRedirect() 17 | 18 | const packageId = searchParams.package_id 19 | const code = searchParams.code 20 | 21 | assertString(packageId, 'package_id') 22 | assertString(code, 'code') 23 | 24 | const pkg = await getUnsafePackageById(packageId) 25 | 26 | if (!pkg) { 27 | redirect('/packages') 28 | } 29 | 30 | assertString(pkg.oauth_authorization_url, 'oauth_authorization_url') 31 | assertString(pkg.oauth_client_id, 'oauth_client_id') 32 | assertString(pkg.oauth_client_secret, 'oauth_client_secret') 33 | assertString(pkg.oauth_token_url, 'oauth_token_url') 34 | 35 | const accessToken = await fetchAccessToken({ 36 | code, 37 | clientId: pkg.oauth_client_id, 38 | clientSecret: pkg.oauth_client_secret, 39 | tokenUrl: pkg.oauth_token_url, 40 | }) 41 | 42 | await createUserConnection({ 43 | packageId, 44 | userId, 45 | accessToken, 46 | }) 47 | 48 | redirect(`/account`) 49 | } 50 | -------------------------------------------------------------------------------- /app/docs/page.tsx: -------------------------------------------------------------------------------- 1 | import {MainTemplate} from '@/components/main-template' 2 | 3 | export default async function AiPluginPage() { 4 | return ( 5 | 6 |
    7 |

    8 | There are currently two ways of integrating APIs into OpenAI's GPT: through 9 | chat functions, or through ChatGPT. Here are some resources on both approaches. 10 |

    11 | 12 |

    ChatGPT integration

    13 | 14 |

    15 | To integrate an API into ChatGPT you are going to need: an ai-plugin.json file, 16 | an OpenAPI file, and a ChatGPT developer account. 17 |

    18 | 19 |
      20 |
    1. 21 | Read ChatGPT's{' '} 22 | 23 | official docs 24 | 25 | . 26 |
    2. 27 |
    3. 28 | See example OpenAPI files, and{' '} 29 | learn how to generate your own. 30 |
    4. 31 |
    5. 32 | Clone our{' '} 33 | 34 | Cloudflare worker 35 | {' '} 36 | to easily boot up your own AI plugin. 37 |
    6. 38 |
    39 | 40 |

    Function calling from GPT3 and GPT4

    41 |

    42 | OpenAI have recently released a fine-tuned model and API specifically for 43 | exposing a set of functions for the LLM to call. This is a powerful way of 44 | mixing in AI into your existing app. 45 |

    46 | 47 |
      48 |
    1. 49 | Read OpenAI's official docs on{' '} 50 | 51 | function calling 52 | 53 | . 54 |
    2. 55 |
    3. 56 | We have created a framework called{' '} 57 | WorkGPT, which you can 58 | use to expose your APIs to GPT 3.5 and 4. 59 |
    4. 60 |
    61 |
    62 |
    63 | ) 64 | } 65 | -------------------------------------------------------------------------------- /app/export/page.tsx: -------------------------------------------------------------------------------- 1 | import {MainTemplate} from '@/components/main-template' 2 | 3 | export default async function AboutPage() { 4 | return ( 5 | 6 |
    7 |

    Export

    8 | 9 |
    10 |

    11 | At any point you can choose to export all of our packages. The export is MIT 12 | licensed. You are free to use it for your personal use, create a new 13 | package-manager, or whatever you want to do with it. 14 |

    15 | 16 |

    17 | Currently the export in JSON format. We're working on more export formats 18 | in the future. 19 |

    20 | 21 |

    22 | 23 | Download Export 24 | 25 |

    26 |
    27 |
    28 |
    29 | ) 30 | } 31 | -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/team-openpm/openpm/8c040c59bb0e36ea5ffa38d12e1db514d3d49de6/app/favicon.ico -------------------------------------------------------------------------------- /app/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | a { 6 | color: inherit; 7 | text-decoration: none; 8 | } 9 | 10 | @media (prefers-color-scheme: dark) { 11 | html { 12 | color-scheme: dark; 13 | } 14 | } 15 | 16 | :root { 17 | --shiki-color-text: theme('colors.white'); 18 | --shiki-token-constant: theme('colors.emerald.300'); 19 | --shiki-token-string: theme('colors.emerald.300'); 20 | --shiki-token-comment: theme('colors.zinc.500'); 21 | --shiki-token-keyword: theme('colors.sky.300'); 22 | --shiki-token-parameter: theme('colors.pink.300'); 23 | --shiki-token-function: theme('colors.violet.300'); 24 | --shiki-token-string-expression: theme('colors.emerald.300'); 25 | --shiki-token-punctuation: theme('colors.zinc.200'); 26 | } 27 | 28 | @layer utilities { 29 | .increase-click-area { 30 | @apply relative before:absolute before:-inset-3; 31 | } 32 | } 33 | 34 | hanko-auth, 35 | hanko-profile { 36 | /* Color Scheme */ 37 | --color: #18181b; 38 | --color-shade-1: #8f9095; 39 | --color-shade-2: #e5e6ef; 40 | 41 | --brand-color: #ec4899; 42 | --brand-color-shade-1: rgb(39 39 42 / 2%); 43 | --brand-contrast-color: #18181b; 44 | 45 | --background-color: transparent; 46 | --error-color: #e82020; 47 | --link-color: #3b82f6; 48 | 49 | /* Font Styles */ 50 | --font-weight: 400; 51 | --font-size: 14px; 52 | --font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 53 | 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 54 | 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'; 55 | 56 | /* Border Styles */ 57 | --border-radius: 4px; 58 | --border-style: solid; 59 | --border-width: 1px; 60 | 61 | /* Item Styles */ 62 | --item-height: 34px; 63 | --item-margin: 0.5rem 0; 64 | 65 | /* Container Styles */ 66 | --container-padding: 0; 67 | --container-max-width: 600px; 68 | 69 | /* Headline Styles */ 70 | --headline1-font-size: 16px; 71 | --headline1-font-weight: 600; 72 | --headline1-margin: 0 0 0.5rem; 73 | 74 | --headline2-font-size: 14px; 75 | --headline2-font-weight: 600; 76 | --headline2-margin: 1rem 0 0.25rem; 77 | 78 | /* Divider Styles */ 79 | --divider-padding: 0 42px; 80 | --divider-display: block; 81 | --divider-visibility: visible; 82 | 83 | /* Link Styles */ 84 | --link-text-decoration: none; 85 | --link-text-decoration-hover: underline; 86 | 87 | /* Input Styles */ 88 | --input-min-width: 12em; 89 | 90 | /* Button Styles */ 91 | --button-min-width: max-content; 92 | } 93 | 94 | hanko-auth::part(headline1), 95 | hanko-profile::part(headline1) { 96 | margin-top: 30px; 97 | } 98 | 99 | hanko-auth::part(divider-line) { 100 | display: none; 101 | } 102 | 103 | hanko-auth::part(divider-text) { 104 | color: #18181b; 105 | padding: 0; 106 | } 107 | 108 | .dark hanko-auth::part(divider-text) { 109 | color: rgb(255 255 255 / 90%); 110 | } 111 | 112 | .dark hanko-auth, 113 | .dark hanko-profile { 114 | /* Color Scheme */ 115 | --color: rgb(255 255 255 / 90%); 116 | 117 | --brand-color-shade-1: rgb(255 255 255 / 5%); 118 | --brand-contrast-color: white; 119 | } 120 | -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import {Inter} from 'next/font/google' 2 | import './globals.css' 3 | import Head from 'next/head' 4 | 5 | export const metadata = { 6 | title: 'openpm', 7 | description: 'AI plugin package manager', 8 | } 9 | 10 | const inter = Inter({subsets: ['latin']}) 11 | 12 | export default function RootLayout({children}: {children: React.ReactNode}) { 13 | return ( 14 | 15 | 16 | 22 | 23 | 24 | 27 | {children} 28 | 29 | 30 | ) 31 | } 32 | -------------------------------------------------------------------------------- /app/new/page.tsx: -------------------------------------------------------------------------------- 1 | import {MainTemplate} from '@/components/main-template' 2 | import NewPackageForm from '@/components/new-package/new-package-form' 3 | import {authOrRedirect} from '@/server/helpers/auth' 4 | 5 | export default async function NewPackage() { 6 | await authOrRedirect() 7 | 8 | return ( 9 | 10 | 11 | 12 | ) 13 | } 14 | -------------------------------------------------------------------------------- /app/packages/[packageId]/connect/page.tsx: -------------------------------------------------------------------------------- 1 | 'use server' 2 | 3 | import {ReadonlyHeaders} from 'next/dist/server/web/spec-extension/adapters/headers' 4 | import {headers} from 'next/headers' 5 | import {redirect} from 'next/navigation' 6 | import React from 'react' 7 | 8 | import ConnectPackageForm from '@/components/connect-package/connect-package-form' 9 | import {MainTemplate} from '@/components/main-template' 10 | import {parseJsonSpec} from '@/helpers/openapi' 11 | import {buildAuthorizationUrl} from '@/helpers/proxy/oauth' 12 | import {assertString} from '@/lib/assert' 13 | import {getUnsafePackageById} from '@/server/db/packages/getters' 14 | import {authOrRedirect} from '@/server/helpers/auth' 15 | 16 | export default async function ConnectPackage({params}: {params: {packageId: string}}) { 17 | const requestOrigin = getOrigin(headers()) 18 | await authOrRedirect() 19 | 20 | const pkg = await getUnsafePackageById(params.packageId) 21 | 22 | if (!pkg) { 23 | redirect('/packages') 24 | } 25 | 26 | const doc = await parseJsonSpec(pkg.openapi) 27 | 28 | if (doc.hasOauth) { 29 | assertString(pkg.oauth_authorization_url, 'oauth_authorization_url') 30 | assertString(pkg.oauth_client_id, 'oauth_client_id') 31 | 32 | const redirectUrl = buildAuthorizationUrl({ 33 | packageId: pkg.id, 34 | authorizationUrl: pkg.oauth_authorization_url, 35 | redirectUrl: `${requestOrigin}/connect/oauth-callback`, 36 | clientId: pkg.oauth_client_id, 37 | }) 38 | 39 | redirect(redirectUrl) 40 | } 41 | 42 | if (doc.hasApiKey) { 43 | return ( 44 | 45 | 46 | 47 | ) 48 | } 49 | 50 | redirect(`/packages/${pkg.id}`) 51 | } 52 | 53 | // returns https://localhost:3001 54 | function getOrigin(headers: ReadonlyHeaders): string { 55 | const host = headers.get('host') 56 | const protocol = headers.get('x-forwarded-proto') 57 | 58 | return `${protocol}://${host}` 59 | } 60 | -------------------------------------------------------------------------------- /app/packages/[packageId]/description/page.tsx: -------------------------------------------------------------------------------- 1 | import {ArrowLongLeftIcon} from '@heroicons/react/20/solid' 2 | import Link from 'next/link' 3 | import React from 'react' 4 | 5 | import {AccountHeader} from '@/components/account-header' 6 | import {MarkdownDynamic} from '@/components/markdown/markdown-dynamic' 7 | import {getPackageByIdOrNotFound} from '@/server/db/packages/getters' 8 | 9 | export const revalidate = 15 10 | 11 | export default async function Package({params}: {params: {packageId: string}}) { 12 | const pkg = await getPackageByIdOrNotFound(params.packageId) 13 | 14 | return ( 15 |
    16 |
    17 |
    18 |

    19 | 20 | openpm.ai 21 | 22 |

    23 | 24 |
    25 | 26 | apis 27 | 28 | / 29 | 30 | {pkg.id} 31 | 32 | / 33 | description 34 |
    35 |
    36 | 37 |
    38 | 43 | 44 | Back to api 45 | 46 |
    47 |
    48 | 49 |
    50 | 51 | 52 |
    53 |

    {pkg.id}

    54 | 55 | {pkg.description && ( 56 |
    57 | 58 |
    59 | )} 60 |
    61 |
    62 |
    63 | ) 64 | } 65 | -------------------------------------------------------------------------------- /app/packages/[packageId]/edit/page.tsx: -------------------------------------------------------------------------------- 1 | 'use server' 2 | 3 | import React from 'react' 4 | 5 | import EditPackageForm from '@/components/edit-package/edit-package-form' 6 | import {MainTemplate} from '@/components/main-template' 7 | import {parseJsonSpec} from '@/helpers/openapi' 8 | import {getPackageForEditByUserOrNotFound} from '@/server/db/packages/getters' 9 | import {authOrRedirect} from '@/server/helpers/auth' 10 | 11 | export default async function EditPackage({params}: {params: {packageId: string}}) { 12 | const userId = await authOrRedirect() 13 | 14 | const pkg = await getPackageForEditByUserOrNotFound({ 15 | packageId: params.packageId, 16 | userId, 17 | }) 18 | 19 | const doc = await parseJsonSpec(pkg.openapi) 20 | 21 | const hasOauth = doc.hasOauth 22 | 23 | return ( 24 | 25 | 26 | 27 | ) 28 | } 29 | -------------------------------------------------------------------------------- /app/packages/[packageId]/endpoint/page.tsx: -------------------------------------------------------------------------------- 1 | import truncate from 'lodash/truncate' 2 | import {redirect} from 'next/navigation' 3 | import React from 'react' 4 | 5 | import {AccountHeader} from '@/components/account-header' 6 | import {DocumentEndpoints} from '@/components/show-package/document-endpoints' 7 | import {PackageSidebar} from '@/components/show-package/package-sidebar' 8 | import {parseJsonSpec} from '@/helpers/openapi' 9 | import {getPackageByIdOrNotFound} from '@/server/db/packages/getters' 10 | 11 | export const revalidate = 60 12 | 13 | export async function generateMetadata({params}: {params: {packageId: string}}) { 14 | const pkg = await getPackageByIdOrNotFound(params.packageId) 15 | 16 | const title = `${pkg.id} | openpm` 17 | const description = truncate(pkg.description || 'OpenAPI package manager', { 18 | length: 160, 19 | }) 20 | 21 | return { 22 | title, 23 | description, 24 | openGraph: { 25 | title, 26 | description, 27 | url: `https://openpm.ai/packages/${pkg.id}`, 28 | siteName: 'openpm', 29 | locale: 'en-US', 30 | type: 'website', 31 | }, 32 | } 33 | } 34 | 35 | export default async function PackageEndpoint({ 36 | params, 37 | searchParams, 38 | }: { 39 | params: {packageId: string} 40 | searchParams: {path: string} 41 | }) { 42 | const pkg = await getPackageByIdOrNotFound(params.packageId) 43 | const doc = await parseJsonSpec(pkg.openapi) 44 | 45 | if (!searchParams.path) { 46 | redirect(`/packages/${pkg.id}`) 47 | } 48 | 49 | const groupedEndpoints = doc.groupedEndpointsForPath(searchParams.path) 50 | 51 | return ( 52 |
    53 |
    54 | 55 |
    56 | 57 |
    58 | 59 | 60 |
    61 | 62 |
    63 |
    64 |
    65 | ) 66 | } 67 | -------------------------------------------------------------------------------- /app/packages/[packageId]/openapi/page.tsx: -------------------------------------------------------------------------------- 1 | 'use server' 2 | 3 | import React from 'react' 4 | 5 | import {Code} from '@/components/code' 6 | import {parseJsonSpec} from '@/helpers/openapi' 7 | import {highlight} from '@/lib/code-highlighter' 8 | import {getPackageByIdOrNotFound} from '@/server/db/packages/getters' 9 | 10 | export default async function PackageOpenApi({params}: {params: {packageId: string}}) { 11 | const pkg = await getPackageByIdOrNotFound(params.packageId) 12 | const doc = await parseJsonSpec(pkg.openapi) 13 | 14 | const json = JSON.stringify(doc, null, 2) 15 | const jsonHighlighted = await highlight(json, 'json') 16 | 17 | return 18 | } 19 | -------------------------------------------------------------------------------- /app/packages/[packageId]/page.tsx: -------------------------------------------------------------------------------- 1 | import truncate from 'lodash/truncate' 2 | import React from 'react' 3 | 4 | import {AccountHeader} from '@/components/account-header' 5 | import {DocumentEndpoints} from '@/components/show-package/document-endpoints' 6 | import {PackageHeader} from '@/components/show-package/package-header' 7 | import {PackageSidebar} from '@/components/show-package/package-sidebar' 8 | import {parseJsonSpec} from '@/helpers/openapi' 9 | import {OpenApiEndpoint} from '@/helpers/openapi/endpoint' 10 | import {getAllPackageIds, getPackageByIdOrNotFound} from '@/server/db/packages/getters' 11 | 12 | export const revalidate = 15 13 | 14 | export async function generateStaticParams() { 15 | const packages = await getAllPackageIds() 16 | 17 | return packages.map((pkg) => ({ 18 | packageId: pkg.id, 19 | })) 20 | } 21 | 22 | export async function generateMetadata({params}: {params: {packageId: string}}) { 23 | const pkg = await getPackageByIdOrNotFound(params.packageId) 24 | 25 | const title = `${pkg.id} | openpm` 26 | const description = truncate(pkg.description || 'OpenAPI package manager', { 27 | length: 160, 28 | }) 29 | 30 | return { 31 | title, 32 | description, 33 | openGraph: { 34 | title, 35 | description, 36 | url: `https://openpm.ai/packages/${pkg.id}`, 37 | siteName: 'openpm', 38 | locale: 'en-US', 39 | type: 'website', 40 | }, 41 | } 42 | } 43 | 44 | export default async function PackageEndpoint({params}: {params: {packageId: string}}) { 45 | const pkg = await getPackageByIdOrNotFound(params.packageId) 46 | const doc = await parseJsonSpec(pkg.openapi) 47 | 48 | let groupedEndpoints: Map 49 | let pagedEndpoints = false 50 | 51 | if (doc.pagedEndpoints) { 52 | groupedEndpoints = doc.firstGroupedEndpoint 53 | pagedEndpoints = true 54 | } else { 55 | groupedEndpoints = doc.groupedEndpoints 56 | } 57 | 58 | return ( 59 |
    60 |
    61 | 62 |
    63 | 64 |
    65 | 66 | 67 |
    68 | 69 | 70 |
    71 |
    72 |
    73 | ) 74 | } 75 | -------------------------------------------------------------------------------- /app/packages/[packageId]/versions/[packageVersion]/endpoint/page.tsx: -------------------------------------------------------------------------------- 1 | import truncate from 'lodash/truncate' 2 | import {redirect} from 'next/navigation' 3 | import React from 'react' 4 | 5 | import {AccountHeader} from '@/components/account-header' 6 | import {DocumentEndpoints} from '@/components/show-package/document-endpoints' 7 | import {PackageSidebar} from '@/components/show-package/package-sidebar' 8 | import {parseJsonSpec} from '@/helpers/openapi' 9 | import { 10 | getPackageByIdOrNotFound, 11 | getPackageVersionOrNotFound, 12 | } from '@/server/db/packages/getters' 13 | 14 | export const revalidate = 60 15 | 16 | export async function generateMetadata({params}: {params: {packageId: string}}) { 17 | const pkg = await getPackageByIdOrNotFound(params.packageId) 18 | 19 | const title = `${pkg.id} | openpm` 20 | const description = truncate(pkg.description || 'OpenAPI package manager', { 21 | length: 160, 22 | }) 23 | 24 | return { 25 | title, 26 | description, 27 | openGraph: { 28 | title, 29 | description, 30 | url: `https://openpm.ai/packages/${pkg.id}`, 31 | siteName: 'openpm', 32 | locale: 'en-US', 33 | type: 'website', 34 | }, 35 | } 36 | } 37 | 38 | export default async function PackageVersionEndpoint({ 39 | params, 40 | searchParams, 41 | }: { 42 | params: {packageId: string; packageVersion: string} 43 | searchParams: {path: string} 44 | }) { 45 | const pkg = await getPackageByIdOrNotFound(params.packageId) 46 | const version = await getPackageVersionOrNotFound({ 47 | packageId: params.packageId, 48 | version: params.packageVersion, 49 | }) 50 | const doc = await parseJsonSpec(version.openapi) 51 | 52 | if (!searchParams.path) { 53 | redirect(`/packages/${pkg.id}`) 54 | } 55 | 56 | const groupedEndpoints = doc.groupedEndpointsForPath(searchParams.path) 57 | 58 | return ( 59 |
    60 |
    61 | 67 |
    68 | 69 |
    70 | 71 | 72 |
    73 | 74 |
    75 |
    76 |
    77 | ) 78 | } 79 | -------------------------------------------------------------------------------- /app/packages/[packageId]/versions/[packageVersion]/page.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import {AccountHeader} from '@/components/account-header' 4 | import {PackageMain} from '@/components/show-package/package-main' 5 | import {PackageSidebar} from '@/components/show-package/package-sidebar' 6 | import {parseJsonSpec} from '@/helpers/openapi' 7 | import {OpenApiEndpoint} from '@/helpers/openapi/endpoint' 8 | import { 9 | getPackageByIdOrNotFound, 10 | getPackageVersionOrNotFound, 11 | } from '@/server/db/packages/getters' 12 | 13 | export const revalidate = 15 14 | 15 | export default async function PackageVersion({ 16 | params, 17 | }: { 18 | params: {packageId: string; packageVersion: string} 19 | }) { 20 | const pkg = await getPackageByIdOrNotFound(params.packageId) 21 | const pkgVersion = await getPackageVersionOrNotFound({ 22 | packageId: params.packageId, 23 | version: params.packageVersion, 24 | }) 25 | const doc = await parseJsonSpec(pkgVersion.openapi) 26 | 27 | let groupedEndpoints: Map 28 | let pagedEndpoints = false 29 | 30 | if (doc.pagedEndpoints) { 31 | groupedEndpoints = doc.firstGroupedEndpoint 32 | pagedEndpoints = true 33 | } else { 34 | groupedEndpoints = doc.groupedEndpoints 35 | } 36 | 37 | return ( 38 |
    39 |
    40 | 46 |
    47 | 48 |
    49 | 50 | 51 | 52 |
    53 |
    54 | ) 55 | } 56 | -------------------------------------------------------------------------------- /app/packages/header.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import clsx from 'clsx' 4 | import {ReactNode, useEffect, useState} from 'react' 5 | 6 | export function Header({children}: {children: ReactNode}) { 7 | const [top, setTop] = useState(true) 8 | 9 | // detect whether user has scrolled the page down by 10px 10 | useEffect(() => { 11 | const scrollHandler = () => { 12 | window.pageYOffset > 40 ? setTop(false) : setTop(true) 13 | } 14 | window.addEventListener('scroll', scrollHandler) 15 | return () => window.removeEventListener('scroll', scrollHandler) 16 | }, [top]) 17 | 18 | return ( 19 |
    25 | {children} 26 |
    27 | ) 28 | } 29 | -------------------------------------------------------------------------------- /app/packages/package-item.tsx: -------------------------------------------------------------------------------- 1 | import {truncate} from 'lodash' 2 | import Link from 'next/link' 3 | import {useMemo} from 'react' 4 | 5 | import {cleanDescription} from '@/lib/description' 6 | import {formatDistanceToNow} from '@/lib/format-distance' 7 | 8 | import {PackageResponse} from './types' 9 | 10 | export function PackageItem({pkg}: {pkg: PackageResponse}) { 11 | const publishedAt = useMemo(() => new Date(pkg.published_at), [pkg.published_at]) 12 | 13 | return ( 14 |
  • 18 | 19 |
    20 |
    {pkg.id}
    21 | 22 |
    {pkg.domain}
    23 |
    24 | 25 |
    26 |
    27 | {pkg.description 28 | ? truncate(cleanDescription(pkg.description), {length: 80}) 29 | : null} 30 |
    31 | 32 |
    33 | {pkg.version}{' '} 34 | 37 |
    38 |
    39 | 40 |
  • 41 | ) 42 | } 43 | -------------------------------------------------------------------------------- /app/packages/pagination.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx' 2 | import React from 'react' 3 | 4 | export function Pagination({ 5 | page, 6 | limit, 7 | total, 8 | onChange, 9 | }: { 10 | page: number 11 | limit: number 12 | total: number 13 | onChange: (page: number) => void 14 | }) { 15 | const pages = Math.ceil(total / limit) 16 | const pagesArray = Array.from({length: pages}, (_, i) => i + 1) 17 | 18 | return ( 19 |
    20 |
    21 | {pagesArray.map((p) => ( 22 | 35 | ))} 36 |
    37 |
    38 | ) 39 | } 40 | -------------------------------------------------------------------------------- /app/packages/types.tsx: -------------------------------------------------------------------------------- 1 | export interface PackageResponse { 2 | id: string 3 | name: string 4 | description: string 5 | version: string 6 | domain: string 7 | published_at: string 8 | } 9 | 10 | export interface PaginatedResponse { 11 | items: PackageResponse[] 12 | total: number 13 | } 14 | -------------------------------------------------------------------------------- /app/sign-out/route.ts: -------------------------------------------------------------------------------- 1 | import {NextResponse} from 'next/server' 2 | 3 | export async function GET() { 4 | const response = new NextResponse('', { 5 | status: 302, 6 | headers: { 7 | Location: '/', 8 | }, 9 | }) 10 | 11 | response.cookies.delete('hanko') 12 | 13 | return response 14 | } 15 | -------------------------------------------------------------------------------- /components/account-auth.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import {Hanko, register} from '@teamhanko/hanko-elements' 4 | import {useRouter} from 'next/navigation' 5 | import {useCallback, useEffect} from 'react' 6 | 7 | import {assertString} from '@/lib/assert' 8 | 9 | const hankoApiUrl = process.env.NEXT_PUBLIC_HANKO_API_URL 10 | 11 | export default function AccountAuth({redirect = ''}: {redirect?: string}) { 12 | assertString(hankoApiUrl, 'Hanko API URL is not defined.') 13 | const router = useRouter() 14 | 15 | const redirectAfterLogin = useCallback(() => { 16 | const url = new URL('/auth/complete', window.location.href) 17 | 18 | if (redirect) { 19 | url.searchParams.set('redirect', redirect) 20 | } 21 | 22 | router.replace(url.toString()) 23 | }, [router, redirect]) 24 | 25 | useEffect(() => { 26 | const hanko = new Hanko(hankoApiUrl) 27 | hanko.onAuthFlowCompleted(() => { 28 | redirectAfterLogin() 29 | }) 30 | }, [redirectAfterLogin]) 31 | 32 | useEffect(() => { 33 | // register the component 34 | // see: https://github.com/teamhanko/hanko/blob/main/frontend/elements/README.md#script 35 | register(hankoApiUrl) 36 | }, []) 37 | 38 | return 39 | } 40 | -------------------------------------------------------------------------------- /components/account-header/account-header.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link' 2 | 3 | import AccountHeaderMenu from './menu' 4 | import {ThemeToggle} from './theme-toggle' 5 | import {SearchInput} from '../search-input' 6 | 7 | export const AccountHeader = () => { 8 | return ( 9 |
    10 |
    11 |
    12 | 13 | 14 |
    15 | 27 |
    28 | 29 |
    30 | 31 | 32 | K 33 | 34 |
    35 |
    36 |
    37 | 38 |
    39 | 40 | submit api... 41 | 42 | 43 | 44 | 45 | 46 |
    47 |
    48 | ) 49 | } 50 | -------------------------------------------------------------------------------- /components/account-header/index.ts: -------------------------------------------------------------------------------- 1 | export * from './account-header' 2 | -------------------------------------------------------------------------------- /components/account-header/menu.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import {Menu, Transition} from '@headlessui/react' 4 | import {ChevronDownIcon} from '@heroicons/react/20/solid' 5 | import {Fragment, Suspense} from 'react' 6 | 7 | import {AccountHeaderMenuItems} from './menu-items' 8 | 9 | export default function AccountHeaderMenu() { 10 | return ( 11 | 12 | {({open}) => ( 13 | <> 14 |
    15 | 16 | account 17 | 22 |
    23 | 24 | 33 |
    34 | 35 | {open ? : null} 36 | 37 |
    38 |
    39 | 40 | )} 41 |
    42 | ) 43 | } 44 | -------------------------------------------------------------------------------- /components/account-header/theme-toggle.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import {SVGProps, useEffect} from 'react' 4 | 5 | import {useTheme} from '@/lib/use-theme' 6 | 7 | export function ThemeToggle() { 8 | const [darkMode, setDarkMode] = useTheme() 9 | 10 | useEffect(() => { 11 | const html = document.querySelector('html') 12 | html?.classList.toggle('dark', darkMode) 13 | }, [darkMode]) 14 | 15 | const toggleMode = () => { 16 | setDarkMode(!darkMode) 17 | } 18 | 19 | return ( 20 | 29 | ) 30 | } 31 | 32 | function SunIcon(props: SVGProps) { 33 | return ( 34 | 41 | ) 42 | } 43 | 44 | function MoonIcon(props: SVGProps) { 45 | return ( 46 | 49 | ) 50 | } 51 | -------------------------------------------------------------------------------- /components/account-profile.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import {register} from '@teamhanko/hanko-elements' 4 | import React, {useEffect} from 'react' 5 | 6 | import {assertString} from '@/lib/assert' 7 | 8 | const hankoApiUrl = process.env.NEXT_PUBLIC_HANKO_API_URL 9 | 10 | export default function AccountProfile() { 11 | assertString(hankoApiUrl, 'Hanko API URL is not defined.') 12 | 13 | useEffect(() => { 14 | // register the component 15 | // see: https://github.com/teamhanko/hanko/blob/main/frontend/elements/README.md#script 16 | register(hankoApiUrl) 17 | }, []) 18 | 19 | return 20 | } 21 | -------------------------------------------------------------------------------- /components/buttons/default-button.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx' 2 | import React, {ReactNode} from 'react' 3 | 4 | export function DefaultButton({ 5 | type, 6 | children, 7 | loading = false, 8 | }: { 9 | children: ReactNode 10 | loading?: boolean 11 | type?: 'submit' | 'button' 12 | }) { 13 | return ( 14 | 28 | ) 29 | } 30 | -------------------------------------------------------------------------------- /components/code.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | export function Code({html: html}: {html: string}) { 4 | return ( 5 |
    9 | ) 10 | } 11 | -------------------------------------------------------------------------------- /components/connect-package/connect-package-form.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import {useRouter} from 'next/navigation' 4 | import React, {useState} from 'react' 5 | 6 | import {jsonFetch} from '@/lib/json-fetch' 7 | import {FullPackage} from '@/server/db/packages/types' 8 | 9 | import {DefaultButton} from '../buttons/default-button' 10 | import {TextInput} from '../text-input' 11 | 12 | export default function ConnectPackageForm({package: pkg}: {package: FullPackage}) { 13 | const [loading, setLoading] = useState(false) 14 | const router = useRouter() 15 | const [apiKey, setApiKey] = useState('') 16 | 17 | const onSubmit = async (event: React.FormEvent) => { 18 | event.preventDefault() 19 | 20 | if (loading) { 21 | return 22 | } 23 | 24 | setLoading(true) 25 | 26 | const {error} = await jsonFetch(`/api/connections`, { 27 | method: 'POST', 28 | data: {api_key: apiKey, package_id: pkg.id}, 29 | }) 30 | 31 | if (error) { 32 | alert(error?.message) 33 | setLoading(false) 34 | return 35 | } 36 | 37 | router.push(`/account`) 38 | } 39 | 40 | return ( 41 |
    42 |
    43 |
    44 |
    45 |

    46 | Connect to {pkg.name} 47 |

    48 | 49 |
    50 |
    51 | 57 | 58 |
    59 | 65 |
    66 |
    67 |
    68 |
    69 |
    70 | 71 |
    72 |
    73 | 77 | Cancel 78 | 79 | 80 | Update package 81 |
    82 |
    83 |
    84 |
    85 | ) 86 | } 87 | -------------------------------------------------------------------------------- /components/error-boundary.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import {ReactElement, ReactNode} from 'react' 4 | import {ErrorBoundary as ErrorBoundaryBase} from 'react-error-boundary' 5 | 6 | export function ErrorBoundary({ 7 | children, 8 | fallback =
    Something went wrong
    , 9 | }: { 10 | children: ReactNode 11 | fallback?: ReactElement 12 | }) { 13 | return {children} 14 | } 15 | -------------------------------------------------------------------------------- /components/icons/logo-github.tsx: -------------------------------------------------------------------------------- 1 | export const LogoGithub: React.FC> = (props) => { 2 | return ( 3 | 4 | 8 | 9 | ) 10 | } 11 | -------------------------------------------------------------------------------- /components/json-viewer.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import ReactJson from 'react-json-view' 4 | 5 | export default function JsonViewer({json}: {json: any}) { 6 | return ( 7 | 18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /components/markdown/markdown-dynamic.tsx: -------------------------------------------------------------------------------- 1 | export {Markdown as MarkdownDynamic} from './markdown' 2 | -------------------------------------------------------------------------------- /components/markdown/markdown.tsx: -------------------------------------------------------------------------------- 1 | import DOMPurify from 'isomorphic-dompurify' 2 | import {marked} from 'marked' 3 | 4 | export function Markdown({text}: {text: string}) { 5 | return ( 6 |
    10 | ) 11 | } 12 | 13 | const generateMarkdown = (text: string) => { 14 | const html = marked.parse(text, {mangle: false, headerIds: false}) 15 | return DOMPurify.sanitize(html) 16 | } 17 | -------------------------------------------------------------------------------- /components/modal.tsx: -------------------------------------------------------------------------------- 1 | import {Dialog, Transition} from '@headlessui/react' 2 | import {Fragment} from 'react' 3 | 4 | export function Modal({ 5 | show, 6 | onClose, 7 | children, 8 | title, 9 | }: { 10 | show: boolean 11 | onClose: () => void 12 | children: React.ReactNode 13 | title: string 14 | }) { 15 | return ( 16 | 17 | 18 | 27 |
    28 | 29 | 30 |
    31 |
    32 | 41 | 42 | 46 | {title} 47 | 48 | 49 |
    {children}
    50 |
    51 |
    52 |
    53 |
    54 |
    55 |
    56 | ) 57 | } 58 | -------------------------------------------------------------------------------- /components/search-input.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import {useEffect, useRef, useState} from 'react' 4 | 5 | import {useKeyboardShortcut} from '@/lib/use-keyboard-shortcut' 6 | 7 | export function SearchInput(props: React.InputHTMLAttributes) { 8 | const [value, setValue] = useState('') 9 | const ref = useRef(null) 10 | 11 | // Add a cmd+k hotkey to focus the search input 12 | useKeyboardShortcut( 13 | 'k', 14 | () => { 15 | ref.current?.focus() 16 | }, 17 | {metaKey: true}, 18 | ) 19 | 20 | useEffect(() => { 21 | const uri = new URL(window.location.href) 22 | const search = uri.searchParams.get('q') 23 | 24 | if (search) { 25 | setValue(search) 26 | } 27 | }, []) 28 | 29 | return ( 30 |
    31 | setValue(event.target.value)} 38 | {...props} 39 | /> 40 |
    41 | ) 42 | } 43 | -------------------------------------------------------------------------------- /components/select/index.ts: -------------------------------------------------------------------------------- 1 | export * from './select' 2 | -------------------------------------------------------------------------------- /components/select/option.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import {Listbox} from '@headlessui/react' 4 | import clsx from 'clsx' 5 | import React from 'react' 6 | 7 | export interface OptionProps { 8 | label: string 9 | value: string 10 | } 11 | 12 | export const Option: React.FC = ({label, value}) => ( 13 | 14 | {({selected, active}) => ( 15 |
    25 | 26 | {label} 27 | 28 |
    29 | )} 30 |
    31 | ) 32 | -------------------------------------------------------------------------------- /components/select/select.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import {Listbox, Transition} from '@headlessui/react' 4 | import {ChevronUpDownIcon} from '@heroicons/react/20/solid' 5 | import React from 'react' 6 | 7 | import {Option} from './option' 8 | 9 | export interface SelectProps { 10 | label?: string 11 | selectedValue?: string 12 | onChange: (value: string) => void 13 | options: [value: string, label: string][] 14 | className?: string 15 | } 16 | 17 | export const Select: React.FC = ({ 18 | label, 19 | selectedValue, 20 | options, 21 | onChange, 22 | className = '', 23 | }) => { 24 | const selectedOption = options.find(([value]) => value === selectedValue) 25 | const [, selectedLabel] = selectedOption || [] 26 | 27 | return ( 28 | onChange(value)} 33 | > 34 | {({open}) => ( 35 | <> 36 | {label && ( 37 | 38 | {label} 39 | 40 | )} 41 | 42 |
    43 | 57 | {selectedLabel} 58 | 59 | 60 | 61 | 62 | 63 | 70 | 79 | {options.map(([value, label]) => ( 80 | 83 | 84 |
    85 | 86 | )} 87 |
    88 | ) 89 | } 90 | -------------------------------------------------------------------------------- /components/show-package/document-authentication.tsx: -------------------------------------------------------------------------------- 1 | import {OpenApiDocument} from '@/helpers/openapi/document' 2 | 3 | import {DocumentSecurityScheme} from './document-security-schema' 4 | 5 | export function DocumentAuthentication({document}: {document: OpenApiDocument}) { 6 | if (!document.hasAuthentication) { 7 | return null 8 | } 9 | 10 | return ( 11 |
    12 |

    13 | Authentication 14 |

    15 | 16 |
    17 | {Object.entries(document.securitySchemes).map(([name, scheme]) => ( 18 | 19 | ))} 20 |
    21 |
    22 | ) 23 | } 24 | -------------------------------------------------------------------------------- /components/show-package/document-endpoint-request-example-tabs.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import clsx from 'clsx' 4 | import {Fragment, useEffect, useState} from 'react' 5 | 6 | import {Code} from '@/components/code' 7 | import {TabGroup, TabList, TabPanel, TabPanels, Tab as BaseTab} from '@/components/tab' 8 | import {useMessageListener} from '@/lib/use-message' 9 | 10 | const namespace = 'example-language' 11 | 12 | export function DocumentEndpointRequestExampleTabs({ 13 | exampleCurlHtml, 14 | exampleJavaScriptHtml, 15 | examplePythonHtml, 16 | }: { 17 | exampleCurlHtml: string 18 | exampleJavaScriptHtml: string 19 | examplePythonHtml: string 20 | }) { 21 | const [selectedIndex, setSelectedIndex] = useState(0) 22 | 23 | const onTabChange = (index: number) => { 24 | setSelectedIndex(index) 25 | window.localStorage.setItem(namespace, String(index)) 26 | window.parent.postMessage({type: namespace, newValue: index}, window.location.origin) 27 | } 28 | 29 | useEffect(() => { 30 | const defaultSelectedIndex = window.localStorage.getItem(namespace) 31 | 32 | if (defaultSelectedIndex) { 33 | setSelectedIndex(Number(defaultSelectedIndex)) 34 | } 35 | }, []) 36 | 37 | useMessageListener((event) => { 38 | if (event.data.type === namespace) { 39 | setSelectedIndex(event.data.newValue) 40 | } 41 | }) 42 | 43 | return ( 44 | 45 |
    46 |
    47 |

    Request

    48 | 49 | 50 | cURL 51 | JavaScript 52 | Python 53 | 54 |
    55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 |
    68 |
    69 | ) 70 | } 71 | 72 | export function Tab({children}: {children: React.ReactNode}) { 73 | return ( 74 | 75 | {({selected}) => ( 76 | 85 | )} 86 | 87 | ) 88 | } 89 | -------------------------------------------------------------------------------- /components/show-package/document-endpoint-request-example.tsx: -------------------------------------------------------------------------------- 1 | 'use server' 2 | 3 | import {OpenApiRequestExample} from '@/helpers/openapi/request-example' 4 | import {safeHighlight} from '@/lib/code-highlighter' 5 | 6 | import {DocumentEndpointRequestExampleTabs} from './document-endpoint-request-example-tabs' 7 | 8 | export async function DocumentEndpointRequestExample({ 9 | requestExample, 10 | }: { 11 | requestExample: OpenApiRequestExample 12 | }) { 13 | const [exampleCurlHtml, exampleJavaScriptHtml, examplePythonHtml] = await Promise.all([ 14 | safeHighlight(requestExample.exampleCurl, 'shellscript'), 15 | safeHighlight(requestExample.exampleJavaScript, 'javascript'), 16 | safeHighlight(requestExample.examplePython, 'python'), 17 | ]) 18 | 19 | return ( 20 | 25 | ) 26 | } 27 | -------------------------------------------------------------------------------- /components/show-package/document-endpoint-response-example.tsx: -------------------------------------------------------------------------------- 1 | 'use server' 2 | 3 | import {Code} from '@/components/code' 4 | import {OpenApiResponseExample} from '@/helpers/openapi/response-example' 5 | import {safeHighlight} from '@/lib/code-highlighter' 6 | 7 | export async function DocumentEndpointResponseExample({ 8 | responseExample, 9 | }: { 10 | responseExample: OpenApiResponseExample 11 | }) { 12 | const exampleJson = responseExample.exampleJson 13 | 14 | if (!exampleJson) { 15 | return null 16 | } 17 | 18 | const exampleJsonHtml = await safeHighlight(exampleJson, 'json') 19 | 20 | return ( 21 |
    22 |
    23 |

    Response

    24 | 25 |
    26 | 27 |
    28 |
    29 | JSON 30 |
    31 |
    32 |
    33 | 34 |
    35 |
    36 | 37 |
    38 |
    39 |
    40 | ) 41 | } 42 | -------------------------------------------------------------------------------- /components/show-package/document-endpoints.tsx: -------------------------------------------------------------------------------- 1 | import startCase from 'lodash/startCase' 2 | 3 | import {OpenApiEndpoint} from '@/helpers/openapi/endpoint' 4 | 5 | import {DocumentEndpoint} from './document-endpoint' 6 | 7 | export function DocumentEndpoints({ 8 | groupedEndpoints, 9 | }: { 10 | groupedEndpoints: Map 11 | }) { 12 | return ( 13 |
    14 | {Array.from(groupedEndpoints).map(([group, endpoints]) => ( 15 |
    16 |

    17 | {startCase(group)} 18 |

    19 | 20 |
    21 | {endpoints.map((endpoint) => ( 22 | 23 | ))} 24 |
    25 |
    26 | ))} 27 |
    28 | ) 29 | } 30 | -------------------------------------------------------------------------------- /components/show-package/document-schema.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import {OpenApiSchema} from '@/helpers/openapi/schema' 4 | 5 | export const DocumentSchema: React.FC<{ 6 | schema: OpenApiSchema 7 | nestled: boolean 8 | }> = ({schema, nestled: _nested}) => { 9 | if (!schema.name) { 10 | return ( 11 | <> 12 | {schema.properties.length > 0 && ( 13 |
    14 | {schema.properties.map((property) => ( 15 | 16 | ))} 17 |
    18 | )} 19 | 20 | {schema.items && ( 21 |
    22 | 23 |
    24 | )} 25 | 26 | ) 27 | } 28 | 29 | return ( 30 |
    31 |
    32 | {schema.name && ( 33 |
    34 |
    Name
    35 |
    36 | 37 | {schema.name} 38 | 39 |
    40 |
    41 | )} 42 | 43 | {schema.type && ( 44 | <> 45 |
    Type
    46 |
    47 | {schema.type} 48 |
    49 | 50 | )} 51 | 52 | {schema.required &&
    Required
    } 53 | 54 | {schema.description && ( 55 | <> 56 |
    Description
    57 |
    {schema.description}
    58 | 59 | )} 60 |
    61 | 62 | {schema.properties.length > 0 && ( 63 |
    64 | {schema.properties.map((property) => ( 65 | 66 | ))} 67 |
    68 | )} 69 | 70 | {schema.items && ( 71 |
    72 | {schema.items && } 73 |
    74 | )} 75 |
    76 | ) 77 | } 78 | -------------------------------------------------------------------------------- /components/show-package/package-description.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link' 2 | 3 | import {cleanDescription} from '@/lib/description' 4 | 5 | export const PackageDescription = ({ 6 | packageId, 7 | description, 8 | maxLength, 9 | showMore = true, 10 | }: { 11 | packageId: string 12 | description: string 13 | showMore?: boolean 14 | maxLength?: number 15 | }) => { 16 | const cleanedDescription = cleanDescription(description) 17 | const willTruncate = maxLength ? cleanedDescription.length > maxLength : false 18 | const truncatedDescription = willTruncate 19 | ? cleanedDescription.substring(0, maxLength) + '...' 20 | : cleanedDescription 21 | 22 | return ( 23 |
    24 | {truncatedDescription} 25 | 26 | {willTruncate && showMore && ( 27 | 32 | read more 33 | 34 | )} 35 |
    36 | ) 37 | } 38 | -------------------------------------------------------------------------------- /components/show-package/package-header.tsx: -------------------------------------------------------------------------------- 1 | import {OpenApiDocument} from '@/helpers/openapi/document' 2 | import {FullPackage} from '@/server/db/packages/types' 3 | 4 | import {DocumentAuthentication} from './document-authentication' 5 | import {PackageInfo} from './package-info' 6 | import {PackageVersions} from './package-versions' 7 | 8 | export function PackageHeader({ 9 | package: pkg, 10 | document, 11 | }: { 12 | package: FullPackage 13 | document: OpenApiDocument 14 | }) { 15 | return ( 16 |
    17 | 18 | 19 | 20 | 21 | 22 |
    23 | ) 24 | } 25 | -------------------------------------------------------------------------------- /components/show-package/package-info.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link' 2 | 3 | import {OpenApiDocument} from '@/helpers/openapi/document' 4 | import {LitePackage} from '@/server/db/packages/types' 5 | 6 | import {PackageDescription} from './package-description' 7 | 8 | export function PackageInfo({ 9 | package: pkg, 10 | document, 11 | }: { 12 | package: LitePackage 13 | document: OpenApiDocument 14 | }) { 15 | return ( 16 |
    17 |
    18 |

    19 | {pkg.id} 20 |

    21 | 22 | {pkg.version !== document.version && ( 23 | <> 24 |
    25 | A newer version of this package 26 | 27 | is available 28 | 29 | . 30 |
    31 | 32 | )} 33 |
    34 | 35 |
    36 | {pkg.version !== document.version && ( 37 | <> 38 |
    Viewing version
    39 |
    40 | 44 | {document.version} 45 | 46 |
    47 | 48 | )} 49 | 50 |
    Latest version
    51 |
    52 | 56 | {pkg.version} 57 | 58 |
    59 | 60 |
    Domain
    61 | 66 | 67 |
    OpenAPI file
    68 | 69 |
    70 | 71 | HTML 72 | 73 | {' / '} 74 | 75 | JSON 76 | 77 | {' / '} 78 | 79 | YAML 80 | 81 |
    82 |
    83 | 84 | {pkg.description && ( 85 | 90 | )} 91 |
    92 | ) 93 | } 94 | -------------------------------------------------------------------------------- /components/show-package/package-main.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import {OpenApiDocument} from '@/helpers/openapi/document' 4 | import {OpenApiEndpoint} from '@/helpers/openapi/endpoint' 5 | import {FullPackage} from '@/server/db/packages/types' 6 | 7 | import {DocumentEndpoints} from './document-endpoints' 8 | import {PackageHeader} from './package-header' 9 | 10 | export const PackageMain: React.FC<{ 11 | package: FullPackage 12 | document: OpenApiDocument 13 | groupedEndpoints: Map 14 | }> = ({package: pkg, document, groupedEndpoints}) => { 15 | return ( 16 |
    17 | 18 | 19 |
    20 | ) 21 | } 22 | -------------------------------------------------------------------------------- /components/show-package/package-versions.tsx: -------------------------------------------------------------------------------- 1 | 'use server' 2 | 3 | import Link from 'next/link' 4 | 5 | import {getVersionsForPackageId} from '@/server/db/packages/getters' 6 | import {LitePackage} from '@/server/db/packages/types' 7 | 8 | export async function PackageVersions({package: pkg}: {package: LitePackage}) { 9 | const packageVersions = await getVersionsForPackageId(pkg.id) 10 | 11 | return ( 12 |
    13 |

    Versions

    14 | 15 |
    16 | {packageVersions.map((packageVersion) => ( 17 |
    21 | 26 | 27 |
    28 | 32 | {packageVersion.version} 33 | 34 |
    35 |
    36 | ))} 37 |
    38 |
    39 | ) 40 | } 41 | -------------------------------------------------------------------------------- /components/tab.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import dynamic from 'next/dynamic' 4 | 5 | const TabGroup = dynamic(() => import('@headlessui/react').then((mod) => mod.Tab.Group), { 6 | ssr: false, 7 | }) 8 | const TabList = dynamic(() => import('@headlessui/react').then((mod) => mod.Tab.List), { 9 | ssr: false, 10 | }) 11 | const TabPanels = dynamic( 12 | () => import('@headlessui/react').then((mod) => mod.Tab.Panels), 13 | {ssr: false}, 14 | ) 15 | const TabPanel = dynamic(() => import('@headlessui/react').then((mod) => mod.Tab.Panel), { 16 | ssr: false, 17 | }) 18 | const Tab = dynamic(() => import('@headlessui/react').then((mod) => mod.Tab), { 19 | ssr: false, 20 | }) 21 | 22 | export {TabGroup, TabList, TabPanels, TabPanel, Tab} 23 | -------------------------------------------------------------------------------- /components/text-input.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import clsx from 'clsx' 4 | import React from 'react' 5 | 6 | interface TextInputProps { 7 | name?: string 8 | value?: string 9 | onChange?: (value: string) => void 10 | onBlur?: () => void 11 | onFocus?: () => void 12 | required?: boolean 13 | autoComplete?: string 14 | pattern?: string 15 | error?: boolean 16 | autoFocus?: boolean 17 | disabled?: boolean 18 | placeholder?: string 19 | className?: string 20 | type?: string 21 | } 22 | 23 | export const TextInput: React.FC = ({ 24 | name, 25 | value, 26 | onChange, 27 | onBlur, 28 | onFocus, 29 | required, 30 | autoComplete, 31 | pattern, 32 | autoFocus, 33 | disabled, 34 | placeholder, 35 | className, 36 | type = 'text', 37 | error = false, 38 | }) => ( 39 | onChange?.(event.target.value)} 50 | onBlur={onBlur} 51 | onFocus={onFocus} 52 | className={clsx( 53 | ` 54 | block w-full rounded-md border border-pink-900/20 bg-white px-3 py-2 55 | text-sm text-black shadow-sm outline-none 56 | transition-all duration-300 focus:border-pink-300 57 | focus:ring focus:ring-pink-200/50 dark:border-gray-800 dark:bg-gray-900 58 | dark:text-white dark:focus:border-pink-600 dark:focus:ring-pink-900/50`, 59 | error && 60 | 'border-red-300 text-red-900 placeholder-red-300 focus:border-red-500 focus:outline-none focus:ring-red-500', 61 | className, 62 | )} 63 | /> 64 | ) 65 | -------------------------------------------------------------------------------- /components/textarea-input.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import clsx from 'clsx' 4 | import React from 'react' 5 | 6 | interface Props { 7 | name?: string 8 | value?: string 9 | onChange?: (value: string) => void 10 | required?: boolean 11 | autoComplete?: string 12 | error?: boolean 13 | placeholder?: string 14 | className?: string 15 | rows?: number 16 | } 17 | 18 | export const TextareaInput: React.FC = ({ 19 | name, 20 | value, 21 | onChange, 22 | required, 23 | autoComplete, 24 | placeholder, 25 | className, 26 | rows, 27 | error = false, 28 | }) => ( 29 |