diff --git a/cmd/internal/openapi/client.gen.go b/cmd/internal/openapi/client.gen.go index 5343cc9e..a357a521 100644 --- a/cmd/internal/openapi/client.gen.go +++ b/cmd/internal/openapi/client.gen.go @@ -19,6 +19,42 @@ import ( openapi_types "github.com/oapi-codegen/runtime/types" ) +// Defines values for ActorAttributionType. +const ( + ActorAttributionTypeAgent ActorAttributionType = "agent" + ActorAttributionTypeAnonymous ActorAttributionType = "anonymous" + ActorAttributionTypeApiKey ActorAttributionType = "api_key" + ActorAttributionTypeDevice ActorAttributionType = "device" + ActorAttributionTypeOauth ActorAttributionType = "oauth" + ActorAttributionTypeSystem ActorAttributionType = "system" + ActorAttributionTypeTaskUpload ActorAttributionType = "task-upload" + ActorAttributionTypeUser ActorAttributionType = "user" +) + +// Valid indicates whether the value is a known member of the ActorAttributionType enum. +func (e ActorAttributionType) Valid() bool { + switch e { + case ActorAttributionTypeAgent: + return true + case ActorAttributionTypeAnonymous: + return true + case ActorAttributionTypeApiKey: + return true + case ActorAttributionTypeDevice: + return true + case ActorAttributionTypeOauth: + return true + case ActorAttributionTypeSystem: + return true + case ActorAttributionTypeTaskUpload: + return true + case ActorAttributionTypeUser: + return true + default: + return false + } +} + // Defines values for AdminAnalyticsGrowthComparisonCoverageQuality. const ( AdminAnalyticsGrowthComparisonCoverageQualityExact AdminAnalyticsGrowthComparisonCoverageQuality = "exact" @@ -1068,34 +1104,34 @@ func (e AdminDashboardDownloadersItemsStatus) Valid() bool { // Defines values for AuditEventActorType. const ( - Agent AuditEventActorType = "agent" - Anonymous AuditEventActorType = "anonymous" - ApiKey AuditEventActorType = "api_key" - Device AuditEventActorType = "device" - Oauth AuditEventActorType = "oauth" - System AuditEventActorType = "system" - TaskUpload AuditEventActorType = "task-upload" - User AuditEventActorType = "user" + AuditEventActorTypeAgent AuditEventActorType = "agent" + AuditEventActorTypeAnonymous AuditEventActorType = "anonymous" + AuditEventActorTypeApiKey AuditEventActorType = "api_key" + AuditEventActorTypeDevice AuditEventActorType = "device" + AuditEventActorTypeOauth AuditEventActorType = "oauth" + AuditEventActorTypeSystem AuditEventActorType = "system" + AuditEventActorTypeTaskUpload AuditEventActorType = "task-upload" + AuditEventActorTypeUser AuditEventActorType = "user" ) // Valid indicates whether the value is a known member of the AuditEventActorType enum. func (e AuditEventActorType) Valid() bool { switch e { - case Agent: + case AuditEventActorTypeAgent: return true - case Anonymous: + case AuditEventActorTypeAnonymous: return true - case ApiKey: + case AuditEventActorTypeApiKey: return true - case Device: + case AuditEventActorTypeDevice: return true - case Oauth: + case AuditEventActorTypeOauth: return true - case System: + case AuditEventActorTypeSystem: return true - case TaskUpload: + case AuditEventActorTypeTaskUpload: return true - case User: + case AuditEventActorTypeUser: return true default: return false @@ -4071,6 +4107,19 @@ type ActivityPage struct { Total int `json:"total"` } +// ActorAttribution defines model for ActorAttribution. +type ActorAttribution struct { + Image *string `json:"image"` + Issuer *string `json:"issuer"` + Name string `json:"name"` + Ref *string `json:"ref"` + Resolved bool `json:"resolved"` + Type ActorAttributionType `json:"type"` +} + +// ActorAttributionType defines model for ActorAttribution.Type. +type ActorAttributionType string + // AdminAnalyticsGrowth defines model for AdminAnalyticsGrowth. type AdminAnalyticsGrowth struct { ActiveUserTrend []struct { @@ -5166,11 +5215,12 @@ type DownloadTask struct { Action DownloadTaskControlAction `json:"action"` RequestedAt string `json:"requestedAt"` } `json:"control,omitempty"` - CreatedAt string `json:"createdAt"` - CreatedBy *string `json:"createdBy,omitempty"` - Id string `json:"id"` - OrgId *string `json:"orgId,omitempty"` - Spec struct { + CreatedAt string `json:"createdAt"` + CreatedBy *string `json:"createdBy,omitempty"` + Id string `json:"id"` + OrgId *string `json:"orgId,omitempty"` + RequestedBy *ActorAttribution `json:"requestedBy,omitempty"` + Spec struct { Destination struct { Folder string `json:"folder"` Name *string `json:"name"` @@ -5186,9 +5236,10 @@ type DownloadTask struct { } `json:"spec"` Status struct { Assignment *struct { - AssignedAt *string `json:"assignedAt,omitempty"` - DownloaderId string `json:"downloaderId"` - UploadToken *string `json:"uploadToken,omitempty"` + AssignedAt *string `json:"assignedAt,omitempty"` + DownloaderId string `json:"downloaderId"` + Executor *ActorAttribution `json:"executor,omitempty"` + UploadToken *string `json:"uploadToken,omitempty"` } `json:"assignment"` Attempt int `json:"attempt"` Billing struct { @@ -5763,13 +5814,14 @@ type ManualImageDomainSettingsProvider string // Matter defines model for Matter. type Matter struct { - Alias string `json:"alias"` - CreatedAt string `json:"createdAt"` - Dirtype *int `json:"dirtype"` - Id string `json:"id"` - Name string `json:"name"` - Object string `json:"object"` - OrgId string `json:"orgId"` + Alias string `json:"alias"` + CreatedAt string `json:"createdAt"` + CreatedBy *ActorAttribution `json:"createdBy"` + Dirtype *int `json:"dirtype"` + Id string `json:"id"` + Name string `json:"name"` + Object string `json:"object"` + OrgId string `json:"orgId"` // Parent Slash-delimited parent folder path relative to the workspace root; empty for root objects. Parent string `json:"parent"` @@ -5803,9 +5855,10 @@ type NotificationPage struct { // ObjectListItem defines model for ObjectListItem. type ObjectListItem struct { - Alias string `json:"alias"` - CreatedAt string `json:"createdAt"` - Dirtype *int `json:"dirtype"` + Alias string `json:"alias"` + CreatedAt string `json:"createdAt"` + CreatedBy *ActorAttribution `json:"createdBy"` + Dirtype *int `json:"dirtype"` // HasChildren Whether this folder contains at least one child folder. HasChildren bool `json:"hasChildren"` @@ -23386,13 +23439,14 @@ type CreateObjectResponse struct { Body []byte HTTPResponse *http.Response JSON201 *struct { - Alias string `json:"alias"` - CreatedAt string `json:"createdAt"` - Dirtype *int `json:"dirtype"` - Id string `json:"id"` - Name string `json:"name"` - Object string `json:"object"` - OrgId string `json:"orgId"` + Alias string `json:"alias"` + CreatedAt string `json:"createdAt"` + CreatedBy *ActorAttribution `json:"createdBy"` + Dirtype *int `json:"dirtype"` + Id string `json:"id"` + Name string `json:"name"` + Object string `json:"object"` + OrgId string `json:"orgId"` // Parent Slash-delimited parent folder path relative to the workspace root; empty for root objects. Parent string `json:"parent"` @@ -23517,14 +23571,15 @@ type GetObjectResponse struct { Body []byte HTTPResponse *http.Response JSON200 *struct { - Alias string `json:"alias"` - CreatedAt string `json:"createdAt"` - Dirtype *int `json:"dirtype"` - DownloadUrl *string `json:"downloadUrl,omitempty"` - Id string `json:"id"` - Name string `json:"name"` - Object string `json:"object"` - OrgId string `json:"orgId"` + Alias string `json:"alias"` + CreatedAt string `json:"createdAt"` + CreatedBy *ActorAttribution `json:"createdBy"` + Dirtype *int `json:"dirtype"` + DownloadUrl *string `json:"downloadUrl,omitempty"` + Id string `json:"id"` + Name string `json:"name"` + Object string `json:"object"` + OrgId string `json:"orgId"` // Parent Slash-delimited parent folder path relative to the workspace root; empty for root objects. Parent string `json:"parent"` @@ -31035,13 +31090,14 @@ func ParseCreateObjectResponse(rsp *http.Response) (*CreateObjectResponse, error switch { case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 201: var dest struct { - Alias string `json:"alias"` - CreatedAt string `json:"createdAt"` - Dirtype *int `json:"dirtype"` - Id string `json:"id"` - Name string `json:"name"` - Object string `json:"object"` - OrgId string `json:"orgId"` + Alias string `json:"alias"` + CreatedAt string `json:"createdAt"` + CreatedBy *ActorAttribution `json:"createdBy"` + Dirtype *int `json:"dirtype"` + Id string `json:"id"` + Name string `json:"name"` + Object string `json:"object"` + OrgId string `json:"orgId"` // Parent Slash-delimited parent folder path relative to the workspace root; empty for root objects. Parent string `json:"parent"` @@ -31200,14 +31256,15 @@ func ParseGetObjectResponse(rsp *http.Response) (*GetObjectResponse, error) { switch { case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: var dest struct { - Alias string `json:"alias"` - CreatedAt string `json:"createdAt"` - Dirtype *int `json:"dirtype"` - DownloadUrl *string `json:"downloadUrl,omitempty"` - Id string `json:"id"` - Name string `json:"name"` - Object string `json:"object"` - OrgId string `json:"orgId"` + Alias string `json:"alias"` + CreatedAt string `json:"createdAt"` + CreatedBy *ActorAttribution `json:"createdBy"` + Dirtype *int `json:"dirtype"` + DownloadUrl *string `json:"downloadUrl,omitempty"` + Id string `json:"id"` + Name string `json:"name"` + Object string `json:"object"` + OrgId string `json:"orgId"` // Parent Slash-delimited parent folder path relative to the workspace root; empty for root objects. Parent string `json:"parent"` diff --git a/e2e/responsive.spec.ts b/e2e/responsive.spec.ts index 86715627..e3849e97 100644 --- a/e2e/responsive.spec.ts +++ b/e2e/responsive.spec.ts @@ -87,13 +87,42 @@ test.describe('Toolbar responsive layout', () => { // Table: secondary columns hidden on small screens // --------------------------------------------------------------------------- test.describe('File table responsive columns', () => { - test('desktop: all columns visible (name, size, modified, actions) @desktop', async ({ page }) => { + test('desktop: creator uses an avatar-only column with identity details on hover @desktop', async ({ page }) => { await signUpAndGoToFiles(page) await createFolder(page, 'test-folder') await expect(page.getByRole('columnheader', { name: /size/i })).toBeVisible() await expect(page.getByRole('columnheader', { name: /modified/i })).toBeVisible() + await expect(page.getByRole('columnheader', { name: /creator/i })).toBeVisible() + + const row = page.getByRole('row', { name: /test-folder/ }) + const creatorCell = row.getByRole('cell').nth(4) + const creatorTrigger = creatorCell.getByRole('button', { name: /created by:/i }) + await expect(creatorTrigger).toBeVisible() + const creatorLabel = await creatorTrigger.getAttribute('aria-label') + const creatorName = creatorLabel?.replace(/^Created by:\s*/i, '') + expect(creatorName).toBeTruthy() + await expect(creatorTrigger).not.toContainText(creatorName!) + + await creatorTrigger.hover() + const identityCard = page.locator('[data-slot="hover-card-content"]') + await expect(identityCard).toBeVisible() + await expect(identityCard.getByText(creatorName!, { exact: true })).toBeVisible() + await expect(identityCard.getByText('User', { exact: true })).toBeVisible() + + await page.getByLabel('Grid view').click() + const gridCard = page.getByRole('button', { name: /test-folder/ }) + await expect(gridCard).toBeVisible() + await expect(gridCard).not.toContainText(creatorName!) + + await page.getByLabel('List view').click() + + await row.getByRole('button').last().click() + await page.getByRole('menuitem', { name: /details/i }).click() + const details = page.getByRole('dialog', { name: 'test-folder' }) + await expect(details.getByText(/created by/i)).toBeVisible() + await expect(details.getByTitle(creatorName!)).toBeVisible() }) test('mobile: size and modified columns are hidden @mobile', async ({ page }) => { diff --git a/migrations/0093_actor_attribution.sql b/migrations/0093_actor_attribution.sql new file mode 100644 index 00000000..93001004 --- /dev/null +++ b/migrations/0093_actor_attribution.sql @@ -0,0 +1,6 @@ +ALTER TABLE `download_tasks` ADD `requested_by_actor_type` text;--> statement-breakpoint +ALTER TABLE `download_tasks` ADD `requested_by_actor_ref` text;--> statement-breakpoint +ALTER TABLE `download_tasks` ADD `requested_by_actor_issuer` text;--> statement-breakpoint +ALTER TABLE `matters` ADD `created_by_actor_type` text;--> statement-breakpoint +ALTER TABLE `matters` ADD `created_by_actor_ref` text;--> statement-breakpoint +ALTER TABLE `matters` ADD `created_by_actor_issuer` text; \ No newline at end of file diff --git a/migrations/meta/0093_snapshot.json b/migrations/meta/0093_snapshot.json new file mode 100644 index 00000000..10e633ff --- /dev/null +++ b/migrations/meta/0093_snapshot.json @@ -0,0 +1,6454 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "88bdc4fa-e780-4ae6-ac94-231f8f7eb886", + "prevId": "47fec986-9897-4988-a2b2-544efe2fed73", + "tables": { + "announcements": { + "name": "announcements", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'draft'" + }, + "priority": { + "name": "priority", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "published_at": { + "name": "published_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "announcements_status_priority_idx": { + "name": "announcements_status_priority_idx", + "columns": [ + "status", + "priority" + ], + "isUnique": false + }, + "announcements_published_idx": { + "name": "announcements_published_idx", + "columns": [ + "published_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "audit_events": { + "name": "audit_events", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "event_key": { + "name": "event_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_type": { + "name": "target_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_id": { + "name": "target_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "target_name": { + "name": "target_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "actor_type": { + "name": "actor_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "actor_ref": { + "name": "actor_ref", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "actor_issuer": { + "name": "actor_issuer", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "audit_events_event_key_unique": { + "name": "audit_events_event_key_unique", + "columns": [ + "event_key" + ], + "isUnique": true + }, + "audit_events_org_created_idx": { + "name": "audit_events_org_created_idx", + "columns": [ + "org_id", + "created_at" + ], + "isUnique": false + }, + "audit_events_user_created_idx": { + "name": "audit_events_user_created_idx", + "columns": [ + "user_id", + "created_at" + ], + "isUnique": false + }, + "audit_events_action_created_idx": { + "name": "audit_events_action_created_idx", + "columns": [ + "action", + "created_at" + ], + "isUnique": false + }, + "audit_events_target_created_idx": { + "name": "audit_events_target_created_idx", + "columns": [ + "target_type", + "target_id", + "created_at" + ], + "isUnique": false + }, + "audit_events_created_idx": { + "name": "audit_events_created_idx", + "columns": [ + "created_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "background_jobs": { + "name": "background_jobs", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_folder": { + "name": "target_folder", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "target_path": { + "name": "target_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "input_bytes": { + "name": "input_bytes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "output_bytes": { + "name": "output_bytes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "processed_bytes": { + "name": "processed_bytes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "file_count": { + "name": "file_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "current_filename": { + "name": "current_filename", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "result_metadata": { + "name": "result_metadata", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "retryable": { + "name": "retryable", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "cancelable": { + "name": "cancelable", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "retried_from_job_id": { + "name": "retried_from_job_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "finished_at": { + "name": "finished_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "background_jobs_org_created_idx": { + "name": "background_jobs_org_created_idx", + "columns": [ + "org_id", + "created_at" + ], + "isUnique": false + }, + "background_jobs_org_status_idx": { + "name": "background_jobs_org_status_idx", + "columns": [ + "org_id", + "status" + ], + "isUnique": false + }, + "background_jobs_org_type_idx": { + "name": "background_jobs_org_type_idx", + "columns": [ + "org_id", + "type" + ], + "isUnique": false + }, + "background_jobs_created_idx": { + "name": "background_jobs_created_idx", + "columns": [ + "created_at" + ], + "isUnique": false + }, + "background_jobs_org_page_idx": { + "name": "background_jobs_org_page_idx", + "columns": [ + "org_id", + "created_at", + "id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "cloud_traffic_reports": { + "name": "cloud_traffic_reports", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "period": { + "name": "period", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "event_id": { + "name": "event_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "bytes": { + "name": "bytes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "storage_id": { + "name": "storage_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "unit_bytes": { + "name": "unit_bytes", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "credits_per_unit": { + "name": "credits_per_unit", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "next_retry_at": { + "name": "next_retry_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "issued_at": { + "name": "issued_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "cloud_traffic_reports_event_uniq": { + "name": "cloud_traffic_reports_event_uniq", + "columns": [ + "event_id" + ], + "isUnique": true + }, + "cloud_traffic_reports_org_period_idx": { + "name": "cloud_traffic_reports_org_period_idx", + "columns": [ + "org_id", + "period" + ], + "isUnique": false + }, + "cloud_traffic_reports_status_idx": { + "name": "cloud_traffic_reports_status_idx", + "columns": [ + "status" + ], + "isUnique": false + }, + "cloud_traffic_reports_retry_idx": { + "name": "cloud_traffic_reports_retry_idx", + "columns": [ + "status", + "next_retry_at", + "created_at" + ], + "isUnique": false + }, + "cloud_traffic_reports_issued_idx": { + "name": "cloud_traffic_reports_issued_idx", + "columns": [ + "issued_at" + ], + "isUnique": false + }, + "cloud_traffic_reports_updated_idx": { + "name": "cloud_traffic_reports_updated_idx", + "columns": [ + "updated_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "download_tasks": { + "name": "download_tasks", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "requested_by_actor_type": { + "name": "requested_by_actor_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "requested_by_actor_ref": { + "name": "requested_by_actor_ref", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "requested_by_actor_issuer": { + "name": "requested_by_actor_issuer", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_uri": { + "name": "source_uri", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "target_folder": { + "name": "target_folder", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "assigned_downloader_id": { + "name": "assigned_downloader_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "attempt": { + "name": "attempt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "billing_authorized_bytes": { + "name": "billing_authorized_bytes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "billing_charged_bytes": { + "name": "billing_charged_bytes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "billing_charged_credits": { + "name": "billing_charged_credits", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "billing_status": { + "name": "billing_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'none'" + }, + "error_code": { + "name": "error_code", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "result_object_id": { + "name": "result_object_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "runtime": { + "name": "runtime", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "events": { + "name": "events", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "resolve_started_at": { + "name": "resolve_started_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "resolve_completed_at": { + "name": "resolve_completed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "download_completed_at": { + "name": "download_completed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ingest_started_at": { + "name": "ingest_started_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ingest_completed_at": { + "name": "ingest_completed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "seeding_started_at": { + "name": "seeding_started_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "seeding_stopped_at": { + "name": "seeding_stopped_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "assigned_at": { + "name": "assigned_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "finished_at": { + "name": "finished_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "download_tasks_org_created_idx": { + "name": "download_tasks_org_created_idx", + "columns": [ + "org_id", + "created_at" + ], + "isUnique": false + }, + "download_tasks_org_status_idx": { + "name": "download_tasks_org_status_idx", + "columns": [ + "org_id", + "status" + ], + "isUnique": false + }, + "download_tasks_org_category_idx": { + "name": "download_tasks_org_category_idx", + "columns": [ + "org_id", + "category" + ], + "isUnique": false + }, + "download_tasks_org_tags_idx": { + "name": "download_tasks_org_tags_idx", + "columns": [ + "org_id", + "tags" + ], + "isUnique": false + }, + "download_tasks_downloader_idx": { + "name": "download_tasks_downloader_idx", + "columns": [ + "assigned_downloader_id", + "status" + ], + "isUnique": false + }, + "download_tasks_created_idx": { + "name": "download_tasks_created_idx", + "columns": [ + "created_at" + ], + "isUnique": false + }, + "download_tasks_finished_idx": { + "name": "download_tasks_finished_idx", + "columns": [ + "finished_at" + ], + "isUnique": false + }, + "download_tasks_org_deleted_created_idx": { + "name": "download_tasks_org_deleted_created_idx", + "columns": [ + "org_id", + "deleted_at", + "created_at" + ], + "isUnique": false + }, + "download_tasks_org_page_idx": { + "name": "download_tasks_org_page_idx", + "columns": [ + "org_id", + "deleted_at", + "created_at", + "id" + ], + "isUnique": false + }, + "download_tasks_downloader_page_idx": { + "name": "download_tasks_downloader_page_idx", + "columns": [ + "assigned_downloader_id", + "deleted_at", + "created_at", + "id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "downloaders": { + "name": "downloaders", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_jti": { + "name": "token_jti", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'offline'" + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'unknown'" + }, + "hostname": { + "name": "hostname", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'unknown'" + }, + "platform": { + "name": "platform", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'unknown'" + }, + "arch": { + "name": "arch", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'unknown'" + }, + "engine": { + "name": "engine", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'http'" + }, + "capabilities": { + "name": "capabilities", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "max_concurrent_tasks": { + "name": "max_concurrent_tasks", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "current_tasks": { + "name": "current_tasks", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "download_bps": { + "name": "download_bps", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "upload_bps": { + "name": "upload_bps", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "free_disk_bytes": { + "name": "free_disk_bytes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "remote_download_credit_billing_enabled": { + "name": "remote_download_credit_billing_enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "remote_download_credit_unit_bytes": { + "name": "remote_download_credit_unit_bytes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 104857600 + }, + "remote_download_credit_per_unit": { + "name": "remote_download_credit_per_unit", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "last_heartbeat_at": { + "name": "last_heartbeat_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "downloaders_token_jti_unique": { + "name": "downloaders_token_jti_unique", + "columns": [ + "token_jti" + ], + "isUnique": true + }, + "downloaders_status_idx": { + "name": "downloaders_status_idx", + "columns": [ + "status" + ], + "isUnique": false + }, + "downloaders_enabled_idx": { + "name": "downloaders_enabled_idx", + "columns": [ + "enabled" + ], + "isUnique": false + }, + "downloaders_created_idx": { + "name": "downloaders_created_idx", + "columns": [ + "created_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "image_hosting_configs": { + "name": "image_hosting_configs", + "columns": { + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "custom_domain": { + "name": "custom_domain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "domain_provider": { + "name": "domain_provider", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "provider_hostname_id": { + "name": "provider_hostname_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "domain_status": { + "name": "domain_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "domain_error": { + "name": "domain_error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "verification_token": { + "name": "verification_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "domain_last_checked_at": { + "name": "domain_last_checked_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "domain_verified_at": { + "name": "domain_verified_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "referer_allowlist": { + "name": "referer_allowlist", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "image_hosting_configs_custom_domain_unique": { + "name": "image_hosting_configs_custom_domain_unique", + "columns": [ + "custom_domain" + ], + "isUnique": true + } + }, + "foreignKeys": { + "image_hosting_configs_org_id_organization_id_fk": { + "name": "image_hosting_configs_org_id_organization_id_fk", + "tableFrom": "image_hosting_configs", + "tableTo": "organization", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "image_hostings": { + "name": "image_hostings", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "storage_id": { + "name": "storage_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "storage_key": { + "name": "storage_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "mime": { + "name": "mime", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "width": { + "name": "width", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "height": { + "name": "height", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'draft'" + }, + "purged_at": { + "name": "purged_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_count": { + "name": "access_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "last_accessed_at": { + "name": "last_accessed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "image_hostings_token_unique": { + "name": "image_hostings_token_unique", + "columns": [ + "token" + ], + "isUnique": true + }, + "image_hostings_org_path_uniq": { + "name": "image_hostings_org_path_uniq", + "columns": [ + "org_id", + "path" + ], + "isUnique": true, + "where": "\"image_hostings\".\"purged_at\" IS NULL" + }, + "image_hostings_org_created_idx": { + "name": "image_hostings_org_created_idx", + "columns": [ + "org_id", + "created_at" + ], + "isUnique": false + }, + "image_hostings_page_idx": { + "name": "image_hostings_page_idx", + "columns": [ + "org_id", + "status", + "purged_at", + "created_at", + "id" + ], + "isUnique": false + }, + "image_hostings_token_idx": { + "name": "image_hostings_token_idx", + "columns": [ + "token" + ], + "isUnique": false + } + }, + "foreignKeys": { + "image_hostings_org_id_organization_id_fk": { + "name": "image_hostings_org_id_organization_id_fk", + "tableFrom": "image_hostings", + "tableTo": "organization", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "image_hostings_storage_id_storages_id_fk": { + "name": "image_hostings_storage_id_storages_id_fk", + "tableFrom": "image_hostings", + "tableTo": "storages", + "columnsFrom": [ + "storage_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "invite_codes": { + "name": "invite_codes", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "used_by": { + "name": "used_by", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "used_at": { + "name": "used_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "invite_codes_code_unique": { + "name": "invite_codes_code_unique", + "columns": [ + "code" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "license_bindings": { + "name": "license_bindings", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "cloud_binding_id": { + "name": "cloud_binding_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cloud_store_id": { + "name": "cloud_store_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "instance_id": { + "name": "instance_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cloud_account_id": { + "name": "cloud_account_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cloud_account_email": { + "name": "cloud_account_email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cached_certificate": { + "name": "cached_certificate", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cached_certificate_expires_at": { + "name": "cached_certificate_expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "bound_at": { + "name": "bound_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "disconnected_at": { + "name": "disconnected_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_refresh_at": { + "name": "last_refresh_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_refresh_error": { + "name": "last_refresh_error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "license_bindings_active_uniq": { + "name": "license_bindings_active_uniq", + "columns": [ + "status" + ], + "isUnique": true, + "where": "status = 'active'" + }, + "license_bindings_cloud_binding_idx": { + "name": "license_bindings_cloud_binding_idx", + "columns": [ + "cloud_binding_id" + ], + "isUnique": false + }, + "license_bindings_instance_idx": { + "name": "license_bindings_instance_idx", + "columns": [ + "instance_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "matters": { + "name": "matters", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "alias": { + "name": "alias", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "dirtype": { + "name": "dirtype", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "parent": { + "name": "parent", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "object": { + "name": "object", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "storage_id": { + "name": "storage_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'draft'" + }, + "trashed_at": { + "name": "trashed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "purged_at": { + "name": "purged_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_by_actor_type": { + "name": "created_by_actor_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_by_actor_ref": { + "name": "created_by_actor_ref", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_by_actor_issuer": { + "name": "created_by_actor_issuer", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "matters_alias_unique": { + "name": "matters_alias_unique", + "columns": [ + "alias" + ], + "isUnique": true + }, + "matters_status_dir_created_idx": { + "name": "matters_status_dir_created_idx", + "columns": [ + "status", + "dirtype", + "created_at" + ], + "isUnique": false + }, + "matters_webdav_path_idx": { + "name": "matters_webdav_path_idx", + "columns": [ + "org_id", + "parent", + "name", + "status", + "trashed_at", + "purged_at" + ], + "isUnique": false + }, + "matters_webdav_children_idx": { + "name": "matters_webdav_children_idx", + "columns": [ + "org_id", + "parent", + "status", + "trashed_at", + "purged_at", + "\"dirtype\" desc", + "name" + ], + "isUnique": false + }, + "matters_list_page_idx": { + "name": "matters_list_page_idx", + "columns": [ + "org_id", + "parent", + "status", + "trashed_at", + "purged_at", + "\"dirtype\" desc", + "created_at", + "id" + ], + "isUnique": false + }, + "matters_trash_page_idx": { + "name": "matters_trash_page_idx", + "columns": [ + "org_id", + "status", + "purged_at", + "trashed_at", + "created_at", + "id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "notifications": { + "name": "notifications", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "ref_type": { + "name": "ref_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ref_id": { + "name": "ref_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "read_at": { + "name": "read_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "notifications_user_created_idx": { + "name": "notifications_user_created_idx", + "columns": [ + "user_id", + "created_at" + ], + "isUnique": false + }, + "notifications_user_read_idx": { + "name": "notifications_user_read_idx", + "columns": [ + "user_id", + "read_at" + ], + "isUnique": false + }, + "notifications_user_page_idx": { + "name": "notifications_user_page_idx", + "columns": [ + "user_id", + "read_at", + "created_at", + "id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "object_upload_sessions": { + "name": "object_upload_sessions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "object_id": { + "name": "object_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "storage_id": { + "name": "storage_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "storage_key": { + "name": "storage_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "upload_id": { + "name": "upload_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "part_size": { + "name": "part_size", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "on_conflict": { + "name": "on_conflict", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'fail'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "object_upload_sessions_object_idx": { + "name": "object_upload_sessions_object_idx", + "columns": [ + "org_id", + "object_id" + ], + "isUnique": false + }, + "object_upload_sessions_expires_idx": { + "name": "object_upload_sessions_expires_idx", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "org_quota_entitlements": { + "name": "org_quota_entitlements", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "resource_type": { + "name": "resource_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "entitlement_type": { + "name": "entitlement_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'grant'" + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "bytes": { + "name": "bytes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "starts_at": { + "name": "starts_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "org_quota_entitlements_org_resource_idx": { + "name": "org_quota_entitlements_org_resource_idx", + "columns": [ + "org_id", + "resource_type", + "status" + ], + "isUnique": false + }, + "org_quota_entitlements_org_type_idx": { + "name": "org_quota_entitlements_org_type_idx", + "columns": [ + "org_id", + "resource_type", + "entitlement_type", + "status" + ], + "isUnique": false + }, + "org_quota_entitlements_source_resource_uniq": { + "name": "org_quota_entitlements_source_resource_uniq", + "columns": [ + "source", + "source_id", + "resource_type" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "org_quotas": { + "name": "org_quotas", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "quota": { + "name": "quota", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "used": { + "name": "used", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "traffic_quota": { + "name": "traffic_quota", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "traffic_used": { + "name": "traffic_used", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "traffic_period": { + "name": "traffic_period", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'1970-01'" + } + }, + "indexes": { + "org_quotas_org_uniq": { + "name": "org_quotas_org_uniq", + "columns": [ + "org_id" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "remote_download_usage_reports": { + "name": "remote_download_usage_reports", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "downloader_id": { + "name": "downloader_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "event_id": { + "name": "event_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "unit_index": { + "name": "unit_index", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "unit_bytes": { + "name": "unit_bytes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "credits_per_unit": { + "name": "credits_per_unit", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "remote_download_usage_reports_event_id_unique": { + "name": "remote_download_usage_reports_event_id_unique", + "columns": [ + "event_id" + ], + "isUnique": true + }, + "remote_download_usage_task_unit_uniq": { + "name": "remote_download_usage_task_unit_uniq", + "columns": [ + "task_id", + "unit_index" + ], + "isUnique": true + }, + "remote_download_usage_org_idx": { + "name": "remote_download_usage_org_idx", + "columns": [ + "org_id" + ], + "isUnique": false + }, + "remote_download_usage_status_idx": { + "name": "remote_download_usage_status_idx", + "columns": [ + "status" + ], + "isUnique": false + }, + "remote_download_usage_created_idx": { + "name": "remote_download_usage_created_idx", + "columns": [ + "created_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "resource_changes": { + "name": "resource_changes", + "columns": { + "sequence": { + "name": "sequence", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "scope_type": { + "name": "scope_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "resource_type": { + "name": "resource_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "change_type": { + "name": "change_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "occurred_at": { + "name": "occurred_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "resource_changes_scope_sequence_idx": { + "name": "resource_changes_scope_sequence_idx", + "columns": [ + "scope_type", + "scope_id", + "sequence" + ], + "isUnique": false + }, + "resource_changes_resource_sequence_idx": { + "name": "resource_changes_resource_sequence_idx", + "columns": [ + "resource_type", + "resource_id", + "sequence" + ], + "isUnique": false + }, + "resource_changes_occurred_idx": { + "name": "resource_changes_occurred_idx", + "columns": [ + "occurred_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "share_recipients": { + "name": "share_recipients", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "share_id": { + "name": "share_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "recipient_user_id": { + "name": "recipient_user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "recipient_email": { + "name": "recipient_email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "share_recipients_share_id_idx": { + "name": "share_recipients_share_id_idx", + "columns": [ + "share_id" + ], + "isUnique": false + }, + "share_recipients_user_id_idx": { + "name": "share_recipients_user_id_idx", + "columns": [ + "recipient_user_id" + ], + "isUnique": false + }, + "share_recipients_email_idx": { + "name": "share_recipients_email_idx", + "columns": [ + "recipient_email" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "shares": { + "name": "shares", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "matter_id": { + "name": "matter_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "creator_id": { + "name": "creator_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "download_limit": { + "name": "download_limit", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "views": { + "name": "views", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "downloads": { + "name": "downloads", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'active'" + }, + "private": { + "name": "private", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "shares_token_unique": { + "name": "shares_token_unique", + "columns": [ + "token" + ], + "isUnique": true + }, + "shares_creator_status_created_idx": { + "name": "shares_creator_status_created_idx", + "columns": [ + "creator_id", + "status", + "created_at", + "id" + ], + "isUnique": false + }, + "shares_creator_private_created_idx": { + "name": "shares_creator_private_created_idx", + "columns": [ + "creator_id", + "private", + "created_at" + ], + "isUnique": false + }, + "shares_created_idx": { + "name": "shares_created_idx", + "columns": [ + "created_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "site_invitations": { + "name": "site_invitations", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "invited_by": { + "name": "invited_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "accepted_by": { + "name": "accepted_by", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "accepted_at": { + "name": "accepted_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "revoked_by": { + "name": "revoked_by", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "site_invitations_token_unique": { + "name": "site_invitations_token_unique", + "columns": [ + "token" + ], + "isUnique": true + }, + "site_invitations_email_idx": { + "name": "site_invitations_email_idx", + "columns": [ + "email" + ], + "isUnique": false + }, + "site_invitations_created_idx": { + "name": "site_invitations_created_idx", + "columns": [ + "created_at" + ], + "isUnique": false + }, + "site_invitations_expires_idx": { + "name": "site_invitations_expires_idx", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "stats_rollups_hourly": { + "name": "stats_rollups_hourly", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "bucket_start": { + "name": "bucket_start", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "metric_key": { + "name": "metric_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dimension_key": { + "name": "dimension_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "dimension_value": { + "name": "dimension_value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "count": { + "name": "count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "bytes": { + "name": "bytes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "unique_count": { + "name": "unique_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "stats_rollups_hourly_bucket_metric_dim_uniq": { + "name": "stats_rollups_hourly_bucket_metric_dim_uniq", + "columns": [ + "bucket_start", + "org_id", + "metric_key", + "dimension_key", + "dimension_value" + ], + "isUnique": true + }, + "stats_rollups_hourly_metric_bucket_idx": { + "name": "stats_rollups_hourly_metric_bucket_idx", + "columns": [ + "metric_key", + "bucket_start" + ], + "isUnique": false + }, + "stats_rollups_hourly_dimension_bucket_idx": { + "name": "stats_rollups_hourly_dimension_bucket_idx", + "columns": [ + "metric_key", + "dimension_key", + "bucket_start" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "storage_usage_breakdowns": { + "name": "storage_usage_breakdowns", + "columns": { + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "bytes": { + "name": "bytes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "file_count": { + "name": "file_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "storage_usage_breakdowns_org_category_uniq": { + "name": "storage_usage_breakdowns_org_category_uniq", + "columns": [ + "org_id", + "category" + ], + "isUnique": true + }, + "storage_usage_breakdowns_org_idx": { + "name": "storage_usage_breakdowns_org_idx", + "columns": [ + "org_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "storage_usage_ledger": { + "name": "storage_usage_ledger", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "event_key": { + "name": "event_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "storage_id": { + "name": "storage_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "resource_type": { + "name": "resource_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "delta_bytes": { + "name": "delta_bytes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "occurred_at": { + "name": "occurred_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "storage_usage_ledger_event_key_unique": { + "name": "storage_usage_ledger_event_key_unique", + "columns": [ + "event_key" + ], + "isUnique": true + }, + "storage_usage_ledger_occurred_idx": { + "name": "storage_usage_ledger_occurred_idx", + "columns": [ + "occurred_at" + ], + "isUnique": false + }, + "storage_usage_ledger_org_occurred_idx": { + "name": "storage_usage_ledger_org_occurred_idx", + "columns": [ + "org_id", + "occurred_at" + ], + "isUnique": false + }, + "storage_usage_ledger_storage_occurred_idx": { + "name": "storage_usage_ledger_storage_occurred_idx", + "columns": [ + "storage_id", + "occurred_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "storages": { + "name": "storages", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "bucket": { + "name": "bucket", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "endpoint": { + "name": "endpoint", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "region": { + "name": "region", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'auto'" + }, + "access_key": { + "name": "access_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "secret_key": { + "name": "secret_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "file_path": { + "name": "file_path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "custom_host": { + "name": "custom_host", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "''" + }, + "capacity": { + "name": "capacity", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "egress_credit_billing_enabled": { + "name": "egress_credit_billing_enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "egress_credit_unit_bytes": { + "name": "egress_credit_unit_bytes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 104857600 + }, + "egress_credit_per_unit": { + "name": "egress_credit_per_unit", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "force_path_style": { + "name": "force_path_style", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "used": { + "name": "used", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'unknown'" + }, + "status_reason": { + "name": "status_reason", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status_checked_at": { + "name": "status_checked_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "system_options": { + "name": "system_options", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "team_invite_links": { + "name": "team_invite_links", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'member'" + }, + "inviter_id": { + "name": "inviter_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "team_invite_links_token_unique": { + "name": "team_invite_links_token_unique", + "columns": [ + "token" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "webdav_dead_properties": { + "name": "webdav_dead_properties", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "resource_path": { + "name": "resource_path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "namespace": { + "name": "namespace", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "webdav_dead_properties_resource_prop_uniq": { + "name": "webdav_dead_properties_resource_prop_uniq", + "columns": [ + "org_id", + "resource_path", + "namespace", + "name" + ], + "isUnique": true + }, + "webdav_dead_properties_resource_idx": { + "name": "webdav_dead_properties_resource_idx", + "columns": [ + "org_id", + "resource_path" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "webdav_locks": { + "name": "webdav_locks", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "resource_path": { + "name": "resource_path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "owner": { + "name": "owner", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "depth": { + "name": "depth", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'infinity'" + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "webdav_locks_token_unique": { + "name": "webdav_locks_token_unique", + "columns": [ + "token" + ], + "isUnique": true + }, + "webdav_locks_resource_idx": { + "name": "webdav_locks_resource_idx", + "columns": [ + "org_id", + "resource_path" + ], + "isUnique": false + }, + "webdav_locks_expires_idx": { + "name": "webdav_locks_expires_idx", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "webhook_events": { + "name": "webhook_events", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'cloud'" + }, + "event_id": { + "name": "event_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'order.quota_changed'" + }, + "payload_hash": { + "name": "payload_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "raw_payload": { + "name": "raw_payload", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "processed_at": { + "name": "processed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "webhook_events_source_event_uniq": { + "name": "webhook_events_source_event_uniq", + "columns": [ + "source", + "event_id" + ], + "isUnique": true + }, + "webhook_events_source_created_idx": { + "name": "webhook_events_source_created_idx", + "columns": [ + "source", + "created_at" + ], + "isUnique": false + }, + "webhook_events_status_idx": { + "name": "webhook_events_status_idx", + "columns": [ + "status" + ], + "isUnique": false + }, + "webhook_events_processed_idx": { + "name": "webhook_events_processed_idx", + "columns": [ + "processed_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "x402_capacity_purchase_intents": { + "name": "x402_capacity_purchase_intents", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "request_hash": { + "name": "request_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cloud_order_id": { + "name": "cloud_order_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cloud_attempt_id": { + "name": "cloud_attempt_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'created'" + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "x402_capacity_purchase_intents_org_request_uniq": { + "name": "x402_capacity_purchase_intents_org_request_uniq", + "columns": [ + "org_id", + "resource_id", + "request_hash" + ], + "isUnique": true + }, + "x402_capacity_purchase_intents_org_idempotency_uniq": { + "name": "x402_capacity_purchase_intents_org_idempotency_uniq", + "columns": [ + "org_id", + "idempotency_key" + ], + "isUnique": true + }, + "x402_capacity_purchase_intents_attempt_idx": { + "name": "x402_capacity_purchase_intents_attempt_idx", + "columns": [ + "cloud_attempt_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "account": { + "name": "account", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "issuer": { + "name": "issuer", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "account_userId_idx": { + "name": "account_userId_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "account_issuer_providerAccountId_unique": { + "name": "account_issuer_providerAccountId_unique", + "columns": [ + "issuer", + "account_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "apikey": { + "name": "apikey", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "config_id": { + "name": "config_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'default'" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "start": { + "name": "start", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "prefix": { + "name": "prefix", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "refill_interval": { + "name": "refill_interval", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "refill_amount": { + "name": "refill_amount", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_refill_at": { + "name": "last_refill_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "rate_limit_enabled": { + "name": "rate_limit_enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "rate_limit_time_window": { + "name": "rate_limit_time_window", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rate_limit_max": { + "name": "rate_limit_max", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "request_count": { + "name": "request_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "remaining": { + "name": "remaining", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_request": { + "name": "last_request", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permissions": { + "name": "permissions", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "apikey_config_id_idx": { + "name": "apikey_config_id_idx", + "columns": [ + "config_id" + ], + "isUnique": false + }, + "apikey_reference_id_idx": { + "name": "apikey_reference_id_idx", + "columns": [ + "reference_id" + ], + "isUnique": false + }, + "apikey_key_idx": { + "name": "apikey_key_idx", + "columns": [ + "key" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "deviceCode": { + "name": "deviceCode", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "device_code": { + "name": "device_code", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_code": { + "name": "user_code", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_polled_at": { + "name": "last_polled_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "polling_interval": { + "name": "polling_interval", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + } + }, + "indexes": { + "deviceCode_device_code_idx": { + "name": "deviceCode_device_code_idx", + "columns": [ + "device_code" + ], + "isUnique": false + }, + "deviceCode_user_code_idx": { + "name": "deviceCode_user_code_idx", + "columns": [ + "user_code" + ], + "isUnique": false + }, + "deviceCode_status_idx": { + "name": "deviceCode_status_idx", + "columns": [ + "status" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "downloader_bootstrap_credentials": { + "name": "downloader_bootstrap_credentials", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_code": { + "name": "device_code", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "consumed_at": { + "name": "consumed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + } + }, + "indexes": { + "downloader_bootstrap_credentials_token_hash_unique": { + "name": "downloader_bootstrap_credentials_token_hash_unique", + "columns": [ + "token_hash" + ], + "isUnique": true + }, + "downloader_bootstrap_token_hash_idx": { + "name": "downloader_bootstrap_token_hash_idx", + "columns": [ + "token_hash" + ], + "isUnique": false + }, + "downloader_bootstrap_user_idx": { + "name": "downloader_bootstrap_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "downloader_bootstrap_consumed_idx": { + "name": "downloader_bootstrap_consumed_idx", + "columns": [ + "consumed_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "downloader_bootstrap_credentials_user_id_user_id_fk": { + "name": "downloader_bootstrap_credentials_user_id_user_id_fk", + "tableFrom": "downloader_bootstrap_credentials", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "invitation": { + "name": "invitation", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'pending'" + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + }, + "inviter_id": { + "name": "inviter_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "invitation_organizationId_idx": { + "name": "invitation_organizationId_idx", + "columns": [ + "organization_id" + ], + "isUnique": false + }, + "invitation_email_idx": { + "name": "invitation_email_idx", + "columns": [ + "email" + ], + "isUnique": false + } + }, + "foreignKeys": { + "invitation_organization_id_organization_id_fk": { + "name": "invitation_organization_id_organization_id_fk", + "tableFrom": "invitation", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "invitation_inviter_id_user_id_fk": { + "name": "invitation_inviter_id_user_id_fk", + "tableFrom": "invitation", + "tableTo": "user", + "columnsFrom": [ + "inviter_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "jwks": { + "name": "jwks", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "alg": { + "name": "alg", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "crv": { + "name": "crv", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "member": { + "name": "member", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'member'" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + } + }, + "indexes": { + "member_organizationId_idx": { + "name": "member_organizationId_idx", + "columns": [ + "organization_id" + ], + "isUnique": false + }, + "member_userId_idx": { + "name": "member_userId_idx", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "member_organization_id_organization_id_fk": { + "name": "member_organization_id_organization_id_fk", + "tableFrom": "member", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "member_user_id_user_id_fk": { + "name": "member_user_id_user_id_fk", + "tableFrom": "member", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "oauthAccessToken": { + "name": "oauthAccessToken", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "authorization_code_id": { + "name": "authorization_code_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "resources": { + "name": "resources", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "requested_user_info_claims": { + "name": "requested_user_info_claims", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "authorization_details": { + "name": "authorization_details", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "refresh_id": { + "name": "refresh_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + }, + "revoked": { + "name": "revoked", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "confirmation": { + "name": "confirmation", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "oauthAccessToken_token_unique": { + "name": "oauthAccessToken_token_unique", + "columns": [ + "token" + ], + "isUnique": true + }, + "oauthAccessToken_client_id_idx": { + "name": "oauthAccessToken_client_id_idx", + "columns": [ + "client_id" + ], + "isUnique": false + }, + "oauthAccessToken_session_id_idx": { + "name": "oauthAccessToken_session_id_idx", + "columns": [ + "session_id" + ], + "isUnique": false + }, + "oauthAccessToken_user_id_idx": { + "name": "oauthAccessToken_user_id_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "oauthAccessToken_refresh_id_idx": { + "name": "oauthAccessToken_refresh_id_idx", + "columns": [ + "refresh_id" + ], + "isUnique": false + }, + "oauthAccessToken_token_idx": { + "name": "oauthAccessToken_token_idx", + "columns": [ + "token" + ], + "isUnique": false + } + }, + "foreignKeys": { + "oauthAccessToken_client_id_oauthClient_client_id_fk": { + "name": "oauthAccessToken_client_id_oauthClient_client_id_fk", + "tableFrom": "oauthAccessToken", + "tableTo": "oauthClient", + "columnsFrom": [ + "client_id" + ], + "columnsTo": [ + "client_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oauthAccessToken_session_id_session_id_fk": { + "name": "oauthAccessToken_session_id_session_id_fk", + "tableFrom": "oauthAccessToken", + "tableTo": "session", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "oauthAccessToken_user_id_user_id_fk": { + "name": "oauthAccessToken_user_id_user_id_fk", + "tableFrom": "oauthAccessToken", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oauthAccessToken_refresh_id_oauthRefreshToken_id_fk": { + "name": "oauthAccessToken_refresh_id_oauthRefreshToken_id_fk", + "tableFrom": "oauthAccessToken", + "tableTo": "oauthRefreshToken", + "columnsFrom": [ + "refresh_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "oauthClient": { + "name": "oauthClient", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_secret": { + "name": "client_secret", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "disabled": { + "name": "disabled", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "skip_consent": { + "name": "skip_consent", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_end_session": { + "name": "enable_end_session", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "subject_type": { + "name": "subject_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "uri": { + "name": "uri", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "contacts": { + "name": "contacts", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tos": { + "name": "tos", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "policy": { + "name": "policy", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "software_id": { + "name": "software_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "software_version": { + "name": "software_version", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "software_statement": { + "name": "software_statement", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "redirect_uris": { + "name": "redirect_uris", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "post_logout_redirect_uris": { + "name": "post_logout_redirect_uris", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "backchannel_logout_uri": { + "name": "backchannel_logout_uri", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "backchannel_logout_session_required": { + "name": "backchannel_logout_session_required", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "token_endpoint_auth_method": { + "name": "token_endpoint_auth_method", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "jwks": { + "name": "jwks", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "jwks_uri": { + "name": "jwks_uri", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "grant_types": { + "name": "grant_types", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "response_types": { + "name": "response_types", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "public": { + "name": "public", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "require_pkce": { + "name": "require_pkce", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "dpop_bound_access_tokens": { + "name": "dpop_bound_access_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "oauthClient_client_id_unique": { + "name": "oauthClient_client_id_unique", + "columns": [ + "client_id" + ], + "isUnique": true + }, + "oauthClient_client_id_idx": { + "name": "oauthClient_client_id_idx", + "columns": [ + "client_id" + ], + "isUnique": false + }, + "oauthClient_user_id_idx": { + "name": "oauthClient_user_id_idx", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "oauthClient_user_id_user_id_fk": { + "name": "oauthClient_user_id_user_id_fk", + "tableFrom": "oauthClient", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "oauthClientAssertion": { + "name": "oauthClientAssertion", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "oauthClientRegistration": { + "name": "oauthClientRegistration", + "columns": { + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + } + }, + "indexes": { + "oauthClientRegistration_token_hash_unique": { + "name": "oauthClientRegistration_token_hash_unique", + "columns": [ + "token_hash" + ], + "isUnique": true + }, + "oauthClientRegistration_token_hash_idx": { + "name": "oauthClientRegistration_token_hash_idx", + "columns": [ + "token_hash" + ], + "isUnique": false + } + }, + "foreignKeys": { + "oauthClientRegistration_client_id_oauthClient_client_id_fk": { + "name": "oauthClientRegistration_client_id_oauthClient_client_id_fk", + "tableFrom": "oauthClientRegistration", + "tableTo": "oauthClient", + "columnsFrom": [ + "client_id" + ], + "columnsTo": [ + "client_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "oauthClientResource": { + "name": "oauthClientResource", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + } + }, + "indexes": { + "oauthClientResource_client_id_idx": { + "name": "oauthClientResource_client_id_idx", + "columns": [ + "client_id" + ], + "isUnique": false + }, + "oauthClientResource_resource_id_idx": { + "name": "oauthClientResource_resource_id_idx", + "columns": [ + "resource_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "oauthClientResource_client_id_oauthClient_client_id_fk": { + "name": "oauthClientResource_client_id_oauthClient_client_id_fk", + "tableFrom": "oauthClientResource", + "tableTo": "oauthClient", + "columnsFrom": [ + "client_id" + ], + "columnsTo": [ + "client_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oauthClientResource_resource_id_oauthResource_identifier_fk": { + "name": "oauthClientResource_resource_id_oauthResource_identifier_fk", + "tableFrom": "oauthClientResource", + "tableTo": "oauthResource", + "columnsFrom": [ + "resource_id" + ], + "columnsTo": [ + "identifier" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "oauthConsent": { + "name": "oauthConsent", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "resources": { + "name": "resources", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "requested_user_info_claims": { + "name": "requested_user_info_claims", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "authorization_details": { + "name": "authorization_details", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + }, + "last_used_at": { + "name": "last_used_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + } + }, + "indexes": { + "oauthConsent_client_id_idx": { + "name": "oauthConsent_client_id_idx", + "columns": [ + "client_id" + ], + "isUnique": false + }, + "oauthConsent_user_id_idx": { + "name": "oauthConsent_user_id_idx", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "oauthConsent_client_id_oauthClient_client_id_fk": { + "name": "oauthConsent_client_id_oauthClient_client_id_fk", + "tableFrom": "oauthConsent", + "tableTo": "oauthClient", + "columnsFrom": [ + "client_id" + ], + "columnsTo": [ + "client_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oauthConsent_user_id_user_id_fk": { + "name": "oauthConsent_user_id_user_id_fk", + "tableFrom": "oauthConsent", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "oauthJwtRevocation": { + "name": "oauthJwtRevocation", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + } + }, + "indexes": { + "oauthJwtRevocation_expires_at_idx": { + "name": "oauthJwtRevocation_expires_at_idx", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "oauthPushedAuthorizationRequest": { + "name": "oauthPushedAuthorizationRequest", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "request_uri": { + "name": "request_uri", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "parameters": { + "name": "parameters", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + } + }, + "indexes": { + "oauthPushedAuthorizationRequest_request_uri_unique": { + "name": "oauthPushedAuthorizationRequest_request_uri_unique", + "columns": [ + "request_uri" + ], + "isUnique": true + }, + "oauthPushedAuthorizationRequest_client_id_idx": { + "name": "oauthPushedAuthorizationRequest_client_id_idx", + "columns": [ + "client_id" + ], + "isUnique": false + }, + "oauthPushedAuthorizationRequest_expires_at_idx": { + "name": "oauthPushedAuthorizationRequest_expires_at_idx", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "oauthPushedAuthorizationRequest_client_id_oauthClient_client_id_fk": { + "name": "oauthPushedAuthorizationRequest_client_id_oauthClient_client_id_fk", + "tableFrom": "oauthPushedAuthorizationRequest", + "tableTo": "oauthClient", + "columnsFrom": [ + "client_id" + ], + "columnsTo": [ + "client_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "oauthRefreshToken": { + "name": "oauthRefreshToken", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "authorization_code_id": { + "name": "authorization_code_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "resources": { + "name": "resources", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "requested_user_info_claims": { + "name": "requested_user_info_claims", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "authorization_details": { + "name": "authorization_details", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + }, + "revoked": { + "name": "revoked", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rotated_at": { + "name": "rotated_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rotation_replay_response": { + "name": "rotation_replay_response", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rotation_replay_expires_at": { + "name": "rotation_replay_expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auth_time": { + "name": "auth_time", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "confirmation": { + "name": "confirmation", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "oauthRefreshToken_token_unique": { + "name": "oauthRefreshToken_token_unique", + "columns": [ + "token" + ], + "isUnique": true + }, + "oauthRefreshToken_client_id_idx": { + "name": "oauthRefreshToken_client_id_idx", + "columns": [ + "client_id" + ], + "isUnique": false + }, + "oauthRefreshToken_session_id_idx": { + "name": "oauthRefreshToken_session_id_idx", + "columns": [ + "session_id" + ], + "isUnique": false + }, + "oauthRefreshToken_user_id_idx": { + "name": "oauthRefreshToken_user_id_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "oauthRefreshToken_token_idx": { + "name": "oauthRefreshToken_token_idx", + "columns": [ + "token" + ], + "isUnique": false + } + }, + "foreignKeys": { + "oauthRefreshToken_client_id_oauthClient_client_id_fk": { + "name": "oauthRefreshToken_client_id_oauthClient_client_id_fk", + "tableFrom": "oauthRefreshToken", + "tableTo": "oauthClient", + "columnsFrom": [ + "client_id" + ], + "columnsTo": [ + "client_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oauthRefreshToken_session_id_session_id_fk": { + "name": "oauthRefreshToken_session_id_session_id_fk", + "tableFrom": "oauthRefreshToken", + "tableTo": "session", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "oauthRefreshToken_user_id_user_id_fk": { + "name": "oauthRefreshToken_user_id_user_id_fk", + "tableFrom": "oauthRefreshToken", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "oauthResource": { + "name": "oauthResource", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "access_token_ttl": { + "name": "access_token_ttl", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "refresh_token_ttl": { + "name": "refresh_token_ttl", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "signing_algorithm": { + "name": "signing_algorithm", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "signing_key_id": { + "name": "signing_key_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "allowed_scopes": { + "name": "allowed_scopes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "custom_claims": { + "name": "custom_claims", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "dpop_bound_access_tokens_required": { + "name": "dpop_bound_access_tokens_required", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "disabled": { + "name": "disabled", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "policy_version": { + "name": "policy_version", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 1 + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + } + }, + "indexes": { + "oauthResource_identifier_unique": { + "name": "oauthResource_identifier_unique", + "columns": [ + "identifier" + ], + "isUnique": true + }, + "oauthResource_identifier_idx": { + "name": "oauthResource_identifier_idx", + "columns": [ + "identifier" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "organization": { + "name": "organization", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "logo": { + "name": "logo", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + } + }, + "indexes": { + "organization_slug_unique": { + "name": "organization_slug_unique", + "columns": [ + "slug" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "session": { + "name": "session", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "impersonated_by": { + "name": "impersonated_by", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "active_organization_id": { + "name": "active_organization_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "session_token_unique": { + "name": "session_token_unique", + "columns": [ + "token" + ], + "isUnique": true + }, + "session_userId_idx": { + "name": "session_userId_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "session_created_idx": { + "name": "session_created_idx", + "columns": [ + "created_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user": { + "name": "user", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email_verified": { + "name": "email_verified", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "banned": { + "name": "banned", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "ban_reason": { + "name": "ban_reason", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ban_expires": { + "name": "ban_expires", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "display_username": { + "name": "display_username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_active_at": { + "name": "last_active_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + } + }, + "indexes": { + "user_email_unique": { + "name": "user_email_unique", + "columns": [ + "email" + ], + "isUnique": true + }, + "user_username_unique": { + "name": "user_username_unique", + "columns": [ + "username" + ], + "isUnique": true + }, + "user_created_idx": { + "name": "user_created_idx", + "columns": [ + "created_at" + ], + "isUnique": false + }, + "user_lastActiveAt_idx": { + "name": "user_lastActiveAt_idx", + "columns": [ + "last_active_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "verification": { + "name": "verification", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + } + }, + "indexes": { + "verification_identifier_idx": { + "name": "verification_identifier_idx", + "columns": [ + "identifier" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": { + "matters_webdav_children_idx": { + "columns": { + "\"dirtype\" desc": { + "isExpression": true + } + } + }, + "matters_list_page_idx": { + "columns": { + "\"dirtype\" desc": { + "isExpression": true + } + } + } + } + } +} \ No newline at end of file diff --git a/migrations/meta/_journal.json b/migrations/meta/_journal.json index 3e030383..912fe55e 100644 --- a/migrations/meta/_journal.json +++ b/migrations/meta/_journal.json @@ -645,6 +645,13 @@ "when": 1785867717485, "tag": "0092_audit-event-key", "breakpoints": true + }, + { + "idx": 93, + "version": "6", + "when": 1786165482732, + "tag": "0093_actor_attribution", + "breakpoints": true } ] } \ No newline at end of file diff --git a/server/adapters/gateways/archive-jobs.ts b/server/adapters/gateways/archive-jobs.ts index 1a540879..8b907c65 100644 --- a/server/adapters/gateways/archive-jobs.ts +++ b/server/adapters/gateways/archive-jobs.ts @@ -75,6 +75,7 @@ export function createArchiveJobsGateway(platform: Platform): ArchiveJobsGateway await processArchiveJob(deps, { orgId: message.orgId, userId: message.userId, + createdBy: message.createdBy, request: message.request, jobId: message.jobId, }) diff --git a/server/adapters/repos/audit-actor-directory.integration.test.ts b/server/adapters/repos/audit-actor-directory.integration.test.ts index b5299895..972716f2 100644 --- a/server/adapters/repos/audit-actor-directory.integration.test.ts +++ b/server/adapters/repos/audit-actor-directory.integration.test.ts @@ -1,10 +1,41 @@ import { describe, expect, it } from 'vitest' -import { apikey, oauthClient } from '../../db/auth-schema' +import { apikey, oauthClient, user } from '../../db/auth-schema' import { downloaders } from '../../db/schema' import { createTestApp } from '../../test/setup' import { createAuditActorDirectoryRepo } from './audit-actor-directory' describe('audit actor directory repository', () => { + it('resolves user display profiles in one local lookup', async () => { + const { db } = await createTestApp() + await db.insert(user).values({ + id: 'user-profile-1', + name: 'Amber', + email: 'amber-profile@example.com', + image: 'https://example.com/amber.png', + }) + const directory = createAuditActorDirectoryRepo(db) + + await expect(directory.findUserProfiles(['user-profile-1', 'missing'])).resolves.toEqual( + new Map([['user-profile-1', { name: 'Amber', image: 'https://example.com/amber.png', resolved: true }]]), + ) + }) + + it('falls back to the username when a user has no profile name', async () => { + const { db } = await createTestApp() + await db.insert(user).values({ + id: 'username-only-profile', + name: '', + username: 'amber', + displayUsername: 'Amber', + email: 'username-only@example.com', + }) + const directory = createAuditActorDirectoryRepo(db) + + await expect(directory.findUserProfiles(['username-only-profile'])).resolves.toEqual( + new Map([['username-only-profile', { name: 'Amber', image: null, resolved: true }]]), + ) + }) + it('resolves API key names in one local lookup', async () => { const { db } = await createTestApp() await db.insert(apikey).values({ @@ -89,5 +120,6 @@ describe('audit actor directory repository', () => { await expect(directory.findApiKeyNames([])).resolves.toEqual(new Map()) await expect(directory.findDeviceNames([])).resolves.toEqual(new Map()) + await expect(directory.findUserProfiles([])).resolves.toEqual(new Map()) }) }) diff --git a/server/adapters/repos/audit-actor-directory.ts b/server/adapters/repos/audit-actor-directory.ts index 75ccf707..6ce6dbe2 100644 --- a/server/adapters/repos/audit-actor-directory.ts +++ b/server/adapters/repos/audit-actor-directory.ts @@ -1,11 +1,39 @@ import { inArray } from 'drizzle-orm' -import { apikey, oauthClient } from '../../db/auth-schema' +import { apikey, oauthClient, user } from '../../db/auth-schema' import { downloaders } from '../../db/schema' import type { Database } from '../../platform/interface' import type { AuditActorDirectory } from '../../usecases/ports' export function createAuditActorDirectoryRepo(db: Database): AuditActorDirectory { return { + async findUserProfiles(userIds) { + const uniqueIds = [...new Set(userIds)] + if (uniqueIds.length === 0) return new Map() + const rows = await db + .select({ + id: user.id, + name: user.name, + username: user.username, + displayUsername: user.displayUsername, + image: user.image, + }) + .from(user) + .where(inArray(user.id, uniqueIds)) + return new Map( + rows.map( + (row) => + [ + row.id, + { + name: row.name || row.displayUsername || row.username || row.id, + image: row.image, + resolved: true, + }, + ] as const, + ), + ) + }, + async findApiKeyNames(keyIds) { const uniqueIds = [...new Set(keyIds)] if (uniqueIds.length === 0) return new Map() diff --git a/server/adapters/repos/download-task.ts b/server/adapters/repos/download-task.ts index 72a187b9..c097dae1 100644 --- a/server/adapters/repos/download-task.ts +++ b/server/adapters/repos/download-task.ts @@ -1,6 +1,6 @@ import { toDownloadTaskListItem } from '@shared/download-task' import { generateId } from '@shared/ids' -import { downloadTaskRuntimeSchema } from '@shared/schemas' +import { type ActorType, downloadTaskRuntimeSchema } from '@shared/schemas' import type { DownloadTask, DownloadTaskRuntime } from '@shared/types' import { and, @@ -21,6 +21,7 @@ import { } from 'drizzle-orm' import { downloaders, downloadTasks } from '../../db/schema' import { executeWriteTransaction, executeWriteTransactionWithResults } from '../../db/transaction' +import { fallbackActorAttribution } from '../../domain/actor-attribution' import { parseDownloadTaskEvents } from '../../domain/download-task-events' import type { Database } from '../../platform/interface' import { @@ -203,6 +204,9 @@ function toRecord(row: DownloadTaskRow): DownloadTaskRecord { id: row.id, orgId: row.orgId, createdByUserId: row.createdByUserId, + requestedByActorType: row.requestedByActorType as DownloadTaskRecord['requestedByActorType'], + requestedByActorRef: row.requestedByActorRef, + requestedByActorIssuer: row.requestedByActorIssuer, sourceType: row.sourceType, sourceUri: row.sourceUri, displayName: row.displayName, @@ -239,10 +243,19 @@ function toRecord(row: DownloadTaskRow): DownloadTaskRecord { function toDownloadTask(row: DownloadTaskRow, control?: { action: 'delete'; requestedAt: string }): DownloadTask { const runtime = parseTaskRuntime(row.runtime) + const requestedBy = + row.requestedByActorType && row.requestedByActorRef + ? fallbackActorAttribution({ + type: row.requestedByActorType as ActorType, + ref: row.requestedByActorRef, + issuer: row.requestedByActorIssuer, + }) + : null return { id: row.id, orgId: row.orgId, createdBy: row.createdByUserId, + requestedBy, spec: { source: { type: row.sourceType as DownloadTask['spec']['source']['type'], @@ -261,7 +274,11 @@ function toDownloadTask(row: DownloadTaskRow, control?: { action: 'delete'; requ state: row.status as DownloadTask['status']['state'], attempt: row.attempt, assignment: row.assignedDownloaderId - ? { downloaderId: row.assignedDownloaderId, assignedAt: row.assignedAt?.toISOString() ?? null } + ? { + downloaderId: row.assignedDownloaderId, + assignedAt: row.assignedAt?.toISOString() ?? null, + executor: fallbackActorAttribution({ type: 'device', ref: row.assignedDownloaderId, issuer: null }), + } : null, progress: runtime?.progress ?? emptyTaskProgress(), billing: { @@ -361,6 +378,9 @@ export function createDownloadTaskRepo(db: Database): DownloadTaskRepo { id: input.id, orgId: input.orgId, createdByUserId: input.createdByUserId, + requestedByActorType: input.requestedByActorType ?? 'user', + requestedByActorRef: input.requestedByActorRef ?? input.createdByUserId, + requestedByActorIssuer: input.requestedByActorIssuer ?? null, sourceType: input.sourceType, sourceUri: input.sourceUri, displayName: input.displayName, diff --git a/server/adapters/repos/matter.ts b/server/adapters/repos/matter.ts index 83c68d25..d0098b18 100644 --- a/server/adapters/repos/matter.ts +++ b/server/adapters/repos/matter.ts @@ -310,6 +310,9 @@ export function createMatterRepo(db: Database): MatterRepo { status: input.status, trashedAt: null, purgedAt: null, + createdByActorType: input.createdByActorType ?? null, + createdByActorRef: input.createdByActorRef ?? null, + createdByActorIssuer: input.createdByActorIssuer ?? null, createdAt: now, updatedAt: now, } @@ -521,6 +524,9 @@ export function createMatterRepo(db: Database): MatterRepo { status: 'active', trashedAt: null, purgedAt: null, + createdByActorType: opts.createdByActorType ?? null, + createdByActorRef: opts.createdByActorRef ?? null, + createdByActorIssuer: opts.createdByActorIssuer ?? null, createdAt: now, updatedAt: now, } diff --git a/server/db/schema.ts b/server/db/schema.ts index 53df9baf..d146c841 100644 --- a/server/db/schema.ts +++ b/server/db/schema.ts @@ -18,6 +18,9 @@ export const matters = sqliteTable( status: text('status').notNull().default('draft'), // draft, active trashedAt: integer('trashed_at'), // null = live, epoch ms = in trash (soft delete) purgedAt: integer('purged_at'), // null = retained/billable, epoch ms = content permanently removed + createdByActorType: text('created_by_actor_type'), + createdByActorRef: text('created_by_actor_ref'), + createdByActorIssuer: text('created_by_actor_issuer'), createdAt: integer('created_at', { mode: 'timestamp' }).notNull(), updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull(), }, @@ -412,6 +415,9 @@ export const downloadTasks = sqliteTable( id: text('id').primaryKey(), orgId: text('org_id').notNull(), createdByUserId: text('created_by_user_id').notNull(), + requestedByActorType: text('requested_by_actor_type'), + requestedByActorRef: text('requested_by_actor_ref'), + requestedByActorIssuer: text('requested_by_actor_issuer'), sourceType: text('source_type').notNull(), sourceUri: text('source_uri').notNull(), displayName: text('display_name'), diff --git a/server/domain/actor-attribution.ts b/server/domain/actor-attribution.ts new file mode 100644 index 00000000..b462023b --- /dev/null +++ b/server/domain/actor-attribution.ts @@ -0,0 +1,23 @@ +import type { ActorAttribution } from '@shared/schemas' + +type ActorIdentity = Pick +type ActorProfile = Pick + +export function fallbackActorProfile(identity: ActorIdentity): ActorProfile { + const ref = identity.ref ?? 'unknown' + const labels: Partial> = { + user: 'User', + api_key: 'API key', + oauth: 'Agent', + agent: 'Agent', + device: 'Device', + system: 'System', + anonymous: 'Anonymous', + 'task-upload': 'Task upload', + } + return { name: `${labels[identity.type] ?? 'Actor'} · ${ref}`, image: null, resolved: false } +} + +export function fallbackActorAttribution(identity: ActorIdentity): ActorAttribution { + return { ...identity, ...fallbackActorProfile(identity) } +} diff --git a/server/http/background-jobs.integration.test.ts b/server/http/background-jobs.integration.test.ts index 88c01eb3..b228460b 100644 --- a/server/http/background-jobs.integration.test.ts +++ b/server/http/background-jobs.integration.test.ts @@ -143,6 +143,7 @@ describe('background jobs API', () => { const created = (await res.json()) as { id: string; status: string } expect(created.status).toBe('queued') expect(messages).toHaveLength(1) + expect(messages[0].createdBy).toMatchObject({ type: 'user', issuer: null }) await expect(createBackgroundJobRepo(db).get(orgId, created.id)).resolves.toMatchObject({ status: 'queued' }) await createArchiveJobsGateway(platform).runMessage(messages[0]) diff --git a/server/http/background-jobs.ts b/server/http/background-jobs.ts index d4b74c5b..97d1ed2b 100644 --- a/server/http/background-jobs.ts +++ b/server/http/background-jobs.ts @@ -6,7 +6,7 @@ import { listBackgroundJobsQuerySchema, opaqueIdSchema, } from '../../shared/schemas' -import type { Env } from '../middleware/platform' +import { authzActorIdentity, type Env } from '../middleware/platform' import { cancelBackgroundJob, createBackgroundJob, @@ -203,7 +203,12 @@ const backgroundJobs = app const orgId = requireOrg(c) const userId = c.get('userId') if (!userId) throw new BackgroundJobError('not_found') - return c.json(await createBackgroundJob(c.get('deps'), { orgId, userId, request: c.req.valid('json') }), 201) + const createdBy = authzActorIdentity(c.get('authzContext')) + if (!createdBy) throw new Error('authenticated_actor_missing') + return c.json( + await createBackgroundJob(c.get('deps'), { orgId, userId, createdBy, request: c.req.valid('json') }), + 201, + ) }) .openapi(getJobRoute, async (c) => c.json(await getBackgroundJob(c.get('deps'), requireOrg(c), c.req.valid('param').id), 200), @@ -211,8 +216,10 @@ const backgroundJobs = app .openapi(cancelJobRoute, async (c) => c.json(await cancelBackgroundJob(c.get('deps'), requireOrg(c), c.req.valid('param').id), 200), ) - .openapi(retryJobRoute, async (c) => - c.json(await retryBackgroundJob(c.get('deps'), requireOrg(c), c.req.valid('param').id), 201), - ) + .openapi(retryJobRoute, async (c) => { + const createdBy = authzActorIdentity(c.get('authzContext')) + if (!createdBy) throw new Error('authenticated_actor_missing') + return c.json(await retryBackgroundJob(c.get('deps'), requireOrg(c), c.req.valid('param').id, createdBy), 201) + }) export default backgroundJobs diff --git a/server/http/downloads/download-tasks.integration.test.ts b/server/http/downloads/download-tasks.integration.test.ts index 5739ea9b..d79d1957 100644 --- a/server/http/downloads/download-tasks.integration.test.ts +++ b/server/http/downloads/download-tasks.integration.test.ts @@ -319,7 +319,7 @@ describe('Download tasks API integration', () => { expect(new Set(ids)).toHaveLength(3) }) - it('uses the API key owner UID in the object storage key', async () => { + it('uses the API key owner UID in the object storage key [spec: download-tasks/actor-attribution]', async () => { const { app, db } = await createTestApp({ DOWNLOAD_TOKEN_SECRET: 'test-download-token-secret' }) await insertStorage(db) const admin = await adminHeaders(app) @@ -359,9 +359,18 @@ describe('Download tasks API integration', () => { expect(createTaskRes.status).toBe(201) const task = (await createTaskRes.json()) as DownloadTask expect(task.createdBy).toBe(identity.userId) + expect(task.requestedBy).toMatchObject({ type: 'api_key', name: 'API key · storage-key-owner', resolved: true }) const downloader = await registerDownloaderThroughDeviceLogin(app, 'api-key-storage-downloader', admin) const assigned = await claimTaskForDownloader(app, downloader.token, task.id) + const detailRes = await app.request(`/api/downloads/tasks/${task.id}`, { headers: userHeaders }) + expect(detailRes.status).toBe(200) + const detail = (await detailRes.json()) as DownloadTask + expect(detail.status.assignment?.executor).toMatchObject({ + type: 'device', + name: 'Device · api-key-storage-downloader', + resolved: true, + }) const uploadToken = assigned.status.assignment?.uploadToken expect(uploadToken).toBeTruthy() diff --git a/server/http/downloads/download-tasks.ts b/server/http/downloads/download-tasks.ts index e597ff73..558886df 100644 --- a/server/http/downloads/download-tasks.ts +++ b/server/http/downloads/download-tasks.ts @@ -283,7 +283,16 @@ const downloadTasksRoute = new OpenAPIHono() if (!orgId) throw unauthorized() // API keys carry their owning user's UID in userId, so downloader uploads // build the same org/uid object path as browser-created tasks. - return c.json(await createDownloadTask(c.get('deps'), orgId, c.get('userId') as string, c.req.valid('json')), 201) + const actor = c.get('authzContext').actor + if (!actor) throw unauthorized() + return c.json( + await createDownloadTask(c.get('deps'), orgId, c.get('userId') as string, c.req.valid('json'), { + type: actor.type, + ref: actor.ref, + issuer: 'issuer' in actor ? actor.issuer : null, + }), + 201, + ) }) .openapi(getRoute, async (c) => { const orgId = c.get('orgId') diff --git a/server/http/objects.integration.test.ts b/server/http/objects.integration.test.ts index e96ae873..e4d2d5f3 100644 --- a/server/http/objects.integration.test.ts +++ b/server/http/objects.integration.test.ts @@ -2689,6 +2689,7 @@ async function createUserApiKey( const result = (await (auth.api as any).createApiKey({ body: { configId: opts.orgId ? 'ihost' : 'webdav', + name: 'test-api-key', userId, ...(opts.orgId ? { organizationId: opts.orgId } : {}), permissions: opts.permissions, @@ -2803,10 +2804,11 @@ describe('Objects API — error branches', () => { await expect(res.json()).resolves.toMatchObject({ name: '藏.mp3', parent: targetFolder, + createdBy: { type: 'device', resolved: true }, }) }) - it('creates a folder with a workspace API key that has objects:create', async () => { + it('creates a folder with a workspace API key that has objects:create [spec: objects/creator-attribution]', async () => { const { app, db, auth } = await createTestApp() await authedHeaders(app) await insertStorage(db) @@ -2820,7 +2822,24 @@ describe('Objects API — error branches', () => { body: JSON.stringify({ name: 'api-key-folder', type: 'folder', dirtype: 1, parent: '' }), }) expect(res.status).toBe(201) - await expect(res.json()).resolves.toMatchObject({ name: 'api-key-folder', orgId }) + await expect(res.json()).resolves.toMatchObject({ + name: 'api-key-folder', + orgId, + createdBy: { type: 'api_key', name: 'API key · test-api-key', resolved: true }, + }) + }) + + it('returns null instead of attributing a legacy object to the workspace owner [spec: objects/legacy-creator]', async () => { + const { app, db } = await createTestApp() + const headers = await authedHeaders(app) + await insertStorage(db) + const orgId = await getOrgId(db) + await insertFile(db, orgId, { id: 'legacy-creator-file', name: 'legacy.txt' }) + + const res = await app.request('/api/objects/legacy-creator-file', { headers }) + + expect(res.status).toBe(200) + await expect(res.json()).resolves.toMatchObject({ id: 'legacy-creator-file', createdBy: null }) }) it('returns 403 when an object API key is missing the route scope', async () => { diff --git a/server/http/objects.ts b/server/http/objects.ts index f845958c..3d552f87 100644 --- a/server/http/objects.ts +++ b/server/http/objects.ts @@ -1,6 +1,7 @@ import { OpenAPIHono, z } from '@hono/zod-openapi' import { AuthorizationScope } from '@shared/authorization' import { + actorAttributionSchema, completeObjectUploadSchema, copyObjectBodySchema, createMatterSchema, @@ -29,11 +30,12 @@ import { type ObjectActor, ObjectUploadSessionError, presignUploadSessionParts, + resolveMatterCreators, transferObject, trashObject, updateObject, } from '../usecases/object' -import { badRequest, forbidden, type Matter, type MatterListItem, quotaExceeded } from '../usecases/ports' +import { badRequest, forbidden, type Matter, type MatterListItem, quotaExceeded, unauthorized } from '../usecases/ports' import { describeCapacityRequirement } from '../usecases/store/store' import { recordDownloadIssued } from '../usecases/transfer-activity' import { authRoute, errorResponse, jsonBody, jsonContent } from './openapi' @@ -60,6 +62,7 @@ const matterSchema = z storageId: opaqueIdSchema, status: z.string(), trashedAt: z.number().int().nullable(), + createdBy: actorAttributionSchema.nullable(), createdAt: z.string(), updatedAt: z.string(), }) @@ -71,7 +74,7 @@ type MatterDTO = z.infer // timestamps to ISO strings, pass everything else through. Its return type is the // schema's inferred type, so a drift between `Matter` and `matterSchema` is a // compile error — not a silent lie in the document. -function toMatterDTO(m: Matter): MatterDTO { +function toMatterDTO(m: Matter, createdBy: MatterDTO['createdBy']): MatterDTO { return { id: m.id, orgId: m.orgId, @@ -85,6 +88,7 @@ function toMatterDTO(m: Matter): MatterDTO { storageId: m.storageId, status: m.status, trashedAt: m.trashedAt, + createdBy, createdAt: m.createdAt.toISOString(), updatedAt: m.updatedAt.toISOString(), } @@ -101,8 +105,13 @@ const objectListItemSchema = matterSchema .openapi('ObjectListItem') type ObjectListItemDTO = z.infer -function toObjectListItemDTO(item: MatterListItem): ObjectListItemDTO { - return { ...toMatterDTO(item), hasChildren: item.hasChildren } +function toObjectListItemDTO(item: MatterListItem, createdBy: MatterDTO['createdBy']): ObjectListItemDTO { + return { ...toMatterDTO(item, createdBy), hasChildren: item.hasChildren } +} + +async function matterDTO(deps: Env['Variables']['deps'], matter: Matter): Promise { + const creators = await resolveMatterCreators(deps, [matter]) + return toMatterDTO(matter, creators.get(matter.id) ?? null) } const objectPageSchema = cursorPageSchema(objectListItemSchema, 'ObjectPage') @@ -177,9 +186,16 @@ function objectActor(c: Context): ObjectActor { taskId: principal.taskId, targetFolder: principal.targetFolder, createdByUserId: principal.createdByUserId, + identity: { type: 'device', ref: principal.downloaderId, issuer: null }, } } - return { kind: 'user', userId: c.get('userId') as string } + const actor = c.get('authzContext').actor + if (!actor) throw unauthorized() + return { + kind: 'user', + userId: c.get('userId') as string, + identity: { type: actor.type, ref: actor.ref, issuer: 'issuer' in actor ? actor.issuer : null }, + } } // The id recorded in matter/activity logs. @@ -456,9 +472,10 @@ const objects = app }, }) if (!result.ok) throw result.error + const creators = await resolveMatterCreators(c.get('deps'), result.result.items) return c.json( { - items: result.result.items.map(toObjectListItemDTO), + items: result.result.items.map((item) => toObjectListItemDTO(item, creators.get(item.id) ?? null)), nextPageToken: await encodeNextPageToken(c.get('platform'), result.result.nextBoundary, { query: fingerprint, codec: directoryCursorCodec, @@ -503,8 +520,9 @@ const objects = app 402, ) } - if ('upload' in result) return c.json({ ...toMatterDTO(result.matter), upload: result.upload }, 201) - return c.json(toMatterDTO(result.matter), 201) + const matter = await matterDTO(c.get('deps'), result.matter) + if ('upload' in result) return c.json({ ...matter, upload: result.upload }, 201) + return c.json(matter, 201) }) .openapi(presignPartsRoute, async (c) => { const orgId = c.get('orgId') @@ -542,7 +560,7 @@ const objects = app if ('error' in result) throw result.error // quota exceeded throw new ObjectUploadSessionError('not_found') // draft gone } - return c.json(toMatterDTO(result.matter), 200) + return c.json(await matterDTO(c.get('deps'), result.matter), 200) }) .openapi(abortUploadRoute, async (c) => { const orgId = c.get('orgId') @@ -585,9 +603,9 @@ const objects = app }, result.receipt.trafficEventId, ) - return c.json({ ...toMatterDTO(result.matter), downloadUrl: result.downloadUrl }, 200) + return c.json({ ...(await matterDTO(c.get('deps'), result.matter)), downloadUrl: result.downloadUrl }, 200) } - return c.json(toMatterDTO(result.matter), 200) + return c.json(await matterDTO(c.get('deps'), result.matter), 200) } throw result.error }) @@ -600,7 +618,7 @@ const objects = app input: c.req.valid('json'), }) if (!result.ok) throw result.error - return c.json(toMatterDTO(result.matter), 200) + return c.json(await matterDTO(c.get('deps'), result.matter), 200) }) // Soft delete: move a live object to trash. Permanent removal is // DELETE /trash/objects/{id}; discarding a draft is DELETE /{id}/uploads/{sid}. @@ -622,10 +640,11 @@ const objects = app const result = await copyObject(c.get('deps'), { orgId, userId: c.get('userId')!, + actor: objectActor(c), input: { copyFrom: c.req.valid('param').id, parent: body.parent, onConflict: body.onConflict }, }) if (!result.ok) throw result.error - return c.json(toMatterDTO(result.matter), 201) + return c.json(await matterDTO(c.get('deps'), result.matter), 201) }) .openapi(transferObjectRoute, async (c) => { const orgId = c.get('orgId') @@ -635,13 +654,14 @@ const objects = app const result = await transferObject(c.get('deps'), { orgId, userId: c.get('userId')!, + actor: objectActor(c), objectId: c.req.valid('param').id, input: c.req.valid('json'), }) if (!result.ok) throw result.error return c.json( { - saved: result.result.saved.map(toMatterDTO), + saved: await Promise.all(result.result.saved.map((matter) => matterDTO(c.get('deps'), matter))), skipped: result.result.skipped, sourceDeleted: result.result.sourceDeleted, }, diff --git a/server/http/shares.ts b/server/http/shares.ts index c326f0c9..67fc36a5 100644 --- a/server/http/shares.ts +++ b/server/http/shares.ts @@ -13,7 +13,7 @@ import { shareRecipientViewSchema, } from '../../shared/schemas/share' import { transferAuditActor } from '../middleware/audit-transfers' -import { boundWorkspaceOrgId, type Env } from '../middleware/platform' +import { authzActorIdentity, boundWorkspaceOrgId, type Env } from '../middleware/platform' import type { Matter, ShareListItem } from '../usecases/ports' import { notFound } from '../usecases/ports' import { @@ -577,6 +577,8 @@ export const authedShares = authedApp .openapi(saveShareRoute, async (c) => { const token = c.req.valid('param').token const { targetOrgId, targetParent } = c.req.valid('json') + const createdBy = authzActorIdentity(c.get('authzContext')) + if (!createdBy) throw new Error('authenticated_actor_missing') const out = await saveShare(c.get('deps'), { token, currentUserId: c.get('userId')!, @@ -584,6 +586,7 @@ export const authedShares = authedApp boundTargetOrgId: boundWorkspaceOrgId(c.get('authzContext')), targetParent, accessCookie: getCookie(c, cookieName(token)), + createdBy, }) if (out.ok) return c.json({ saved: out.result.saved.map(toSavedMatterDTO), skipped: out.result.skipped }, 201) throw out.error diff --git a/server/http/webdav.ts b/server/http/webdav.ts index 763c90d0..4be491b1 100644 --- a/server/http/webdav.ts +++ b/server/http/webdav.ts @@ -99,6 +99,10 @@ type DavAuth = { permissions: Record | null } +function webDavActor(auth: DavAuth) { + return { type: 'api_key' as const, ref: auth.keyId, issuer: null } +} + interface NativeRateLimiter { limit(options: { key: string }): Promise<{ success: boolean }> } @@ -1278,6 +1282,7 @@ async function putFile(c: DavContext, auth: DavAuth): Promise { contentType, contentLength, body, + createdBy: webDavActor(auth), onTiming: (phase, durationMs) => c.get('webDavTrace').push(`${phase}:${Math.round(durationMs)}`), }) c.get('webDavTrace').push(`upload:${Math.round(performance.now() - startedAt)}`) @@ -1322,6 +1327,7 @@ async function makeCollection(c: DavContext, auth: DavAuth): Promise { userId: auth.userId, name: target.name, parent: target.parent, + createdBy: webDavActor(auth), }) return new Response(null, { status: 201 }) } catch (e) { @@ -1441,6 +1447,7 @@ async function copyMatterRoute(c: DavContext, auth: DavAuth): Promise targetMatter: target.matter, replacingTarget, depth, + createdBy: webDavActor(auth), }) if (!result.ok) return c.text('Storage not found', 404) c.header('Location', matterLocation(c, targetWorkspace.pathSegment, result.location)) @@ -1463,6 +1470,7 @@ async function copyMatterRoute(c: DavContext, auth: DavAuth): Promise targetResourcePath: resourcePath(target), replacedMatterId: target.matter?.id ?? null, replacingTarget, + createdBy: webDavActor(auth), }) if (!result.ok) return c.text('Storage not found', 404) c.header('Location', matterLocation(c, targetWorkspace.pathSegment, result.location)) @@ -1520,6 +1528,7 @@ async function lockMatter(c: DavContext, auth: DavAuth): Promise { owner: lockInfo.owner, depth, timeoutSeconds: parseTimeout(c.req.header('Timeout')), + createdBy: webDavActor(auth), }) c.get('webDavTrace').push(`lock:${Math.round(performance.now() - startedAt)}`) if (!acquired) return xmlResponse(errorXml('no-conflicting-lock'), 423) diff --git a/server/middleware/platform.ts b/server/middleware/platform.ts index 65af90ca..1217a149 100644 --- a/server/middleware/platform.ts +++ b/server/middleware/platform.ts @@ -5,7 +5,7 @@ import type { DavLock } from '../domain/webdav' import type { WebDavMountPath } from '../domain/webdav-public-url' import type { Platform } from '../platform/interface' import type { Deps } from '../usecases/deps' -import type { WebDavTarget } from '../usecases/ports' +import type { ActorIdentity, WebDavTarget } from '../usecases/ports' import type { TransferAuditTarget } from '../usecases/transfer-activity' export type Env = { @@ -157,6 +157,15 @@ export function boundWorkspaceOrgId(context: AuthzContext): string | null { return context.workspace.mode === 'bound' ? context.workspace.orgId : null } +export function authzActorIdentity(context: AuthzContext): ActorIdentity | null { + if (!context.actor) return null + return { + type: context.actor.type, + ref: context.actor.ref, + issuer: 'issuer' in context.actor ? context.actor.issuer : null, + } +} + export const anonymousAuthzContext = (): AuthzContext => ({ credential: 'anonymous', userId: null, diff --git a/server/test/setup.ts b/server/test/setup.ts index 639abb22..4512e69f 100644 --- a/server/test/setup.ts +++ b/server/test/setup.ts @@ -318,6 +318,9 @@ const APP_SCHEMA_SQL = ` status TEXT NOT NULL DEFAULT 'draft', trashed_at INTEGER, purged_at INTEGER, + created_by_actor_type TEXT, + created_by_actor_ref TEXT, + created_by_actor_issuer TEXT, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL ); @@ -698,6 +701,9 @@ const APP_SCHEMA_SQL = ` id TEXT PRIMARY KEY, org_id TEXT NOT NULL, created_by_user_id TEXT NOT NULL, + requested_by_actor_type TEXT, + requested_by_actor_ref TEXT, + requested_by_actor_issuer TEXT, source_type TEXT NOT NULL, source_uri TEXT NOT NULL, display_name TEXT, diff --git a/server/usecases/archive-processing.integration.test.ts b/server/usecases/archive-processing.integration.test.ts index a74568c8..4d2acd45 100644 --- a/server/usecases/archive-processing.integration.test.ts +++ b/server/usecases/archive-processing.integration.test.ts @@ -222,21 +222,51 @@ describe('archive processing', () => { const job = await createArchiveJob(archiveDeps(db), { orgId: ORG_ID, userId: USER_ID, + createdBy: { type: 'oauth', ref: 'agent-1', issuer: 'https://realm.example.com' }, request: { type: 'archive_extract', matterId: 'zip-matter' }, s3: s3 as unknown as S3Gateway, }) expect(job).toMatchObject({ status: 'completed', type: 'archive_extract' }) expect(job.progress).toMatchObject({ outputBytes: 5, fileCount: 1 }) - const rows = await db.all<{ name: string; parent: string; dirtype: number; size: number; object: string }>(sql` - SELECT name, parent, dirtype, size, object FROM matters + const rows = await db.all<{ + name: string + parent: string + dirtype: number + size: number + object: string + createdByActorType: string | null + createdByActorRef: string | null + createdByActorIssuer: string | null + }>(sql` + SELECT name, parent, dirtype, size, object, + created_by_actor_type AS createdByActorType, + created_by_actor_ref AS createdByActorRef, + created_by_actor_issuer AS createdByActorIssuer + FROM matters WHERE org_id = ${ORG_ID} AND status = 'active' ORDER BY dirtype DESC, name ASC `) expect(rows).toEqual( expect.arrayContaining([ - expect.objectContaining({ name: 'docs', parent: '', dirtype: 1, size: 0 }), - expect.objectContaining({ name: 'hello.txt', parent: 'docs', dirtype: 0, size: 5 }), + expect.objectContaining({ + name: 'docs', + parent: '', + dirtype: 1, + size: 0, + createdByActorType: 'oauth', + createdByActorRef: 'agent-1', + createdByActorIssuer: 'https://realm.example.com', + }), + expect.objectContaining({ + name: 'hello.txt', + parent: 'docs', + dirtype: 0, + size: 5, + createdByActorType: 'oauth', + createdByActorRef: 'agent-1', + createdByActorIssuer: 'https://realm.example.com', + }), ]), ) expect(s3.putKeys).toHaveLength(1) diff --git a/server/usecases/archive-processing.ts b/server/usecases/archive-processing.ts index 28f58556..dff3fa90 100644 --- a/server/usecases/archive-processing.ts +++ b/server/usecases/archive-processing.ts @@ -3,6 +3,7 @@ import type { CreateBackgroundJobRequest } from '@shared/schemas' import type { BackgroundJob } from '@shared/types' import { buildObjectKey } from '../lib/path-template' import type { + ActorIdentity, ArchiveTargetFolderRepo, BackgroundJobRepo, MatterRepo, @@ -36,6 +37,7 @@ export type ArchiveProcessingDeps = { export interface CreateArchiveJobInput { orgId: string userId: string + createdBy?: ActorIdentity request: CreateBackgroundJobRequest // Overrides deps.s3 for tests; production paths use the wired gateway. s3?: S3Gateway @@ -74,12 +76,13 @@ export async function processArchiveJob( input: CreateArchiveJobInput & { jobId: string }, ): Promise { const s3 = input.s3 ?? deps.s3 + const createdBy = input.createdBy ?? { type: 'user', ref: input.userId, issuer: null } try { await deps.backgroundJobs.update(input.orgId, input.jobId, { status: 'running', startedAt: new Date() }) const finished = input.request.type === 'archive_compress' - ? await runCompressionJob(deps, s3, input.jobId, input.orgId, input.userId, input.request) - : await runExtractionJob(deps, s3, input.jobId, input.orgId, input.userId, input.request) + ? await runCompressionJob(deps, s3, input.jobId, input.orgId, input.userId, createdBy, input.request) + : await runExtractionJob(deps, s3, input.jobId, input.orgId, input.userId, createdBy, input.request) await notifyArchiveJobFinished(deps, finished) return finished } catch (error) { @@ -100,6 +103,7 @@ async function runCompressionJob( jobId: string, orgId: string, userId: string, + createdBy: ActorIdentity, request: Extract, ): Promise { if (request.targetFolder !== undefined) @@ -153,6 +157,9 @@ async function runCompressionJob( storageId: targetStorage.id, status: 'active', onConflict: 'rename', + createdByActorType: createdBy.type, + createdByActorRef: createdBy.ref, + createdByActorIssuer: createdBy.issuer, }) return deps.backgroundJobs.update(orgId, jobId, { @@ -184,6 +191,7 @@ async function runExtractionJob( jobId: string, orgId: string, userId: string, + createdBy: ActorIdentity, request: Extract, ): Promise { const zipMatter = await deps.matter.get(request.matterId, orgId) @@ -243,6 +251,9 @@ async function runExtractionJob( storageId: targetStorage.id, status: 'active', onConflict: 'rename', + createdByActorType: createdBy.type, + createdByActorRef: createdBy.ref, + createdByActorIssuer: createdBy.issuer, }) createdMatterIds.push(matter.id) }) @@ -284,6 +295,9 @@ async function runExtractionJob( storageId: targetStorage.id, status: 'active', onConflict: 'rename', + createdByActorType: createdBy.type, + createdByActorRef: createdBy.ref, + createdByActorIssuer: createdBy.issuer, }) createdMatterIds.push(folder.id) const matterPath = buildMatterPath(folder.parent, folder.name) diff --git a/server/usecases/audit-actors.test.ts b/server/usecases/audit-actors.test.ts new file mode 100644 index 00000000..621f9da1 --- /dev/null +++ b/server/usecases/audit-actors.test.ts @@ -0,0 +1,69 @@ +import { describe, expect, it, vi } from 'vitest' +import { resolveActorAttributions } from './audit-actors' +import { type ActorIdentity, actorIdentityKey } from './ports' + +describe('resolveActorAttributions', () => { + it('resolves user, API key, device, and agent identities through the shared directory', async () => { + const identities: ActorIdentity[] = [ + { type: 'user', ref: 'user-1', issuer: null }, + { type: 'api_key', ref: 'key-1', issuer: null }, + { type: 'device', ref: 'device-1', issuer: null }, + { type: 'agent', ref: 'agent-1', issuer: 'https://realm.example.com' }, + ] + const resolve = vi.fn( + async () => + new Map([ + [ + actorIdentityKey(identities[3]!), + { name: 'Media Agent', image: 'https://realm.example.com/avatar.png', resolved: true }, + ], + ]), + ) + + const actors = await resolveActorAttributions( + { + auditActorDirectory: { + findUserProfiles: async () => + new Map([['user-1', { name: 'Amber', image: 'https://example.com/amber.png', resolved: true }]]), + findApiKeyNames: async () => new Map([['key-1', 'zme']]), + findDeviceNames: async () => new Map([['device-1', 'zpan-downloader']]), + listTrustedAgentIssuerOrigins: async () => new Set(['https://realm.example.com']), + }, + agentInfo: { resolve }, + }, + identities, + ) + + expect(actors.get(actorIdentityKey(identities[0]!))).toMatchObject({ name: 'Amber', image: expect.any(String) }) + expect(actors.get(actorIdentityKey(identities[1]!))).toMatchObject({ name: 'API key · zme', resolved: true }) + expect(actors.get(actorIdentityKey(identities[2]!))).toMatchObject({ + name: 'Device · zpan-downloader', + resolved: true, + }) + expect(actors.get(actorIdentityKey(identities[3]!))).toMatchObject({ name: 'Media Agent', resolved: true }) + }) + + it('keeps a stable identity fallback when display metadata is unavailable', async () => { + const identity: ActorIdentity = { type: 'agent', ref: 'agent-missing', issuer: 'https://realm.example.com' } + + const actors = await resolveActorAttributions( + { + auditActorDirectory: { + findUserProfiles: async () => new Map(), + findApiKeyNames: async () => new Map(), + findDeviceNames: async () => new Map(), + listTrustedAgentIssuerOrigins: async () => new Set(), + }, + agentInfo: { resolve: async () => new Map() }, + }, + [identity], + ) + + expect(actors.get(actorIdentityKey(identity))).toEqual({ + ...identity, + name: 'Agent · agent-missing', + image: null, + resolved: false, + }) + }) +}) diff --git a/server/usecases/audit-actors.ts b/server/usecases/audit-actors.ts index 5464fb07..ce7c5965 100644 --- a/server/usecases/audit-actors.ts +++ b/server/usecases/audit-actors.ts @@ -1,4 +1,7 @@ +import { fallbackActorProfile } from '../domain/actor-attribution' import { + type ActorAttribution, + type ActorIdentity, type AgentInfoGateway, type AuditActorDirectory, type AuditActorIdentity, @@ -14,7 +17,7 @@ export async function resolveAuditActorProfiles( const identities = uniqueResolvableIdentities(events) if (identities.length === 0) return events - const profiles = await resolveProfiles(deps, identities) + const profiles = await resolveActorProfiles(deps, identities) return events.map((event) => { const profile = profiles.get( auditActorIdentityKey({ type: event.actorType, ref: event.actorRef, issuer: event.actorIssuer }), @@ -23,11 +26,21 @@ export async function resolveAuditActorProfiles( }) } -async function resolveProfiles( +export async function resolveActorProfiles( deps: { auditActorDirectory: AuditActorDirectory; agentInfo: AgentInfoGateway }, - identities: readonly AuditActorIdentity[], + identities: readonly ActorIdentity[], ): Promise> { const profiles = new Map() + const userActors = identities.flatMap((identity) => + identity.type === 'user' && identity.ref ? [{ identity, ref: identity.ref }] : [], + ) + if (userActors.length > 0) { + const users = await deps.auditActorDirectory.findUserProfiles(userActors.map((actor) => actor.ref)) + for (const actor of userActors) { + const profile = users.get(actor.ref) + if (profile) profiles.set(auditActorIdentityKey(actor.identity), profile) + } + } const apiKeyActors = identities.flatMap((identity) => identity.type === 'api_key' && identity.ref ? [{ identity, ref: identity.ref }] : [], ) @@ -71,6 +84,20 @@ async function resolveProfiles( return profiles } +export async function resolveActorAttributions( + deps: { auditActorDirectory: AuditActorDirectory; agentInfo: AgentInfoGateway }, + identities: readonly ActorIdentity[], +): Promise> { + const unique = new Map(identities.map((identity) => [auditActorIdentityKey(identity), identity])) + const profiles = await resolveActorProfiles(deps, [...unique.values()]) + return new Map( + [...unique].map(([key, identity]) => { + const profile = profiles.get(key) ?? fallbackActorProfile(identity) + return [key, { ...identity, ...profile }] as const + }), + ) +} + function uniqueResolvableIdentities(events: readonly AuditEventWithUser[]): AuditActorIdentity[] { const identities = new Map() for (const event of events) { diff --git a/server/usecases/background-job.test.ts b/server/usecases/background-job.test.ts index 63a14275..95c4966e 100644 --- a/server/usecases/background-job.test.ts +++ b/server/usecases/background-job.test.ts @@ -22,6 +22,7 @@ const sampleJob = { } as BackgroundJob const extractRequest: CreateBackgroundJobRequest = { type: 'archive_extract', matterId: 'm-1' } +const agent = { type: 'oauth' as const, ref: 'agent-1', issuer: 'https://realm.example.com' } // Fake ports as plain objects. backgroundJobs.create is what enqueueArchiveJob // (the real composed function) drives on create; everything else is forwarded. @@ -105,7 +106,12 @@ describe('background-job usecase', () => { it('enqueues the request then dispatches the job, and returns it', async () => { const create = vi.fn(async () => sampleJob) const { deps, dispatch } = makeDeps({ backgroundJobs: { create } }) - const job = await createBackgroundJob(deps, { orgId: 'org-1', userId: 'user-1', request: extractRequest }) + const job = await createBackgroundJob(deps, { + orgId: 'org-1', + userId: 'user-1', + createdBy: agent, + request: extractRequest, + }) expect(job).toBe(sampleJob) // enqueueArchiveJob maps the request onto the create input. @@ -120,6 +126,7 @@ describe('background-job usecase', () => { expect(dispatch).toHaveBeenCalledWith({ orgId: 'org-1', userId: 'user-1', + createdBy: agent, request: extractRequest, jobId: 'job-1', } satisfies ArchiveJobMessage) @@ -135,7 +142,12 @@ describe('background-job usecase', () => { calls.push('dispatch') }) const { deps } = makeDeps({ backgroundJobs: { create }, dispatch }) - await createBackgroundJob(deps, { orgId: 'org-1', userId: 'user-1', request: extractRequest }) + await createBackgroundJob(deps, { + orgId: 'org-1', + userId: 'user-1', + createdBy: agent, + request: extractRequest, + }) expect(calls).toEqual(['create', 'dispatch']) }) @@ -143,7 +155,7 @@ describe('background-job usecase', () => { const create = vi.fn(async () => sampleJob) const { deps } = makeDeps({ backgroundJobs: { create } }) const request: CreateBackgroundJobRequest = { type: 'archive_extract', matterId: 'm-1', targetFolder: 'dest' } - await createBackgroundJob(deps, { orgId: 'org-1', userId: 'user-1', request }) + await createBackgroundJob(deps, { orgId: 'org-1', userId: 'user-1', createdBy: agent, request }) expect(create).toHaveBeenCalledWith(expect.objectContaining({ targetFolder: 'dest', metadata: request })) }) }) @@ -154,7 +166,7 @@ describe('background-job usecase', () => { const retry = vi.fn(async () => retriedJob) const { deps, dispatch } = makeDeps({ backgroundJobs: { retry } }) - const out = await retryBackgroundJob(deps, 'org-1', 'job-1') + const out = await retryBackgroundJob(deps, 'org-1', 'job-1', agent) expect(out).toBe(retriedJob) expect(retry).toHaveBeenCalledWith('org-1', 'job-1') @@ -162,6 +174,7 @@ describe('background-job usecase', () => { expect(dispatch).toHaveBeenCalledWith({ orgId: 'org-1', userId: 'owner-9', + createdBy: agent, request: extractRequest, jobId: 'job-2', } satisfies ArchiveJobMessage) @@ -172,7 +185,7 @@ describe('background-job usecase', () => { const retry = vi.fn(async () => retriedJob) const { deps, dispatch } = makeDeps({ backgroundJobs: { retry } }) - const out = await retryBackgroundJob(deps, 'org-1', 'job-1') + const out = await retryBackgroundJob(deps, 'org-1', 'job-1', agent) expect(out).toBe(retriedJob) expect(dispatch).not.toHaveBeenCalled() @@ -181,7 +194,7 @@ describe('background-job usecase', () => { it('does not dispatch when the stored metadata is null', async () => { const retry = vi.fn(async () => ({ ...sampleJob, metadata: null }) as BackgroundJob) const { deps, dispatch } = makeDeps({ backgroundJobs: { retry } }) - await retryBackgroundJob(deps, 'org-1', 'job-1') + await retryBackgroundJob(deps, 'org-1', 'job-1', agent) expect(dispatch).not.toHaveBeenCalled() }) @@ -190,7 +203,7 @@ describe('background-job usecase', () => { throw new BackgroundJobError('not_retryable') }) const { deps, dispatch } = makeDeps({ backgroundJobs: { retry } }) - await expect(retryBackgroundJob(deps, 'org-1', 'job-1')).rejects.toMatchObject({ code: 'not_retryable' }) + await expect(retryBackgroundJob(deps, 'org-1', 'job-1', agent)).rejects.toMatchObject({ code: 'not_retryable' }) expect(dispatch).not.toHaveBeenCalled() }) }) diff --git a/server/usecases/background-job.ts b/server/usecases/background-job.ts index 9e674da5..a790a986 100644 --- a/server/usecases/background-job.ts +++ b/server/usecases/background-job.ts @@ -15,7 +15,7 @@ import { enqueueArchiveJob } from './archive-processing' // create dispatches on top of enqueueArchiveJob, which reaches across the archive // ports, so it forwards the whole Deps; the reads need only the narrow ports. import type { Deps } from './deps' -import type { ListBackgroundJobsOptions } from './ports' +import type { ActorIdentity, ListBackgroundJobsOptions } from './ports' export function listBackgroundJobs(deps: Pick, orgId: string, opts: ListBackgroundJobsOptions) { return deps.backgroundJobs.list(orgId, opts) @@ -43,11 +43,11 @@ export function cancelBackgroundJob( export async function createBackgroundJob( deps: Deps, - params: { orgId: string; userId: string; request: CreateBackgroundJobRequest }, + params: { orgId: string; userId: string; createdBy: ActorIdentity; request: CreateBackgroundJobRequest }, ): Promise { - const { orgId, userId, request } = params + const { orgId, userId, createdBy, request } = params const job = await enqueueArchiveJob(deps, { orgId, userId, request }) - await deps.archiveJobs.dispatch({ orgId, userId, request, jobId: job.id }) + await deps.archiveJobs.dispatch({ orgId, userId, createdBy, request, jobId: job.id }) return job } @@ -55,11 +55,12 @@ export async function retryBackgroundJob( deps: Pick, orgId: string, id: string, + createdBy: ActorIdentity, ): Promise { const job = await deps.backgroundJobs.retry(orgId, id) const request = createBackgroundJobRequestSchema.safeParse(job.metadata) if (request.success) { - await deps.archiveJobs.dispatch({ orgId, userId: job.userId, request: request.data, jobId: job.id }) + await deps.archiveJobs.dispatch({ orgId, userId: job.userId, createdBy, request: request.data, jobId: job.id }) } return job } diff --git a/server/usecases/downloads/download-folders.ts b/server/usecases/downloads/download-folders.ts index 8e8f3a6e..a432ba18 100644 --- a/server/usecases/downloads/download-folders.ts +++ b/server/usecases/downloads/download-folders.ts @@ -1,6 +1,7 @@ import { DirType, ObjectStatus } from '@shared/constants' import { formatError } from '../../lib/errors' import type { Deps } from '../deps' +import type { ActorIdentity } from '../ports' import { AppError, type Matter, NameConflictError } from '../ports' function joinPath(parent: string, name: string): string { @@ -16,7 +17,7 @@ function targetIsFile(path: string): AppError { export async function ensureDownloadFolderPath( deps: Pick, - params: { orgId: string; folderPath: string }, + params: { orgId: string; folderPath: string; createdBy?: ActorIdentity }, ): Promise { const parts = params.folderPath.split('/').filter(Boolean) if (parts.length === 0) return '' @@ -39,6 +40,9 @@ export async function ensureDownloadFolderPath( object: '', storageId, status: ObjectStatus.ACTIVE, + createdByActorType: params.createdBy?.type, + createdByActorRef: params.createdBy?.ref, + createdByActorIssuer: params.createdBy?.issuer, }) } catch (error) { if (!(error instanceof NameConflictError) && !formatError(error).includes('UNIQUE constraint failed')) diff --git a/server/usecases/downloads/downloads.test.ts b/server/usecases/downloads/downloads.test.ts index 995fac9d..0a7e71ff 100644 --- a/server/usecases/downloads/downloads.test.ts +++ b/server/usecases/downloads/downloads.test.ts @@ -1,6 +1,7 @@ import type { CreateDownloaderInput } from '@shared/schemas' -import type { BindingState, Downloader } from '@shared/types' +import type { BindingState, Downloader, DownloadTask } from '@shared/types' import { beforeEach, describe, expect, it, vi } from 'vitest' +import type { Platform } from '../../platform/interface' import type { DownloaderRecord, DownloaderRepo } from '../ports' import { type AppError, DownloadError } from '../ports' import { loadBindingState } from '../site/licensing' @@ -8,6 +9,7 @@ import { createDownloaderWithBootstrapCredential, type DownloadsDeps, downloaderHeartbeatPersistence, + listDownloadTasks, updateDownloaderCreditBilling, } from './downloads' @@ -205,6 +207,80 @@ describe('updateDownloaderCreditBilling', () => { }) }) +describe('listDownloadTasks', () => { + it('resolves every requester and executor in one batch for the page', async () => { + const requestedBy = (ref: string) => ({ + type: 'api_key' as const, + ref, + issuer: null, + name: `API key · ${ref}`, + image: null, + resolved: false, + }) + const tasks = ['key-1', 'key-2'].map( + (ref, index) => + ({ + id: `task-${index + 1}`, + requestedBy: requestedBy(ref), + status: { + assignment: { + downloaderId: `device-${index + 1}`, + assignedAt: null, + executor: { + type: 'device', + ref: `device-${index + 1}`, + issuer: null, + name: `Device · device-${index + 1}`, + image: null, + resolved: false, + }, + }, + }, + }) as DownloadTask, + ) + const findApiKeyNames = vi.fn( + async () => + new Map([ + ['key-1', 'zme'], + ['key-2', 'automation'], + ]), + ) + const findDeviceNames = vi.fn( + async () => + new Map([ + ['device-1', 'Living room'], + ['device-2', 'Office'], + ]), + ) + const listTrustedAgentIssuerOrigins = vi.fn(async () => new Set()) + const agentResolve = vi.fn(async () => new Map()) + const deps = { + downloadTasks: { list: vi.fn(async () => ({ items: tasks, rows: [], nextBoundary: null })) }, + auditActorDirectory: { + findUserProfiles: vi.fn(async () => new Map()), + findApiKeyNames, + findDeviceNames, + listTrustedAgentIssuerOrigins, + }, + agentInfo: { resolve: agentResolve }, + } as unknown as DownloadsDeps + + const result = await listDownloadTasks(deps, {} as Platform, { pageSize: 20 }) + + expect(findApiKeyNames).toHaveBeenCalledTimes(1) + expect(findApiKeyNames).toHaveBeenCalledWith(['key-1', 'key-2']) + expect(findDeviceNames).toHaveBeenCalledTimes(1) + expect(findDeviceNames).toHaveBeenCalledWith(['device-1', 'device-2']) + expect(listTrustedAgentIssuerOrigins).not.toHaveBeenCalled() + expect(agentResolve).not.toHaveBeenCalled() + expect(result.items.map((task) => task.requestedBy?.name)).toEqual(['API key · zme', 'API key · automation']) + expect(result.items.map((task) => task.status.assignment?.executor?.name)).toEqual([ + 'Device · Living room', + 'Device · Office', + ]) + }) +}) + describe('createDownloaderWithBootstrapCredential', () => { const input = { name: 'Bootstrap downloader', diff --git a/server/usecases/downloads/downloads.ts b/server/usecases/downloads/downloads.ts index f99c77ca..f3db4a9b 100644 --- a/server/usecases/downloads/downloads.ts +++ b/server/usecases/downloads/downloads.ts @@ -1,4 +1,5 @@ import type { + ActorAttribution, CreateDownloaderInput, CreateDownloadTaskInput, DownloaderHeartbeatInput, @@ -19,10 +20,15 @@ import type { } from '@shared/types' import { ZPAN_CLOUD_URL_DEFAULT } from '../../../shared/constants' import { generateId } from '../../../shared/ids' +import { fallbackActorAttribution } from '../../domain/actor-attribution' import { parseDownloadTaskEvents } from '../../domain/download-task-events' import { hasFeature } from '../../domain/licensing' import type { Platform } from '../../platform/interface' +import { resolveActorAttributions } from '../audit-actors' import type { + ActorIdentity, + AgentInfoGateway, + AuditActorDirectory, AuditEvent, AuditRepo, DownloaderBootstrapCredentialRepo, @@ -39,7 +45,7 @@ import type { StorageRepo, UpdateDownloadTaskFields, } from '../ports' -import { DownloadError, featureBlocked, unauthorized } from '../ports' +import { actorIdentityKey, DownloadError, featureBlocked, unauthorized } from '../ports' import { loadBindingState } from '../site/licensing' import { ensureDownloadFolderPath } from './download-folders' import { RemoteDownloadBillingBlockedError, reportRemoteDownloadUnit } from './remote-download-usage' @@ -51,6 +57,8 @@ import { RemoteDownloadBillingBlockedError, reportRemoteDownloadUnit } from './r // is platform-per-call, mirroring auth.ts). export type DownloadsDeps = { + auditActorDirectory: AuditActorDirectory + agentInfo: AgentInfoGateway downloaders: DownloaderRepo downloaderBootstrapCredentials: DownloaderBootstrapCredentialRepo downloadTasks: DownloadTaskRepo @@ -311,10 +319,12 @@ export async function createDownloadTask( orgId: string, userId: string, input: CreateDownloadTaskInput, + requestedBy: ActorIdentity = { type: 'user', ref: userId, issuer: null }, ): Promise { const targetFolder = await ensureDownloadFolderPath(deps, { orgId, folderPath: input.targetFolder, + createdBy: requestedBy, }) const now = new Date() const id = generateId() @@ -322,6 +332,9 @@ export async function createDownloadTask( id, orgId, createdByUserId: userId, + requestedByActorType: requestedBy.type, + requestedByActorRef: requestedBy.ref ?? userId, + requestedByActorIssuer: requestedBy.issuer, sourceType: input.source.type, sourceUri: input.source.uri, displayName: input.name ?? null, @@ -333,7 +346,7 @@ export async function createDownloadTask( assignedAt: null, now, }) - return deps.downloadTasks.get(orgId, id) + return decorateDownloadTaskActors(deps, await deps.downloadTasks.get(orgId, id)) } export async function listDownloadTasks( @@ -345,10 +358,13 @@ export async function listDownloadTasks( nextBoundary: { createdAt: Date; id: string } | null }> { const { items, rows, nextBoundary } = await deps.downloadTasks.list(opts) - if (!opts.includeUploadToken) return { items, nextBoundary } - const decorated = await Promise.all( + if (!opts.includeUploadToken) { + return { items: await decorateDownloadTasksActors(deps, items), nextBoundary } + } + const tasksWithUploadTokens = await Promise.all( items.map((task, index) => decorateWithUploadToken(deps, platform, task, rows[index])), ) + const decorated = await decorateDownloadTasksActors(deps, tasksWithUploadTokens) return { items: decorated, nextBoundary } } @@ -362,8 +378,39 @@ export async function listDownloadTaskItems( return deps.downloadTasks.listItems(opts) } -export function getDownloadTask(deps: DownloadsDeps, orgId: string, id: string): Promise { - return deps.downloadTasks.get(orgId, id) +export async function getDownloadTask(deps: DownloadsDeps, orgId: string, id: string): Promise { + return decorateDownloadTaskActors(deps, await deps.downloadTasks.get(orgId, id)) +} + +async function decorateDownloadTaskActors(deps: DownloadsDeps, task: DownloadTask): Promise { + return (await decorateDownloadTasksActors(deps, [task]))[0] +} + +async function decorateDownloadTasksActors( + deps: DownloadsDeps, + tasks: readonly DownloadTask[], +): Promise { + const identities = tasks + .flatMap((task) => [ + task.requestedBy && { type: task.requestedBy.type, ref: task.requestedBy.ref, issuer: task.requestedBy.issuer }, + task.status.assignment && { type: 'device' as const, ref: task.status.assignment.downloaderId, issuer: null }, + ]) + .filter((identity): identity is ActorIdentity => identity !== null) + const actors = await resolveActorAttributions(deps, identities) + return tasks.map((task) => decorateDownloadTaskActor(task, actors)) +} + +function decorateDownloadTaskActor(task: DownloadTask, actors: ReadonlyMap): DownloadTask { + const requestedBy = task.requestedBy ? (actors.get(actorIdentityKey(task.requestedBy)) ?? task.requestedBy) : null + const assignment = task.status.assignment + ? { + ...task.status.assignment, + executor: + actors.get(actorIdentityKey({ type: 'device', ref: task.status.assignment.downloaderId, issuer: null })) ?? + task.status.assignment.executor, + } + : null + return { ...task, requestedBy, status: { ...task.status, assignment } } } export async function getDownloadTaskTimeline( @@ -1038,10 +1085,19 @@ function actionSeverity(action: string): DownloadTaskTimelineItem['severity'] { function downloadTaskFromRecord(row: DownloadTaskRecord): DownloadTask { const runtime = parseTaskRuntime(row.runtime) + const requestedBy = + row.requestedByActorType && row.requestedByActorRef + ? fallbackActorAttribution({ + type: row.requestedByActorType, + ref: row.requestedByActorRef, + issuer: row.requestedByActorIssuer ?? null, + }) + : null return { id: row.id, orgId: row.orgId, createdBy: row.createdByUserId, + requestedBy, spec: { source: { type: row.sourceType as DownloadTask['spec']['source']['type'], @@ -1060,7 +1116,11 @@ function downloadTaskFromRecord(row: DownloadTaskRecord): DownloadTask { state: row.status as DownloadTask['status']['state'], attempt: row.attempt, assignment: row.assignedDownloaderId - ? { downloaderId: row.assignedDownloaderId, assignedAt: row.assignedAt?.toISOString() ?? null } + ? { + downloaderId: row.assignedDownloaderId, + assignedAt: row.assignedAt?.toISOString() ?? null, + executor: fallbackActorAttribution({ type: 'device', ref: row.assignedDownloaderId, issuer: null }), + } : null, progress: runtime?.progress ?? { download: { bytes: 0, totalBytes: null, bytesPerSecond: 0 }, diff --git a/server/usecases/object.test.ts b/server/usecases/object.test.ts index c3b9d85e..eb6d4508 100644 --- a/server/usecases/object.test.ts +++ b/server/usecases/object.test.ts @@ -397,6 +397,29 @@ describe('object usecase', () => { expect(presignUpload).not.toHaveBeenCalled() }) + it('persists the stable OAuth agent identity on a created object', async () => { + const create = vi.fn(async () => folder('agent-folder', { name: 'Agent Folder' })) + const { deps } = makeDeps({ matter: { create } }) + + await createObject(deps, { + orgId: 'o1', + actor: { + kind: 'user', + userId: 'owner-1', + identity: { type: 'oauth', ref: 'agent-1', issuer: 'https://realm.example.com' }, + }, + input: { name: 'Agent Folder', type: 'folder', dirtype: DirType.USER_FOLDER, parent: '' }, + }) + + expect(create).toHaveBeenCalledWith( + expect.objectContaining({ + createdByActorType: 'oauth', + createdByActorRef: 'agent-1', + createdByActorIssuer: 'https://realm.example.com', + }), + ) + }) + it('creates a small file draft and returns single-PUT upload instructions', async () => { const create = vi.fn(async (input: Parameters[0]) => file('d1', input as Partial)) const presignUpload = vi.fn(async () => 'https://up') @@ -1664,7 +1687,12 @@ describe('object usecase', () => { expect(source.object).toBe('legacy_/source-file-.txt') expect(destinationKey).toMatch(/^o1\/u1\/\d{8}\/[A-Za-z0-9]{17}\.txt$/) expect(copyObjectS3).toHaveBeenCalledWith(storage, source.object, storage, destinationKey) - expect(copy).toHaveBeenCalledWith(source, 'Dest', destinationKey, { onConflict: undefined }) + expect(copy).toHaveBeenCalledWith(source, 'Dest', destinationKey, { + onConflict: undefined, + createdByActorType: 'user', + createdByActorRef: 'u1', + createdByActorIssuer: null, + }) }) it('returns not_found for a missing source', async () => { diff --git a/server/usecases/object.ts b/server/usecases/object.ts index 87a48da4..e979926c 100644 --- a/server/usecases/object.ts +++ b/server/usecases/object.ts @@ -12,6 +12,7 @@ import { DirType } from '@shared/constants' import type { + ActorAttribution, CompleteObjectUploadInput, ConflictStrategy, CreateMatterInput, @@ -21,11 +22,14 @@ import type { } from '@shared/schemas' import type { ObjectUploadInstructions } from '@shared/types' import { buildObjectKey, fileExt } from '../lib/path-template' +import { resolveActorAttributions } from './audit-actors' import type { Deps } from './deps' import { assertFolderNotUsedByDownload, ensureDownloadFolderPath } from './downloads/download-folders' import { assertTaskUploadAllowed } from './downloads/downloads' import { + type ActorIdentity, type AppError, + actorIdentityKey, badRequest, forbidden, insufficientCredits, @@ -61,13 +65,14 @@ export interface CopyObjectInput { // upload token acts on behalf of the task creator but is logged as the // downloader and is constrained to its authorized target folder. export type ObjectActor = - | { kind: 'user'; userId: string } + | { kind: 'user'; userId: string; identity?: ActorIdentity } | { kind: 'download-task-upload' downloaderId: string taskId: string targetFolder: string createdByUserId: string + identity?: ActorIdentity } // The user id used to build object keys (whose owner is the task creator for @@ -81,6 +86,13 @@ function actorLogId(actor: ObjectActor): string { return actor.kind === 'download-task-upload' ? `downloader:${actor.downloaderId}` : actor.userId } +function creatorIdentity(actor: ObjectActor): ActorIdentity { + if (actor.identity) return actor.identity + return actor.kind === 'download-task-upload' + ? { type: 'device', ref: actor.downloaderId, issuer: null } + : { type: 'user', ref: actor.userId, issuer: null } +} + const ROLE_LEVELS: Record = { owner: 3, admin: 3, editor: 2, viewer: 1, member: 1 } // Whether the user may write (editor+) in the org. Personal orgs grant full @@ -164,6 +176,7 @@ export async function createObject( params: { orgId: string; actor: ObjectActor; input: CreateMatterInput }, ): Promise { const { orgId, actor, input } = params + const createdBy = creatorIdentity(actor) const { name, type, dirtype, onConflict } = input let { parent } = input const isFolder = dirtype !== DirType.FILE @@ -180,6 +193,7 @@ export async function createObject( parent = await ensureDownloadFolderPath(deps, { orgId, folderPath: parent, + createdBy, }) } @@ -224,6 +238,9 @@ export async function createObject( object: objectKey, storageId: storage.id, status: isFolder ? 'active' : 'draft', + createdByActorType: createdBy.type, + createdByActorRef: createdBy.ref, + createdByActorIssuer: createdBy.issuer, onConflict, }) @@ -242,6 +259,36 @@ export async function createObject( return { ok: true, matter, upload } } +export async function resolveMatterCreators( + deps: Pick, + matters: readonly Matter[], +): Promise> { + const identities = matters.flatMap((matter) => + matter.createdByActorType && matter.createdByActorRef + ? [ + { + type: matter.createdByActorType as ActorIdentity['type'], + ref: matter.createdByActorRef, + issuer: matter.createdByActorIssuer ?? null, + } satisfies ActorIdentity, + ] + : [], + ) + const actors = await resolveActorAttributions(deps, identities) + return new Map( + matters.flatMap((matter) => { + if (!matter.createdByActorType || !matter.createdByActorRef) return [] + const identity = { + type: matter.createdByActorType as ActorIdentity['type'], + ref: matter.createdByActorRef, + issuer: matter.createdByActorIssuer ?? null, + } + const actor = actors.get(actorIdentityKey(identity)) + return actor ? [[matter.id, actor] as const] : [] + }), + ) +} + // Decides the S3 mechanism, presigns every URL up front, and records the upload // session. The chosen onConflict is stored on the session so completion can apply // a deferred 'replace' (createMatter keeps the incumbent until bytes land). @@ -860,9 +907,10 @@ export type CopyObjectOutcome = { ok: true; matter: Matter } | { ok: false; erro export async function copyObject( deps: Pick, - params: { orgId: string; userId: string; input: CopyObjectInput }, + params: { orgId: string; userId: string; actor?: ObjectActor; input: CopyObjectInput }, ): Promise { const { orgId, userId, input } = params + const identity = params.actor ? creatorIdentity(params.actor) : { type: 'user' as const, ref: userId, issuer: null } const { copyFrom, parent, onConflict } = input const source = await deps.matter.get(copyFrom, orgId) if (!source || source.trashedAt != null) return { ok: false, error: notFound() } @@ -884,7 +932,12 @@ export async function copyObject( await deps.s3.copyObject(objectStorage, source.object, objectStorage, newObject) ctx.onRollback(() => deps.s3.deleteObject(objectStorage, newObject)) } - return deps.matter.copy(source, parent, newObject, { onConflict }) + return deps.matter.copy(source, parent, newObject, { + onConflict, + createdByActorType: identity.type, + createdByActorRef: identity.ref, + createdByActorIssuer: identity.issuer, + }) }, ) return { ok: true, matter: copy } @@ -898,7 +951,7 @@ export type TransferObjectOutcome = { ok: true; result: TransferObjectResult } | export async function transferObject( deps: Pick, - params: { orgId: string; userId: string; objectId: string; input: TransferMatterInput }, + params: { orgId: string; userId: string; actor?: ObjectActor; objectId: string; input: TransferMatterInput }, ): Promise { const { orgId, userId, objectId, input } = params const { targetOrgId, targetParent, mode } = input @@ -927,6 +980,7 @@ export async function transferObject( currentUserId: userId, targetOrgId, targetParent, + createdBy: params.actor ? creatorIdentity(params.actor) : undefined, }) // Move = copy + delete source. Only delete when every file copied — a partial @@ -1130,6 +1184,7 @@ export interface SaveShareInput { targetOrgId: string targetParent: string teamQuotaEnabled?: boolean + createdBy?: ActorIdentity } export interface SaveShareResult { @@ -1143,6 +1198,7 @@ export interface CopyMatterToOrgInput { targetOrgId: string targetParent: string teamQuotaEnabled?: boolean + createdBy?: ActorIdentity } function buildPath(parent: string, name: string): string { @@ -1158,6 +1214,7 @@ async function saveFile( targetOrgId: string, targetParent: string, teamQuotaEnabled = true, + createdBy: ActorIdentity = { type: 'user', ref: currentUserId, issuer: null }, ): Promise { const bytes = sourceMatter.size ?? 0 const dstKey = buildObjectKey({ uid: currentUserId, orgId: targetOrgId, rawExt: fileExt(sourceMatter.name) }) @@ -1184,6 +1241,9 @@ async function saveFile( storageId: targetStorage.id, status: 'active', onConflict: 'rename', + createdByActorType: createdBy.type, + createdByActorRef: createdBy.ref, + createdByActorIssuer: createdBy.issuer, }) return newMatter @@ -1200,6 +1260,7 @@ async function saveFolderRecursive( targetOrgId: string, targetParent: string, teamQuotaEnabled = true, + createdBy: ActorIdentity = { type: 'user', ref: currentUserId, issuer: null }, ): Promise { const saved: Matter[] = [] const skipped: Array<{ name: string; reason: string }> = [] @@ -1215,6 +1276,9 @@ async function saveFolderRecursive( storageId: targetStorage.id, status: 'active', onConflict: 'rename', + createdByActorType: createdBy.type, + createdByActorRef: createdBy.ref, + createdByActorIssuer: createdBy.issuer, }) saved.push(rootFolder) @@ -1241,6 +1305,7 @@ async function saveFolderRecursive( targetOrgId, targetPath, teamQuotaEnabled, + createdBy, ) saved.push(newFile) } catch (e) { @@ -1258,6 +1323,9 @@ async function saveFolderRecursive( storageId: targetStorage.id, status: 'active', onConflict: 'rename', + createdByActorType: createdBy.type, + createdByActorRef: createdBy.ref, + createdByActorIssuer: createdBy.issuer, }) saved.push(newFolder) queue.push({ @@ -1275,7 +1343,7 @@ async function saveFolderRecursive( // target org per file; files that fail (e.g. quota) are reported in `skipped` // rather than failing the whole operation. export async function copyMatterToOrg(deps: SaveToDriveDeps, input: CopyMatterToOrgInput): Promise { - const { sourceMatter, currentUserId, targetOrgId, targetParent, teamQuotaEnabled = true } = input + const { sourceMatter, currentUserId, targetOrgId, targetParent, teamQuotaEnabled = true, createdBy } = input const sourceStorage = await deps.storages.get(sourceMatter.storageId) if (!sourceStorage) throw new Error('Source storage not found') @@ -1292,6 +1360,7 @@ export async function copyMatterToOrg(deps: SaveToDriveDeps, input: CopyMatterTo targetOrgId, targetParent, teamQuotaEnabled, + createdBy, ) return { saved: [newMatter], skipped: [] } } @@ -1305,6 +1374,7 @@ export async function copyMatterToOrg(deps: SaveToDriveDeps, input: CopyMatterTo targetOrgId, targetParent, teamQuotaEnabled, + createdBy, ) } diff --git a/server/usecases/ports/archive-jobs.ts b/server/usecases/ports/archive-jobs.ts index 9eb38396..bd7d5da3 100644 --- a/server/usecases/ports/archive-jobs.ts +++ b/server/usecases/ports/archive-jobs.ts @@ -1,9 +1,11 @@ import type { CreateBackgroundJobRequest } from '@shared/schemas' +import type { ActorIdentity } from './audit' export interface ArchiveJobMessage { jobId: string orgId: string userId: string + createdBy: ActorIdentity request: CreateBackgroundJobRequest } diff --git a/server/usecases/ports/audit.ts b/server/usecases/ports/audit.ts index f579130d..fb2635fd 100644 --- a/server/usecases/ports/audit.ts +++ b/server/usecases/ports/audit.ts @@ -1,6 +1,8 @@ // Plain, framework-free DTOs and the repository port for audit events. -export type AuditActorType = 'user' | 'api_key' | 'oauth' | 'agent' | 'anonymous' | 'system' | 'device' | 'task-upload' +import type { ActorAttribution, ActorType } from '@shared/schemas' + +export type AuditActorType = ActorType export interface RecordAuditEventInput { orgId: string @@ -73,19 +75,20 @@ export interface AuditRepo { ): Promise<{ items: AuditEvent[]; total: number; page: number; pageSize: number }> } -export interface AuditActorIdentity { - type: AuditActorType +export interface ActorIdentity { + type: ActorType ref: string | null issuer: string | null } -export interface AuditActorProfile { +export interface ActorProfile { name: string image: string | null resolved: boolean } -export interface AuditActorDirectory { +export interface ActorDirectory { + findUserProfiles(userIds: readonly string[]): Promise> findApiKeyNames(keyIds: readonly string[]): Promise> findDeviceNames(deviceIds: readonly string[]): Promise> listTrustedAgentIssuerOrigins(): Promise> @@ -95,11 +98,18 @@ export interface AgentInfoGateway { // Profiles are display-only and never authoritative. An omitted identity // tells the caller to retain the stable issuer/subject fallback. resolve( - actors: readonly AuditActorIdentity[], + actors: readonly ActorIdentity[], trustedIssuerOrigins: ReadonlySet, - ): Promise> + ): Promise> } -export function auditActorIdentityKey(actor: AuditActorIdentity): string { +export type AuditActorIdentity = ActorIdentity +export type AuditActorProfile = ActorProfile +export type AuditActorDirectory = ActorDirectory +export type { ActorAttribution } + +export function actorIdentityKey(actor: ActorIdentity): string { return JSON.stringify([actor.type, actor.issuer, actor.ref]) } + +export const auditActorIdentityKey = actorIdentityKey diff --git a/server/usecases/ports/downloads.ts b/server/usecases/ports/downloads.ts index f356315d..746dbde4 100644 --- a/server/usecases/ports/downloads.ts +++ b/server/usecases/ports/downloads.ts @@ -1,3 +1,4 @@ +import type { ActorType } from '@shared/schemas' import type { Downloader, DownloadTask, DownloadTaskListItem } from '@shared/types' // ─── Errors ────────────────────────────────────────────────────────────────── @@ -51,6 +52,9 @@ export interface DownloadTaskRecord { id: string orgId: string createdByUserId: string + requestedByActorType?: ActorType | null + requestedByActorRef?: string | null + requestedByActorIssuer?: string | null sourceType: string sourceUri: string displayName: string | null @@ -145,6 +149,9 @@ export interface CreateDownloadTaskRecordInput { id: string orgId: string createdByUserId: string + requestedByActorType?: ActorType + requestedByActorRef?: string + requestedByActorIssuer?: string | null sourceType: string sourceUri: string displayName: string | null diff --git a/server/usecases/ports/matter.ts b/server/usecases/ports/matter.ts index 1beeb773..1cad53a1 100644 --- a/server/usecases/ports/matter.ts +++ b/server/usecases/ports/matter.ts @@ -1,4 +1,4 @@ -import type { ConflictStrategy } from '@shared/schemas' +import type { ActorType, ConflictStrategy } from '@shared/schemas' export type { ConflictStrategy } from '@shared/schemas' @@ -20,6 +20,9 @@ export interface Matter { status: string trashedAt: number | null purgedAt: number | null + createdByActorType?: string | null + createdByActorRef?: string | null + createdByActorIssuer?: string | null createdAt: Date updatedAt: Date } @@ -34,6 +37,9 @@ export interface CreateMatterInput { object: string storageId: string status: string + createdByActorType?: ActorType | null + createdByActorRef?: string | null + createdByActorIssuer?: string | null /** How to handle name collision with an existing active sibling. Default 'fail'. */ onConflict?: ConflictStrategy } @@ -69,6 +75,9 @@ export interface UpdateMatterInput { export interface CopyMatterOptions { onConflict?: ConflictStrategy + createdByActorType?: ActorType | null + createdByActorRef?: string | null + createdByActorIssuer?: string | null } export interface ConflictResolveOptions { diff --git a/server/usecases/share.test.ts b/server/usecases/share.test.ts index b9daf05f..b58932a9 100644 --- a/server/usecases/share.test.ts +++ b/server/usecases/share.test.ts @@ -1110,6 +1110,7 @@ describe('saveShare', () => { targetOrgId: 'o-2', targetParent: '', accessCookie: undefined, + createdBy: { type: 'oauth' as const, ref: 'agent-1', issuer: 'https://realm.example.com' }, } it('returns matter_trashed when the share target was trashed', async () => { @@ -1183,6 +1184,7 @@ describe('saveShare', () => { currentUserId: 'u1', targetOrgId: 'o-2', targetParent: 'dest', + createdBy: { type: 'oauth', ref: 'agent-1', issuer: 'https://realm.example.com' }, }) }) }) diff --git a/server/usecases/share.ts b/server/usecases/share.ts index 0177074d..39f01482 100644 --- a/server/usecases/share.ts +++ b/server/usecases/share.ts @@ -18,6 +18,7 @@ import { verifyPassword as verifyPasswordHash } from '../lib/password' import type { Platform } from '../platform/interface' import { type SaveToDriveDeps, saveShareToDrive } from './object' import { + type ActorIdentity, AppError, badRequest, CreateShareError, @@ -641,6 +642,7 @@ export type SaveShareParams = { boundTargetOrgId?: string | null targetParent: string accessCookie: string | undefined + createdBy: ActorIdentity } export type SaveShareOutcome = @@ -674,7 +676,13 @@ export async function saveShare(deps: ShareDeps, params: SaveShareParams): Promi const totalBytes = await deps.share.computeSourceBytes(matter) if (!(await deps.share.hasQuotaForBytes(targetOrgId, totalBytes))) return { ok: false, error: quotaExceeded() } - const result = await saveShareToDrive(deps, { matter, currentUserId, targetOrgId, targetParent }) + const result = await saveShareToDrive(deps, { + matter, + currentUserId, + targetOrgId, + targetParent, + createdBy: params.createdBy, + }) return { ok: true, result } } diff --git a/server/usecases/site/audit.test.ts b/server/usecases/site/audit.test.ts index 9d076735..df3bb66f 100644 --- a/server/usecases/site/audit.test.ts +++ b/server/usecases/site/audit.test.ts @@ -8,12 +8,13 @@ describe('audit usecase', () => { const listAdminAudit = vi.fn(async () => result) const resolve = vi.fn(async () => new Map()) const findApiKeyNames = vi.fn(async () => new Map()) + const findUserProfiles = vi.fn(async () => new Map()) const findDeviceNames = vi.fn(async () => new Map()) const listTrustedAgentIssuerOrigins = vi.fn(async () => new Set()) const out = await listAuditEvents( { audit: { listAdminAudit } as Pick, - auditActorDirectory: { findApiKeyNames, findDeviceNames, listTrustedAgentIssuerOrigins }, + auditActorDirectory: { findUserProfiles, findApiKeyNames, findDeviceNames, listTrustedAgentIssuerOrigins }, agentInfo: { resolve } as AgentInfoGateway, }, { @@ -56,6 +57,7 @@ describe('audit usecase', () => { { audit: { listAdminAudit: async () => ({ items: [event], total: 1, page: 1, pageSize: 20 }) }, auditActorDirectory: { + findUserProfiles: async () => new Map(), findApiKeyNames: async () => new Map(), findDeviceNames: async () => new Map(), listTrustedAgentIssuerOrigins: async () => new Set(['https://id.realmroot.dev']), @@ -93,6 +95,7 @@ describe('audit usecase', () => { { audit: { listAdminAudit: async () => ({ items: [event], total: 1, page: 1, pageSize: 20 }) }, auditActorDirectory: { + findUserProfiles: async () => new Map(), findApiKeyNames: async () => new Map([['key-1', 'CME downloader']]), findDeviceNames: async () => new Map(), listTrustedAgentIssuerOrigins: async () => new Set(), @@ -132,6 +135,7 @@ describe('audit usecase', () => { { audit: { listAdminAudit: async () => ({ items: [event], total: 1, page: 1, pageSize: 20 }) }, auditActorDirectory: { + findUserProfiles: async () => new Map(), findApiKeyNames: async () => new Map(), findDeviceNames: async () => new Map([['device-1', 'Office Mac']]), listTrustedAgentIssuerOrigins: async () => new Set(), diff --git a/server/usecases/team.test.ts b/server/usecases/team.test.ts index 06a71581..949ec59e 100644 --- a/server/usecases/team.test.ts +++ b/server/usecases/team.test.ts @@ -87,6 +87,7 @@ function makeDeps( const deps: TeamDeps = { audit: { record: async () => {}, list: async () => ({ items: [], total: 0 }) } as unknown as AuditRepo, auditActorDirectory: { + findUserProfiles: async () => new Map(), findApiKeyNames: async () => new Map(), findDeviceNames: async () => new Map(), listTrustedAgentIssuerOrigins: async () => new Set(), diff --git a/server/usecases/webdav.test.ts b/server/usecases/webdav.test.ts index 2f4b7882..0e1a52cb 100644 --- a/server/usecases/webdav.test.ts +++ b/server/usecases/webdav.test.ts @@ -479,6 +479,7 @@ describe('webdav usecase', () => { contentType: 'text/plain', contentLength: 9 as number | null, body: new Uint8Array(9), + createdBy: { type: 'api_key' as const, ref: 'key-1', issuer: null }, ...overrides, }) @@ -503,6 +504,9 @@ describe('webdav usecase', () => { dirtype: DirType.FILE, status: 'active', object: storageKey, + createdByActorType: 'api_key', + createdByActorRef: 'key-1', + createdByActorIssuer: null, }), ) }) @@ -607,7 +611,13 @@ describe('webdav usecase', () => { it('creates a folder matter under the selected private storage', async () => { const create = vi.fn(async () => folder('Projects')) const deps = makeDeps({ matter: { create } }) - await createWebDavCollection(deps, { orgId: 'ws-1', userId: 'u1', name: 'Projects', parent: 'Docs' }) + await createWebDavCollection(deps, { + orgId: 'ws-1', + userId: 'u1', + name: 'Projects', + parent: 'Docs', + createdBy: { type: 'api_key', ref: 'key-1', issuer: null }, + }) expect(create).toHaveBeenCalledWith( expect.objectContaining({ name: 'Projects', @@ -616,6 +626,8 @@ describe('webdav usecase', () => { parent: 'Docs', object: '', status: 'active', + createdByActorType: 'api_key', + createdByActorRef: 'key-1', }), ) }) @@ -720,6 +732,7 @@ describe('webdav usecase', () => { targetResourcePath: 'dst.txt', replacedMatterId: null, replacingTarget: false, + createdBy: { type: 'api_key' as const, ref: 'key-1', issuer: null }, ...overrides, }) @@ -737,6 +750,12 @@ describe('webdav usecase', () => { expect(sourceMatter.object).toBe('legacy_/webdav-source-.txt') expect(destinationKey).toMatch(/^ws1\/u1\/\d{8}\/[A-Za-z0-9]{17}\.txt$/) expect(copyObject).toHaveBeenCalledWith(storage, sourceMatter.object, storage, destinationKey) + expect(copy).toHaveBeenCalledWith(expect.any(Object), '', destinationKey, { + onConflict: 'fail', + createdByActorType: 'api_key', + createdByActorRef: 'key-1', + createdByActorIssuer: null, + }) expect(copyDeadProperties).toHaveBeenCalledWith('ws1', 'src.txt', 'dst.txt') }) @@ -794,6 +813,7 @@ describe('webdav usecase', () => { targetMatter: null, replacingTarget: false, depth: 'infinity' as const, + createdBy: { type: 'api_key' as const, ref: 'key-1', issuer: null }, ...overrides, }) @@ -823,6 +843,12 @@ describe('webdav usecase', () => { object: expect.stringMatching(/^ws1\/u1\/\d{8}\/[A-Za-z0-9]{17}\.txt$/), }, ]) + expect(copy).toHaveBeenCalledWith(expect.any(Object), '', '', { + onConflict: 'fail', + createdByActorType: 'api_key', + createdByActorRef: 'key-1', + createdByActorIssuer: null, + }) }) it('copies only the root when depth=0', async () => { @@ -881,6 +907,7 @@ describe('webdav usecase', () => { owner: 'tester', depth: '0', timeoutSeconds: 600, + createdBy: { type: 'api_key', ref: 'key-1', issuer: null }, }) expect(out).toEqual({ lock, created: false }) expect(create).not.toHaveBeenCalled() @@ -906,6 +933,7 @@ describe('webdav usecase', () => { owner: 'tester', depth: 'infinity', timeoutSeconds: 3600, + createdBy: { type: 'api_key', ref: 'key-1', issuer: null }, }) expect(out).not.toBeNull() if (!out) throw new Error('Expected lock to be created') @@ -920,6 +948,8 @@ describe('webdav usecase', () => { dirtype: DirType.FILE, status: 'active', object: storageKey, + createdByActorType: 'api_key', + createdByActorRef: 'key-1', }), ) }) diff --git a/server/usecases/webdav.ts b/server/usecases/webdav.ts index 2a29419e..5c0d61ce 100644 --- a/server/usecases/webdav.ts +++ b/server/usecases/webdav.ts @@ -19,6 +19,7 @@ import type { Database } from '../platform/interface' import type { Deps } from './deps' import { assertFolderNotUsedByDownload } from './downloads/download-folders' import { + type ActorIdentity, type ApiKeyAuth, ApiKeyRateLimitError, type DavDeadProperty, @@ -308,6 +309,7 @@ export async function putWebDavFile( contentType: string contentLength: number | null body: ReadableStream | Uint8Array + createdBy: ActorIdentity onTiming?: (phase: 'storage' | 's3' | 'persist', durationMs: number) => void }, ): Promise { @@ -351,6 +353,7 @@ export async function putWebDavFile( objectKey, contentType, uploadedSize, + createdBy: params.createdBy, }), ), ) @@ -366,6 +369,7 @@ export async function putWebDavFile( objectKey, contentType, uploadedSize, + createdBy: params.createdBy, }), ) if (sizeDelta < 0) await deps.storageUsage.reconcile(orgId, [storage.id]) @@ -402,6 +406,7 @@ async function persistWebDavUpload( objectKey: string contentType: string uploadedSize: number + createdBy: ActorIdentity }, ): Promise<{ status: 201 | 204; matterId: string; bytes: number }> { const { orgId, target, fileName, parent, storage, objectKey, contentType, uploadedSize } = params @@ -421,6 +426,9 @@ async function persistWebDavUpload( object: objectKey, storageId: storage.id, status: ObjectStatus.ACTIVE, + createdByActorType: params.createdBy.type, + createdByActorRef: params.createdBy.ref, + createdByActorIssuer: params.createdBy.issuer, }) return { status: 201, matterId: matter.id, bytes: uploadedSize } } @@ -429,7 +437,7 @@ async function persistWebDavUpload( export async function createWebDavCollection( deps: Pick, - params: { orgId: string; userId: string; name: string; parent: string }, + params: { orgId: string; userId: string; name: string; parent: string; createdBy: ActorIdentity }, ): Promise { const storage = await deps.storages.select() await deps.matter.create({ @@ -442,6 +450,9 @@ export async function createWebDavCollection( object: '', storageId: storage.id, status: ObjectStatus.ACTIVE, + createdByActorType: params.createdBy.type, + createdByActorRef: params.createdBy.ref, + createdByActorIssuer: params.createdBy.issuer, }) } @@ -514,6 +525,7 @@ export async function copyWebDavFile( targetResourcePath: string replacedMatterId: string | null replacingTarget: boolean + createdBy: ActorIdentity }, ): Promise { const { orgId, userId, sourceMatter } = params @@ -540,6 +552,9 @@ export async function copyWebDavFile( } const copy = await deps.matter.copy({ ...sourceMatter, name: params.targetName }, params.targetParent, newObject, { onConflict: 'fail', + createdByActorType: params.createdBy.type, + createdByActorRef: params.createdBy.ref, + createdByActorIssuer: params.createdBy.issuer, }) const copyPath = joinMatterPath(copy.parent, copy.name) await deps.webdavState.copyDeadProperties(orgId, params.sourceResourcePath, copyPath) @@ -575,6 +590,7 @@ export async function copyWebDavCollection( targetMatter: Matter | null replacingTarget: boolean depth: '0' | 'infinity' + createdBy: ActorIdentity }, ): Promise { const { orgId, userId, sourceMatter, sourceRoot, targetMatter } = params @@ -627,6 +643,9 @@ export async function copyWebDavCollection( const rootCopy = await deps.matter.copy({ ...sourceMatter, name: params.targetName }, params.targetParent, '', { onConflict: 'fail', + createdByActorType: params.createdBy.type, + createdByActorRef: params.createdBy.ref, + createdByActorIssuer: params.createdBy.issuer, }) createdIds.push(rootCopy.id) await deps.webdavState.copyDeadProperties(orgId, sourceRoot, joinMatterPath(rootCopy.parent, rootCopy.name)) @@ -634,6 +653,9 @@ export async function copyWebDavCollection( for (const prepared of preparedCopies) { const copy = await deps.matter.copy(prepared.item, prepared.targetParent, prepared.objectKey, { onConflict: 'fail', + createdByActorType: params.createdBy.type, + createdByActorRef: params.createdBy.ref, + createdByActorIssuer: params.createdBy.issuer, }) createdIds.push(copy.id) await deps.webdavState.copyDeadProperties( @@ -698,6 +720,7 @@ export async function createWebDavLock( owner: string depth: string timeoutSeconds: number + createdBy: ActorIdentity }, ): Promise<{ lock: DavLock; created: boolean } | null> { const { orgId, userId, target } = params @@ -729,6 +752,9 @@ export async function createWebDavLock( object: objectKey, storageId: storage.id, status: ObjectStatus.ACTIVE, + createdByActorType: params.createdBy.type, + createdByActorRef: params.createdBy.ref, + createdByActorIssuer: params.createdBy.issuer, }) const lock = await deps.webdavState.createLock({ orgId, diff --git a/shared/schemas/actors.ts b/shared/schemas/actors.ts new file mode 100644 index 00000000..21959b89 --- /dev/null +++ b/shared/schemas/actors.ts @@ -0,0 +1,26 @@ +import { z } from '@hono/zod-openapi' + +export const actorTypeSchema = z.enum([ + 'user', + 'api_key', + 'oauth', + 'agent', + 'anonymous', + 'system', + 'device', + 'task-upload', +]) + +export const actorAttributionSchema = z + .object({ + type: actorTypeSchema, + ref: z.string().nullable(), + issuer: z.string().nullable(), + name: z.string(), + image: z.string().nullable(), + resolved: z.boolean(), + }) + .openapi('ActorAttribution') + +export type ActorType = z.infer +export type ActorAttribution = z.infer diff --git a/shared/schemas/downloads.ts b/shared/schemas/downloads.ts index 37d8a824..f50932f8 100644 --- a/shared/schemas/downloads.ts +++ b/shared/schemas/downloads.ts @@ -1,5 +1,6 @@ import { z } from '@hono/zod-openapi' import { isSafeHttpUrl } from '../url-safety' +import { actorAttributionSchema } from './actors' import { opaqueIdSchema } from './id' export const downloaderStatusSchema = z.enum(['online', 'offline', 'disabled']) @@ -125,6 +126,7 @@ export const downloadTaskSchema = z id: opaqueIdSchema, orgId: opaqueIdSchema.optional(), createdBy: opaqueIdSchema.optional(), + requestedBy: actorAttributionSchema.nullable().optional(), spec: z.object({ source: z.object({ type: downloadSourceTypeSchema, @@ -147,6 +149,7 @@ export const downloadTaskSchema = z downloaderId: opaqueIdSchema, assignedAt: z.string().nullable().optional(), uploadToken: z.string().optional(), + executor: actorAttributionSchema.optional(), }) .nullable(), progress: downloadTaskProgressSchema, diff --git a/shared/schemas/index.ts b/shared/schemas/index.ts index e79dcc37..6c763de9 100644 --- a/shared/schemas/index.ts +++ b/shared/schemas/index.ts @@ -1,6 +1,9 @@ import { z } from 'zod' import { opaqueIdSchema } from './id' +export type { ActorAttribution, ActorType } from './actors' +export { actorAttributionSchema, actorTypeSchema } from './actors' + export { adminAnalyticsGrowthSchema, adminAnalyticsOperationsSchema, diff --git a/shared/types/index.ts b/shared/types/index.ts index e6e89a56..ef3e8a00 100644 --- a/shared/types/index.ts +++ b/shared/types/index.ts @@ -1,5 +1,6 @@ import type { CommercePayment, CommerceProduct, ProductPrice } from 'zpan-cloud-sdk' import type { DirType, ObjectStatus, StorageStatus, StorageStatusReason } from '../constants' +import type { ActorAttribution } from '../schemas/actors' import type { CloudOrder as ZPanCloudOrder, CloudOrderFulfillmentPayload as ZPanCloudOrderFulfillmentPayload, @@ -20,6 +21,7 @@ export interface StorageObject { status: ObjectStatus // Soft-delete marker: null = live, epoch ms = in trash. trashedAt: number | null + createdBy?: ActorAttribution | null createdAt: string updatedAt: string } diff --git a/spec/download-tasks.feature b/spec/download-tasks.feature index 2392e657..2d4f6374 100644 --- a/spec/download-tasks.feature +++ b/spec/download-tasks.feature @@ -129,6 +129,12 @@ Feature: Remote download tasks When a user lists tasks and then opens one task Then the list returns only its required fields and the detail returns the full runtime + @download-tasks/actor-attribution @api + Scenario: Task detail identifies the requester and executing device + Given a task created by an authenticated actor and assigned to a downloader + When a user opens the task detail + Then the requester and executing device display profiles are returned separately + @download-tasks/events @api Scenario: Task events expose lifecycle and audit history Given a task with lifecycle timestamps and audit events diff --git a/spec/objects.feature b/spec/objects.feature index a4935eab..53dd017b 100644 --- a/spec/objects.feature +++ b/spec/objects.feature @@ -41,6 +41,18 @@ Feature: Objects When they create a folder Then the folder is created + @objects/creator-attribution @api + Scenario: Object creation records the authenticated actor + Given an authenticated user, API key, agent, or device + When that actor creates an object + Then the object stores the stable actor identity and returns its display profile + + @objects/legacy-creator @api + Scenario: Historical objects do not impersonate the workspace owner + Given an object created before actor attribution was recorded + When the object is listed or inspected + Then its creator is reported as not recorded + @objects/create-invalid @api Scenario: Invalid create input is rejected Given invalid object input diff --git a/src/components/actor-identity.test.tsx b/src/components/actor-identity.test.tsx new file mode 100644 index 00000000..c43ad911 --- /dev/null +++ b/src/components/actor-identity.test.tsx @@ -0,0 +1,103 @@ +import type { ActorAttribution } from '@shared/schemas' +import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react' +import { afterEach, describe, expect, it, vi } from 'vitest' +import { ActorAvatarHoverCard, ActorIdentity } from './actor-identity' + +vi.mock('react-i18next', () => ({ + useTranslation: () => ({ t: (key: string) => (key === 'actors.notRecorded' ? '-' : key) }), +})) + +afterEach(cleanup) + +describe('ActorIdentity', () => { + it('renders the resolved name and avatar', () => { + const actor: ActorAttribution = { + type: 'agent', + ref: 'agent-1', + issuer: 'https://realm.example.com', + name: 'Research Agent', + image: 'https://example.com/agent.png', + resolved: true, + } + + render() + + expect(screen.getByText('Research Agent')).toBeTruthy() + expect(screen.getByTitle('Research Agent')).toBeTruthy() + }) + + it('uses a dash for missing historical attribution', () => { + render() + + expect(screen.getByText('-')).toBeTruthy() + }) + + it('renders machine identities without inventing user initials', () => { + render( + , + ) + + expect(screen.getByText('Device · downloader')).toBeTruthy() + expect(document.querySelector('svg')).toBeTruthy() + }) +}) + +describe('ActorAvatarHoverCard', () => { + it('keeps the file-list cell avatar-only and reveals the detailed identity card on hover', async () => { + const actor: ActorAttribution = { + type: 'oauth', + ref: 'agent-1', + issuer: 'https://realm.example.com', + name: 'Research Agent', + image: 'https://example.com/agent.png', + resolved: true, + } + + render() + + const trigger = screen.getByLabelText('files.createdBy: Research Agent') + expect(trigger.textContent).toBe('') + fireEvent.pointerEnter(trigger) + + await waitFor(() => expect(screen.getByText('Research Agent')).toBeTruthy()) + expect(screen.getByText('actors.type.oauth')).toBeTruthy() + expect(screen.getByText('agent-1')).toBeTruthy() + expect(screen.getByText('realm.example.com')).toBeTruthy() + }) + + it('shortens long stable identifiers without losing the full value', async () => { + const ref = 'agent-0123456789abcdef0123456789abcdef' + render( + , + ) + + fireEvent.pointerEnter(screen.getByLabelText('files.createdBy: Research Agent')) + + await waitFor(() => expect(screen.getByText('agent-01…abcdef')).toBeTruthy()) + expect(screen.getByTitle(ref)).toBeTruthy() + expect(screen.queryByText(ref)).toBeNull() + }) + + it('uses a dash when attribution was not recorded', () => { + render() + expect(screen.getByText('-')).toBeTruthy() + }) +}) diff --git a/src/components/actor-identity.tsx b/src/components/actor-identity.tsx new file mode 100644 index 00000000..20a879f4 --- /dev/null +++ b/src/components/actor-identity.tsx @@ -0,0 +1,134 @@ +import type { ActorAttribution } from '@shared/schemas' +import type { AuditEvent } from '@shared/types' +import { Bot, CircleHelp, KeyRound, Monitor, UserRound } from 'lucide-react' +import { useTranslation } from 'react-i18next' +import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar' +import { Badge } from '@/components/ui/badge' +import { HoverCard, HoverCardContent, HoverCardTrigger } from '@/components/ui/hover-card' +import { Separator } from '@/components/ui/separator' +import { cn } from '@/lib/utils' + +interface ActorIdentityProps { + actor?: ActorAttribution | null + compact?: boolean + className?: string +} + +export function auditEventActor(event: AuditEvent): ActorAttribution { + return { + type: event.actorType, + ref: event.actorRef, + issuer: event.actorIssuer, + ...event.actor, + } +} + +function ActorFallback({ actor }: { actor: ActorAttribution }) { + if (actor.type === 'api_key') return