@opengeni/db 0.27.11 → 0.28.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-JYQPJ6Y5.js → chunk-P6CUHXQZ.js} +2967 -2272
- package/dist/chunk-P6CUHXQZ.js.map +1 -0
- package/dist/{chunk-RPHPNVWW.js → chunk-VHBFHMPC.js} +15 -1
- package/dist/chunk-VHBFHMPC.js.map +1 -0
- package/dist/environment-crypto.d.ts +8 -4
- package/dist/index.d.ts +272 -26
- package/dist/index.js +7798 -5008
- package/dist/index.js.map +1 -1
- package/dist/lossless-columns.d.ts +58 -0
- package/dist/lossless-json.d.ts +39 -0
- package/dist/memory-domain.d.ts +3 -6
- package/dist/persistence-errors.d.ts +7 -14
- package/dist/provision-roles.js +1 -1
- package/dist/runtime-posture.d.ts +2 -2
- package/dist/schema.d.ts +1420 -271
- package/dist/schema.js +25 -1
- package/dist/session-realtime-terminal.d.ts +7 -0
- package/dist/transcription-recordings-schema.d.ts +1264 -0
- package/dist/transcription-recordings.d.ts +228 -0
- package/drizzle/0065_enrollment_credential_generation.sql +10 -0
- package/drizzle/0140_retained_screenshot_artifacts.sql +192 -0
- package/drizzle/0170_resumable_transcription_recordings.sql +440 -0
- package/drizzle/0175_resumable_transcription_provider_deadline.sql +24 -0
- package/drizzle/0176_lossless_canonical_json.sql +975 -0
- package/drizzle/0177_session_events_workspace_turn_type_index.sql +5 -0
- package/drizzle/0178_permissioned_secret_reads.sql +14 -0
- package/drizzle/0179_slack_private_shortcut_delivery_gate.sql +85 -0
- package/drizzle/0180_retained_screenshot_lifecycle_fences.sql +273 -0
- package/drizzle/0181_connected_machine_removal.sql +52 -0
- package/drizzle/0182_connected_machine_remove_session_default.sql +77 -0
- package/package.json +4 -4
- package/src/database.ts +7 -1
- package/src/environment-crypto.ts +22 -9
- package/src/index.ts +4368 -1678
- package/src/lossless-columns.ts +35 -0
- package/src/lossless-json.ts +322 -0
- package/src/memory-domain.ts +5 -44
- package/src/persistence-errors.ts +29 -34
- package/src/runtime-posture.ts +14 -0
- package/src/schema.ts +264 -42
- package/src/session-control.ts +125 -76
- package/src/session-queue-commands.ts +325 -234
- package/src/session-realtime-context.ts +18 -5
- package/src/session-realtime-ledger.ts +29 -7
- package/src/session-realtime-mirror.ts +4 -2
- package/src/session-realtime-terminal.ts +89 -21
- package/src/session-realtime.ts +3 -2
- package/src/session-tool-call-settlement.ts +29 -15
- package/src/transcription-recordings-schema.ts +253 -0
- package/src/transcription-recordings.ts +1538 -0
- package/dist/chunk-JYQPJ6Y5.js.map +0 -1
- package/dist/chunk-RPHPNVWW.js.map +0 -1
- package/dist/event-payload-sanitizer.d.ts +0 -32
- package/src/event-payload-sanitizer.ts +0 -377
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import type { TranscriptionRecording, TranscriptionRecordingErrorCode, TranscriptionRecordingResponse } from "@opengeni/contracts";
|
|
2
|
+
import type { Database } from "./database";
|
|
3
|
+
import * as schema from "./schema";
|
|
4
|
+
type ChunkRow = typeof schema.transcriptionRecordingChunks.$inferSelect;
|
|
5
|
+
type SegmentRow = typeof schema.transcriptionRecordingSegments.$inferSelect;
|
|
6
|
+
export declare class TranscriptionRecordingNotFoundError extends Error {
|
|
7
|
+
readonly name = "TranscriptionRecordingNotFoundError";
|
|
8
|
+
}
|
|
9
|
+
export declare class TranscriptionRecordingConflictError extends Error {
|
|
10
|
+
readonly name = "TranscriptionRecordingConflictError";
|
|
11
|
+
}
|
|
12
|
+
export declare class TranscriptionRecordingStateError extends Error {
|
|
13
|
+
readonly name = "TranscriptionRecordingStateError";
|
|
14
|
+
}
|
|
15
|
+
export type TranscriptionRecordingChunkReservation = {
|
|
16
|
+
recording: TranscriptionRecordingResponse;
|
|
17
|
+
chunk: ChunkRow;
|
|
18
|
+
deduplicated: boolean;
|
|
19
|
+
};
|
|
20
|
+
export type TranscriptionRecordingAssemblyClaim = {
|
|
21
|
+
recording: TranscriptionRecordingResponse;
|
|
22
|
+
claimed: boolean;
|
|
23
|
+
generation: number;
|
|
24
|
+
owner: string | null;
|
|
25
|
+
staleObjectKeys: string[];
|
|
26
|
+
};
|
|
27
|
+
export type TranscriptionRecordingSegmentClaim = {
|
|
28
|
+
recording: TranscriptionRecordingResponse;
|
|
29
|
+
claimed: boolean;
|
|
30
|
+
attemptId: string | null;
|
|
31
|
+
segment: SegmentRow | null;
|
|
32
|
+
};
|
|
33
|
+
export type TranscriptionRecordingObjectCleanupClaim = {
|
|
34
|
+
accountId: string;
|
|
35
|
+
workspaceId: string;
|
|
36
|
+
subjectId: string;
|
|
37
|
+
recordingId: string;
|
|
38
|
+
objectKey: string;
|
|
39
|
+
cleanupClaimId: string;
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* A stale attempt is reclaimable only after both durable fences have expired:
|
|
43
|
+
* the processing lease and the server-owned provider deadline. Missing
|
|
44
|
+
* deadlines fail closed so an old pre-deadline row cannot create an overlap.
|
|
45
|
+
*/
|
|
46
|
+
export declare function canReclaimTranscriptionRecordingAttempt(input: {
|
|
47
|
+
attemptStartedAt: Date | null;
|
|
48
|
+
attemptDeadlineAt: Date | null;
|
|
49
|
+
staleBefore: Date;
|
|
50
|
+
now: Date;
|
|
51
|
+
}): boolean;
|
|
52
|
+
export declare function transcriptionRecordingChunkObjectKey(input: {
|
|
53
|
+
accountId: string;
|
|
54
|
+
workspaceId: string;
|
|
55
|
+
recordingId: string;
|
|
56
|
+
chunkNumber: number;
|
|
57
|
+
sha256: string;
|
|
58
|
+
}): string;
|
|
59
|
+
export declare function transcriptionRecordingSegmentObjectKey(input: {
|
|
60
|
+
accountId: string;
|
|
61
|
+
workspaceId: string;
|
|
62
|
+
recordingId: string;
|
|
63
|
+
generation: number;
|
|
64
|
+
segmentNumber: number;
|
|
65
|
+
sha256: string;
|
|
66
|
+
}): string;
|
|
67
|
+
export declare function createTranscriptionRecording(db: Database, input: {
|
|
68
|
+
accountId: string;
|
|
69
|
+
workspaceId: string;
|
|
70
|
+
subjectId: string;
|
|
71
|
+
recordingId: string;
|
|
72
|
+
mimeType: string;
|
|
73
|
+
expiresAt: Date;
|
|
74
|
+
}): Promise<TranscriptionRecordingResponse>;
|
|
75
|
+
export declare function getTranscriptionRecording(db: Database, input: {
|
|
76
|
+
workspaceId: string;
|
|
77
|
+
subjectId: string;
|
|
78
|
+
recordingId: string;
|
|
79
|
+
}): Promise<TranscriptionRecordingResponse>;
|
|
80
|
+
export declare function listTranscriptionRecordings(db: Database, input: {
|
|
81
|
+
workspaceId: string;
|
|
82
|
+
subjectId: string;
|
|
83
|
+
limit?: number;
|
|
84
|
+
}): Promise<TranscriptionRecording[]>;
|
|
85
|
+
export declare function reserveTranscriptionRecordingChunk(db: Database, input: {
|
|
86
|
+
accountId: string;
|
|
87
|
+
workspaceId: string;
|
|
88
|
+
subjectId: string;
|
|
89
|
+
recordingId: string;
|
|
90
|
+
chunkNumber: number;
|
|
91
|
+
byteLength: number;
|
|
92
|
+
sha256: string;
|
|
93
|
+
startMilliseconds: number;
|
|
94
|
+
durationMilliseconds: number;
|
|
95
|
+
maxTotalBytes: number;
|
|
96
|
+
maxDurationMilliseconds: number;
|
|
97
|
+
}): Promise<TranscriptionRecordingChunkReservation>;
|
|
98
|
+
export declare function completeTranscriptionRecordingChunk(db: Database, input: {
|
|
99
|
+
workspaceId: string;
|
|
100
|
+
subjectId: string;
|
|
101
|
+
recordingId: string;
|
|
102
|
+
chunkNumber: number;
|
|
103
|
+
}): Promise<TranscriptionRecordingChunkReservation>;
|
|
104
|
+
export declare function claimTranscriptionRecordingAssembly(db: Database, input: {
|
|
105
|
+
workspaceId: string;
|
|
106
|
+
subjectId: string;
|
|
107
|
+
recordingId: string;
|
|
108
|
+
owner: string;
|
|
109
|
+
chunkCount: number;
|
|
110
|
+
totalBytes: number;
|
|
111
|
+
totalDurationMilliseconds: number;
|
|
112
|
+
staleBefore: Date;
|
|
113
|
+
}): Promise<TranscriptionRecordingAssemblyClaim>;
|
|
114
|
+
export declare function listTranscriptionRecordingChunks(db: Database, input: {
|
|
115
|
+
workspaceId: string;
|
|
116
|
+
subjectId: string;
|
|
117
|
+
recordingId: string;
|
|
118
|
+
}): Promise<ChunkRow[]>;
|
|
119
|
+
export declare function reserveTranscriptionRecordingSegment(db: Database, input: {
|
|
120
|
+
accountId: string;
|
|
121
|
+
workspaceId: string;
|
|
122
|
+
subjectId: string;
|
|
123
|
+
recordingId: string;
|
|
124
|
+
owner: string;
|
|
125
|
+
generation: number;
|
|
126
|
+
segmentNumber: number;
|
|
127
|
+
byteLength: number;
|
|
128
|
+
sha256: string;
|
|
129
|
+
startMilliseconds: number;
|
|
130
|
+
durationMilliseconds: number;
|
|
131
|
+
}): Promise<SegmentRow>;
|
|
132
|
+
export declare function completeTranscriptionRecordingSegmentPreparation(db: Database, input: {
|
|
133
|
+
workspaceId: string;
|
|
134
|
+
subjectId: string;
|
|
135
|
+
recordingId: string;
|
|
136
|
+
owner: string;
|
|
137
|
+
generation: number;
|
|
138
|
+
segmentNumber: number;
|
|
139
|
+
}): Promise<void>;
|
|
140
|
+
export declare function completeTranscriptionRecordingAssembly(db: Database, input: {
|
|
141
|
+
workspaceId: string;
|
|
142
|
+
subjectId: string;
|
|
143
|
+
recordingId: string;
|
|
144
|
+
owner: string;
|
|
145
|
+
generation: number;
|
|
146
|
+
}): Promise<TranscriptionRecordingResponse>;
|
|
147
|
+
export declare function failTranscriptionRecordingAssembly(db: Database, input: {
|
|
148
|
+
workspaceId: string;
|
|
149
|
+
subjectId: string;
|
|
150
|
+
recordingId: string;
|
|
151
|
+
owner: string;
|
|
152
|
+
generation: number;
|
|
153
|
+
errorCode: TranscriptionRecordingErrorCode;
|
|
154
|
+
retryable: boolean;
|
|
155
|
+
}): Promise<TranscriptionRecordingResponse>;
|
|
156
|
+
export declare function claimNextTranscriptionRecordingSegment(db: Database, input: {
|
|
157
|
+
workspaceId: string;
|
|
158
|
+
subjectId: string;
|
|
159
|
+
recordingId: string;
|
|
160
|
+
attemptId: string;
|
|
161
|
+
providerId: string;
|
|
162
|
+
staleBefore: Date;
|
|
163
|
+
providerDeadlineAt: Date;
|
|
164
|
+
}): Promise<TranscriptionRecordingSegmentClaim>;
|
|
165
|
+
export declare function startTranscriptionRecordingSegmentProviderCall(db: Database, input: {
|
|
166
|
+
workspaceId: string;
|
|
167
|
+
subjectId: string;
|
|
168
|
+
recordingId: string;
|
|
169
|
+
segmentNumber: number;
|
|
170
|
+
attemptId: string;
|
|
171
|
+
providerStartedAt: Date;
|
|
172
|
+
providerDeadlineAt: Date;
|
|
173
|
+
}): Promise<void>;
|
|
174
|
+
export declare function assembleTranscriptionSegments(segments: readonly (Pick<SegmentRow, "segmentNumber" | "transcriptText" | "languages"> & Partial<Pick<SegmentRow, "transcriptTextCodecVersion">>)[]): {
|
|
175
|
+
text: string;
|
|
176
|
+
languages: string[];
|
|
177
|
+
};
|
|
178
|
+
export declare function completeTranscriptionRecordingSegment(db: Database, input: {
|
|
179
|
+
workspaceId: string;
|
|
180
|
+
subjectId: string;
|
|
181
|
+
recordingId: string;
|
|
182
|
+
segmentNumber: number;
|
|
183
|
+
attemptId: string;
|
|
184
|
+
text: string;
|
|
185
|
+
languages: string[];
|
|
186
|
+
providerId: string;
|
|
187
|
+
}): Promise<TranscriptionRecordingResponse>;
|
|
188
|
+
export declare function failTranscriptionRecordingSegment(db: Database, input: {
|
|
189
|
+
workspaceId: string;
|
|
190
|
+
subjectId: string;
|
|
191
|
+
recordingId: string;
|
|
192
|
+
segmentNumber: number;
|
|
193
|
+
attemptId: string;
|
|
194
|
+
errorCode: TranscriptionRecordingErrorCode;
|
|
195
|
+
retryable: boolean;
|
|
196
|
+
}): Promise<TranscriptionRecordingResponse>;
|
|
197
|
+
export declare function transcriptionRecordingObjectKeys(db: Database, input: {
|
|
198
|
+
workspaceId: string;
|
|
199
|
+
subjectId: string;
|
|
200
|
+
recordingId: string;
|
|
201
|
+
}): Promise<string[]>;
|
|
202
|
+
export declare function markTranscriptionRecordingObjectCleaned(db: Database, input: {
|
|
203
|
+
workspaceId: string;
|
|
204
|
+
subjectId: string;
|
|
205
|
+
recordingId: string;
|
|
206
|
+
objectKey: string;
|
|
207
|
+
}): Promise<TranscriptionRecordingResponse>;
|
|
208
|
+
export declare function markTranscriptionRecordingObjectsCleaned(db: Database, input: {
|
|
209
|
+
workspaceId: string;
|
|
210
|
+
subjectId: string;
|
|
211
|
+
recordingId: string;
|
|
212
|
+
}): Promise<TranscriptionRecordingResponse>;
|
|
213
|
+
export declare function discardTranscriptionRecording(db: Database, input: {
|
|
214
|
+
workspaceId: string;
|
|
215
|
+
subjectId: string;
|
|
216
|
+
recordingId: string;
|
|
217
|
+
}): Promise<TranscriptionRecordingResponse>;
|
|
218
|
+
export declare function claimDueTranscriptionRecordingObjectCleanup(db: Database, input: {
|
|
219
|
+
graceMs: number;
|
|
220
|
+
claimTimeoutMs: number;
|
|
221
|
+
limit: number;
|
|
222
|
+
}): Promise<TranscriptionRecordingObjectCleanupClaim[]>;
|
|
223
|
+
export declare function completeDueTranscriptionRecordingObjectCleanup(db: Database, input: TranscriptionRecordingObjectCleanupClaim): Promise<boolean>;
|
|
224
|
+
export declare function purgeExpiredTranscriptionRecordings(db: Database, input: {
|
|
225
|
+
graceMs: number;
|
|
226
|
+
limit: number;
|
|
227
|
+
}): Promise<number>;
|
|
228
|
+
export {};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
-- deployment-mode: rolling
|
|
2
|
+
-- Enrollment credential-generation fence.
|
|
3
|
+
--
|
|
4
|
+
-- Schema-first/backward-compatible rollout: every pre-existing enrollment is
|
|
5
|
+
-- generation 1, matching legacy signed bearers that did not carry an explicit
|
|
6
|
+
-- generation claim. New binaries atomically increment this column whenever the
|
|
7
|
+
-- same machine identity is enrolled again and require the signed claim to match.
|
|
8
|
+
|
|
9
|
+
ALTER TABLE "enrollments"
|
|
10
|
+
ADD COLUMN IF NOT EXISTS "credential_generation" integer NOT NULL DEFAULT 1;
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
-- deployment-mode: rolling
|
|
2
|
+
-- Retain computer screenshots as deterministic, tenant-owned files while
|
|
3
|
+
-- keeping provider object locations server-private. Pending/ready bytes are
|
|
4
|
+
-- quota-accounted, and one bounded SECURITY DEFINER claim seam drives stale
|
|
5
|
+
-- settlement reconciliation plus expiry/orphan cleanup across FORCE-RLS rows.
|
|
6
|
+
|
|
7
|
+
CREATE UNIQUE INDEX IF NOT EXISTS "files_workspace_id_uq"
|
|
8
|
+
ON "files" ("workspace_id", "id");
|
|
9
|
+
|
|
10
|
+
CREATE TABLE "workspace_screenshot_quotas" (
|
|
11
|
+
"workspace_id" uuid PRIMARY KEY,
|
|
12
|
+
"account_id" uuid NOT NULL,
|
|
13
|
+
"reserved_bytes" bigint NOT NULL DEFAULT 0,
|
|
14
|
+
"ready_bytes" bigint NOT NULL DEFAULT 0,
|
|
15
|
+
"updated_at" timestamptz NOT NULL DEFAULT now(),
|
|
16
|
+
CONSTRAINT "workspace_screenshot_quotas_workspace_account_fk"
|
|
17
|
+
FOREIGN KEY ("workspace_id", "account_id")
|
|
18
|
+
REFERENCES "workspaces"("id", "account_id") ON DELETE CASCADE,
|
|
19
|
+
CONSTRAINT "workspace_screenshot_quotas_nonnegative_chk"
|
|
20
|
+
CHECK ("reserved_bytes" >= 0 AND "ready_bytes" >= 0)
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
CREATE TABLE "retained_screenshot_artifacts" (
|
|
24
|
+
"artifact_id" uuid PRIMARY KEY,
|
|
25
|
+
"account_id" uuid NOT NULL,
|
|
26
|
+
"workspace_id" uuid NOT NULL,
|
|
27
|
+
"session_id" uuid NOT NULL,
|
|
28
|
+
"turn_id" uuid NOT NULL,
|
|
29
|
+
"attempt_id" uuid NOT NULL,
|
|
30
|
+
"settlement_key" text NOT NULL,
|
|
31
|
+
"tool_call_id" text NOT NULL,
|
|
32
|
+
"tool_output_id" text NOT NULL,
|
|
33
|
+
"status" text NOT NULL DEFAULT 'pending',
|
|
34
|
+
"media_type" text NOT NULL,
|
|
35
|
+
"size_bytes" bigint NOT NULL,
|
|
36
|
+
"sha256" text NOT NULL,
|
|
37
|
+
"width" integer NOT NULL,
|
|
38
|
+
"height" integer NOT NULL,
|
|
39
|
+
"retention_expires_at" timestamptz NOT NULL,
|
|
40
|
+
"ready_at" timestamptz,
|
|
41
|
+
"cleanup_reason" text,
|
|
42
|
+
"last_error" text,
|
|
43
|
+
"created_at" timestamptz NOT NULL DEFAULT now(),
|
|
44
|
+
"updated_at" timestamptz NOT NULL DEFAULT now(),
|
|
45
|
+
CONSTRAINT "retained_screenshot_artifacts_workspace_account_fk"
|
|
46
|
+
FOREIGN KEY ("workspace_id", "account_id")
|
|
47
|
+
REFERENCES "workspaces"("id", "account_id") ON DELETE CASCADE,
|
|
48
|
+
CONSTRAINT "retained_screenshot_artifacts_workspace_session_fk"
|
|
49
|
+
FOREIGN KEY ("workspace_id", "session_id")
|
|
50
|
+
REFERENCES "sessions"("workspace_id", "id") ON DELETE CASCADE,
|
|
51
|
+
CONSTRAINT "retained_screenshot_artifacts_workspace_turn_fk"
|
|
52
|
+
FOREIGN KEY ("workspace_id", "turn_id")
|
|
53
|
+
REFERENCES "session_turns"("workspace_id", "id") ON DELETE CASCADE,
|
|
54
|
+
CONSTRAINT "retained_screenshot_artifacts_workspace_attempt_fk"
|
|
55
|
+
FOREIGN KEY ("workspace_id", "attempt_id")
|
|
56
|
+
REFERENCES "session_turn_attempts"("workspace_id", "id") ON DELETE RESTRICT,
|
|
57
|
+
CONSTRAINT "retained_screenshot_artifacts_workspace_file_fk"
|
|
58
|
+
FOREIGN KEY ("workspace_id", "artifact_id")
|
|
59
|
+
REFERENCES "files"("workspace_id", "id") ON DELETE CASCADE,
|
|
60
|
+
CONSTRAINT "retained_screenshot_artifacts_settlement_key_uq"
|
|
61
|
+
UNIQUE ("settlement_key"),
|
|
62
|
+
CONSTRAINT "retained_screenshot_artifacts_status_chk"
|
|
63
|
+
CHECK ("status" IN ('pending', 'reconciling', 'ready', 'cleanup_pending', 'failed', 'expired', 'deleted')),
|
|
64
|
+
CONSTRAINT "retained_screenshot_artifacts_media_type_chk"
|
|
65
|
+
CHECK ("media_type" = 'image/png'),
|
|
66
|
+
CONSTRAINT "retained_screenshot_artifacts_size_chk"
|
|
67
|
+
CHECK ("size_bytes" > 0 AND "size_bytes" <= 33554432),
|
|
68
|
+
CONSTRAINT "retained_screenshot_artifacts_sha256_chk"
|
|
69
|
+
CHECK ("sha256" ~ '^[0-9a-f]{64}$'),
|
|
70
|
+
CONSTRAINT "retained_screenshot_artifacts_dimensions_chk"
|
|
71
|
+
CHECK (
|
|
72
|
+
"width" BETWEEN 1 AND 16384
|
|
73
|
+
AND "height" BETWEEN 1 AND 16384
|
|
74
|
+
AND ("width"::bigint * "height"::bigint) <= 67108864
|
|
75
|
+
),
|
|
76
|
+
CONSTRAINT "retained_screenshot_artifacts_ready_shape_chk"
|
|
77
|
+
CHECK (
|
|
78
|
+
("status" = 'ready' AND "ready_at" IS NOT NULL)
|
|
79
|
+
OR ("status" IN ('pending', 'reconciling') AND "ready_at" IS NULL)
|
|
80
|
+
OR "status" IN ('cleanup_pending', 'failed', 'expired', 'deleted')
|
|
81
|
+
),
|
|
82
|
+
CONSTRAINT "retained_screenshot_artifacts_terminal_cleanup_chk"
|
|
83
|
+
CHECK (
|
|
84
|
+
("status" IN ('cleanup_pending', 'failed', 'expired', 'deleted'))
|
|
85
|
+
OR "cleanup_reason" IS NULL
|
|
86
|
+
)
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
CREATE INDEX "retained_screenshot_artifacts_session_created_idx"
|
|
90
|
+
ON "retained_screenshot_artifacts" ("workspace_id", "session_id", "created_at", "artifact_id");
|
|
91
|
+
CREATE INDEX "retained_screenshot_artifacts_ready_expiry_idx"
|
|
92
|
+
ON "retained_screenshot_artifacts" ("retention_expires_at", "artifact_id")
|
|
93
|
+
WHERE "status" = 'ready';
|
|
94
|
+
CREATE INDEX "retained_screenshot_artifacts_pending_reconcile_idx"
|
|
95
|
+
ON "retained_screenshot_artifacts" ("updated_at", "artifact_id")
|
|
96
|
+
WHERE "status" IN ('pending', 'reconciling', 'cleanup_pending');
|
|
97
|
+
|
|
98
|
+
ALTER TABLE "workspace_screenshot_quotas" ENABLE ROW LEVEL SECURITY;
|
|
99
|
+
ALTER TABLE "workspace_screenshot_quotas" FORCE ROW LEVEL SECURITY;
|
|
100
|
+
CREATE POLICY "workspace_isolation" ON "workspace_screenshot_quotas"
|
|
101
|
+
USING (opengeni_private.workspace_rls_visible(account_id, workspace_id))
|
|
102
|
+
WITH CHECK (opengeni_private.workspace_rls_visible(account_id, workspace_id));
|
|
103
|
+
|
|
104
|
+
ALTER TABLE "retained_screenshot_artifacts" ENABLE ROW LEVEL SECURITY;
|
|
105
|
+
ALTER TABLE "retained_screenshot_artifacts" FORCE ROW LEVEL SECURITY;
|
|
106
|
+
CREATE POLICY "workspace_isolation" ON "retained_screenshot_artifacts"
|
|
107
|
+
USING (opengeni_private.workspace_rls_visible(account_id, workspace_id))
|
|
108
|
+
WITH CHECK (opengeni_private.workspace_rls_visible(account_id, workspace_id));
|
|
109
|
+
|
|
110
|
+
CREATE OR REPLACE FUNCTION opengeni_private.claim_retained_screenshot_maintenance(
|
|
111
|
+
p_pending_grace_ms bigint,
|
|
112
|
+
p_claim_timeout_ms bigint,
|
|
113
|
+
p_limit integer
|
|
114
|
+
)
|
|
115
|
+
RETURNS TABLE (
|
|
116
|
+
action text,
|
|
117
|
+
artifact_id uuid,
|
|
118
|
+
account_id uuid,
|
|
119
|
+
workspace_id uuid,
|
|
120
|
+
session_id uuid,
|
|
121
|
+
object_key text,
|
|
122
|
+
media_type text,
|
|
123
|
+
size_bytes bigint,
|
|
124
|
+
sha256 text,
|
|
125
|
+
width integer,
|
|
126
|
+
height integer,
|
|
127
|
+
retention_expires_at timestamptz,
|
|
128
|
+
cleanup_reason text
|
|
129
|
+
)
|
|
130
|
+
LANGUAGE sql
|
|
131
|
+
SECURITY DEFINER
|
|
132
|
+
AS $$
|
|
133
|
+
WITH candidates AS (
|
|
134
|
+
SELECT A.artifact_id,
|
|
135
|
+
CASE
|
|
136
|
+
WHEN A.status IN ('pending', 'reconciling') THEN 'reconcile'
|
|
137
|
+
ELSE 'delete'
|
|
138
|
+
END AS action,
|
|
139
|
+
CASE
|
|
140
|
+
WHEN A.status = 'ready' THEN 'expired'
|
|
141
|
+
WHEN A.status IN ('pending', 'reconciling') THEN A.cleanup_reason
|
|
142
|
+
ELSE coalesce(A.cleanup_reason, 'orphaned')
|
|
143
|
+
END AS cleanup_reason
|
|
144
|
+
FROM retained_screenshot_artifacts A
|
|
145
|
+
WHERE
|
|
146
|
+
(A.status = 'ready' AND A.retention_expires_at <= clock_timestamp())
|
|
147
|
+
OR (
|
|
148
|
+
A.status = 'pending'
|
|
149
|
+
AND A.updated_at <= clock_timestamp() - (
|
|
150
|
+
greatest(p_pending_grace_ms, 0)::double precision * interval '1 millisecond'
|
|
151
|
+
)
|
|
152
|
+
)
|
|
153
|
+
OR (
|
|
154
|
+
A.status IN ('reconciling', 'cleanup_pending')
|
|
155
|
+
AND A.updated_at <= clock_timestamp() - (
|
|
156
|
+
greatest(p_claim_timeout_ms, 0)::double precision * interval '1 millisecond'
|
|
157
|
+
)
|
|
158
|
+
)
|
|
159
|
+
ORDER BY A.retention_expires_at, A.artifact_id
|
|
160
|
+
LIMIT least(greatest(p_limit, 0), 1000)
|
|
161
|
+
FOR UPDATE SKIP LOCKED
|
|
162
|
+
), claimed AS (
|
|
163
|
+
UPDATE retained_screenshot_artifacts A
|
|
164
|
+
SET
|
|
165
|
+
status = CASE WHEN C.action = 'reconcile' THEN 'reconciling' ELSE 'cleanup_pending' END,
|
|
166
|
+
cleanup_reason = C.cleanup_reason,
|
|
167
|
+
updated_at = clock_timestamp()
|
|
168
|
+
FROM candidates C
|
|
169
|
+
WHERE A.artifact_id = C.artifact_id
|
|
170
|
+
RETURNING A.*, C.action
|
|
171
|
+
)
|
|
172
|
+
SELECT C.action, C.artifact_id, C.account_id, C.workspace_id, C.session_id,
|
|
173
|
+
F.object_key, C.media_type, C.size_bytes, C.sha256, C.width, C.height,
|
|
174
|
+
C.retention_expires_at, C.cleanup_reason
|
|
175
|
+
FROM claimed C
|
|
176
|
+
JOIN files F
|
|
177
|
+
ON F.workspace_id = C.workspace_id
|
|
178
|
+
AND F.id = C.artifact_id;
|
|
179
|
+
$$;
|
|
180
|
+
|
|
181
|
+
REVOKE ALL ON FUNCTION opengeni_private.claim_retained_screenshot_maintenance(bigint, bigint, integer)
|
|
182
|
+
FROM PUBLIC;
|
|
183
|
+
|
|
184
|
+
DO $$
|
|
185
|
+
BEGIN
|
|
186
|
+
IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'opengeni_app') THEN
|
|
187
|
+
GRANT SELECT, INSERT, UPDATE, DELETE ON "workspace_screenshot_quotas" TO opengeni_app;
|
|
188
|
+
GRANT SELECT, INSERT, UPDATE, DELETE ON "retained_screenshot_artifacts" TO opengeni_app;
|
|
189
|
+
GRANT EXECUTE ON FUNCTION opengeni_private.claim_retained_screenshot_maintenance(bigint, bigint, integer)
|
|
190
|
+
TO opengeni_app;
|
|
191
|
+
END IF;
|
|
192
|
+
END $$;
|