mirror of
https://github.com/saltbo/zpan.git
synced 2026-09-17 16:39:27 +08:00
feat(store): add agent x402 capacity purchases
This commit is contained in:
@@ -17,6 +17,7 @@ export const AGENT_OAUTH_RESOURCE_SCOPES = [
|
||||
AuthorizationScope.SHARES_CREATE,
|
||||
AuthorizationScope.SHARES_DELETE,
|
||||
AuthorizationScope.QUOTA_READ,
|
||||
AuthorizationScope.QUOTA_PURCHASE,
|
||||
AuthorizationScope.STORAGE_USAGE_READ,
|
||||
] as const
|
||||
export const AGENT_OAUTH_SCOPES = [...AGENT_OAUTH_STANDARD_SCOPES, ...AGENT_OAUTH_RESOURCE_SCOPES] as const
|
||||
@@ -29,5 +30,6 @@ export const AGENT_OAUTH_SCOPE_DESCRIPTIONS: Record<(typeof AGENT_OAUTH_RESOURCE
|
||||
[AuthorizationScope.SHARES_CREATE]: 'Create public shares',
|
||||
[AuthorizationScope.SHARES_DELETE]: 'Revoke shares',
|
||||
[AuthorizationScope.QUOTA_READ]: 'Inspect workspace quota',
|
||||
[AuthorizationScope.QUOTA_PURCHASE]: 'Purchase workspace storage capacity',
|
||||
[AuthorizationScope.STORAGE_USAGE_READ]: 'Inspect workspace storage usage',
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ export const AuthorizationScope = {
|
||||
SHARES_CREATE: 'shares:create',
|
||||
SHARES_DELETE: 'shares:delete',
|
||||
QUOTA_READ: 'quota:read',
|
||||
QUOTA_PURCHASE: 'quota:purchase',
|
||||
STORAGE_USAGE_READ: 'storage-usage:read',
|
||||
IMAGES_UPLOAD: 'images:upload',
|
||||
DOWNLOAD_TASKS_READ: 'download-tasks:read',
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { cloudOrderQuotaChangeSchema } from './cloud-store-legacy'
|
||||
|
||||
function deliveryEvent(context: Record<string, unknown>) {
|
||||
return {
|
||||
eventId: 'event-1',
|
||||
eventType: 'commerce.order_item.fulfilled',
|
||||
orderId: 'order-1',
|
||||
orderItemId: 'item-1',
|
||||
productId: 'product-1',
|
||||
productName: 'Storage',
|
||||
quantity: 1,
|
||||
deliverable: { storageBytes: 1024 },
|
||||
target: { orgId: 'org-1' },
|
||||
context: {
|
||||
storeId: 'store-1',
|
||||
paymentProvider: 'x402',
|
||||
...context,
|
||||
},
|
||||
occurredAt: '2026-07-31T00:00:00.000Z',
|
||||
}
|
||||
}
|
||||
|
||||
describe('cloudOrderQuotaChangeSchema', () => {
|
||||
it('accepts ISO billing periods', () => {
|
||||
expect(
|
||||
cloudOrderQuotaChangeSchema.safeParse(
|
||||
deliveryEvent({
|
||||
billingPeriodStart: '2026-07-31T00:00:00.000Z',
|
||||
billingPeriodEnd: '2026-08-31T00:00:00.000Z',
|
||||
}),
|
||||
).success,
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
it('rejects malformed billing periods', () => {
|
||||
expect(
|
||||
cloudOrderQuotaChangeSchema.safeParse(
|
||||
deliveryEvent({
|
||||
billingPeriodStart: 'not-a-date',
|
||||
billingPeriodEnd: '2026-08-31T00:00:00.000Z',
|
||||
}),
|
||||
).success,
|
||||
).toBe(false)
|
||||
})
|
||||
|
||||
it('rejects incomplete billing periods', () => {
|
||||
expect(
|
||||
cloudOrderQuotaChangeSchema.safeParse(
|
||||
deliveryEvent({
|
||||
billingPeriodStart: '2026-07-31T00:00:00.000Z',
|
||||
}),
|
||||
).success,
|
||||
).toBe(false)
|
||||
})
|
||||
})
|
||||
@@ -55,18 +55,29 @@ const storeDeliveryEventSchema = z
|
||||
quantity: z.number().int().positive(),
|
||||
deliverable: z.record(z.string(), z.unknown()),
|
||||
target: z.record(z.string(), z.unknown()).nullable(),
|
||||
context: z.object({
|
||||
storeId: z.string().min(1),
|
||||
paymentProvider: z.enum(['stripe', 'gift_card', 'credits']).nullable(),
|
||||
stripePriceId: z.string().nullable().optional(),
|
||||
stripePriceLookupKey: z.string().nullable().optional(),
|
||||
stripePriceRecurring: z.unknown().optional(),
|
||||
stripePriceMetadata: z.record(z.string(), z.string()).optional(),
|
||||
stripeSubscriptionId: z.string().nullable().optional(),
|
||||
stripeInvoiceId: z.string().nullable().optional(),
|
||||
billingPeriodStart: z.string().nullable().optional(),
|
||||
billingPeriodEnd: z.string().nullable().optional(),
|
||||
}),
|
||||
context: z
|
||||
.object({
|
||||
storeId: z.string().min(1),
|
||||
paymentProvider: z.enum(['stripe', 'gift_card', 'credits', 'x402']).nullable(),
|
||||
providerTransactionId: z.string().nullable().optional(),
|
||||
x402AuditContext: z.record(z.string(), z.unknown()).nullable().optional(),
|
||||
stripePriceId: z.string().nullable().optional(),
|
||||
stripePriceLookupKey: z.string().nullable().optional(),
|
||||
stripePriceRecurring: z.unknown().optional(),
|
||||
stripePriceMetadata: z.record(z.string(), z.string()).optional(),
|
||||
stripeSubscriptionId: z.string().nullable().optional(),
|
||||
stripeInvoiceId: z.string().nullable().optional(),
|
||||
billingPeriodStart: z.string().datetime().nullable().optional(),
|
||||
billingPeriodEnd: z.string().datetime().nullable().optional(),
|
||||
})
|
||||
.superRefine((context, ctx) => {
|
||||
if (Boolean(context.billingPeriodStart) === Boolean(context.billingPeriodEnd)) return
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: [context.billingPeriodStart ? 'billingPeriodEnd' : 'billingPeriodStart'],
|
||||
message: 'Billing period start and end must be provided together',
|
||||
})
|
||||
}),
|
||||
occurredAt: z.string().min(1),
|
||||
})
|
||||
.superRefine((event, ctx) => {
|
||||
@@ -104,6 +115,16 @@ function targetOrgId(target: Record<string, unknown> | null) {
|
||||
function sourceId(event: z.infer<typeof storeDeliveryEventSchema>) {
|
||||
const orgId = targetOrgId(event.target)
|
||||
if (event.context.stripeSubscriptionId) return `stripe_subscription:${event.context.stripeSubscriptionId}:${orgId}`
|
||||
if (event.context.billingPeriodStart && event.context.billingPeriodEnd) {
|
||||
return [
|
||||
event.context.paymentProvider ?? 'commerce',
|
||||
'period',
|
||||
event.productId,
|
||||
event.context.billingPeriodStart,
|
||||
event.context.billingPeriodEnd,
|
||||
orgId,
|
||||
].join(':')
|
||||
}
|
||||
return event.orderId
|
||||
}
|
||||
|
||||
@@ -128,7 +149,12 @@ export const cloudOrderQuotaChangeSchema = z.union([
|
||||
storageBytes: numberDeliverableValue(event.deliverable, 'storageBytes'),
|
||||
trafficBytes: numberDeliverableValue(event.deliverable, 'trafficBytes'),
|
||||
trafficOveragePriceCents: optionalNumberDeliverableValue(event.deliverable, 'trafficOveragePriceCents'),
|
||||
source: event.context.stripeSubscriptionId ? 'stripe_subscription' : 'stripe',
|
||||
source: event.context.billingPeriodStart ? 'commerce_period' : (event.context.paymentProvider ?? 'commerce'),
|
||||
entitlementType: event.context.billingPeriodStart ? ('plan' as const) : ('grant' as const),
|
||||
startsAt: event.context.billingPeriodStart ?? event.occurredAt,
|
||||
paymentProvider: event.context.paymentProvider,
|
||||
providerTransactionId: event.context.providerTransactionId,
|
||||
x402AuditContext: event.context.x402AuditContext,
|
||||
packageId: event.productId,
|
||||
packageName: stringDeliverableValue(event.deliverable, 'packageName') ?? event.productName,
|
||||
occurredAt: event.occurredAt,
|
||||
|
||||
@@ -14,5 +14,6 @@ export const oauthResourceScopeLabels = {
|
||||
[AuthorizationScope.SHARES_CREATE]: 'settings.agentAccess.scope.sharesCreate',
|
||||
[AuthorizationScope.SHARES_DELETE]: 'settings.agentAccess.scope.sharesDelete',
|
||||
[AuthorizationScope.QUOTA_READ]: 'settings.agentAccess.scope.quotaRead',
|
||||
[AuthorizationScope.QUOTA_PURCHASE]: 'settings.agentAccess.scope.quotaPurchase',
|
||||
[AuthorizationScope.STORAGE_USAGE_READ]: 'settings.agentAccess.scope.storageUsageRead',
|
||||
} as const satisfies Record<OAuthResourceScope, string>
|
||||
|
||||
Reference in New Issue
Block a user