@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.
- 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/entities/attachment_partition/index.js +2 -0
- package/dist/generated/entities/attachment_partition/index.js.map +2 -2
- package/dist/generated/entity-fields-registry.js +2 -0
- package/dist/generated/entity-fields-registry.js.map +2 -2
- package/dist/helpers/integration/attachmentAccessFixtures.js +144 -0
- package/dist/helpers/integration/attachmentAccessFixtures.js.map +7 -0
- 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/api/file/[id]/route.js +16 -10
- package/dist/modules/attachments/api/file/[id]/route.js.map +2 -2
- package/dist/modules/attachments/api/image/[id]/[[...slug]]/route.js +15 -9
- package/dist/modules/attachments/api/image/[id]/[[...slug]]/route.js.map +2 -2
- package/dist/modules/attachments/api/library/[id]/route.js +100 -56
- package/dist/modules/attachments/api/library/[id]/route.js.map +3 -3
- package/dist/modules/attachments/api/library/route.js +56 -17
- package/dist/modules/attachments/api/library/route.js.map +2 -2
- package/dist/modules/attachments/api/partitions/route.js +2 -0
- package/dist/modules/attachments/api/partitions/route.js.map +2 -2
- package/dist/modules/attachments/api/route.js +74 -34
- package/dist/modules/attachments/api/route.js.map +3 -3
- package/dist/modules/attachments/api/transfer/route.js +63 -37
- package/dist/modules/attachments/api/transfer/route.js.map +2 -2
- package/dist/modules/attachments/data/entities.js +3 -0
- package/dist/modules/attachments/data/entities.js.map +2 -2
- package/dist/modules/attachments/data/validators.js +41 -0
- package/dist/modules/attachments/data/validators.js.map +7 -0
- package/dist/modules/attachments/di.js +5 -2
- package/dist/modules/attachments/di.js.map +2 -2
- package/dist/modules/attachments/generators.js +29 -0
- package/dist/modules/attachments/generators.js.map +7 -0
- package/dist/modules/attachments/index.js +8 -1
- package/dist/modules/attachments/index.js.map +2 -2
- package/dist/modules/attachments/lib/access-errors.js +35 -0
- package/dist/modules/attachments/lib/access-errors.js.map +7 -0
- package/dist/modules/attachments/lib/access-mutation.js +23 -0
- package/dist/modules/attachments/lib/access-mutation.js.map +7 -0
- package/dist/modules/attachments/lib/access-protection.js +73 -0
- package/dist/modules/attachments/lib/access-protection.js.map +7 -0
- package/dist/modules/attachments/lib/access-query.js +74 -0
- package/dist/modules/attachments/lib/access-query.js.map +7 -0
- package/dist/modules/attachments/lib/access-registry.js +87 -0
- package/dist/modules/attachments/lib/access-registry.js.map +7 -0
- package/dist/modules/attachments/lib/access-runner.js +161 -0
- package/dist/modules/attachments/lib/access-runner.js.map +7 -0
- package/dist/modules/attachments/lib/access-types.js +1 -0
- package/dist/modules/attachments/lib/access-types.js.map +7 -0
- package/dist/modules/attachments/lib/attachment-service.js +14 -6
- package/dist/modules/attachments/lib/attachment-service.js.map +2 -2
- package/dist/modules/attachments/lib/createFromBuffer.js +2 -0
- package/dist/modules/attachments/lib/createFromBuffer.js.map +2 -2
- package/dist/modules/attachments/lib/partitions.js +3 -2
- package/dist/modules/attachments/lib/partitions.js.map +2 -2
- package/dist/modules/attachments/lib/scoped-upload-service.js +2 -0
- package/dist/modules/attachments/lib/scoped-upload-service.js.map +2 -2
- package/dist/modules/attachments/lib/target-access-service.js +12 -3
- package/dist/modules/attachments/lib/target-access-service.js.map +2 -2
- package/dist/modules/attachments/migrations/Migration20261001120000_attachments.js +38 -0
- package/dist/modules/attachments/migrations/Migration20261001120000_attachments.js.map +7 -0
- package/dist/modules/attachments/setup.js +10 -0
- package/dist/modules/attachments/setup.js.map +2 -2
- package/dist/modules/notifications/api/[id]/action/route.js +8 -0
- package/dist/modules/notifications/api/[id]/action/route.js.map +2 -2
- package/dist/modules/notifications/api/route.js +17 -0
- package/dist/modules/notifications/api/route.js.map +2 -2
- package/dist/modules/notifications/data/validators.js +1 -1
- package/dist/modules/notifications/data/validators.js.map +2 -2
- package/dist/modules/notifications/lib/notificationService.js +45 -3
- package/dist/modules/notifications/lib/notificationService.js.map +2 -2
- package/dist/modules/notifications/lib/routeHelpers.js +27 -0
- package/dist/modules/notifications/lib/routeHelpers.js.map +2 -2
- package/dist/modules/payment_gateways/lib/gateway-service.js +50 -34
- package/dist/modules/payment_gateways/lib/gateway-service.js.map +2 -2
- package/dist/modules/payment_gateways/lib/status-machine.js +10 -1
- package/dist/modules/payment_gateways/lib/status-machine.js.map +2 -2
- package/dist/modules/payment_gateways/lib/webhook-processor.js +1 -1
- package/dist/modules/payment_gateways/lib/webhook-processor.js.map +2 -2
- package/dist/modules/staff/notifications.js +4 -2
- package/dist/modules/staff/notifications.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/data/entities.js.map +2 -2
- package/dist/modules/workflows/lib/activity-executor.js +18 -4
- package/dist/modules/workflows/lib/activity-executor.js.map +2 -2
- package/dist/modules/workflows/lib/activity-types.js +2 -2
- package/dist/modules/workflows/lib/activity-types.js.map +2 -2
- package/dist/modules/workflows/lib/condition-handler.js +7 -1
- package/dist/modules/workflows/lib/condition-handler.js.map +2 -2
- package/dist/modules/workflows/lib/duration.js +13 -0
- package/dist/modules/workflows/lib/duration.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/step-handler.js +9 -2
- package/dist/modules/workflows/lib/step-handler.js.map +2 -2
- 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-quick-action.js +13 -10
- package/dist/modules/workflows/lib/task-quick-action.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/transition-handler.js +2 -1
- package/dist/modules/workflows/lib/transition-handler.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/dist/modules/workflows/notifications.js +4 -0
- package/dist/modules/workflows/notifications.js.map +2 -2
- package/generated/entities/api_key/index.ts +1 -0
- package/generated/entities/attachment_partition/index.ts +1 -0
- package/generated/entity-fields-registry.ts +2 -0
- package/package.json +7 -7
- package/src/helpers/integration/attachmentAccessFixtures.ts +141 -0
- 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/api/file/[id]/route.ts +12 -9
- package/src/modules/attachments/api/image/[id]/[[...slug]]/route.ts +10 -8
- package/src/modules/attachments/api/library/[id]/route.ts +87 -58
- package/src/modules/attachments/api/library/route.ts +40 -11
- package/src/modules/attachments/api/partitions/route.ts +2 -0
- package/src/modules/attachments/api/route.ts +68 -35
- package/src/modules/attachments/api/transfer/route.ts +65 -42
- package/src/modules/attachments/data/entities.ts +4 -0
- package/src/modules/attachments/data/validators.ts +36 -0
- package/src/modules/attachments/di.ts +4 -1
- package/src/modules/attachments/generators.ts +27 -0
- package/src/modules/attachments/i18n/de.json +4 -0
- package/src/modules/attachments/i18n/en.json +4 -0
- package/src/modules/attachments/i18n/es.json +4 -0
- package/src/modules/attachments/i18n/ko.json +4 -0
- package/src/modules/attachments/i18n/pl.json +4 -0
- package/src/modules/attachments/index.ts +18 -0
- package/src/modules/attachments/lib/access-errors.ts +32 -0
- package/src/modules/attachments/lib/access-mutation.ts +26 -0
- package/src/modules/attachments/lib/access-protection.ts +83 -0
- package/src/modules/attachments/lib/access-query.ts +86 -0
- package/src/modules/attachments/lib/access-registry.ts +117 -0
- package/src/modules/attachments/lib/access-runner.ts +183 -0
- package/src/modules/attachments/lib/access-types.ts +78 -0
- package/src/modules/attachments/lib/attachment-service.ts +8 -4
- package/src/modules/attachments/lib/createFromBuffer.ts +3 -0
- package/src/modules/attachments/lib/partitions.ts +3 -2
- package/src/modules/attachments/lib/scoped-upload-service.ts +2 -0
- package/src/modules/attachments/lib/target-access-service.ts +10 -3
- package/src/modules/attachments/migrations/.snapshot-open-mercato.json +17 -0
- package/src/modules/attachments/migrations/Migration20261001120000_attachments.ts +35 -0
- package/src/modules/attachments/setup.ts +10 -0
- 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/notifications/api/[id]/action/route.ts +8 -0
- package/src/modules/notifications/api/route.ts +17 -0
- package/src/modules/notifications/data/validators.ts +1 -1
- package/src/modules/notifications/i18n/de.json +2 -0
- package/src/modules/notifications/i18n/en.json +2 -0
- package/src/modules/notifications/i18n/es.json +2 -0
- package/src/modules/notifications/i18n/ko.json +2 -0
- package/src/modules/notifications/i18n/pl.json +2 -0
- package/src/modules/notifications/lib/notificationService.ts +67 -1
- package/src/modules/notifications/lib/routeHelpers.ts +38 -0
- package/src/modules/payment_gateways/lib/gateway-service.ts +50 -33
- package/src/modules/payment_gateways/lib/status-machine.ts +19 -0
- package/src/modules/payment_gateways/lib/webhook-processor.ts +1 -1
- package/src/modules/staff/notifications.ts +2 -0
- package/src/modules/workflows/AGENTS.md +2 -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/data/entities.ts +6 -0
- 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/activity-executor.ts +36 -4
- package/src/modules/workflows/lib/activity-types.ts +2 -2
- package/src/modules/workflows/lib/condition-handler.ts +16 -1
- package/src/modules/workflows/lib/duration.ts +25 -0
- package/src/modules/workflows/lib/instance-status.ts +29 -0
- package/src/modules/workflows/lib/step-handler.ts +16 -1
- package/src/modules/workflows/lib/task-handler.ts +18 -10
- package/src/modules/workflows/lib/task-quick-action.ts +19 -10
- package/src/modules/workflows/lib/task-sla.ts +13 -0
- package/src/modules/workflows/lib/transition-handler.ts +2 -1
- package/src/modules/workflows/lib/workflow-executor.ts +44 -1
- 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
|
-
/**
|
|
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
|
-
"
|
|
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
|
+
}
|