
Security News
Risky Biz Podcast: Making Reachability Analysis Work in Real-World Codebases
This episode explores the hard problem of reachability analysis, from static analysis limits to handling dynamic languages and massive dependency trees.
@multidots/sanity-plugin-amazon-product-sync
Advanced tools
Sanity Studio plugin to fetch and embed Amazon products by ASIN (PA-API v5), with settings tool and document actions.
A comprehensive Sanity Studio plugin for fetching and managing Amazon products using the Amazon Product Advertising API (PA-API v5). This plugin provides seamless integration between Sanity Studio and Amazon's product data with real-time fetching capabilities.
useFormValue
Amazon Product Sync plugin overview in Sanity Studio
API Settings: https://share.cleanshot.com/sYc88tRMBvHPvsjvt9YT
Product Fields Display Settings: https://share.cleanshot.com/Nrtl8RpdhgJrSr7Lj0j6
Test Connection and Debug Actions: https://share.cleanshot.com/Nrtl8RpdhgJrSr7Lj0j6
npm install @multidots/sanity-plugin-amazon-product-sync
// sanity.config.ts
import { defineConfig } from 'sanity'
import { structureTool } from 'sanity/structure'
import { visionTool } from '@sanity/vision'
import { amazonProductsPlugin } from '@multidots/sanity-plugin-amazon-product-sync'
import { structure } from './structure'
export default defineConfig({
name: 'default',
title: 'Your Studio',
projectId: 'your-project-id',
dataset: 'production',
plugins: [
structureTool({ structure }),
visionTool(),
amazonProductsPlugin(),
],
schema: {
types: [
// Your existing schemas
],
},
})
Create structure.ts
in your project root:
// structure.ts
import type { StructureResolver } from 'sanity/structure'
export const structure: StructureResolver = (S) =>
S.list()
.title('Content')
.items([
...S.documentTypeListItems().filter(
(item) => item.getId() &&
!['amazon.settings'].includes(item.getId()!)
),
S.divider(),
S.listItem()
.id('amazonSettings')
.title('Amazon Settings')
.child(
S.document()
.schemaType('amazon.settings')
.documentId('amazon-settings')
),
])
// sanity.config.ts (add these document rules)
export default defineConfig({
// ... other config
document: {
// Hide singleton types from the global "New document" menu
newDocumentOptions: (prev: any, { creationContext }: any) => {
if (creationContext.type === 'global') {
return prev.filter(
(templateItem: any) =>
!['amazon.settings'].includes(templateItem.templateId)
)
}
return prev
},
// Prevent duplicate/delete on singleton documents
actions: (prev: any, { schemaType }: any) => {
if (['amazon.settings'].includes(schemaType)) {
return prev.filter(({ action }: any) => action !== 'duplicate' && action !== 'delete')
}
return prev
},
},
// ... rest of config
})
The plugin requires server-side API routes to handle Amazon PA-API calls due to CORS restrictions and security requirements.
CRITICAL: You must set up these environment variables in your Sanity Studio root .env.local
file:
# .env.local (in Sanity Studio root)
SANITY_STUDIO_AMAZON_API_URL=http://localhost:3001
SANITY_STUDIO_AMAZON_TEST_CONNECTION_PATH=/api/amazon/test-connection
SANITY_STUDIO_AMAZON_FETCH_PRODUCT_PATH=/api/amazon/fetch-product
Important Notes:
SANITY_STUDIO_
to be accessible in Sanity StudioThe plugin now includes a centralized configuration system (src/lib/config.ts
) that:
You need to implement these endpoints in your frontend:
POST /api/amazon/test-connection
- Test PA-API credentialsPOST /api/amazon/fetch-product
- Fetch product data by ASINnpm install @sanity/client
Create .env.local
:
# Sanity Configuration
NEXT_PUBLIC_SANITY_PROJECT_ID=your-project-id
NEXT_PUBLIC_SANITY_DATASET=production
SANITY_API_TOKEN=your-sanity-api-token
# API Configuration
NEXT_PUBLIC_API_URL=http://localhost:3001
// lib/sanity.ts
import { createClient } from '@sanity/client'
export const client = createClient({
projectId: process.env.NEXT_PUBLIC_SANITY_PROJECT_ID!,
dataset: process.env.NEXT_PUBLIC_SANITY_DATASET || 'production',
apiVersion: '2025-01-01',
useCdn: false,
token: process.env.SANITY_API_TOKEN,
})
export async function getAmazonSettings() {
const query = `*[_type == "amazon.settings"][0]{
accessKey,
secretKey,
region,
partnerTag
}`
return await client.fetch(query)
}
You can easily reference Amazon products in your own document schemas using a simple field definition. This allows you to associate Amazon products with blog posts, reviews, comparisons, or any other content type.
// Copy this field into any schema where you want to reference Amazon products
defineField({
name: 'amazonProduct',
title: 'Choose Product',
type: 'reference',
to: [{ type: 'amazon.product' }],
options: {
filter: ({ document }) => ({
filter: '_type == "amazon.product"',
params: {}
})
},
description: 'Select an Amazon product',
})
Single Product (Required):
defineField({
name: 'amazonProduct',
title: 'Choose Product',
type: 'reference',
to: [{ type: 'amazon.product' }],
validation: Rule => Rule.required(),
})
Single Product (Optional):
defineField({
name: 'amazonProduct',
title: 'Choose Product',
type: 'reference',
to: [{ type: 'amazon.product' }],
})
Multiple Products (Array):
defineField({
name: 'amazonProducts',
title: 'Choose Products',
type: 'array',
of: [
{
type: 'reference',
to: [{ type: 'amazon.product' }],
}
],
validation: Rule => Rule.min(1).max(10),
})
Blog Post with Amazon Product:
export const blogPostSchema = defineType({
name: 'post',
title: 'Blog Post',
type: 'document',
fields: [
defineField({
name: 'title',
title: 'Title',
type: 'string',
validation: Rule => Rule.required(),
}),
// Amazon Product Reference
defineField({
name: 'amazonProduct',
title: 'Choose Product',
type: 'reference',
to: [{ type: 'amazon.product' }],
description: 'Select an Amazon product to feature in this blog post',
}),
// ... other fields
],
})
Product Review Schema:
export const productReviewSchema = defineType({
name: 'review',
title: 'Product Review',
type: 'document',
fields: [
defineField({
name: 'title',
title: 'Review Title',
type: 'string',
validation: Rule => Rule.required(),
}),
// Amazon Product Reference (Required)
defineField({
name: 'amazonProduct',
title: 'Choose Product',
type: 'reference',
to: [{ type: 'amazon.product' }],
validation: Rule => Rule.required(),
description: 'Select the Amazon product being reviewed',
}),
defineField({
name: 'rating',
title: 'Rating',
type: 'number',
options: { range: { min: 1, max: 5, step: 0.5 } },
}),
// ... other fields
],
})
Product Comparison Schema:
export const comparisonSchema = defineType({
name: 'comparison',
title: 'Product Comparison',
type: 'document',
fields: [
defineField({
name: 'title',
title: 'Comparison Title',
type: 'string',
validation: Rule => Rule.required(),
}),
// Multiple Amazon Products
defineField({
name: 'amazonProducts',
title: 'Choose Products',
type: 'array',
of: [
{
type: 'reference',
to: [{ type: 'amazon.product' }],
}
],
validation: Rule => Rule.required().min(2).max(5),
description: 'Select 2-5 Amazon products to compare',
}),
// ... other fields
],
})
Once you have Amazon product references in your schemas, you can query them using GROQ. Here are comprehensive examples:
// Get blog post with Amazon product reference
const query = `*[_type == "post" && defined(amazonProduct)][0]{
_id,
title,
slug,
amazonProduct->{
_id,
asin,
title,
brand,
price,
currency,
url,
images,
features,
lastSyncedAt
}
}`
const post = await sanityClient.fetch(query)
console.log(post.amazonProduct.title) // Amazon product title
console.log(post.amazonProduct.price) // Amazon product price
Get product by ASIN:
const query = `*[_type == "amazon.product" && asin == $asin][0]{
_id, asin, title, brand, price, currency, url, images, features, lastSyncedAt
}`
const product = await sanityClient.fetch(query, { asin: "B09XFQL45V" })
Get product by Reference ID:
const query = `*[_type == "amazon.product" && _id == $referenceId][0]{
_id, asin, title, brand, price, currency, url, images, features, lastSyncedAt
}`
const product = await sanityClient.fetch(query, { referenceId: "3f995870-12ea-4d02-b242-ce78abfbf56e" })
Unified query (either ASIN or Reference ID):
const query = `*[_type == "amazon.product" && (
($asin != null && asin == $asin) ||
($referenceId != null && _id == $referenceId)
)][0]{
_id, asin, title, brand, price, currency, url, images, features, lastSyncedAt
}`
// Use with ASIN
const productByAsin = await sanityClient.fetch(query, { asin: "B09XFQL45V", referenceId: null })
// Use with Reference ID
const productByRef = await sanityClient.fetch(query, { asin: null, referenceId: "3f995870-12ea-4d02-b242-ce78abfbf56e" })
// Get comparison with multiple Amazon products
const query = `*[_type == "comparison"][0]{
title,
amazonProducts[]->{
asin,
title,
brand,
price,
currency,
url,
images[0].url,
features[0..2]
},
"productCount": count(amazonProducts)
}`
const comparison = await sanityClient.fetch(query)
comparison.amazonProducts.forEach(product => {
console.log(`${product.title}: ${product.price}`)
})
// Find posts featuring specific Amazon product
const postsByProduct = `*[_type == "post" && amazonProduct->asin == $asin]{
title,
slug,
amazonProduct->{
asin,
title,
price
}
}`
const posts = await sanityClient.fetch(postsByProduct, { asin: 'B0F15TM77B' })
// Find posts with products from specific brand
const postsByBrand = `*[_type == "post" && amazonProduct->brand match $brand + "*"]{
title,
amazonProduct->{
asin,
title,
brand,
price
}
}`
const applePosts = await sanityClient.fetch(postsByBrand, { brand: 'Apple' })
// Get product reviews with ratings
const reviewsQuery = `*[_type == "review" && defined(amazonProduct)] | order(rating desc){
title,
rating,
pros,
cons,
amazonProduct->{
asin,
title,
brand,
price,
url,
images[0].url
}
}`
const reviews = await sanityClient.fetch(reviewsQuery)
// Get average rating for specific product
const ratingQuery = `{
"product": *[_type == "amazon.product" && asin == $asin][0]{
asin,
title,
price
},
"averageRating": math::avg(*[_type == "review" && amazonProduct->asin == $asin].rating),
"reviewCount": count(*[_type == "review" && amazonProduct->asin == $asin])
}`
const productStats = await sanityClient.fetch(ratingQuery, { asin: 'B0F15TM77B' })
// Get comparison with price analysis
const analysisQuery = `*[_type == "comparison"][0]{
title,
amazonProducts[]->{
asin,
title,
brand,
price,
currency
},
"lowestPrice": amazonProducts[]->price | order(@) | [0],
"highestPrice": amazonProducts[]->price | order(@ desc) | [0],
"brands": array::unique(amazonProducts[]->brand)
}`
const analysis = await sanityClient.fetch(analysisQuery)
console.log(`Price range: ${analysis.lowestPrice} - ${analysis.highestPrice}`)
interface PostWithProduct {
_id: string
title: string
slug: string
amazonProduct: {
asin: string
title: string
brand: string
price: string
currency: string
url: string
images: Array<{ url: string; width: number; height: number }>
features: string[]
}
}
// Use with type safety
const post: PostWithProduct = await sanityClient.fetch(query)
For more GROQ query examples, check the examples/groqQueries.ts
file in the plugin.
The plugin includes a flexible React component for displaying Amazon products in your frontend applications.
import AmazonProductDisplay from './AmazonProductDisplay'
import { createClient } from '@sanity/client'
const client = createClient({
projectId: 'your-project-id',
dataset: 'production',
apiVersion: '2025-01-01',
useCdn: true,
})
// Option 1: Display product from reference field
function BlogPost({ post }) {
return (
<div>
<h1>{post.title}</h1>
{post.amazonProduct && (
<AmazonProductDisplay
product={post.amazonProduct}
layout="card"
imageSize="medium"
/>
)}
</div>
)
}
// Option 2: Display product by ASIN lookup
function ProductPage({ asin }) {
return (
<AmazonProductDisplay
asin={asin}
client={client}
layout="horizontal"
imageSize="large"
onProductClick={(product) => {
console.log('Product clicked:', product.title)
}}
/>
)
}
interface AmazonProductDisplayProps {
// Data source (choose one)
product?: AmazonProduct // Direct product data
asin?: string // ASIN for lookup
referenceId?: string // Reference ID for lookup
client?: SanityClient // Required when using asin or referenceId
// Display options
showFeatures?: boolean // Show feature list (default: true)
showImages?: boolean // Show product image (default: true)
showPrice?: boolean // Show pricing (default: true)
showBrand?: boolean // Show brand name (default: true)
showCTA?: boolean // Show "View on Amazon" button (default: true)
// Styling options
layout?: 'horizontal' | 'vertical' | 'card' // Layout style (default: 'card')
imageSize?: 'small' | 'medium' | 'large' // Image size (default: 'medium')
className?: string // Custom CSS classes
ctaText?: string // Custom CTA button text (default: 'View on Amazon')
// Development options
debug?: boolean // Enable debug mode (default: false)
// Interaction
onProductClick?: (product: AmazonProduct) => void // Click handler
}
The component supports 3 different ways to display products:
1. By ASIN Lookup:
import { AmazonProductByASIN } from '@/components/AmazonProductDisplay'
import { sanityClient } from '@/lib/sanity-client'
<AmazonProductByASIN
asin="B09XFQL45V"
client={sanityClient}
layout="card"
debug={true}
/>
2. By Reference ID Lookup:
import { AmazonProductByReferenceId } from '@/components/AmazonProductDisplay'
import { sanityClient } from '@/lib/sanity-client'
<AmazonProductByReferenceId
referenceId="3f995870-12ea-4d02-b242-ce78abfbf56e"
client={sanityClient}
layout="card"
debug={true}
/>
3. Unified Component (accepts any parameter):
import AmazonProductDisplay from '@/components/AmazonProductDisplay'
import { sanityClient } from '@/lib/sanity-client'
// Option A: ASIN lookup
<AmazonProductDisplay asin="B09XFQL45V" client={sanityClient} />
// Option B: Reference ID lookup
<AmazonProductDisplay referenceId="3f995870..." client={sanityClient} />
// Option C: Direct product data
<AmazonProductDisplay product={productData} />
All lookup methods use one optimized GROQ query:
*[_type == "amazon.product" && (
($asin != null && asin == $asin) ||
($referenceId != null && _id == $referenceId)
)][0]{
_id, asin, title, brand, price, currency, url, images, features, lastSyncedAt
}
4. Blog Post with Featured Product:
function BlogPostWithProduct({ slug }) {
const [post, setPost] = useState(null)
useEffect(() => {
const query = `*[_type == "post" && slug.current == $slug][0]{
title, content,
amazonProduct->{ asin, title, brand, price, url, images, features }
}`
client.fetch(query, { slug }).then(setPost)
}, [slug])
return (
<article>
<h1>{post?.title}</h1>
{post?.amazonProduct && (
<AmazonProductDisplay
product={post.amazonProduct}
layout="horizontal"
imageSize="large"
/>
)}
</article>
)
}
Product Comparison Grid:
function ProductComparison({ comparisonId }) {
const [comparison, setComparison] = useState(null)
useEffect(() => {
const query = `*[_type == "comparison" && _id == $comparisonId][0]{
title,
amazonProducts[]->{ asin, title, brand, price, url, images, features }
}`
client.fetch(query, { comparisonId }).then(setComparison)
}, [comparisonId])
return (
<div>
<h1>{comparison?.title}</h1>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
{comparison?.amazonProducts.map((product) => (
<AmazonProductDisplay
key={product.asin}
product={product}
layout="card"
showFeatures={false}
/>
))}
</div>
</div>
)
}
Direct ASIN Lookup:
function ProductWidget({ asin }) {
return (
<div className="bg-gray-50 p-4 rounded">
<h3>Recommended Product</h3>
<AmazonProductDisplay
asin={asin}
client={client}
layout="horizontal"
imageSize="small"
showFeatures={false}
/>
</div>
)
}
Product Review with Rating:
function ProductReview({ reviewId }) {
const [review, setReview] = useState(null)
useEffect(() => {
const query = `*[_type == "review" && _id == $reviewId][0]{
title, rating, pros, cons,
amazonProduct->{ asin, title, brand, price, url, images }
}`
client.fetch(query, { reviewId }).then(setReview)
}, [reviewId])
return (
<div className="grid grid-cols-2 gap-8">
<AmazonProductDisplay
product={review?.amazonProduct}
layout="card"
ctaText="Buy Now"
/>
<div>
<h1>{review?.title}</h1>
<div className="rating">Rating: {review?.rating}/5</div>
{/* Review content */}
</div>
</div>
)
}
The component includes default Tailwind CSS classes and can be customized with:
import './AmazonProductDisplay.css'
<AmazonProductDisplay
className="my-custom-product-card shadow-lg"
// ... other props
/>
card
- Card layout with border and shadowhorizontal
- Side-by-side image and contentvertical
- Stacked image above contentFor immediate setup, copy the complete working example:
# 1. Copy the complete Next.js example files
cp examples/complete-nextjs-example/page.tsx src/app/page.tsx
cp examples/components/AmazonProductDisplay.tsx src/components/
cp examples/frontend-integration/sanity-client-setup.ts src/lib/sanity-client.ts
# 2. Install required dependencies
npm install @sanity/client @sanity/image-url
# 3. Configure environment variables
cp examples/complete-nextjs-example/env-template.txt .env.local
# Edit .env.local with your actual Sanity project details
# 4. Start development server
npm run dev
http://localhost:3000
(or your configured port)Complete Working Examples:
examples/complete-nextjs-example/
- π Full Next.js integration
page.tsx
- Complete page demonstrating ASIN & Reference ID lookupspackage.json
- All required dependenciesenv-template.txt
- Environment variables templateSETUP_GUIDE.md
- π Detailed step-by-step setup guideIndividual Components:
examples/components/AmazonProductDisplay.tsx
- Main component with dual parameter supportexamples/components/AmazonProductDisplay.css
- Optional stylingexamples/frontend-integration/sanity-client-setup.ts
- Production-ready Sanity clientSchema Integration:
examples/productReferenceField.ts
- Reusable reference field snippetsexamples/exampleSchema.ts
- Blog post & comparison schema examplesexamples/groqQueries.ts
- Comprehensive GROQ query examplesAPI Integration (Optional):
examples/frontend-integration/amazon-client.ts
- Amazon PA-API clientexamples/frontend-integration/nextjs-api-route.ts
- API route examplesexamples/frontend-integration/fetch-product-api-route.ts
- Product fetch APITroubleshooting:
examples/environment-setup.md
- Environment setup & common issuesOpen your Sanity Studio
Go to "Amazon Settings" from the sidebar
Fill in your Amazon PA-API credentials:
B0F15TM77B
)Click "Test API Connection" to verify your setup
Go to "Amazon Products" from the sidebar
Click "Add new" to create a product document
Enter an ASIN number in the ASIN field
Navigate to the "Actions" tab
Click "Fetch from Amazon" button
The document will be auto-populated with:
Review and edit the data as needed
Click "Publish" when ready
NEW: Product titles now automatically generate URL-friendly slugs!
https://share.cleanshot.com/n2tnCdSDwsW11cwYH7dq
Creating a new Amazon product document
You can also manually enter product information without fetching from Amazon. All fields are editable.
amazon.settings
)API Settings:
region
- Amazon marketplace regionaccessKey
- PA-API access keysecretKey
- PA-API secret keypartnerTag
- Amazon Associate tagasinNumber
- Test ASIN for API validationcacheHours
- Cache duration (1-168 hours)Display Settings:
showTitle
- Show/hide product titleshowImage
- Show/hide product imageshowFeatures
- Show/hide feature listshowPrice
- Show/hide pricingshowCtaLink
- Show/hide CTA linkActions:
NEW: Real-time form validation using useFormValue
- buttons enable/disable instantly as you type!
https://share.cleanshot.com/P2LbmkdNCJcwJFf14CHC
Amazon Settings actions panel with Test API Connection button
amazon.product
)Product Information:
asin
- Amazon Standard Identification Numbertitle
- Product titleslug
- Auto-generated URL-friendly slug from titlebrand
- Brand/manufacturer namepermalink
- Direct Amazon product URLshortDescription
- Product description (HTML stripped)type
- Product type: 'simple', 'variable', 'grouped', 'external'featured
- Featured product statussku
- Stock keeping unitPricing:
regularPrice
- Regular pricesalePrice
- Sale price (if on sale)price
- Current selling priceMedia & Ratings:
primaryImage
- Primary product image URLaverageRating
- Average customer rating (0-5)ratingCount
- Number of customer ratingsstockStatus
- Stock status: 'instock', 'outofstock', 'onbackorder'Metadata:
lastSyncedAt
- Timestamp of last Amazon syncActions:
useFormValue
for instant feedback and button state updatesAmazonSettingsActions.tsx:
useFormValue
hooks for immediate validationSchema Simplification:
description
(Portable Text)slug
, type
, featured
, stockStatus
Component Cleanup:
AmazonAsinInput.tsx
(redundant functionality)AmazonProductFetch.tsx
(unused component)AmazonFetchButton.tsx
as the primary fetch componentConfiguration System:
src/lib/config.ts
for centralized environment variable handlingThe plugin now uses a streamlined component architecture:
The following components were removed for cleaner architecture:
cd plugins/@multidots/sanity-plugin-amazon-product-sync
npm install
npm run build
cd your-frontend-app
npm install
npm run dev
cd your-sanity-project
npm run dev
"No product available" despite correct ASIN
<AmazonProductByASIN
asin="B09XFQL45V"
client={sanityClient}
debug={true} // Shows debug panel with manual fetch button
/>
Environment variables not working
process.env.NEXT_PUBLIC_SANITY_PROJECT_ID
is undefined.env.local
exists with correct variables# .env.local
NEXT_PUBLIC_SANITY_PROJECT_ID=your-project-id
NEXT_PUBLIC_SANITY_DATASET=production
SANITY_API_TOKEN=your-token
NEXT_PUBLIC_
prefix for client-side variablesCORS errors when fetching from Sanity
export const sanityClient = createClient({
projectId: process.env.NEXT_PUBLIC_SANITY_PROJECT_ID!,
dataset: 'production',
apiVersion: '2025-01-01',
useCdn: true, // Important for client-side
// NO token needed for public documents
})
useEffect not triggering automatically
useEffect(() => {
console.log('useEffect conditions:', {
hasInitialProduct: !!initialProduct,
hasAsin: !!asin,
hasClient: !!client
})
}, [asin, client, initialProduct])
"Amazon API credentials not configured"
amazon.settings
"InvalidSignature" error
Product documents not accessible
curl -X POST "https://YOUR_PROJECT_ID.api.sanity.io/v2025-01-01/data/query/production" \
-H "Content-Type: application/json" \
-d '{"query": "*[_type == \"amazon.product\"][0]{asin, title}"}'
The component includes built-in debugging tools:
// Enable debug mode to see detailed information
<AmazonProductDisplay
asin="B09XFQL45V"
client={sanityClient}
debug={process.env.NODE_ENV === 'development'}
/>
Debug features:
Test Sanity Connection First
import { testSanityConnection } from './lib/sanity-client-setup'
testSanityConnection('B09XFQL45V').then(result => {
console.log('Sanity test:', result)
})
Create Test Products
See examples/environment-setup.md
for the script
Use Debug Mode
<AmazonProductByASIN debug={true} />
Check Browser Console
MIT
For issues and questions:
Note: This plugin requires an active Amazon Product Advertising API account and valid credentials. The PA-API has rate limits and approval requirements.
FAQs
Sanity Studio plugin to fetch and embed Amazon products by ASIN (PA-API v5), with settings tool and document actions.
The npm package @multidots/sanity-plugin-amazon-product-sync receives a total of 198 weekly downloads. As such, @multidots/sanity-plugin-amazon-product-sync popularity was classified as not popular.
We found that @multidots/sanity-plugin-amazon-product-sync demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago.Β It has 1 open source maintainer collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Security News
This episode explores the hard problem of reachability analysis, from static analysis limits to handling dynamic languages and massive dependency trees.
Security News
/Research
Malicious Nx npm versions stole secrets and wallet info using AI CLI tools; Socketβs AI scanner detected the supply chain attack and flagged the malware.
Security News
CISAβs 2025 draft SBOM guidance adds new fields like hashes, licenses, and tool metadata to make software inventories more actionable.