@hablala/api-contract 0.6.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/openapi.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "responses": {},
4
4
  "schemas": {
5
5
  "AccessToken": {
6
- "description": "Metadatos de un access token de máquina (`hpat_…`): la credencial con la que un agente (Claude Code) o un script CI escribe el modelo de datos por HTTP. El secreto en claro NO se incluye aquí — solo se devuelve una vez, al crearlo.",
6
+ "description": "Metadatos de un access token de máquina (`mat_…`): la credencial con la que un agente (Claude Code) o un script CI escribe el modelo de datos por HTTP. El secreto en claro NO se incluye aquí — solo se devuelve una vez, al crearlo.",
7
7
  "properties": {
8
8
  "active": {
9
9
  "description": "¿Ni revocado ni expirado?",
@@ -54,7 +54,7 @@
54
54
  "x-validate": null
55
55
  },
56
56
  "prefix": {
57
- "description": "Primeros caracteres del secreto (no secreto), p. ej. `hpat_a1b2`.",
57
+ "description": "Primeros caracteres del secreto (no secreto), p. ej. `mat_a1b2`.",
58
58
  "nullable": true,
59
59
  "type": "string",
60
60
  "x-struct": null,
@@ -147,7 +147,7 @@
147
147
  "x-validate": null
148
148
  },
149
149
  "token": {
150
- "description": "El secreto en claro (`hpat_…`). Solo en esta respuesta.",
150
+ "description": "El secreto en claro (`mat_…`). Solo en esta respuesta.",
151
151
  "type": "string",
152
152
  "x-struct": null,
153
153
  "x-validate": null
@@ -246,7 +246,7 @@
246
246
  "x-validate": null
247
247
  },
248
248
  "name": {
249
- "description": "Etiqueta para identificarlo en el panel.",
249
+ "description": "Etiqueta opcional para el panel. Por defecto: \"Default\".",
250
250
  "type": "string",
251
251
  "x-struct": null,
252
252
  "x-validate": null
@@ -269,9 +269,6 @@
269
269
  "x-validate": null
270
270
  }
271
271
  },
272
- "required": [
273
- "name"
274
- ],
275
272
  "title": "AccessTokenRequest",
276
273
  "type": "object",
277
274
  "x-struct": "Elixir.HablalaApiWeb.Schemas.AccessTokenRequest",
@@ -953,6 +950,17 @@
953
950
  "x-struct": null,
954
951
  "x-validate": null
955
952
  },
953
+ "packages": {
954
+ "description": "Los verticales instalables sobre el core horizontal (p. ej. `commerce`, `crm`). El server materializa sus objects (seeded, con su `package`) desde su definición propia; el manifiesto solo declara los NOMBRES. Instalar es idempotente; declarar no desinstala.",
955
+ "items": {
956
+ "type": "string",
957
+ "x-struct": null,
958
+ "x-validate": null
959
+ },
960
+ "type": "array",
961
+ "x-struct": null,
962
+ "x-validate": null
963
+ },
956
964
  "policies": {
957
965
  "oneOf": [
958
966
  {
@@ -1828,8 +1836,41 @@
1828
1836
  "x-struct": "Elixir.HablalaApiWeb.Schemas.ApiKeyCreatedResponse",
1829
1837
  "x-validate": null
1830
1838
  },
1839
+ "ApiKeyPairCreated": {
1840
+ "description": "El par de API keys recién creado: la publishable (`pk_`) y la secret (`sk_`), cada una con su secreto en claro (`token`) presente UNA sola vez. Guárdalos: no se pueden recuperar.",
1841
+ "properties": {
1842
+ "publishable": {
1843
+ "$ref": "#/components/schemas/ApiKeyCreated"
1844
+ },
1845
+ "secret": {
1846
+ "$ref": "#/components/schemas/ApiKeyCreated"
1847
+ }
1848
+ },
1849
+ "required": [
1850
+ "publishable",
1851
+ "secret"
1852
+ ],
1853
+ "title": "ApiKeyPairCreated",
1854
+ "type": "object",
1855
+ "x-struct": "Elixir.HablalaApiWeb.Schemas.ApiKeyPairCreated",
1856
+ "x-validate": null
1857
+ },
1858
+ "ApiKeyPairCreatedResponse": {
1859
+ "properties": {
1860
+ "data": {
1861
+ "$ref": "#/components/schemas/ApiKeyPairCreated"
1862
+ }
1863
+ },
1864
+ "required": [
1865
+ "data"
1866
+ ],
1867
+ "title": "ApiKeyPairCreatedResponse",
1868
+ "type": "object",
1869
+ "x-struct": "Elixir.HablalaApiWeb.Schemas.ApiKeyPairCreatedResponse",
1870
+ "x-validate": null
1871
+ },
1831
1872
  "ApiKeyRequest": {
1832
- "description": "Datos para emitir una API key. Todos opcionales (kind por defecto: publishable).",
1873
+ "description": "Datos para emitir el par de API keys. Todos opcionales: `name` por defecto \"Default\" (lo comparten ambas keys del par). `kind` solo aplica a la vía avanzada de una sola key.",
1833
1874
  "properties": {
1834
1875
  "expiresAt": {
1835
1876
  "description": "Caducidad opcional (ISO 8601). Omitir = no caduca.",
@@ -1839,7 +1880,7 @@
1839
1880
  "x-validate": null
1840
1881
  },
1841
1882
  "kind": {
1842
- "description": "Por defecto: publishable.",
1883
+ "description": "Solo vía avanzada (una key). Por defecto: publishable.",
1843
1884
  "enum": [
1844
1885
  "publishable",
1845
1886
  "secret"
@@ -1849,7 +1890,7 @@
1849
1890
  "x-validate": null
1850
1891
  },
1851
1892
  "name": {
1852
- "description": "Etiqueta para identificarla en el panel.",
1893
+ "description": "Etiqueta opcional para el panel. Por defecto: \"Default\".",
1853
1894
  "type": "string",
1854
1895
  "x-struct": null,
1855
1896
  "x-validate": null
@@ -2638,6 +2679,29 @@
2638
2679
  "x-struct": "Elixir.HablalaApiWeb.Schemas.AvailabilityRequest",
2639
2680
  "x-validate": null
2640
2681
  },
2682
+ "AvailabilityResponse": {
2683
+ "description": "Disponibilidad de compra por variante (patrón Shopify `availableForSale`): un mapa id-público → booleano. `true` = comprable (activa y con stock). Se expone el booleano derivado, no el conteo de stock (mínima exposición).",
2684
+ "properties": {
2685
+ "availability": {
2686
+ "additionalProperties": {
2687
+ "type": "boolean",
2688
+ "x-struct": null,
2689
+ "x-validate": null
2690
+ },
2691
+ "description": "Mapa `variantId` (`rec_…`) → `true`/`false` (comprable).",
2692
+ "type": "object",
2693
+ "x-struct": null,
2694
+ "x-validate": null
2695
+ }
2696
+ },
2697
+ "required": [
2698
+ "availability"
2699
+ ],
2700
+ "title": "AvailabilityResponse",
2701
+ "type": "object",
2702
+ "x-struct": "Elixir.HablalaApiWeb.Schemas.AvailabilityResponse",
2703
+ "x-validate": null
2704
+ },
2641
2705
  "BanMemberRequest": {
2642
2706
  "description": "Banea a un miembro del grupo (bloqueo, estilo Telegram). Sin `until` = ban permanente; con `until` = ban temporal que expira en esa fecha.",
2643
2707
  "properties": {
@@ -3125,6 +3189,60 @@
3125
3189
  "x-struct": "Elixir.HablalaApiWeb.Schemas.ChannelRequest",
3126
3190
  "x-validate": null
3127
3191
  },
3192
+ "CheckoutItem": {
3193
+ "description": "Un artículo del checkout: qué variante comprar y cuántas unidades.",
3194
+ "properties": {
3195
+ "quantity": {
3196
+ "description": "Unidades a comprar (≥ 1).",
3197
+ "minimum": 1,
3198
+ "type": "integer",
3199
+ "x-struct": null,
3200
+ "x-validate": null
3201
+ },
3202
+ "variantId": {
3203
+ "description": "Id público (`rec_…`) del `product_variant`.",
3204
+ "type": "string",
3205
+ "x-struct": null,
3206
+ "x-validate": null
3207
+ }
3208
+ },
3209
+ "required": [
3210
+ "variantId",
3211
+ "quantity"
3212
+ ],
3213
+ "title": "CheckoutItem",
3214
+ "type": "object",
3215
+ "x-struct": "Elixir.HablalaApiWeb.Schemas.CheckoutItem",
3216
+ "x-validate": null
3217
+ },
3218
+ "CheckoutRequest": {
3219
+ "description": "Checkout nativo: los artículos del carrito + la moneda. El pedido se ata al usuario final identificado por la sesión (`sess_…`), no a un cliente enviado por el cuerpo.",
3220
+ "properties": {
3221
+ "currency": {
3222
+ "description": "Código ISO 4217 (MXN, USD, EUR).",
3223
+ "type": "string",
3224
+ "x-struct": null,
3225
+ "x-validate": null
3226
+ },
3227
+ "items": {
3228
+ "items": {
3229
+ "$ref": "#/components/schemas/CheckoutItem"
3230
+ },
3231
+ "minItems": 1,
3232
+ "type": "array",
3233
+ "x-struct": null,
3234
+ "x-validate": null
3235
+ }
3236
+ },
3237
+ "required": [
3238
+ "items",
3239
+ "currency"
3240
+ ],
3241
+ "title": "CheckoutRequest",
3242
+ "type": "object",
3243
+ "x-struct": "Elixir.HablalaApiWeb.Schemas.CheckoutRequest",
3244
+ "x-validate": null
3245
+ },
3128
3246
  "ContactContent": {
3129
3247
  "description": "Payload de un mensaje `contact` (compartir contacto, estilo WhatsApp). El cliente pinta una tarjeta con acción 'Agregar a contactos'. `vcard` es el vCard crudo opcional (fuente de verdad para importar); `name`/`phones` son la vista renderizable.",
3130
3248
  "properties": {
@@ -4498,6 +4616,75 @@
4498
4616
  "x-struct": "Elixir.HablalaApiWeb.Schemas.EventsResponse",
4499
4617
  "x-validate": null
4500
4618
  },
4619
+ "FileEnvelope": {
4620
+ "description": "Un archivo dentro del valor de un atributo `data_type: file` (siempre un array de estos, aun para un solo archivo). Solo `url` es obligatoria; el resto es metadata para renderizar sin un HEAD por archivo. La `url` DEBE ser del storage de la plataforma (subida por el flujo de upload prefirmado) — una URL externa se rechaza.",
4621
+ "properties": {
4622
+ "blurhash": {
4623
+ "description": "Placeholder BlurHash de la imagen (preview borroso instantáneo). Null si no es imagen.",
4624
+ "nullable": true,
4625
+ "type": "string",
4626
+ "x-struct": null,
4627
+ "x-validate": null
4628
+ },
4629
+ "content_type": {
4630
+ "nullable": true,
4631
+ "type": "string",
4632
+ "x-struct": null,
4633
+ "x-validate": null
4634
+ },
4635
+ "duration_ms": {
4636
+ "description": "Duración del medio en milisegundos (audio/video). Permite pintar el reproductor sin descargar.",
4637
+ "minimum": 0,
4638
+ "nullable": true,
4639
+ "type": "integer",
4640
+ "x-struct": null,
4641
+ "x-validate": null
4642
+ },
4643
+ "filename": {
4644
+ "nullable": true,
4645
+ "type": "string",
4646
+ "x-struct": null,
4647
+ "x-validate": null
4648
+ },
4649
+ "height": {
4650
+ "description": "Alto en px.",
4651
+ "minimum": 0,
4652
+ "nullable": true,
4653
+ "type": "integer",
4654
+ "x-struct": null,
4655
+ "x-validate": null
4656
+ },
4657
+ "size": {
4658
+ "description": "Tamaño en bytes.",
4659
+ "minimum": 0,
4660
+ "nullable": true,
4661
+ "type": "integer",
4662
+ "x-struct": null,
4663
+ "x-validate": null
4664
+ },
4665
+ "url": {
4666
+ "description": "URL permanente del blob en el object storage (la que devuelve el presign).",
4667
+ "type": "string",
4668
+ "x-struct": null,
4669
+ "x-validate": null
4670
+ },
4671
+ "width": {
4672
+ "description": "Ancho en px (para reservar el aspect-ratio y evitar layout shift).",
4673
+ "minimum": 0,
4674
+ "nullable": true,
4675
+ "type": "integer",
4676
+ "x-struct": null,
4677
+ "x-validate": null
4678
+ }
4679
+ },
4680
+ "required": [
4681
+ "url"
4682
+ ],
4683
+ "title": "FileEnvelope",
4684
+ "type": "object",
4685
+ "x-struct": "Elixir.HablalaApiWeb.Schemas.FileEnvelope",
4686
+ "x-validate": null
4687
+ },
4501
4688
  "Friend": {
4502
4689
  "properties": {
4503
4690
  "displayName": {
@@ -4804,7 +4991,7 @@
4804
4991
  "x-validate": null
4805
4992
  },
4806
4993
  "GraphNode": {
4807
- "description": "Un nodo del grafo de datos: un record, con su object, su título y su grado.",
4994
+ "description": "Un nodo del grafo de datos: un record, con su object, su título, su imagen representativa y su grado.",
4808
4995
  "properties": {
4809
4996
  "degree": {
4810
4997
  "description": "Nº de aristas incidentes al nodo (grado). El cliente lo usa para el tamaño.",
@@ -4818,6 +5005,13 @@
4818
5005
  "x-struct": null,
4819
5006
  "x-validate": null
4820
5007
  },
5008
+ "imageUrl": {
5009
+ "description": "URL de la imagen representativa del record (primer atributo file de tipo imagen, respetando el field_mask), o null. El cliente la pinta como avatar del nodo; sin ella, el nodo es el círculo tintado por object.",
5010
+ "nullable": true,
5011
+ "type": "string",
5012
+ "x-struct": null,
5013
+ "x-validate": null
5014
+ },
4821
5015
  "objectSlug": {
4822
5016
  "description": "Slug del object del record (para color/navegación).",
4823
5017
  "type": "string",
@@ -8084,7 +8278,7 @@
8084
8278
  "x-validate": null
8085
8279
  },
8086
8280
  "OauthGrantApproveRequest": {
8087
- "description": "Aprobación del consent: qué permisos tendrá el token (mismas reglas que emitir un `hpat_` en el panel: `preset` gana, y todo se intersecta con los permisos del aprobador).",
8281
+ "description": "Aprobación del consent: qué permisos tendrá el token (mismas reglas que emitir un `mat_` en el panel: `preset` gana, y todo se intersecta con los permisos del aprobador).",
8088
8282
  "properties": {
8089
8283
  "permissions": {
8090
8284
  "description": "Permisos crudos `recurso:accion` (vía avanzada).",
@@ -8175,10 +8369,10 @@
8175
8369
  "x-validate": null
8176
8370
  },
8177
8371
  "OauthTokenResponse": {
8178
- "description": "Éxito del token endpoint (RFC 6749 §5.1): el `access_token` es un `hpat_…` normal (la misma credencial que emite el panel), mostrado UNA sola vez.",
8372
+ "description": "Éxito del token endpoint (RFC 6749 §5.1): el `access_token` es un `mat_…` normal (la misma credencial que emite el panel), mostrado UNA sola vez.",
8179
8373
  "properties": {
8180
8374
  "access_token": {
8181
- "description": "El secreto `hpat_…` en claro.",
8375
+ "description": "El secreto `mat_…` en claro.",
8182
8376
  "type": "string",
8183
8377
  "x-struct": null,
8184
8378
  "x-validate": null
@@ -8407,6 +8601,63 @@
8407
8601
  "x-struct": "Elixir.HablalaApiWeb.Schemas.ObjectsResponse",
8408
8602
  "x-validate": null
8409
8603
  },
8604
+ "OrderLineItem": {
8605
+ "description": "Un renglón del pedido: la variante comprada con snapshots de título/sku/precio (el catálogo puede cambiar después). `id` + `properties` como cualquier record del diccionario.",
8606
+ "properties": {
8607
+ "id": {
8608
+ "type": "string",
8609
+ "x-struct": null,
8610
+ "x-validate": null
8611
+ },
8612
+ "properties": {
8613
+ "description": "Valores del renglón por slug: `variant_ref`, `product_ref`, `title`, `sku`, `quantity`, `unit_price` (centavos), `line_total` (centavos).",
8614
+ "type": "object",
8615
+ "x-struct": null,
8616
+ "x-validate": null
8617
+ }
8618
+ },
8619
+ "required": [
8620
+ "id",
8621
+ "properties"
8622
+ ],
8623
+ "title": "OrderLineItem",
8624
+ "type": "object",
8625
+ "x-struct": "Elixir.HablalaApiWeb.Schemas.OrderLineItem",
8626
+ "x-validate": null
8627
+ },
8628
+ "OrderResponse": {
8629
+ "description": "El pedido recién creado por el checkout, con sus renglones. `properties` trae el `order_number` (#1001), `financial_status` (nace `pending`), `fulfillment_status` (nace `unfulfilled`), `total_amount` (centavos), `currency` y `customer`.",
8630
+ "properties": {
8631
+ "id": {
8632
+ "type": "string",
8633
+ "x-struct": null,
8634
+ "x-validate": null
8635
+ },
8636
+ "lineItems": {
8637
+ "items": {
8638
+ "$ref": "#/components/schemas/OrderLineItem"
8639
+ },
8640
+ "type": "array",
8641
+ "x-struct": null,
8642
+ "x-validate": null
8643
+ },
8644
+ "properties": {
8645
+ "description": "Valores del pedido por slug (order_number, financial_status, total_amount…).",
8646
+ "type": "object",
8647
+ "x-struct": null,
8648
+ "x-validate": null
8649
+ }
8650
+ },
8651
+ "required": [
8652
+ "id",
8653
+ "properties",
8654
+ "lineItems"
8655
+ ],
8656
+ "title": "OrderResponse",
8657
+ "type": "object",
8658
+ "x-struct": "Elixir.HablalaApiWeb.Schemas.OrderResponse",
8659
+ "x-validate": null
8660
+ },
8410
8661
  "Organization": {
8411
8662
  "properties": {
8412
8663
  "avatarUrl": {
@@ -9105,6 +9356,7 @@
9105
9356
  "x-validate": null
9106
9357
  },
9107
9358
  "condition": {
9359
+ "description": "Predicado por fila: hoja `{attr, op, value|value_from}` o árbol `and`/`or`. `attr` admite un attribute path de UN nivel por un attribute `reference` (`pet.owner`: condiciona sobre el record destino; fail-closed si la referencia es nula).",
9108
9360
  "type": "object",
9109
9361
  "x-struct": null,
9110
9362
  "x-validate": null
@@ -9206,6 +9458,7 @@
9206
9458
  "x-validate": null
9207
9459
  },
9208
9460
  "condition": {
9461
+ "description": "Predicado por fila: hoja `{attr, op, value|value_from}` o árbol `and`/`or`. `attr` admite un attribute path de UN nivel por un attribute `reference` (`pet.owner`: condiciona sobre el record destino; fail-closed si la referencia es nula).",
9209
9462
  "type": "object",
9210
9463
  "x-struct": null,
9211
9464
  "x-validate": null
@@ -9321,6 +9574,14 @@
9321
9574
  "x-struct": null,
9322
9575
  "x-validate": null
9323
9576
  },
9577
+ "maxSize": {
9578
+ "description": "Tope de bytes que el servidor FIRMA en el borde (POST policy `content-length-range`): un cuerpo mayor se rechaza con 413, sin confiar en el cliente. Tómalo del `max_size_bytes` del atributo `file`.",
9579
+ "minimum": 1,
9580
+ "nullable": true,
9581
+ "type": "integer",
9582
+ "x-struct": null,
9583
+ "x-validate": null
9584
+ },
9324
9585
  "purpose": {
9325
9586
  "description": "Uso del archivo. `avatar` restringe el `contentType` a imágenes (perfil de usuario / icono de org). Omitir para adjuntos genéricos.",
9326
9587
  "enum": [
@@ -9355,37 +9616,56 @@
9355
9616
  "x-validate": null
9356
9617
  },
9357
9618
  "PresignResult": {
9619
+ "description": "Sobre de subida auto-descriptivo. El cliente OBEDECE `method`: `POST` → arma un form multipart con TODOS los `fields` y añade el archivo como el campo `file` al final, luego hace POST a `url`; `PUT` → hace PUT del blob crudo a `url` con `headers`. En ambos, `fileUrl` es la URL final del blob.",
9358
9620
  "properties": {
9621
+ "fields": {
9622
+ "additionalProperties": {
9623
+ "type": "string",
9624
+ "x-struct": null,
9625
+ "x-validate": null
9626
+ },
9627
+ "description": "Campos del form multipart a enviar ANTES del archivo (POST policy: `key`, `Content-Type`, `Policy`, firma…). Vacío cuando `method` es `PUT`.",
9628
+ "type": "object",
9629
+ "x-struct": null,
9630
+ "x-validate": null
9631
+ },
9359
9632
  "fileUrl": {
9360
- "description": "URL final del blob; va en el `attachment` del mensaje.",
9633
+ "description": "URL final del blob; va en el `attachment` del mensaje o el atributo `file`.",
9361
9634
  "type": "string",
9362
9635
  "x-struct": null,
9363
9636
  "x-validate": null
9364
9637
  },
9365
9638
  "headers": {
9366
- "description": "Cabeceras que el cliente debe enviar en el PUT.",
9639
+ "additionalProperties": {
9640
+ "type": "string",
9641
+ "x-struct": null,
9642
+ "x-validate": null
9643
+ },
9644
+ "description": "Cabeceras que el cliente envía en el `PUT`. Vacío cuando `method` es `POST`.",
9367
9645
  "type": "object",
9368
9646
  "x-struct": null,
9369
9647
  "x-validate": null
9370
9648
  },
9371
9649
  "method": {
9650
+ "description": "Cómo subir: `POST` (form multipart, POST policy) o `PUT` (blob directo).",
9372
9651
  "enum": [
9652
+ "POST",
9373
9653
  "PUT"
9374
9654
  ],
9375
9655
  "type": "string",
9376
9656
  "x-struct": null,
9377
9657
  "x-validate": null
9378
9658
  },
9379
- "uploadUrl": {
9380
- "description": "Destino del PUT del blob.",
9659
+ "url": {
9660
+ "description": "Destino de la subida.",
9381
9661
  "type": "string",
9382
9662
  "x-struct": null,
9383
9663
  "x-validate": null
9384
9664
  }
9385
9665
  },
9386
9666
  "required": [
9387
- "uploadUrl",
9388
9667
  "method",
9668
+ "url",
9389
9669
  "fileUrl"
9390
9670
  ],
9391
9671
  "title": "PresignResult",
@@ -9393,6 +9673,49 @@
9393
9673
  "x-struct": "Elixir.HablalaApiWeb.Schemas.PresignResult",
9394
9674
  "x-validate": null
9395
9675
  },
9676
+ "PublicOrganization": {
9677
+ "description": "Identidad PÚBLICA del tenant (la org del token) — el paralelo horizontal del `shop`/`brand` de Shopify. Deja al cliente pintar su chrome (nombre, logo) y conocer su `endUserObjectSlug` sin hardcodear la marca. NUNCA incluye la PII legal del negocio.",
9678
+ "properties": {
9679
+ "avatarUrl": {
9680
+ "description": "Logo/icono de la marca (el \"server icon\" del tenant), o `null`.",
9681
+ "nullable": true,
9682
+ "type": "string",
9683
+ "x-struct": null,
9684
+ "x-validate": null
9685
+ },
9686
+ "id": {
9687
+ "description": "ID público de la organización.",
9688
+ "type": "string",
9689
+ "x-struct": null,
9690
+ "x-validate": null
9691
+ },
9692
+ "name": {
9693
+ "description": "Nombre visible de la marca.",
9694
+ "type": "string",
9695
+ "x-struct": null,
9696
+ "x-validate": null
9697
+ },
9698
+ "settings": {
9699
+ "$ref": "#/components/schemas/ManifestSettings"
9700
+ },
9701
+ "slug": {
9702
+ "description": "Slug estable de la organización.",
9703
+ "type": "string",
9704
+ "x-struct": null,
9705
+ "x-validate": null
9706
+ }
9707
+ },
9708
+ "required": [
9709
+ "id",
9710
+ "name",
9711
+ "slug",
9712
+ "settings"
9713
+ ],
9714
+ "title": "PublicOrganization",
9715
+ "type": "object",
9716
+ "x-struct": "Elixir.HablalaApiWeb.Schemas.PublicOrganization",
9717
+ "x-validate": null
9718
+ },
9396
9719
  "PublicSchema": {
9397
9720
  "properties": {
9398
9721
  "objects": {
@@ -9403,6 +9726,9 @@
9403
9726
  "x-struct": null,
9404
9727
  "x-validate": null
9405
9728
  },
9729
+ "organization": {
9730
+ "$ref": "#/components/schemas/PublicOrganization"
9731
+ },
9406
9732
  "relationships": {
9407
9733
  "items": {
9408
9734
  "$ref": "#/components/schemas/PublicSchemaRelationship"
@@ -9413,6 +9739,7 @@
9413
9739
  }
9414
9740
  },
9415
9741
  "required": [
9742
+ "organization",
9416
9743
  "objects",
9417
9744
  "relationships"
9418
9745
  ],
@@ -9948,10 +10275,55 @@
9948
10275
  "x-validate": null
9949
10276
  },
9950
10277
  "properties": {
9951
- "type": "object",
9952
- "x-struct": null,
9953
- "x-validate": null
9954
- },
10278
+ "additionalProperties": {
10279
+ "oneOf": [
10280
+ {
10281
+ "items": {
10282
+ "$ref": "#/components/schemas/FileEnvelope"
10283
+ },
10284
+ "type": "array",
10285
+ "x-struct": null,
10286
+ "x-validate": null
10287
+ },
10288
+ {
10289
+ "type": "string",
10290
+ "x-struct": null,
10291
+ "x-validate": null
10292
+ },
10293
+ {
10294
+ "type": "number",
10295
+ "x-struct": null,
10296
+ "x-validate": null
10297
+ },
10298
+ {
10299
+ "type": "boolean",
10300
+ "x-struct": null,
10301
+ "x-validate": null
10302
+ },
10303
+ {
10304
+ "items": {
10305
+ "type": "string",
10306
+ "x-struct": null,
10307
+ "x-validate": null
10308
+ },
10309
+ "type": "array",
10310
+ "x-struct": null,
10311
+ "x-validate": null
10312
+ },
10313
+ {
10314
+ "type": "object",
10315
+ "x-struct": null,
10316
+ "x-validate": null
10317
+ }
10318
+ ],
10319
+ "x-struct": null,
10320
+ "x-validate": null
10321
+ },
10322
+ "description": "Mapa slug→valor. El valor de un atributo `data_type: file` es un array de `FileEnvelope`; el resto de tipos son escalares/arrays/objetos según el `data_type` del atributo.",
10323
+ "type": "object",
10324
+ "x-struct": null,
10325
+ "x-validate": null
10326
+ },
9955
10327
  "referenceLabels": {
9956
10328
  "additionalProperties": {
9957
10329
  "properties": {
@@ -9995,10 +10367,76 @@
9995
10367
  "x-struct": "Elixir.HablalaApiWeb.Schemas.Record",
9996
10368
  "x-validate": null
9997
10369
  },
10370
+ "RecordDependencies": {
10371
+ "description": "Preview del impacto de borrar un record, SIN borrarlo (confirmación dependent-aware, patrón Salesforce/Notion). `edgeCount` = relaciones del grafo que se desvincularían (el vecino sobrevive). `blockers` = referencias core activas que IMPEDIRÍAN el borrado (restrict-on-delete); `deletable: false` si hay alguna.",
10372
+ "properties": {
10373
+ "data": {
10374
+ "properties": {
10375
+ "blockers": {
10376
+ "description": "Referencias core activas que bloquean; vacía si se puede borrar.",
10377
+ "items": {
10378
+ "properties": {
10379
+ "count": {
10380
+ "minimum": 1,
10381
+ "type": "integer",
10382
+ "x-struct": null,
10383
+ "x-validate": null
10384
+ },
10385
+ "label": {
10386
+ "description": "Etiqueta legible (p. ej. \"facturas sin cerrar\").",
10387
+ "type": "string",
10388
+ "x-struct": null,
10389
+ "x-validate": null
10390
+ }
10391
+ },
10392
+ "required": [
10393
+ "count",
10394
+ "label"
10395
+ ],
10396
+ "type": "object",
10397
+ "x-struct": null,
10398
+ "x-validate": null
10399
+ },
10400
+ "type": "array",
10401
+ "x-struct": null,
10402
+ "x-validate": null
10403
+ },
10404
+ "deletable": {
10405
+ "description": "`false` si algún blocker impediría el borrado.",
10406
+ "type": "boolean",
10407
+ "x-struct": null,
10408
+ "x-validate": null
10409
+ },
10410
+ "edgeCount": {
10411
+ "description": "Aristas del grafo (ambos extremos) que se romperían al borrar.",
10412
+ "minimum": 0,
10413
+ "type": "integer",
10414
+ "x-struct": null,
10415
+ "x-validate": null
10416
+ }
10417
+ },
10418
+ "required": [
10419
+ "edgeCount",
10420
+ "deletable",
10421
+ "blockers"
10422
+ ],
10423
+ "type": "object",
10424
+ "x-struct": null,
10425
+ "x-validate": null
10426
+ }
10427
+ },
10428
+ "required": [
10429
+ "data"
10430
+ ],
10431
+ "title": "RecordDependencies",
10432
+ "type": "object",
10433
+ "x-struct": "Elixir.HablalaApiWeb.Schemas.RecordDependencies",
10434
+ "x-validate": null
10435
+ },
9998
10436
  "RecordInput": {
9999
10437
  "properties": {
10000
10438
  "properties": {
10001
- "description": "Mapa slug→valor validado contra el diccionario. Un atributo `reference` acepta el id público del record destino (`\"rec_…\"`) o, como azúcar de DX, una **referencia por clave de negocio**: `{\"$ref\": {\"<attr>\": <valor>}}`, donde `<attr>` es un atributo único del objeto destino (p. ej. `{\"$ref\": {\"sku\": \"DRM-VITC-30\"}}`). Se resuelve al `rec_…` en el servidor — útil para sincronizar por external id sin conocer el id de Hablalá (patrón Salesforce/Airtable).",
10439
+ "description": "Mapa slug→valor validado contra el diccionario. Un atributo `reference` acepta el id público del record destino (`\"rec_…\"`) o, como azúcar de DX, una **referencia por clave de negocio**: `{\"$ref\": {\"<attr>\": <valor>}}`, donde `<attr>` es un atributo único del objeto destino (p. ej. `{\"$ref\": {\"sku\": \"DRM-VITC-30\"}}`). Se resuelve al `rec_…` en el servidor — útil para sincronizar por external id sin conocer el id de Hablalá (patrón Salesforce/Airtable). Un atributo `data_type: file` toma un array de `FileEnvelope`: la `url` de cada archivo debe venir del flujo de upload prefirmado (una URL externa se rechaza).",
10002
10440
  "type": "object",
10003
10441
  "x-struct": null,
10004
10442
  "x-validate": null
@@ -10040,6 +10478,96 @@
10040
10478
  "x-struct": "Elixir.HablalaApiWeb.Schemas.RecordResponse",
10041
10479
  "x-validate": null
10042
10480
  },
10481
+ "RecordUploadRequest": {
10482
+ "description": "Archivo a adjuntar a un attribute `file` de un record: para qué attribute es y qué se va a subir. El servidor lo valida contra el config del attribute (`accepted_content_types` con comodines `image/*`, `max_size_bytes`) antes de emitir la URL de subida.",
10483
+ "properties": {
10484
+ "attribute": {
10485
+ "description": "Slug del attribute `data_type: file` del object al que se adjuntará.",
10486
+ "type": "string",
10487
+ "x-struct": null,
10488
+ "x-validate": null
10489
+ },
10490
+ "contentType": {
10491
+ "description": "MIME type del archivo (p. ej. `image/jpeg`). Se valida contra `accepted_content_types` del attribute.",
10492
+ "type": "string",
10493
+ "x-struct": null,
10494
+ "x-validate": null
10495
+ },
10496
+ "filename": {
10497
+ "description": "Nombre del archivo (p. ej. `firulais.jpg`).",
10498
+ "type": "string",
10499
+ "x-struct": null,
10500
+ "x-validate": null
10501
+ },
10502
+ "size": {
10503
+ "description": "Tamaño declarado en bytes (opcional). Si excede `max_size_bytes` el presign se rechaza aquí (fail-fast); la verdad final la impone el storage al confirmar el record (el tamaño REAL se relee del blob).",
10504
+ "minimum": 1,
10505
+ "nullable": true,
10506
+ "type": "integer",
10507
+ "x-struct": null,
10508
+ "x-validate": null
10509
+ }
10510
+ },
10511
+ "required": [
10512
+ "attribute",
10513
+ "filename",
10514
+ "contentType"
10515
+ ],
10516
+ "title": "RecordUploadRequest",
10517
+ "type": "object",
10518
+ "x-struct": "Elixir.HablalaApiWeb.Schemas.RecordUploadRequest",
10519
+ "x-validate": null
10520
+ },
10521
+ "RecordUploadResponse": {
10522
+ "properties": {
10523
+ "data": {
10524
+ "$ref": "#/components/schemas/RecordUploadTarget"
10525
+ }
10526
+ },
10527
+ "required": [
10528
+ "data"
10529
+ ],
10530
+ "title": "RecordUploadResponse",
10531
+ "type": "object",
10532
+ "x-struct": "Elixir.HablalaApiWeb.Schemas.RecordUploadResponse",
10533
+ "x-validate": null
10534
+ },
10535
+ "RecordUploadTarget": {
10536
+ "description": "Destino de subida prefirmado (*staged upload target*, patrón Shopify `stagedUploadsCreate` / S3 presigned upload): el cliente hace `PUT` del blob crudo a `uploadUrl` con `headers`, y luego CONFIRMA escribiendo el record con `fileUrl` en el envelope del attribute `file` (`{\"url\": fileUrl, \"filename\": …, \"content_type\": …}`). Un blob nunca confirmado expira solo en el staging.",
10537
+ "properties": {
10538
+ "fileUrl": {
10539
+ "description": "URL del blob (en staging `pending/…`): va como `url` en el envelope del attribute `file` al crear/actualizar el record. Al confirmarse, el servidor la reescribe a la definitiva.",
10540
+ "type": "string",
10541
+ "x-struct": null,
10542
+ "x-validate": null
10543
+ },
10544
+ "headers": {
10545
+ "additionalProperties": {
10546
+ "type": "string",
10547
+ "x-struct": null,
10548
+ "x-validate": null
10549
+ },
10550
+ "description": "Cabeceras que el cliente debe enviar en el `PUT` (p. ej. `content-type`).",
10551
+ "type": "object",
10552
+ "x-struct": null,
10553
+ "x-validate": null
10554
+ },
10555
+ "uploadUrl": {
10556
+ "description": "Destino del `PUT` del blob (URL prefirmada, expira).",
10557
+ "type": "string",
10558
+ "x-struct": null,
10559
+ "x-validate": null
10560
+ }
10561
+ },
10562
+ "required": [
10563
+ "uploadUrl",
10564
+ "fileUrl"
10565
+ ],
10566
+ "title": "RecordUploadTarget",
10567
+ "type": "object",
10568
+ "x-struct": "Elixir.HablalaApiWeb.Schemas.RecordUploadTarget",
10569
+ "x-validate": null
10570
+ },
10043
10571
  "RegisterCredentials": {
10044
10572
  "properties": {
10045
10573
  "email": {
@@ -11482,6 +12010,29 @@
11482
12010
  "x-struct": "Elixir.HablalaApiWeb.Schemas.UsernameAvailableResponse",
11483
12011
  "x-validate": null
11484
12012
  },
12013
+ "VariantAvailabilityRequest": {
12014
+ "description": "Los ids públicos (`rec_…`) de las variantes cuya disponibilidad de compra se consulta (una llamada resuelve toda una página de catálogo).",
12015
+ "properties": {
12016
+ "variantIds": {
12017
+ "items": {
12018
+ "description": "Id público (`rec_…`) de un `product_variant`.",
12019
+ "type": "string",
12020
+ "x-struct": null,
12021
+ "x-validate": null
12022
+ },
12023
+ "type": "array",
12024
+ "x-struct": null,
12025
+ "x-validate": null
12026
+ }
12027
+ },
12028
+ "required": [
12029
+ "variantIds"
12030
+ ],
12031
+ "title": "VariantAvailabilityRequest",
12032
+ "type": "object",
12033
+ "x-struct": "Elixir.HablalaApiWeb.Schemas.VariantAvailabilityRequest",
12034
+ "x-validate": null
12035
+ },
11485
12036
  "VerificationRejectRequest": {
11486
12037
  "properties": {
11487
12038
  "reason": {
@@ -14935,7 +15486,7 @@
14935
15486
  "/v1/me/uploads/presign": {
14936
15487
  "post": {
14937
15488
  "callbacks": {},
14938
- "description": "Como el presign de organización, pero el blob se namespacea por usuario. El cliente hace `PUT` a `uploadUrl` y referencia `fileUrl` en los adjuntos del mensaje.",
15489
+ "description": "Como el presign de organización, pero el blob se namespacea por usuario. Devuelve un sobre auto-descriptivo (`method`/`url`/`fields`/`headers`); el cliente sube según `method` y referencia `fileUrl` en los adjuntos del mensaje.",
14939
15490
  "operationId": "HablalaApiWeb.Me.UploadController.presign",
14940
15491
  "parameters": [],
14941
15492
  "requestBody": {
@@ -14958,7 +15509,7 @@
14958
15509
  }
14959
15510
  }
14960
15511
  },
14961
- "description": "URL prefirmada"
15512
+ "description": "Sobre de subida prefirmado"
14962
15513
  },
14963
15514
  "415": {
14964
15515
  "content": {
@@ -14971,7 +15522,7 @@
14971
15522
  "description": "Tipo de archivo no soportado"
14972
15523
  }
14973
15524
  },
14974
- "summary": "Pedir una URL de subida prefirmada (adjunto personal)",
15525
+ "summary": "Pedir un sobre de subida prefirmado (adjunto personal)",
14975
15526
  "tags": [
14976
15527
  "personal uploads"
14977
15528
  ]
@@ -15172,7 +15723,7 @@
15172
15723
  "/v1/oauth/token": {
15173
15724
  "post": {
15174
15725
  "callbacks": {},
15175
- "description": "`grant_type=authorization_code` (loopback + PKCE) o `grant_type=urn:ietf:params:oauth:grant-type:device_code` (polling del device flow). El `access_token` devuelto es un `hpat_…` normal; guárdalo, no se puede recuperar.",
15726
+ "description": "`grant_type=authorization_code` (loopback + PKCE) o `grant_type=urn:ietf:params:oauth:grant-type:device_code` (polling del device flow). El `access_token` devuelto es un `mat_…` normal; guárdalo, no se puede recuperar.",
15176
15727
  "operationId": "HablalaApiWeb.Oauth.TokenController.create",
15177
15728
  "parameters": [],
15178
15729
  "requestBody": {
@@ -15756,6 +16307,84 @@
15756
16307
  ]
15757
16308
  }
15758
16309
  },
16310
+ "/v1/objects/{object_slug}/records/trash": {
16311
+ "get": {
16312
+ "callbacks": {},
16313
+ "description": "Lista los records SOFT-DELETED del objeto (`deleted_at IS NOT NULL`), para restaurarlos. Misma lectura gobernada que `POST /query` (RBAC + ABAC por fila + field_mask + paginación keyset), sobre el otro lado de `deleted_at`. Gate RBAC: `records:delete`.",
16314
+ "operationId": "HablalaApiWeb.Machine.RecordController.trash",
16315
+ "parameters": [
16316
+ {
16317
+ "description": "",
16318
+ "in": "path",
16319
+ "name": "object_slug",
16320
+ "required": true,
16321
+ "schema": {
16322
+ "type": "string",
16323
+ "x-struct": null,
16324
+ "x-validate": null
16325
+ }
16326
+ },
16327
+ {
16328
+ "description": "",
16329
+ "in": "query",
16330
+ "name": "limit",
16331
+ "required": false,
16332
+ "schema": {
16333
+ "type": "integer",
16334
+ "x-struct": null,
16335
+ "x-validate": null
16336
+ }
16337
+ },
16338
+ {
16339
+ "description": "",
16340
+ "in": "query",
16341
+ "name": "cursor",
16342
+ "required": false,
16343
+ "schema": {
16344
+ "type": "string",
16345
+ "x-struct": null,
16346
+ "x-validate": null
16347
+ }
16348
+ }
16349
+ ],
16350
+ "responses": {
16351
+ "200": {
16352
+ "content": {
16353
+ "application/json": {
16354
+ "schema": {
16355
+ "$ref": "#/components/schemas/QueryResultResponse"
16356
+ }
16357
+ }
16358
+ },
16359
+ "description": "Records borrados"
16360
+ },
16361
+ "403": {
16362
+ "content": {
16363
+ "application/json": {
16364
+ "schema": {
16365
+ "$ref": "#/components/schemas/Error"
16366
+ }
16367
+ }
16368
+ },
16369
+ "description": "Sin permiso"
16370
+ },
16371
+ "404": {
16372
+ "content": {
16373
+ "application/json": {
16374
+ "schema": {
16375
+ "$ref": "#/components/schemas/Error"
16376
+ }
16377
+ }
16378
+ },
16379
+ "description": "Objeto desconocido"
16380
+ }
16381
+ },
16382
+ "summary": "Listar los records borrados (soft) del objeto — la Papelera",
16383
+ "tags": [
16384
+ "records"
16385
+ ]
16386
+ }
16387
+ },
15759
16388
  "/v1/objects/{object_slug}/records/{id}": {
15760
16389
  "delete": {
15761
16390
  "callbacks": {},
@@ -16049,10 +16678,11 @@
16049
16678
  ]
16050
16679
  }
16051
16680
  },
16052
- "/v1/objects/{object_slug}/records/{record_id}/events": {
16681
+ "/v1/objects/{object_slug}/records/{record_id}/dependencies": {
16053
16682
  "get": {
16054
16683
  "callbacks": {},
16055
- "operationId": "HablalaApiWeb.Machine.EventController.index",
16684
+ "description": "Devuelve cuántos edges se desvincularían y qué referencias core BLOQUEARÍAN el borrado (restrict-on-delete), sin mutar nada. Alimenta la confirmación dependent-aware de la UI. Misma fuente que el guard del borrado real, así que preview y borrado no divergen. Gate `records:delete` + ABAC de escritura por fila.",
16685
+ "operationId": "HablalaApiWeb.Machine.RecordController.dependencies",
16056
16686
  "parameters": [
16057
16687
  {
16058
16688
  "description": "",
@@ -16082,20 +16712,86 @@
16082
16712
  "content": {
16083
16713
  "application/json": {
16084
16714
  "schema": {
16085
- "$ref": "#/components/schemas/EventsResponse"
16715
+ "$ref": "#/components/schemas/RecordDependencies"
16086
16716
  }
16087
16717
  }
16088
16718
  },
16089
- "description": "Eventos"
16090
- }
16091
- },
16092
- "summary": "Historial (stream) de un record",
16093
- "tags": [
16094
- "events"
16095
- ]
16096
- }
16097
- },
16098
- "/v1/objects/{object_slug}/records/{record_id}/restore": {
16719
+ "description": "Dependencias"
16720
+ },
16721
+ "403": {
16722
+ "content": {
16723
+ "application/json": {
16724
+ "schema": {
16725
+ "$ref": "#/components/schemas/Error"
16726
+ }
16727
+ }
16728
+ },
16729
+ "description": "Sin permiso ABAC sobre este record"
16730
+ },
16731
+ "404": {
16732
+ "content": {
16733
+ "application/json": {
16734
+ "schema": {
16735
+ "$ref": "#/components/schemas/Error"
16736
+ }
16737
+ }
16738
+ },
16739
+ "description": "No encontrado (o de otro objeto)"
16740
+ }
16741
+ },
16742
+ "summary": "Preview de dependencias de un record (impacto de borrarlo, sin borrar)",
16743
+ "tags": [
16744
+ "records"
16745
+ ]
16746
+ }
16747
+ },
16748
+ "/v1/objects/{object_slug}/records/{record_id}/events": {
16749
+ "get": {
16750
+ "callbacks": {},
16751
+ "operationId": "HablalaApiWeb.Machine.EventController.index",
16752
+ "parameters": [
16753
+ {
16754
+ "description": "",
16755
+ "in": "path",
16756
+ "name": "object_slug",
16757
+ "required": true,
16758
+ "schema": {
16759
+ "type": "string",
16760
+ "x-struct": null,
16761
+ "x-validate": null
16762
+ }
16763
+ },
16764
+ {
16765
+ "description": "",
16766
+ "in": "path",
16767
+ "name": "record_id",
16768
+ "required": true,
16769
+ "schema": {
16770
+ "type": "string",
16771
+ "x-struct": null,
16772
+ "x-validate": null
16773
+ }
16774
+ }
16775
+ ],
16776
+ "responses": {
16777
+ "200": {
16778
+ "content": {
16779
+ "application/json": {
16780
+ "schema": {
16781
+ "$ref": "#/components/schemas/EventsResponse"
16782
+ }
16783
+ }
16784
+ },
16785
+ "description": "Eventos"
16786
+ }
16787
+ },
16788
+ "summary": "Historial (stream) de un record",
16789
+ "tags": [
16790
+ "events"
16791
+ ]
16792
+ }
16793
+ },
16794
+ "/v1/objects/{object_slug}/records/{record_id}/restore": {
16099
16795
  "post": {
16100
16796
  "callbacks": {},
16101
16797
  "operationId": "HablalaApiWeb.Machine.RecordController.restore",
@@ -16171,6 +16867,73 @@
16171
16867
  ]
16172
16868
  }
16173
16869
  },
16870
+ "/v1/objects/{object_slug}/uploads": {
16871
+ "post": {
16872
+ "callbacks": {},
16873
+ "description": "Fase 1 del flujo de archivos de un record: valida el archivo declarado contra el config del attribute `file` (`accepted_content_types` con comodines `image/*`, `max_size_bytes`) y devuelve el destino de subida prefirmado. Luego: (2) `PUT` del blob crudo a `uploadUrl` con `headers`; (3) confirmar escribiendo el record con `fileUrl` en el envelope del attribute (`{\"url\": fileUrl, …}`) — el servidor verifica el blob contra el storage (tamaño/tipo REALES) y lo promueve a su ubicación definitiva. Un blob nunca confirmado expira solo. Gate `records:create`.",
16874
+ "operationId": "HablalaApiWeb.Machine.UploadController.create",
16875
+ "parameters": [
16876
+ {
16877
+ "description": "",
16878
+ "in": "path",
16879
+ "name": "object_slug",
16880
+ "required": true,
16881
+ "schema": {
16882
+ "type": "string",
16883
+ "x-struct": null,
16884
+ "x-validate": null
16885
+ }
16886
+ }
16887
+ ],
16888
+ "requestBody": {
16889
+ "content": {
16890
+ "application/json": {
16891
+ "schema": {
16892
+ "$ref": "#/components/schemas/RecordUploadRequest"
16893
+ }
16894
+ }
16895
+ },
16896
+ "description": "Archivo a subir",
16897
+ "required": false
16898
+ },
16899
+ "responses": {
16900
+ "200": {
16901
+ "content": {
16902
+ "application/json": {
16903
+ "schema": {
16904
+ "$ref": "#/components/schemas/RecordUploadResponse"
16905
+ }
16906
+ }
16907
+ },
16908
+ "description": "Destino de subida prefirmado"
16909
+ },
16910
+ "404": {
16911
+ "content": {
16912
+ "application/json": {
16913
+ "schema": {
16914
+ "$ref": "#/components/schemas/Error"
16915
+ }
16916
+ }
16917
+ },
16918
+ "description": "Objeto desconocido"
16919
+ },
16920
+ "422": {
16921
+ "content": {
16922
+ "application/json": {
16923
+ "schema": {
16924
+ "$ref": "#/components/schemas/Error"
16925
+ }
16926
+ }
16927
+ },
16928
+ "description": "Attribute inexistente/no-file, MIME no admitido o tamaño excedido"
16929
+ }
16930
+ },
16931
+ "summary": "Pedir una URL de subida para un attribute file (staged upload)",
16932
+ "tags": [
16933
+ "records"
16934
+ ]
16935
+ }
16936
+ },
16174
16937
  "/v1/organizations": {
16175
16938
  "get": {
16176
16939
  "callbacks": {},
@@ -16642,7 +17405,7 @@
16642
17405
  },
16643
17406
  "post": {
16644
17407
  "callbacks": {},
16645
- "description": "Emite una credencial de máquina (`hpat_…`) con la que un agente (Claude Code) o un script CI escribe el modelo de datos por HTTP. La respuesta incluye el secreto en claro UNA sola vez (`token`); guárdalo, no se puede recuperar. Los permisos se intersectan con los del creador (un token nunca puede más que quien lo emite).",
17408
+ "description": "Emite una credencial de máquina (`mat_…`) con la que un agente (Claude Code) o un script CI escribe el modelo de datos por HTTP. La respuesta incluye el secreto en claro UNA sola vez (`token`); guárdalo, no se puede recuperar. Los permisos se intersectan con los del creador (un token nunca puede más que quien lo emite).",
16646
17409
  "operationId": "HablalaApiWeb.AccessTokens.TokenController.create",
16647
17410
  "parameters": [
16648
17411
  {
@@ -17176,7 +17939,7 @@
17176
17939
  },
17177
17940
  "post": {
17178
17941
  "callbacks": {},
17179
- "description": "Emite una credencial read-only para la Data API. La respuesta incluye el secreto en claro UNA sola vez (`token`); guárdalo, no se puede recuperar. `kind`: public (client-side) o private (server-side).",
17942
+ "description": "Emite el **par** de API keys del canal público en una operación: una **publishable** (`pk_`, client-side) y una **secret** (`sk_`, server-side). La respuesta incluye el secreto en claro de AMBAS una sola vez (`publishable.token`/`secret.token`); guárdalos, no se pueden recuperar. Es el flujo del panel: se muestran completos para copiar/pegar en el `.env` de la app cliente. `name` por defecto \"Default\".",
17180
17943
  "operationId": "HablalaApiWeb.PublicApi.ApiKeyController.create",
17181
17944
  "parameters": [
17182
17945
  {
@@ -17199,7 +17962,7 @@
17199
17962
  }
17200
17963
  }
17201
17964
  },
17202
- "description": "Datos del token",
17965
+ "description": "Datos del par",
17203
17966
  "required": false
17204
17967
  },
17205
17968
  "responses": {
@@ -17207,11 +17970,11 @@
17207
17970
  "content": {
17208
17971
  "application/json": {
17209
17972
  "schema": {
17210
- "$ref": "#/components/schemas/ApiKeyCreatedResponse"
17973
+ "$ref": "#/components/schemas/ApiKeyPairCreatedResponse"
17211
17974
  }
17212
17975
  }
17213
17976
  },
17214
- "description": "Token creado (con secreto)"
17977
+ "description": "Par creado (con secretos)"
17215
17978
  },
17216
17979
  "403": {
17217
17980
  "content": {
@@ -17234,7 +17997,7 @@
17234
17997
  "description": "Error de validación"
17235
17998
  }
17236
17999
  },
17237
- "summary": "Crear una API key",
18000
+ "summary": "Crear un par de API keys",
17238
18001
  "tags": [
17239
18002
  "public"
17240
18003
  ]
@@ -17290,6 +18053,63 @@
17290
18053
  ]
17291
18054
  }
17292
18055
  },
18056
+ "/v1/organizations/{organization_id}/api-keys/{id}/rotate": {
18057
+ "post": {
18058
+ "callbacks": {},
18059
+ "description": "Rota la key (modelo Stripe *roll*): emite un reemplazo del mismo tipo y nombre y revoca la original de inmediato. La respuesta incluye el nuevo secreto en claro UNA sola vez (`token`); guárdalo. Las requests que usaban la key vieja dejan de autenticar.",
18060
+ "operationId": "HablalaApiWeb.PublicApi.ApiKeyController.rotate",
18061
+ "parameters": [
18062
+ {
18063
+ "description": "",
18064
+ "in": "path",
18065
+ "name": "organization_id",
18066
+ "required": true,
18067
+ "schema": {
18068
+ "type": "string",
18069
+ "x-struct": null,
18070
+ "x-validate": null
18071
+ }
18072
+ },
18073
+ {
18074
+ "description": "",
18075
+ "in": "path",
18076
+ "name": "id",
18077
+ "required": true,
18078
+ "schema": {
18079
+ "type": "string",
18080
+ "x-struct": null,
18081
+ "x-validate": null
18082
+ }
18083
+ }
18084
+ ],
18085
+ "responses": {
18086
+ "201": {
18087
+ "content": {
18088
+ "application/json": {
18089
+ "schema": {
18090
+ "$ref": "#/components/schemas/ApiKeyCreatedResponse"
18091
+ }
18092
+ }
18093
+ },
18094
+ "description": "Key rotada (nuevo secreto)"
18095
+ },
18096
+ "403": {
18097
+ "content": {
18098
+ "application/json": {
18099
+ "schema": {
18100
+ "$ref": "#/components/schemas/Error"
18101
+ }
18102
+ }
18103
+ },
18104
+ "description": "Sin permiso"
18105
+ }
18106
+ },
18107
+ "summary": "Rotar una API key",
18108
+ "tags": [
18109
+ "public"
18110
+ ]
18111
+ }
18112
+ },
17293
18113
  "/v1/organizations/{organization_id}/appointments": {
17294
18114
  "get": {
17295
18115
  "callbacks": {},
@@ -22189,7 +23009,7 @@
22189
23009
  "/v1/organizations/{organization_id}/oauth/grants/{user_code}/approve": {
22190
23010
  "post": {
22191
23011
  "callbacks": {},
22192
- "description": "El consent: fija la organización del path y los permisos elegidos (intersectados con los del aprobador) en el grant. El CLI que sondea recibirá su `hpat_…` en el siguiente poll.",
23012
+ "description": "El consent: fija la organización del path y los permisos elegidos (intersectados con los del aprobador) en el grant. El CLI que sondea recibirá su `mat_…` en el siguiente poll.",
22193
23013
  "operationId": "HablalaApiWeb.Oauth.GrantController.approve",
22194
23014
  "parameters": [
22195
23015
  {
@@ -22929,10 +23749,11 @@
22929
23749
  ]
22930
23750
  }
22931
23751
  },
22932
- "/v1/organizations/{organization_id}/objects/{object_slug}/records/{id}": {
22933
- "delete": {
23752
+ "/v1/organizations/{organization_id}/objects/{object_slug}/records/trash": {
23753
+ "get": {
22934
23754
  "callbacks": {},
22935
- "operationId": "HablalaApiWeb.Data.RecordController.delete",
23755
+ "description": "Lista los records SOFT-DELETED del objeto (`deleted_at IS NOT NULL`), para restaurarlos. Misma lectura gobernada que `POST /query` (RBAC + ABAC por fila + field_mask + paginación keyset), sobre el otro lado de `deleted_at`. Gate RBAC: `records:delete`.",
23756
+ "operationId": "HablalaApiWeb.Data.RecordController.trash",
22936
23757
  "parameters": [
22937
23758
  {
22938
23759
  "description": "",
@@ -22958,9 +23779,20 @@
22958
23779
  },
22959
23780
  {
22960
23781
  "description": "",
22961
- "in": "path",
22962
- "name": "id",
22963
- "required": true,
23782
+ "in": "query",
23783
+ "name": "limit",
23784
+ "required": false,
23785
+ "schema": {
23786
+ "type": "integer",
23787
+ "x-struct": null,
23788
+ "x-validate": null
23789
+ }
23790
+ },
23791
+ {
23792
+ "description": "",
23793
+ "in": "query",
23794
+ "name": "cursor",
23795
+ "required": false,
22964
23796
  "schema": {
22965
23797
  "type": "string",
22966
23798
  "x-struct": null,
@@ -22969,8 +23801,15 @@
22969
23801
  }
22970
23802
  ],
22971
23803
  "responses": {
22972
- "204": {
22973
- "description": "Borrado"
23804
+ "200": {
23805
+ "content": {
23806
+ "application/json": {
23807
+ "schema": {
23808
+ "$ref": "#/components/schemas/QueryResultResponse"
23809
+ }
23810
+ }
23811
+ },
23812
+ "description": "Records borrados"
22974
23813
  },
22975
23814
  "403": {
22976
23815
  "content": {
@@ -22980,7 +23819,7 @@
22980
23819
  }
22981
23820
  }
22982
23821
  },
22983
- "description": "Sin permiso ABAC sobre este record"
23822
+ "description": "Sin permiso"
22984
23823
  },
22985
23824
  "404": {
22986
23825
  "content": {
@@ -22990,17 +23829,19 @@
22990
23829
  }
22991
23830
  }
22992
23831
  },
22993
- "description": "No encontrado (o de otro objeto)"
23832
+ "description": "Objeto desconocido"
22994
23833
  }
22995
23834
  },
22996
- "summary": "Borrar (soft) un record",
23835
+ "summary": "Listar los records borrados (soft) del objeto — la Papelera",
22997
23836
  "tags": [
22998
23837
  "records"
22999
23838
  ]
23000
- },
23001
- "get": {
23839
+ }
23840
+ },
23841
+ "/v1/organizations/{organization_id}/objects/{object_slug}/records/{id}": {
23842
+ "delete": {
23002
23843
  "callbacks": {},
23003
- "operationId": "HablalaApiWeb.Data.RecordController.show",
23844
+ "operationId": "HablalaApiWeb.Data.RecordController.delete",
23004
23845
  "parameters": [
23005
23846
  {
23006
23847
  "description": "",
@@ -23037,15 +23878,8 @@
23037
23878
  }
23038
23879
  ],
23039
23880
  "responses": {
23040
- "200": {
23041
- "content": {
23042
- "application/json": {
23043
- "schema": {
23044
- "$ref": "#/components/schemas/RecordResponse"
23045
- }
23046
- }
23047
- },
23048
- "description": "Record"
23881
+ "204": {
23882
+ "description": "Borrado"
23049
23883
  },
23050
23884
  "403": {
23051
23885
  "content": {
@@ -23065,17 +23899,17 @@
23065
23899
  }
23066
23900
  }
23067
23901
  },
23068
- "description": "No encontrado"
23902
+ "description": "No encontrado (o de otro objeto)"
23069
23903
  }
23070
23904
  },
23071
- "summary": "Ver un record",
23905
+ "summary": "Borrar (soft) un record",
23072
23906
  "tags": [
23073
23907
  "records"
23074
23908
  ]
23075
23909
  },
23076
- "patch": {
23910
+ "get": {
23077
23911
  "callbacks": {},
23078
- "operationId": "HablalaApiWeb.Data.RecordController.update (2)",
23912
+ "operationId": "HablalaApiWeb.Data.RecordController.show",
23079
23913
  "parameters": [
23080
23914
  {
23081
23915
  "description": "",
@@ -23111,12 +23945,87 @@
23111
23945
  }
23112
23946
  }
23113
23947
  ],
23114
- "requestBody": {
23115
- "content": {
23116
- "application/json": {
23117
- "schema": {
23118
- "$ref": "#/components/schemas/RecordRequest"
23119
- }
23948
+ "responses": {
23949
+ "200": {
23950
+ "content": {
23951
+ "application/json": {
23952
+ "schema": {
23953
+ "$ref": "#/components/schemas/RecordResponse"
23954
+ }
23955
+ }
23956
+ },
23957
+ "description": "Record"
23958
+ },
23959
+ "403": {
23960
+ "content": {
23961
+ "application/json": {
23962
+ "schema": {
23963
+ "$ref": "#/components/schemas/Error"
23964
+ }
23965
+ }
23966
+ },
23967
+ "description": "Sin permiso ABAC sobre este record"
23968
+ },
23969
+ "404": {
23970
+ "content": {
23971
+ "application/json": {
23972
+ "schema": {
23973
+ "$ref": "#/components/schemas/Error"
23974
+ }
23975
+ }
23976
+ },
23977
+ "description": "No encontrado"
23978
+ }
23979
+ },
23980
+ "summary": "Ver un record",
23981
+ "tags": [
23982
+ "records"
23983
+ ]
23984
+ },
23985
+ "patch": {
23986
+ "callbacks": {},
23987
+ "operationId": "HablalaApiWeb.Data.RecordController.update (2)",
23988
+ "parameters": [
23989
+ {
23990
+ "description": "",
23991
+ "in": "path",
23992
+ "name": "organization_id",
23993
+ "required": true,
23994
+ "schema": {
23995
+ "type": "string",
23996
+ "x-struct": null,
23997
+ "x-validate": null
23998
+ }
23999
+ },
24000
+ {
24001
+ "description": "",
24002
+ "in": "path",
24003
+ "name": "object_slug",
24004
+ "required": true,
24005
+ "schema": {
24006
+ "type": "string",
24007
+ "x-struct": null,
24008
+ "x-validate": null
24009
+ }
24010
+ },
24011
+ {
24012
+ "description": "",
24013
+ "in": "path",
24014
+ "name": "id",
24015
+ "required": true,
24016
+ "schema": {
24017
+ "type": "string",
24018
+ "x-struct": null,
24019
+ "x-validate": null
24020
+ }
24021
+ }
24022
+ ],
24023
+ "requestBody": {
24024
+ "content": {
24025
+ "application/json": {
24026
+ "schema": {
24027
+ "$ref": "#/components/schemas/RecordRequest"
24028
+ }
23120
24029
  }
23121
24030
  },
23122
24031
  "description": "Propiedades del record",
@@ -23266,6 +24175,84 @@
23266
24175
  ]
23267
24176
  }
23268
24177
  },
24178
+ "/v1/organizations/{organization_id}/objects/{object_slug}/records/{record_id}/dependencies": {
24179
+ "get": {
24180
+ "callbacks": {},
24181
+ "description": "Devuelve cuántos edges se desvincularían y qué referencias core BLOQUEARÍAN el borrado (restrict-on-delete), sin mutar nada. Alimenta la confirmación dependent-aware de la UI. Misma fuente que el guard del borrado real, así que preview y borrado no divergen. Gate `records:delete` + ABAC de escritura por fila.",
24182
+ "operationId": "HablalaApiWeb.Data.RecordController.dependencies",
24183
+ "parameters": [
24184
+ {
24185
+ "description": "",
24186
+ "in": "path",
24187
+ "name": "organization_id",
24188
+ "required": true,
24189
+ "schema": {
24190
+ "type": "string",
24191
+ "x-struct": null,
24192
+ "x-validate": null
24193
+ }
24194
+ },
24195
+ {
24196
+ "description": "",
24197
+ "in": "path",
24198
+ "name": "object_slug",
24199
+ "required": true,
24200
+ "schema": {
24201
+ "type": "string",
24202
+ "x-struct": null,
24203
+ "x-validate": null
24204
+ }
24205
+ },
24206
+ {
24207
+ "description": "",
24208
+ "in": "path",
24209
+ "name": "record_id",
24210
+ "required": true,
24211
+ "schema": {
24212
+ "type": "string",
24213
+ "x-struct": null,
24214
+ "x-validate": null
24215
+ }
24216
+ }
24217
+ ],
24218
+ "responses": {
24219
+ "200": {
24220
+ "content": {
24221
+ "application/json": {
24222
+ "schema": {
24223
+ "$ref": "#/components/schemas/RecordDependencies"
24224
+ }
24225
+ }
24226
+ },
24227
+ "description": "Dependencias"
24228
+ },
24229
+ "403": {
24230
+ "content": {
24231
+ "application/json": {
24232
+ "schema": {
24233
+ "$ref": "#/components/schemas/Error"
24234
+ }
24235
+ }
24236
+ },
24237
+ "description": "Sin permiso ABAC sobre este record"
24238
+ },
24239
+ "404": {
24240
+ "content": {
24241
+ "application/json": {
24242
+ "schema": {
24243
+ "$ref": "#/components/schemas/Error"
24244
+ }
24245
+ }
24246
+ },
24247
+ "description": "No encontrado (o de otro objeto)"
24248
+ }
24249
+ },
24250
+ "summary": "Preview de dependencias de un record (impacto de borrarlo, sin borrar)",
24251
+ "tags": [
24252
+ "records"
24253
+ ]
24254
+ }
24255
+ },
23269
24256
  "/v1/organizations/{organization_id}/objects/{object_slug}/records/{record_id}/events": {
23270
24257
  "get": {
23271
24258
  "callbacks": {},
@@ -25915,7 +26902,7 @@
25915
26902
  "/v1/organizations/{organization_id}/uploads/presign": {
25916
26903
  "post": {
25917
26904
  "callbacks": {},
25918
- "description": "Devuelve una URL prefirmada para subir un archivo directo al object storage. El cliente hace `PUT` del blob a `uploadUrl` y luego referencia `fileUrl` en los adjuntos del mensaje. (El camino personal usa `HablalaApiWeb.Me.UploadController` en `/v1/me/uploads/presign`.)",
26905
+ "description": "Devuelve un sobre auto-descriptivo para subir un archivo directo al object storage. Según `method`: `POST` → form multipart con `fields` + el archivo como campo `file` (S3/R2 POST policy, con tope de tamaño firmado en el borde); `PUT` → PUT del blob a `url` con `headers` (adaptador local de dev). Luego se referencia `fileUrl`. (El camino personal usa `HablalaApiWeb.Me.UploadController` en `/v1/me/uploads/presign`.)",
25919
26906
  "operationId": "HablalaApiWeb.UploadController.presign",
25920
26907
  "parameters": [
25921
26908
  {
@@ -25950,7 +26937,7 @@
25950
26937
  }
25951
26938
  }
25952
26939
  },
25953
- "description": "URL prefirmada"
26940
+ "description": "Sobre de subida prefirmado"
25954
26941
  },
25955
26942
  "415": {
25956
26943
  "content": {
@@ -25963,7 +26950,7 @@
25963
26950
  "description": "Tipo de archivo no soportado"
25964
26951
  }
25965
26952
  },
25966
- "summary": "Pedir una URL de subida prefirmada",
26953
+ "summary": "Pedir un sobre de subida prefirmado",
25967
26954
  "tags": [
25968
26955
  "uploads"
25969
26956
  ]
@@ -26949,6 +27936,98 @@
26949
27936
  ]
26950
27937
  }
26951
27938
  },
27939
+ "/v1/public/me/checkout": {
27940
+ "post": {
27941
+ "callbacks": {},
27942
+ "description": "Crea un pedido atómicamente: valida stock por variante, numera (#1001), crea el `order` (source = web, financial_status = pending) + sus renglones con snapshots de precio, y decrementa el inventario. Autenticado por end-user session token; el comprador sale de la sesión (no del cuerpo). Idempotente vía el header `Idempotency-Key`.",
27943
+ "operationId": "HablalaApiWeb.PublicApi.CheckoutController.create",
27944
+ "parameters": [
27945
+ {
27946
+ "description": "Clave de idempotencia (estándar Stripe): dos envíos con la misma → un pedido.",
27947
+ "in": "header",
27948
+ "name": "Idempotency-Key",
27949
+ "required": false,
27950
+ "schema": {
27951
+ "type": "string",
27952
+ "x-struct": null,
27953
+ "x-validate": null
27954
+ }
27955
+ }
27956
+ ],
27957
+ "requestBody": {
27958
+ "content": {
27959
+ "application/json": {
27960
+ "schema": {
27961
+ "$ref": "#/components/schemas/CheckoutRequest"
27962
+ }
27963
+ }
27964
+ },
27965
+ "description": "Artículos del carrito + moneda",
27966
+ "required": false
27967
+ },
27968
+ "responses": {
27969
+ "201": {
27970
+ "content": {
27971
+ "application/json": {
27972
+ "schema": {
27973
+ "$ref": "#/components/schemas/OrderResponse"
27974
+ }
27975
+ }
27976
+ },
27977
+ "description": "Pedido creado"
27978
+ },
27979
+ "401": {
27980
+ "content": {
27981
+ "application/json": {
27982
+ "schema": {
27983
+ "$ref": "#/components/schemas/Error"
27984
+ }
27985
+ }
27986
+ },
27987
+ "description": "Sesión anónima o token inválido"
27988
+ },
27989
+ "403": {
27990
+ "content": {
27991
+ "application/json": {
27992
+ "schema": {
27993
+ "$ref": "#/components/schemas/Error"
27994
+ }
27995
+ }
27996
+ },
27997
+ "description": "El ABAC no permite crear pedidos"
27998
+ },
27999
+ "409": {
28000
+ "content": {
28001
+ "application/json": {
28002
+ "schema": {
28003
+ "$ref": "#/components/schemas/Error"
28004
+ }
28005
+ }
28006
+ },
28007
+ "description": "Sin stock suficiente para una variante"
28008
+ },
28009
+ "422": {
28010
+ "content": {
28011
+ "application/json": {
28012
+ "schema": {
28013
+ "$ref": "#/components/schemas/Error"
28014
+ }
28015
+ }
28016
+ },
28017
+ "description": "Carrito inválido"
28018
+ }
28019
+ },
28020
+ "security": [
28021
+ {
28022
+ "endUserToken": []
28023
+ }
28024
+ ],
28025
+ "summary": "Checkout nativo (crear un pedido)",
28026
+ "tags": [
28027
+ "public"
28028
+ ]
28029
+ }
28030
+ },
26952
28031
  "/v1/public/me/objects/{object_slug}/records": {
26953
28032
  "post": {
26954
28033
  "callbacks": {},
@@ -27244,6 +28323,83 @@
27244
28323
  ]
27245
28324
  }
27246
28325
  },
28326
+ "/v1/public/me/objects/{object_slug}/uploads": {
28327
+ "post": {
28328
+ "callbacks": {},
28329
+ "description": "Como el staged upload del canal de máquina (`POST /v1/objects/{object_slug}/uploads`), pero gobernado por el ABAC default-deny del canal público: exige una policy `allow` de `create` o `update` para el principal (`public` con la API key; `end_user` con el session token en la variante `/me`) sobre el object — sin policy, 403. Flujo: presign → `PUT` del blob a `uploadUrl` → confirmar escribiendo el record con `fileUrl` en el envelope del attribute `file`.",
28330
+ "operationId": "HablalaApiWeb.PublicApi.UploadController.create",
28331
+ "parameters": [
28332
+ {
28333
+ "description": "",
28334
+ "in": "path",
28335
+ "name": "object_slug",
28336
+ "required": true,
28337
+ "schema": {
28338
+ "type": "string",
28339
+ "x-struct": null,
28340
+ "x-validate": null
28341
+ }
28342
+ }
28343
+ ],
28344
+ "requestBody": {
28345
+ "content": {
28346
+ "application/json": {
28347
+ "schema": {
28348
+ "$ref": "#/components/schemas/RecordUploadRequest"
28349
+ }
28350
+ }
28351
+ },
28352
+ "description": "Archivo a subir",
28353
+ "required": false
28354
+ },
28355
+ "responses": {
28356
+ "200": {
28357
+ "content": {
28358
+ "application/json": {
28359
+ "schema": {
28360
+ "$ref": "#/components/schemas/RecordUploadResponse"
28361
+ }
28362
+ }
28363
+ },
28364
+ "description": "Destino de subida prefirmado"
28365
+ },
28366
+ "403": {
28367
+ "content": {
28368
+ "application/json": {
28369
+ "schema": {
28370
+ "$ref": "#/components/schemas/Error"
28371
+ }
28372
+ }
28373
+ },
28374
+ "description": "Sin policy allow de create/update para este principal"
28375
+ },
28376
+ "404": {
28377
+ "content": {
28378
+ "application/json": {
28379
+ "schema": {
28380
+ "$ref": "#/components/schemas/Error"
28381
+ }
28382
+ }
28383
+ },
28384
+ "description": "Objeto desconocido"
28385
+ },
28386
+ "422": {
28387
+ "content": {
28388
+ "application/json": {
28389
+ "schema": {
28390
+ "$ref": "#/components/schemas/Error"
28391
+ }
28392
+ }
28393
+ },
28394
+ "description": "Attribute inexistente/no-file, MIME no admitido o tamaño excedido"
28395
+ }
28396
+ },
28397
+ "summary": "Pedir una URL de subida para un attribute file (canal público)",
28398
+ "tags": [
28399
+ "public"
28400
+ ]
28401
+ }
28402
+ },
27247
28403
  "/v1/public/me/query": {
27248
28404
  "post": {
27249
28405
  "callbacks": {},
@@ -27619,6 +28775,83 @@
27619
28775
  ]
27620
28776
  }
27621
28777
  },
28778
+ "/v1/public/objects/{object_slug}/uploads": {
28779
+ "post": {
28780
+ "callbacks": {},
28781
+ "description": "Como el staged upload del canal de máquina (`POST /v1/objects/{object_slug}/uploads`), pero gobernado por el ABAC default-deny del canal público: exige una policy `allow` de `create` o `update` para el principal (`public` con la API key; `end_user` con el session token en la variante `/me`) sobre el object — sin policy, 403. Flujo: presign → `PUT` del blob a `uploadUrl` → confirmar escribiendo el record con `fileUrl` en el envelope del attribute `file`.",
28782
+ "operationId": "HablalaApiWeb.PublicApi.UploadController.create (2)",
28783
+ "parameters": [
28784
+ {
28785
+ "description": "",
28786
+ "in": "path",
28787
+ "name": "object_slug",
28788
+ "required": true,
28789
+ "schema": {
28790
+ "type": "string",
28791
+ "x-struct": null,
28792
+ "x-validate": null
28793
+ }
28794
+ }
28795
+ ],
28796
+ "requestBody": {
28797
+ "content": {
28798
+ "application/json": {
28799
+ "schema": {
28800
+ "$ref": "#/components/schemas/RecordUploadRequest"
28801
+ }
28802
+ }
28803
+ },
28804
+ "description": "Archivo a subir",
28805
+ "required": false
28806
+ },
28807
+ "responses": {
28808
+ "200": {
28809
+ "content": {
28810
+ "application/json": {
28811
+ "schema": {
28812
+ "$ref": "#/components/schemas/RecordUploadResponse"
28813
+ }
28814
+ }
28815
+ },
28816
+ "description": "Destino de subida prefirmado"
28817
+ },
28818
+ "403": {
28819
+ "content": {
28820
+ "application/json": {
28821
+ "schema": {
28822
+ "$ref": "#/components/schemas/Error"
28823
+ }
28824
+ }
28825
+ },
28826
+ "description": "Sin policy allow de create/update para este principal"
28827
+ },
28828
+ "404": {
28829
+ "content": {
28830
+ "application/json": {
28831
+ "schema": {
28832
+ "$ref": "#/components/schemas/Error"
28833
+ }
28834
+ }
28835
+ },
28836
+ "description": "Objeto desconocido"
28837
+ },
28838
+ "422": {
28839
+ "content": {
28840
+ "application/json": {
28841
+ "schema": {
28842
+ "$ref": "#/components/schemas/Error"
28843
+ }
28844
+ }
28845
+ },
28846
+ "description": "Attribute inexistente/no-file, MIME no admitido o tamaño excedido"
28847
+ }
28848
+ },
28849
+ "summary": "Pedir una URL de subida para un attribute file (canal público)",
28850
+ "tags": [
28851
+ "public"
28852
+ ]
28853
+ }
28854
+ },
27622
28855
  "/v1/public/query": {
27623
28856
  "post": {
27624
28857
  "callbacks": {},
@@ -27777,6 +29010,66 @@
27777
29010
  ]
27778
29011
  }
27779
29012
  },
29013
+ "/v1/public/variants/availability": {
29014
+ "post": {
29015
+ "callbacks": {},
29016
+ "description": "Dado un lote de ids de `product_variant`, devuelve por variante si es comprable (activa y con stock > 0) — el booleano del badge \"Agotado\". Autenticado por API key. Se expone el booleano derivado, no el conteo de stock.",
29017
+ "operationId": "HablalaApiWeb.PublicApi.AvailabilityController.create",
29018
+ "parameters": [],
29019
+ "requestBody": {
29020
+ "content": {
29021
+ "application/json": {
29022
+ "schema": {
29023
+ "$ref": "#/components/schemas/VariantAvailabilityRequest"
29024
+ }
29025
+ }
29026
+ },
29027
+ "description": "Ids de variante",
29028
+ "required": false
29029
+ },
29030
+ "responses": {
29031
+ "200": {
29032
+ "content": {
29033
+ "application/json": {
29034
+ "schema": {
29035
+ "$ref": "#/components/schemas/AvailabilityResponse"
29036
+ }
29037
+ }
29038
+ },
29039
+ "description": "Disponibilidad por variante"
29040
+ },
29041
+ "401": {
29042
+ "content": {
29043
+ "application/json": {
29044
+ "schema": {
29045
+ "$ref": "#/components/schemas/Error"
29046
+ }
29047
+ }
29048
+ },
29049
+ "description": "Token inválido"
29050
+ },
29051
+ "422": {
29052
+ "content": {
29053
+ "application/json": {
29054
+ "schema": {
29055
+ "$ref": "#/components/schemas/Error"
29056
+ }
29057
+ }
29058
+ },
29059
+ "description": "Petición inválida"
29060
+ }
29061
+ },
29062
+ "security": [
29063
+ {
29064
+ "apiKey": []
29065
+ }
29066
+ ],
29067
+ "summary": "Disponibilidad de compra de variantes (Public API)",
29068
+ "tags": [
29069
+ "public"
29070
+ ]
29071
+ }
29072
+ },
27780
29073
  "/v1/push-tokens": {
27781
29074
  "post": {
27782
29075
  "callbacks": {},