
Security News
Risky Biz Podcast: AI Agents Are Raising the Stakes for Software Supply Chain Security
Open source attacks are accelerating as AI coding agents pull in dependencies faster, with less human review.
@01.software/sdk
Advanced tools
Official TypeScript SDK for the 01.software platform.
npm install @01.software/sdk
# or
pnpm add @01.software/sdk
./server, ./webhook, ./realtime, ./ui/*) for tree-shakingcollections.from() for Client (compile-time write prevention)// Analytics — browser pageview tracking + custom events
import { createAnalytics } from '@01.software/sdk/analytics'
const analytics = createAnalytics({ publishableKey: 'pk_xxx' })
// auto-tracks pageviews; call analytics.pageview('/custom-path') manually if needed
analytics.track('signup', { plan: 'pro', trial: false }) // custom event with optional props
// Main entry - browser client, query builder, hooks, utilities
import { createClient } from '@01.software/sdk'
// Server-only entry - avoids importing browser Client APIs
import { createServerClient } from '@01.software/sdk/server'
// Webhook only - webhook handlers
import {
handleWebhook,
createTypedWebhookHandler,
} from '@01.software/sdk/webhook'
// Realtime only
import { createRealtimeClient } from '@01.software/sdk/realtime'
// Components - sub-path imports per domain
import { RichTextContent } from '@01.software/sdk/ui/rich-text'
import { Image } from '@01.software/sdk/ui/image'
import { FormRenderer } from '@01.software/sdk/ui/form'
import { CodeBlock } from '@01.software/sdk/ui/code-block'
import { CanvasRenderer } from '@01.software/sdk/ui/canvas'
import { VideoPlayer } from '@01.software/sdk/ui/video'
import { createClient } from '@01.software/sdk'
const client = createClient({
publishableKey: process.env.NEXT_PUBLIC_SOFTWARE_PUBLISHABLE_KEY,
})
// Query data (returns Payload native response)
const { docs } = await client.collections.from('products').find({
limit: 10,
where: { status: { equals: 'published' } },
})
import { createServerClient } from '@01.software/sdk/server'
const server = createServerClient({
publishableKey: process.env.SOFTWARE_PUBLISHABLE_KEY,
secretKey: process.env.SOFTWARE_SECRET_KEY, // sk01_... opaque API key from Console
})
// Create order (server only)
const order = await server.commerce.orders.create({
orderNumber: generateOrderNumber(),
customerSnapshot: { email: 'user@example.com' },
shippingAddress: { recipientName: 'John', phone: '010-1234-5678', postalCode: '12345', address1: 'Seoul', address2: 'Apt 101' },
orderItems: [...],
totalAmount: 10000,
pgPaymentId: 'pay_123', // optional (omit for free orders)
discountCode: 'WELCOME10', // optional
})
// SSR prefetch (server)
await server.query.prefetchQuery({
collection: 'products',
options: { limit: 10 },
})
const client = createClient({
publishableKey: string, // Required
})
const server = createServerClient({
publishableKey: string,
secretKey: string, // sk01_... or pat01_...
})
| Option | Type | Description |
|---|---|---|
publishableKey | string | API publishable key |
secretKey | string | API secret key (server only) |
API URL은 환경변수로 오버라이드 가능합니다:
SOFTWARE_API_URL (서버용) 또는 NEXT_PUBLIC_SOFTWARE_API_URL (브라우저용)@dev 태그)는 api-dev.01.software, 정식 릴리즈는 api.01.softwareAccess collections via client.collections.from(slug).
Note:
client.collections.from()returns aReadOnlyQueryBuilder(onlyfind,findById,count,findMetadata,findMetadataById). Write operations (create,update,remove,updateMany,removeMany) are only available onserver.collections.from().
// List query - returns PayloadFindResponse
const { docs, totalDocs, hasNextPage } = await client.collections
.from('products')
.find({
limit: 20,
page: 1,
sort: '-createdAt',
where: { status: { equals: 'published' } },
depth: 2,
select: { title: true, slug: true },
})
// Query with populate/joins control
const { docs } = await client.collections.from('products').find({
select: { title: true, slug: true, price: true, thumbnail: true },
joins: false, // disable joins for lightweight list
})
// Override relationship populate
const product = await client.collections.from('products').findById(id, {
populate: { brands: { name: true, logo: true } },
joins: { variants: { limit: 50 } },
})
// Single item query - returns document directly
const product = await client.collections.from('products').findById('id')
// Create (server only) - returns PayloadMutationResponse
const { doc, message } = await server.collections
.from('products')
.create({ name: 'Product' })
// Create with file upload (server only) - uses multipart/form-data
const { doc } = await server.collections
.from('images')
.create({ alt: 'Hero image' }, { file: imageFile, filename: 'hero.jpg' })
// Update (server only) - returns PayloadMutationResponse
const { doc } = await server.collections
.from('products')
.update('id', { name: 'Updated' })
// Update with file replacement (server only)
await server.collections
.from('images')
.update('id', { alt: 'New alt' }, { file: newFile })
// Delete (server only) - returns document directly
const deletedDoc = await server.collections.from('products').remove('id')
// Count
const { totalDocs } = await client.collections.from('products').count()
// SEO Metadata (fetch + generate in one call, depth: 1 auto-applied)
const metadata = await client.collections
.from('products')
.findMetadata(
{ where: { slug: { equals: 'my-product' } } },
{ siteName: 'My Store' },
)
const metadataById = await client.collections
.from('products')
.findMetadataById('id', {
siteName: 'My Store',
})
// Bulk operations (server only)
await server.collections.from('products').updateMany(where, data)
await server.collections.from('products').removeMany(where)
The SDK returns Payload CMS native response types without wrapping:
// find() returns PayloadFindResponse<T>
interface PayloadFindResponse<T> {
docs: T[]
totalDocs: number
limit: number
totalPages: number
page: number
pagingCounter: number
hasPrevPage: boolean
hasNextPage: boolean
prevPage: number | null
nextPage: number | null
}
// create() / update() returns PayloadMutationResponse<T>
interface PayloadMutationResponse<T> {
message: string
doc: T
errors?: unknown[]
}
// findById() / remove() returns T (document directly)
| Operation | Response Type |
|---|---|
find() | PayloadFindResponse<T> - { docs, totalDocs, hasNextPage, ... } |
findById() | T - document object directly |
create() | PayloadMutationResponse<T> - { doc, message } |
update() | PayloadMutationResponse<T> - { doc, message } |
remove() | T - deleted document object directly |
count() | { totalDocs: number } |
findMetadata() | Metadata | null - Next.js Metadata (null if no match) |
findMetadataById() | Metadata - Next.js Metadata (throws on 404) |
Read hooks are available on both Client and ServerClient via client.query.*. Mutation hooks (useCreate, useUpdate, useRemove) are only available on ServerClient.
// List query
const { data, isLoading } = client.query.useQuery({
collection: 'products',
options: { limit: 10 },
})
// Suspense mode
const { data } = client.query.useSuspenseQuery({
collection: 'products',
options: { limit: 10 },
})
// Query by ID
const { data } = client.query.useQueryById({
collection: 'products',
id: 'product_id',
})
// Infinite scroll
const { data, fetchNextPage, hasNextPage } = client.query.useInfiniteQuery({
collection: 'products',
options: { limit: 20 },
})
// Mutation hooks — ServerClient only (auto-invalidate cache on success)
const { mutate: create } = client.query.useCreate({ collection: 'images' })
const { mutate: update } = client.query.useUpdate({ collection: 'products' })
const { mutate: remove } = client.query.useRemove({ collection: 'products' })
create({ data: { alt: 'Hero' }, file: imageFile, filename: 'hero.jpg' })
update({ id: 'product_id', data: { title: 'Updated' } })
remove('product_id')
// SSR Prefetch
await client.query.prefetchQuery({
collection: 'products',
options: { limit: 10 },
})
await client.query.prefetchQueryById({
collection: 'products',
id: 'product_id',
})
await client.query.prefetchInfiniteQuery({
collection: 'products',
pageSize: 20,
})
// Cache utilities
client.query.invalidateQueries('products')
client.query.getQueryData('products', 'list', options)
client.query.setQueryData('products', 'detail', id, data)
// Customer auth hooks (Client only)
const { data: profile } = client.query.useCustomerMe()
const { mutate: login } = client.query.useCustomerLogin()
const { mutate: register } = client.query.useCustomerRegister()
const { mutate: logout } = client.query.useCustomerLogout()
login({ email: 'user@example.com', password: 'password' })
// Other customer mutations
client.query.useCustomerForgotPassword()
client.query.useCustomerResetPassword()
client.query.useCustomerChangePassword()
// Customer cache utilities
client.query.invalidateCustomerQueries()
client.query.getCustomerData()
client.query.setCustomerData(profile)
Available on Client via client.customer.auth.*.
const client = createClient({
publishableKey: process.env.NEXT_PUBLIC_SOFTWARE_PUBLISHABLE_KEY,
customer: { persist: true },
})
// Register & login
const { customer } = await client.customer.auth.register({
name: 'John',
email: 'john@example.com',
password: 'secure123',
})
const { token, customer } = await client.customer.auth.login({
email: 'john@example.com',
password: 'secure123',
})
// Profile & token management
const profile = await client.customer.auth.me()
client.customer.auth.isAuthenticated()
client.customer.auth.logout()
// Authenticated customer's own orders (Client-only)
const orders = await client.commerce.orders.listMine({
page: 1,
limit: 10,
status: 'paid',
})
// Password
await client.customer.auth.forgotPassword('john@example.com')
await client.customer.auth.resetPassword(token, newPassword)
await client.customer.auth.changePassword(currentPassword, newPassword)
Available on ServerClient via server.commerce.orders.*. Only checkout and listMine are also on Client.
// Orders
await server.commerce.orders.create(params)
await server.commerce.orders.update({ orderNumber, status })
await server.commerce.orders.checkout({ cartId, pgPaymentId?, orderNumber, customerSnapshot, discountCode? })
// Single-order lookup (getOrder endpoint removed — use collection find)
const { docs: [order] } = await server.collections.from('orders').find({
where: { orderNumber: { equals: 'ORD-...' } },
limit: 1,
depth: 1,
})
// Fulfillment & transactions
await server.commerce.orders.createFulfillment({ orderNumber, carrier, trackingNumber, items })
await server.commerce.orders.bulkImportFulfillments({ items: [{ orderNumber, carrier?, trackingNumber? }] })
await server.commerce.orders.updateTransaction({ pgPaymentId, status, paymentMethod, receiptUrl })
// Returns
await server.commerce.orders.createReturn({ orderNumber, returnItems, refundAmount, reason? })
await server.commerce.orders.updateReturn({ returnId, status })
await server.commerce.orders.returnWithRefund({ orderNumber, returnItems, refundAmount, pgPaymentId })
Available on both Client (customer JWT) and ServerClient (secretKey).
await client.commerce.discounts.validate({ code, orderAmount })
await client.commerce.shipping.calculate({ shippingPolicyId?, orderAmount, postalCode? })
// calculateShipping returns: { shippingAmount, baseShippingAmount, extraShippingAmount, freeShipping, freeShippingMinAmount, isJeju, isRemoteIsland }
Available on both Client and ServerClient via commerce.product.*.
// Batch stock check (point-in-time read, NOT a reservation)
const { results, allAvailable } = await client.commerce.product.stockCheck({
items: [{ variantId: '...', quantity: 2 }],
})
Available on both Client and ServerClient via commerce.cart.*.
// Add item to cart
await client.commerce.cart.addItem({
cartId,
product,
variant,
option,
quantity,
})
// Update item quantity
await client.commerce.cart.updateItem({ cartItemId, quantity })
// Remove item from cart
await client.commerce.cart.removeItem({ cartItemId })
// Other cart operations
await client.commerce.cart.get(cartId)
await client.commerce.cart.applyDiscount({ cartId, discountCode })
await client.commerce.cart.removeDiscount({ cartId })
await client.commerce.cart.clear({ cartId })
Available only on ServerClient via server.community.moderation.*.
await server.community.moderation.banCustomer({ customerId, isPermanent?, bannedUntil?, reason? })
await server.community.moderation.unbanCustomer({ customerId })
import {
handleWebhook,
createCustomerAuthWebhookHandler,
createTypedWebhookHandler,
} from '@01.software/sdk'
// Basic handler
export async function POST(request: Request) {
return handleWebhook(request, async (event) => {
console.log(event.collection, event.operation, event.data)
})
}
// With HMAC-SHA256 signature verification (recommended)
export async function POST(request: Request) {
return handleWebhook(request, handler, {
secret: process.env.WEBHOOK_SECRET,
})
}
// Signed deliveries include x-webhook-signature, x-webhook-timestamp,
// and x-webhook-delivery-id. handleWebhook rejects stale or unsigned
// deliveries when secret is set.
// Type-safe handler
const handler = createTypedWebhookHandler('orders', async (event) => {
// event.data is typed as Order
console.log(event.data.orderNumber)
})
// Customer auth helper
const customerAuthHandler = createCustomerAuthWebhookHandler({
passwordReset: async ({ email, resetPasswordToken }) => {
await sendPasswordResetEmail(email, resetPasswordToken)
},
})
Source of truth: packages/sdk/src/core/collection/const.ts (COLLECTIONS: 75).
| Category | Collections |
|---|---|
| Tenant | tenants, tenant-metadata, tenant-logos |
| Products | products, product-variants, product-options, product-option-values, product-categories, product-tags, product-collections, brands, brand-logos |
| Orders | orders, order-items, returns, return-items, fulfillments, fulfillment-items, transactions |
| Customers | customers, customer-profiles, customer-profile-lists, customer-addresses |
| Carts | carts, cart-items |
| Commerce | discounts, promotions, shipping-policies |
| Content | documents, document-categories, document-types, articles, article-authors, article-categories, article-tags, links, link-categories, link-tags |
| Playlists / Tracks | playlists, playlist-categories, playlist-tags, tracks, track-categories, track-tags |
| Galleries | galleries, gallery-categories, gallery-tags, gallery-items |
| Canvas | canvases, canvas-node-types, canvas-edge-types, canvas-categories, canvas-tags, canvas-nodes, canvas-edges |
| Videos | videos, video-categories, video-tags |
| Live Streams | live-streams |
| Media | images |
| Forms | forms, form-submissions |
| Community | posts, comments, reactions, reaction-types, bookmarks, post-categories, reports, community-bans |
| Events | event-calendars, events, event-categories, event-occurrences, event-tags |
Server-only collections: customer-groups is available from createServerClient().collections for segmentation and campaign targeting, but is intentionally absent from browser collection discovery.
Resolves a Payload CMS relation field value. When depth is 0, relation fields return just an ID (number). When depth > 0, they return the full object. This utility normalizes both cases.
import { resolveRelation } from '@01.software/sdk'
const authors = post.authors?.map((a) => resolveRelation(a)) ?? [] // Author[]
Note: Prefer
resolveRelation. It covers the same normalization use case directly.
import { generateOrderNumber } from '@01.software/sdk'
const orderNumber = generateOrderNumber()
// "260121123456" (YYMMDDRRRRRR format)
import { formatOrderName } from '@01.software/sdk'
formatOrderName([{ product: { title: 'Product A' } }])
// "Product A"
formatOrderName([
{ product: { title: 'Product A' } },
{ product: { title: 'Product B' } },
])
// "Product A 외 1건"
React component for rendering Payload CMS Lexical rich text. Two variants:
RichTextContent — Base component with maximum flexibility (converters, blocks, nodeMap, disableDefaultConverters)StyledRichTextContent — Headless component with slot-based customization (components prop)import {
RichTextContent,
StyledRichTextContent,
richTextNodeMap,
} from '@01.software/sdk/ui/rich-text'
// Base: full converter control
<RichTextContent
data={content}
className="prose"
internalDocToHref={({ linkNode }) => `/articles/${linkNode.fields.doc?.value?.slug}`}
blocks={{
Iframe: ({ node }) => <iframe src={node.fields.url} />,
Player: ({ node }) => <VideoPlayer url={node.fields.url} />,
}}
/>
// Shared Payload view-map rendering for supported lightweight blocks
<RichTextContent data={content} nodeMap={richTextNodeMap} />
// Headless: component slots (Radix-style)
<StyledRichTextContent
data={content}
components={{
Heading: ({ tag: Tag, children }) => (
<Tag className={Tag === 'h1' ? 'text-4xl font-bold' : 'text-2xl'}>{children}</Tag>
),
Link: ({ href, children, target, rel }) => (
<a href={href} target={target} rel={rel} className="text-blue-600 underline">{children}</a>
),
Upload: ({ src, alt, width, height }) => (
<img src={src} alt={alt} width={width} height={height} className="rounded-lg" />
),
}}
/>
The SDK throws typed errors instead of returning error responses:
import { isNetworkError, isApiError, isValidationError } from '@01.software/sdk'
try {
const { docs } = await client.collections.from('products').find()
} catch (error) {
if (isNetworkError(error)) {
console.error('Network issue:', error.message)
} else if (isApiError(error)) {
console.error('API error:', error.status, error.message)
}
}
Error classes: SDKError, ApiError, NetworkError, ValidationError, ConfigError, TimeoutError
NEXT_PUBLIC_SOFTWARE_PUBLISHABLE_KEY=your_publishable_key
SOFTWARE_PUBLISHABLE_KEY=your_publishable_key
SOFTWARE_SECRET_KEY=sk01_... # Server only — opaque API key from Console
secretKeyshould only be used in server environments. Never expose it to the browser.
secretKeymay be ansk01_...API key or apat01_...personal access token. Server SDK calls must also send the matchingpublishableKey; PAT tenant selection is pinned server-side, and callers must not sendX-Tenant-Id.
API keys created without explicit scopes use the default ['read', 'write']. Console API-key creation can request narrower scopes[]; in the first explicit-scope slice tenant admins may grant only read and write. Higher-authority scopes such as webhook, analytics, and super-admin require platform authority and otherwise return scope_grant_forbidden.
SDK 0.9.0: Server auth now uses opaque bearer tokens (
sk01_...). Generate API keys from the Console.createServerToken,createApiKey, andparseApiKeyare no longer part of the SDK surface.
swatchColor, thumbnail, images) to Payload types and ecommerce utility shapes.stock - reservedStock, matching checkout stock checks.New error codes propagated via SDKError.code (no breaking change; existing callers ignore unknown codes safely):
| Code | Phase | Trigger |
|---|---|---|
account_suspended | P1 | Suspended session / sk01_ / pat01_ / customer JWT — 401 |
pat_tenant_header_forbidden | P1 | pat01_ request carrying any X-Tenant-Id header — 401 |
tenant_mismatch | P3 | Cross-tenant FK rejection (forms / community / orders) |
server_derived | P3 | Body-driven write into a server-derived state field — 422 |
scope_denied | P5 | pat01_ whose ApiKeys.scopes lacks the operation |
P5 also adds JWT-jti revocation: revokeCustomerJti(jti, ttl) on the server invalidates a token immediately; subsequent SDK calls receive 401 { code: 'token_revoked' }.
COLLECTIONS and INTERNAL_COLLECTIONS are now both exported from @01.software/sdk. Use INTERNAL_COLLECTIONS to detect admin-only slugs in custom tooling.
Field renames — update any code that reads these fields from API responses:
| Collection | Old | New |
|---|---|---|
| Customers | socialId | providerUserId |
| Customers | loginAttempts | loginAttemptCount |
| Customers | resetPasswordExpiry | resetPasswordExpiresAt |
| Orders, Carts | shippingFee | shippingAmount |
| Carts | itemsTotal | subtotalAmount |
| Transactions | paymentId | pgPaymentId |
| Discounts | type | discountType |
| Discounts | value | discountValue |
| Discounts | usageLimit | maxUses |
| Discounts | usageCount | usesCount |
| Discounts | perCustomerLimit | maxUsesPerCustomer |
| ShippingPolicies | baseFee | baseAmount |
| ShippingPolicies | freeShippingThreshold | freeShippingMinAmount |
| Documents | effectiveDate | effectiveAt |
| Documents | expiryDate | expiresAt |
| Articles | readTime | readingMinutes |
| ApiUsage | count | apiCallCount |
| ApiUsage | storageUsed | storageUsedBytes |
| ApiUsage | totalDocuments | documentCount |
Collection renames:
order-products → order-itemsreturn-products → return-itemsexchanges, exchange-productsproduct-option-valuesBoolean field renames (6 collections):
status: 'active' | 'inactive' → isActive: boolean on Forms, ArticleAuthors, CustomerGroups, ShippingPolicies, ProductVariantsFAQs
01.software SDK
The npm package @01.software/sdk receives a total of 830 weekly downloads. As such, @01.software/sdk popularity was classified as not popular.
We found that @01.software/sdk 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
Open source attacks are accelerating as AI coding agents pull in dependencies faster, with less human review.

Research
/Security News
Malicious Chrome and Firefox extensions posed as free VPNs while stealing clipboard data through later extension updates.

Research
/Security News
Miasma Mini Shai-Hulud hits @immobiliarelabs Backstage plugins, targeting GitLab and LDAP auth packages on npm.