Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
3bf1cde
add profile lexicon + scope, add profile button to AuthModal, add pro…
zeucapua Feb 6, 2026
09c99c1
Like endpoint
fatfingers23 Feb 7, 2026
ecdc8cf
Merge pull request #4 from fatfingers23/feat/profile-page
zeucapua Feb 7, 2026
9591998
init useProfileLikes composable, add likes grid to profile page
zeucapua Feb 10, 2026
c467c3e
create and implement BasicCard for likes grid
zeucapua Feb 10, 2026
dfc4d7d
rename BasicCard to LikeCard, copy paste like button to card
zeucapua Feb 12, 2026
58a3b0c
implement like update
zeucapua Feb 12, 2026
0215014
add website to profile
zeucapua Feb 12, 2026
30741b8
working update profile endpoint, util, and cache update
zeucapua Feb 26, 2026
6e5be0e
style edit buttons
zeucapua Feb 26, 2026
45d35ef
Merge branch 'main' into feat/profile-page
zeucapua Feb 26, 2026
c012a45
fix merge, change likes cols to 2
zeucapua Feb 27, 2026
b81eda7
add TODOs, use LinkBase for profile website
zeucapua Feb 27, 2026
f732b14
fix: use resolved handle from profile in OAuth callback
danielroe Feb 27, 2026
73ce1df
fix: remove duplicate and unused imports in oauth.ts
danielroe Feb 27, 2026
78db755
fix(ui): check isEditing.value instead of ref object in watchEffect
danielroe Feb 27, 2026
d72a756
fix(ui): pass handle.value instead of ComputedRef to updateProfile
danielroe Feb 27, 2026
2e1bbd7
fix(ui): handle non-matching URLs in extractPackageFromRef
danielroe Feb 27, 2026
6e41ecc
fix: use DID consistently for profile cache keys
danielroe Feb 27, 2026
7f0c987
fix: validate identifier param in likes endpoint
danielroe Feb 27, 2026
6c49480
fix(ui): remove undefined prefetch reference in AuthModal
danielroe Feb 27, 2026
fe73fea
fix(ui): remove unused imports, duplicate type, and fix title binding
danielroe Feb 27, 2026
75db4ae
fix(i18n): extract hardcoded strings to locale file
danielroe Feb 27, 2026
8dfc2d9
Merge remote-tracking branch 'origin/main' into feat/profile-page
danielroe Feb 27, 2026
5f3874c
fix(a11y): remove nested button inside NuxtLink in AuthModal
danielroe Feb 27, 2026
b5bf1f2
fix(ui): remove invalid second argument from format() call
danielroe Feb 27, 2026
a3b6cc0
fix(ui): fix useRoute argument, useFetch types, and null safety
danielroe Feb 27, 2026
1bb55b8
fix: replace unlisted @atproto/syntax import and fix typo in identity.ts
danielroe Feb 27, 2026
e2e027d
fix: add response.ok check in slingshotMiniDoc fetch
danielroe Feb 27, 2026
7bde6b7
fix: add validation constraints to ProfileEditBodySchema
danielroe Feb 27, 2026
b2ecd2c
fix: add fetch timeout and encode URI in getNpmxProfile
danielroe Feb 27, 2026
0804679
fix(a11y): add LikeCard a11y test and exclude lexicons from knip
danielroe Feb 27, 2026
2cbf1d9
fix(ui): fix likesData type errors and only close editor on success
danielroe Feb 27, 2026
c4ab87d
fix(ui): add website label/placeholder, fix empty string validation, …
danielroe Feb 27, 2026
cdb891b
fix(ui): handle 404 for non-existing profiles
danielroe Feb 27, 2026
91b24a9
fix: use linkbase/buttonbase
danielroe Feb 27, 2026
e5d1638
fix(ui): SSR likes data and add skeleton placeholders to prevent layo…
danielroe Feb 27, 2026
8f19abe
fix: ssr likes
danielroe Feb 27, 2026
0bf4443
fix(ui): reserve space for edit button to prevent layout shift
danielroe Feb 27, 2026
14556af
fix(ui): remove duplicate group class from LikeCard
danielroe Feb 27, 2026
8bf70eb
Merge remote-tracking branch 'origin/main' into feat/profile-page
danielroe Feb 27, 2026
981b70b
moved some fetches to use the xrpc client
fatfingers23 Feb 27, 2026
81402ee
should be the best to tell if an identity is there or not
fatfingers23 Feb 27, 2026
a834d7a
tell the homies about npmx
fatfingers23 Feb 27, 2026
a114bf0
Update i18n/locales/en.json
zeucapua Feb 27, 2026
5676cf6
Update lunaria/files/en-US.json
zeucapua Feb 27, 2026
e5b9bbd
Merge pull request #5 from fatfingers23/feat/profile-page
zeucapua Feb 27, 2026
c89de62
[autofix.ci] apply automated fixes
autofix-ci[bot] Feb 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 23 additions & 6 deletions app/components/Header/AuthModal.client.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
import { useAtproto } from '~/composables/atproto/useAtproto'
import { authRedirect } from '~/utils/atproto/helpers'

const authModal = useModal('auth-modal')

const handleInput = shallowRef('')
const route = useRoute()
const { user, logout } = useAtproto()
Expand Down Expand Up @@ -33,12 +35,27 @@
</p>
</div>
</div>
<button
class="w-full px-4 py-2 font-mono text-sm text-fg-muted bg-bg-subtle border border-border rounded-md transition-colors duration-200 hover:text-fg hover:border-border-hover focus-visible:outline-accent/70"
@click="logout"
>
{{ $t('auth.modal.disconnect') }}
</button>

<div class="flex flex-col space-y-4">
<NuxtLink
:to="{ name: 'profile-handle', params: { handle: user.handle } }"
:prefetch-on="prefetch ? 'visibility' : 'interaction'"

Check failure on line 42 in app/components/Header/AuthModal.client.vue

View workflow job for this annotation

GitHub Actions / 💪 Type check

Property 'prefetch' does not exist on type '{ user: Simplify<SerializeObject<{ did: string; handle: string; pds: string; avatar?: string | undefined; }> | null> | undefined; authModal: { open: () => void; close: () => void; }; ... 662 more ...; $nuxtSiteConfig: any; }'.
>
<button
class="w-full px-4 py-2 font-mono text-sm text-fg-muted bg-bg-subtle border border-border rounded-md transition-colors duration-200 hover:text-fg hover:border-border-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50"
@click="authModal.close()"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# First, let's read the entire file to understand the component structure
wc -l app/components/Header/AuthModal.client.vue

Repository: npmx-dev/npmx.dev

Length of output: 107


🏁 Script executed:

# Read the file to see the full context, focusing on the lines around 79-85
cat -n app/components/Header/AuthModal.client.vue | head -120 | tail -50

Repository: npmx-dev/npmx.dev

Length of output: 2297


🏁 Script executed:

# Search for prefetch definition in the file
rg -n "prefetch" app/components/Header/AuthModal.client.vue

Repository: npmx-dev/npmx.dev

Length of output: 128


🏁 Script executed:

# Search for NuxtLink and button elements to understand the structure
rg -n "NuxtLink|<button" app/components/Header/AuthModal.client.vue -B2 -A8

Repository: npmx-dev/npmx.dev

Length of output: 1126


🏁 Script executed:

# Read the script section to check if prefetch is defined
cat -n app/components/Header/AuthModal.client.vue | head -70

Repository: npmx-dev/npmx.dev

Length of output: 2672


🏁 Script executed:

# Search for all variable/prop definitions
rg -n "prefetch|defineProps|const prefetch|ref\(|computed\(" app/components/Header/AuthModal.client.vue

Repository: npmx-dev/npmx.dev

Length of output: 128


🏁 Script executed:

# Check if there are other uses of prefetch-on in the codebase to understand the pattern
rg -n "prefetch-on" app/ --type vue

Repository: npmx-dev/npmx.dev

Length of output: 88


🏁 Script executed:

# Check other NuxtLink usages to see if they apply classes and click handlers
rg -n "NuxtLink" app/components/ -A3 | head -60

Repository: npmx-dev/npmx.dev

Length of output: 4126


Remove undefined variable and fix invalid interactive element nesting.

The prefetch variable is not defined in the component (lines 1–63 of the script section), causing a type-check failure. Additionally, nesting a <button> inside <NuxtLink> violates HTML semantics for interactive elements. Apply styles and event handlers directly to NuxtLink instead:

Proposed fix
-        <NuxtLink
-          :to="{ name: 'profile-handle', params: { handle: user.handle } }"
-          :prefetch-on="prefetch ? 'visibility' : 'interaction'"
-        >
-          <button
-            class="w-full px-4 py-2 font-mono text-sm text-fg-muted bg-bg-subtle border border-border rounded-md transition-colors duration-200 hover:text-fg hover:border-border-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50"
-            `@click`="authModal.close()"
-          >
-            Profile
-          </button>
-        </NuxtLink>
+        <NuxtLink
+          :to="{ name: 'profile-handle', params: { handle: user.handle } }"
+          class="block w-full px-4 py-2 font-mono text-sm text-fg-muted bg-bg-subtle border border-border rounded-md transition-colors duration-200 hover:text-fg hover:border-border-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50"
+          `@click`="authModal.close()"
+        >
+          Profile
+        </NuxtLink>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<NuxtLink
:to="{ name: 'profile-handle', params: { handle: user.handle } }"
:prefetch-on="prefetch ? 'visibility' : 'interaction'"
>
<button
class="w-full px-4 py-2 font-mono text-sm text-fg-muted bg-bg-subtle border border-border rounded-md transition-colors duration-200 hover:text-fg hover:border-border-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50"
@click="authModal.close()"
<NuxtLink
:to="{ name: 'profile-handle', params: { handle: user.handle } }"
class="block w-full px-4 py-2 font-mono text-sm text-fg-muted bg-bg-subtle border border-border rounded-md transition-colors duration-200 hover:text-fg hover:border-border-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50"
`@click`="authModal.close()"
>
Profile
</NuxtLink>
🧰 Tools
🪛 GitHub Check: 💪 Type check

[failure] 81-81:
Property 'prefetch' does not exist on type '{ user: Simplify<SerializeObject<{ did: string; handle: string; pds: string; avatar?: string | undefined; relogin?: boolean | undefined; }> | null> | undefined; ... 721 more ...; $npmApi: (url: string, options?: NitroFetchOptions<...> | undefined, ttl?: number | undefined) => Promise<...>; }'.

>
Profile
</button>
</NuxtLink>

<button
class="w-full px-4 py-2 font-mono text-sm text-fg-muted bg-bg-subtle border border-border rounded-md transition-colors duration-200 hover:text-fg hover:border-border-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50"
@click="logout"
>
{{ $t('auth.modal.disconnect') }}
</button>
</div>
</div>

<!-- Disconnected state -->
Expand Down
55 changes: 55 additions & 0 deletions app/pages/profile/[handle]/index.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<script setup lang="ts">
import { debounce } from 'perfect-debounce'

Check failure on line 2 in app/pages/profile/[handle]/index.vue

View workflow job for this annotation

GitHub Actions / 💪 Type check

'debounce' is declared but its value is never read.
import { normalizeSearchParam } from '#shared/utils/url'

Check failure on line 3 in app/pages/profile/[handle]/index.vue

View workflow job for this annotation

GitHub Actions / 💪 Type check

'normalizeSearchParam' is declared but its value is never read.

const route = useRoute('/profile/[handle]')

Check failure on line 5 in app/pages/profile/[handle]/index.vue

View workflow job for this annotation

GitHub Actions / 💪 Type check

Argument of type '"/profile/[handle]"' is not assignable to parameter of type 'keyof RouteNamedMap | undefined'.
const router = useRouter()

Check failure on line 6 in app/pages/profile/[handle]/index.vue

View workflow job for this annotation

GitHub Actions / 💪 Type check

'router' is declared but its value is never read.

const handle = computed(() => route.params.handle)

Check failure on line 8 in app/pages/profile/[handle]/index.vue

View workflow job for this annotation

GitHub Actions / 💪 Type check

Property 'handle' does not exist on type 'Record<never, never> | { org?: string | undefined; name: string; } | { path: ParamValueOneOrMore<false>; } | { org: string; } | { path: ParamValueOneOrMore<false>; } | { ...; } | { ...; } | { ...; } | { ...; }'.

const { data: profile }: { data?: NPMXProfile } = useFetch(

Check failure on line 10 in app/pages/profile/[handle]/index.vue

View workflow job for this annotation

GitHub Actions / 💪 Type check

Type 'AsyncData<Simplify<SerializeObject<NPMXProfile> | undefined> | { profile: { displayName: any; }; }, FetchError<any> | undefined>' is not assignable to type '{ data?: NPMXProfile | undefined; }'.
() => `/api/social/profile/${handle.value}`,
{
default: () => ({ profile: { displayName: handle.value } }),
server: false,
},
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
)

useSeoMeta({
title: () => `${handle.value} - npmx`,
description: () => `npmx profile by ${handle.value}`,
})

/**
defineOgImageComponent('Default', {
title: () => `~${username.value}`,
description: () => (results.value ? `${results.value.total} packages` : 'npm user profile'),
primaryColor: '#60a5fa',
})
**/
</script>

<template>
<main class="container flex-1 flex flex-col py-8 sm:py-12 w-full">
<!-- Header -->
<header class="mb-8 pb-8 border-b border-border">
<div class="flex flex-wrap items-center gap-4">
<div>
<h1 class="font-mono text-2xl sm:text-3xl font-medium">{{ profile.displayName }}</h1>

Check failure on line 38 in app/pages/profile/[handle]/index.vue

View workflow job for this annotation

GitHub Actions / 💪 Type check

'__VLS_ctx.profile' is possibly 'undefined'.
<h2>@{{ handle }}</h2>
<p v-if="profile.description">{{ profile.description }}</p>

Check failure on line 40 in app/pages/profile/[handle]/index.vue

View workflow job for this annotation

GitHub Actions / 💪 Type check

'__VLS_ctx.profile' is possibly 'undefined'.

Check failure on line 40 in app/pages/profile/[handle]/index.vue

View workflow job for this annotation

GitHub Actions / 💪 Type check

'__VLS_ctx.profile' is possibly 'undefined'.
</div>
</div>
</header>

<!-- Empty state (no packages found for user) -->
<div class="flex-1 flex items-center justify-center">
<div class="text-center">
<p class="text-fg-muted font-mono">
{{ $t('user.page.no_packages') }} <span class="text-fg">~{{ handle }}</span>
</p>
<p class="text-fg-subtle text-sm mt-2">{{ $t('user.page.no_packages_hint') }}</p>
</div>
</div>
</main>
</template>
37 changes: 37 additions & 0 deletions lexicons/dev/npmx/actor/profile.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
{
"lexicon": 1,
"id": "dev.npmx.actor.profile",
"defs": {
"main": {
"key": "literal:self",
"type": "record",
"record": {
"type": "object",
"properties": {
"avatar": {
"type": "blob",
"accept": ["image/png", "image/jpeg"],
"maxSize": 1000000,
"description": "AKA, 'profile picture'"
},
"website": {
"type": "string",
"format": "uri"
},
"description": {
"type": "string",
"maxLength": 2560,
"description": "Free-form profile description text.",
"maxGraphemes": 256
},
"displayName": {
"type": "string",
"maxLength": 640,
"maxGraphemes": 64
}
}
},
"description": "A declaration of an npmx account profile."
}
}
}
56 changes: 46 additions & 10 deletions server/api/auth/atproto.get.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,21 +119,57 @@ export default defineEventHandler(async event => {
const agent = new Agent(authSession)
event.context.agent = agent

const response = await fetch(
`https://${SLINGSHOT_HOST}/xrpc/com.bad-example.identity.resolveMiniDoc?identifier=${agent.did}`,
const miniDocResponse = await fetch(
`https://${SLINGSHOT_HOST}/xrpc/blue.microcosm.identity.resolveMiniDoc?identifier=${agent.did}`,
{ headers: { 'User-Agent': 'npmx' } },
)
if (response.ok) {
const miniDoc: PublicUserSession = await response.json()

if (miniDocResponse.ok) {
const miniDoc: PublicUserSession = await miniDocResponse.json()

let avatar: string | undefined = await getAvatar(authSession.did, miniDoc.pds)

await session.update({
public: {
...miniDoc,
avatar,
},
})
// get existing npmx profile OR create a new one
const profileUri = `at://${agent.did}/dev.npmx.actor.profile/self`
const profileResponse = await fetch(
`https://${SLINGSHOT_HOST}/xrpc/blue.microcosm.repo.getRecordByUri?at_uri=${profileUri}`,
{ headers: { 'User-Agent': 'npmx' } },
)

if (profileResponse.ok) {
const profile = await profileResponse.json()
await session.update({
public: {
...miniDoc,
avatar,
},
profile: profile.value,
})
} else {
const profile = {
website: '',
displayName: miniDoc.handle,
description: '',
}

await agent.com.atproto.repo.createRecord({
repo: miniDoc.handle,
collection: 'dev.npmx.actor.profile',
rkey: 'self',
record: {
$type: 'dev.npmx.actor.profile',
...profile,
},
})

await session.update({
public: {
...miniDoc,
avatar,
},
profile: profile,
})
}
} else {
//If slingshot fails we still want to set some key info we need.
const pdsBase = (await authSession.getTokenInfo()).aud
Expand Down
14 changes: 14 additions & 0 deletions server/api/social/profile/[...handle].get.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
export default defineEventHandler(async event => {
const handle = getRouterParam(event, 'handle')
if (!handle) {
throw createError({
status: 400,
message: 'handle not provided',
})
}

const profileUtil = new ProfileUtils()
const profile = await profileUtil.getProfile(handle)
console.log('ENDPOINT', { handle, profile })
return profile
})
4 changes: 2 additions & 2 deletions server/utils/atproto/oauth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@ import { NodeOAuthClient, AtprotoDohHandleResolver } from '@atproto/oauth-client
import { parse } from 'valibot'
import { getOAuthLock } from '#server/utils/atproto/lock'
import { useOAuthStorage } from '#server/utils/atproto/storage'
import { LIKES_SCOPE } from '#shared/utils/constants'
import { LIKES_SCOPE, PROFILE_SCOPE } from '#shared/utils/constants'
import { OAuthMetadataSchema } from '#shared/schemas/oauth'
// @ts-expect-error virtual file from oauth module
import { clientUri } from '#oauth/config'
// TODO: If you add writing a new record you will need to add a scope for it
export const scope = `atproto ${LIKES_SCOPE}`
export const scope = `atproto ${LIKES_SCOPE} ${PROFILE_SCOPE}`

/**
* Resolves a did to a handle via DoH or via the http website calls
Expand Down
85 changes: 85 additions & 0 deletions server/utils/atproto/utils/profile.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import type { MiniDoc, NPMXProfile } from '~~/shared/types/social'

//Cache keys and helpers
const CACHE_PREFIX = 'atproto-profile:'
const CACHE_PROFILE_MINI_DOC = (handle: string) => `${CACHE_PREFIX}${handle}:minidoc`
const CACHE_PROFILE_KEY = (did: string) => `${CACHE_PREFIX}${did}:profile`

const CACHE_MAX_AGE = CACHE_MAX_AGE_ONE_MINUTE * 5

/**
* Logic to handle and update profile queries
*/
export class ProfileUtils {
private readonly constellation: Constellation
private readonly cache: CacheAdapter

constructor() {
this.constellation = new Constellation(
// Passes in a fetch wrapped as cachedfetch since are already doing some heavy caching here
async <T = unknown>(
url: string,
options: Parameters<typeof $fetch>[1] = {},
_ttl?: number,
): Promise<CachedFetchResult<T>> => {
const data = (await $fetch<T>(url, options)) as T
return { data, isStale: false, cachedAt: null }
},
)
this.cache = getCacheAdapter('generic')
}

private async slingshotMiniDoc(handle: string) {
const miniDocKey = CACHE_PROFILE_MINI_DOC(handle)
const cachedMiniDoc = await this.cache.get<MiniDoc>(miniDocKey)

let miniDoc
if (cachedMiniDoc) {
miniDoc = cachedMiniDoc
} else {
const resolveUrl = `https://${SLINGSHOT_HOST}/xrpc/blue.microcosm.identity.resolveMiniDoc?identifier=${encodeURIComponent(handle)}`
Comment thread
zeucapua marked this conversation as resolved.
Outdated
console.log({ resolveUrl })
const response = await fetch(resolveUrl, {
headers: { 'User-Agent': 'npmx' },
})
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's read the file to examine lines 30-32 and 63-74
cat -n server/utils/atproto/utils/profile.ts | sed -n '20,85p'

Repository: npmx-dev/npmx.dev

Length of output: 2621


🏁 Script executed:

# Check for existing timeout patterns in the codebase
rg -n 'AbortSignal\.timeout|signal:.*timeout' --type=ts -C 2 | head -50

Repository: npmx-dev/npmx.dev

Length of output: 820


🏁 Script executed:

# Verify the encoding pattern used elsewhere in the codebase
rg -n 'encodeURIComponent' --type=ts | head -20

Repository: npmx-dev/npmx.dev

Length of output: 2663


Harden Slingshot fetches: add timeouts and distinguish missing profile from upstream failure.

Lines 30–32 and 63–68 lack timeout signals, and lines 69–74 silently swallow non-OK responses without distinguishing a missing profile (404) from upstream failures. The first fetch (line 33) correctly throws on error, but the second does not—violating consistent error handling. Timeouts and encoding patterns are already established elsewhere in the codebase (e.g., server/api/auth/atproto.get.ts line 296 applies both fixes to the same Slingshot endpoint).

🛠️ Proposed fix
       const response = await fetch(resolveUrl, {
         headers: { 'User-Agent': 'npmx' },
+        signal: AbortSignal.timeout(5_000),
       })
       if (!response.ok) {
         throw createError({
           status: response.status,
           message: `Failed to resolve MiniDoc for ${handle}`,
         })
       }
@@
       const profileUri = `at://${miniDoc.did}/dev.npmx.actor.profile/self`
       const response = await fetch(
-        `https://${SLINGSHOT_HOST}/xrpc/blue.microcosm.repo.getRecordByUri?at_uri=${profileUri}`,
+        `https://${SLINGSHOT_HOST}/xrpc/blue.microcosm.repo.getRecordByUri?at_uri=${encodeURIComponent(profileUri)}`,
         {
           headers: { 'User-Agent': 'npmx' },
+          signal: AbortSignal.timeout(5_000),
         },
       )
       if (response.ok) {
         const { value } = (await response.json()) as { value: NPMXProfile }
         profile = value
         await this.cache.set(profileKey, profile, CACHE_MAX_AGE)
+      } else if (response.status !== 404) {
+        throw createError({
+          status: response.status,
+          message: `Failed to fetch profile for DID ${miniDoc.did}`,
+        })
       }

const value = (await response.json()) as MiniDoc

miniDoc = value
await this.cache.set(miniDocKey, value, CACHE_MAX_AGE)
}
console.log({ miniDoc })

return miniDoc
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

/**
* Gets an npmx profile based on a handle
* @param handle
* @returns
*/
async getProfile(handle: string) {
const profileKey = CACHE_PROFILE_KEY(handle)
const cachedProfile = await this.cache.get<NPMXProfile>(profileKey)

let profile: NPMXProfile | undefined
if (cachedProfile) {
profile = cachedProfile
} else {
const miniDoc = await this.slingshotMiniDoc(handle)
const profileUri = `at://${miniDoc.did}/dev.npmx.actor.profile/self`
const response = await fetch(
`https://${SLINGSHOT_HOST}/xrpc/blue.microcosm.repo.getRecordByUri?at_uri=${profileUri}`,
{
headers: { 'User-Agent': 'npmx' },
},
)
if (response.ok) {
const { value } = (await response.json()) as { value: NPMXProfile }
profile = value
await this.cache.set(profileKey, profile, CACHE_MAX_AGE)
}
}

return profile
}
}
21 changes: 21 additions & 0 deletions shared/types/social.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,24 @@ export type PackageLikes = {
// If the logged in user has liked the package, false if not logged in
userHasLiked: boolean
}

/**
* A shortened DID Doc for AT Protocol accounts
* Returned by Slingshot's `/xrpc/blue.microcosm.identity.resolveMiniDoc` endpoint
*/
export type MiniDoc = {
did: string
handle: string
pds: string
signing_key: string
}

/**
* NPMX Profile details
* TODO: avatar
*/
export type NPMXProfile = {
displayName: string
website?: string
description?: string
}
5 changes: 5 additions & 0 deletions shared/types/userSession.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ export interface UserServerSession {
pds: string
avatar?: string
}
profile: {
Comment thread
fatfingers23 marked this conversation as resolved.
website?: string
description?: string
displayName?: string
}
// Only to be used in the atproto session and state stores
// Will need to change to Record<string, T> and add a current logged in user if we ever want to support
// multiple did logins per server session
Expand Down
1 change: 1 addition & 0 deletions shared/utils/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export const PACKAGE_SUBJECT_REF = (packageName: string) =>
`https://npmx.dev/package/${packageName}`
// OAuth scopes as we add new ones we need to check these on certain actions. If not redirect the user to login again to upgrade the scopes
export const LIKES_SCOPE = `repo:${dev.npmx.feed.like.$nsid}`
export const PROFILE_SCOPE = `repo:${dev.npmx.actor.profile.$nsid}`

// Theming
export const ACCENT_COLORS = {
Expand Down
Loading