diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 682b64a..5b9ac2d 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -40,6 +40,7 @@ jobs: run: | # Exclude redoc HTML from diff check (output varies by platform due to styled-components) git checkout -- docs/gen/redoc.v1.html 2>/dev/null || true + git checkout -- docs/gen/redoc.ingress.v1.html 2>/dev/null || true if [ -n "$(git status --porcelain)" ]; then echo "::error::Generated files are out of date. Please run 'make check' locally and commit the changes." diff --git a/docs/config/swagger-merger-ingress-config.json b/docs/config/swagger-merger-ingress-config.json new file mode 100644 index 0000000..d0947f0 --- /dev/null +++ b/docs/config/swagger-merger-ingress-config.json @@ -0,0 +1,31 @@ +{ + "swagger": "2.0", + "info": { + "title": "Stash Managed Catalog API", + "description": "Read-only API for querying the Stash managed product catalog. Designed for server-to-server communication between partner backends and Stash services.\n\n## Base URLs\n\n| Environment | Base URL |\n|---|---|\n| **Test** | `https://test-api.stash.gg` |\n| **Production** | `https://api.stash.gg` |\n\nAll endpoint paths below are relative to the base URL. For example, to list products in the test environment:\n\n```\nGET https://test-api.stash.gg/api/v1/studio/{shop_id}/products\n```\n\n## Authentication\n\nAll requests must include an HMAC-SHA256 signature in the `stash-hmac-signature` header.\n\n### Setup\n\n1. Go to **Stash Studio > Project Settings > API Secrets**\n2. Click **Generate Secret** to create a new key\n3. Keys created through this portal are Ingress keys by default and work with this API (this is the same key used for `/sdk/` API authentication)\n4. Store the secret securely — you will need it to sign every request\n\n### Signing a Request\n\n1. For GET requests (no body), sign an **empty string** using HMAC-SHA256 with your API secret\n2. Base64-encode the resulting signature\n3. Send it in the `stash-hmac-signature` HTTP header\n\n### Example (curl)\n\n```bash\nSIGNATURE=$(echo -n \"\" | openssl dgst -sha256 -hmac \"YOUR_API_SECRET\" -binary | base64)\ncurl -H \"stash-hmac-signature: $SIGNATURE\" \\\n \"https://test-api.stash.gg/api/v1/studio/{shop_id}/products\"\n```", + "version": "1.0.0", + "contact": { + "name": "API Support", + "url": "https://docs.stash.gg/", + "email": "help@stash.gg" + } + }, + "schemes": ["https"], + "consumes": ["application/json"], + "produces": ["application/json"], + "tags": [ + { + "name": "ManagedCatalog", + "description": "Read-only endpoints for querying the managed product catalog" + } + ], + "paths": { + "$ref": "../../gen/openapiv2/server/ingress/studio/v1/service.swagger.json#paths" + }, + "definitions": { + "$ref": "../../gen/openapiv2/server/ingress/studio/v1/service.swagger.json#definitions" + }, + "securityDefinitions": { + "$ref": "../../gen/openapiv2/server/ingress/server.swagger.json#securityDefinitions" + } +} diff --git a/docs/gen/redoc.ingress.v1.html b/docs/gen/redoc.ingress.v1.html new file mode 100644 index 0000000..cd97840 --- /dev/null +++ b/docs/gen/redoc.ingress.v1.html @@ -0,0 +1,515 @@ + + + + + + Stash Managed Catalog API + + + + + + + + + +

Stash Managed Catalog API (1.0.0)

Download OpenAPI specification:

Read-only API for querying the Stash managed product catalog. Designed for server-to-server communication between partner backends and Stash services.

+

Base URLs

+ + + + + + + + + + + + + + +
EnvironmentBase URL
Testhttps://test-api.stash.gg
Productionhttps://api.stash.gg
+

All endpoint paths below are relative to the base URL. For example, to list products in the test environment:

+
GET https://test-api.stash.gg/api/v1/studio/{shop_id}/products
+
+

Authentication

All requests must include an HMAC-SHA256 signature in the stash-hmac-signature header.

+

Setup

+
    +
  1. Go to Stash Studio > Project Settings > API Secrets
  2. +
  3. Click Generate Secret to create a new key
  4. +
  5. Keys created through this portal are Ingress keys by default and work with this API (this is the same key used for /sdk/ API authentication)
  6. +
  7. Store the secret securely — you will need it to sign every request
  8. +
+

Signing a Request

+
    +
  1. For GET requests (no body), sign an empty string using HMAC-SHA256 with your API secret
  2. +
  3. Base64-encode the resulting signature
  4. +
  5. Send it in the stash-hmac-signature HTTP header
  6. +
+

Example (curl)

+
SIGNATURE=$(echo -n "" | openssl dgst -sha256 -hmac "YOUR_API_SECRET" -binary | base64)
+curl -H "stash-hmac-signature: $SIGNATURE" \
+  "https://test-api.stash.gg/api/v1/studio/{shop_id}/products"
+
+

ManagedCatalog

Read-only endpoints for querying the managed product catalog

+

List published products

Retrieves a paginated list of published products for a shop. Results are ordered by creation date (most recent first). Only published products are returned; drafts and deleted products are excluded.

+
Authorizations:
hmac
path Parameters
shopId
required
string

Shop identifier from Stash Studio

+
query Parameters
limit
integer <int64>

Maximum results per page. Defaults to 50. Maximum allowed value is 100.

+
offset
integer <int64>

Pagination offset. Use next_offset from the previous response to fetch the next page. Defaults to 0.

+

Responses

Response samples

Content type
application/json
{
  • "products": [
    ],
  • "nextOffset": 0
}

Get a single published product

Retrieves a single published product by its GUID. Returns NOT_FOUND if the product does not exist or is not published.

+
Authorizations:
hmac
path Parameters
shopId
required
string

Shop identifier from Stash Studio

+
guid
required
string

Stash-internal unique product identifier (UUID)

+

Responses

Response samples

Content type
application/json
{
  • "product": {
    }
}
+ + + + diff --git a/docs/gen/swagger.ingress.v1.json b/docs/gen/swagger.ingress.v1.json new file mode 100644 index 0000000..174d29c --- /dev/null +++ b/docs/gen/swagger.ingress.v1.json @@ -0,0 +1,1391 @@ +{ + "swagger": "2.0", + "info": { + "title": "Stash Managed Catalog API", + "description": "Read-only API for querying the Stash managed product catalog. Designed for server-to-server communication between partner backends and Stash services.\n\n## Base URLs\n\n| Environment | Base URL |\n|---|---|\n| **Test** | `https://test-api.stash.gg` |\n| **Production** | `https://api.stash.gg` |\n\nAll endpoint paths below are relative to the base URL. For example, to list products in the test environment:\n\n```\nGET https://test-api.stash.gg/api/v1/studio/{shop_id}/products\n```\n\n## Authentication\n\nAll requests must include an HMAC-SHA256 signature in the `stash-hmac-signature` header.\n\n### Setup\n\n1. Go to **Stash Studio > Project Settings > API Secrets**\n2. Click **Generate Secret** to create a new key\n3. Keys created through this portal are Ingress keys by default and work with this API (this is the same key used for `/sdk/` API authentication)\n4. Store the secret securely — you will need it to sign every request\n\n### Signing a Request\n\n1. For GET requests (no body), sign an **empty string** using HMAC-SHA256 with your API secret\n2. Base64-encode the resulting signature\n3. Send it in the `stash-hmac-signature` HTTP header\n\n### Example (curl)\n\n```bash\nSIGNATURE=$(echo -n \"\" | openssl dgst -sha256 -hmac \"YOUR_API_SECRET\" -binary | base64)\ncurl -H \"stash-hmac-signature: $SIGNATURE\" \\\n \"https://test-api.stash.gg/api/v1/studio/{shop_id}/products\"\n```", + "version": "1.0.0", + "contact": { + "name": "API Support", + "url": "https://docs.stash.gg/", + "email": "help@stash.gg" + } + }, + "schemes": [ + "https" + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + { + "name": "ManagedCatalog", + "description": "Read-only endpoints for querying the managed product catalog" + } + ], + "paths": { + "/api/v1/studio/{shopId}/products": { + "get": { + "summary": "List published products", + "description": "Retrieves a paginated list of published products for a shop. Results are ordered by creation date (most recent first). Only published products are returned; drafts and deleted products are excluded.", + "operationId": "ManagedCatalogService_ListProducts", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "type": "object", + "properties": { + "products": { + "type": "array", + "items": { + "type": "object", + "properties": { + "guid": { + "type": "string", + "description": "Stash-internal unique product identifier (UUID)" + }, + "name": { + "type": "object", + "properties": { + "defaultText": { + "type": "string", + "description": "Default text content, usually in English" + }, + "localizationKey": { + "type": "string", + "description": "Key for client-side localization lookup" + }, + "localizedText": { + "type": "string", + "description": "Server-resolved localized text" + } + }, + "description": "Product name", + "title": "Localizable Text" + }, + "productId": { + "type": "string", + "description": "Partner-defined product identifier (e.g., SKU). Always non-empty for published products" + }, + "description": { + "type": "object", + "properties": { + "defaultText": { + "type": "string", + "description": "Default text content, usually in English" + }, + "localizationKey": { + "type": "string", + "description": "Key for client-side localization lookup" + }, + "localizedText": { + "type": "string", + "description": "Server-resolved localized text" + } + }, + "description": "Product description. Always non-empty for published products", + "title": "Localizable Text" + }, + "images": { + "type": "object", + "properties": { + "mainImage": { + "type": "string", + "description": "Primary product image URL. Always non-empty for published products" + }, + "backgroundImage": { + "type": "string", + "description": "Background image URL" + } + }, + "description": "Product images", + "title": "Images" + }, + "status": { + "type": "string", + "enum": [ + "PRODUCT_STATUS_UNSPECIFIED", + "PRODUCT_STATUS_PUBLISHED" + ], + "default": "PRODUCT_STATUS_PUBLISHED", + "description": "Product status. Always PRODUCT_STATUS_PUBLISHED for this API", + "example": "PRODUCT_STATUS_PUBLISHED" + }, + "createdAt": { + "type": "string", + "format": "date-time", + "description": "Timestamp when the product was created (RFC 3339)" + }, + "updatedAt": { + "type": "string", + "format": "date-time", + "description": "Timestamp when the product was last updated (RFC 3339)" + }, + "displayName": { + "type": "object", + "properties": { + "defaultText": { + "type": "string", + "description": "Default text content, usually in English" + }, + "localizationKey": { + "type": "string", + "description": "Key for client-side localization lookup" + }, + "localizedText": { + "type": "string", + "description": "Server-resolved localized text" + } + }, + "description": "Human-readable display name. Always non-empty for published products", + "title": "Localizable Text" + }, + "prices": { + "type": "array", + "example": [ + { + "currency": "USD", + "platform": "PRODUCT_PRICE_PLATFORM_UNIVERSAL", + "region": "US", + "cents": 499, + "updatedAt": "2026-01-15T10:30:00Z" + } + ], + "items": { + "type": "object", + "properties": { + "currency": { + "type": "string", + "description": "Currency code (ISO-4217, e.g., \"USD\", \"EUR\")" + }, + "platform": { + "type": "string", + "enum": [ + "PRODUCT_PRICE_PLATFORM_UNSPECIFIED", + "PRODUCT_PRICE_PLATFORM_UNIVERSAL" + ], + "default": "PRODUCT_PRICE_PLATFORM_UNIVERSAL", + "description": "Platform (e.g., UNIVERSAL)", + "example": "PRODUCT_PRICE_PLATFORM_UNIVERSAL" + }, + "region": { + "type": "string", + "description": "Region code (ISO-3166-1 alpha-2, e.g., \"US\", \"EU\")" + }, + "cents": { + "type": "integer", + "format": "int64", + "description": "Price in smallest currency unit (e.g., 499 for $4.99)" + }, + "updatedAt": { + "type": "string", + "format": "date-time", + "description": "Timestamp when this price was last updated (RFC 3339)" + } + }, + "description": "A price entry for a product", + "title": "Price" + }, + "description": "Array of price entries. At least one entry (USD/UNIVERSAL/US) is guaranteed for paid products. Free gift products have an empty array." + }, + "items": { + "type": "array", + "example": [ + { + "image": "https://example.com/coin.png", + "quantity": "100", + "displayName": { + "defaultText": "Gold Coin" + } + } + ], + "items": { + "type": "object", + "properties": { + "image": { + "type": "string", + "description": "URL to the item image. Always non-empty for published products" + }, + "quantity": { + "type": "string", + "description": "In-game item quantity as string for big integer support (no decimals allowed). Always > 0 for published products" + }, + "displayName": { + "type": "object", + "properties": { + "defaultText": { + "type": "string", + "description": "Default text content, usually in English" + }, + "localizationKey": { + "type": "string", + "description": "Key for client-side localization lookup" + }, + "localizedText": { + "type": "string", + "description": "Server-resolved localized text" + } + }, + "description": "Optional display name for the item (max 50 characters)", + "title": "Localizable Text" + } + }, + "description": "An item contained in a product", + "title": "Item" + }, + "description": "Items contained in this product. May be empty if no items configured. If present, each item has a non-empty image and quantity > 0." + } + }, + "description": "A published product from the managed catalog", + "title": "Product" + }, + "description": "List of published products" + }, + "nextOffset": { + "type": "integer", + "format": "int64", + "description": "Offset for the next page. Omitted if there are no more results." + } + }, + "description": "Paginated list of published products", + "title": "List Products Response" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "format": "int32" + }, + "message": { + "type": "string" + }, + "details": { + "type": "array", + "items": { + "type": "object", + "properties": { + "@type": { + "type": "string" + } + }, + "additionalProperties": {} + } + } + } + } + } + }, + "parameters": [ + { + "name": "shopId", + "description": "Shop identifier from Stash Studio", + "in": "path", + "required": true, + "type": "string" + }, + { + "name": "limit", + "description": "Maximum results per page. Defaults to 50. Maximum allowed value is 100.", + "in": "query", + "required": false, + "type": "integer", + "format": "int64" + }, + { + "name": "offset", + "description": "Pagination offset. Use next_offset from the previous response to fetch the next page. Defaults to 0.", + "in": "query", + "required": false, + "type": "integer", + "format": "int64" + } + ], + "tags": [ + "ManagedCatalog" + ], + "security": [ + { + "hmac": [] + } + ] + } + }, + "/api/v1/studio/{shopId}/products/{guid}": { + "get": { + "summary": "Get a single published product", + "description": "Retrieves a single published product by its GUID. Returns NOT_FOUND if the product does not exist or is not published.", + "operationId": "ManagedCatalogService_GetProduct", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "type": "object", + "properties": { + "product": { + "type": "object", + "properties": { + "guid": { + "type": "string", + "description": "Stash-internal unique product identifier (UUID)" + }, + "name": { + "type": "object", + "properties": { + "defaultText": { + "type": "string", + "description": "Default text content, usually in English" + }, + "localizationKey": { + "type": "string", + "description": "Key for client-side localization lookup" + }, + "localizedText": { + "type": "string", + "description": "Server-resolved localized text" + } + }, + "description": "Product name", + "title": "Localizable Text" + }, + "productId": { + "type": "string", + "description": "Partner-defined product identifier (e.g., SKU). Always non-empty for published products" + }, + "description": { + "type": "object", + "properties": { + "defaultText": { + "type": "string", + "description": "Default text content, usually in English" + }, + "localizationKey": { + "type": "string", + "description": "Key for client-side localization lookup" + }, + "localizedText": { + "type": "string", + "description": "Server-resolved localized text" + } + }, + "description": "Product description. Always non-empty for published products", + "title": "Localizable Text" + }, + "images": { + "type": "object", + "properties": { + "mainImage": { + "type": "string", + "description": "Primary product image URL. Always non-empty for published products" + }, + "backgroundImage": { + "type": "string", + "description": "Background image URL" + } + }, + "description": "Product images", + "title": "Images" + }, + "status": { + "type": "string", + "enum": [ + "PRODUCT_STATUS_UNSPECIFIED", + "PRODUCT_STATUS_PUBLISHED" + ], + "default": "PRODUCT_STATUS_PUBLISHED", + "description": "Product status. Always PRODUCT_STATUS_PUBLISHED for this API", + "example": "PRODUCT_STATUS_PUBLISHED" + }, + "createdAt": { + "type": "string", + "format": "date-time", + "description": "Timestamp when the product was created (RFC 3339)" + }, + "updatedAt": { + "type": "string", + "format": "date-time", + "description": "Timestamp when the product was last updated (RFC 3339)" + }, + "displayName": { + "type": "object", + "properties": { + "defaultText": { + "type": "string", + "description": "Default text content, usually in English" + }, + "localizationKey": { + "type": "string", + "description": "Key for client-side localization lookup" + }, + "localizedText": { + "type": "string", + "description": "Server-resolved localized text" + } + }, + "description": "Human-readable display name. Always non-empty for published products", + "title": "Localizable Text" + }, + "prices": { + "type": "array", + "example": [ + { + "currency": "USD", + "platform": "PRODUCT_PRICE_PLATFORM_UNIVERSAL", + "region": "US", + "cents": 499, + "updatedAt": "2026-01-15T10:30:00Z" + } + ], + "items": { + "type": "object", + "properties": { + "currency": { + "type": "string", + "description": "Currency code (ISO-4217, e.g., \"USD\", \"EUR\")" + }, + "platform": { + "type": "string", + "enum": [ + "PRODUCT_PRICE_PLATFORM_UNSPECIFIED", + "PRODUCT_PRICE_PLATFORM_UNIVERSAL" + ], + "default": "PRODUCT_PRICE_PLATFORM_UNIVERSAL", + "description": "Platform (e.g., UNIVERSAL)", + "example": "PRODUCT_PRICE_PLATFORM_UNIVERSAL" + }, + "region": { + "type": "string", + "description": "Region code (ISO-3166-1 alpha-2, e.g., \"US\", \"EU\")" + }, + "cents": { + "type": "integer", + "format": "int64", + "description": "Price in smallest currency unit (e.g., 499 for $4.99)" + }, + "updatedAt": { + "type": "string", + "format": "date-time", + "description": "Timestamp when this price was last updated (RFC 3339)" + } + }, + "description": "A price entry for a product", + "title": "Price" + }, + "description": "Array of price entries. At least one entry (USD/UNIVERSAL/US) is guaranteed for paid products. Free gift products have an empty array." + }, + "items": { + "type": "array", + "example": [ + { + "image": "https://example.com/coin.png", + "quantity": "100", + "displayName": { + "defaultText": "Gold Coin" + } + } + ], + "items": { + "type": "object", + "properties": { + "image": { + "type": "string", + "description": "URL to the item image. Always non-empty for published products" + }, + "quantity": { + "type": "string", + "description": "In-game item quantity as string for big integer support (no decimals allowed). Always > 0 for published products" + }, + "displayName": { + "type": "object", + "properties": { + "defaultText": { + "type": "string", + "description": "Default text content, usually in English" + }, + "localizationKey": { + "type": "string", + "description": "Key for client-side localization lookup" + }, + "localizedText": { + "type": "string", + "description": "Server-resolved localized text" + } + }, + "description": "Optional display name for the item (max 50 characters)", + "title": "Localizable Text" + } + }, + "description": "An item contained in a product", + "title": "Item" + }, + "description": "Items contained in this product. May be empty if no items configured. If present, each item has a non-empty image and quantity > 0." + } + }, + "description": "The requested product", + "title": "Product" + } + }, + "description": "Response containing a single published product", + "title": "Get Product Response", + "required": [ + "product" + ] + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "format": "int32" + }, + "message": { + "type": "string" + }, + "details": { + "type": "array", + "items": { + "type": "object", + "properties": { + "@type": { + "type": "string" + } + }, + "additionalProperties": {} + } + } + } + } + } + }, + "parameters": [ + { + "name": "shopId", + "description": "Shop identifier from Stash Studio", + "in": "path", + "required": true, + "type": "string" + }, + { + "name": "guid", + "description": "Stash-internal unique product identifier (UUID)", + "in": "path", + "required": true, + "type": "string" + } + ], + "tags": [ + "ManagedCatalog" + ], + "security": [ + { + "hmac": [] + } + ] + } + } + }, + "definitions": { + "protobufAny": { + "type": "object", + "properties": { + "@type": { + "type": "string" + } + }, + "additionalProperties": {} + }, + "rpcStatus": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "format": "int32" + }, + "message": { + "type": "string" + }, + "details": { + "type": "array", + "items": { + "type": "object", + "properties": { + "@type": { + "type": "string" + } + }, + "additionalProperties": {} + } + } + } + }, + "v1GetProductResponse": { + "type": "object", + "properties": { + "product": { + "type": "object", + "properties": { + "guid": { + "type": "string", + "description": "Stash-internal unique product identifier (UUID)" + }, + "name": { + "type": "object", + "properties": { + "defaultText": { + "type": "string", + "description": "Default text content, usually in English" + }, + "localizationKey": { + "type": "string", + "description": "Key for client-side localization lookup" + }, + "localizedText": { + "type": "string", + "description": "Server-resolved localized text" + } + }, + "description": "Product name", + "title": "Localizable Text" + }, + "productId": { + "type": "string", + "description": "Partner-defined product identifier (e.g., SKU). Always non-empty for published products" + }, + "description": { + "type": "object", + "properties": { + "defaultText": { + "type": "string", + "description": "Default text content, usually in English" + }, + "localizationKey": { + "type": "string", + "description": "Key for client-side localization lookup" + }, + "localizedText": { + "type": "string", + "description": "Server-resolved localized text" + } + }, + "description": "Product description. Always non-empty for published products", + "title": "Localizable Text" + }, + "images": { + "type": "object", + "properties": { + "mainImage": { + "type": "string", + "description": "Primary product image URL. Always non-empty for published products" + }, + "backgroundImage": { + "type": "string", + "description": "Background image URL" + } + }, + "description": "Product images", + "title": "Images" + }, + "status": { + "type": "string", + "enum": [ + "PRODUCT_STATUS_UNSPECIFIED", + "PRODUCT_STATUS_PUBLISHED" + ], + "default": "PRODUCT_STATUS_PUBLISHED", + "description": "Product status. Always PRODUCT_STATUS_PUBLISHED for this API", + "example": "PRODUCT_STATUS_PUBLISHED" + }, + "createdAt": { + "type": "string", + "format": "date-time", + "description": "Timestamp when the product was created (RFC 3339)" + }, + "updatedAt": { + "type": "string", + "format": "date-time", + "description": "Timestamp when the product was last updated (RFC 3339)" + }, + "displayName": { + "type": "object", + "properties": { + "defaultText": { + "type": "string", + "description": "Default text content, usually in English" + }, + "localizationKey": { + "type": "string", + "description": "Key for client-side localization lookup" + }, + "localizedText": { + "type": "string", + "description": "Server-resolved localized text" + } + }, + "description": "Human-readable display name. Always non-empty for published products", + "title": "Localizable Text" + }, + "prices": { + "type": "array", + "example": [ + { + "currency": "USD", + "platform": "PRODUCT_PRICE_PLATFORM_UNIVERSAL", + "region": "US", + "cents": 499, + "updatedAt": "2026-01-15T10:30:00Z" + } + ], + "items": { + "type": "object", + "properties": { + "currency": { + "type": "string", + "description": "Currency code (ISO-4217, e.g., \"USD\", \"EUR\")" + }, + "platform": { + "type": "string", + "enum": [ + "PRODUCT_PRICE_PLATFORM_UNSPECIFIED", + "PRODUCT_PRICE_PLATFORM_UNIVERSAL" + ], + "default": "PRODUCT_PRICE_PLATFORM_UNIVERSAL", + "description": "Platform (e.g., UNIVERSAL)", + "example": "PRODUCT_PRICE_PLATFORM_UNIVERSAL" + }, + "region": { + "type": "string", + "description": "Region code (ISO-3166-1 alpha-2, e.g., \"US\", \"EU\")" + }, + "cents": { + "type": "integer", + "format": "int64", + "description": "Price in smallest currency unit (e.g., 499 for $4.99)" + }, + "updatedAt": { + "type": "string", + "format": "date-time", + "description": "Timestamp when this price was last updated (RFC 3339)" + } + }, + "description": "A price entry for a product", + "title": "Price" + }, + "description": "Array of price entries. At least one entry (USD/UNIVERSAL/US) is guaranteed for paid products. Free gift products have an empty array." + }, + "items": { + "type": "array", + "example": [ + { + "image": "https://example.com/coin.png", + "quantity": "100", + "displayName": { + "defaultText": "Gold Coin" + } + } + ], + "items": { + "type": "object", + "properties": { + "image": { + "type": "string", + "description": "URL to the item image. Always non-empty for published products" + }, + "quantity": { + "type": "string", + "description": "In-game item quantity as string for big integer support (no decimals allowed). Always > 0 for published products" + }, + "displayName": { + "type": "object", + "properties": { + "defaultText": { + "type": "string", + "description": "Default text content, usually in English" + }, + "localizationKey": { + "type": "string", + "description": "Key for client-side localization lookup" + }, + "localizedText": { + "type": "string", + "description": "Server-resolved localized text" + } + }, + "description": "Optional display name for the item (max 50 characters)", + "title": "Localizable Text" + } + }, + "description": "An item contained in a product", + "title": "Item" + }, + "description": "Items contained in this product. May be empty if no items configured. If present, each item has a non-empty image and quantity > 0." + } + }, + "description": "The requested product", + "title": "Product" + } + }, + "description": "Response containing a single published product", + "title": "Get Product Response", + "required": [ + "product" + ] + }, + "v1Images": { + "type": "object", + "properties": { + "mainImage": { + "type": "string", + "description": "Primary product image URL. Always non-empty for published products" + }, + "backgroundImage": { + "type": "string", + "description": "Background image URL" + } + }, + "description": "Product images", + "title": "Images" + }, + "v1Item": { + "type": "object", + "properties": { + "image": { + "type": "string", + "description": "URL to the item image. Always non-empty for published products" + }, + "quantity": { + "type": "string", + "description": "In-game item quantity as string for big integer support (no decimals allowed). Always > 0 for published products" + }, + "displayName": { + "type": "object", + "properties": { + "defaultText": { + "type": "string", + "description": "Default text content, usually in English" + }, + "localizationKey": { + "type": "string", + "description": "Key for client-side localization lookup" + }, + "localizedText": { + "type": "string", + "description": "Server-resolved localized text" + } + }, + "description": "Optional display name for the item (max 50 characters)", + "title": "Localizable Text" + } + }, + "description": "An item contained in a product", + "title": "Item" + }, + "v1ListProductsResponse": { + "type": "object", + "properties": { + "products": { + "type": "array", + "items": { + "type": "object", + "properties": { + "guid": { + "type": "string", + "description": "Stash-internal unique product identifier (UUID)" + }, + "name": { + "type": "object", + "properties": { + "defaultText": { + "type": "string", + "description": "Default text content, usually in English" + }, + "localizationKey": { + "type": "string", + "description": "Key for client-side localization lookup" + }, + "localizedText": { + "type": "string", + "description": "Server-resolved localized text" + } + }, + "description": "Product name", + "title": "Localizable Text" + }, + "productId": { + "type": "string", + "description": "Partner-defined product identifier (e.g., SKU). Always non-empty for published products" + }, + "description": { + "type": "object", + "properties": { + "defaultText": { + "type": "string", + "description": "Default text content, usually in English" + }, + "localizationKey": { + "type": "string", + "description": "Key for client-side localization lookup" + }, + "localizedText": { + "type": "string", + "description": "Server-resolved localized text" + } + }, + "description": "Product description. Always non-empty for published products", + "title": "Localizable Text" + }, + "images": { + "type": "object", + "properties": { + "mainImage": { + "type": "string", + "description": "Primary product image URL. Always non-empty for published products" + }, + "backgroundImage": { + "type": "string", + "description": "Background image URL" + } + }, + "description": "Product images", + "title": "Images" + }, + "status": { + "type": "string", + "enum": [ + "PRODUCT_STATUS_UNSPECIFIED", + "PRODUCT_STATUS_PUBLISHED" + ], + "default": "PRODUCT_STATUS_PUBLISHED", + "description": "Product status. Always PRODUCT_STATUS_PUBLISHED for this API", + "example": "PRODUCT_STATUS_PUBLISHED" + }, + "createdAt": { + "type": "string", + "format": "date-time", + "description": "Timestamp when the product was created (RFC 3339)" + }, + "updatedAt": { + "type": "string", + "format": "date-time", + "description": "Timestamp when the product was last updated (RFC 3339)" + }, + "displayName": { + "type": "object", + "properties": { + "defaultText": { + "type": "string", + "description": "Default text content, usually in English" + }, + "localizationKey": { + "type": "string", + "description": "Key for client-side localization lookup" + }, + "localizedText": { + "type": "string", + "description": "Server-resolved localized text" + } + }, + "description": "Human-readable display name. Always non-empty for published products", + "title": "Localizable Text" + }, + "prices": { + "type": "array", + "example": [ + { + "currency": "USD", + "platform": "PRODUCT_PRICE_PLATFORM_UNIVERSAL", + "region": "US", + "cents": 499, + "updatedAt": "2026-01-15T10:30:00Z" + } + ], + "items": { + "type": "object", + "properties": { + "currency": { + "type": "string", + "description": "Currency code (ISO-4217, e.g., \"USD\", \"EUR\")" + }, + "platform": { + "type": "string", + "enum": [ + "PRODUCT_PRICE_PLATFORM_UNSPECIFIED", + "PRODUCT_PRICE_PLATFORM_UNIVERSAL" + ], + "default": "PRODUCT_PRICE_PLATFORM_UNIVERSAL", + "description": "Platform (e.g., UNIVERSAL)", + "example": "PRODUCT_PRICE_PLATFORM_UNIVERSAL" + }, + "region": { + "type": "string", + "description": "Region code (ISO-3166-1 alpha-2, e.g., \"US\", \"EU\")" + }, + "cents": { + "type": "integer", + "format": "int64", + "description": "Price in smallest currency unit (e.g., 499 for $4.99)" + }, + "updatedAt": { + "type": "string", + "format": "date-time", + "description": "Timestamp when this price was last updated (RFC 3339)" + } + }, + "description": "A price entry for a product", + "title": "Price" + }, + "description": "Array of price entries. At least one entry (USD/UNIVERSAL/US) is guaranteed for paid products. Free gift products have an empty array." + }, + "items": { + "type": "array", + "example": [ + { + "image": "https://example.com/coin.png", + "quantity": "100", + "displayName": { + "defaultText": "Gold Coin" + } + } + ], + "items": { + "type": "object", + "properties": { + "image": { + "type": "string", + "description": "URL to the item image. Always non-empty for published products" + }, + "quantity": { + "type": "string", + "description": "In-game item quantity as string for big integer support (no decimals allowed). Always > 0 for published products" + }, + "displayName": { + "type": "object", + "properties": { + "defaultText": { + "type": "string", + "description": "Default text content, usually in English" + }, + "localizationKey": { + "type": "string", + "description": "Key for client-side localization lookup" + }, + "localizedText": { + "type": "string", + "description": "Server-resolved localized text" + } + }, + "description": "Optional display name for the item (max 50 characters)", + "title": "Localizable Text" + } + }, + "description": "An item contained in a product", + "title": "Item" + }, + "description": "Items contained in this product. May be empty if no items configured. If present, each item has a non-empty image and quantity > 0." + } + }, + "description": "A published product from the managed catalog", + "title": "Product" + }, + "description": "List of published products" + }, + "nextOffset": { + "type": "integer", + "format": "int64", + "description": "Offset for the next page. Omitted if there are no more results." + } + }, + "description": "Paginated list of published products", + "title": "List Products Response" + }, + "v1LocalizableText": { + "type": "object", + "properties": { + "defaultText": { + "type": "string", + "description": "Default text content, usually in English" + }, + "localizationKey": { + "type": "string", + "description": "Key for client-side localization lookup" + }, + "localizedText": { + "type": "string", + "description": "Server-resolved localized text" + } + }, + "description": "Text content that can be localized for a specific language", + "title": "Localizable Text" + }, + "v1Price": { + "type": "object", + "properties": { + "currency": { + "type": "string", + "description": "Currency code (ISO-4217, e.g., \"USD\", \"EUR\")" + }, + "platform": { + "type": "string", + "enum": [ + "PRODUCT_PRICE_PLATFORM_UNSPECIFIED", + "PRODUCT_PRICE_PLATFORM_UNIVERSAL" + ], + "default": "PRODUCT_PRICE_PLATFORM_UNIVERSAL", + "description": "Platform (e.g., UNIVERSAL)", + "example": "PRODUCT_PRICE_PLATFORM_UNIVERSAL" + }, + "region": { + "type": "string", + "description": "Region code (ISO-3166-1 alpha-2, e.g., \"US\", \"EU\")" + }, + "cents": { + "type": "integer", + "format": "int64", + "description": "Price in smallest currency unit (e.g., 499 for $4.99)" + }, + "updatedAt": { + "type": "string", + "format": "date-time", + "description": "Timestamp when this price was last updated (RFC 3339)" + } + }, + "description": "A price entry for a product", + "title": "Price" + }, + "v1Product": { + "type": "object", + "properties": { + "guid": { + "type": "string", + "description": "Stash-internal unique product identifier (UUID)" + }, + "name": { + "type": "object", + "properties": { + "defaultText": { + "type": "string", + "description": "Default text content, usually in English" + }, + "localizationKey": { + "type": "string", + "description": "Key for client-side localization lookup" + }, + "localizedText": { + "type": "string", + "description": "Server-resolved localized text" + } + }, + "description": "Product name", + "title": "Localizable Text" + }, + "productId": { + "type": "string", + "description": "Partner-defined product identifier (e.g., SKU). Always non-empty for published products" + }, + "description": { + "type": "object", + "properties": { + "defaultText": { + "type": "string", + "description": "Default text content, usually in English" + }, + "localizationKey": { + "type": "string", + "description": "Key for client-side localization lookup" + }, + "localizedText": { + "type": "string", + "description": "Server-resolved localized text" + } + }, + "description": "Product description. Always non-empty for published products", + "title": "Localizable Text" + }, + "images": { + "type": "object", + "properties": { + "mainImage": { + "type": "string", + "description": "Primary product image URL. Always non-empty for published products" + }, + "backgroundImage": { + "type": "string", + "description": "Background image URL" + } + }, + "description": "Product images", + "title": "Images" + }, + "status": { + "type": "string", + "enum": [ + "PRODUCT_STATUS_UNSPECIFIED", + "PRODUCT_STATUS_PUBLISHED" + ], + "default": "PRODUCT_STATUS_PUBLISHED", + "description": "Product status. Always PRODUCT_STATUS_PUBLISHED for this API", + "example": "PRODUCT_STATUS_PUBLISHED" + }, + "createdAt": { + "type": "string", + "format": "date-time", + "description": "Timestamp when the product was created (RFC 3339)" + }, + "updatedAt": { + "type": "string", + "format": "date-time", + "description": "Timestamp when the product was last updated (RFC 3339)" + }, + "displayName": { + "type": "object", + "properties": { + "defaultText": { + "type": "string", + "description": "Default text content, usually in English" + }, + "localizationKey": { + "type": "string", + "description": "Key for client-side localization lookup" + }, + "localizedText": { + "type": "string", + "description": "Server-resolved localized text" + } + }, + "description": "Human-readable display name. Always non-empty for published products", + "title": "Localizable Text" + }, + "prices": { + "type": "array", + "example": [ + { + "currency": "USD", + "platform": "PRODUCT_PRICE_PLATFORM_UNIVERSAL", + "region": "US", + "cents": 499, + "updatedAt": "2026-01-15T10:30:00Z" + } + ], + "items": { + "type": "object", + "properties": { + "currency": { + "type": "string", + "description": "Currency code (ISO-4217, e.g., \"USD\", \"EUR\")" + }, + "platform": { + "type": "string", + "enum": [ + "PRODUCT_PRICE_PLATFORM_UNSPECIFIED", + "PRODUCT_PRICE_PLATFORM_UNIVERSAL" + ], + "default": "PRODUCT_PRICE_PLATFORM_UNIVERSAL", + "description": "Platform (e.g., UNIVERSAL)", + "example": "PRODUCT_PRICE_PLATFORM_UNIVERSAL" + }, + "region": { + "type": "string", + "description": "Region code (ISO-3166-1 alpha-2, e.g., \"US\", \"EU\")" + }, + "cents": { + "type": "integer", + "format": "int64", + "description": "Price in smallest currency unit (e.g., 499 for $4.99)" + }, + "updatedAt": { + "type": "string", + "format": "date-time", + "description": "Timestamp when this price was last updated (RFC 3339)" + } + }, + "description": "A price entry for a product", + "title": "Price" + }, + "description": "Array of price entries. At least one entry (USD/UNIVERSAL/US) is guaranteed for paid products. Free gift products have an empty array." + }, + "items": { + "type": "array", + "example": [ + { + "image": "https://example.com/coin.png", + "quantity": "100", + "displayName": { + "defaultText": "Gold Coin" + } + } + ], + "items": { + "type": "object", + "properties": { + "image": { + "type": "string", + "description": "URL to the item image. Always non-empty for published products" + }, + "quantity": { + "type": "string", + "description": "In-game item quantity as string for big integer support (no decimals allowed). Always > 0 for published products" + }, + "displayName": { + "type": "object", + "properties": { + "defaultText": { + "type": "string", + "description": "Default text content, usually in English" + }, + "localizationKey": { + "type": "string", + "description": "Key for client-side localization lookup" + }, + "localizedText": { + "type": "string", + "description": "Server-resolved localized text" + } + }, + "description": "Optional display name for the item (max 50 characters)", + "title": "Localizable Text" + } + }, + "description": "An item contained in a product", + "title": "Item" + }, + "description": "Items contained in this product. May be empty if no items configured. If present, each item has a non-empty image and quantity > 0." + } + }, + "description": "A published product from the managed catalog", + "title": "Product" + }, + "v1ProductPricePlatform": { + "type": "string", + "enum": [ + "PRODUCT_PRICE_PLATFORM_UNSPECIFIED", + "PRODUCT_PRICE_PLATFORM_UNIVERSAL" + ], + "default": "PRODUCT_PRICE_PLATFORM_UNSPECIFIED", + "description": "Platform for a price entry" + }, + "v1ProductStatus": { + "type": "string", + "enum": [ + "PRODUCT_STATUS_UNSPECIFIED", + "PRODUCT_STATUS_PUBLISHED" + ], + "default": "PRODUCT_STATUS_UNSPECIFIED", + "description": "Product publication status. This API only returns published products." + } + }, + "securityDefinitions": { + "hmac": { + "type": "apiKey", + "description": "HMAC-SHA256 signature generated by signing the request body (empty string for GET requests) with the Ingress API key from Stash Studio. Base64-encoded.", + "name": "stash-hmac-signature", + "in": "header" + } + } +} \ No newline at end of file diff --git a/gen.sh b/gen.sh index 648e56f..41d5e75 100755 --- a/gen.sh +++ b/gen.sh @@ -39,4 +39,10 @@ npx --yes swagger-merger@1.5.4 -i ./docs/config/swagger-merger-config.json -o ./ echo "Building Redoc static HTML file..." npx --yes @redocly/cli@2.6.0 build-docs ./docs/gen/swagger.v1.json -o ./docs/gen/redoc.v1.html +echo "Merging Ingress Swagger files..." +npx swagger-merger@1.5.4 -i ./docs/config/swagger-merger-ingress-config.json -o ./docs/gen/swagger.ingress.v1.json + +echo "Building Ingress Redoc static HTML file..." +npx @redocly/cli@2.6.0 build-docs ./docs/gen/swagger.ingress.v1.json -o ./docs/gen/redoc.ingress.v1.html + echo "Code generation completed successfully!" diff --git a/server/ingress/server.proto b/server/ingress/server.proto new file mode 100644 index 0000000..66f1adf --- /dev/null +++ b/server/ingress/server.proto @@ -0,0 +1,34 @@ +syntax = "proto3"; + +// buf:lint:ignore PACKAGE_VERSION_SUFFIX +package server.ingress; + +import "protoc-gen-openapiv2/options/annotations.proto"; + +option go_package = "github.com/stashgg/public-api/gen/proto/server/ingress"; +option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { + info: { + title: "Stash Managed Catalog API" + description: "Read-only API for querying the Stash managed product catalog. Designed for server-to-server communication between partner backends and Stash services." + version: "1.0" + contact: { + name: "API Support" + url: "https://docs.stash.gg/" + email: "help@stash.gg" + } + } + schemes: HTTPS + consumes: "application/json" + produces: "application/json" + security_definitions: { + security: { + key: "hmac" + value: { + name: "stash-hmac-signature" + in: IN_HEADER + type: TYPE_API_KEY + description: "HMAC-SHA256 signature generated by signing the request body (empty string for GET requests) with the Ingress API key from Stash Studio. Base64-encoded." + } + } + } +}; diff --git a/server/ingress/studio/v1/enums.proto b/server/ingress/studio/v1/enums.proto new file mode 100644 index 0000000..a2c1f37 --- /dev/null +++ b/server/ingress/studio/v1/enums.proto @@ -0,0 +1,21 @@ +syntax = "proto3"; + +package server.ingress.studio.v1; + +import "protoc-gen-openapiv2/options/annotations.proto"; + +option go_package = "github.com/stashgg/public-api/gen/proto/server/ingress/studio/v1"; + +enum ProductStatus { + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_enum) = {description: "Product publication status. This API only returns published products."}; + reserved 1; + reserved "PRODUCT_STATUS_DRAFT"; + PRODUCT_STATUS_UNSPECIFIED = 0; + PRODUCT_STATUS_PUBLISHED = 2; +} + +enum ProductPricePlatform { + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_enum) = {description: "Platform for a price entry"}; + PRODUCT_PRICE_PLATFORM_UNSPECIFIED = 0; + PRODUCT_PRICE_PLATFORM_UNIVERSAL = 1; +} diff --git a/server/ingress/studio/v1/service.proto b/server/ingress/studio/v1/service.proto new file mode 100644 index 0000000..c41d011 --- /dev/null +++ b/server/ingress/studio/v1/service.proto @@ -0,0 +1,220 @@ +syntax = "proto3"; + +package server.ingress.studio.v1; + +import "google/api/annotations.proto"; +import "google/api/field_behavior.proto"; +import "google/protobuf/timestamp.proto"; +import "protoc-gen-openapiv2/options/annotations.proto"; +import "server/ingress/studio/v1/enums.proto"; +import "server/ingress/studio/v1/types.proto"; +import "validate/validate.proto"; + +option go_package = "github.com/stashgg/public-api/gen/proto/server/ingress/studio/v1"; + +service ManagedCatalogService { + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_tag) = { + name: "ManagedCatalog" + description: "Read-only endpoints for querying the managed product catalog" + }; + rpc ListProducts(ListProductsRequest) returns (ListProductsResponse) { + option (google.api.http) = {get: "/api/v1/studio/{shop_id}/products"}; + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + summary: "List published products" + description: "Retrieves a paginated list of published products for a shop. Results are ordered by creation date (most recent first). Only published products are returned; drafts and deleted products are excluded." + security: [ + { + security_requirement: {key: "hmac"} + } + ] + }; + } + rpc GetProduct(GetProductRequest) returns (GetProductResponse) { + option (google.api.http) = {get: "/api/v1/studio/{shop_id}/products/{guid}"}; + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + summary: "Get a single published product" + description: "Retrieves a single published product by its GUID. Returns NOT_FOUND if the product does not exist or is not published." + security: [ + { + security_requirement: {key: "hmac"} + } + ] + }; + } +} + +message Product { + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_schema) = { + json_schema: { + title: "Product" + description: "A published product from the managed catalog" + } + }; + string guid = 1 [(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {description: "Stash-internal unique product identifier (UUID)"}]; + LocalizableText name = 2 [(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {description: "Product name"}]; + optional string product_id = 3 [(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {description: "Partner-defined product identifier (e.g., SKU). Always non-empty for published products"}]; + optional LocalizableText description = 4 [(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {description: "Product description. Always non-empty for published products"}]; + optional Images images = 5 [(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {description: "Product images"}]; + ProductStatus status = 6 [(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "Product status. Always PRODUCT_STATUS_PUBLISHED for this API" + default: "PRODUCT_STATUS_PUBLISHED" + example: "\"PRODUCT_STATUS_PUBLISHED\"" + }]; + google.protobuf.Timestamp created_at = 7 [(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {description: "Timestamp when the product was created (RFC 3339)"}]; + google.protobuf.Timestamp updated_at = 8 [(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {description: "Timestamp when the product was last updated (RFC 3339)"}]; + optional LocalizableText display_name = 9 [(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {description: "Human-readable display name. Always non-empty for published products"}]; + repeated Price prices = 10 [(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "Array of price entries. At least one entry (USD/UNIVERSAL/US) is guaranteed for paid products. Free gift products have an empty array." + example: "[{\"currency\": \"USD\", \"platform\": \"PRODUCT_PRICE_PLATFORM_UNIVERSAL\", \"region\": \"US\", \"cents\": 499, \"updatedAt\": \"2026-01-15T10:30:00Z\"}]" + }]; + repeated Item items = 11 [(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "Items contained in this product. May be empty if no items configured. If present, each item has a non-empty image and quantity > 0." + example: "[{\"image\": \"https://example.com/coin.png\", \"quantity\": \"100\", \"displayName\": {\"defaultText\": \"Gold Coin\"}}]" + }]; +} + +message Images { + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_schema) = { + json_schema: { + title: "Images" + description: "Product images" + } + }; + string main_image = 1 [ + (validate.rules).string = { + uri: true + ignore_empty: true + }, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {description: "Primary product image URL. Always non-empty for published products"} + ]; + optional string background_image = 2 [ + (validate.rules).string = { + uri: true + ignore_empty: true + }, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {description: "Background image URL"} + ]; +} + +message Price { + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_schema) = { + json_schema: { + title: "Price" + description: "A price entry for a product" + } + }; + string currency = 1 [ + (validate.rules).string = { + min_len: 3 + max_len: 3 + }, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {description: "Currency code (ISO-4217, e.g., \"USD\", \"EUR\")"} + ]; + ProductPricePlatform platform = 2 [(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "Platform (e.g., UNIVERSAL)" + default: "PRODUCT_PRICE_PLATFORM_UNIVERSAL" + example: "\"PRODUCT_PRICE_PLATFORM_UNIVERSAL\"" + }]; + string region = 3 [ + (validate.rules).string = { + min_len: 2 + max_len: 3 + }, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {description: "Region code (ISO-3166-1 alpha-2, e.g., \"US\", \"EU\")"} + ]; + uint32 cents = 4 [(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {description: "Price in smallest currency unit (e.g., 499 for $4.99)"}]; + google.protobuf.Timestamp updated_at = 5 [(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {description: "Timestamp when this price was last updated (RFC 3339)"}]; +} + +message Item { + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_schema) = { + json_schema: { + title: "Item" + description: "An item contained in a product" + } + }; + string image = 1 [(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {description: "URL to the item image. Always non-empty for published products"}]; + string quantity = 2 [ + (validate.rules).string = { + min_len: 1 + pattern: "^[0-9]+$" + }, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {description: "In-game item quantity as string for big integer support (no decimals allowed). Always > 0 for published products"} + ]; + optional LocalizableText display_name = 3 [(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {description: "Optional display name for the item (max 50 characters)"}]; +} + +message ListProductsRequest { + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_schema) = { + json_schema: { + title: "List Products Request" + description: "Request to list published products for a shop" + required: ["shop_id"] + } + }; + string shop_id = 1 [ + (google.api.field_behavior) = REQUIRED, + (validate.rules).string.uuid = true, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {description: "Shop identifier from Stash Studio"} + ]; + optional uint32 limit = 2 [ + (validate.rules).uint32.lte = 100, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "Maximum results per page. Defaults to 50. Maximum allowed value is 100." + minimum: 1 + maximum: 100 + } + ]; + optional uint32 offset = 3 [(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "Pagination offset. Use next_offset from the previous response to fetch the next page. Defaults to 0." + minimum: 0 + }]; +} + +message ListProductsResponse { + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_schema) = { + json_schema: { + title: "List Products Response" + description: "Paginated list of published products" + } + }; + repeated Product products = 1 [(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {description: "List of published products"}]; + optional uint32 next_offset = 2 [(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {description: "Offset for the next page. Omitted if there are no more results."}]; +} + +message GetProductRequest { + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_schema) = { + json_schema: { + title: "Get Product Request" + description: "Request to get a single published product" + required: [ + "shop_id", + "guid" + ] + } + }; + string shop_id = 1 [ + (google.api.field_behavior) = REQUIRED, + (validate.rules).string.uuid = true, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {description: "Shop identifier from Stash Studio"} + ]; + string guid = 2 [ + (google.api.field_behavior) = REQUIRED, + (validate.rules).string.uuid = true, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {description: "Stash-internal unique product identifier (UUID)"} + ]; +} + +message GetProductResponse { + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_schema) = { + json_schema: { + title: "Get Product Response" + description: "Response containing a single published product" + required: ["product"] + } + }; + Product product = 1 [ + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {description: "The requested product"} + ]; +} diff --git a/server/ingress/studio/v1/types.proto b/server/ingress/studio/v1/types.proto new file mode 100644 index 0000000..0a804fd --- /dev/null +++ b/server/ingress/studio/v1/types.proto @@ -0,0 +1,34 @@ +syntax = "proto3"; + +package server.ingress.studio.v1; + +import "protoc-gen-openapiv2/options/annotations.proto"; +import "validate/validate.proto"; + +option go_package = "github.com/stashgg/public-api/gen/proto/server/ingress/studio/v1"; + +message LocalizableText { + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_schema) = { + json_schema: { + title: "Localizable Text" + description: "Text content that can be localized for a specific language" + } + }; + string default_text = 1 [(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {description: "Default text content, usually in English"}]; + oneof localization { + string localization_key = 2 [ + (validate.rules).string = { + min_len: 1 + ignore_empty: true + }, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {description: "Key for client-side localization lookup"} + ]; + string localized_text = 3 [ + (validate.rules).string = { + min_len: 1 + ignore_empty: true + }, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {description: "Server-resolved localized text"} + ]; + } +}