commit 226ed50e0d94e069d4f53c2ef6b59fe25e916408 parent f1fcd13732731a1fa02f79349a2f97f0dcdc1a95 Author: Christian Grothoff <christian@grothoff.org> Date: Tue, 4 Aug 2026 16:02:17 +0200 introduce 'Slug' type as a refinement of 'string' Diffstat:
54 files changed, 109 insertions(+), 80 deletions(-)
diff --git a/core/api-apns.rst b/core/api-apns.rst @@ -90,7 +90,7 @@ Device Registration interface DeviceRegistrationRequest { // Device token - token: string; + token: Slug; } **Response:** diff --git a/core/api-common.rst b/core/api-common.rst @@ -277,6 +277,35 @@ Common encodings This section describes how certain types of values are represented throughout the API. +.. _slug: + +Slugs +^^^^^ + +Many objects are identified by short, mostly human-chosen labels, such as +merchant instance, order, product, template or token family identifiers. +Because these labels are used as path components of HTTP endpoint URLs (like +``$INSTANCE`` or ``$ORDER_ID``), they must be usable in a URL path segment +without any escaping. We call such an identifier a *slug*. + +.. ts:def:: Slug + + // Non-empty string that can be used as-is as a single path + // component of a URL. Thus, a slug must not contain a "/" + // and must only use characters that RFC 3986 allows in a path + // segment without percent-encoding, that is unreserved + // characters ("A-Z", "a-z", "0-9", "-", ".", "_", "~"), + // sub-delimiters ("!$&'()*+,;=") and ":" and "@". + // Slugs are case-sensitive. Individual endpoints may impose + // additional restrictions on the slugs they accept; those are + // documented with the respective endpoint or field. + type Slug = string; + +Note that a slug is *not* the same as an opaque identifier that merely happens +to be a string: identifiers that may contain arbitrary characters (and thus +require percent-encoding when put into a URL) are documented as plain +``string``. + .. _payto: diff --git a/core/api-corebank.rst b/core/api-corebank.rst @@ -207,7 +207,7 @@ remain absolutely unchanged. interface Challenge { // Unique identifier of the challenge to solve to run this protected // operation. - challenge_id: string; + challenge_id: Slug; // Channel that will be used to transmit the challenge. tan_channel: TanChannel; diff --git a/core/api-merchant.rst b/core/api-merchant.rst @@ -268,7 +268,7 @@ remain absolutely unchanged. interface Challenge { // Unique identifier of the challenge to solve to run this protected // operation. - challenge_id: string; + challenge_id: Slug; // Channel of the last successful transmission of the TAN challenge. tan_channel: TanChannel; @@ -1239,7 +1239,7 @@ Template Choices // Token family slug as configured in the merchant backend. Slug is unique // across all configured tokens of a merchant. - token_family_slug: string; + token_family_slug: Slug; // How many units of the input are required. // Defaults to 1 if not specified. Output with count == 0 are ignored by @@ -1261,7 +1261,7 @@ Template Choices // Token family slug as configured in the merchant backend. Slug is unique // across all configured tokens of a merchant. - token_family_slug: string; + token_family_slug: Slug; // How many units of the output are issued by the merchant. // Defaults to 1 if not specified. Output with count == 0 are ignored by @@ -1313,7 +1313,7 @@ and basically present (or optional) all the time. // before the customer paid for them, the ``order_id`` can be used // by the frontend to restore a proposal from the information // encoded in it (such as a short product identifier and timestamp). - order_id: string; + order_id: Slug; // URL where the same contract could be ordered again (if // available). Returned also at the public order endpoint @@ -1548,7 +1548,7 @@ frontend must already have calculated the total price --- including the interface MinimalInventoryProduct { // Which product is requested (here mandatory!). - product_id: string; + product_id: Slug; // Legacy integer quantity. // Deprecated since **v25**; @@ -1730,7 +1730,7 @@ and is thus not yet buyer-specific. // Map of storing metadata and issue keys of // token families referenced in this contract. // @since protocol **v21** - token_families: { [token_family_slug: string]: ContractTokenFamily }; + token_families: { [token_family_slug: Slug]: ContractTokenFamily }; } .. ts:def:: ContractTermsCommon @@ -1831,7 +1831,7 @@ and is thus not yet buyer-specific. // Slug of the token family in the // ``token_families`` map on the order. - token_family_slug: string; + token_family_slug: Slug; // Number of tokens of this type required. // Defaults to one if the field is not provided. @@ -1850,7 +1850,7 @@ and is thus not yet buyer-specific. // Slug of the token family in the // 'token_families' map on the top-level. - token_family_slug: string; + token_family_slug: Slug; // Number of tokens to be issued. // Defaults to one if the field is not provided. @@ -1898,7 +1898,7 @@ It has the following structure: interface ProductSold { // Merchant-internal identifier for the product. - product_id?: string; + product_id?: Slug; // Name of the product. // Since API version **v20**. Optional only for @@ -1919,7 +1919,7 @@ It has the following structure: unit_quantity?: string; // Unit in which the product is measured (liters, kilograms, packages, etc.). - unit?: string; + unit?: Slug; // The price of the product; // Deprecated since **v25**; diff --git a/core/api-terminal.rst b/core/api-terminal.rst @@ -251,7 +251,7 @@ Config interface TerminalWithdrawalSetupResponse { // ID identifying the withdrawal operation being created. - withdrawal_id: string; + withdrawal_id: Slug; } diff --git a/core/corebank/get-accounts.rst b/core/corebank/get-accounts.rst @@ -54,7 +54,7 @@ interface AccountMinimalData { // Username of the account - username: string; + username: Slug; // Legal name of the account owner. name: string; diff --git a/core/corebank/get-cashouts.rst b/core/corebank/get-cashouts.rst @@ -45,5 +45,5 @@ interface GlobalCashoutInfo { cashout_id: Integer; - username: string; + username: Slug; } diff --git a/core/corebank/get-public-accounts.rst b/core/corebank/get-public-accounts.rst @@ -38,7 +38,7 @@ interface PublicAccount { // Username of the account - username: string; + username: Slug; // Full payto URI of this bank account. payto_uri: string; diff --git a/core/corebank/get-withdrawals-WITHDRAWAL_ID.rst b/core/corebank/get-withdrawals-WITHDRAWAL_ID.rst @@ -53,7 +53,7 @@ no_amount_to_wallet?: boolean; // Account username - username: string; + username: Slug; // Reserve public key selected by the exchange, // only non-null if ``status`` is ``selected`` or ``confirmed``. diff --git a/core/corebank/post-accounts-USERNAME-withdrawals.rst b/core/corebank/post-accounts-USERNAME-withdrawals.rst @@ -47,7 +47,7 @@ interface BankAccountCreateWithdrawalResponse { // ID identifying the operation being created - withdrawal_id: string; + withdrawal_id: Slug; // URI that can be passed to the wallet to initiate the withdrawal taler_withdraw_uri: string; diff --git a/core/corebank/post-accounts.rst b/core/corebank/post-accounts.rst @@ -11,7 +11,7 @@ interface RegisterAccountRequest { // Username of the account // Must match [a-zA-Z0-9-._~]{1, 126} - username: string; + username: Slug; // Password of the account used for authentication password: string; diff --git a/core/merchant/get-fountain-info.rst b/core/merchant/get-fountain-info.rst @@ -42,7 +42,7 @@ interface FountainWalletGrant { // Slug of the token family. - token_family_slug: string; + token_family_slug: Slug; // Maximum withdrawals per key validity period. tokens_per_period_limit: Integer; diff --git a/core/merchant/get-management-instances.rst b/core/merchant/get-management-instances.rst @@ -39,7 +39,7 @@ logo?: ImageDataUrl; // Merchant instance this response is about ($INSTANCE). - id: string; + id: Slug; // Public key of the merchant/instance, in Crockford Base32 encoding. merchant_pub: EddsaPublicKey; diff --git a/core/merchant/get-orders-ORDER_ID.rst b/core/merchant/get-orders-ORDER_ID.rst @@ -136,5 +136,5 @@ // Alternative order ID which was paid for already in the same session. // Only given if the same product was purchased before in the same session. - already_paid_order_id?: string; + already_paid_order_id?: Slug; } diff --git a/core/merchant/get-private-categories-CATEGORY_ID.rst b/core/merchant/get-private-categories-CATEGORY_ID.rst @@ -43,6 +43,6 @@ interface CategoryProductSummary { // ID of a product in the category. - product_id: string; + product_id: Slug; } diff --git a/core/merchant/get-private-fountains.rst b/core/merchant/get-private-fountains.rst @@ -30,7 +30,7 @@ interface FountainEntry { // Identifier of the fountain. - fountain_id: string; + fountain_id: Slug; // Human-readable description of the fountain. description: string; diff --git a/core/merchant/get-private-incoming-ID.rst b/core/merchant/get-private-incoming-ID.rst @@ -89,7 +89,7 @@ // ID of the order for which these are the // reconciliation details. - order_id: string; + order_id: Slug; // Remaining deposit total to be paid, // that is the total amount of the order diff --git a/core/merchant/get-private-orders-ORDER_ID.rst b/core/merchant/get-private-orders-ORDER_ID.rst @@ -177,7 +177,7 @@ // Alternative order ID which was paid for already in the same session. // Only given if the same product was purchased before in the same session. - already_paid_order_id?: string; + already_paid_order_id?: Slug; // Fulfillment URL of an already paid order. Only given if under this // session an already paid order with a fulfillment URL exists. diff --git a/core/merchant/get-private-orders.rst b/core/merchant/get-private-orders.rst @@ -66,7 +66,7 @@ interface OrderHistoryEntry { // Order ID of the transaction related to this entry. - order_id: string; + order_id: Slug; // Row ID of the order in the database. row_id: Integer; diff --git a/core/merchant/get-private-otp-devices.rst b/core/merchant/get-private-otp-devices.rst @@ -31,7 +31,7 @@ interface OtpDeviceEntry { // Device identifier. - otp_device_id: string; + otp_device_id: Slug; // Human-readable description for the device. device_description: string; diff --git a/core/merchant/get-private-pos.rst b/core/merchant/get-private-pos.rst @@ -39,7 +39,7 @@ product_serial: Integer; // A merchant-internal unique identifier for the product. - product_id: string; + product_id: Slug; // Human-readable product name. // Since API version **v20**. @@ -56,7 +56,7 @@ description_i18n: { [lang_tag: string]: string }; // Unit in which the product is measured (liters, kilograms, packages, etc.). - unit: string; + unit: Slug; // Does the backend allow fractional quantities for this unit? unit_allow_fraction: boolean; diff --git a/core/merchant/get-private-products-PRODUCT_ID.rst b/core/merchant/get-private-products-PRODUCT_ID.rst @@ -35,7 +35,7 @@ description_i18n: { [lang_tag: string]: string }; // Unit in which the product is measured (liters, kilograms, packages, etc.). - unit: string; + unit: Slug; // Does the backend allow fractional quantities for this unit? unit_allow_fraction: boolean; diff --git a/core/merchant/get-private-products.rst b/core/merchant/get-private-products.rst @@ -41,7 +41,7 @@ interface InventoryEntry { // Product identifier, as found in the product. - product_id: string; + product_id: Slug; // ``product_serial_id`` of the product in the database. product_serial: Integer; diff --git a/core/merchant/get-private-templates-TEMPLATE_ID.rst b/core/merchant/get-private-templates-TEMPLATE_ID.rst @@ -31,7 +31,7 @@ // OTP device ID. // This parameter is optional. - otp_id?: string; + otp_id?: Slug; // Additional information in a separate template. template_contract: TemplateContractDetails; diff --git a/core/merchant/get-private-templates.rst b/core/merchant/get-private-templates.rst @@ -34,7 +34,7 @@ interface TemplateEntry { // Template identifier, as found in the template. - template_id: string; + template_id: Slug; // Human-readable description for the template. template_description: string; diff --git a/core/merchant/get-private-tokenfamilies-TOKEN_FAMILY_SLUG.rst b/core/merchant/get-private-tokenfamilies-TOKEN_FAMILY_SLUG.rst @@ -30,7 +30,7 @@ // Identifier for the token family consisting of unreserved characters // according to RFC 3986. - slug: string; + slug: Slug; // Human-readable name for the token family. name: string; diff --git a/core/merchant/get-private-tokenfamilies.rst b/core/merchant/get-private-tokenfamilies.rst @@ -34,7 +34,7 @@ interface TokenFamilySummary { // Identifier for the token family consisting of unreserved characters // according to RFC 3986. - slug: string; + slug: Slug; // Human-readable name for the token family. name: string; diff --git a/core/merchant/get-private-units.rst b/core/merchant/get-private-units.rst @@ -29,7 +29,7 @@ unit_serial: Integer; // Short identifier used in product definitions. - unit: string; + unit: Slug; // Human-readable long label. unit_name_long: string; diff --git a/core/merchant/get-private-webhooks.rst b/core/merchant/get-private-webhooks.rst @@ -35,7 +35,7 @@ interface WebhookEntry { // Webhook identifier, as found in the webhook. - webhook_id: string; + webhook_id: Slug; // The event of the webhook: why the webhook is used. event_type: WebhookEventType; diff --git a/core/merchant/get-sessions-SESSION_ID.rst b/core/merchant/get-sessions-SESSION_ID.rst @@ -47,7 +47,7 @@ interface GetSessionStatusPaidResponse { // Order ID of the paid order. - order_id: string; + order_id: Slug; } @@ -56,6 +56,6 @@ interface GetSessionStatusUnpaidResponse { // Order ID of the unpaid order. - order_id: string; + order_id: Slug; } diff --git a/core/merchant/get-templates-TEMPLATE_ID.rst b/core/merchant/get-templates-TEMPLATE_ID.rst @@ -73,7 +73,7 @@ interface InventoryPayloadProduct { // Product identifier. // Since protocol **v25**. - product_id: string; + product_id: Slug; // Human-readable product name. // Since protocol **v25**. @@ -89,7 +89,7 @@ // Unit identifier for the product. // Since protocol **v25**. - unit: string; + unit: Slug; // Price tiers for the product. // Since protocol **v25**. @@ -141,7 +141,7 @@ interface InventoryPayloadUnit { // Unit identifier. // Since protocol **v25**. - unit: string; + unit: Slug; // Human-readable long label. // Since protocol **v25**. diff --git a/core/merchant/patch-private-products-PRODUCT_ID.rst b/core/merchant/patch-private-products-PRODUCT_ID.rst @@ -63,7 +63,7 @@ description_i18n?: { [lang_tag: string]: string }; // Unit in which the product is measured (liters, kilograms, packages, etc.). - unit: string; + unit: Slug; // Optional override to control whether fractional quantities are permitted. unit_allow_fraction?: boolean; diff --git a/core/merchant/patch-private-templates-TEMPLATE_ID.rst b/core/merchant/patch-private-templates-TEMPLATE_ID.rst @@ -36,7 +36,7 @@ // OTP device ID. // This parameter is optional. - otp_id?: string; + otp_id?: Slug; // Additional information in a separate template. template_contract: TemplateContractDetails; diff --git a/core/merchant/post-fountain-withdraw.rst b/core/merchant/post-fountain-withdraw.rst @@ -54,7 +54,7 @@ // Token family to withdraw from. Must match one of the // fountain's grants. - token_family_slug: string; + token_family_slug: Slug; // Desired validity time of the tokens; selects the issue // key slot. Must lie within the grant's @@ -82,7 +82,7 @@ interface FountainGrantWithdrawalResult { // Token family these signatures belong to. - token_family_slug: string; + token_family_slug: Slug; // Hash of the issue public key that was used, so the // wallet can associate the tokens with the right slot. diff --git a/core/merchant/post-management-instances.rst b/core/merchant/post-management-instances.rst @@ -52,7 +52,7 @@ // Name of the merchant instance to create (will become $INSTANCE). // Must match the regex ``^[A-Za-z0-9][A-Za-z0-9_.@-]+$``. - id: string; + id: Slug; // Merchant name corresponding to this instance. name: string; diff --git a/core/merchant/post-orders-ORDER_ID-paid.rst b/core/merchant/post-orders-ORDER_ID-paid.rst @@ -69,5 +69,5 @@ h_contract: HashCode; // Session id for which the payment is proven. - session_id: string; + session_id: Slug; } diff --git a/core/merchant/post-orders-ORDER_ID-pay.rst b/core/merchant/post-orders-ORDER_ID-pay.rst @@ -253,7 +253,7 @@ // The session for which the payment is made (or replayed). // Only set for session-based payments. - session_id?: string; + session_id?: Slug; } diff --git a/core/merchant/post-private-fountains.rst b/core/merchant/post-private-fountains.rst @@ -52,7 +52,7 @@ interface FountainGrant { // Slug of the token family this grant refers to. - token_family_slug: string; + token_family_slug: Slug; // Maximum number of tokens the fountain will blind-sign // per key validity period for this family. @@ -75,7 +75,7 @@ // Identifier of the fountain, used for subsequent CRUD // operations and included in the wallet onboarding URI. - fountain_id: string; + fountain_id: Slug; // 256-bit bearer credential for the wallet, Crockford // Base32 encoded. Returned exactly once; the backend diff --git a/core/merchant/post-private-orders-ORDER_ID-collect.rst b/core/merchant/post-private-orders-ORDER_ID-collect.rst @@ -78,7 +78,7 @@ // Session ID to store with the payment, like the ``session_id`` of // a wallet payment. Allows a point-of-sale device to tag and later // list the orders it collected. Absent for no session. - session_id?: string; + session_id?: Slug; // For v1 contracts, the index of the choice to complete within // the ``choices`` array of the contract terms. Mandatory for v1 diff --git a/core/merchant/post-private-orders.rst b/core/merchant/post-private-orders.rst @@ -118,7 +118,7 @@ // The session for which the payment is made (or replayed). // Only set for session-based payments. // Since protocol **v6**. - session_id?: string; + session_id?: Slug; // Specifies that some products are to be included in the // order from the inventory. For these inventory management @@ -145,7 +145,7 @@ // OTP device ID to associate with the order. // This parameter is optional. - otp_id?: string; + otp_id?: Slug; } @@ -153,7 +153,7 @@ interface PostOrderResponse { // Order ID of the response that was just created. - order_id: string; + order_id: Slug; // Deadline when the offer expires; the customer must pay before. // @since protocol **v21**. @@ -170,7 +170,7 @@ interface OutOfStockResponse { // Product ID of an out-of-stock item. - product_id: string; + product_id: Slug; // Legacy integer quantity requested. Deprecated; see ``unit_requested_quantity``. requested_quantity: Integer; diff --git a/core/merchant/post-private-otp-devices.rst b/core/merchant/post-private-otp-devices.rst @@ -43,7 +43,7 @@ interface OtpDeviceAddDetails { // Device ID to use. - otp_device_id: string; + otp_device_id: Slug; // Human-readable description for the device. otp_device_description: string; diff --git a/core/merchant/post-private-products.rst b/core/merchant/post-private-products.rst @@ -42,7 +42,7 @@ interface ProductAddDetailRequest { // Product ID to use. - product_id: string; + product_id: Slug; // Human-readable product name. // Since API version **v20**. Optional only for @@ -62,7 +62,7 @@ categories?: Integer[]; // Unit in which the product is measured (liters, kilograms, packages, etc.). - unit: string; + unit: Slug; // Optional override to control whether fractional quantities are permitted. // Defaults to the policy implied by ``unit``. diff --git a/core/merchant/post-private-templates.rst b/core/merchant/post-private-templates.rst @@ -39,14 +39,14 @@ interface TemplateCreateRequest { // Template ID to use. - template_id: string; + template_id: Slug; // Human-readable description for the template. template_description: string; // OTP device ID. // This parameter is optional. - otp_id?: string; + otp_id?: Slug; // Fixed contract information for orders created from // this template. diff --git a/core/merchant/post-private-tokenfamilies.rst b/core/merchant/post-private-tokenfamilies.rst @@ -42,7 +42,7 @@ // Identifier for the token family consisting of unreserved characters // according to RFC 3986. - slug: string; + slug: Slug; // Human-readable name for the token family. name: string; diff --git a/core/merchant/post-private-units.rst b/core/merchant/post-private-units.rst @@ -35,7 +35,7 @@ interface UnitAddRequest { // Short identifier to reference the unit from products and orders. - unit: string; + unit: Slug; // Human-readable long label (e.g. "kilogram"). unit_name_long: string; diff --git a/core/merchant/post-private-webhooks.rst b/core/merchant/post-private-webhooks.rst @@ -38,7 +38,7 @@ interface WebhookAddDetails { // Webhook ID to use. - webhook_id: string; + webhook_id: Slug; // The event of the webhook: why the webhook is used. event_type: WebhookEventType; diff --git a/core/merchant/post-templates-TEMPLATE_ID.rst b/core/merchant/post-templates-TEMPLATE_ID.rst @@ -101,7 +101,7 @@ interface InventorySelectionEntry { // Inventory product to add. - product_id: string; + product_id: Slug; // Quantity in "<integer>[.<fraction>]" form using the product unit rules. quantity: DecimalQuantity; diff --git a/core/taldir/get-config.rst b/core/taldir/get-config.rst @@ -30,7 +30,7 @@ interface TaldirAliasType { // Name of the alias type, e.g. "email" or "sms". - name: string; + name: Slug; // per challenge fee challenge_fee: Amount; diff --git a/design-documents/046-mumimo-contracts.rst b/design-documents/046-mumimo-contracts.rst @@ -104,7 +104,7 @@ The contract terms v1 will have the following structure: // before the customer paid for them, the ``order_id`` can be used // by the frontend to restore a proposal from the information // encoded in it (such as a short product identifier and timestamp). - order_id: string; + order_id: Slug; // URL where the same contract could be ordered again (if // available). Returned also at the public order endpoint @@ -201,7 +201,7 @@ The contract terms v1 will have the following structure: // Map from token family slugs to meta data about the // respective token family. - token_families: { [token_family_slug: string]: ContractTokenFamily }; + token_families: { [token_family_slug: Slug]: ContractTokenFamily }; // Extra data that is only interpreted by the merchant frontend. // Useful when the merchant needs to store extra information on a @@ -263,7 +263,7 @@ The contract terms v1 will have the following structure: // Slug of the token family in the // 'token_families' map on the order. - token_family_slug: string; + token_family_slug: Slug; // Start of the validity period of the token. This is used to find the // matching public key within the token family. @@ -312,7 +312,7 @@ The contract terms v1 will have the following structure: // Slug of the token family in the // 'token_families' map on the top-level. - token_family_slug: string; + token_family_slug: Slug; // Start of the validity period of the token. This is used to find the // matching public key within the token family. diff --git a/design-documents/072-products-units.rst b/design-documents/072-products-units.rst @@ -84,7 +84,7 @@ Proposed Solution interface MerchantUnit { // Backend identifier used in product payloads. - unit: string; + unit: Slug; // Localised long label. unit_name_long: string; @@ -139,7 +139,7 @@ Proposed Solution .. ts:def:: MerchantUnitCreateRequest interface MerchantUnitCreateRequest { - unit: string; + unit: Slug; unit_name_long: string; // Optional translations for the long label (defaults to null). unit_name_long_i18n?: InternationalizedString | null; diff --git a/design-documents/073-extended-merchant-template.rst b/design-documents/073-extended-merchant-template.rst @@ -99,14 +99,14 @@ contract. interface TemplateAddDetails { // Template ID to use. - template_id: string; + template_id: Slug; // Human-readable description for the template. template_description: string; // OTP device ID. // This parameter is optional. - otp_id?: string; + otp_id?: Slug; // Fixed contract information for orders created from // this template. @@ -283,12 +283,12 @@ wallet and backend. .. ts:def:: WalletInventoryProduct interface WalletInventoryProduct { - product_id: string; + product_id: Slug; product_name: string; description: string; description_i18n: { [lang_tag: string]: string }; taxes?: Tax[]; - unit: string; + unit: Slug; // Optional translations for non-standard units. unit_name_short_i18n?: { [lang_tag: string]: string }; unit_prices: Amount[]; diff --git a/design-documents/083-wallet-initiated-withdrawal.rst b/design-documents/083-wallet-initiated-withdrawal.rst @@ -69,7 +69,7 @@ API interface BankWithdrawalOperationCreateResponse { // ID identifying the operation being created - withdrawal_id: string; + withdrawal_id: Slug; } Test Plan diff --git a/design-documents/098-token-fountains.rst b/design-documents/098-token-fountains.rst @@ -203,7 +203,7 @@ Creation returns: interface FountainCreateResponse { // Public identifier included in the wallet onboarding URI. - fountain_id: string; + fountain_id: Slug; // 256-bit bearer credential, Crockford Base32 encoded. // Returned only at creation; the backend stores only its hash. diff --git a/taler-merchant-pos-terminal.rst b/taler-merchant-pos-terminal.rst @@ -107,7 +107,7 @@ The elements of the JSON file are defined as follows: interface MerchantProduct { // A merchant-internal unique identifier for the product - product_id?: string; + product_id?: Slug; // Human-readable product description // that will be shown to the user and used in contract terms