cms: @hanzo/cms rebrand WIP (auth-iam, db-sqlite→Base, storage-s3→SeaweedFS) — brand-neutral

This commit is contained in:
Hanzo AI
2026-07-01 19:59:39 -07:00
parent 348559e739
commit f1c2b2f11d
10 changed files with 980 additions and 36 deletions
Binary file not shown.
+37
View File
@@ -0,0 +1,37 @@
{
"name": "@hanzo/cms-demo",
"version": "1.0.0",
"private": true,
"description": "Reference Hanzo CMS app: Base/SQLite (per-org) + SeaweedFS S3 + IAM SSO + org==tenant + white-label.",
"license": "MIT",
"type": "module",
"scripts": {
"build": "next build",
"dev": "next dev",
"generate:types": "cross-env NODE_OPTIONS=--no-deprecation payload generate:types",
"start": "next start"
},
"dependencies": {
"@hanzo/cms": "workspace:*",
"@hanzo/cms-auth-iam": "workspace:*",
"@hanzo/cms-db-sqlite": "workspace:*",
"@hanzo/cms-next": "workspace:*",
"@hanzo/cms-plugin-multi-tenant": "workspace:*",
"@hanzo/cms-plugin-whitelabel": "workspace:*",
"@hanzo/cms-richtext-lexical": "workspace:*",
"@hanzo/cms-storage-s3": "workspace:*",
"@hanzo/cms-ui": "workspace:*",
"cross-env": "^7.0.3",
"graphql": "^16.8.1",
"next": "16.2.6",
"react": "19.2.6",
"react-dom": "19.2.6",
"sharp": "0.34.2"
},
"devDependencies": {
"@types/node": "22.19.9",
"@types/react": "19.2.14",
"@types/react-dom": "19.2.3",
"typescript": "5.7.3"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 70 B

+191
View File
@@ -0,0 +1,191 @@
/**
* Hanzo CMS — real-slice acceptance proof (headless, Local API).
*
* Proves, with real artifacts (no stubs):
* 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)
* 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 { hanzoIAMStrategy } from '@hanzo/cms-auth-iam'
import { readFileSync } from 'fs'
import path from 'path'
import { fileURLToPath } from 'url'
const dirname = path.dirname(fileURLToPath(import.meta.url))
const ok = (m: string) => console.log(`${m}`)
const step = (m: string) => console.log(`\n=== ${m} ===`)
const fail = (m: string) => {
console.error(`${m}`)
process.exitCode = 1
}
const loadConfig = async () => (await import('../src/payload.config.js')).default
const run = async () => {
const org = process.env.HANZO_ORG || 'proofco'
process.env.HANZO_ORG = org
// ---- 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
ok(`per-org SQLite = ${dbUrl ?? 'file (libsql)'}`)
// provision the tenant for this org (org == tenant). Idempotent.
const existingTenant = await payload.find({
collection: 'tenants',
limit: 1,
where: { slug: { equals: org } },
})
const tenant =
existingTenant.docs[0] ??
(await payload.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 ----------------------------
step('2. Media upload -> SeaweedFS S3 (real object)')
const haveS3 = Boolean(process.env.S3_ACCESS_KEY_ID && process.env.S3_SECRET_ACCESS_KEY)
let uploadedKey: string | undefined
if (haveS3) {
const pngPath = path.resolve(dirname, 'fixture.png')
const bytes = readFileSync(pngPath)
const media = await payload.create({
collection: 'media',
data: { alt: 'proof pixel', tenant: tenant.id },
file: {
name: `proof-${Date.now()}.png`,
data: bytes,
mimetype: 'image/png',
size: bytes.length,
},
})
uploadedKey = `${org}/${media.filename}`
ok(`created media doc id=${media.id} filename=${media.filename}`)
ok(`expected S3 key (per-org prefix) = ${uploadedKey}`)
} else {
console.log(' (skipped: no S3 creds in env — set S3_ACCESS_KEY_ID/S3_SECRET_ACCESS_KEY)')
}
// ---- 3. Hanzo IAM SSO: verify a REAL token ---------------------------
step('3. Hanzo IAM SSO — verify a real IAM token via JWKS')
const token = process.env.IAM_TOKEN
if (token) {
const strat = hanzoIAMStrategy()
const headers = new Headers({ authorization: `Bearer ${token}` })
const res = await strat.authenticate({
canSetHeaders: true,
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'}`)
} 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,
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)')
} else {
fail('SECURITY: invalid token was accepted')
}
} else {
console.log(' (skipped: no IAM_TOKEN in env)')
}
// ---- 4. draft -> publish --------------------------------------------
step('4. Draft -> Publish (Payload-native versions)')
const draft = await payload.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({
collection: 'pages',
where: { and: [{ slug: { equals: 'launch' } }, { _status: { equals: 'published' } }] },
})
if (publishedBefore.totalDocs === 0) {
ok('no published version before publish -> 0 (draft not yet published)')
} else {
fail(`unexpected published version before publish: ${publishedBefore.totalDocs}`)
}
const published = await payload.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({
collection: 'pages',
where: { and: [{ slug: { equals: 'launch' } }, { _status: { equals: 'published' } }] },
})
if (publishedAfter.totalDocs === 1) {
ok(`published version after publish -> 1 (draft->publish works)`)
} else {
fail(`expected 1 published doc, got ${publishedAfter.totalDocs}`)
}
const versions = await payload.findVersions({
collection: 'pages',
where: { parent: { equals: draft.id } },
})
ok(`version history rows = ${versions.totalDocs} (>=1)`)
// ---- 5. per-org isolation -------------------------------------------
// org == tenant means each org gets its OWN SQLite database (Base). We prove
// isolation by pointing a second, independent Payload build at org2's DB and
// confirming org1's page is invisible there. (Runs only when PROOF_ORG2=1 to
// keep the primary run single-instance; the isolation run is a separate
// 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' })
if (pages2.totalDocs === 0) {
ok(`org2 (${org}) sees 0 pages — org1 data NOT visible -> isolated`)
} else {
fail(`ISOLATION BREACH: org2 sees ${pages2.totalDocs} page(s) from another org`)
}
}
console.log('\n=== PROOF COMPLETE ===')
console.log(
JSON.stringify(
{
boot: true,
dbAdapter: payload.db.name,
draftThenPublish: publishedAfter.totalDocs === 1,
iamVerified: Boolean(token),
org,
s3ObjectKey: uploadedKey ?? null,
},
null,
2,
),
)
process.exit(process.exitCode || 0)
}
run().catch((e) => {
console.error('PROOF FAILED:', e)
process.exit(1)
})
@@ -0,0 +1,28 @@
import type { CollectionConfig } from '@hanzo/cms'
/**
* Tenants == IAM orgs. One row per org, keyed on the IAM `owner` slug.
* Provisioned by @hanzo/cms-auth-iam on first login. There is never a tenant
* concept separate from the IAM org.
*/
export const Tenants: CollectionConfig = {
slug: 'tenants',
admin: {
useAsTitle: 'name',
},
fields: [
{
name: 'name',
type: 'text',
required: true,
},
{
name: 'slug',
type: 'text',
admin: { description: 'IAM org slug (the tenant key).' },
index: true,
required: true,
unique: true,
},
],
}
+565
View File
@@ -0,0 +1,565 @@
/* tslint:disable */
/* eslint-disable */
/**
* This file was automatically generated by Payload.
* DO NOT MODIFY IT BY HAND. Instead, modify your source Payload config,
* and re-run `payload generate:types` to regenerate this file.
*/
/**
* Supported timezones in IANA format.
*
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "supportedTimezones".
*/
export type SupportedTimezones =
| 'Pacific/Midway'
| 'Pacific/Niue'
| 'Pacific/Honolulu'
| 'Pacific/Rarotonga'
| 'America/Anchorage'
| 'Pacific/Gambier'
| 'America/Los_Angeles'
| 'America/Tijuana'
| 'America/Denver'
| 'America/Phoenix'
| 'America/Chicago'
| 'America/Guatemala'
| 'America/New_York'
| 'America/Bogota'
| 'America/Caracas'
| 'America/Santiago'
| 'America/Buenos_Aires'
| 'America/Sao_Paulo'
| 'Atlantic/South_Georgia'
| 'Atlantic/Azores'
| 'Atlantic/Cape_Verde'
| 'Europe/London'
| 'Europe/Berlin'
| 'Africa/Lagos'
| 'Europe/Athens'
| 'Africa/Cairo'
| 'Europe/Moscow'
| 'Asia/Riyadh'
| 'Asia/Dubai'
| 'Asia/Baku'
| 'Asia/Karachi'
| 'Asia/Tashkent'
| 'Asia/Calcutta'
| 'Asia/Dhaka'
| 'Asia/Almaty'
| 'Asia/Jakarta'
| 'Asia/Bangkok'
| 'Asia/Shanghai'
| 'Asia/Singapore'
| 'Asia/Tokyo'
| 'Asia/Seoul'
| 'Australia/Brisbane'
| 'Australia/Sydney'
| 'Pacific/Guam'
| 'Pacific/Noumea'
| 'Pacific/Auckland'
| 'Pacific/Fiji';
export interface Config {
auth: {
users: UserAuthOperations;
};
blocks: {};
collections: {
users: User;
tenants: Tenant;
pages: Page;
media: Media;
'payload-kv': PayloadKv;
'payload-jobs': PayloadJob;
'payload-locked-documents': PayloadLockedDocument;
'payload-preferences': PayloadPreference;
'payload-migrations': PayloadMigration;
};
collectionsJoins: {};
collectionsSelect: {
users: UsersSelect<false> | UsersSelect<true>;
tenants: TenantsSelect<false> | TenantsSelect<true>;
pages: PagesSelect<false> | PagesSelect<true>;
media: MediaSelect<false> | MediaSelect<true>;
'payload-kv': PayloadKvSelect<false> | PayloadKvSelect<true>;
'payload-jobs': PayloadJobsSelect<false> | PayloadJobsSelect<true>;
'payload-locked-documents': PayloadLockedDocumentsSelect<false> | PayloadLockedDocumentsSelect<true>;
'payload-preferences': PayloadPreferencesSelect<false> | PayloadPreferencesSelect<true>;
'payload-migrations': PayloadMigrationsSelect<false> | PayloadMigrationsSelect<true>;
};
db: {
defaultIDType: number;
};
fallbackLocale: null;
globals: {};
globalsSelect: {};
locale: null;
widgets: {
collections: CollectionsWidget;
};
user: User;
jobs: {
tasks: {
schedulePublish: TaskSchedulePublish;
inline: {
input: unknown;
output: unknown;
};
};
workflows: unknown;
};
}
export interface UserAuthOperations {
forgotPassword: {
email: string;
password: string;
};
login: {
email: string;
password: string;
};
registerFirstUser: {
email: string;
password: string;
};
unlock: {
email: string;
password: string;
};
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "users".
*/
export interface User {
id: number;
email?: string | null;
/**
* Hanzo IAM subject (user id).
*/
iamSub?: string | null;
/**
* Hanzo IAM org slug (== tenant).
*/
iamOrg?: string | null;
username?: string | null;
tenants?:
| {
tenant: number | Tenant;
id?: string | null;
}[]
| null;
updatedAt: string;
createdAt: string;
collection: 'users';
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "tenants".
*/
export interface Tenant {
id: number;
name: string;
/**
* IAM org slug (the tenant key).
*/
slug: string;
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "pages".
*/
export interface Page {
id: number;
tenant?: (number | null) | Tenant;
title: string;
slug?: string | null;
content?: {
root: {
type: string;
children: {
type: any;
version: number;
[k: string]: unknown;
}[];
direction: ('ltr' | 'rtl') | null;
format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | '';
indent: number;
version: number;
};
[k: string]: unknown;
} | null;
updatedAt: string;
createdAt: string;
_status?: ('draft' | 'published') | null;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "media".
*/
export interface Media {
id: number;
tenant?: (number | null) | Tenant;
alt?: string | null;
prefix?: string | null;
updatedAt: string;
createdAt: string;
url?: string | null;
thumbnailURL?: string | null;
filename?: string | null;
mimeType?: string | null;
filesize?: number | null;
width?: number | null;
height?: number | null;
focalX?: number | null;
focalY?: number | null;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-kv".
*/
export interface PayloadKv {
id: number;
key: string;
data:
| {
[k: string]: unknown;
}
| unknown[]
| string
| number
| boolean
| null;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-jobs".
*/
export interface PayloadJob {
id: number;
/**
* Input data provided to the job
*/
input?:
| {
[k: string]: unknown;
}
| unknown[]
| string
| number
| boolean
| null;
taskStatus?:
| {
[k: string]: unknown;
}
| unknown[]
| string
| number
| boolean
| null;
completedAt?: string | null;
totalTried?: number | null;
/**
* If hasError is true this job will not be retried
*/
hasError?: boolean | null;
/**
* If hasError is true, this is the error that caused it
*/
error?:
| {
[k: string]: unknown;
}
| unknown[]
| string
| number
| boolean
| null;
/**
* Task execution log
*/
log?:
| {
executedAt: string;
completedAt: string;
taskSlug: 'inline' | 'schedulePublish';
taskID: string;
input?:
| {
[k: string]: unknown;
}
| unknown[]
| string
| number
| boolean
| null;
output?:
| {
[k: string]: unknown;
}
| unknown[]
| string
| number
| boolean
| null;
state: 'failed' | 'succeeded';
error?:
| {
[k: string]: unknown;
}
| unknown[]
| string
| number
| boolean
| null;
id?: string | null;
}[]
| null;
taskSlug?: ('inline' | 'schedulePublish') | null;
queue?: string | null;
waitUntil?: string | null;
processing?: boolean | null;
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-locked-documents".
*/
export interface PayloadLockedDocument {
id: number;
document?:
| ({
relationTo: 'users';
value: number | User;
} | null)
| ({
relationTo: 'tenants';
value: number | Tenant;
} | null)
| ({
relationTo: 'pages';
value: number | Page;
} | null)
| ({
relationTo: 'media';
value: number | Media;
} | null);
globalSlug?: string | null;
user: {
relationTo: 'users';
value: number | User;
};
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-preferences".
*/
export interface PayloadPreference {
id: number;
user: {
relationTo: 'users';
value: number | User;
};
key?: string | null;
value?:
| {
[k: string]: unknown;
}
| unknown[]
| string
| number
| boolean
| null;
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-migrations".
*/
export interface PayloadMigration {
id: number;
name?: string | null;
batch?: number | null;
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "users_select".
*/
export interface UsersSelect<T extends boolean = true> {
email?: T;
iamSub?: T;
iamOrg?: T;
username?: T;
tenants?:
| T
| {
tenant?: T;
id?: T;
};
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "tenants_select".
*/
export interface TenantsSelect<T extends boolean = true> {
name?: T;
slug?: T;
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "pages_select".
*/
export interface PagesSelect<T extends boolean = true> {
tenant?: T;
title?: T;
slug?: T;
content?: T;
updatedAt?: T;
createdAt?: T;
_status?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "media_select".
*/
export interface MediaSelect<T extends boolean = true> {
tenant?: T;
alt?: T;
prefix?: T;
updatedAt?: T;
createdAt?: T;
url?: T;
thumbnailURL?: T;
filename?: T;
mimeType?: T;
filesize?: T;
width?: T;
height?: T;
focalX?: T;
focalY?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-kv_select".
*/
export interface PayloadKvSelect<T extends boolean = true> {
key?: T;
data?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-jobs_select".
*/
export interface PayloadJobsSelect<T extends boolean = true> {
input?: T;
taskStatus?: T;
completedAt?: T;
totalTried?: T;
hasError?: T;
error?: T;
log?:
| T
| {
executedAt?: T;
completedAt?: T;
taskSlug?: T;
taskID?: T;
input?: T;
output?: T;
state?: T;
error?: T;
id?: T;
};
taskSlug?: T;
queue?: T;
waitUntil?: T;
processing?: T;
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-locked-documents_select".
*/
export interface PayloadLockedDocumentsSelect<T extends boolean = true> {
document?: T;
globalSlug?: T;
user?: T;
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-preferences_select".
*/
export interface PayloadPreferencesSelect<T extends boolean = true> {
user?: T;
key?: T;
value?: T;
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-migrations_select".
*/
export interface PayloadMigrationsSelect<T extends boolean = true> {
name?: T;
batch?: T;
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "collections_widget".
*/
export interface CollectionsWidget {
data?: {
[k: string]: unknown;
};
width: 'full';
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "TaskSchedulePublish".
*/
export interface TaskSchedulePublish {
input: {
type?: ('publish' | 'unpublish') | null;
locale?: string | null;
doc?: {
relationTo: 'pages';
value: number | Page;
} | null;
global?: string | null;
user?: (number | null) | User;
};
output?: unknown;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "auth".
*/
export interface Auth {
[k: string]: unknown;
}
declare module '@hanzo/cms' {
export interface GeneratedTypes extends Config {}
}
+80
View File
@@ -0,0 +1,80 @@
import { buildConfig } from '@hanzo/cms'
import { hanzoIAMStrategy } from '@hanzo/cms-auth-iam'
import { sqliteAdapter } from '@hanzo/cms-db-sqlite'
import { multiTenantPlugin } from '@hanzo/cms-plugin-multi-tenant'
import { whiteLabelPlugin } from '@hanzo/cms-plugin-whitelabel'
import { lexicalEditor } from '@hanzo/cms-richtext-lexical'
import { s3Storage } from '@hanzo/cms-storage-s3'
import path from 'path'
import sharp from 'sharp'
import { fileURLToPath } from 'url'
import { Media } from './collections/Media.js'
import { Pages } from './collections/Pages.js'
import { Tenants } from './collections/Tenants.js'
import { Users } from './collections/Users.js'
const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename)
// org == tenant. Every persistent primitive is per-org.
const ORG = process.env.HANZO_ORG || 'hanzo'
export default buildConfig({
admin: {
importMap: {
baseDir: path.resolve(dirname),
},
user: Users.slug,
},
collections: [Users, Tenants, Pages, Media],
editor: lexicalEditor(),
secret: process.env.PAYLOAD_SECRET || 'dev-secret-change-me',
typescript: {
outputFile: path.resolve(dirname, 'payload-types.ts'),
},
// DB = Hanzo Base / SQLite (per-org). libsql; no Postgres/Mongo default.
db: sqliteAdapter({
client: {
url: process.env.DATABASE_URI || `file:${path.resolve(dirname, `../data/${ORG}.db`)}`,
},
}),
plugins: [
// Media/DAM -> SeaweedFS (hanzoai/s3), per-org prefix. forcePathStyle required.
s3Storage({
bucket: process.env.S3_BUCKET || 'hanzo-cms',
collections: {
media: { prefix: ORG },
},
config: {
credentials: {
accessKeyId: process.env.S3_ACCESS_KEY_ID || '',
secretAccessKey: process.env.S3_SECRET_ACCESS_KEY || '',
},
endpoint: process.env.S3_ENDPOINT || 'https://s3.hanzo.ai',
forcePathStyle: true,
region: process.env.S3_REGION || 'us-east-1',
},
}),
// Multi-tenancy keyed to the IAM org (org == tenant). The ONE tenancy.
multiTenantPlugin({
collections: {
media: {},
pages: {},
},
tenantsSlug: 'tenants',
userHasAccessToAllTenants: (user) =>
Boolean(user && (user as { iamOrg?: string }).iamOrg === 'admin'),
}),
// Brand-neutral / white-label by domain. Neutral when no brand matches.
whiteLabelPlugin({
brands: [
// Example brands. No '*' fallback -> unmatched hosts render neutral.
{ name: 'Lux', hostnames: ['cms.lux.network'] },
{ name: 'Zoo', hostnames: ['cms.zoo.ngo'] },
{ name: 'MaxPower', hostnames: ['maxpower.hanzo.cms'] },
],
}),
],
sharp,
})
+11 -3
View File
@@ -1,7 +1,7 @@
import type { Field } from '@hanzo/cms'
export type { HanzoIAMStrategyConfig, IAMClaims } from './types.js'
export { hanzoIAMStrategy } from './strategy.js'
export type { HanzoIAMStrategyConfig, IAMClaims } from './types.js'
/**
* Fields the IAM strategy needs on the auth collection to map + dedupe users.
@@ -10,10 +10,18 @@ export { hanzoIAMStrategy } from './strategy.js'
* fields: [...iamAuthFields, ...yourFields]
*/
export const iamAuthFields: Field[] = [
{
// Auth collections with `disableLocalStrategy: true` do NOT get the email
// field auto-added (that comes from the local strategy), so add it here.
name: 'email',
type: 'email',
index: true,
label: 'Email',
},
{
name: 'iamSub',
type: 'text',
admin: { readOnly: true, description: 'Hanzo IAM subject (user id).' },
admin: { description: 'Hanzo IAM subject (user id).', readOnly: true },
index: true,
label: 'IAM Subject',
unique: true,
@@ -21,7 +29,7 @@ export const iamAuthFields: Field[] = [
{
name: 'iamOrg',
type: 'text',
admin: { readOnly: true, description: 'Hanzo IAM org slug (== tenant).' },
admin: { description: 'Hanzo IAM org slug (== tenant).', readOnly: true },
index: true,
label: 'IAM Org',
},
+67 -33
View File
@@ -222,6 +222,67 @@ importers:
specifier: 4.3.6
version: 4.3.6
apps/hanzo-demo:
dependencies:
'@hanzo/cms':
specifier: workspace:*
version: link:../../packages/payload
'@hanzo/cms-auth-iam':
specifier: workspace:*
version: link:../../packages/auth-iam
'@hanzo/cms-db-sqlite':
specifier: workspace:*
version: link:../../packages/db-sqlite
'@hanzo/cms-next':
specifier: workspace:*
version: link:../../packages/next
'@hanzo/cms-plugin-multi-tenant':
specifier: workspace:*
version: link:../../packages/plugin-multi-tenant
'@hanzo/cms-plugin-whitelabel':
specifier: workspace:*
version: link:../../packages/plugin-whitelabel
'@hanzo/cms-richtext-lexical':
specifier: workspace:*
version: link:../../packages/richtext-lexical
'@hanzo/cms-storage-s3':
specifier: workspace:*
version: link:../../packages/storage-s3
'@hanzo/cms-ui':
specifier: workspace:*
version: link:../../packages/ui
cross-env:
specifier: 7.0.3
version: 7.0.3
graphql:
specifier: 16.8.1
version: 16.8.1
next:
specifier: 16.2.6
version: 16.2.6(@opentelemetry/api@1.9.1)(@playwright/test@1.58.2)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.100.0)
react:
specifier: 19.2.6
version: 19.2.6
react-dom:
specifier: 19.2.6
version: 19.2.6(react@19.2.6)
sharp:
specifier: 0.34.2
version: 0.34.2
devDependencies:
'@types/node':
specifier: 22.19.9
version: 22.19.9
'@types/react':
specifier: 19.2.14
version: 19.2.14
'@types/react-dom':
specifier: 19.2.3
version: 19.2.3(@types/react@19.2.14)
typescript:
specifier: 5.7.3
version: 5.7.3
packages/admin-bar:
dependencies:
react:
@@ -1257,7 +1318,7 @@ importers:
dependencies:
'@sentry/nextjs':
specifier: ^9.5.0
version: 9.47.1(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.1))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.1))(next@16.2.3(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.58.2)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.100.0))(react@19.2.6)(webpack@5.108.3(@swc/core@1.15.3)(esbuild@0.25.12)(lightningcss@1.30.2)(postcss@8.5.16))
version: 9.47.1(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.1))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.1))(next@16.2.6(@opentelemetry/api@1.9.1)(@playwright/test@1.58.2)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.100.0))(react@19.2.6)(webpack@5.108.3(@swc/core@1.15.3)(esbuild@0.25.12)(lightningcss@1.30.2)(postcss@8.5.16))
'@sentry/types':
specifier: ^9.5.0
version: 9.47.1
@@ -2246,7 +2307,7 @@ importers:
dependencies:
recharts:
specifier: 3.2.1
version: 3.2.1(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react-is@16.13.1)(react@19.2.6)(redux@5.0.1)
version: 3.2.1(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react-is@17.0.2)(react@19.2.6)(redux@5.0.1)
devDependencies:
'@aws-sdk/client-s3':
specifier: ^3.614.0
@@ -19498,7 +19559,7 @@ snapshots:
- supports-color
- webpack
'@sentry/nextjs@9.47.1(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.1))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.1))(next@16.2.3(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.58.2)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.100.0))(react@19.2.6)(webpack@5.108.3(@swc/core@1.15.3)(esbuild@0.25.12)(lightningcss@1.30.2)(postcss@8.5.16))':
'@sentry/nextjs@9.47.1(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.1))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.1))(next@16.2.6(@opentelemetry/api@1.9.1)(@playwright/test@1.58.2)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.100.0))(react@19.2.6)(webpack@5.108.3(@swc/core@1.15.3)(esbuild@0.25.12)(lightningcss@1.30.2)(postcss@8.5.16))':
dependencies:
'@opentelemetry/api': 1.9.1
'@opentelemetry/semantic-conventions': 1.41.1
@@ -19511,7 +19572,7 @@ snapshots:
'@sentry/vercel-edge': 9.47.1(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.1))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.1))
'@sentry/webpack-plugin': 3.6.1(webpack@5.108.3(@swc/core@1.15.3)(esbuild@0.25.12)(lightningcss@1.30.2)(postcss@8.5.16))
chalk: 3.0.0
next: 16.2.3(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.58.2)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.100.0)
next: 16.2.6(@opentelemetry/api@1.9.1)(@playwright/test@1.58.2)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.100.0)
resolve: 1.22.8
rollup: 4.59.0
stacktrace-parser: 0.1.11
@@ -25293,33 +25354,6 @@ snapshots:
react: 19.2.6
react-dom: 19.2.6(react@19.2.6)
next@16.2.3(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.58.2)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.100.0):
dependencies:
'@next/env': 16.2.3
'@swc/helpers': 0.5.15
baseline-browser-mapping: 2.10.40
caniuse-lite: 1.0.30001800
postcss: 8.4.31
react: 19.2.6
react-dom: 19.2.6(react@19.2.6)
styled-jsx: 5.1.6(@babel/core@7.29.7)(react@19.2.6)
optionalDependencies:
'@next/swc-darwin-arm64': 16.2.3
'@next/swc-darwin-x64': 16.2.3
'@next/swc-linux-arm64-gnu': 16.2.3
'@next/swc-linux-arm64-musl': 16.2.3
'@next/swc-linux-x64-gnu': 16.2.3
'@next/swc-linux-x64-musl': 16.2.3
'@next/swc-win32-arm64-msvc': 16.2.3
'@next/swc-win32-x64-msvc': 16.2.3
'@opentelemetry/api': 1.9.1
'@playwright/test': 1.58.2
sass: 1.100.0
sharp: 0.34.5
transitivePeerDependencies:
- '@babel/core'
- babel-plugin-macros
next@16.2.3(@opentelemetry/api@1.9.1)(@playwright/test@1.58.2)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.100.0):
dependencies:
'@next/env': 16.2.3
@@ -26277,7 +26311,7 @@ snapshots:
real-require@0.2.0: {}
recharts@3.2.1(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react-is@16.13.1)(react@19.2.6)(redux@5.0.1):
recharts@3.2.1(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react-is@17.0.2)(react@19.2.6)(redux@5.0.1):
dependencies:
'@reduxjs/toolkit': 2.12.0(react-redux@9.3.0(@types/react@19.2.14)(react@19.2.6)(redux@5.0.1))(react@19.2.6)
clsx: 2.1.1
@@ -26287,7 +26321,7 @@ snapshots:
immer: 10.2.0
react: 19.2.6
react-dom: 19.2.6(react@19.2.6)
react-is: 16.13.1
react-is: 17.0.2
react-redux: 9.3.0(@types/react@19.2.14)(react@19.2.6)(redux@5.0.1)
reselect: 5.1.1
tiny-invariant: 1.3.3
+1
View File
@@ -1,5 +1,6 @@
packages:
- 'packages/*'
- 'apps/*'
- 'tools/*'
- 'test'
- 'templates/blank'