fix(cms): resolve type errors surfaced by the monorepo build (rename edge cases)

Real turbo build over the monorepo (pnpm 10.27) verified the Payload→CMS rename
compiles. 41/45 build tasks green — every @hanzo/cms* library package builds.
Fixed the edge cases the token-transform got wrong:

- auth/strategies/jwt.ts: transform renamed jose's { payload } destructure (EXTERNAL
  library property on JWTVerifyResult) to { cms } — reverted to { payload: decodedCMS }.
- auth/getAccessResults.ts: two stray payload.config where in-scope var is cms → cms.config.
- apps/hanzo-demo/next.config.ts + scripts/proof.ts (outside src/, missed by partition):
  withPayload→withCMS identifier (import path /withPayload kept), getPayload→getCMS,
  payload local-API object → cms, cms-tenant cookie log. payload.config.js path preserved.

core @hanzo/cms builds clean. Demo app's remaining failure is a Next Turbopack
workspace-root error (next/package.json resolution) — orthogonal to the rename.

Claude-Session: https://claude.ai/code/session_013jh8aka8q8RvhhVQ1psMeW
This commit is contained in:
hanzo-dev
2026-07-12 21:26:20 -07:00
parent 79fff23766
commit 1779549972
4 changed files with 35 additions and 34 deletions
+6 -5
View File
@@ -1,5 +1,6 @@
import { withPayload } from '@hanzo/cms-next/withPayload'
import type { NextConfig } from 'next'
import { withCMS } from '@hanzo/cms-next/withPayload'
import path from 'path'
import { fileURLToPath } from 'url'
@@ -14,6 +15,9 @@ const nextConfig: NextConfig = {
},
],
},
turbopack: {
root: path.resolve(dirname),
},
webpack: (webpackConfig) => {
webpackConfig.resolve.extensionAlias = {
'.cjs': ['.cts', '.cjs'],
@@ -23,9 +27,6 @@ const nextConfig: NextConfig = {
return webpackConfig
},
turbopack: {
root: path.resolve(dirname),
},
}
export default withPayload(nextConfig, { devBundleServerPackages: false })
export default withCMS(nextConfig, { devBundleServerPackages: false })
+19 -19
View File
@@ -5,12 +5,12 @@
* 1. @hanzo/cms boots on Base/SQLite (per-org libsql file)
* 2. a real media upload lands in SeaweedFS S3 (real object)
* 3. Hanzo IAM SSO: a REAL IAM token verifies via the auth strategy (JWKS)
* 4. draft -> publish (Payload-native versions)
* 4. draft -> publish (CMS-native versions)
* 5. per-org isolation (two orgs, separate SQLite, no cross-read)
*
* Run: HANZO_ORG=... S3_*=... IAM_TOKEN=... tsx scripts/proof.ts
*/
import { getPayload } from '@hanzo/cms'
import { getCMS } from '@hanzo/cms'
import { hanzoIAMStrategy } from '@hanzo/cms-auth-iam'
import { readFileSync } from 'fs'
import path from 'path'
@@ -34,20 +34,20 @@ const run = async () => {
// ---- 1. BOOT on Base/SQLite (per-org) --------------------------------
step(`1. BOOT @hanzo/cms on Base/SQLite (org=${org})`)
const config = await loadConfig()
const payload = await getPayload({ config })
ok(`booted; db adapter = ${payload.db.name}`)
const dbUrl = (payload.db as unknown as { client?: { url?: string } }).client?.url
const cms = await getCMS({ config })
ok(`booted; db adapter = ${cms.db.name}`)
const dbUrl = (cms.db as unknown as { client?: { url?: string } }).client?.url
ok(`per-org SQLite = ${dbUrl ?? 'file (libsql)'}`)
// provision the tenant for this org (org == tenant). Idempotent.
const existingTenant = await payload.find({
const existingTenant = await cms.find({
collection: 'tenants',
limit: 1,
where: { slug: { equals: org } },
})
const tenant =
existingTenant.docs[0] ??
(await payload.create({ collection: 'tenants', data: { name: org, slug: org } }))
(await cms.create({ collection: 'tenants', data: { name: org, slug: org } }))
ok(`tenant (== IAM org) id=${tenant.id} slug=${(tenant as { slug?: string }).slug}`)
// ---- 2. real media upload -> SeaweedFS S3 ----------------------------
@@ -57,7 +57,7 @@ const run = async () => {
if (haveS3) {
const pngPath = path.resolve(dirname, 'fixture.png')
const bytes = readFileSync(pngPath)
const media = await payload.create({
const media = await cms.create({
collection: 'media',
data: { alt: 'proof pixel', tenant: tenant.id },
file: {
@@ -82,22 +82,22 @@ const run = async () => {
const headers = new Headers({ authorization: `Bearer ${token}` })
const res = await strat.authenticate({
canSetHeaders: true,
cms,
headers,
payload,
} as Parameters<typeof strat.authenticate>[0])
if (res.user) {
ok(`IAM token verified; user email=${(res.user as { email?: string }).email}`)
ok(`mapped iamOrg (== tenant) = ${(res.user as { iamOrg?: string }).iamOrg}`)
const setCookie = res.responseHeaders?.get('set-cookie')
ok(`payload-tenant cookie set: ${setCookie ? 'yes' : 'no'}`)
ok(`cms-tenant cookie set: ${setCookie ? 'yes' : 'no'}`)
} else {
fail('IAM token did NOT verify (expected a real valid token)')
}
// negative control: a garbage token must be rejected
const bad = await strat.authenticate({
canSetHeaders: true,
cms,
headers: new Headers({ authorization: 'Bearer not.a.jwt' }),
payload,
} as Parameters<typeof strat.authenticate>[0])
if (!bad.user) {
ok('negative control: invalid token rejected (user=null)')
@@ -109,15 +109,15 @@ const run = async () => {
}
// ---- 4. draft -> publish --------------------------------------------
step('4. Draft -> Publish (Payload-native versions)')
const draft = await payload.create({
step('4. Draft -> Publish (CMS-native versions)')
const draft = await cms.create({
collection: 'pages',
data: { slug: 'launch', _status: 'draft', tenant: tenant.id, title: 'Launch Announcement' },
})
ok(`created DRAFT page id=${draft.id} status=${(draft as { _status?: string })._status}`)
// before publish: there must be NO row whose published status is 'published'
const publishedBefore = await payload.find({
const publishedBefore = await cms.find({
collection: 'pages',
where: { and: [{ slug: { equals: 'launch' } }, { _status: { equals: 'published' } }] },
})
@@ -127,14 +127,14 @@ const run = async () => {
fail(`unexpected published version before publish: ${publishedBefore.totalDocs}`)
}
const published = await payload.update({
const published = await cms.update({
id: draft.id,
collection: 'pages',
data: { _status: 'published' },
})
ok(`published page id=${published.id} status=${(published as { _status?: string })._status}`)
const publishedAfter = await payload.find({
const publishedAfter = await cms.find({
collection: 'pages',
where: { and: [{ slug: { equals: 'launch' } }, { _status: { equals: 'published' } }] },
})
@@ -144,7 +144,7 @@ const run = async () => {
fail(`expected 1 published doc, got ${publishedAfter.totalDocs}`)
}
const versions = await payload.findVersions({
const versions = await cms.findVersions({
collection: 'pages',
where: { parent: { equals: draft.id } },
})
@@ -158,7 +158,7 @@ const run = async () => {
// process — see the runner.)
if (process.env.PROOF_ORG2 === '1') {
step('5. Per-org isolation — org2 on its OWN SQLite')
const pages2 = await payload.find({ collection: 'pages' })
const pages2 = await cms.find({ collection: 'pages' })
if (pages2.totalDocs === 0) {
ok(`org2 (${org}) sees 0 pages — org1 data NOT visible -> isolated`)
} else {
@@ -171,7 +171,7 @@ const run = async () => {
JSON.stringify(
{
boot: true,
dbAdapter: payload.db.name,
dbAdapter: cms.db.name,
draftThenPublish: publishedAfter.totalDocs === 1,
iamVerified: Boolean(token),
org,
@@ -30,7 +30,7 @@ export async function getAccessResults({
const blockReferencesPermissions = {}
await Promise.all(
payload.config.collections.map(async (collection) => {
cms.config.collections.map(async (collection) => {
const collectionOperations: AllOperations[] = ['create', 'read', 'update', 'delete']
if (
@@ -58,7 +58,7 @@ export async function getAccessResults({
)
await Promise.all(
payload.config.globals.map(async (global) => {
cms.config.globals.map(async (global) => {
const globalOperations: AllOperations[] = ['read', 'update']
if (global.versions) {
+8 -8
View File
@@ -12,12 +12,12 @@ type JWTToken = {
}
async function autoLogin({
isGraphQL,
cms,
isGraphQL,
strategyName = 'local-jwt',
}: {
isGraphQL: boolean
cms: CMS
isGraphQL: boolean
strategyName?: string
}): Promise<{
user: AuthStrategyResult['user']
@@ -75,23 +75,23 @@ async function autoLogin({
* Authentication strategy function for JWT tokens
*/
export const JWTAuthentication: AuthStrategyFunction = async ({
cms,
headers,
isGraphQL = false,
cms,
strategyName = 'local-jwt',
}) => {
try {
const token = extractJWT({ headers, cms })
const token = extractJWT({ cms, headers })
if (!token) {
if (headers.get('DisableAutologin') !== 'true') {
return await autoLogin({ isGraphQL, cms, strategyName })
return await autoLogin({ cms, isGraphQL, strategyName })
}
return { user: null }
}
const secretKey = new TextEncoder().encode(cms.secret)
const { cms: decodedCMS } = await jwtVerify<JWTToken>(token, secretKey)
const { payload: decodedCMS } = await jwtVerify<JWTToken>(token, secretKey)
const collection = cms.collections[decodedCMS.collection]
const user = (await cms.findByID({
@@ -120,13 +120,13 @@ export const JWTAuthentication: AuthStrategyFunction = async ({
}
} else {
if (headers.get('DisableAutologin') !== 'true') {
return await autoLogin({ isGraphQL, cms, strategyName })
return await autoLogin({ cms, isGraphQL, strategyName })
}
return { user: null }
}
} catch (ignore) {
if (headers.get('DisableAutologin') !== 'true') {
return await autoLogin({ isGraphQL, cms, strategyName })
return await autoLogin({ cms, isGraphQL, strategyName })
}
return { user: null }
}