Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
10 changes: 10 additions & 0 deletions app/components/global/BlogPostWrapper.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<script setup lang="ts">
import type { BlogPostFrontmatter } from '#shared/schemas/blog'
import { generateBlogTID } from '#shared/utils/atproto'
Copy link
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:

cat -n app/components/global/BlogPostWrapper.vue | head -30

Repository: npmx-dev/npmx.dev

Length of output: 1162


🏁 Script executed:

fd constants -type f -path "*/shared/*" | head -20

Repository: npmx-dev/npmx.dev

Length of output: 231


🏁 Script executed:

rg "NPMX_DEV_DID" --type ts --type js -B 2 -A 2

Repository: npmx-dev/npmx.dev

Length of output: 1638


Import NPMX_DEV_DID before using it on line 22.

The constant is used in the useHead block but not imported, which will fail type-checking and the build.

Proposed fix
 import type { BlogPostFrontmatter } from '#shared/schemas/blog'
 import { generateBlogTID } from '#shared/utils/atproto'
+import { NPMX_DEV_DID } from '#shared/utils/constants'
📝 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
import { generateBlogTID } from '#shared/utils/atproto'
import type { BlogPostFrontmatter } from '#shared/schemas/blog'
import { generateBlogTID } from '#shared/utils/atproto'
import { NPMX_DEV_DID } from '#shared/utils/constants'


const props = defineProps<{
frontmatter: BlogPostFrontmatter
Expand All @@ -14,6 +15,15 @@ useSeoMeta({
...(props.frontmatter.draft ? { robots: 'noindex, nofollow' } : {}),
Copy link
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:

cd app/components/global && wc -l BlogPostWrapper.vue

Repository: npmx-dev/npmx.dev

Length of output: 83


🏁 Script executed:

cd app/components/global && cat -n BlogPostWrapper.vue | sed -n '1,50p'

Repository: npmx-dev/npmx.dev

Length of output: 2026


🏁 Script executed:

cd app/components/global && grep -r "site.standard.document" --include="*.ts" --include="*.tsx" --include="*.vue" --include="*.js" | head -20

Repository: npmx-dev/npmx.dev

Length of output: 263


🏁 Script executed:

grep -r "NPMX_DEV_DID" --include="*.ts" --include="*.tsx" --include="*.vue" --include="*.js" | head -10

Repository: npmx-dev/npmx.dev

Length of output: 670


🏁 Script executed:

grep -r "generateBlogTID" --include="*.ts" --include="*.tsx" --include="*.vue" --include="*.js" -A 3 | head -30

Repository: npmx-dev/npmx.dev

Length of output: 1466


🏁 Script executed:

cat app/modules/standard-site-sync.ts

Repository: npmx-dev/npmx.dev

Length of output: 126


🏁 Script executed:

grep -r "frontmatter.draft" --include="*.ts" --include="*.tsx" --include="*.vue" --include="*.js" -B 2 -A 2

Repository: npmx-dev/npmx.dev

Length of output: 849


🏁 Script executed:

fd "standard-site-sync" --type f

Repository: npmx-dev/npmx.dev

Length of output: 90


🏁 Script executed:

fd "modules" --type d | head -5

Repository: npmx-dev/npmx.dev

Length of output: 69


🏁 Script executed:

cat modules/standard-site-sync.ts

Repository: npmx-dev/npmx.dev

Length of output: 9654


Guard site.standard.document link emission for draft posts.

Line 15 prevents search engine indexing of drafts with noindex, nofollow, but lines 18-25 unconditionally emit a site.standard.document link containing the draft's identifier. The AT Protocol sync module (standard-site-sync.ts) correctly filters drafts from being synced, so this link points to a non-existent record. However, the link header itself leaks the draft's existence to anything monitoring the page source.

Proposed fix
-useHead({
-  link: [
-    {
-      rel: 'site.standard.document',
-      href: `at://${NPMX_DEV_DID}/site.standard.document/${generateBlogTID(props.frontmatter.date, props.frontmatter.slug)}`,
-    },
-  ],
-})
+if (!props.frontmatter.draft) {
+  useHead({
+    link: [
+      {
+        rel: 'site.standard.document',
+        href: `at://${NPMX_DEV_DID}/site.standard.document/${generateBlogTID(props.frontmatter.date, props.frontmatter.slug)}`,
+      },
+    ],
+  })
+}
📝 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
...(props.frontmatter.draft ? { robots: 'noindex, nofollow' } : {}),
useHead({
link: props.frontmatter.draft ? [] : [
{
rel: 'site.standard.document',
href: `at://${NPMX_DEV_DID}/site.standard.document/${generateBlogTID(props.frontmatter.date, props.frontmatter.slug)}`,
},
],
})

})

useHead({
link: [
{
rel: 'site.standard.document',
href: `at://${NPMX_DEV_DID}/site.standard.document/${generateBlogTID(props.frontmatter.date, props.frontmatter.slug)}`,
},
],
})
Comment on lines +18 to +25
Copy link
Member

Choose a reason for hiding this comment

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

this should probably be done only for production

Copy link
Contributor Author

Choose a reason for hiding this comment

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

import { isProduction } from '../config/env' the best check for that?


defineOgImageComponent('BlogPost', {
title: props.frontmatter.title,
authors: props.frontmatter.authors,
Expand Down
46 changes: 14 additions & 32 deletions modules/standard-site-sync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,9 @@ import process from 'node:process'
import { createHash } from 'node:crypto'
import { defineNuxtModule, useNuxt, createResolver } from 'nuxt/kit'
import { safeParse } from 'valibot'
import { BlogPostSchema, type BlogPostFrontmatter } from '../shared/schemas/blog'
import { NPMX_SITE } from '../shared/utils/constants'
import { BlogPostSchema, type BlogPostFrontmatter } from '#shared/schemas/blog'
import { NPMX_DEV_DID, NPMX_SITE } from '#shared/utils/constants'
import { read } from 'gray-matter'
import { TID } from '@atproto/common'
import { PasswordSession } from '@atproto/lex-password-session'
import {
Client,
Expand All @@ -15,11 +14,9 @@ import {
} from '@atproto/lex'
import * as com from '../shared/types/lexicons/com'
import * as site from '../shared/types/lexicons/site'
import { generateBlogTID, npmxPublicationRkey } from '#shared/utils/atproto'

const syncedDocuments = new Map<string, string>()
const CLOCK_ID_THREE = 3
const MS_TO_MICROSECONDS = 1000
const ONE_DAY_MILLISECONDS = 86400000

type BlogPostDocument = Pick<
BlogPostFrontmatter,
Expand Down Expand Up @@ -89,6 +86,8 @@ export default defineNuxtModule({
rkey: possiblePublication.tid,
},
)
// Wait for the firehose and indexers to catch up if we create a publication
await new Promise(sleepResolve => setTimeout(sleepResolve, 2_000))
}
if (documentsToSync.length > 0) {
await syncsiteStandardDocuments(authenticatedClient, documentsToSync)
Expand Down Expand Up @@ -153,23 +152,6 @@ async function syncsiteStandardDocuments(client: Client, documentsToSync: Docume
console.log('[standard-site-sync] synced all new publications')
}

// Parse date from frontmatter, add file-path entropy for same-date collision resolution
function generateTID(dateString: string, filePath: string): string {
let timestamp = new Date(dateString).getTime()

// If date has no time component (exact midnight), add file-based entropy
// This ensures unique TIDs when multiple posts share the same date
if (timestamp % ONE_DAY_MILLISECONDS === 0) {
// Hash the file path to generate deterministic microseconds offset
const pathHash = createHash('md5').update(filePath).digest('hex')
const offset = parseInt(pathHash.slice(0, 8), 16) % 1000000 // 0-999999 microseconds
timestamp += offset
}

// Clock id(3) needs to be the same everytime to get the same TID from a timestamp
return TID.fromTime(timestamp * MS_TO_MICROSECONDS, CLOCK_ID_THREE).str
}

// Schema expects 'path' & frontmatter provides 'slug'
function normalizeBlogFrontmatter(frontmatter: Record<string, unknown>): Record<string, unknown> {
return {
Expand All @@ -187,12 +169,13 @@ function createContentHash(data: unknown): string {

function buildATProtoDocument(siteUrl: string, data: BlogPostDocument) {
return site.standard.document.$build({
site: siteUrl as `${string}:${string}`,
site: `at://${NPMX_DEV_DID}/site.standard.publication/${npmxPublicationRkey()}`,
path: data.path,
title: data.title,
description: data.description ?? data.excerpt,
tags: data.tags,
publishedAt: new Date(data.date).toISOString(),
// Publish on the record with the current date
publishedAt: new Date().toISOString(),
})
}

Expand Down Expand Up @@ -242,7 +225,7 @@ const syncFile = async (
return
}

const tid = generateTID(data.date, filePath)
const tid = generateBlogTID(data.date, data.slug)

let checkForBlogResult = await pdsPublicClient.xrpcSafe(com.atproto.repo.getRecord, {
params: {
Expand Down Expand Up @@ -275,11 +258,7 @@ const syncFile = async (
* @returns
*/
const checkPublication = async (identifier: AtIdentifierString, pdsPublicClient: Client) => {
// Using our release date as the tid for the publication
const publicationTid = TID.fromTime(
new Date('2026-03-03').getTime() * MS_TO_MICROSECONDS,
CLOCK_ID_THREE,
).str
const publicationTid = npmxPublicationRkey()

//Check to see if we have a publication yet
const publicationCheck = await pdsPublicClient.xrpcSafe(com.atproto.repo.getRecord, {
Expand All @@ -302,8 +281,11 @@ const checkPublication = async (identifier: AtIdentifierString, pdsPublicClient:
tid: publicationTid,
record: site.standard.publication.$build({
name: 'npmx.dev',
url: 'https://npmx.dev/blog',
url: 'https://npmx.dev',
description: 'a fast, modern browser for the npm registry',
preferences: {
showInDiscover: true,
},
}),
}
}
Expand Down
1 change: 1 addition & 0 deletions nuxt.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ export default defineNuxtConfig({
'/opensearch.xml': { isr: true },
'/oauth-client-metadata.json': { prerender: true },
'/.well-known/jwks.json': { prerender: true },
'/.well-known/site.standard.publication': { prerender: true },
// never cache
'/api/auth/**': { isr: false, cache: false },
'/api/social/**': { isr: false, cache: false },
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,8 @@
},
"dependencies": {
"@atcute/bluesky-richtext-segmenter": "3.0.0",
"@atcute/tid": "1.1.2",
"@atproto/api": "^0.19.0",
"@atproto/common": "0.5.13",
"@atproto/lex": "0.0.19",
"@atproto/lex-password-session": "0.0.7",
"@atproto/oauth-client-node": "^0.3.15",
Expand Down
35 changes: 32 additions & 3 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions server/routes/.well-known/site.standard.publication.get.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { npmxPublicationRkey } from '#shared/utils/atproto'
Copy link
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:

cat -n server/routes/.well-known/site.standard.publication.get.ts

Repository: npmx-dev/npmx.dev

Length of output: 357


🏁 Script executed:

rg "NPMX_DEV_DID" --type ts --type tsx -A 2 -B 2

Repository: npmx-dev/npmx.dev

Length of output: 88


🏁 Script executed:

rg "NPMX_DEV_DID" -A 2 -B 2

Repository: npmx-dev/npmx.dev

Length of output: 1638


🏁 Script executed:

rg "defineEventHandler|setResponseHeader" shared/utils/constants.ts

Repository: npmx-dev/npmx.dev

Length of output: 43


🏁 Script executed:

fd "nuxt.config" -type f

Repository: npmx-dev/npmx.dev

Length of output: 231


🏁 Script executed:

grep -r "auto-import\|defineEventHandler" . --include="*.config.*" --include="*.json" 2>/dev/null | head -20

Repository: npmx-dev/npmx.dev

Length of output: 43


🏁 Script executed:

find server/routes -name "*.ts" -type f | head -5 | xargs cat

Repository: npmx-dev/npmx.dev

Length of output: 3058


🏁 Script executed:

cat -n server/routes/.well-known/site.standard.publication.get.ts | head -10

Repository: npmx-dev/npmx.dev

Length of output: 357


Import NPMX_DEV_DID from #shared/utils/constants.

Line 5 references NPMX_DEV_DID without importing it, which will cause a runtime error.

Proposed fix
 import { npmxPublicationRkey } from '#shared/utils/atproto'
+import { NPMX_DEV_DID } from '#shared/utils/constants'
📝 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
import { npmxPublicationRkey } from '#shared/utils/atproto'
import { npmxPublicationRkey } from '#shared/utils/atproto'
import { NPMX_DEV_DID } from '#shared/utils/constants'


export default defineEventHandler(async event => {
setResponseHeader(event, 'content-type', 'text/plain')
return `at://${NPMX_DEV_DID}/site.standard.publication/${npmxPublicationRkey()}`
})
6 changes: 3 additions & 3 deletions server/utils/atproto/utils/likes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { Backlink } from '#shared/utils/constellation'
import type * as blue from '#shared/types/lexicons/blue'
import * as dev from '#shared/types/lexicons/dev'
import { Client } from '@atproto/lex'
import { TID } from '@atproto/common'
import * as TID from '@atcute/tid'

//Cache keys and helpers
const CACHE_PREFIX = 'atproto-likes:'
Expand All @@ -26,8 +26,8 @@ export function aggregateBacklinksByDay(
const countsByDay = new Map<string, number>()
for (const backlink of backlinks) {
try {
const tid = TID.fromStr(backlink.rkey)
const timestampMs = tid.timestamp() / 1000
const { timestamp } = TID.parse(backlink.rkey)
const timestampMs = timestamp / 1000
const date = new Date(timestampMs)
const day = date.toISOString().slice(0, 10)
countsByDay.set(day, (countsByDay.get(day) ?? 0) + 1)
Expand Down
31 changes: 31 additions & 0 deletions shared/utils/atproto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { TID_CLOCK_ID } from './constants'
import * as TID from '@atcute/tid'

const ONE_DAY_MILLISECONDS = 86400000
const MS_TO_MICROSECONDS = 1000

// A very simple hasher to get an offset for blog posts on the same day
const simpleHash = (str: string): number => {
let h = 0
for (let i = 0; i < str.length; i++) {
h = ((h << 5) - h + str.charCodeAt(i)) >>> 0
}
return h
}

// Parse date from frontmatter, add slug-path entropy for same-date collision resolution
export const generateBlogTID = (dateString: string, slug: string): string => {
let timestamp = new Date(dateString).getTime()

if (timestamp % ONE_DAY_MILLISECONDS === 0) {
const offset = simpleHash(slug) % 1000000
timestamp += offset
}

// Clock id(3) needs to be the same everytime to get the same TID from a timestamp
return TID.create(timestamp * MS_TO_MICROSECONDS, TID_CLOCK_ID)
}

// Using our release date as the tid for the publication
export const npmxPublicationRkey = () =>
TID.create(new Date('2026-03-03').getTime() * MS_TO_MICROSECONDS, TID_CLOCK_ID)
2 changes: 2 additions & 0 deletions shared/utils/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ export const PACKAGE_SUBJECT_REF = (packageName: string) =>
// 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}`
export const NPMX_DEV_DID = 'did:plc:u5zp7npt5kpueado77kuihyz'
export const TID_CLOCK_ID = 3

// Discord
export const DISCORD_COMMUNITY_URL = 'https://chat.npmx.dev'
Expand Down
4 changes: 2 additions & 2 deletions test/unit/server/utils/likes-evolution.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, expect, it, vi, beforeEach, type Mocked } from 'vitest'
import { TID } from '@atproto/common'
import * as TID from '@atcute/tid'
import type { ConstellationLike } from '../../../../server/utils/atproto/utils/likes'
import type { CacheAdapter } from '../../../../server/utils/cache/shared'

Expand All @@ -18,7 +18,7 @@ const { aggregateBacklinksByDay, PackageLikesUtils } =

function tidFromDate(date: Date): string {
const microseconds = date.getTime() * 1000
return TID.fromTime(microseconds, 0).toString()
return TID.create(microseconds, 0).toString()
}

function backlink(date: Date): { did: string; collection: string; rkey: string } {
Expand Down
Loading