├── .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 |
39 | Revoke
40 |
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 |
14 | {apiKeys.map((apiKey) => (
15 |
16 | ))}
17 |
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 |
49 | Revoke
50 |
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 |
13 | {connections.map((conn) => (
14 |
15 | ))}
16 |
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 |
33 | New API key...
34 |
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 |
61 | Close
62 |
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 |
13 | {packages.map((pkg) => (
14 |
15 |
16 | Id
17 |
18 |
23 | {pkg.id}
24 |
25 |
26 | Version
27 | {pkg.version}
28 | Actions
29 |
30 |
35 | Edit
36 |
37 |
38 |
39 |
40 | ))}
41 |
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 |
24 |
25 |
26 |
27 |
28 |
29 |
Account
30 |
31 |
32 |
35 |
36 |
39 |
40 |
43 |
44 |
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 |
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 |
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 |
21 | Read ChatGPT's{' '}
22 |
23 | official docs
24 |
25 | .
26 |
27 |
28 | See example OpenAPI files, and{' '}
29 | learn how to generate your own .
30 |
31 |
32 | Clone our{' '}
33 |
34 | Cloudflare worker
35 | {' '}
36 | to easily boot up your own AI plugin.
37 |
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 |
49 | Read OpenAI's official docs on{' '}
50 |
51 | function calling
52 |
53 | .
54 |
55 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
35 | published {formatDistanceToNow(publishedAt)}
36 |
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 | onChange(p)}
25 | type="button"
26 | className={clsx(
27 | `rounded-md border border-pink-500/30 px-4 py-2 text-sm font-medium focus:outline-none focus:ring-2 focus:ring-pink-500 focus:ring-offset-2`,
28 | p === page
29 | ? 'bg-pink-500 text-white hover:bg-pink-500/90'
30 | : 'bg-white text-pink-500 hover:bg-pink-500/20',
31 | )}
32 | >
33 | {p}
34 |
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 |
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 |
21 |
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 |
26 |
27 |
28 |
29 | )
30 | }
31 |
32 | function SunIcon(props: SVGProps) {
33 | return (
34 |
35 |
36 |
40 |
41 | )
42 | }
43 |
44 | function MoonIcon(props: SVGProps) {
45 | return (
46 |
47 |
48 |
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 |
26 | {children}
27 |
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 |
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 |
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 |
81 | ))}
82 |
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 |
83 | {children}
84 |
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 |
32 |
33 |
34 |
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 |
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 |
22 | {packageVersion.created_at.toLocaleDateString(undefined, {
23 | dateStyle: 'medium',
24 | })}
25 |
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 |