Skip to content

Built for people who want to own their automations. Join the waitlist for an invite.

Package listing

@kody/producthunt

src/guide.ts

71 lines · 2.6 KB · TypeScript
import {
	DEFAULT_ACCESS_TOKEN_SECRET,
	DEFAULT_CLIENT_SECRET_SECRET,
	DEFAULT_INTEGRATION_NAME,
	DEFAULT_OAUTH_SCOPES,
	PRODUCTHUNT_APPLICATIONS_URL,
	PRODUCTHUNT_AUTHORIZE_URL,
	PRODUCTHUNT_CALLBACK_URL,
	PRODUCTHUNT_GRAPHQL_URL,
	PRODUCTHUNT_REQUIRED_HOST,
	PRODUCTHUNT_TOKEN_URL,
	accessTokenSetupUrl,
	clientSecretSetupUrl,
	oauthConnectUrl,
	reconnectUrl,
} from './setup.ts'
import { optionalString, requireRecord } from './types.ts'

export const DEFAULT_KODY_ORIGIN = 'https://kody.codes'

export function getConnectUrl(origin = DEFAULT_KODY_ORIGIN) {
	if (origin === DEFAULT_KODY_ORIGIN) return oauthConnectUrl()
	const params = new URLSearchParams({
		provider: DEFAULT_INTEGRATION_NAME,
		authorizeUrl: PRODUCTHUNT_AUTHORIZE_URL,
		tokenUrl: PRODUCTHUNT_TOKEN_URL,
		apiBaseUrl: PRODUCTHUNT_GRAPHQL_URL,
		flow: 'confidential',
		allowedHosts: PRODUCTHUNT_REQUIRED_HOST,
		dashboardUrl: PRODUCTHUNT_APPLICATIONS_URL,
		scopes: DEFAULT_OAUTH_SCOPES.join(' '),
	})
	return `${origin.replace(/\/$/, '')}/connect/oauth?${params.toString()}`
}

export function getSetupGuide(origin = DEFAULT_KODY_ORIGIN) {
	return {
		integration: DEFAULT_INTEGRATION_NAME,
		connectUrl: getConnectUrl(origin),
		reconnectUrl: origin === DEFAULT_KODY_ORIGIN ? reconnectUrl() : `${origin.replace(/\/$/, '')}/connect/oauth?provider=producthunt`,
		redirectUri: `${origin.replace(/\/$/, '')}/connect/oauth`,
		callbackUrl: PRODUCTHUNT_CALLBACK_URL,
		applicationUrl: PRODUCTHUNT_APPLICATIONS_URL,
		clientType: 'confidential',
		scopes: [...DEFAULT_OAUTH_SCOPES],
		requiredSecrets: [DEFAULT_ACCESS_TOKEN_SECRET, DEFAULT_CLIENT_SECRET_SECRET],
		accessTokenUrl: accessTokenSetupUrl(),
		clientSecretUrl: clientSecretSetupUrl(),
		recommendedApprovedHosts: [PRODUCTHUNT_REQUIRED_HOST],
		notes: [
			'Choose Confidential when creating the Product Hunt app. Kody stores the client secret server-side.',
			'Register the redirect URI exactly as shown. A truncated URI will fail the callback.',
			'Private scope is required for viewer.user. Public is enough for featured posts and comments.',
			'The public API can list comments but does not expose a comment-create mutation.',
			'Write mutations (goals, follow) need Product Hunt app approval plus the write scope, then confirm: true.',
		],
	}
}

/**
 * Return Product Hunt OAuth and developer-token setup guidance.
 * @example
 * import guide from 'kody:@kody/producthunt/guide'
 * const info = await guide()
 */
export default async function guideEntrypoint(
	params: { origin?: string } & Record<string, unknown> = {},
) {
	const input = requireRecord(params, 'guide')
	return getSetupGuide(optionalString(input.origin, 'origin') ?? DEFAULT_KODY_ORIGIN)
}