@open-mercato/core 0.10.1-develop.7728.1.6b52eb2849 → 0.10.1-develop.7741.1.61a2652671
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/.turbo/turbo-build.log +1 -1
- package/dist/generated/entities/api_key/index.js +2 -0
- package/dist/generated/entities/api_key/index.js.map +2 -2
- package/dist/generated/entity-fields-registry.js +1 -0
- package/dist/generated/entity-fields-registry.js.map +2 -2
- package/dist/modules/api_keys/data/entities.js +7 -0
- package/dist/modules/api_keys/data/entities.js.map +2 -2
- package/dist/modules/api_keys/migrations/Migration20261012202110_api_keys.js +30 -0
- package/dist/modules/api_keys/migrations/Migration20261012202110_api_keys.js.map +7 -0
- package/dist/modules/api_keys/services/apiKeyService.js +31 -9
- package/dist/modules/api_keys/services/apiKeyService.js.map +2 -2
- package/dist/modules/attachments/lib/ocrService.js +8 -0
- package/dist/modules/attachments/lib/ocrService.js.map +2 -2
- package/dist/modules/auth/api/logout.js +3 -1
- package/dist/modules/auth/api/logout.js.map +2 -2
- package/dist/modules/sales/commands/documents.js +1190 -1408
- package/dist/modules/sales/commands/documents.js.map +2 -2
- package/dist/modules/sales/commands/returns.js +34 -11
- package/dist/modules/sales/commands/returns.js.map +2 -2
- package/dist/modules/sales/commands/shared.js +5 -0
- package/dist/modules/sales/commands/shared.js.map +2 -2
- package/dist/modules/sales/commands/shipments.js +9 -0
- package/dist/modules/sales/commands/shipments.js.map +2 -2
- package/dist/modules/staff/api/timesheets/time-projects/route.js +6 -2
- package/dist/modules/staff/api/timesheets/time-projects/route.js.map +2 -2
- package/dist/modules/workflows/api/portal/tasks/[id]/complete/route.js +1 -1
- package/dist/modules/workflows/api/portal/tasks/[id]/complete/route.js.map +2 -2
- package/dist/modules/workflows/api/tasks/[id]/complete/route.js +7 -1
- package/dist/modules/workflows/api/tasks/[id]/complete/route.js.map +2 -2
- package/dist/modules/workflows/backend/tasks/[id]/page.js +2 -1
- package/dist/modules/workflows/backend/tasks/[id]/page.js.map +2 -2
- package/dist/modules/workflows/lib/instance-status.js +16 -0
- package/dist/modules/workflows/lib/instance-status.js.map +7 -0
- package/dist/modules/workflows/lib/task-handler.js +16 -8
- package/dist/modules/workflows/lib/task-handler.js.map +2 -2
- package/dist/modules/workflows/lib/task-sla.js +10 -0
- package/dist/modules/workflows/lib/task-sla.js.map +2 -2
- package/dist/modules/workflows/lib/workflow-executor.js +29 -1
- package/dist/modules/workflows/lib/workflow-executor.js.map +2 -2
- package/generated/entities/api_key/index.ts +1 -0
- package/generated/entity-fields-registry.ts +1 -0
- package/package.json +7 -7
- package/src/modules/api_keys/data/entities.ts +25 -1
- package/src/modules/api_keys/migrations/.snapshot-open-mercato.json +234 -68
- package/src/modules/api_keys/migrations/Migration20261012202110_api_keys.ts +47 -0
- package/src/modules/api_keys/services/apiKeyService.ts +55 -9
- package/src/modules/attachments/lib/ocrService.ts +8 -0
- package/src/modules/auth/api/logout.ts +3 -1
- package/src/modules/auth/i18n/de.json +3 -0
- package/src/modules/auth/i18n/en.json +3 -0
- package/src/modules/auth/i18n/es.json +3 -0
- package/src/modules/auth/i18n/ko.json +3 -0
- package/src/modules/auth/i18n/pl.json +3 -0
- package/src/modules/sales/commands/documents.ts +1501 -1626
- package/src/modules/sales/commands/returns.ts +41 -14
- package/src/modules/sales/commands/shared.ts +40 -0
- package/src/modules/sales/commands/shipments.ts +10 -0
- package/src/modules/staff/api/timesheets/time-projects/route.ts +6 -2
- package/src/modules/workflows/api/portal/tasks/[id]/complete/route.ts +5 -1
- package/src/modules/workflows/api/tasks/[id]/complete/route.ts +7 -1
- package/src/modules/workflows/backend/tasks/[id]/page.tsx +6 -2
- package/src/modules/workflows/i18n/de.json +2 -0
- package/src/modules/workflows/i18n/en.json +2 -0
- package/src/modules/workflows/i18n/es.json +2 -0
- package/src/modules/workflows/i18n/ko.json +2 -0
- package/src/modules/workflows/i18n/pl.json +2 -0
- package/src/modules/workflows/lib/instance-status.ts +29 -0
- package/src/modules/workflows/lib/task-handler.ts +18 -10
- package/src/modules/workflows/lib/task-sla.ts +13 -0
- package/src/modules/workflows/lib/workflow-executor.ts +44 -1
|
@@ -1,29 +1,63 @@
|
|
|
1
1
|
{
|
|
2
|
+
"name": "public",
|
|
2
3
|
"namespaces": [
|
|
3
4
|
"public"
|
|
4
5
|
],
|
|
5
|
-
"name": "public",
|
|
6
6
|
"tables": [
|
|
7
7
|
{
|
|
8
|
+
"name": "api_keys",
|
|
9
|
+
"schema": "public",
|
|
8
10
|
"columns": {
|
|
9
|
-
"
|
|
10
|
-
"name": "
|
|
11
|
-
"type": "
|
|
11
|
+
"created_at": {
|
|
12
|
+
"name": "created_at",
|
|
13
|
+
"type": "timestamptz(6)",
|
|
12
14
|
"unsigned": false,
|
|
13
15
|
"autoincrement": false,
|
|
14
16
|
"primary": false,
|
|
15
17
|
"nullable": false,
|
|
16
|
-
"
|
|
18
|
+
"unique": false,
|
|
19
|
+
"length": 6,
|
|
20
|
+
"precision": null,
|
|
21
|
+
"scale": null,
|
|
22
|
+
"default": null,
|
|
23
|
+
"comment": null,
|
|
24
|
+
"collation": null,
|
|
25
|
+
"enumItems": [],
|
|
26
|
+
"mappedType": "datetime"
|
|
27
|
+
},
|
|
28
|
+
"created_by": {
|
|
29
|
+
"name": "created_by",
|
|
30
|
+
"type": "uuid",
|
|
31
|
+
"unsigned": false,
|
|
32
|
+
"autoincrement": false,
|
|
33
|
+
"primary": false,
|
|
34
|
+
"nullable": true,
|
|
35
|
+
"unique": false,
|
|
36
|
+
"length": null,
|
|
37
|
+
"precision": null,
|
|
38
|
+
"scale": null,
|
|
39
|
+
"default": null,
|
|
40
|
+
"comment": null,
|
|
41
|
+
"collation": null,
|
|
42
|
+
"enumItems": [],
|
|
17
43
|
"mappedType": "uuid"
|
|
18
44
|
},
|
|
19
|
-
"
|
|
20
|
-
"name": "
|
|
21
|
-
"type": "
|
|
45
|
+
"deleted_at": {
|
|
46
|
+
"name": "deleted_at",
|
|
47
|
+
"type": "timestamptz(6)",
|
|
22
48
|
"unsigned": false,
|
|
23
49
|
"autoincrement": false,
|
|
24
50
|
"primary": false,
|
|
25
|
-
"nullable":
|
|
26
|
-
"
|
|
51
|
+
"nullable": true,
|
|
52
|
+
"unique": false,
|
|
53
|
+
"length": 6,
|
|
54
|
+
"precision": null,
|
|
55
|
+
"scale": null,
|
|
56
|
+
"default": null,
|
|
57
|
+
"comment": null,
|
|
58
|
+
"collation": null,
|
|
59
|
+
"enumItems": [],
|
|
60
|
+
"mappedType": "datetime"
|
|
27
61
|
},
|
|
28
62
|
"description": {
|
|
29
63
|
"name": "description",
|
|
@@ -32,24 +66,48 @@
|
|
|
32
66
|
"autoincrement": false,
|
|
33
67
|
"primary": false,
|
|
34
68
|
"nullable": true,
|
|
69
|
+
"unique": false,
|
|
70
|
+
"length": null,
|
|
71
|
+
"precision": null,
|
|
72
|
+
"scale": null,
|
|
73
|
+
"default": null,
|
|
74
|
+
"comment": null,
|
|
75
|
+
"collation": null,
|
|
76
|
+
"enumItems": [],
|
|
35
77
|
"mappedType": "text"
|
|
36
78
|
},
|
|
37
|
-
"
|
|
38
|
-
"name": "
|
|
39
|
-
"type": "
|
|
79
|
+
"expires_at": {
|
|
80
|
+
"name": "expires_at",
|
|
81
|
+
"type": "timestamptz(6)",
|
|
40
82
|
"unsigned": false,
|
|
41
83
|
"autoincrement": false,
|
|
42
84
|
"primary": false,
|
|
43
85
|
"nullable": true,
|
|
44
|
-
"
|
|
86
|
+
"unique": false,
|
|
87
|
+
"length": 6,
|
|
88
|
+
"precision": null,
|
|
89
|
+
"scale": null,
|
|
90
|
+
"default": null,
|
|
91
|
+
"comment": null,
|
|
92
|
+
"collation": null,
|
|
93
|
+
"enumItems": [],
|
|
94
|
+
"mappedType": "datetime"
|
|
45
95
|
},
|
|
46
|
-
"
|
|
47
|
-
"name": "
|
|
96
|
+
"id": {
|
|
97
|
+
"name": "id",
|
|
48
98
|
"type": "uuid",
|
|
49
99
|
"unsigned": false,
|
|
50
100
|
"autoincrement": false,
|
|
51
|
-
"primary":
|
|
52
|
-
"nullable":
|
|
101
|
+
"primary": true,
|
|
102
|
+
"nullable": false,
|
|
103
|
+
"unique": false,
|
|
104
|
+
"length": null,
|
|
105
|
+
"precision": null,
|
|
106
|
+
"scale": null,
|
|
107
|
+
"default": "gen_random_uuid()",
|
|
108
|
+
"comment": null,
|
|
109
|
+
"collation": null,
|
|
110
|
+
"enumItems": [],
|
|
53
111
|
"mappedType": "uuid"
|
|
54
112
|
},
|
|
55
113
|
"key_hash": {
|
|
@@ -59,6 +117,14 @@
|
|
|
59
117
|
"autoincrement": false,
|
|
60
118
|
"primary": false,
|
|
61
119
|
"nullable": false,
|
|
120
|
+
"unique": false,
|
|
121
|
+
"length": null,
|
|
122
|
+
"precision": null,
|
|
123
|
+
"scale": null,
|
|
124
|
+
"default": null,
|
|
125
|
+
"comment": null,
|
|
126
|
+
"collation": null,
|
|
127
|
+
"enumItems": [],
|
|
62
128
|
"mappedType": "text"
|
|
63
129
|
},
|
|
64
130
|
"key_prefix": {
|
|
@@ -68,44 +134,101 @@
|
|
|
68
134
|
"autoincrement": false,
|
|
69
135
|
"primary": false,
|
|
70
136
|
"nullable": false,
|
|
137
|
+
"unique": true,
|
|
138
|
+
"length": null,
|
|
139
|
+
"precision": null,
|
|
140
|
+
"scale": null,
|
|
141
|
+
"default": null,
|
|
142
|
+
"comment": null,
|
|
143
|
+
"collation": null,
|
|
144
|
+
"enumItems": [],
|
|
71
145
|
"mappedType": "text"
|
|
72
146
|
},
|
|
73
|
-
"
|
|
74
|
-
"name": "
|
|
75
|
-
"type": "
|
|
147
|
+
"last_used_at": {
|
|
148
|
+
"name": "last_used_at",
|
|
149
|
+
"type": "timestamptz(6)",
|
|
76
150
|
"unsigned": false,
|
|
77
151
|
"autoincrement": false,
|
|
78
152
|
"primary": false,
|
|
79
153
|
"nullable": true,
|
|
80
|
-
"
|
|
154
|
+
"unique": false,
|
|
155
|
+
"length": 6,
|
|
156
|
+
"precision": null,
|
|
157
|
+
"scale": null,
|
|
158
|
+
"default": null,
|
|
159
|
+
"comment": null,
|
|
160
|
+
"collation": null,
|
|
161
|
+
"enumItems": [],
|
|
162
|
+
"mappedType": "datetime"
|
|
81
163
|
},
|
|
82
|
-
"
|
|
83
|
-
"name": "
|
|
84
|
-
"type": "
|
|
164
|
+
"name": {
|
|
165
|
+
"name": "name",
|
|
166
|
+
"type": "text",
|
|
85
167
|
"unsigned": false,
|
|
86
168
|
"autoincrement": false,
|
|
87
169
|
"primary": false,
|
|
88
|
-
"nullable":
|
|
89
|
-
"
|
|
170
|
+
"nullable": false,
|
|
171
|
+
"unique": false,
|
|
172
|
+
"length": null,
|
|
173
|
+
"precision": null,
|
|
174
|
+
"scale": null,
|
|
175
|
+
"default": null,
|
|
176
|
+
"comment": null,
|
|
177
|
+
"collation": null,
|
|
178
|
+
"enumItems": [],
|
|
179
|
+
"mappedType": "text"
|
|
90
180
|
},
|
|
91
|
-
"
|
|
92
|
-
"name": "
|
|
181
|
+
"opencode_session_id": {
|
|
182
|
+
"name": "opencode_session_id",
|
|
93
183
|
"type": "text",
|
|
94
184
|
"unsigned": false,
|
|
95
185
|
"autoincrement": false,
|
|
96
186
|
"primary": false,
|
|
97
187
|
"nullable": true,
|
|
188
|
+
"unique": false,
|
|
189
|
+
"length": null,
|
|
190
|
+
"precision": null,
|
|
191
|
+
"scale": null,
|
|
192
|
+
"default": null,
|
|
193
|
+
"comment": null,
|
|
194
|
+
"collation": null,
|
|
195
|
+
"enumItems": [],
|
|
98
196
|
"mappedType": "text"
|
|
99
197
|
},
|
|
100
|
-
"
|
|
101
|
-
"name": "
|
|
198
|
+
"organization_id": {
|
|
199
|
+
"name": "organization_id",
|
|
102
200
|
"type": "uuid",
|
|
103
201
|
"unsigned": false,
|
|
104
202
|
"autoincrement": false,
|
|
105
203
|
"primary": false,
|
|
106
204
|
"nullable": true,
|
|
205
|
+
"unique": false,
|
|
206
|
+
"length": null,
|
|
207
|
+
"precision": null,
|
|
208
|
+
"scale": null,
|
|
209
|
+
"default": null,
|
|
210
|
+
"comment": null,
|
|
211
|
+
"collation": null,
|
|
212
|
+
"enumItems": [],
|
|
107
213
|
"mappedType": "uuid"
|
|
108
214
|
},
|
|
215
|
+
"roles_json": {
|
|
216
|
+
"name": "roles_json",
|
|
217
|
+
"type": "jsonb",
|
|
218
|
+
"unsigned": false,
|
|
219
|
+
"autoincrement": false,
|
|
220
|
+
"primary": false,
|
|
221
|
+
"nullable": true,
|
|
222
|
+
"unique": false,
|
|
223
|
+
"length": null,
|
|
224
|
+
"precision": null,
|
|
225
|
+
"scale": null,
|
|
226
|
+
"default": null,
|
|
227
|
+
"comment": null,
|
|
228
|
+
"collation": null,
|
|
229
|
+
"enumItems": [],
|
|
230
|
+
"mappedType": "json"
|
|
231
|
+
},
|
|
109
232
|
"session_secret_encrypted": {
|
|
110
233
|
"name": "session_secret_encrypted",
|
|
111
234
|
"type": "text",
|
|
@@ -113,88 +236,129 @@
|
|
|
113
236
|
"autoincrement": false,
|
|
114
237
|
"primary": false,
|
|
115
238
|
"nullable": true,
|
|
239
|
+
"unique": false,
|
|
240
|
+
"length": null,
|
|
241
|
+
"precision": null,
|
|
242
|
+
"scale": null,
|
|
243
|
+
"default": null,
|
|
244
|
+
"comment": null,
|
|
245
|
+
"collation": null,
|
|
246
|
+
"enumItems": [],
|
|
116
247
|
"mappedType": "text"
|
|
117
248
|
},
|
|
118
|
-
"
|
|
119
|
-
"name": "
|
|
249
|
+
"session_token": {
|
|
250
|
+
"name": "session_token",
|
|
120
251
|
"type": "text",
|
|
121
252
|
"unsigned": false,
|
|
122
253
|
"autoincrement": false,
|
|
123
254
|
"primary": false,
|
|
124
255
|
"nullable": true,
|
|
256
|
+
"unique": false,
|
|
257
|
+
"length": null,
|
|
258
|
+
"precision": null,
|
|
259
|
+
"scale": null,
|
|
260
|
+
"default": null,
|
|
261
|
+
"comment": null,
|
|
262
|
+
"collation": null,
|
|
263
|
+
"enumItems": [],
|
|
125
264
|
"mappedType": "text"
|
|
126
265
|
},
|
|
127
|
-
"
|
|
128
|
-
"name": "
|
|
129
|
-
"type": "
|
|
266
|
+
"session_token_hash": {
|
|
267
|
+
"name": "session_token_hash",
|
|
268
|
+
"type": "text",
|
|
130
269
|
"unsigned": false,
|
|
131
270
|
"autoincrement": false,
|
|
132
271
|
"primary": false,
|
|
133
272
|
"nullable": true,
|
|
134
|
-
"
|
|
135
|
-
"
|
|
273
|
+
"unique": false,
|
|
274
|
+
"length": null,
|
|
275
|
+
"precision": null,
|
|
276
|
+
"scale": null,
|
|
277
|
+
"default": null,
|
|
278
|
+
"comment": null,
|
|
279
|
+
"collation": null,
|
|
280
|
+
"enumItems": [],
|
|
281
|
+
"mappedType": "text"
|
|
136
282
|
},
|
|
137
|
-
"
|
|
138
|
-
"name": "
|
|
139
|
-
"type": "
|
|
283
|
+
"session_user_id": {
|
|
284
|
+
"name": "session_user_id",
|
|
285
|
+
"type": "uuid",
|
|
140
286
|
"unsigned": false,
|
|
141
287
|
"autoincrement": false,
|
|
142
288
|
"primary": false,
|
|
143
289
|
"nullable": true,
|
|
144
|
-
"
|
|
145
|
-
"
|
|
290
|
+
"unique": false,
|
|
291
|
+
"length": null,
|
|
292
|
+
"precision": null,
|
|
293
|
+
"scale": null,
|
|
294
|
+
"default": null,
|
|
295
|
+
"comment": null,
|
|
296
|
+
"collation": null,
|
|
297
|
+
"enumItems": [],
|
|
298
|
+
"mappedType": "uuid"
|
|
146
299
|
},
|
|
147
|
-
"
|
|
148
|
-
"name": "
|
|
149
|
-
"type": "
|
|
300
|
+
"tenant_id": {
|
|
301
|
+
"name": "tenant_id",
|
|
302
|
+
"type": "uuid",
|
|
150
303
|
"unsigned": false,
|
|
151
304
|
"autoincrement": false,
|
|
152
305
|
"primary": false,
|
|
153
|
-
"nullable":
|
|
154
|
-
"
|
|
155
|
-
"
|
|
306
|
+
"nullable": true,
|
|
307
|
+
"unique": false,
|
|
308
|
+
"length": null,
|
|
309
|
+
"precision": null,
|
|
310
|
+
"scale": null,
|
|
311
|
+
"default": null,
|
|
312
|
+
"comment": null,
|
|
313
|
+
"collation": null,
|
|
314
|
+
"enumItems": [],
|
|
315
|
+
"mappedType": "uuid"
|
|
156
316
|
},
|
|
157
317
|
"updated_at": {
|
|
158
318
|
"name": "updated_at",
|
|
159
|
-
"type": "timestamptz",
|
|
160
|
-
"unsigned": false,
|
|
161
|
-
"autoincrement": false,
|
|
162
|
-
"primary": false,
|
|
163
|
-
"nullable": true,
|
|
164
|
-
"length": 6,
|
|
165
|
-
"mappedType": "datetime"
|
|
166
|
-
},
|
|
167
|
-
"deleted_at": {
|
|
168
|
-
"name": "deleted_at",
|
|
169
|
-
"type": "timestamptz",
|
|
319
|
+
"type": "timestamptz(6)",
|
|
170
320
|
"unsigned": false,
|
|
171
321
|
"autoincrement": false,
|
|
172
322
|
"primary": false,
|
|
173
323
|
"nullable": true,
|
|
324
|
+
"unique": false,
|
|
174
325
|
"length": 6,
|
|
326
|
+
"precision": null,
|
|
327
|
+
"scale": null,
|
|
328
|
+
"default": null,
|
|
329
|
+
"comment": null,
|
|
330
|
+
"collation": null,
|
|
331
|
+
"enumItems": [],
|
|
175
332
|
"mappedType": "datetime"
|
|
176
333
|
}
|
|
177
334
|
},
|
|
178
|
-
"name": "api_keys",
|
|
179
|
-
"schema": "public",
|
|
180
335
|
"indexes": [
|
|
181
336
|
{
|
|
182
|
-
"keyName": "api_keys_key_prefix_unique",
|
|
183
337
|
"columnNames": [
|
|
184
338
|
"key_prefix"
|
|
185
339
|
],
|
|
186
340
|
"composite": false,
|
|
187
341
|
"constraint": true,
|
|
342
|
+
"keyName": "api_keys_key_prefix_unique",
|
|
188
343
|
"primary": false,
|
|
189
344
|
"unique": true
|
|
190
345
|
},
|
|
191
346
|
{
|
|
192
|
-
"
|
|
347
|
+
"columnNames": [],
|
|
348
|
+
"composite": false,
|
|
349
|
+
"constraint": false,
|
|
350
|
+
"keyName": "api_keys_opencode_session_id_uq",
|
|
351
|
+
"primary": false,
|
|
352
|
+
"unique": false,
|
|
353
|
+
"expression": "create unique index \"api_keys_opencode_session_id_uq\" on \"api_keys\" (\"opencode_session_id\") where \"opencode_session_id\" is not null and \"deleted_at\" is null"
|
|
354
|
+
},
|
|
355
|
+
{
|
|
193
356
|
"columnNames": [
|
|
194
357
|
"id"
|
|
195
358
|
],
|
|
196
359
|
"composite": false,
|
|
197
360
|
"constraint": true,
|
|
361
|
+
"keyName": "api_keys_pkey",
|
|
198
362
|
"primary": true,
|
|
199
363
|
"unique": true
|
|
200
364
|
},
|
|
@@ -202,16 +366,18 @@
|
|
|
202
366
|
"columnNames": [],
|
|
203
367
|
"composite": false,
|
|
204
368
|
"constraint": false,
|
|
205
|
-
"keyName": "
|
|
369
|
+
"keyName": "api_keys_session_token_hash_uq",
|
|
206
370
|
"primary": false,
|
|
207
371
|
"unique": false,
|
|
208
|
-
"expression": "create unique index \"
|
|
372
|
+
"expression": "create unique index \"api_keys_session_token_hash_uq\" on \"api_keys\" (\"session_token_hash\") where \"session_token_hash\" is not null and \"deleted_at\" is null"
|
|
209
373
|
}
|
|
210
374
|
],
|
|
211
375
|
"checks": [],
|
|
376
|
+
"triggers": [],
|
|
212
377
|
"foreignKeys": {},
|
|
213
|
-
"
|
|
378
|
+
"comment": null
|
|
214
379
|
}
|
|
215
380
|
],
|
|
381
|
+
"views": [],
|
|
216
382
|
"nativeEnums": {}
|
|
217
|
-
}
|
|
383
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { Migration } from '@mikro-orm/migrations';
|
|
2
|
+
|
|
3
|
+
export class Migration20261012202110_api_keys extends Migration {
|
|
4
|
+
|
|
5
|
+
override name = 'Migration20261012202110';
|
|
6
|
+
|
|
7
|
+
override async up(): Promise<void> {
|
|
8
|
+
this.addSql(`alter table "api_keys" add "session_token_hash" text null;`);
|
|
9
|
+
|
|
10
|
+
// Backfill before switching any lookup to the hash column (#2254). AI-chat session keys
|
|
11
|
+
// carry a 2-hour TTL and mint once per conversation (ai_assistant chat route, `!sessionId`
|
|
12
|
+
// branch) — a session already in flight when this deploys must keep resolving, or the
|
|
13
|
+
// conversation silently loses its MCP tool-call auth with no automatic re-mint. `sha256()`
|
|
14
|
+
// is a PostgreSQL 13+ built-in (no pgcrypto dependency) and matches
|
|
15
|
+
// `hashSessionToken`'s `createHash('sha256').update(token).digest('hex')` byte for byte.
|
|
16
|
+
this.addSql(`
|
|
17
|
+
update "api_keys"
|
|
18
|
+
set "session_token_hash" = encode(sha256("session_token"::bytea), 'hex')
|
|
19
|
+
where "session_token" is not null and "session_token_hash" is null;
|
|
20
|
+
`);
|
|
21
|
+
|
|
22
|
+
// Legacy rows embedded the raw token in `name` (`__session_<token>__`, for operator
|
|
23
|
+
// visibility in an admin listing) — the same credential leaking a second way. Replace it
|
|
24
|
+
// with a hash-derived, non-secret label before the token itself is cleared below.
|
|
25
|
+
this.addSql(`
|
|
26
|
+
update "api_keys"
|
|
27
|
+
set "name" = '__session_' || substr("session_token_hash", 1, 12) || '__'
|
|
28
|
+
where "session_token" is not null and "name" = '__session_' || "session_token" || '__';
|
|
29
|
+
`);
|
|
30
|
+
|
|
31
|
+
// Stop storing the plaintext bearer credential. The column is kept, not dropped, per the
|
|
32
|
+
// "renamed/removed DB column" rule in `.ai/review-checklist.md` (keep old column, add new
|
|
33
|
+
// one, backfill, deprecate old) — see `ApiKey.sessionToken`'s `@deprecated` doc comment for
|
|
34
|
+
// the removal criteria for a later migration.
|
|
35
|
+
this.addSql(`update "api_keys" set "session_token" = null where "session_token" is not null;`);
|
|
36
|
+
|
|
37
|
+
this.addSql(`create unique index "api_keys_session_token_hash_uq" on "api_keys" ("session_token_hash") where "session_token_hash" is not null and "deleted_at" is null;`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
override down(): void | Promise<void> {
|
|
41
|
+
// Schema-only reversal. The plaintext `session_token` values cleared above cannot be
|
|
42
|
+
// restored — that data loss is the point of the migration, not an oversight.
|
|
43
|
+
this.addSql(`drop index "api_keys_session_token_hash_uq";`);
|
|
44
|
+
this.addSql(`alter table "api_keys" drop column "session_token_hash";`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { randomBytes } from 'node:crypto'
|
|
1
|
+
import { randomBytes, createHash } from 'node:crypto'
|
|
2
2
|
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
3
3
|
import { hash, compare } from 'bcryptjs'
|
|
4
4
|
import type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'
|
|
@@ -227,6 +227,17 @@ export function generateSessionToken(): string {
|
|
|
227
227
|
return `sess_${randomBytes(16).toString('hex')}`
|
|
228
228
|
}
|
|
229
229
|
|
|
230
|
+
/**
|
|
231
|
+
* Hash a session token for storage/lookup (#2254). The session token is a bearer credential —
|
|
232
|
+
* it unlocks the decrypted API key secret and the user's ACL on the MCP server — so, mirroring
|
|
233
|
+
* `customer_accounts`' `hashToken`, only its hash is ever persisted. 128 bits of entropy from
|
|
234
|
+
* `generateSessionToken` is sufficient for a plain SHA-256 (no bcrypt cost needed, and a slow
|
|
235
|
+
* hash would just add latency to every MCP tool call that resolves a session).
|
|
236
|
+
*/
|
|
237
|
+
export function hashSessionToken(sessionToken: string): string {
|
|
238
|
+
return createHash('sha256').update(sessionToken).digest('hex')
|
|
239
|
+
}
|
|
240
|
+
|
|
230
241
|
/**
|
|
231
242
|
* Create an ephemeral API key scoped to a chat session.
|
|
232
243
|
* The key inherits the user's roles and expires after ttlMinutes (default 30).
|
|
@@ -244,12 +255,16 @@ export async function createSessionApiKey(
|
|
|
244
255
|
// Encrypt the secret for later retrieval (used by MCP server for API calls)
|
|
245
256
|
const encryptedSecret = await encryptSessionSecret(secret, input.tenantId ?? null)
|
|
246
257
|
|
|
258
|
+
// Hash before storage (#2254) — the row (and its `name` column, previously embedding the raw
|
|
259
|
+
// token for operator visibility) must never carry the live bearer credential in the clear.
|
|
260
|
+
const sessionTokenHash = hashSessionToken(input.sessionToken)
|
|
261
|
+
|
|
247
262
|
const roleIds = Array.from(new Set(input.userRoles.filter(Boolean))).sort((left, right) => left.localeCompare(right))
|
|
248
263
|
let record!: ApiKey
|
|
249
264
|
await withAtomicFlush(em, [async () => {
|
|
250
265
|
await lockRoleWriterAuthorizationState(em, roleIds)
|
|
251
266
|
record = em.create(ApiKey, {
|
|
252
|
-
name: `__session_${
|
|
267
|
+
name: `__session_${sessionTokenHash.slice(0, 12)}__`,
|
|
253
268
|
description: 'Ephemeral session API key for AI chat',
|
|
254
269
|
tenantId: input.tenantId ?? null,
|
|
255
270
|
organizationId: input.organizationId ?? null,
|
|
@@ -257,7 +272,7 @@ export async function createSessionApiKey(
|
|
|
257
272
|
keyPrefix: prefix,
|
|
258
273
|
rolesJson: roleIds,
|
|
259
274
|
createdBy: input.userId,
|
|
260
|
-
|
|
275
|
+
sessionTokenHash,
|
|
261
276
|
sessionUserId: input.userId,
|
|
262
277
|
sessionSecretEncrypted: encryptedSecret,
|
|
263
278
|
expiresAt,
|
|
@@ -283,10 +298,26 @@ export async function findApiKeyBySessionToken(
|
|
|
283
298
|
): Promise<ApiKey | null> {
|
|
284
299
|
if (!sessionToken) return null
|
|
285
300
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
301
|
+
// Looked up by hash, never by the raw token (#2254).
|
|
302
|
+
const sessionTokenHash = hashSessionToken(sessionToken)
|
|
303
|
+
let record = await em.findOne(ApiKey, { sessionTokenHash, deletedAt: null })
|
|
304
|
+
|
|
305
|
+
if (!record) {
|
|
306
|
+
// Version-skew fallback (#7304): during a rolling deploy/rollback window the app and the
|
|
307
|
+
// separate MCP process can briefly run different code versions. A row minted by
|
|
308
|
+
// pre-#2254 code in that window carries only the plaintext token (sessionTokenHash is
|
|
309
|
+
// still null) — fall back to it so the session still resolves instead of answering a
|
|
310
|
+
// false SESSION_EXPIRED, then self-heal the row onto the hash immediately so this is the
|
|
311
|
+
// only lookup that ever sees the plaintext, and the window closes as soon as each
|
|
312
|
+
// outstanding legacy row is touched once.
|
|
313
|
+
const legacyRecord = await em.findOne(ApiKey, { sessionToken, deletedAt: null })
|
|
314
|
+
if (legacyRecord) {
|
|
315
|
+
legacyRecord.sessionTokenHash = sessionTokenHash
|
|
316
|
+
legacyRecord.sessionToken = null
|
|
317
|
+
await em.persist(legacyRecord).flush()
|
|
318
|
+
record = legacyRecord
|
|
319
|
+
}
|
|
320
|
+
}
|
|
290
321
|
|
|
291
322
|
if (!record) return null
|
|
292
323
|
if (record.expiresAt && record.expiresAt.getTime() < Date.now()) return null
|
|
@@ -392,14 +423,29 @@ export async function deleteSessionApiKey(
|
|
|
392
423
|
em: EntityManager,
|
|
393
424
|
sessionToken: string
|
|
394
425
|
): Promise<void> {
|
|
395
|
-
|
|
426
|
+
// Looked up and re-checked by hash, never by the raw token (#2254). Deliberately does not
|
|
427
|
+
// call findApiKeyBySessionToken: that helper also drops already-expired rows, but deleting
|
|
428
|
+
// an expired-but-not-yet-cleaned-up row must still work here.
|
|
429
|
+
const sessionTokenHash = hashSessionToken(sessionToken)
|
|
430
|
+
let candidate = await em.findOne(ApiKey, { sessionTokenHash, deletedAt: null })
|
|
431
|
+
if (!candidate) {
|
|
432
|
+
// Version-skew fallback (#7304): see findApiKeyBySessionToken for the full rationale — a
|
|
433
|
+
// row minted by pre-#2254 code during a deploy window is plaintext-only until touched.
|
|
434
|
+
const legacyCandidate = await em.findOne(ApiKey, { sessionToken, deletedAt: null })
|
|
435
|
+
if (legacyCandidate) {
|
|
436
|
+
legacyCandidate.sessionTokenHash = sessionTokenHash
|
|
437
|
+
legacyCandidate.sessionToken = null
|
|
438
|
+
await em.persist(legacyCandidate).flush()
|
|
439
|
+
candidate = legacyCandidate
|
|
440
|
+
}
|
|
441
|
+
}
|
|
396
442
|
if (!candidate) return
|
|
397
443
|
let deletedId: string | null = null
|
|
398
444
|
await withAtomicFlush(em, [async () => {
|
|
399
445
|
const records = await lockAuthorizationApiKeyRows(em, [String(candidate.id)])
|
|
400
446
|
const record = records.find((entry) => (
|
|
401
447
|
String(entry.id) === String(candidate.id)
|
|
402
|
-
&& entry.
|
|
448
|
+
&& entry.sessionTokenHash === sessionTokenHash
|
|
403
449
|
&& !entry.deletedAt
|
|
404
450
|
))
|
|
405
451
|
if (!record) return
|
|
@@ -155,6 +155,14 @@ export class OcrService {
|
|
|
155
155
|
guidance = 'Rate limit exceeded. Please try again later.'
|
|
156
156
|
break
|
|
157
157
|
default:
|
|
158
|
+
if (err?.name === 'TimeoutError') {
|
|
159
|
+
guidance = `OCR request timed out: ${rawMessage}`
|
|
160
|
+
break
|
|
161
|
+
}
|
|
162
|
+
if (err?.name === 'AbortError') {
|
|
163
|
+
guidance = `OCR request was aborted: ${rawMessage}`
|
|
164
|
+
break
|
|
165
|
+
}
|
|
158
166
|
guidance = rawMessage.includes('https://')
|
|
159
167
|
? rawMessage
|
|
160
168
|
: `${rawMessage}. Check OPENAI_API_KEY.`
|
|
@@ -86,6 +86,8 @@ export async function POST(req: Request) {
|
|
|
86
86
|
const res = buildSafeRedirectResponse(req, '/login')
|
|
87
87
|
res.cookies.set('auth_token', '', { path: '/', maxAge: 0 })
|
|
88
88
|
res.cookies.set('session_token', '', { path: '/', maxAge: 0 })
|
|
89
|
+
res.cookies.set('om_selected_org', '', { path: '/', maxAge: 0 })
|
|
90
|
+
res.cookies.set('om_selected_tenant', '', { path: '/', maxAge: 0 })
|
|
89
91
|
return res
|
|
90
92
|
}
|
|
91
93
|
|
|
@@ -99,7 +101,7 @@ export const openApi: OpenApiRouteDoc = {
|
|
|
99
101
|
methods: {
|
|
100
102
|
POST: {
|
|
101
103
|
summary: 'Invalidate session and redirect',
|
|
102
|
-
description: 'Revokes the session identified by the `auth_token` sid claim and the `session_token` cookie, clears both cookies, and redirects the browser to the login page. A valid access token is not required, so a logout submitted after the access token expired still revokes the remember-me refresh session. A request that carries neither cookie is only redirected.',
|
|
104
|
+
description: 'Revokes the session identified by the `auth_token` sid claim and the `session_token` cookie, clears both cookies together with the organization-scope cookies (`om_selected_org`, `om_selected_tenant`), and redirects the browser to the login page. A valid access token is not required, so a logout submitted after the access token expired still revokes the remember-me refresh session. A request that carries neither session cookie is only redirected.',
|
|
103
105
|
responses: [
|
|
104
106
|
{ status: 307, description: 'Redirect to login after successful logout', mediaType: 'text/html' },
|
|
105
107
|
],
|
|
@@ -228,6 +228,8 @@
|
|
|
228
228
|
"auth.acl.features.forms.submissions.export": "Einreichungsdaten exportieren (Datenübertragbarkeit, DSGVO Art. 15/20)",
|
|
229
229
|
"auth.acl.features.forms.submissions.manage": "Einreichungen verwalten (erneut öffnen, Beteiligte zuweisen, exportieren)",
|
|
230
230
|
"auth.acl.features.forms.view": "Formulare und Einreichungen ansehen",
|
|
231
|
+
"auth.acl.features.gateway_autopay.configure": "Configure Autopay gateway settings",
|
|
232
|
+
"auth.acl.features.gateway_autopay.view": "View Autopay gateway configuration",
|
|
231
233
|
"auth.acl.features.gateway_stripe.configure": "Configure Stripe gateway settings",
|
|
232
234
|
"auth.acl.features.gateway_stripe.view": "View Stripe gateway configuration",
|
|
233
235
|
"auth.acl.features.gateway_tpay.configure": "Tpay-Gateway-Einstellungen konfigurieren",
|
|
@@ -459,6 +461,7 @@
|
|
|
459
461
|
"auth.acl.modules.example": "Example Module",
|
|
460
462
|
"auth.acl.modules.example_customers_sync": "Example Customers Sync",
|
|
461
463
|
"auth.acl.modules.feature_toggles": "Feature Toggles",
|
|
464
|
+
"auth.acl.modules.gateway_autopay": "Autopay Payment Gateway",
|
|
462
465
|
"auth.acl.modules.gateway_stripe": "Stripe Payment Gateway",
|
|
463
466
|
"auth.acl.modules.gateway_tpay": "Tpay-Zahlungsgateway",
|
|
464
467
|
"auth.acl.modules.inbox_ops": "InboxOps — Email-to-ERP Agent",
|