@hablala/api-contract 0.1.2 → 0.1.3

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
@@ -2,6 +2,244 @@
2
2
  "components": {
3
3
  "responses": {},
4
4
  "schemas": {
5
+ "AccessToken": {
6
+ "description": "Metadatos de un access token de máquina (`hpat_…`): la credencial con la que un agente (Claude Code / Lá) o un script CI escribe la ontología por HTTP. El secreto en claro NO se incluye aquí — solo se devuelve una vez, al crearlo.",
7
+ "properties": {
8
+ "active": {
9
+ "description": "¿Ni revocado ni expirado?",
10
+ "type": "boolean",
11
+ "x-struct": null,
12
+ "x-validate": null
13
+ },
14
+ "expiresAt": {
15
+ "format": "date-time",
16
+ "nullable": true,
17
+ "type": "string",
18
+ "x-struct": null,
19
+ "x-validate": null
20
+ },
21
+ "id": {
22
+ "type": "string",
23
+ "x-struct": null,
24
+ "x-validate": null
25
+ },
26
+ "insertedAt": {
27
+ "format": "date-time",
28
+ "type": "string",
29
+ "x-struct": null,
30
+ "x-validate": null
31
+ },
32
+ "lastUsedAt": {
33
+ "format": "date-time",
34
+ "nullable": true,
35
+ "type": "string",
36
+ "x-struct": null,
37
+ "x-validate": null
38
+ },
39
+ "name": {
40
+ "description": "Etiqueta para el panel (p. ej. \"CI deploy\").",
41
+ "type": "string",
42
+ "x-struct": null,
43
+ "x-validate": null
44
+ },
45
+ "permissions": {
46
+ "description": "Permisos RBAC del token (`recurso:accion`), su propio scope (mínimo privilegio).",
47
+ "items": {
48
+ "type": "string",
49
+ "x-struct": null,
50
+ "x-validate": null
51
+ },
52
+ "type": "array",
53
+ "x-struct": null,
54
+ "x-validate": null
55
+ },
56
+ "prefix": {
57
+ "description": "Primeros caracteres del secreto (no secreto), p. ej. `hpat_a1b2`.",
58
+ "nullable": true,
59
+ "type": "string",
60
+ "x-struct": null,
61
+ "x-validate": null
62
+ },
63
+ "revokedAt": {
64
+ "format": "date-time",
65
+ "nullable": true,
66
+ "type": "string",
67
+ "x-struct": null,
68
+ "x-validate": null
69
+ },
70
+ "updatedAt": {
71
+ "format": "date-time",
72
+ "type": "string",
73
+ "x-struct": null,
74
+ "x-validate": null
75
+ }
76
+ },
77
+ "required": [
78
+ "id",
79
+ "name",
80
+ "permissions",
81
+ "active"
82
+ ],
83
+ "title": "AccessToken",
84
+ "type": "object",
85
+ "x-struct": "Elixir.HablalaApiWeb.Schemas.AccessToken",
86
+ "x-validate": null
87
+ },
88
+ "AccessTokenCreated": {
89
+ "description": "El token recién creado, con el secreto en claro (`token`) presente UNA sola vez. Guárdalo: no se puede recuperar después.",
90
+ "properties": {
91
+ "active": {
92
+ "type": "boolean",
93
+ "x-struct": null,
94
+ "x-validate": null
95
+ },
96
+ "expiresAt": {
97
+ "format": "date-time",
98
+ "nullable": true,
99
+ "type": "string",
100
+ "x-struct": null,
101
+ "x-validate": null
102
+ },
103
+ "id": {
104
+ "type": "string",
105
+ "x-struct": null,
106
+ "x-validate": null
107
+ },
108
+ "insertedAt": {
109
+ "format": "date-time",
110
+ "type": "string",
111
+ "x-struct": null,
112
+ "x-validate": null
113
+ },
114
+ "lastUsedAt": {
115
+ "format": "date-time",
116
+ "nullable": true,
117
+ "type": "string",
118
+ "x-struct": null,
119
+ "x-validate": null
120
+ },
121
+ "name": {
122
+ "type": "string",
123
+ "x-struct": null,
124
+ "x-validate": null
125
+ },
126
+ "permissions": {
127
+ "items": {
128
+ "type": "string",
129
+ "x-struct": null,
130
+ "x-validate": null
131
+ },
132
+ "type": "array",
133
+ "x-struct": null,
134
+ "x-validate": null
135
+ },
136
+ "prefix": {
137
+ "nullable": true,
138
+ "type": "string",
139
+ "x-struct": null,
140
+ "x-validate": null
141
+ },
142
+ "revokedAt": {
143
+ "format": "date-time",
144
+ "nullable": true,
145
+ "type": "string",
146
+ "x-struct": null,
147
+ "x-validate": null
148
+ },
149
+ "token": {
150
+ "description": "El secreto en claro (`hpat_…`). Solo en esta respuesta.",
151
+ "type": "string",
152
+ "x-struct": null,
153
+ "x-validate": null
154
+ },
155
+ "updatedAt": {
156
+ "format": "date-time",
157
+ "type": "string",
158
+ "x-struct": null,
159
+ "x-validate": null
160
+ }
161
+ },
162
+ "required": [
163
+ "id",
164
+ "name",
165
+ "permissions",
166
+ "token"
167
+ ],
168
+ "title": "AccessTokenCreated",
169
+ "type": "object",
170
+ "x-struct": "Elixir.HablalaApiWeb.Schemas.AccessTokenCreated",
171
+ "x-validate": null
172
+ },
173
+ "AccessTokenCreatedResponse": {
174
+ "properties": {
175
+ "data": {
176
+ "$ref": "#/components/schemas/AccessTokenCreated"
177
+ }
178
+ },
179
+ "required": [
180
+ "data"
181
+ ],
182
+ "title": "AccessTokenCreatedResponse",
183
+ "type": "object",
184
+ "x-struct": "Elixir.HablalaApiWeb.Schemas.AccessTokenCreatedResponse",
185
+ "x-validate": null
186
+ },
187
+ "AccessTokenRequest": {
188
+ "description": "Datos para emitir un access token. Los permisos se intersectan con los del creador (privilege de-escalation): un token nunca puede más que quien lo emite.",
189
+ "properties": {
190
+ "expiresAt": {
191
+ "description": "Caducidad opcional (ISO 8601). Omitir = no caduca.",
192
+ "format": "date-time",
193
+ "type": "string",
194
+ "x-struct": null,
195
+ "x-validate": null
196
+ },
197
+ "name": {
198
+ "description": "Etiqueta para identificarlo en el panel.",
199
+ "type": "string",
200
+ "x-struct": null,
201
+ "x-validate": null
202
+ },
203
+ "permissions": {
204
+ "description": "Permisos RBAC (`recurso:accion`), p. ej. `[\"objects:create\", \"objects:read\"]`.",
205
+ "items": {
206
+ "type": "string",
207
+ "x-struct": null,
208
+ "x-validate": null
209
+ },
210
+ "type": "array",
211
+ "x-struct": null,
212
+ "x-validate": null
213
+ }
214
+ },
215
+ "required": [
216
+ "name",
217
+ "permissions"
218
+ ],
219
+ "title": "AccessTokenRequest",
220
+ "type": "object",
221
+ "x-struct": "Elixir.HablalaApiWeb.Schemas.AccessTokenRequest",
222
+ "x-validate": null
223
+ },
224
+ "AccessTokensResponse": {
225
+ "properties": {
226
+ "data": {
227
+ "items": {
228
+ "$ref": "#/components/schemas/AccessToken"
229
+ },
230
+ "type": "array",
231
+ "x-struct": null,
232
+ "x-validate": null
233
+ }
234
+ },
235
+ "required": [
236
+ "data"
237
+ ],
238
+ "title": "AccessTokensResponse",
239
+ "type": "object",
240
+ "x-struct": "Elixir.HablalaApiWeb.Schemas.AccessTokensResponse",
241
+ "x-validate": null
242
+ },
5
243
  "AddMemberRequest": {
6
244
  "description": "Añade un usuario del tenant a una conversación (invitar a un canal).",
7
245
  "properties": {
@@ -293,6 +531,81 @@
293
531
  "x-struct": "Elixir.HablalaApiWeb.Schemas.AttributesResponse",
294
532
  "x-validate": null
295
533
  },
534
+ "BulkInvitationRequest": {
535
+ "description": "Invita a varios emails a la vez, todos con el mismo rol opcional.",
536
+ "properties": {
537
+ "emails": {
538
+ "description": "Lista de emails a invitar. Duplicados y vacíos se ignoran.",
539
+ "items": {
540
+ "format": "email",
541
+ "type": "string",
542
+ "x-struct": null,
543
+ "x-validate": null
544
+ },
545
+ "type": "array",
546
+ "x-struct": null,
547
+ "x-validate": null
548
+ },
549
+ "role_id": {
550
+ "description": "Rol que recibirán los invitados (opcional; id público `role_…`).",
551
+ "nullable": true,
552
+ "type": "string",
553
+ "x-struct": null,
554
+ "x-validate": null
555
+ }
556
+ },
557
+ "required": [
558
+ "emails"
559
+ ],
560
+ "title": "BulkInvitationRequest",
561
+ "type": "object",
562
+ "x-struct": "Elixir.HablalaApiWeb.Schemas.BulkInvitationRequest",
563
+ "x-validate": null
564
+ },
565
+ "BulkInvitationResponse": {
566
+ "description": "Un resultado por email de entrada, en el mismo orden.",
567
+ "properties": {
568
+ "data": {
569
+ "items": {
570
+ "properties": {
571
+ "email": {
572
+ "type": "string",
573
+ "x-struct": null,
574
+ "x-validate": null
575
+ },
576
+ "status": {
577
+ "description": "`sent` (invitación creada/reenviada), `already` (ya es miembro), `invalid` (email mal formado o falló al persistir).",
578
+ "enum": [
579
+ "sent",
580
+ "already",
581
+ "invalid"
582
+ ],
583
+ "type": "string",
584
+ "x-struct": null,
585
+ "x-validate": null
586
+ }
587
+ },
588
+ "required": [
589
+ "email",
590
+ "status"
591
+ ],
592
+ "type": "object",
593
+ "x-struct": null,
594
+ "x-validate": null
595
+ },
596
+ "type": "array",
597
+ "x-struct": null,
598
+ "x-validate": null
599
+ }
600
+ },
601
+ "required": [
602
+ "data"
603
+ ],
604
+ "title": "BulkInvitationResponse",
605
+ "type": "object",
606
+ "x-struct": "Elixir.HablalaApiWeb.Schemas.BulkInvitationResponse",
607
+ "x-validate": null
608
+ },
296
609
  "ChannelRequest": {
297
610
  "properties": {
298
611
  "channel": {
@@ -1163,52 +1476,205 @@
1163
1476
  }
1164
1477
  },
1165
1478
  "required": [
1166
- "email"
1479
+ "email"
1480
+ ],
1481
+ "type": "object",
1482
+ "x-struct": null,
1483
+ "x-validate": null
1484
+ }
1485
+ },
1486
+ "required": [
1487
+ "invitation"
1488
+ ],
1489
+ "title": "InvitationRequest",
1490
+ "type": "object",
1491
+ "x-struct": "Elixir.HablalaApiWeb.Schemas.InvitationRequest",
1492
+ "x-validate": null
1493
+ },
1494
+ "InvitationResponse": {
1495
+ "properties": {
1496
+ "data": {
1497
+ "$ref": "#/components/schemas/Invitation"
1498
+ }
1499
+ },
1500
+ "required": [
1501
+ "data"
1502
+ ],
1503
+ "title": "InvitationResponse",
1504
+ "type": "object",
1505
+ "x-struct": "Elixir.HablalaApiWeb.Schemas.InvitationResponse",
1506
+ "x-validate": null
1507
+ },
1508
+ "InvitationsResponse": {
1509
+ "properties": {
1510
+ "data": {
1511
+ "items": {
1512
+ "$ref": "#/components/schemas/Invitation"
1513
+ },
1514
+ "type": "array",
1515
+ "x-struct": null,
1516
+ "x-validate": null
1517
+ }
1518
+ },
1519
+ "required": [
1520
+ "data"
1521
+ ],
1522
+ "title": "InvitationsResponse",
1523
+ "type": "object",
1524
+ "x-struct": "Elixir.HablalaApiWeb.Schemas.InvitationsResponse",
1525
+ "x-validate": null
1526
+ },
1527
+ "InviteLink": {
1528
+ "description": "Workspace invite link de una organización. El secreto en claro (`token`/`url`) solo está presente al crearlo o rotarlo; en un `show` posterior es `null`.",
1529
+ "properties": {
1530
+ "expiresAt": {
1531
+ "description": "Caducidad opcional; `null` = no caduca.",
1532
+ "format": "date-time",
1533
+ "nullable": true,
1534
+ "type": "string",
1535
+ "x-struct": null,
1536
+ "x-validate": null
1537
+ },
1538
+ "id": {
1539
+ "type": "string",
1540
+ "x-struct": null,
1541
+ "x-validate": null
1542
+ },
1543
+ "insertedAt": {
1544
+ "format": "date-time",
1545
+ "type": "string",
1546
+ "x-struct": null,
1547
+ "x-validate": null
1548
+ },
1549
+ "prefix": {
1550
+ "description": "Primeros caracteres del secreto (`invl_a1b2…`), para mostrar sin el claro.",
1551
+ "nullable": true,
1552
+ "type": "string",
1553
+ "x-struct": null,
1554
+ "x-validate": null
1555
+ },
1556
+ "role": {
1557
+ "description": "Rol con el que entra quien canjea el link.",
1558
+ "nullable": true,
1559
+ "properties": {
1560
+ "id": {
1561
+ "type": "string",
1562
+ "x-struct": null,
1563
+ "x-validate": null
1564
+ },
1565
+ "name": {
1566
+ "type": "string",
1567
+ "x-struct": null,
1568
+ "x-validate": null
1569
+ }
1570
+ },
1571
+ "type": "object",
1572
+ "x-struct": null,
1573
+ "x-validate": null
1574
+ },
1575
+ "token": {
1576
+ "description": "Secreto en claro (`invl_…`). Presente SOLO al crear/rotar.",
1577
+ "nullable": true,
1578
+ "type": "string",
1579
+ "x-struct": null,
1580
+ "x-validate": null
1581
+ },
1582
+ "updatedAt": {
1583
+ "format": "date-time",
1584
+ "type": "string",
1585
+ "x-struct": null,
1586
+ "x-validate": null
1587
+ },
1588
+ "url": {
1589
+ "description": "URL compartible (`/join?token=…`). Presente SOLO al crear/rotar.",
1590
+ "nullable": true,
1591
+ "type": "string",
1592
+ "x-struct": null,
1593
+ "x-validate": null
1594
+ }
1595
+ },
1596
+ "required": [
1597
+ "id"
1598
+ ],
1599
+ "title": "InviteLink",
1600
+ "type": "object",
1601
+ "x-struct": "Elixir.HablalaApiWeb.Schemas.InviteLink",
1602
+ "x-validate": null
1603
+ },
1604
+ "InviteLinkAcceptRequest": {
1605
+ "properties": {
1606
+ "token": {
1607
+ "description": "Secreto del link (`invl_…`) a canjear.",
1608
+ "type": "string",
1609
+ "x-struct": null,
1610
+ "x-validate": null
1611
+ }
1612
+ },
1613
+ "required": [
1614
+ "token"
1615
+ ],
1616
+ "title": "InviteLinkAcceptRequest",
1617
+ "type": "object",
1618
+ "x-struct": "Elixir.HablalaApiWeb.Schemas.InviteLinkAcceptRequest",
1619
+ "x-validate": null
1620
+ },
1621
+ "InviteLinkPreviewResponse": {
1622
+ "description": "Datos públicos de un invite link para mostrar a qué organización lleva.",
1623
+ "properties": {
1624
+ "data": {
1625
+ "properties": {
1626
+ "organizationAvatarUrl": {
1627
+ "description": "Icono del workspace; `null` usa el fallback de iniciales.",
1628
+ "nullable": true,
1629
+ "type": "string",
1630
+ "x-struct": null,
1631
+ "x-validate": null
1632
+ },
1633
+ "organizationName": {
1634
+ "nullable": true,
1635
+ "type": "string",
1636
+ "x-struct": null,
1637
+ "x-validate": null
1638
+ },
1639
+ "status": {
1640
+ "description": "`valid` (se puede unir) o `invalid` (revocado/caducado/inexistente).",
1641
+ "enum": [
1642
+ "valid",
1643
+ "invalid"
1644
+ ],
1645
+ "type": "string",
1646
+ "x-struct": null,
1647
+ "x-validate": null
1648
+ }
1649
+ },
1650
+ "required": [
1651
+ "status"
1167
1652
  ],
1168
1653
  "type": "object",
1169
1654
  "x-struct": null,
1170
1655
  "x-validate": null
1171
1656
  }
1172
1657
  },
1173
- "required": [
1174
- "invitation"
1175
- ],
1176
- "title": "InvitationRequest",
1177
- "type": "object",
1178
- "x-struct": "Elixir.HablalaApiWeb.Schemas.InvitationRequest",
1179
- "x-validate": null
1180
- },
1181
- "InvitationResponse": {
1182
- "properties": {
1183
- "data": {
1184
- "$ref": "#/components/schemas/Invitation"
1185
- }
1186
- },
1187
1658
  "required": [
1188
1659
  "data"
1189
1660
  ],
1190
- "title": "InvitationResponse",
1661
+ "title": "InviteLinkPreviewResponse",
1191
1662
  "type": "object",
1192
- "x-struct": "Elixir.HablalaApiWeb.Schemas.InvitationResponse",
1663
+ "x-struct": "Elixir.HablalaApiWeb.Schemas.InviteLinkPreviewResponse",
1193
1664
  "x-validate": null
1194
1665
  },
1195
- "InvitationsResponse": {
1666
+ "InviteLinkResponse": {
1196
1667
  "properties": {
1197
1668
  "data": {
1198
- "items": {
1199
- "$ref": "#/components/schemas/Invitation"
1200
- },
1201
- "type": "array",
1202
- "x-struct": null,
1203
- "x-validate": null
1669
+ "$ref": "#/components/schemas/InviteLink"
1204
1670
  }
1205
1671
  },
1206
1672
  "required": [
1207
1673
  "data"
1208
1674
  ],
1209
- "title": "InvitationsResponse",
1675
+ "title": "InviteLinkResponse",
1210
1676
  "type": "object",
1211
- "x-struct": "Elixir.HablalaApiWeb.Schemas.InvitationsResponse",
1677
+ "x-struct": "Elixir.HablalaApiWeb.Schemas.InviteLinkResponse",
1212
1678
  "x-validate": null
1213
1679
  },
1214
1680
  "LogicalQueryRequest": {
@@ -4357,6 +4823,98 @@
4357
4823
  ]
4358
4824
  }
4359
4825
  },
4826
+ "/v1/invite-links/accept": {
4827
+ "post": {
4828
+ "callbacks": {},
4829
+ "description": "Canjea el secreto de un workspace invite link para unirse a la organización con el rol configurado en el link. A diferencia de la invitación por email, no requiere email-match: el link es el secreto.",
4830
+ "operationId": "HablalaApiWeb.Orgs.InviteLinkController.accept",
4831
+ "parameters": [],
4832
+ "requestBody": {
4833
+ "content": {
4834
+ "application/json": {
4835
+ "schema": {
4836
+ "$ref": "#/components/schemas/InviteLinkAcceptRequest"
4837
+ }
4838
+ }
4839
+ },
4840
+ "description": "Secreto del link",
4841
+ "required": false
4842
+ },
4843
+ "responses": {
4844
+ "201": {
4845
+ "content": {
4846
+ "application/json": {
4847
+ "schema": {
4848
+ "$ref": "#/components/schemas/MembershipResponse"
4849
+ }
4850
+ }
4851
+ },
4852
+ "description": "Membresía creada"
4853
+ },
4854
+ "404": {
4855
+ "content": {
4856
+ "application/json": {
4857
+ "schema": {
4858
+ "$ref": "#/components/schemas/Error"
4859
+ }
4860
+ }
4861
+ },
4862
+ "description": "Link inválido, revocado o caducado"
4863
+ },
4864
+ "422": {
4865
+ "content": {
4866
+ "application/json": {
4867
+ "schema": {
4868
+ "$ref": "#/components/schemas/Error"
4869
+ }
4870
+ }
4871
+ },
4872
+ "description": "Ya eres miembro"
4873
+ }
4874
+ },
4875
+ "summary": "Canjear un link de invitación",
4876
+ "tags": [
4877
+ "invite-links"
4878
+ ]
4879
+ }
4880
+ },
4881
+ "/v1/invite-links/preview": {
4882
+ "get": {
4883
+ "callbacks": {},
4884
+ "description": "Devuelve la organización (nombre + icono) a la que lleva un workspace invite link, sin consumirlo ni requerir sesión. Sirve para mostrar a dónde te unes en `/join` antes de confirmar. Siempre 200: el `status` del cuerpo distingue `valid` de `invalid`.",
4885
+ "operationId": "HablalaApiWeb.Orgs.InviteLinkController.preview",
4886
+ "parameters": [
4887
+ {
4888
+ "description": "",
4889
+ "in": "query",
4890
+ "name": "token",
4891
+ "required": true,
4892
+ "schema": {
4893
+ "type": "string",
4894
+ "x-struct": null,
4895
+ "x-validate": null
4896
+ }
4897
+ }
4898
+ ],
4899
+ "responses": {
4900
+ "200": {
4901
+ "content": {
4902
+ "application/json": {
4903
+ "schema": {
4904
+ "$ref": "#/components/schemas/InviteLinkPreviewResponse"
4905
+ }
4906
+ }
4907
+ },
4908
+ "description": "Datos del link"
4909
+ }
4910
+ },
4911
+ "security": [],
4912
+ "summary": "Previsualizar un link de invitación",
4913
+ "tags": [
4914
+ "invite-links"
4915
+ ]
4916
+ }
4917
+ },
4360
4918
  "/v1/organizations": {
4361
4919
  "get": {
4362
4920
  "callbacks": {},
@@ -4470,36 +5028,181 @@
4470
5028
  "content": {
4471
5029
  "application/json": {
4472
5030
  "schema": {
4473
- "$ref": "#/components/schemas/OrganizationResponse"
4474
- }
4475
- }
4476
- },
4477
- "description": "Organización"
4478
- },
4479
- "404": {
4480
- "content": {
4481
- "application/json": {
4482
- "schema": {
4483
- "$ref": "#/components/schemas/Error"
5031
+ "$ref": "#/components/schemas/OrganizationResponse"
5032
+ }
5033
+ }
5034
+ },
5035
+ "description": "Organización"
5036
+ },
5037
+ "404": {
5038
+ "content": {
5039
+ "application/json": {
5040
+ "schema": {
5041
+ "$ref": "#/components/schemas/Error"
5042
+ }
5043
+ }
5044
+ },
5045
+ "description": "No encontrada"
5046
+ }
5047
+ },
5048
+ "summary": "Ver organización",
5049
+ "tags": [
5050
+ "organizations"
5051
+ ]
5052
+ },
5053
+ "patch": {
5054
+ "callbacks": {},
5055
+ "operationId": "HablalaApiWeb.Orgs.OrganizationController.update (2)",
5056
+ "parameters": [
5057
+ {
5058
+ "description": "",
5059
+ "in": "path",
5060
+ "name": "id",
5061
+ "required": true,
5062
+ "schema": {
5063
+ "type": "string",
5064
+ "x-struct": null,
5065
+ "x-validate": null
5066
+ }
5067
+ }
5068
+ ],
5069
+ "requestBody": {
5070
+ "content": {
5071
+ "application/json": {
5072
+ "schema": {
5073
+ "$ref": "#/components/schemas/OrganizationUpdateRequest"
5074
+ }
5075
+ }
5076
+ },
5077
+ "description": "Datos de la organización (el slug es inmutable, no se acepta aquí)",
5078
+ "required": false
5079
+ },
5080
+ "responses": {
5081
+ "200": {
5082
+ "content": {
5083
+ "application/json": {
5084
+ "schema": {
5085
+ "$ref": "#/components/schemas/OrganizationResponse"
5086
+ }
5087
+ }
5088
+ },
5089
+ "description": "Organización actualizada"
5090
+ },
5091
+ "422": {
5092
+ "content": {
5093
+ "application/json": {
5094
+ "schema": {
5095
+ "$ref": "#/components/schemas/Error"
5096
+ }
5097
+ }
5098
+ },
5099
+ "description": "Error de validación"
5100
+ }
5101
+ },
5102
+ "summary": "Actualizar organización",
5103
+ "tags": [
5104
+ "organizations"
5105
+ ]
5106
+ },
5107
+ "put": {
5108
+ "callbacks": {},
5109
+ "operationId": "HablalaApiWeb.Orgs.OrganizationController.update",
5110
+ "parameters": [
5111
+ {
5112
+ "description": "",
5113
+ "in": "path",
5114
+ "name": "id",
5115
+ "required": true,
5116
+ "schema": {
5117
+ "type": "string",
5118
+ "x-struct": null,
5119
+ "x-validate": null
5120
+ }
5121
+ }
5122
+ ],
5123
+ "requestBody": {
5124
+ "content": {
5125
+ "application/json": {
5126
+ "schema": {
5127
+ "$ref": "#/components/schemas/OrganizationUpdateRequest"
5128
+ }
5129
+ }
5130
+ },
5131
+ "description": "Datos de la organización (el slug es inmutable, no se acepta aquí)",
5132
+ "required": false
5133
+ },
5134
+ "responses": {
5135
+ "200": {
5136
+ "content": {
5137
+ "application/json": {
5138
+ "schema": {
5139
+ "$ref": "#/components/schemas/OrganizationResponse"
5140
+ }
5141
+ }
5142
+ },
5143
+ "description": "Organización actualizada"
5144
+ },
5145
+ "422": {
5146
+ "content": {
5147
+ "application/json": {
5148
+ "schema": {
5149
+ "$ref": "#/components/schemas/Error"
5150
+ }
5151
+ }
5152
+ },
5153
+ "description": "Error de validación"
5154
+ }
5155
+ },
5156
+ "summary": "Actualizar organización",
5157
+ "tags": [
5158
+ "organizations"
5159
+ ]
5160
+ }
5161
+ },
5162
+ "/v1/organizations/{organization_id}/access-tokens": {
5163
+ "get": {
5164
+ "callbacks": {},
5165
+ "description": "Los access tokens de máquina de la organización (solo metadatos; el secreto no se recupera).",
5166
+ "operationId": "HablalaApiWeb.AccessTokens.TokenController.index",
5167
+ "parameters": [
5168
+ {
5169
+ "description": "",
5170
+ "in": "path",
5171
+ "name": "organization_id",
5172
+ "required": true,
5173
+ "schema": {
5174
+ "type": "string",
5175
+ "x-struct": null,
5176
+ "x-validate": null
5177
+ }
5178
+ }
5179
+ ],
5180
+ "responses": {
5181
+ "200": {
5182
+ "content": {
5183
+ "application/json": {
5184
+ "schema": {
5185
+ "$ref": "#/components/schemas/AccessTokensResponse"
4484
5186
  }
4485
5187
  }
4486
5188
  },
4487
- "description": "No encontrada"
5189
+ "description": "Tokens"
4488
5190
  }
4489
5191
  },
4490
- "summary": "Ver organización",
5192
+ "summary": "Listar access tokens",
4491
5193
  "tags": [
4492
- "organizations"
5194
+ "access-tokens"
4493
5195
  ]
4494
5196
  },
4495
- "patch": {
5197
+ "post": {
4496
5198
  "callbacks": {},
4497
- "operationId": "HablalaApiWeb.Orgs.OrganizationController.update (2)",
5199
+ "description": "Emite una credencial de máquina (`hpat_…`) con la que un agente (Claude Code / Lá) o un script CI escribe la ontología 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).",
5200
+ "operationId": "HablalaApiWeb.AccessTokens.TokenController.create",
4498
5201
  "parameters": [
4499
5202
  {
4500
5203
  "description": "",
4501
5204
  "in": "path",
4502
- "name": "id",
5205
+ "name": "organization_id",
4503
5206
  "required": true,
4504
5207
  "schema": {
4505
5208
  "type": "string",
@@ -4512,23 +5215,33 @@
4512
5215
  "content": {
4513
5216
  "application/json": {
4514
5217
  "schema": {
4515
- "$ref": "#/components/schemas/OrganizationUpdateRequest"
5218
+ "$ref": "#/components/schemas/AccessTokenRequest"
4516
5219
  }
4517
5220
  }
4518
5221
  },
4519
- "description": "Datos de la organización (el slug es inmutable, no se acepta aquí)",
5222
+ "description": "Datos del token",
4520
5223
  "required": false
4521
5224
  },
4522
5225
  "responses": {
4523
- "200": {
5226
+ "201": {
4524
5227
  "content": {
4525
5228
  "application/json": {
4526
5229
  "schema": {
4527
- "$ref": "#/components/schemas/OrganizationResponse"
5230
+ "$ref": "#/components/schemas/AccessTokenCreatedResponse"
4528
5231
  }
4529
5232
  }
4530
5233
  },
4531
- "description": "Organización actualizada"
5234
+ "description": "Token creado (con secreto)"
5235
+ },
5236
+ "403": {
5237
+ "content": {
5238
+ "application/json": {
5239
+ "schema": {
5240
+ "$ref": "#/components/schemas/Error"
5241
+ }
5242
+ }
5243
+ },
5244
+ "description": "Sin permiso"
4532
5245
  },
4533
5246
  "422": {
4534
5247
  "content": {
@@ -4541,15 +5254,29 @@
4541
5254
  "description": "Error de validación"
4542
5255
  }
4543
5256
  },
4544
- "summary": "Actualizar organización",
5257
+ "summary": "Crear un access token",
4545
5258
  "tags": [
4546
- "organizations"
5259
+ "access-tokens"
4547
5260
  ]
4548
- },
4549
- "put": {
5261
+ }
5262
+ },
5263
+ "/v1/organizations/{organization_id}/access-tokens/{id}": {
5264
+ "delete": {
4550
5265
  "callbacks": {},
4551
- "operationId": "HablalaApiWeb.Orgs.OrganizationController.update",
5266
+ "description": "Revoca la credencial (irreversible). Las requests que la usen dejan de autenticar.",
5267
+ "operationId": "HablalaApiWeb.AccessTokens.TokenController.delete",
4552
5268
  "parameters": [
5269
+ {
5270
+ "description": "",
5271
+ "in": "path",
5272
+ "name": "organization_id",
5273
+ "required": true,
5274
+ "schema": {
5275
+ "type": "string",
5276
+ "x-struct": null,
5277
+ "x-validate": null
5278
+ }
5279
+ },
4553
5280
  {
4554
5281
  "description": "",
4555
5282
  "in": "path",
@@ -4562,29 +5289,11 @@
4562
5289
  }
4563
5290
  }
4564
5291
  ],
4565
- "requestBody": {
4566
- "content": {
4567
- "application/json": {
4568
- "schema": {
4569
- "$ref": "#/components/schemas/OrganizationUpdateRequest"
4570
- }
4571
- }
4572
- },
4573
- "description": "Datos de la organización (el slug es inmutable, no se acepta aquí)",
4574
- "required": false
4575
- },
4576
5292
  "responses": {
4577
- "200": {
4578
- "content": {
4579
- "application/json": {
4580
- "schema": {
4581
- "$ref": "#/components/schemas/OrganizationResponse"
4582
- }
4583
- }
4584
- },
4585
- "description": "Organización actualizada"
5293
+ "204": {
5294
+ "description": "Revocado"
4586
5295
  },
4587
- "422": {
5296
+ "403": {
4588
5297
  "content": {
4589
5298
  "application/json": {
4590
5299
  "schema": {
@@ -4592,12 +5301,12 @@
4592
5301
  }
4593
5302
  }
4594
5303
  },
4595
- "description": "Error de validación"
5304
+ "description": "Sin permiso"
4596
5305
  }
4597
5306
  },
4598
- "summary": "Actualizar organización",
5307
+ "summary": "Revocar un access token",
4599
5308
  "tags": [
4600
- "organizations"
5309
+ "access-tokens"
4601
5310
  ]
4602
5311
  }
4603
5312
  },
@@ -5967,6 +6676,73 @@
5967
6676
  ]
5968
6677
  }
5969
6678
  },
6679
+ "/v1/organizations/{organization_id}/invitations/bulk": {
6680
+ "post": {
6681
+ "callbacks": {},
6682
+ "description": "Crea (o reenvía) invitaciones para una lista de emails, todos con el mismo rol opcional. No es todo-o-nada: cada email se procesa aparte y su resultado se refleja por separado (`sent` / `already` / `invalid`).",
6683
+ "operationId": "HablalaApiWeb.Orgs.InvitationController.bulk",
6684
+ "parameters": [
6685
+ {
6686
+ "description": "",
6687
+ "in": "path",
6688
+ "name": "organization_id",
6689
+ "required": true,
6690
+ "schema": {
6691
+ "type": "string",
6692
+ "x-struct": null,
6693
+ "x-validate": null
6694
+ }
6695
+ }
6696
+ ],
6697
+ "requestBody": {
6698
+ "content": {
6699
+ "application/json": {
6700
+ "schema": {
6701
+ "$ref": "#/components/schemas/BulkInvitationRequest"
6702
+ }
6703
+ }
6704
+ },
6705
+ "description": "Emails a invitar",
6706
+ "required": false
6707
+ },
6708
+ "responses": {
6709
+ "200": {
6710
+ "content": {
6711
+ "application/json": {
6712
+ "schema": {
6713
+ "$ref": "#/components/schemas/BulkInvitationResponse"
6714
+ }
6715
+ }
6716
+ },
6717
+ "description": "Resultado por email"
6718
+ },
6719
+ "403": {
6720
+ "content": {
6721
+ "application/json": {
6722
+ "schema": {
6723
+ "$ref": "#/components/schemas/Error"
6724
+ }
6725
+ }
6726
+ },
6727
+ "description": "Sin permiso"
6728
+ },
6729
+ "422": {
6730
+ "content": {
6731
+ "application/json": {
6732
+ "schema": {
6733
+ "$ref": "#/components/schemas/Error"
6734
+ }
6735
+ }
6736
+ },
6737
+ "description": "Error de validación"
6738
+ }
6739
+ },
6740
+ "summary": "Invitar a varios emails a la vez",
6741
+ "tags": [
6742
+ "invitations"
6743
+ ]
6744
+ }
6745
+ },
5970
6746
  "/v1/organizations/{organization_id}/invitations/{id}": {
5971
6747
  "delete": {
5972
6748
  "callbacks": {},
@@ -6070,6 +6846,134 @@
6070
6846
  ]
6071
6847
  }
6072
6848
  },
6849
+ "/v1/organizations/{organization_id}/invite-link": {
6850
+ "delete": {
6851
+ "callbacks": {},
6852
+ "operationId": "HablalaApiWeb.Orgs.InviteLinkController.delete",
6853
+ "parameters": [
6854
+ {
6855
+ "description": "",
6856
+ "in": "path",
6857
+ "name": "organization_id",
6858
+ "required": true,
6859
+ "schema": {
6860
+ "type": "string",
6861
+ "x-struct": null,
6862
+ "x-validate": null
6863
+ }
6864
+ }
6865
+ ],
6866
+ "responses": {
6867
+ "204": {
6868
+ "description": "Revocado (idempotente: sin link activo también responde 204)"
6869
+ },
6870
+ "403": {
6871
+ "content": {
6872
+ "application/json": {
6873
+ "schema": {
6874
+ "$ref": "#/components/schemas/Error"
6875
+ }
6876
+ }
6877
+ },
6878
+ "description": "Sin permiso"
6879
+ }
6880
+ },
6881
+ "summary": "Revocar el link de invitación",
6882
+ "tags": [
6883
+ "invite-links"
6884
+ ]
6885
+ },
6886
+ "get": {
6887
+ "callbacks": {},
6888
+ "description": "Devuelve el link activo, creándolo si no existe (lazy). El secreto en claro (`url`) solo viaja cuando se acaba de crear; en llamadas siguientes se devuelve el link sin el claro (que no se puede recuperar).",
6889
+ "operationId": "HablalaApiWeb.Orgs.InviteLinkController.show",
6890
+ "parameters": [
6891
+ {
6892
+ "description": "",
6893
+ "in": "path",
6894
+ "name": "organization_id",
6895
+ "required": true,
6896
+ "schema": {
6897
+ "type": "string",
6898
+ "x-struct": null,
6899
+ "x-validate": null
6900
+ }
6901
+ }
6902
+ ],
6903
+ "responses": {
6904
+ "200": {
6905
+ "content": {
6906
+ "application/json": {
6907
+ "schema": {
6908
+ "$ref": "#/components/schemas/InviteLinkResponse"
6909
+ }
6910
+ }
6911
+ },
6912
+ "description": "Link de invitación"
6913
+ },
6914
+ "403": {
6915
+ "content": {
6916
+ "application/json": {
6917
+ "schema": {
6918
+ "$ref": "#/components/schemas/Error"
6919
+ }
6920
+ }
6921
+ },
6922
+ "description": "Sin permiso"
6923
+ }
6924
+ },
6925
+ "summary": "Obtener el link de invitación de la org",
6926
+ "tags": [
6927
+ "invite-links"
6928
+ ]
6929
+ }
6930
+ },
6931
+ "/v1/organizations/{organization_id}/invite-link/rotate": {
6932
+ "post": {
6933
+ "callbacks": {},
6934
+ "description": "Revoca el link activo (si hay) y crea uno nuevo. El secreto anterior deja de canjear al instante; el nuevo claro (`url`) viaja en la respuesta.",
6935
+ "operationId": "HablalaApiWeb.Orgs.InviteLinkController.rotate",
6936
+ "parameters": [
6937
+ {
6938
+ "description": "",
6939
+ "in": "path",
6940
+ "name": "organization_id",
6941
+ "required": true,
6942
+ "schema": {
6943
+ "type": "string",
6944
+ "x-struct": null,
6945
+ "x-validate": null
6946
+ }
6947
+ }
6948
+ ],
6949
+ "responses": {
6950
+ "201": {
6951
+ "content": {
6952
+ "application/json": {
6953
+ "schema": {
6954
+ "$ref": "#/components/schemas/InviteLinkResponse"
6955
+ }
6956
+ }
6957
+ },
6958
+ "description": "Link rotado"
6959
+ },
6960
+ "403": {
6961
+ "content": {
6962
+ "application/json": {
6963
+ "schema": {
6964
+ "$ref": "#/components/schemas/Error"
6965
+ }
6966
+ }
6967
+ },
6968
+ "description": "Sin permiso"
6969
+ }
6970
+ },
6971
+ "summary": "Rotar el link de invitación",
6972
+ "tags": [
6973
+ "invite-links"
6974
+ ]
6975
+ }
6976
+ },
6073
6977
  "/v1/organizations/{organization_id}/memberships": {
6074
6978
  "get": {
6075
6979
  "callbacks": {},