mirror of
https://github.com/djdevin/recflare.git
synced 2026-09-08 22:51:30 -07:00
[commerce] stub out commerce, for historical purposes
This commit is contained in:
@@ -6,12 +6,20 @@ A Cloudflare Workers application using Hono
|
|||||||
|
|
||||||
- `GET /purchase/v1/hasspentmoney` — whether the player has ever spent money;
|
- `GET /purchase/v1/hasspentmoney` — whether the player has ever spent money;
|
||||||
`false`.
|
`false`.
|
||||||
|
- `POST /purchase/v1/initiatepurchase` — begins a purchase, answering
|
||||||
|
`{ "transactionId": 1234567890 }`. Nothing is charged and no transaction is
|
||||||
|
recorded, so the id is a fixed placeholder and the posted body is ignored.
|
||||||
- `GET /api/catalog/v1/all` — the purchasable SKU catalog (token packs, special
|
- `GET /api/catalog/v1/all` — the purchasable SKU catalog (token packs, special
|
||||||
offers), served from the bundled `static/catalog-v1-all.json`. The client's
|
offers), served from the bundled `static/catalog-v1-all.json`. The client's
|
||||||
`?onlyAvailableSkus=true` is accepted and ignored: the bundled catalog already
|
`?onlyAvailableSkus=true` is accepted and ignored: the bundled catalog already
|
||||||
contains only available SKUs.
|
contains only available SKUs.
|
||||||
- `GET /purchasecampaign/allcurrent/v2` — current purchase campaigns
|
- `GET /purchasecampaign/allcurrent/v2` — current purchase campaigns
|
||||||
(limited-time offers/promos); `[]` (none active).
|
(limited-time offers/promos); `[]` (none active).
|
||||||
|
- `GET /reminder/currentTokenBundles/v2` — token-bundle purchase reminders (the
|
||||||
|
"buy more tokens" nudge); `[]` (none to show).
|
||||||
|
- `GET /openapi.json` — the generated OpenAPI 3.1 spec for the routes above.
|
||||||
|
Descriptive only; nothing is validated against it. Also aggregated into the
|
||||||
|
docs UI on `www` at `/docs`.
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
|
|||||||
@@ -16,8 +16,13 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@repo/hono-helpers": "workspace:*",
|
"@repo/hono-helpers": "workspace:*",
|
||||||
|
"@standard-community/standard-json": "0.3.5",
|
||||||
|
"@standard-community/standard-openapi": "0.2.9",
|
||||||
"hono": "4.12.27",
|
"hono": "4.12.27",
|
||||||
"workers-tagged-logger": "1.0.1"
|
"hono-openapi": "1.3.1",
|
||||||
|
"openapi-types": "12.1.3",
|
||||||
|
"workers-tagged-logger": "1.0.1",
|
||||||
|
"zod": "4.4.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@cloudflare/vitest-pool-workers": "0.16.20",
|
"@cloudflare/vitest-pool-workers": "0.16.20",
|
||||||
|
|||||||
@@ -1,9 +1,21 @@
|
|||||||
import { Hono } from 'hono'
|
import { Hono } from 'hono'
|
||||||
|
import { describeRoute, openAPIRouteHandler } from 'hono-openapi'
|
||||||
import { useWorkersLogger } from 'workers-tagged-logger'
|
import { useWorkersLogger } from 'workers-tagged-logger'
|
||||||
|
|
||||||
import { withNotFound, withOnError } from '@repo/hono-helpers'
|
import { withCleanSpec, withNotFound, withOnError } from '@repo/hono-helpers'
|
||||||
|
|
||||||
import catalog from '../static/catalog-v1-all.json'
|
import catalog from '../static/catalog-v1-all.json'
|
||||||
|
import {
|
||||||
|
BareBoolean,
|
||||||
|
boolQuery,
|
||||||
|
CatalogSku,
|
||||||
|
HealthResponse,
|
||||||
|
InitiatePurchaseRequest,
|
||||||
|
InitiatePurchaseResponse,
|
||||||
|
json,
|
||||||
|
JsonArray,
|
||||||
|
jsonBody,
|
||||||
|
} from './openapi'
|
||||||
|
|
||||||
import type { App } from './context'
|
import type { App } from './context'
|
||||||
|
|
||||||
@@ -11,6 +23,14 @@ import type { App } from './context'
|
|||||||
* Commerce routes. The `commerce` prefix maps to this worker's subdomain, so
|
* Commerce routes. The `commerce` prefix maps to this worker's subdomain, so
|
||||||
* method routes are served bare.
|
* method routes are served bare.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The transaction id every purchase initiation answers with. Real money never changes
|
||||||
|
* hands here and nothing is persisted, so the client only needs a well-formed handle to
|
||||||
|
* carry through the rest of its store flow.
|
||||||
|
*/
|
||||||
|
const PLACEHOLDER_TRANSACTION_ID = 1234567890
|
||||||
|
|
||||||
const app = new Hono<App>()
|
const app = new Hono<App>()
|
||||||
.use(
|
.use(
|
||||||
'*',
|
'*',
|
||||||
@@ -25,24 +45,131 @@ const app = new Hono<App>()
|
|||||||
.onError(withOnError())
|
.onError(withOnError())
|
||||||
.notFound(withNotFound())
|
.notFound(withNotFound())
|
||||||
|
|
||||||
.get('/', (c) => c.json({ service: 'commerce', status: 'ok' }))
|
.get(
|
||||||
|
'/',
|
||||||
|
describeRoute({
|
||||||
|
tags: ['Service'],
|
||||||
|
summary: 'Health check',
|
||||||
|
description: 'Liveness probe for the commerce worker. No auth.',
|
||||||
|
responses: { 200: json(HealthResponse, 'Service is up') },
|
||||||
|
}),
|
||||||
|
(c) => c.json({ service: 'commerce', status: 'ok' })
|
||||||
|
)
|
||||||
|
|
||||||
// Whether the player has ever spent money. A 404 here makes the client treat
|
// Whether the player has ever spent money. A 404 here makes the client treat
|
||||||
// it as an error, so we return `false` (no purchases).
|
// it as an error, so we return `false` (no purchases).
|
||||||
.get('/purchase/v1/hasspentmoney', (c) => c.json(false))
|
.get(
|
||||||
|
'/purchase/v1/hasspentmoney',
|
||||||
|
describeRoute({
|
||||||
|
tags: ['Purchase'],
|
||||||
|
summary: 'Whether the player has ever spent money',
|
||||||
|
description: [
|
||||||
|
'Always `false` — nobody buys anything on this server. A 404 here makes the client',
|
||||||
|
'treat the call as an error, so the answer is the bare boolean rather than nothing.',
|
||||||
|
].join(' '),
|
||||||
|
responses: { 200: json(BareBoolean, 'Always false (no purchases)') },
|
||||||
|
}),
|
||||||
|
(c) => c.json(false)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Begin a purchase. The client asks for a transaction handle before it takes the
|
||||||
|
// player to the platform store; nothing is charged or recorded here, so the id is a
|
||||||
|
// fixed placeholder and the posted body is ignored.
|
||||||
|
.post(
|
||||||
|
'/purchase/v1/initiatepurchase',
|
||||||
|
describeRoute({
|
||||||
|
tags: ['Purchase'],
|
||||||
|
summary: 'Begin a purchase',
|
||||||
|
description: [
|
||||||
|
'Hands the client the transaction handle it carries through the rest of the store',
|
||||||
|
'flow. Nothing is charged and no transaction is recorded, so the id is a fixed',
|
||||||
|
'placeholder and the posted body is accepted and ignored — an absent or unparseable',
|
||||||
|
'body is a 200, not a 400.',
|
||||||
|
].join(' '),
|
||||||
|
requestBody: jsonBody(InitiatePurchaseRequest, 'The purchase the player confirmed'),
|
||||||
|
responses: { 200: json(InitiatePurchaseResponse, 'The (placeholder) transaction id') },
|
||||||
|
}),
|
||||||
|
(c) => c.json({ transactionId: PLACEHOLDER_TRANSACTION_ID })
|
||||||
|
)
|
||||||
|
|
||||||
// The purchasable SKU catalog (token packs, special offers), served from the
|
// The purchasable SKU catalog (token packs, special offers), served from the
|
||||||
// bundled static JSON. The client passes `?onlyAvailableSkus=true`; the bundled
|
// bundled static JSON. The client passes `?onlyAvailableSkus=true`; the bundled
|
||||||
// catalog is already only the available SKUs, so the param doesn't change the
|
// catalog is already only the available SKUs, so the param doesn't change the
|
||||||
// response.
|
// response.
|
||||||
.get('/api/catalog/v1/all', (c) => c.json(catalog))
|
.get(
|
||||||
|
'/api/catalog/v1/all',
|
||||||
|
describeRoute({
|
||||||
|
tags: ['Catalog'],
|
||||||
|
summary: 'The purchasable SKU catalog',
|
||||||
|
description: [
|
||||||
|
'The token packs, bundles and special offers the store shows, served from the bundled',
|
||||||
|
'static catalog. The client’s `onlyAvailableSkus` is accepted and ignored: the bundled',
|
||||||
|
'catalog already contains only available SKUs.',
|
||||||
|
].join(' '),
|
||||||
|
parameters: [
|
||||||
|
boolQuery('onlyAvailableSkus', 'Accepted and ignored — the catalog is already filtered'),
|
||||||
|
],
|
||||||
|
responses: { 200: json(CatalogSku.array(), 'Every available SKU') },
|
||||||
|
}),
|
||||||
|
(c) => c.json(catalog)
|
||||||
|
)
|
||||||
|
|
||||||
// Current purchase campaigns (limited-time offers/promos). None exist, and
|
// Current purchase campaigns (limited-time offers/promos). None exist, and
|
||||||
// an empty list is the client's "no active campaigns" state.
|
// an empty list is the client's "no active campaigns" state.
|
||||||
.get('/purchasecampaign/allcurrent/v2', (c) => c.json([]))
|
.get(
|
||||||
|
'/purchasecampaign/allcurrent/v2',
|
||||||
|
describeRoute({
|
||||||
|
tags: ['Purchase'],
|
||||||
|
summary: 'Current purchase campaigns',
|
||||||
|
description: [
|
||||||
|
'Limited-time offers and promos. Always `[]` — none exist, and an empty list is the',
|
||||||
|
'client’s “no active campaigns” state.',
|
||||||
|
].join(' '),
|
||||||
|
responses: { 200: json(JsonArray, 'Always empty (no active campaigns)') },
|
||||||
|
}),
|
||||||
|
(c) => c.json([])
|
||||||
|
)
|
||||||
|
|
||||||
// Token-bundle purchase reminders (the "buy more tokens" nudge). None to show,
|
// Token-bundle purchase reminders (the "buy more tokens" nudge). None to show,
|
||||||
// and an empty list is the client's "no reminders" state.
|
// and an empty list is the client's "no reminders" state.
|
||||||
.get('/reminder/currentTokenBundles/v2', (c) => c.json([]))
|
.get(
|
||||||
|
'/reminder/currentTokenBundles/v2',
|
||||||
|
describeRoute({
|
||||||
|
tags: ['Purchase'],
|
||||||
|
summary: 'Token-bundle purchase reminders',
|
||||||
|
description: [
|
||||||
|
'The “buy more tokens” nudges. Always `[]` — there are none to show, and an empty list',
|
||||||
|
'is the client’s “no reminders” state.',
|
||||||
|
].join(' '),
|
||||||
|
responses: { 200: json(JsonArray, 'Always empty (no reminders)') },
|
||||||
|
}),
|
||||||
|
(c) => c.json([])
|
||||||
|
)
|
||||||
|
|
||||||
|
// The generated spec. Documentation only — no request is validated against it (see
|
||||||
|
// openapi.ts). `hide: true` keeps this route out of its own output.
|
||||||
|
app.get(
|
||||||
|
'/openapi.json',
|
||||||
|
describeRoute({ hide: true }),
|
||||||
|
withCleanSpec(
|
||||||
|
openAPIRouteHandler(app, {
|
||||||
|
documentation: {
|
||||||
|
info: {
|
||||||
|
title: 'recflare commerce',
|
||||||
|
version: '1.0.0',
|
||||||
|
description: [
|
||||||
|
'The store surface for recflare, a private-server reimplementation of the Rec Room',
|
||||||
|
'backend: the SKU catalog the client shows and the purchase calls it makes around it.',
|
||||||
|
'',
|
||||||
|
'No money moves here. There is no store integration and no purchase storage, so the',
|
||||||
|
'catalog is a bundled static asset, the campaign and reminder feeds are empty, and a',
|
||||||
|
'purchase initiation answers with a placeholder transaction id.',
|
||||||
|
].join('\n'),
|
||||||
|
},
|
||||||
|
servers: [{ url: 'https://commerce.recflare.net', description: 'Production' }],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
export default app
|
export default app
|
||||||
|
|||||||
@@ -0,0 +1,118 @@
|
|||||||
|
import { resolver } from 'hono-openapi'
|
||||||
|
import { z } from 'zod'
|
||||||
|
|
||||||
|
import type { OpenAPIV3_1 } from 'openapi-types'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OpenAPI schemas for the commerce worker.
|
||||||
|
*
|
||||||
|
* IMPORTANT: these are DESCRIPTIVE ONLY. They are passed to `describeRoute` to
|
||||||
|
* generate the spec and are never wired into `hono-openapi`'s `validator()`. Same
|
||||||
|
* rationale as the auth/accounts/econ/match/playersettings workers: a reverse-engineered
|
||||||
|
* protocol, lenient handlers, no runtime validation.
|
||||||
|
*
|
||||||
|
* Do NOT add `.meta({ id })` to these schemas — with this hono-openapi + zod v4 setup a
|
||||||
|
* meta'd schema used in a response emits a `$ref` the framework doesn't always hoist
|
||||||
|
* into `components.schemas`, leaving a dangling reference. Leaving meta off makes every
|
||||||
|
* schema inline, which renders correctly in any tool.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** Emit a zod schema as an `application/json` response body. */
|
||||||
|
export function json(schema: z.ZodType, description: string) {
|
||||||
|
return { description, content: { 'application/json': { schema: resolver(schema) } } }
|
||||||
|
}
|
||||||
|
|
||||||
|
function toOpenApiSchema(schema: z.ZodType): OpenAPIV3_1.SchemaObject {
|
||||||
|
const { $schema: _$schema, additionalProperties: _extra, ...jsonSchema } = z.toJSONSchema(schema)
|
||||||
|
return jsonSchema as OpenAPIV3_1.SchemaObject
|
||||||
|
}
|
||||||
|
|
||||||
|
/** An `application/json` request body. */
|
||||||
|
export function jsonBody(schema: z.ZodType, description: string): OpenAPIV3_1.RequestBodyObject {
|
||||||
|
return { description, content: { 'application/json': { schema: toOpenApiSchema(schema) } } }
|
||||||
|
}
|
||||||
|
|
||||||
|
/** An optional boolean query parameter. */
|
||||||
|
export function boolQuery(name: string, description: string): OpenAPIV3_1.ParameterObject {
|
||||||
|
return { name, in: 'query', required: false, description, schema: { type: 'boolean' } }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Loose shapes ----------------------------------------------------------
|
||||||
|
|
||||||
|
/** An opaque JSON object — a body whose fields haven't been reversed yet. */
|
||||||
|
export const JsonObject = z.record(z.string(), z.unknown())
|
||||||
|
/** An opaque JSON array (an empty-list stub). */
|
||||||
|
export const JsonArray = z.array(z.unknown())
|
||||||
|
|
||||||
|
/** A bare JSON boolean — `hasspentmoney` answers `false` with no envelope. */
|
||||||
|
export const BareBoolean = z.boolean()
|
||||||
|
|
||||||
|
// ---- Service ---------------------------------------------------------------
|
||||||
|
|
||||||
|
/** `GET /` — the root health check. */
|
||||||
|
export const HealthResponse = z.object({
|
||||||
|
service: z.literal('commerce'),
|
||||||
|
status: z.literal('ok'),
|
||||||
|
})
|
||||||
|
|
||||||
|
// ---- Catalog ---------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The per-SKU `data` blob. `giftDropIds` are the drops granted when the SKU is redeemed
|
||||||
|
* (empty for the bundles, which grant their contents directly); `message` is the label the
|
||||||
|
* store shows on the purchase.
|
||||||
|
*/
|
||||||
|
export const CatalogSkuData = z.object({
|
||||||
|
giftDropIds: z.array(z.int()),
|
||||||
|
message: z.string(),
|
||||||
|
subscriptionPurchase: z
|
||||||
|
.unknown()
|
||||||
|
.optional()
|
||||||
|
.describe('Present only on the subscription SKU; its shape is not reversed yet'),
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* One purchasable SKU from `GET /api/catalog/v1/all` — a token pack, a bundle or a
|
||||||
|
* special offer. `price` is in cents on the store the client is running against, and the
|
||||||
|
* per-store id fields are only present where that SKU ships on that store, so all of them
|
||||||
|
* are optional except the Oculus/Apple/Google ids the reference catalog always carries.
|
||||||
|
*/
|
||||||
|
export const CatalogSku = z.object({
|
||||||
|
skuId: z.int(),
|
||||||
|
name: z.string(),
|
||||||
|
description: z.string().describe('Often an empty string for token packs'),
|
||||||
|
imageName: z.string().describe('The store tile image; the img worker serves it by name'),
|
||||||
|
price: z.int().describe('Store price in cents, e.g. 99 = $0.99'),
|
||||||
|
oculusSkuId: z.string(),
|
||||||
|
appleProductId: z.string(),
|
||||||
|
googlePlaySkuId: z.string(),
|
||||||
|
picoSkuId: z.string().optional(),
|
||||||
|
xboxProductId: z.string().optional(),
|
||||||
|
xboxStoreId: z.string().optional(),
|
||||||
|
psnProductLabel: z.string().optional(),
|
||||||
|
psnEntitlementLabel: z.string().optional(),
|
||||||
|
nintendoSkuId: z.string().optional(),
|
||||||
|
isSingleUse: z.boolean(),
|
||||||
|
shouldAppearInTokenStore: z.boolean(),
|
||||||
|
dataSchemaVersion: z.int(),
|
||||||
|
data: CatalogSkuData,
|
||||||
|
})
|
||||||
|
|
||||||
|
// ---- Purchase --------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `POST /purchase/v1/initiatepurchase` body — what the client sends when the player
|
||||||
|
* confirms a purchase (the SKU and the store it is being bought on). Accepted and
|
||||||
|
* ignored: the field names have not been reversed yet, and nothing here talks to a store.
|
||||||
|
*/
|
||||||
|
export const InitiatePurchaseRequest = JsonObject.describe(
|
||||||
|
'The client’s purchase-initiation payload; accepted and ignored'
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `POST /purchase/v1/initiatepurchase` — the handle the client carries through the rest
|
||||||
|
* of the store flow. Nothing is persisted, so this is a fixed placeholder id.
|
||||||
|
*/
|
||||||
|
export const InitiatePurchaseResponse = z.object({
|
||||||
|
transactionId: z.int().describe('Placeholder — no transaction is recorded'),
|
||||||
|
})
|
||||||
@@ -18,6 +18,22 @@ describe('commerce endpoints', () => {
|
|||||||
expect(await res.json()).toBe(false)
|
expect(await res.json()).toBe(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('POST /purchase/v1/initiatepurchase returns a transaction id', async () => {
|
||||||
|
const res = await SELF.fetch(`${ORIGIN}/purchase/v1/initiatepurchase`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'content-type': 'application/json' },
|
||||||
|
body: JSON.stringify({ skuId: 178, platform: 'Standalone' }),
|
||||||
|
})
|
||||||
|
expect(res.status).toBe(200)
|
||||||
|
expect(await res.json()).toEqual({ transactionId: 1234567890 })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('POST /purchase/v1/initiatepurchase ignores the body entirely', async () => {
|
||||||
|
const res = await SELF.fetch(`${ORIGIN}/purchase/v1/initiatepurchase`, { method: 'POST' })
|
||||||
|
expect(res.status).toBe(200)
|
||||||
|
expect(await res.json()).toEqual({ transactionId: 1234567890 })
|
||||||
|
})
|
||||||
|
|
||||||
it('GET /api/catalog/v1/all serves the SKU catalog', async () => {
|
it('GET /api/catalog/v1/all serves the SKU catalog', async () => {
|
||||||
const res = await SELF.fetch(`${ORIGIN}/api/catalog/v1/all?onlyAvailableSkus=true`)
|
const res = await SELF.fetch(`${ORIGIN}/api/catalog/v1/all?onlyAvailableSkus=true`)
|
||||||
expect(res.status).toBe(200)
|
expect(res.status).toBe(200)
|
||||||
@@ -38,4 +54,45 @@ describe('commerce endpoints', () => {
|
|||||||
expect(res.status).toBe(200)
|
expect(res.status).toBe(200)
|
||||||
expect(await res.json()).toEqual([])
|
expect(await res.json()).toEqual([])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('GET /openapi.json documents every route', async () => {
|
||||||
|
const res = await SELF.fetch(`${ORIGIN}/openapi.json`)
|
||||||
|
expect(res.status).toBe(200)
|
||||||
|
const spec = (await res.json()) as {
|
||||||
|
openapi: string
|
||||||
|
paths: Record<string, Record<string, { summary?: string }>>
|
||||||
|
}
|
||||||
|
expect(spec.openapi).toMatch(/^3\.1/)
|
||||||
|
|
||||||
|
// The spec route hides itself.
|
||||||
|
expect(spec.paths['/openapi.json']).toBeUndefined()
|
||||||
|
|
||||||
|
// Every route the worker serves is described. This is the drift guard: adding a
|
||||||
|
// route without a describeRoute() block fails here rather than silently shipping
|
||||||
|
// an incomplete spec.
|
||||||
|
const documented = new Set(
|
||||||
|
Object.entries(spec.paths).flatMap(([path, ops]) =>
|
||||||
|
Object.keys(ops).map((method) => `${method.toUpperCase()} ${path}`)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
expect([...documented].sort()).toEqual([
|
||||||
|
'GET /',
|
||||||
|
'GET /api/catalog/v1/all',
|
||||||
|
'GET /purchase/v1/hasspentmoney',
|
||||||
|
'GET /purchasecampaign/allcurrent/v2',
|
||||||
|
'GET /reminder/currentTokenBundles/v2',
|
||||||
|
'POST /purchase/v1/initiatepurchase',
|
||||||
|
])
|
||||||
|
|
||||||
|
// Every operation carries a summary — a path present but undescribed is not
|
||||||
|
// documentation.
|
||||||
|
for (const ops of Object.values(spec.paths)) {
|
||||||
|
for (const op of Object.values(ops)) expect(op.summary).toBeTruthy()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Schemas are inlined rather than $ref'd into components: a `.meta({ id })`'d
|
||||||
|
// schema used in a response emits a $ref this hono-openapi + zod v4 setup does
|
||||||
|
// not always hoist, leaving a dangling reference.
|
||||||
|
expect(JSON.stringify(spec).includes('"$ref"')).toBe(false)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ export const DOCUMENTED_SERVICES: ReadonlyArray<{ slug: string; title: string }>
|
|||||||
{ slug: 'match', title: 'match — matchmaking & presence' },
|
{ slug: 'match', title: 'match — matchmaking & presence' },
|
||||||
{ slug: 'econ', title: 'econ — avatar & economy' },
|
{ slug: 'econ', title: 'econ — avatar & economy' },
|
||||||
{ slug: 'clubs', title: 'clubs — clubs & clubhouses' },
|
{ slug: 'clubs', title: 'clubs — clubs & clubhouses' },
|
||||||
|
{ slug: 'commerce', title: 'commerce — store catalog & purchases' },
|
||||||
{ slug: 'chat', title: 'chat — threads & messages' },
|
{ slug: 'chat', title: 'chat — threads & messages' },
|
||||||
{ slug: 'img', title: 'img — image serving & resizing' },
|
{ slug: 'img', title: 'img — image serving & resizing' },
|
||||||
{ slug: 'cdn', title: 'cdn — binary asset delivery' },
|
{ slug: 'cdn', title: 'cdn — binary asset delivery' },
|
||||||
|
|||||||
Generated
+15
@@ -362,12 +362,27 @@ importers:
|
|||||||
'@repo/hono-helpers':
|
'@repo/hono-helpers':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../packages/hono-helpers
|
version: link:../../packages/hono-helpers
|
||||||
|
'@standard-community/standard-json':
|
||||||
|
specifier: 0.3.5
|
||||||
|
version: 0.3.5(@standard-schema/spec@1.1.0)(@types/json-schema@7.0.15)(quansync@0.2.11)(zod@4.4.3)
|
||||||
|
'@standard-community/standard-openapi':
|
||||||
|
specifier: 0.2.9
|
||||||
|
version: 0.2.9(@standard-community/standard-json@0.3.5(@standard-schema/spec@1.1.0)(@types/json-schema@7.0.15)(quansync@0.2.11)(zod@4.4.3))(@standard-schema/spec@1.1.0)(openapi-types@12.1.3)(zod@4.4.3)
|
||||||
hono:
|
hono:
|
||||||
specifier: 4.12.27
|
specifier: 4.12.27
|
||||||
version: 4.12.27
|
version: 4.12.27
|
||||||
|
hono-openapi:
|
||||||
|
specifier: 1.3.1
|
||||||
|
version: 1.3.1(@hono/standard-validator@0.2.2(@standard-schema/spec@1.1.0)(hono@4.12.27))(@standard-community/standard-json@0.3.5(@standard-schema/spec@1.1.0)(@types/json-schema@7.0.15)(quansync@0.2.11)(zod@4.4.3))(@standard-community/standard-openapi@0.2.9(@standard-community/standard-json@0.3.5(@standard-schema/spec@1.1.0)(@types/json-schema@7.0.15)(quansync@0.2.11)(zod@4.4.3))(@standard-schema/spec@1.1.0)(openapi-types@12.1.3)(zod@4.4.3))(@types/json-schema@7.0.15)(hono@4.12.27)(openapi-types@12.1.3)
|
||||||
|
openapi-types:
|
||||||
|
specifier: 12.1.3
|
||||||
|
version: 12.1.3
|
||||||
workers-tagged-logger:
|
workers-tagged-logger:
|
||||||
specifier: 1.0.1
|
specifier: 1.0.1
|
||||||
version: 1.0.1
|
version: 1.0.1
|
||||||
|
zod:
|
||||||
|
specifier: 4.4.3
|
||||||
|
version: 4.4.3
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@cloudflare/vitest-pool-workers':
|
'@cloudflare/vitest-pool-workers':
|
||||||
specifier: 0.16.20
|
specifier: 0.16.20
|
||||||
|
|||||||
Reference in New Issue
Block a user