@open-mercato/core 0.10.1-develop.7736.1.88e07906bf → 0.10.1-develop.7748.1.d745ba61aa

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.
Files changed (198) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/generated/entities/api_key/index.js +2 -0
  3. package/dist/generated/entities/api_key/index.js.map +2 -2
  4. package/dist/generated/entities/attachment_partition/index.js +2 -0
  5. package/dist/generated/entities/attachment_partition/index.js.map +2 -2
  6. package/dist/generated/entity-fields-registry.js +2 -0
  7. package/dist/generated/entity-fields-registry.js.map +2 -2
  8. package/dist/helpers/integration/attachmentAccessFixtures.js +144 -0
  9. package/dist/helpers/integration/attachmentAccessFixtures.js.map +7 -0
  10. package/dist/modules/api_keys/data/entities.js +7 -0
  11. package/dist/modules/api_keys/data/entities.js.map +2 -2
  12. package/dist/modules/api_keys/migrations/Migration20261012202110_api_keys.js +30 -0
  13. package/dist/modules/api_keys/migrations/Migration20261012202110_api_keys.js.map +7 -0
  14. package/dist/modules/api_keys/services/apiKeyService.js +31 -9
  15. package/dist/modules/api_keys/services/apiKeyService.js.map +2 -2
  16. package/dist/modules/attachments/api/file/[id]/route.js +16 -10
  17. package/dist/modules/attachments/api/file/[id]/route.js.map +2 -2
  18. package/dist/modules/attachments/api/image/[id]/[[...slug]]/route.js +15 -9
  19. package/dist/modules/attachments/api/image/[id]/[[...slug]]/route.js.map +2 -2
  20. package/dist/modules/attachments/api/library/[id]/route.js +100 -56
  21. package/dist/modules/attachments/api/library/[id]/route.js.map +3 -3
  22. package/dist/modules/attachments/api/library/route.js +56 -17
  23. package/dist/modules/attachments/api/library/route.js.map +2 -2
  24. package/dist/modules/attachments/api/partitions/route.js +2 -0
  25. package/dist/modules/attachments/api/partitions/route.js.map +2 -2
  26. package/dist/modules/attachments/api/route.js +74 -34
  27. package/dist/modules/attachments/api/route.js.map +3 -3
  28. package/dist/modules/attachments/api/transfer/route.js +63 -37
  29. package/dist/modules/attachments/api/transfer/route.js.map +2 -2
  30. package/dist/modules/attachments/data/entities.js +3 -0
  31. package/dist/modules/attachments/data/entities.js.map +2 -2
  32. package/dist/modules/attachments/data/validators.js +41 -0
  33. package/dist/modules/attachments/data/validators.js.map +7 -0
  34. package/dist/modules/attachments/di.js +5 -2
  35. package/dist/modules/attachments/di.js.map +2 -2
  36. package/dist/modules/attachments/generators.js +29 -0
  37. package/dist/modules/attachments/generators.js.map +7 -0
  38. package/dist/modules/attachments/index.js +8 -1
  39. package/dist/modules/attachments/index.js.map +2 -2
  40. package/dist/modules/attachments/lib/access-errors.js +35 -0
  41. package/dist/modules/attachments/lib/access-errors.js.map +7 -0
  42. package/dist/modules/attachments/lib/access-mutation.js +23 -0
  43. package/dist/modules/attachments/lib/access-mutation.js.map +7 -0
  44. package/dist/modules/attachments/lib/access-protection.js +73 -0
  45. package/dist/modules/attachments/lib/access-protection.js.map +7 -0
  46. package/dist/modules/attachments/lib/access-query.js +74 -0
  47. package/dist/modules/attachments/lib/access-query.js.map +7 -0
  48. package/dist/modules/attachments/lib/access-registry.js +87 -0
  49. package/dist/modules/attachments/lib/access-registry.js.map +7 -0
  50. package/dist/modules/attachments/lib/access-runner.js +161 -0
  51. package/dist/modules/attachments/lib/access-runner.js.map +7 -0
  52. package/dist/modules/attachments/lib/access-types.js +1 -0
  53. package/dist/modules/attachments/lib/access-types.js.map +7 -0
  54. package/dist/modules/attachments/lib/attachment-service.js +14 -6
  55. package/dist/modules/attachments/lib/attachment-service.js.map +2 -2
  56. package/dist/modules/attachments/lib/createFromBuffer.js +2 -0
  57. package/dist/modules/attachments/lib/createFromBuffer.js.map +2 -2
  58. package/dist/modules/attachments/lib/partitions.js +3 -2
  59. package/dist/modules/attachments/lib/partitions.js.map +2 -2
  60. package/dist/modules/attachments/lib/scoped-upload-service.js +2 -0
  61. package/dist/modules/attachments/lib/scoped-upload-service.js.map +2 -2
  62. package/dist/modules/attachments/lib/target-access-service.js +12 -3
  63. package/dist/modules/attachments/lib/target-access-service.js.map +2 -2
  64. package/dist/modules/attachments/migrations/Migration20261001120000_attachments.js +38 -0
  65. package/dist/modules/attachments/migrations/Migration20261001120000_attachments.js.map +7 -0
  66. package/dist/modules/attachments/setup.js +10 -0
  67. package/dist/modules/attachments/setup.js.map +2 -2
  68. package/dist/modules/notifications/api/[id]/action/route.js +8 -0
  69. package/dist/modules/notifications/api/[id]/action/route.js.map +2 -2
  70. package/dist/modules/notifications/api/route.js +17 -0
  71. package/dist/modules/notifications/api/route.js.map +2 -2
  72. package/dist/modules/notifications/data/validators.js +1 -1
  73. package/dist/modules/notifications/data/validators.js.map +2 -2
  74. package/dist/modules/notifications/lib/notificationService.js +45 -3
  75. package/dist/modules/notifications/lib/notificationService.js.map +2 -2
  76. package/dist/modules/notifications/lib/routeHelpers.js +27 -0
  77. package/dist/modules/notifications/lib/routeHelpers.js.map +2 -2
  78. package/dist/modules/payment_gateways/lib/gateway-service.js +50 -34
  79. package/dist/modules/payment_gateways/lib/gateway-service.js.map +2 -2
  80. package/dist/modules/payment_gateways/lib/status-machine.js +10 -1
  81. package/dist/modules/payment_gateways/lib/status-machine.js.map +2 -2
  82. package/dist/modules/payment_gateways/lib/webhook-processor.js +1 -1
  83. package/dist/modules/payment_gateways/lib/webhook-processor.js.map +2 -2
  84. package/dist/modules/staff/notifications.js +4 -2
  85. package/dist/modules/staff/notifications.js.map +2 -2
  86. package/dist/modules/workflows/api/portal/tasks/[id]/complete/route.js +1 -1
  87. package/dist/modules/workflows/api/portal/tasks/[id]/complete/route.js.map +2 -2
  88. package/dist/modules/workflows/api/tasks/[id]/complete/route.js +7 -1
  89. package/dist/modules/workflows/api/tasks/[id]/complete/route.js.map +2 -2
  90. package/dist/modules/workflows/backend/tasks/[id]/page.js +2 -1
  91. package/dist/modules/workflows/backend/tasks/[id]/page.js.map +2 -2
  92. package/dist/modules/workflows/data/entities.js.map +2 -2
  93. package/dist/modules/workflows/lib/activity-executor.js +18 -4
  94. package/dist/modules/workflows/lib/activity-executor.js.map +2 -2
  95. package/dist/modules/workflows/lib/activity-types.js +2 -2
  96. package/dist/modules/workflows/lib/activity-types.js.map +2 -2
  97. package/dist/modules/workflows/lib/condition-handler.js +7 -1
  98. package/dist/modules/workflows/lib/condition-handler.js.map +2 -2
  99. package/dist/modules/workflows/lib/duration.js +13 -0
  100. package/dist/modules/workflows/lib/duration.js.map +2 -2
  101. package/dist/modules/workflows/lib/instance-status.js +16 -0
  102. package/dist/modules/workflows/lib/instance-status.js.map +7 -0
  103. package/dist/modules/workflows/lib/step-handler.js +9 -2
  104. package/dist/modules/workflows/lib/step-handler.js.map +2 -2
  105. package/dist/modules/workflows/lib/task-handler.js +16 -8
  106. package/dist/modules/workflows/lib/task-handler.js.map +2 -2
  107. package/dist/modules/workflows/lib/task-quick-action.js +13 -10
  108. package/dist/modules/workflows/lib/task-quick-action.js.map +2 -2
  109. package/dist/modules/workflows/lib/task-sla.js +10 -0
  110. package/dist/modules/workflows/lib/task-sla.js.map +2 -2
  111. package/dist/modules/workflows/lib/transition-handler.js +2 -1
  112. package/dist/modules/workflows/lib/transition-handler.js.map +2 -2
  113. package/dist/modules/workflows/lib/workflow-executor.js +29 -1
  114. package/dist/modules/workflows/lib/workflow-executor.js.map +2 -2
  115. package/dist/modules/workflows/notifications.js +4 -0
  116. package/dist/modules/workflows/notifications.js.map +2 -2
  117. package/generated/entities/api_key/index.ts +1 -0
  118. package/generated/entities/attachment_partition/index.ts +1 -0
  119. package/generated/entity-fields-registry.ts +2 -0
  120. package/package.json +7 -7
  121. package/src/helpers/integration/attachmentAccessFixtures.ts +141 -0
  122. package/src/modules/api_keys/data/entities.ts +25 -1
  123. package/src/modules/api_keys/migrations/.snapshot-open-mercato.json +234 -68
  124. package/src/modules/api_keys/migrations/Migration20261012202110_api_keys.ts +47 -0
  125. package/src/modules/api_keys/services/apiKeyService.ts +55 -9
  126. package/src/modules/attachments/api/file/[id]/route.ts +12 -9
  127. package/src/modules/attachments/api/image/[id]/[[...slug]]/route.ts +10 -8
  128. package/src/modules/attachments/api/library/[id]/route.ts +87 -58
  129. package/src/modules/attachments/api/library/route.ts +40 -11
  130. package/src/modules/attachments/api/partitions/route.ts +2 -0
  131. package/src/modules/attachments/api/route.ts +68 -35
  132. package/src/modules/attachments/api/transfer/route.ts +65 -42
  133. package/src/modules/attachments/data/entities.ts +4 -0
  134. package/src/modules/attachments/data/validators.ts +36 -0
  135. package/src/modules/attachments/di.ts +4 -1
  136. package/src/modules/attachments/generators.ts +27 -0
  137. package/src/modules/attachments/i18n/de.json +4 -0
  138. package/src/modules/attachments/i18n/en.json +4 -0
  139. package/src/modules/attachments/i18n/es.json +4 -0
  140. package/src/modules/attachments/i18n/ko.json +4 -0
  141. package/src/modules/attachments/i18n/pl.json +4 -0
  142. package/src/modules/attachments/index.ts +18 -0
  143. package/src/modules/attachments/lib/access-errors.ts +32 -0
  144. package/src/modules/attachments/lib/access-mutation.ts +26 -0
  145. package/src/modules/attachments/lib/access-protection.ts +83 -0
  146. package/src/modules/attachments/lib/access-query.ts +86 -0
  147. package/src/modules/attachments/lib/access-registry.ts +117 -0
  148. package/src/modules/attachments/lib/access-runner.ts +183 -0
  149. package/src/modules/attachments/lib/access-types.ts +78 -0
  150. package/src/modules/attachments/lib/attachment-service.ts +8 -4
  151. package/src/modules/attachments/lib/createFromBuffer.ts +3 -0
  152. package/src/modules/attachments/lib/partitions.ts +3 -2
  153. package/src/modules/attachments/lib/scoped-upload-service.ts +2 -0
  154. package/src/modules/attachments/lib/target-access-service.ts +10 -3
  155. package/src/modules/attachments/migrations/.snapshot-open-mercato.json +17 -0
  156. package/src/modules/attachments/migrations/Migration20261001120000_attachments.ts +35 -0
  157. package/src/modules/attachments/setup.ts +10 -0
  158. package/src/modules/auth/i18n/de.json +3 -0
  159. package/src/modules/auth/i18n/en.json +3 -0
  160. package/src/modules/auth/i18n/es.json +3 -0
  161. package/src/modules/auth/i18n/ko.json +3 -0
  162. package/src/modules/auth/i18n/pl.json +3 -0
  163. package/src/modules/notifications/api/[id]/action/route.ts +8 -0
  164. package/src/modules/notifications/api/route.ts +17 -0
  165. package/src/modules/notifications/data/validators.ts +1 -1
  166. package/src/modules/notifications/i18n/de.json +2 -0
  167. package/src/modules/notifications/i18n/en.json +2 -0
  168. package/src/modules/notifications/i18n/es.json +2 -0
  169. package/src/modules/notifications/i18n/ko.json +2 -0
  170. package/src/modules/notifications/i18n/pl.json +2 -0
  171. package/src/modules/notifications/lib/notificationService.ts +67 -1
  172. package/src/modules/notifications/lib/routeHelpers.ts +38 -0
  173. package/src/modules/payment_gateways/lib/gateway-service.ts +50 -33
  174. package/src/modules/payment_gateways/lib/status-machine.ts +19 -0
  175. package/src/modules/payment_gateways/lib/webhook-processor.ts +1 -1
  176. package/src/modules/staff/notifications.ts +2 -0
  177. package/src/modules/workflows/AGENTS.md +2 -2
  178. package/src/modules/workflows/api/portal/tasks/[id]/complete/route.ts +5 -1
  179. package/src/modules/workflows/api/tasks/[id]/complete/route.ts +7 -1
  180. package/src/modules/workflows/backend/tasks/[id]/page.tsx +6 -2
  181. package/src/modules/workflows/data/entities.ts +6 -0
  182. package/src/modules/workflows/i18n/de.json +2 -0
  183. package/src/modules/workflows/i18n/en.json +2 -0
  184. package/src/modules/workflows/i18n/es.json +2 -0
  185. package/src/modules/workflows/i18n/ko.json +2 -0
  186. package/src/modules/workflows/i18n/pl.json +2 -0
  187. package/src/modules/workflows/lib/activity-executor.ts +36 -4
  188. package/src/modules/workflows/lib/activity-types.ts +2 -2
  189. package/src/modules/workflows/lib/condition-handler.ts +16 -1
  190. package/src/modules/workflows/lib/duration.ts +25 -0
  191. package/src/modules/workflows/lib/instance-status.ts +29 -0
  192. package/src/modules/workflows/lib/step-handler.ts +16 -1
  193. package/src/modules/workflows/lib/task-handler.ts +18 -10
  194. package/src/modules/workflows/lib/task-quick-action.ts +19 -10
  195. package/src/modules/workflows/lib/task-sla.ts +13 -0
  196. package/src/modules/workflows/lib/transition-handler.ts +2 -1
  197. package/src/modules/workflows/lib/workflow-executor.ts +44 -1
  198. package/src/modules/workflows/notifications.ts +4 -0
@@ -0,0 +1,141 @@
1
+ import { randomUUID } from 'node:crypto'
2
+ import { expect, type APIRequestContext } from '@playwright/test'
3
+ import { apiRequest, clearAuthTokenCache, getAuthToken, withCredentialIsolatedRequest } from './api'
4
+ import { createRoleFixture, createUserFixture, deleteRoleIfExists, deleteUserIfExists, setRoleAclFeatures } from './authFixtures'
5
+ import { deleteAttachmentIfExists, uploadAttachmentFixture } from './attachmentsFixtures'
6
+ import { expectId, getTokenContext, readJsonSafe } from './generalFixtures'
7
+
8
+ export const OWNER_ACCESS_FEATURES = [
9
+ 'documents.view', 'documents.edit', 'documents.create', 'documents.share', 'documents.delete',
10
+ 'attachments.view', 'attachments.manage',
11
+ ]
12
+ export const OWNER_ACCESS_PNG = Buffer.from(
13
+ 'iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAYAAABytg0kAAAACXBIWXMAAAPoAAAD6AG1e1JrAAAAEUlEQVQImWMwSpn2H4QZYAwASOwIrfM45AsAAAAASUVORK5CYII=',
14
+ 'base64',
15
+ )
16
+
17
+ export type OwnerAccessItem = { id: string; tags?: string[]; assignments?: Array<{ type: string; id: string; label?: string | null }> }
18
+ export type OwnerAccessList = { items: OwnerAccessItem[]; total?: number; availableTags?: string[] }
19
+ export type OwnerAccessActor = { id: string; token: string; refreshToken: () => Promise<string> }
20
+ type Assignment = { type: string; id: string; label?: string }
21
+ type Share = { id: string; updatedAt: string }
22
+
23
+ export async function ownerPolicyRequest<Body = { error?: string }>(
24
+ method: string, path: string, token: string, data?: unknown, scheme: 'Bearer' | 'ApiKey' = 'Bearer',
25
+ ) {
26
+ return withCredentialIsolatedRequest(async (isolated) => {
27
+ const response = await isolated.fetch(path, {
28
+ method, headers: { Authorization: `${scheme} ${token}`, 'Content-Type': 'application/json' },
29
+ ...(data === undefined ? {} : { data }),
30
+ })
31
+ return {
32
+ status: response.status(), headers: response.headers(),
33
+ body: await readJsonSafe<Body>(response), bytes: await response.body(),
34
+ }
35
+ })
36
+ }
37
+
38
+ export type AttachmentOwnerFixture = {
39
+ admin: string
40
+ roleId: string
41
+ prefix: string
42
+ recipient: OwnerAccessActor
43
+ unshared: OwnerAccessActor
44
+ visible: string
45
+ hidden: string
46
+ destination: string
47
+ document: (label: string, token?: string) => Promise<string>
48
+ share: (documentId: string, permission: 'viewer' | 'editor', principalType?: 'user' | 'role', principalId?: string, token?: string) => Promise<Share>
49
+ upload: (documentId: string, label: string, options?: { entityId?: string; assignments?: Assignment[]; tags?: string[] }) => Promise<string>
50
+ uploadProxy: (documentId: string) => Promise<{ attachmentId: string; url: string }>
51
+ uploadPublicImage: (documentId: string) => Promise<string>
52
+ key: () => Promise<{ id: string; secret: string }>
53
+ }
54
+
55
+ export async function withAttachmentOwnerFixture(
56
+ request: APIRequestContext, run: (fixture: AttachmentOwnerFixture) => Promise<void>,
57
+ ): Promise<void> {
58
+ const admin = await getAuthToken(request, 'admin')
59
+ const scope = getTokenContext(admin)
60
+ const prefix = `attachment-owner-${randomUUID()}`
61
+ const users: string[] = [], documents: string[] = [], attachments: string[] = [], keys: string[] = []
62
+ let roleId: string | null = null
63
+ try {
64
+ roleId = await createRoleFixture(request, admin, { name: prefix, tenantId: scope.tenantId })
65
+ await setRoleAclFeatures(request, admin, { roleId, features: OWNER_ACCESS_FEATURES })
66
+ const actor = async (label: string): Promise<OwnerAccessActor> => {
67
+ const email = `${prefix}-${label}@example.com`, password = `Owner1!${randomUUID()}`
68
+ const id = await createUserFixture(request, admin, {
69
+ email, password, organizationId: scope.organizationId, roles: [roleId!], name: label,
70
+ })
71
+ users.push(id)
72
+ return { id, token: await getAuthToken(request, email, password), refreshToken: async () => {
73
+ clearAuthTokenCache()
74
+ return getAuthToken(request, email, password)
75
+ } }
76
+ }
77
+ const recipient = await actor('recipient'), unshared = await actor('unshared')
78
+ const document = async (label: string, token = admin): Promise<string> => {
79
+ const response = await apiRequest(request, 'POST', '/api/documents', { token, data: { title: `${prefix}-${label}`, folderId: null } })
80
+ expect(response.status()).toBe(201)
81
+ const body = await readJsonSafe<{ id: string }>(response)
82
+ const id = expectId(body?.id, 'Fixture response must include id'); documents.push(id); return id
83
+ }
84
+ const share: AttachmentOwnerFixture['share'] = async (documentId, permission, principalType = 'user', principalId = recipient.id, token = admin) => {
85
+ const response = await apiRequest(request, 'POST', `/api/documents/${documentId}/shares`, {
86
+ token, data: { principalType, principalId, permission },
87
+ })
88
+ expect(response.status()).toBe(201)
89
+ const body = await readJsonSafe<Share>(response)
90
+ expect(body?.updatedAt).toBeTruthy()
91
+ return { id: expectId(body?.id, 'Fixture response must include id'), updatedAt: body!.updatedAt }
92
+ }
93
+ const upload: AttachmentOwnerFixture['upload'] = async (documentId, label, options = {}) => {
94
+ const file = await uploadAttachmentFixture(request, admin, {
95
+ entityId: options.entityId ?? 'documents:document', recordId: documentId,
96
+ fileName: `${prefix}-${label}.txt`, mimeType: 'text/plain', buffer: Buffer.from(`${prefix}-${label}-content`),
97
+ assignments: options.assignments, tags: options.tags,
98
+ })
99
+ attachments.push(file.id); return file.id
100
+ }
101
+ const uploadProxy = async (documentId: string) => {
102
+ const route = `/api/documents/${documentId}/attachments`
103
+ const response = await request.fetch(process.env.BASE_URL?.trim() ? `${process.env.BASE_URL.trim()}${route}` : route, {
104
+ method: 'POST', headers: { Authorization: `Bearer ${admin}` },
105
+ multipart: { file: { name: `${prefix}-proxy.png`, mimeType: 'image/png', buffer: OWNER_ACCESS_PNG } },
106
+ })
107
+ expect(response.status()).toBe(201)
108
+ const body = await readJsonSafe<{ attachmentId: string; url: string }>(response)
109
+ return { attachmentId: expectId(body?.attachmentId, 'Document upload must include attachmentId'), url: String(body?.url) }
110
+ }
111
+ const key = async () => {
112
+ const response = await apiRequest(request, 'POST', '/api/api_keys/keys', {
113
+ token: admin, data: { name: prefix, tenantId: scope.tenantId, organizationId: scope.organizationId, roles: [roleId] },
114
+ })
115
+ expect(response.status()).toBe(201)
116
+ const body = await readJsonSafe<{ id: string; secret: string }>(response)
117
+ const id = expectId(body?.id, 'Fixture response must include id'); keys.push(id)
118
+ expect(body?.secret).toMatch(/^omk_/)
119
+ return { id, secret: body!.secret }
120
+ }
121
+ const uploadPublicImage = async (documentId: string) => {
122
+ const file = await uploadAttachmentFixture(request, admin, {
123
+ entityId: 'catalog:catalog_product', recordId: randomUUID(),
124
+ fileName: `${prefix}-public.png`, mimeType: 'image/png', buffer: OWNER_ACCESS_PNG,
125
+ assignments: [{ type: 'documents:document', id: documentId }],
126
+ })
127
+ attachments.push(file.id)
128
+ expect(file.partitionCode).toBe('productsMedia')
129
+ return file.id
130
+ }
131
+ const visible = await document('visible'), hidden = await document('hidden'), destination = await document('destination')
132
+ await share(visible, 'viewer')
133
+ await run({ admin, roleId, prefix, recipient, unshared, visible, hidden, destination, document, share, upload, uploadProxy, uploadPublicImage, key })
134
+ } finally {
135
+ for (const id of keys) await apiRequest(request, 'DELETE', `/api/api_keys/keys?id=${id}`, { token: admin }).catch(() => undefined)
136
+ for (const id of attachments) await deleteAttachmentIfExists(request, admin, id)
137
+ for (const id of documents.reverse()) await apiRequest(request, 'DELETE', `/api/documents/${id}`, { token: admin }).catch(() => undefined)
138
+ for (const id of users) await deleteUserIfExists(request, admin, id)
139
+ await deleteRoleIfExists(request, admin, roleId)
140
+ }
141
+ }
@@ -10,6 +10,11 @@ import { Entity, Index, PrimaryKey, Property, Unique } from '@mikro-orm/decorato
10
10
  expression:
11
11
  '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',
12
12
  })
13
+ @Index({
14
+ name: 'api_keys_session_token_hash_uq',
15
+ expression:
16
+ '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',
17
+ })
13
18
  export class ApiKey {
14
19
  @PrimaryKey({ type: 'uuid', defaultRaw: 'gen_random_uuid()' })
15
20
  id!: string
@@ -38,7 +43,26 @@ export class ApiKey {
38
43
  @Property({ name: 'created_by', type: 'uuid', nullable: true })
39
44
  createdBy?: string | null
40
45
 
41
- /** Session token for ephemeral session-scoped keys (used by AI chat) */
46
+ /**
47
+ * SHA-256 hash of the session token for ephemeral session-scoped keys (used by AI chat).
48
+ * The raw token is a bearer credential — the same one that unlocks the decrypted API key
49
+ * secret and the user's ACL on the MCP server — so, like `customer_accounts` session
50
+ * tokens, only its hash is persisted. It is returned to the caller once, at issuance, and
51
+ * never stored or logged in the clear (see #2254).
52
+ */
53
+ @Property({ name: 'session_token_hash', type: 'text', nullable: true })
54
+ sessionTokenHash?: string | null
55
+
56
+ /**
57
+ * @deprecated Superseded by `sessionTokenHash` (#2254) — nothing reads or writes this
58
+ * anymore and `Migration20261012202110_api_keys` nulls out every existing value as part of
59
+ * scrubbing the plaintext token. Kept, not dropped, per the "renamed/removed DB column" rule in
60
+ * `.ai/review-checklist.md` (keep old column, add new one, backfill, deprecate old) — chat
61
+ * sessions in flight across the deploy carry a 2-hour TTL and only mint once per
62
+ * conversation (`ai_assistant`'s chat route, `!sessionId` branch), so the column must survive
63
+ * long enough for every session minted before this ships to expire naturally. Safe to drop in
64
+ * a later migration once that window has passed.
65
+ */
42
66
  @Property({ name: 'session_token', type: 'text', nullable: true })
43
67
  sessionToken?: string | null
44
68
 
@@ -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
- "id": {
10
- "name": "id",
11
- "type": "uuid",
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
- "default": "gen_random_uuid()",
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
- "name": {
20
- "name": "name",
21
- "type": "text",
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": false,
26
- "mappedType": "text"
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
- "tenant_id": {
38
- "name": "tenant_id",
39
- "type": "uuid",
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
- "mappedType": "uuid"
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
- "organization_id": {
47
- "name": "organization_id",
96
+ "id": {
97
+ "name": "id",
48
98
  "type": "uuid",
49
99
  "unsigned": false,
50
100
  "autoincrement": false,
51
- "primary": false,
52
- "nullable": true,
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
- "roles_json": {
74
- "name": "roles_json",
75
- "type": "jsonb",
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
- "mappedType": "json"
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
- "created_by": {
83
- "name": "created_by",
84
- "type": "uuid",
164
+ "name": {
165
+ "name": "name",
166
+ "type": "text",
85
167
  "unsigned": false,
86
168
  "autoincrement": false,
87
169
  "primary": false,
88
- "nullable": true,
89
- "mappedType": "uuid"
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
- "session_token": {
92
- "name": "session_token",
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
- "session_user_id": {
101
- "name": "session_user_id",
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
- "opencode_session_id": {
119
- "name": "opencode_session_id",
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
- "last_used_at": {
128
- "name": "last_used_at",
129
- "type": "timestamptz",
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
- "length": 6,
135
- "mappedType": "datetime"
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
- "expires_at": {
138
- "name": "expires_at",
139
- "type": "timestamptz",
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
- "length": 6,
145
- "mappedType": "datetime"
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
- "created_at": {
148
- "name": "created_at",
149
- "type": "timestamptz",
300
+ "tenant_id": {
301
+ "name": "tenant_id",
302
+ "type": "uuid",
150
303
  "unsigned": false,
151
304
  "autoincrement": false,
152
305
  "primary": false,
153
- "nullable": false,
154
- "length": 6,
155
- "mappedType": "datetime"
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
- "keyName": "api_keys_pkey",
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": "api_keys_opencode_session_id_uq",
369
+ "keyName": "api_keys_session_token_hash_uq",
206
370
  "primary": false,
207
371
  "unique": false,
208
- "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"
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
- "nativeEnums": {}
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
+ }