@hablala/api-contract 0.6.0 → 0.7.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": {
@@ -9321,6 +9572,14 @@
9321
9572
  "x-struct": null,
9322
9573
  "x-validate": null
9323
9574
  },
9575
+ "maxSize": {
9576
+ "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`.",
9577
+ "minimum": 1,
9578
+ "nullable": true,
9579
+ "type": "integer",
9580
+ "x-struct": null,
9581
+ "x-validate": null
9582
+ },
9324
9583
  "purpose": {
9325
9584
  "description": "Uso del archivo. `avatar` restringe el `contentType` a imágenes (perfil de usuario / icono de org). Omitir para adjuntos genéricos.",
9326
9585
  "enum": [
@@ -9355,37 +9614,56 @@
9355
9614
  "x-validate": null
9356
9615
  },
9357
9616
  "PresignResult": {
9617
+ "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
9618
  "properties": {
9619
+ "fields": {
9620
+ "additionalProperties": {
9621
+ "type": "string",
9622
+ "x-struct": null,
9623
+ "x-validate": null
9624
+ },
9625
+ "description": "Campos del form multipart a enviar ANTES del archivo (POST policy: `key`, `Content-Type`, `Policy`, firma…). Vacío cuando `method` es `PUT`.",
9626
+ "type": "object",
9627
+ "x-struct": null,
9628
+ "x-validate": null
9629
+ },
9359
9630
  "fileUrl": {
9360
- "description": "URL final del blob; va en el `attachment` del mensaje.",
9631
+ "description": "URL final del blob; va en el `attachment` del mensaje o el atributo `file`.",
9361
9632
  "type": "string",
9362
9633
  "x-struct": null,
9363
9634
  "x-validate": null
9364
9635
  },
9365
9636
  "headers": {
9366
- "description": "Cabeceras que el cliente debe enviar en el PUT.",
9637
+ "additionalProperties": {
9638
+ "type": "string",
9639
+ "x-struct": null,
9640
+ "x-validate": null
9641
+ },
9642
+ "description": "Cabeceras que el cliente envía en el `PUT`. Vacío cuando `method` es `POST`.",
9367
9643
  "type": "object",
9368
9644
  "x-struct": null,
9369
9645
  "x-validate": null
9370
9646
  },
9371
9647
  "method": {
9648
+ "description": "Cómo subir: `POST` (form multipart, POST policy) o `PUT` (blob directo).",
9372
9649
  "enum": [
9650
+ "POST",
9373
9651
  "PUT"
9374
9652
  ],
9375
9653
  "type": "string",
9376
9654
  "x-struct": null,
9377
9655
  "x-validate": null
9378
9656
  },
9379
- "uploadUrl": {
9380
- "description": "Destino del PUT del blob.",
9657
+ "url": {
9658
+ "description": "Destino de la subida.",
9381
9659
  "type": "string",
9382
9660
  "x-struct": null,
9383
9661
  "x-validate": null
9384
9662
  }
9385
9663
  },
9386
9664
  "required": [
9387
- "uploadUrl",
9388
9665
  "method",
9666
+ "url",
9389
9667
  "fileUrl"
9390
9668
  ],
9391
9669
  "title": "PresignResult",
@@ -9393,6 +9671,49 @@
9393
9671
  "x-struct": "Elixir.HablalaApiWeb.Schemas.PresignResult",
9394
9672
  "x-validate": null
9395
9673
  },
9674
+ "PublicOrganization": {
9675
+ "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.",
9676
+ "properties": {
9677
+ "avatarUrl": {
9678
+ "description": "Logo/icono de la marca (el \"server icon\" del tenant), o `null`.",
9679
+ "nullable": true,
9680
+ "type": "string",
9681
+ "x-struct": null,
9682
+ "x-validate": null
9683
+ },
9684
+ "id": {
9685
+ "description": "ID público de la organización.",
9686
+ "type": "string",
9687
+ "x-struct": null,
9688
+ "x-validate": null
9689
+ },
9690
+ "name": {
9691
+ "description": "Nombre visible de la marca.",
9692
+ "type": "string",
9693
+ "x-struct": null,
9694
+ "x-validate": null
9695
+ },
9696
+ "settings": {
9697
+ "$ref": "#/components/schemas/ManifestSettings"
9698
+ },
9699
+ "slug": {
9700
+ "description": "Slug estable de la organización.",
9701
+ "type": "string",
9702
+ "x-struct": null,
9703
+ "x-validate": null
9704
+ }
9705
+ },
9706
+ "required": [
9707
+ "id",
9708
+ "name",
9709
+ "slug",
9710
+ "settings"
9711
+ ],
9712
+ "title": "PublicOrganization",
9713
+ "type": "object",
9714
+ "x-struct": "Elixir.HablalaApiWeb.Schemas.PublicOrganization",
9715
+ "x-validate": null
9716
+ },
9396
9717
  "PublicSchema": {
9397
9718
  "properties": {
9398
9719
  "objects": {
@@ -9403,6 +9724,9 @@
9403
9724
  "x-struct": null,
9404
9725
  "x-validate": null
9405
9726
  },
9727
+ "organization": {
9728
+ "$ref": "#/components/schemas/PublicOrganization"
9729
+ },
9406
9730
  "relationships": {
9407
9731
  "items": {
9408
9732
  "$ref": "#/components/schemas/PublicSchemaRelationship"
@@ -9413,6 +9737,7 @@
9413
9737
  }
9414
9738
  },
9415
9739
  "required": [
9740
+ "organization",
9416
9741
  "objects",
9417
9742
  "relationships"
9418
9743
  ],
@@ -9948,13 +10273,58 @@
9948
10273
  "x-validate": null
9949
10274
  },
9950
10275
  "properties": {
9951
- "type": "object",
9952
- "x-struct": null,
9953
- "x-validate": null
9954
- },
9955
- "referenceLabels": {
9956
10276
  "additionalProperties": {
9957
- "properties": {
10277
+ "oneOf": [
10278
+ {
10279
+ "items": {
10280
+ "$ref": "#/components/schemas/FileEnvelope"
10281
+ },
10282
+ "type": "array",
10283
+ "x-struct": null,
10284
+ "x-validate": null
10285
+ },
10286
+ {
10287
+ "type": "string",
10288
+ "x-struct": null,
10289
+ "x-validate": null
10290
+ },
10291
+ {
10292
+ "type": "number",
10293
+ "x-struct": null,
10294
+ "x-validate": null
10295
+ },
10296
+ {
10297
+ "type": "boolean",
10298
+ "x-struct": null,
10299
+ "x-validate": null
10300
+ },
10301
+ {
10302
+ "items": {
10303
+ "type": "string",
10304
+ "x-struct": null,
10305
+ "x-validate": null
10306
+ },
10307
+ "type": "array",
10308
+ "x-struct": null,
10309
+ "x-validate": null
10310
+ },
10311
+ {
10312
+ "type": "object",
10313
+ "x-struct": null,
10314
+ "x-validate": null
10315
+ }
10316
+ ],
10317
+ "x-struct": null,
10318
+ "x-validate": null
10319
+ },
10320
+ "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.",
10321
+ "type": "object",
10322
+ "x-struct": null,
10323
+ "x-validate": null
10324
+ },
10325
+ "referenceLabels": {
10326
+ "additionalProperties": {
10327
+ "properties": {
9958
10328
  "label": {
9959
10329
  "type": "string",
9960
10330
  "x-struct": null,
@@ -9995,10 +10365,76 @@
9995
10365
  "x-struct": "Elixir.HablalaApiWeb.Schemas.Record",
9996
10366
  "x-validate": null
9997
10367
  },
10368
+ "RecordDependencies": {
10369
+ "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.",
10370
+ "properties": {
10371
+ "data": {
10372
+ "properties": {
10373
+ "blockers": {
10374
+ "description": "Referencias core activas que bloquean; vacía si se puede borrar.",
10375
+ "items": {
10376
+ "properties": {
10377
+ "count": {
10378
+ "minimum": 1,
10379
+ "type": "integer",
10380
+ "x-struct": null,
10381
+ "x-validate": null
10382
+ },
10383
+ "label": {
10384
+ "description": "Etiqueta legible (p. ej. \"facturas sin cerrar\").",
10385
+ "type": "string",
10386
+ "x-struct": null,
10387
+ "x-validate": null
10388
+ }
10389
+ },
10390
+ "required": [
10391
+ "count",
10392
+ "label"
10393
+ ],
10394
+ "type": "object",
10395
+ "x-struct": null,
10396
+ "x-validate": null
10397
+ },
10398
+ "type": "array",
10399
+ "x-struct": null,
10400
+ "x-validate": null
10401
+ },
10402
+ "deletable": {
10403
+ "description": "`false` si algún blocker impediría el borrado.",
10404
+ "type": "boolean",
10405
+ "x-struct": null,
10406
+ "x-validate": null
10407
+ },
10408
+ "edgeCount": {
10409
+ "description": "Aristas del grafo (ambos extremos) que se romperían al borrar.",
10410
+ "minimum": 0,
10411
+ "type": "integer",
10412
+ "x-struct": null,
10413
+ "x-validate": null
10414
+ }
10415
+ },
10416
+ "required": [
10417
+ "edgeCount",
10418
+ "deletable",
10419
+ "blockers"
10420
+ ],
10421
+ "type": "object",
10422
+ "x-struct": null,
10423
+ "x-validate": null
10424
+ }
10425
+ },
10426
+ "required": [
10427
+ "data"
10428
+ ],
10429
+ "title": "RecordDependencies",
10430
+ "type": "object",
10431
+ "x-struct": "Elixir.HablalaApiWeb.Schemas.RecordDependencies",
10432
+ "x-validate": null
10433
+ },
9998
10434
  "RecordInput": {
9999
10435
  "properties": {
10000
10436
  "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).",
10437
+ "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
10438
  "type": "object",
10003
10439
  "x-struct": null,
10004
10440
  "x-validate": null
@@ -11482,6 +11918,29 @@
11482
11918
  "x-struct": "Elixir.HablalaApiWeb.Schemas.UsernameAvailableResponse",
11483
11919
  "x-validate": null
11484
11920
  },
11921
+ "VariantAvailabilityRequest": {
11922
+ "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).",
11923
+ "properties": {
11924
+ "variantIds": {
11925
+ "items": {
11926
+ "description": "Id público (`rec_…`) de un `product_variant`.",
11927
+ "type": "string",
11928
+ "x-struct": null,
11929
+ "x-validate": null
11930
+ },
11931
+ "type": "array",
11932
+ "x-struct": null,
11933
+ "x-validate": null
11934
+ }
11935
+ },
11936
+ "required": [
11937
+ "variantIds"
11938
+ ],
11939
+ "title": "VariantAvailabilityRequest",
11940
+ "type": "object",
11941
+ "x-struct": "Elixir.HablalaApiWeb.Schemas.VariantAvailabilityRequest",
11942
+ "x-validate": null
11943
+ },
11485
11944
  "VerificationRejectRequest": {
11486
11945
  "properties": {
11487
11946
  "reason": {
@@ -14935,7 +15394,7 @@
14935
15394
  "/v1/me/uploads/presign": {
14936
15395
  "post": {
14937
15396
  "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.",
15397
+ "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
15398
  "operationId": "HablalaApiWeb.Me.UploadController.presign",
14940
15399
  "parameters": [],
14941
15400
  "requestBody": {
@@ -14958,7 +15417,7 @@
14958
15417
  }
14959
15418
  }
14960
15419
  },
14961
- "description": "URL prefirmada"
15420
+ "description": "Sobre de subida prefirmado"
14962
15421
  },
14963
15422
  "415": {
14964
15423
  "content": {
@@ -14971,7 +15430,7 @@
14971
15430
  "description": "Tipo de archivo no soportado"
14972
15431
  }
14973
15432
  },
14974
- "summary": "Pedir una URL de subida prefirmada (adjunto personal)",
15433
+ "summary": "Pedir un sobre de subida prefirmado (adjunto personal)",
14975
15434
  "tags": [
14976
15435
  "personal uploads"
14977
15436
  ]
@@ -15172,7 +15631,7 @@
15172
15631
  "/v1/oauth/token": {
15173
15632
  "post": {
15174
15633
  "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.",
15634
+ "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
15635
  "operationId": "HablalaApiWeb.Oauth.TokenController.create",
15177
15636
  "parameters": [],
15178
15637
  "requestBody": {
@@ -15756,6 +16215,84 @@
15756
16215
  ]
15757
16216
  }
15758
16217
  },
16218
+ "/v1/objects/{object_slug}/records/trash": {
16219
+ "get": {
16220
+ "callbacks": {},
16221
+ "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`.",
16222
+ "operationId": "HablalaApiWeb.Machine.RecordController.trash",
16223
+ "parameters": [
16224
+ {
16225
+ "description": "",
16226
+ "in": "path",
16227
+ "name": "object_slug",
16228
+ "required": true,
16229
+ "schema": {
16230
+ "type": "string",
16231
+ "x-struct": null,
16232
+ "x-validate": null
16233
+ }
16234
+ },
16235
+ {
16236
+ "description": "",
16237
+ "in": "query",
16238
+ "name": "limit",
16239
+ "required": false,
16240
+ "schema": {
16241
+ "type": "integer",
16242
+ "x-struct": null,
16243
+ "x-validate": null
16244
+ }
16245
+ },
16246
+ {
16247
+ "description": "",
16248
+ "in": "query",
16249
+ "name": "cursor",
16250
+ "required": false,
16251
+ "schema": {
16252
+ "type": "string",
16253
+ "x-struct": null,
16254
+ "x-validate": null
16255
+ }
16256
+ }
16257
+ ],
16258
+ "responses": {
16259
+ "200": {
16260
+ "content": {
16261
+ "application/json": {
16262
+ "schema": {
16263
+ "$ref": "#/components/schemas/QueryResultResponse"
16264
+ }
16265
+ }
16266
+ },
16267
+ "description": "Records borrados"
16268
+ },
16269
+ "403": {
16270
+ "content": {
16271
+ "application/json": {
16272
+ "schema": {
16273
+ "$ref": "#/components/schemas/Error"
16274
+ }
16275
+ }
16276
+ },
16277
+ "description": "Sin permiso"
16278
+ },
16279
+ "404": {
16280
+ "content": {
16281
+ "application/json": {
16282
+ "schema": {
16283
+ "$ref": "#/components/schemas/Error"
16284
+ }
16285
+ }
16286
+ },
16287
+ "description": "Objeto desconocido"
16288
+ }
16289
+ },
16290
+ "summary": "Listar los records borrados (soft) del objeto — la Papelera",
16291
+ "tags": [
16292
+ "records"
16293
+ ]
16294
+ }
16295
+ },
15759
16296
  "/v1/objects/{object_slug}/records/{id}": {
15760
16297
  "delete": {
15761
16298
  "callbacks": {},
@@ -16049,6 +16586,73 @@
16049
16586
  ]
16050
16587
  }
16051
16588
  },
16589
+ "/v1/objects/{object_slug}/records/{record_id}/dependencies": {
16590
+ "get": {
16591
+ "callbacks": {},
16592
+ "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.",
16593
+ "operationId": "HablalaApiWeb.Machine.RecordController.dependencies",
16594
+ "parameters": [
16595
+ {
16596
+ "description": "",
16597
+ "in": "path",
16598
+ "name": "object_slug",
16599
+ "required": true,
16600
+ "schema": {
16601
+ "type": "string",
16602
+ "x-struct": null,
16603
+ "x-validate": null
16604
+ }
16605
+ },
16606
+ {
16607
+ "description": "",
16608
+ "in": "path",
16609
+ "name": "record_id",
16610
+ "required": true,
16611
+ "schema": {
16612
+ "type": "string",
16613
+ "x-struct": null,
16614
+ "x-validate": null
16615
+ }
16616
+ }
16617
+ ],
16618
+ "responses": {
16619
+ "200": {
16620
+ "content": {
16621
+ "application/json": {
16622
+ "schema": {
16623
+ "$ref": "#/components/schemas/RecordDependencies"
16624
+ }
16625
+ }
16626
+ },
16627
+ "description": "Dependencias"
16628
+ },
16629
+ "403": {
16630
+ "content": {
16631
+ "application/json": {
16632
+ "schema": {
16633
+ "$ref": "#/components/schemas/Error"
16634
+ }
16635
+ }
16636
+ },
16637
+ "description": "Sin permiso ABAC sobre este record"
16638
+ },
16639
+ "404": {
16640
+ "content": {
16641
+ "application/json": {
16642
+ "schema": {
16643
+ "$ref": "#/components/schemas/Error"
16644
+ }
16645
+ }
16646
+ },
16647
+ "description": "No encontrado (o de otro objeto)"
16648
+ }
16649
+ },
16650
+ "summary": "Preview de dependencias de un record (impacto de borrarlo, sin borrar)",
16651
+ "tags": [
16652
+ "records"
16653
+ ]
16654
+ }
16655
+ },
16052
16656
  "/v1/objects/{object_slug}/records/{record_id}/events": {
16053
16657
  "get": {
16054
16658
  "callbacks": {},
@@ -16642,7 +17246,7 @@
16642
17246
  },
16643
17247
  "post": {
16644
17248
  "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).",
17249
+ "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
17250
  "operationId": "HablalaApiWeb.AccessTokens.TokenController.create",
16647
17251
  "parameters": [
16648
17252
  {
@@ -17176,7 +17780,7 @@
17176
17780
  },
17177
17781
  "post": {
17178
17782
  "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).",
17783
+ "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
17784
  "operationId": "HablalaApiWeb.PublicApi.ApiKeyController.create",
17181
17785
  "parameters": [
17182
17786
  {
@@ -17199,7 +17803,7 @@
17199
17803
  }
17200
17804
  }
17201
17805
  },
17202
- "description": "Datos del token",
17806
+ "description": "Datos del par",
17203
17807
  "required": false
17204
17808
  },
17205
17809
  "responses": {
@@ -17207,11 +17811,11 @@
17207
17811
  "content": {
17208
17812
  "application/json": {
17209
17813
  "schema": {
17210
- "$ref": "#/components/schemas/ApiKeyCreatedResponse"
17814
+ "$ref": "#/components/schemas/ApiKeyPairCreatedResponse"
17211
17815
  }
17212
17816
  }
17213
17817
  },
17214
- "description": "Token creado (con secreto)"
17818
+ "description": "Par creado (con secretos)"
17215
17819
  },
17216
17820
  "403": {
17217
17821
  "content": {
@@ -17234,7 +17838,7 @@
17234
17838
  "description": "Error de validación"
17235
17839
  }
17236
17840
  },
17237
- "summary": "Crear una API key",
17841
+ "summary": "Crear un par de API keys",
17238
17842
  "tags": [
17239
17843
  "public"
17240
17844
  ]
@@ -17290,10 +17894,11 @@
17290
17894
  ]
17291
17895
  }
17292
17896
  },
17293
- "/v1/organizations/{organization_id}/appointments": {
17294
- "get": {
17897
+ "/v1/organizations/{organization_id}/api-keys/{id}/rotate": {
17898
+ "post": {
17295
17899
  "callbacks": {},
17296
- "operationId": "HablalaApiWeb.Scheduling.AppointmentController.index",
17900
+ "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.",
17901
+ "operationId": "HablalaApiWeb.PublicApi.ApiKeyController.rotate",
17297
17902
  "parameters": [
17298
17903
  {
17299
17904
  "description": "",
@@ -17308,19 +17913,75 @@
17308
17913
  },
17309
17914
  {
17310
17915
  "description": "",
17311
- "in": "query",
17312
- "name": "organizerId",
17313
- "required": false,
17916
+ "in": "path",
17917
+ "name": "id",
17918
+ "required": true,
17314
17919
  "schema": {
17315
17920
  "type": "string",
17316
17921
  "x-struct": null,
17317
17922
  "x-validate": null
17318
17923
  }
17319
- },
17320
- {
17321
- "description": "ISO-8601; solape con [from, to).",
17322
- "in": "query",
17323
- "name": "from",
17924
+ }
17925
+ ],
17926
+ "responses": {
17927
+ "201": {
17928
+ "content": {
17929
+ "application/json": {
17930
+ "schema": {
17931
+ "$ref": "#/components/schemas/ApiKeyCreatedResponse"
17932
+ }
17933
+ }
17934
+ },
17935
+ "description": "Key rotada (nuevo secreto)"
17936
+ },
17937
+ "403": {
17938
+ "content": {
17939
+ "application/json": {
17940
+ "schema": {
17941
+ "$ref": "#/components/schemas/Error"
17942
+ }
17943
+ }
17944
+ },
17945
+ "description": "Sin permiso"
17946
+ }
17947
+ },
17948
+ "summary": "Rotar una API key",
17949
+ "tags": [
17950
+ "public"
17951
+ ]
17952
+ }
17953
+ },
17954
+ "/v1/organizations/{organization_id}/appointments": {
17955
+ "get": {
17956
+ "callbacks": {},
17957
+ "operationId": "HablalaApiWeb.Scheduling.AppointmentController.index",
17958
+ "parameters": [
17959
+ {
17960
+ "description": "",
17961
+ "in": "path",
17962
+ "name": "organization_id",
17963
+ "required": true,
17964
+ "schema": {
17965
+ "type": "string",
17966
+ "x-struct": null,
17967
+ "x-validate": null
17968
+ }
17969
+ },
17970
+ {
17971
+ "description": "",
17972
+ "in": "query",
17973
+ "name": "organizerId",
17974
+ "required": false,
17975
+ "schema": {
17976
+ "type": "string",
17977
+ "x-struct": null,
17978
+ "x-validate": null
17979
+ }
17980
+ },
17981
+ {
17982
+ "description": "ISO-8601; solape con [from, to).",
17983
+ "in": "query",
17984
+ "name": "from",
17324
17985
  "required": false,
17325
17986
  "schema": {
17326
17987
  "type": "string",
@@ -22189,7 +22850,7 @@
22189
22850
  "/v1/organizations/{organization_id}/oauth/grants/{user_code}/approve": {
22190
22851
  "post": {
22191
22852
  "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.",
22853
+ "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
22854
  "operationId": "HablalaApiWeb.Oauth.GrantController.approve",
22194
22855
  "parameters": [
22195
22856
  {
@@ -22929,6 +23590,95 @@
22929
23590
  ]
22930
23591
  }
22931
23592
  },
23593
+ "/v1/organizations/{organization_id}/objects/{object_slug}/records/trash": {
23594
+ "get": {
23595
+ "callbacks": {},
23596
+ "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`.",
23597
+ "operationId": "HablalaApiWeb.Data.RecordController.trash",
23598
+ "parameters": [
23599
+ {
23600
+ "description": "",
23601
+ "in": "path",
23602
+ "name": "organization_id",
23603
+ "required": true,
23604
+ "schema": {
23605
+ "type": "string",
23606
+ "x-struct": null,
23607
+ "x-validate": null
23608
+ }
23609
+ },
23610
+ {
23611
+ "description": "",
23612
+ "in": "path",
23613
+ "name": "object_slug",
23614
+ "required": true,
23615
+ "schema": {
23616
+ "type": "string",
23617
+ "x-struct": null,
23618
+ "x-validate": null
23619
+ }
23620
+ },
23621
+ {
23622
+ "description": "",
23623
+ "in": "query",
23624
+ "name": "limit",
23625
+ "required": false,
23626
+ "schema": {
23627
+ "type": "integer",
23628
+ "x-struct": null,
23629
+ "x-validate": null
23630
+ }
23631
+ },
23632
+ {
23633
+ "description": "",
23634
+ "in": "query",
23635
+ "name": "cursor",
23636
+ "required": false,
23637
+ "schema": {
23638
+ "type": "string",
23639
+ "x-struct": null,
23640
+ "x-validate": null
23641
+ }
23642
+ }
23643
+ ],
23644
+ "responses": {
23645
+ "200": {
23646
+ "content": {
23647
+ "application/json": {
23648
+ "schema": {
23649
+ "$ref": "#/components/schemas/QueryResultResponse"
23650
+ }
23651
+ }
23652
+ },
23653
+ "description": "Records borrados"
23654
+ },
23655
+ "403": {
23656
+ "content": {
23657
+ "application/json": {
23658
+ "schema": {
23659
+ "$ref": "#/components/schemas/Error"
23660
+ }
23661
+ }
23662
+ },
23663
+ "description": "Sin permiso"
23664
+ },
23665
+ "404": {
23666
+ "content": {
23667
+ "application/json": {
23668
+ "schema": {
23669
+ "$ref": "#/components/schemas/Error"
23670
+ }
23671
+ }
23672
+ },
23673
+ "description": "Objeto desconocido"
23674
+ }
23675
+ },
23676
+ "summary": "Listar los records borrados (soft) del objeto — la Papelera",
23677
+ "tags": [
23678
+ "records"
23679
+ ]
23680
+ }
23681
+ },
22932
23682
  "/v1/organizations/{organization_id}/objects/{object_slug}/records/{id}": {
22933
23683
  "delete": {
22934
23684
  "callbacks": {},
@@ -23266,6 +24016,84 @@
23266
24016
  ]
23267
24017
  }
23268
24018
  },
24019
+ "/v1/organizations/{organization_id}/objects/{object_slug}/records/{record_id}/dependencies": {
24020
+ "get": {
24021
+ "callbacks": {},
24022
+ "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.",
24023
+ "operationId": "HablalaApiWeb.Data.RecordController.dependencies",
24024
+ "parameters": [
24025
+ {
24026
+ "description": "",
24027
+ "in": "path",
24028
+ "name": "organization_id",
24029
+ "required": true,
24030
+ "schema": {
24031
+ "type": "string",
24032
+ "x-struct": null,
24033
+ "x-validate": null
24034
+ }
24035
+ },
24036
+ {
24037
+ "description": "",
24038
+ "in": "path",
24039
+ "name": "object_slug",
24040
+ "required": true,
24041
+ "schema": {
24042
+ "type": "string",
24043
+ "x-struct": null,
24044
+ "x-validate": null
24045
+ }
24046
+ },
24047
+ {
24048
+ "description": "",
24049
+ "in": "path",
24050
+ "name": "record_id",
24051
+ "required": true,
24052
+ "schema": {
24053
+ "type": "string",
24054
+ "x-struct": null,
24055
+ "x-validate": null
24056
+ }
24057
+ }
24058
+ ],
24059
+ "responses": {
24060
+ "200": {
24061
+ "content": {
24062
+ "application/json": {
24063
+ "schema": {
24064
+ "$ref": "#/components/schemas/RecordDependencies"
24065
+ }
24066
+ }
24067
+ },
24068
+ "description": "Dependencias"
24069
+ },
24070
+ "403": {
24071
+ "content": {
24072
+ "application/json": {
24073
+ "schema": {
24074
+ "$ref": "#/components/schemas/Error"
24075
+ }
24076
+ }
24077
+ },
24078
+ "description": "Sin permiso ABAC sobre este record"
24079
+ },
24080
+ "404": {
24081
+ "content": {
24082
+ "application/json": {
24083
+ "schema": {
24084
+ "$ref": "#/components/schemas/Error"
24085
+ }
24086
+ }
24087
+ },
24088
+ "description": "No encontrado (o de otro objeto)"
24089
+ }
24090
+ },
24091
+ "summary": "Preview de dependencias de un record (impacto de borrarlo, sin borrar)",
24092
+ "tags": [
24093
+ "records"
24094
+ ]
24095
+ }
24096
+ },
23269
24097
  "/v1/organizations/{organization_id}/objects/{object_slug}/records/{record_id}/events": {
23270
24098
  "get": {
23271
24099
  "callbacks": {},
@@ -25915,7 +26743,7 @@
25915
26743
  "/v1/organizations/{organization_id}/uploads/presign": {
25916
26744
  "post": {
25917
26745
  "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`.)",
26746
+ "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
26747
  "operationId": "HablalaApiWeb.UploadController.presign",
25920
26748
  "parameters": [
25921
26749
  {
@@ -25950,7 +26778,7 @@
25950
26778
  }
25951
26779
  }
25952
26780
  },
25953
- "description": "URL prefirmada"
26781
+ "description": "Sobre de subida prefirmado"
25954
26782
  },
25955
26783
  "415": {
25956
26784
  "content": {
@@ -25963,7 +26791,7 @@
25963
26791
  "description": "Tipo de archivo no soportado"
25964
26792
  }
25965
26793
  },
25966
- "summary": "Pedir una URL de subida prefirmada",
26794
+ "summary": "Pedir un sobre de subida prefirmado",
25967
26795
  "tags": [
25968
26796
  "uploads"
25969
26797
  ]
@@ -26949,6 +27777,98 @@
26949
27777
  ]
26950
27778
  }
26951
27779
  },
27780
+ "/v1/public/me/checkout": {
27781
+ "post": {
27782
+ "callbacks": {},
27783
+ "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`.",
27784
+ "operationId": "HablalaApiWeb.PublicApi.CheckoutController.create",
27785
+ "parameters": [
27786
+ {
27787
+ "description": "Clave de idempotencia (estándar Stripe): dos envíos con la misma → un pedido.",
27788
+ "in": "header",
27789
+ "name": "Idempotency-Key",
27790
+ "required": false,
27791
+ "schema": {
27792
+ "type": "string",
27793
+ "x-struct": null,
27794
+ "x-validate": null
27795
+ }
27796
+ }
27797
+ ],
27798
+ "requestBody": {
27799
+ "content": {
27800
+ "application/json": {
27801
+ "schema": {
27802
+ "$ref": "#/components/schemas/CheckoutRequest"
27803
+ }
27804
+ }
27805
+ },
27806
+ "description": "Artículos del carrito + moneda",
27807
+ "required": false
27808
+ },
27809
+ "responses": {
27810
+ "201": {
27811
+ "content": {
27812
+ "application/json": {
27813
+ "schema": {
27814
+ "$ref": "#/components/schemas/OrderResponse"
27815
+ }
27816
+ }
27817
+ },
27818
+ "description": "Pedido creado"
27819
+ },
27820
+ "401": {
27821
+ "content": {
27822
+ "application/json": {
27823
+ "schema": {
27824
+ "$ref": "#/components/schemas/Error"
27825
+ }
27826
+ }
27827
+ },
27828
+ "description": "Sesión anónima o token inválido"
27829
+ },
27830
+ "403": {
27831
+ "content": {
27832
+ "application/json": {
27833
+ "schema": {
27834
+ "$ref": "#/components/schemas/Error"
27835
+ }
27836
+ }
27837
+ },
27838
+ "description": "El ABAC no permite crear pedidos"
27839
+ },
27840
+ "409": {
27841
+ "content": {
27842
+ "application/json": {
27843
+ "schema": {
27844
+ "$ref": "#/components/schemas/Error"
27845
+ }
27846
+ }
27847
+ },
27848
+ "description": "Sin stock suficiente para una variante"
27849
+ },
27850
+ "422": {
27851
+ "content": {
27852
+ "application/json": {
27853
+ "schema": {
27854
+ "$ref": "#/components/schemas/Error"
27855
+ }
27856
+ }
27857
+ },
27858
+ "description": "Carrito inválido"
27859
+ }
27860
+ },
27861
+ "security": [
27862
+ {
27863
+ "endUserToken": []
27864
+ }
27865
+ ],
27866
+ "summary": "Checkout nativo (crear un pedido)",
27867
+ "tags": [
27868
+ "public"
27869
+ ]
27870
+ }
27871
+ },
26952
27872
  "/v1/public/me/objects/{object_slug}/records": {
26953
27873
  "post": {
26954
27874
  "callbacks": {},
@@ -27777,6 +28697,66 @@
27777
28697
  ]
27778
28698
  }
27779
28699
  },
28700
+ "/v1/public/variants/availability": {
28701
+ "post": {
28702
+ "callbacks": {},
28703
+ "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.",
28704
+ "operationId": "HablalaApiWeb.PublicApi.AvailabilityController.create",
28705
+ "parameters": [],
28706
+ "requestBody": {
28707
+ "content": {
28708
+ "application/json": {
28709
+ "schema": {
28710
+ "$ref": "#/components/schemas/VariantAvailabilityRequest"
28711
+ }
28712
+ }
28713
+ },
28714
+ "description": "Ids de variante",
28715
+ "required": false
28716
+ },
28717
+ "responses": {
28718
+ "200": {
28719
+ "content": {
28720
+ "application/json": {
28721
+ "schema": {
28722
+ "$ref": "#/components/schemas/AvailabilityResponse"
28723
+ }
28724
+ }
28725
+ },
28726
+ "description": "Disponibilidad por variante"
28727
+ },
28728
+ "401": {
28729
+ "content": {
28730
+ "application/json": {
28731
+ "schema": {
28732
+ "$ref": "#/components/schemas/Error"
28733
+ }
28734
+ }
28735
+ },
28736
+ "description": "Token inválido"
28737
+ },
28738
+ "422": {
28739
+ "content": {
28740
+ "application/json": {
28741
+ "schema": {
28742
+ "$ref": "#/components/schemas/Error"
28743
+ }
28744
+ }
28745
+ },
28746
+ "description": "Petición inválida"
28747
+ }
28748
+ },
28749
+ "security": [
28750
+ {
28751
+ "apiKey": []
28752
+ }
28753
+ ],
28754
+ "summary": "Disponibilidad de compra de variantes (Public API)",
28755
+ "tags": [
28756
+ "public"
28757
+ ]
28758
+ }
28759
+ },
27780
28760
  "/v1/push-tokens": {
27781
28761
  "post": {
27782
28762
  "callbacks": {},