mirror of
https://github.com/saltbo/zpan.git
synced 2026-09-19 10:01:12 +08:00
Support independent Pro and Business licensing, migrate Cloud store integration through the SDK, gate Business-only credit billing features, and validate the Cloud store E2E flow.
67 lines
2.1 KiB
TypeScript
67 lines
2.1 KiB
TypeScript
import { verify } from 'paseto-ts/v4'
|
|
import { z } from 'zod'
|
|
import { PUBLIC_KEYS } from './public-keys'
|
|
import { trustedIssuerFromCloudUrl } from './verify'
|
|
|
|
const CLOUD_EVENT_TOKEN_MAX_TTL_SECONDS = 5 * 60
|
|
|
|
const cloudEventTokenSchema = z.object({
|
|
type: z.literal('commerce.fulfillment.token'),
|
|
purpose: z.literal('store.delivery'),
|
|
issuer: z.string().min(1),
|
|
audience: z.string().min(1),
|
|
boundLicenseId: z.string().min(1),
|
|
eventId: z.string().min(1),
|
|
payloadHash: z
|
|
.string()
|
|
.regex(/^[0-9a-f]{64}$/i)
|
|
.optional(),
|
|
issuedAt: z.number().int(),
|
|
notBefore: z.number().int().optional(),
|
|
expiresAt: z.number().int(),
|
|
})
|
|
|
|
export type CloudEventToken = z.infer<typeof cloudEventTokenSchema>
|
|
|
|
export interface VerifyCloudEventTokenOptions {
|
|
cloudBaseUrl: string
|
|
instanceId: string
|
|
boundLicenseId: string
|
|
payloadHash: string
|
|
}
|
|
|
|
export function verifyCloudEventToken(token: string, options: VerifyCloudEventTokenOptions): CloudEventToken | null {
|
|
for (const key of PUBLIC_KEYS) {
|
|
const event = tryVerifyCloudEventToken(token, key, options)
|
|
if (event) return event
|
|
}
|
|
return null
|
|
}
|
|
|
|
function tryVerifyCloudEventToken(
|
|
token: string,
|
|
publicKey: string,
|
|
options: VerifyCloudEventTokenOptions,
|
|
): CloudEventToken | null {
|
|
try {
|
|
const { payload } = verify<Record<string, unknown>>(publicKey, token, { validatePayload: false })
|
|
const parsed = cloudEventTokenSchema.safeParse(payload)
|
|
if (!parsed.success) return null
|
|
|
|
const event = parsed.data
|
|
const now = Math.floor(Date.now() / 1000)
|
|
if (event.issuer !== trustedIssuerFromCloudUrl(options.cloudBaseUrl)) return null
|
|
if (event.audience !== options.instanceId && event.audience !== options.boundLicenseId) return null
|
|
if (event.boundLicenseId !== options.boundLicenseId) return null
|
|
if (event.payloadHash && event.payloadHash !== options.payloadHash) return null
|
|
if (event.issuedAt > now) return null
|
|
if (event.notBefore && event.notBefore > now) return null
|
|
if (event.expiresAt <= now) return null
|
|
if (event.expiresAt - event.issuedAt > CLOUD_EVENT_TOKEN_MAX_TTL_SECONDS) return null
|
|
|
|
return event
|
|
} catch {
|
|
return null
|
|
}
|
|
}
|