@opengeni/db 0.31.1 → 0.36.0
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/browser-auth.d.ts +209 -1
- package/dist/browser-downloads.d.ts +51 -0
- package/dist/browser-identities.d.ts +11 -2
- package/dist/browser-sessions.d.ts +83 -1
- package/dist/browser-state-artifacts.d.ts +75 -1
- package/dist/canonical-human-identities.d.ts +48 -0
- package/dist/canonical-human-identities.js +28 -0
- package/dist/canonical-human-identities.js.map +1 -0
- package/dist/capability-integrations.d.ts +8 -6
- package/dist/{chunk-MWDVXQOL.js → chunk-5KTIEDX7.js} +5773 -4212
- package/dist/chunk-5KTIEDX7.js.map +1 -0
- package/dist/{chunk-NBE6CHRI.js → chunk-CQZ4QSP5.js} +49 -9
- package/dist/chunk-CQZ4QSP5.js.map +1 -0
- package/dist/chunk-F32YT7MP.js +117 -0
- package/dist/chunk-F32YT7MP.js.map +1 -0
- package/dist/{chunk-ISI2TIUG.js → chunk-JC5QPLUM.js} +30 -3
- package/dist/chunk-JC5QPLUM.js.map +1 -0
- package/dist/{chunk-F5FT7BEZ.js → chunk-P3RE4D2B.js} +2 -2
- package/dist/chunk-TAUTWOS5.js +259 -0
- package/dist/chunk-TAUTWOS5.js.map +1 -0
- package/dist/{chunk-YHZXQ7HP.js → chunk-VKLB2ZGI.js} +446 -19
- package/dist/chunk-VKLB2ZGI.js.map +1 -0
- package/dist/database.d.ts +5 -0
- package/dist/editable-artifact-durable-export.js +2 -2
- package/dist/editable-artifacts.js +3 -3
- package/dist/index.d.ts +438 -29
- package/dist/index.js +35640 -28969
- package/dist/index.js.map +1 -1
- package/dist/insights.d.ts +23 -7
- package/dist/integration-bindings.d.ts +24 -24
- package/dist/integration-facets.d.ts +99 -0
- package/dist/interaction-revisions.d.ts +11 -0
- package/dist/interaction-schema.d.ts +698 -34
- package/dist/provision-roles.js +1 -1
- package/dist/runtime-posture.d.ts +8 -7
- package/dist/schema.d.ts +9464 -5412
- package/dist/schema.js +51 -9
- package/dist/session-control.d.ts +29 -3
- package/dist/session-tenancy.d.ts +44 -0
- package/dist/session-tenancy.js +16 -0
- package/dist/session-tenancy.js.map +1 -0
- package/dist/slack-task-policy-schema.d.ts +847 -0
- package/dist/slack-task-policy.d.ts +65 -0
- package/dist/task-notes-schema.d.ts +924 -0
- package/dist/task-notes.d.ts +85 -0
- package/dist/video-generation.d.ts +9 -0
- package/dist/video-generation.js +5 -3
- package/dist/xai-subscription.d.ts +326 -0
- package/drizzle/0212_browser_state_transfer_hardening.sql +560 -0
- package/drizzle/0213_browser_interaction_authority.sql +67 -0
- package/drizzle/0214_browser_download_saves.sql +34 -0
- package/drizzle/0215_browser_controller_host.sql +385 -0
- package/drizzle/0216_browser_auth_health_evidence.sql +137 -0
- package/drizzle/0217_external_browser_auth_operations.sql +22 -0
- package/drizzle/0218_organization_tenancy_foundation.sql +299 -0
- package/drizzle/0219_organization_tenancy_managed_human_provisioning.sql +317 -0
- package/drizzle/0219_site_auth_maintenance_sessions.sql +234 -0
- package/drizzle/0220_memory_slack_append_only_cascade.sql +38 -0
- package/drizzle/0220_session_channels.sql +54 -0
- package/drizzle/0221_sessions_channel_index.sql +5 -0
- package/drizzle/0222_session_visibility_authority_epochs.sql +158 -0
- package/drizzle/0222_sessions_channel_fk.sql +89 -0
- package/drizzle/0223_pending_tool_event_output.sql +55 -0
- package/drizzle/0223_sessions_channel_fk_validate.sql +118 -0
- package/drizzle/0224_slack_post_outcome_reconciliation.sql +123 -0
- package/drizzle/0225_session_visibility_fork_activation.sql +1363 -0
- package/drizzle/0226_personal_codex_authority_foundation.sql +365 -0
- package/drizzle/0227_slack_native_actions.sql +145 -0
- package/drizzle/0228_interaction_controller_data_plane.sql +14 -0
- package/drizzle/0228_slack_task_policy.sql +400 -0
- package/drizzle/0229_slack_inbox_file_fact.sql +10 -0
- package/drizzle/0230_user_scoped_variable_sets_rigs.sql +84 -0
- package/drizzle/0231_integration_definition_identity_cutover.sql +271 -0
- package/drizzle/0232_integration_facet_authority_cutover.sql +610 -0
- package/drizzle/0233_skill_and_integration_authority_cutover.sql +1162 -0
- package/drizzle/0234_xai_subscription_authority.sql +1139 -0
- package/drizzle/0235_canonical_human_login_bindings.sql +920 -0
- package/drizzle/0236_browser_identity_lifecycle.sql +39 -0
- package/drizzle/0236_session_visibility_slack_policy.sql +72 -0
- package/drizzle/0237_interaction_transition_reaper.sql +263 -0
- package/drizzle/0238_goal_persistence_policy.sql +471 -0
- package/drizzle/0238_supergrok_realtime_model.sql +29 -0
- package/drizzle/0239_supergrok_video_funding.sql +50 -0
- package/drizzle/0239_task_tree_notes.sql +990 -0
- package/package.json +13 -5
- package/src/browser-auth.ts +2600 -123
- package/src/browser-downloads.ts +517 -0
- package/src/browser-identities.ts +220 -3
- package/src/browser-sessions.ts +334 -45
- package/src/browser-state-artifacts.ts +402 -12
- package/src/canonical-human-identities.ts +317 -0
- package/src/capability-integrations.ts +121 -213
- package/src/database.ts +41 -1
- package/src/index.ts +4157 -394
- package/src/insights.ts +109 -28
- package/src/integration-bindings.ts +101 -101
- package/src/{integration-features.ts → integration-facets.ts} +187 -184
- package/src/interaction-revisions.ts +33 -3
- package/src/interaction-schema.ts +311 -8
- package/src/pack-components.ts +43 -43
- package/src/pack-installations.ts +0 -43
- package/src/plugin-packages.ts +9 -9
- package/src/provision-roles.ts +232 -0
- package/src/runtime-posture.ts +227 -14
- package/src/schema.ts +1188 -75
- package/src/session-control.ts +122 -37
- package/src/session-queue-commands.ts +78 -1
- package/src/session-tenancy.ts +188 -0
- package/src/session-tool-call-settlement.ts +9 -3
- package/src/slack-task-policy-schema.ts +130 -0
- package/src/slack-task-policy.ts +341 -0
- package/src/task-notes-schema.ts +135 -0
- package/src/task-notes.ts +178 -0
- package/src/video-generation.ts +90 -12
- package/src/workspace-artifacts.ts +1 -1
- package/src/xai-subscription.ts +1887 -0
- package/dist/chunk-ISI2TIUG.js.map +0 -1
- package/dist/chunk-MWDVXQOL.js.map +0 -1
- package/dist/chunk-NBE6CHRI.js.map +0 -1
- package/dist/chunk-YHZXQ7HP.js.map +0 -1
- package/dist/integration-features.d.ts +0 -99
- /package/dist/{chunk-F5FT7BEZ.js.map → chunk-P3RE4D2B.js.map} +0 -0
|
@@ -1,10 +1,14 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
1
2
|
import {
|
|
2
3
|
BrowserRevisionMaterialization,
|
|
3
4
|
type BrowserRevisionMaterialization as BrowserRevisionMaterializationValue,
|
|
4
5
|
} from "@opengeni/contracts";
|
|
5
|
-
import
|
|
6
|
+
import { and, eq, sql } from "drizzle-orm";
|
|
7
|
+
import { type Database, rawRows, withRlsContext } from "./database";
|
|
8
|
+
import * as schema from "./schema";
|
|
6
9
|
|
|
7
10
|
type BrowserStateArtifactRow = typeof schema.browserStateArtifacts.$inferSelect;
|
|
11
|
+
type BrowserStateUploadRow = typeof schema.browserStateUploads.$inferSelect;
|
|
8
12
|
|
|
9
13
|
const SHA256_PATTERN = /^[0-9a-f]{64}$/u;
|
|
10
14
|
const OBJECT_KEY_SEGMENT_PATTERN = /^[A-Za-z0-9._=-]+$/u;
|
|
@@ -21,26 +25,48 @@ export type BrowserStateArtifactCommitInput = {
|
|
|
21
25
|
materialization: BrowserRevisionMaterializationValue;
|
|
22
26
|
};
|
|
23
27
|
|
|
28
|
+
export type BrowserStateArtifactCleanupClaim = {
|
|
29
|
+
claimId: string;
|
|
30
|
+
artifactId: string;
|
|
31
|
+
accountId: string;
|
|
32
|
+
workspaceId: string;
|
|
33
|
+
objectKey: string;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export type BrowserStateUploadPurpose = BrowserStateUploadRow["purpose"];
|
|
37
|
+
|
|
38
|
+
export type BrowserStateUploadCleanupClaim = {
|
|
39
|
+
claimId: string;
|
|
40
|
+
uploadId: string;
|
|
41
|
+
accountId: string;
|
|
42
|
+
workspaceId: string;
|
|
43
|
+
objectKey: string;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export class BrowserStateUploadStateError extends Error {
|
|
47
|
+
readonly name = "BrowserStateUploadStateError";
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export type BrowserStateUploadPreparationInput = {
|
|
51
|
+
accountId: string;
|
|
52
|
+
workspaceId: string;
|
|
53
|
+
operationId: string;
|
|
54
|
+
sourceBrowserSessionId: string;
|
|
55
|
+
purpose: BrowserStateUploadPurpose;
|
|
56
|
+
objectKey: string;
|
|
57
|
+
cleanupAfter: Date;
|
|
58
|
+
};
|
|
59
|
+
|
|
24
60
|
/** Validate storage authority before it can become durable state. */
|
|
25
61
|
export function validateBrowserStateArtifactCommitInput(
|
|
26
62
|
workspaceId: string,
|
|
27
63
|
value: BrowserStateArtifactCommitInput,
|
|
28
64
|
): BrowserStateArtifactCommitInput {
|
|
29
|
-
const prefix = `workspaces/${workspaceId}/browser-state/`;
|
|
30
|
-
const objectKeySuffix = value.objectKey.slice(prefix.length);
|
|
31
65
|
if (
|
|
32
66
|
!SHA256_PATTERN.test(value.artifactDigest) ||
|
|
33
67
|
!SHA256_PATTERN.test(value.contentDigest) ||
|
|
34
68
|
!SHA256_PATTERN.test(value.manifestDigest) ||
|
|
35
|
-
!value.objectKey
|
|
36
|
-
objectKeySuffix.length < 1 ||
|
|
37
|
-
objectKeySuffix
|
|
38
|
-
.split("/")
|
|
39
|
-
.some(
|
|
40
|
-
(segment) =>
|
|
41
|
-
!OBJECT_KEY_SEGMENT_PATTERN.test(segment) || segment === "." || segment === "..",
|
|
42
|
-
) ||
|
|
43
|
-
Buffer.byteLength(value.objectKey) > 2_048 ||
|
|
69
|
+
!validBrowserStateObjectKey(workspaceId, value.objectKey) ||
|
|
44
70
|
value.format.trim() !== value.format ||
|
|
45
71
|
Buffer.byteLength(value.format) < 1 ||
|
|
46
72
|
Buffer.byteLength(value.format) > 512 ||
|
|
@@ -57,3 +83,367 @@ export function validateBrowserStateArtifactCommitInput(
|
|
|
57
83
|
}
|
|
58
84
|
return { ...value, materialization };
|
|
59
85
|
}
|
|
86
|
+
|
|
87
|
+
/** Establish durable cleanup authority before a controller receives a PUT URL. */
|
|
88
|
+
export async function prepareBrowserStateUpload(
|
|
89
|
+
db: Database,
|
|
90
|
+
input: BrowserStateUploadPreparationInput,
|
|
91
|
+
): Promise<{ uploadId: string; state: BrowserStateUploadRow["state"] }> {
|
|
92
|
+
return await withRlsContext(db, input, async (scopedDb) =>
|
|
93
|
+
scopedDb.transaction(async (tx) =>
|
|
94
|
+
prepareBrowserStateUploadInTransaction(tx as unknown as Database, input),
|
|
95
|
+
),
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Compose upload authority with operation dispatch in one transaction. */
|
|
100
|
+
export async function prepareBrowserStateUploadInTransaction(
|
|
101
|
+
db: Database,
|
|
102
|
+
input: BrowserStateUploadPreparationInput,
|
|
103
|
+
): Promise<{ uploadId: string; state: BrowserStateUploadRow["state"] }> {
|
|
104
|
+
if (
|
|
105
|
+
!validBrowserStateObjectKey(input.workspaceId, input.objectKey) ||
|
|
106
|
+
!Number.isFinite(input.cleanupAfter.getTime()) ||
|
|
107
|
+
input.cleanupAfter.getTime() <= Date.now()
|
|
108
|
+
) {
|
|
109
|
+
throw new BrowserStateUploadStateError("Browser state upload authority is invalid");
|
|
110
|
+
}
|
|
111
|
+
const [operation] = await db
|
|
112
|
+
.select({
|
|
113
|
+
resourceKind: schema.interactionOperations.resourceKind,
|
|
114
|
+
resourceId: schema.interactionOperations.resourceId,
|
|
115
|
+
kind: schema.interactionOperations.kind,
|
|
116
|
+
state: schema.interactionOperations.state,
|
|
117
|
+
})
|
|
118
|
+
.from(schema.interactionOperations)
|
|
119
|
+
.where(
|
|
120
|
+
and(
|
|
121
|
+
eq(schema.interactionOperations.workspaceId, input.workspaceId),
|
|
122
|
+
eq(schema.interactionOperations.operationId, input.operationId),
|
|
123
|
+
),
|
|
124
|
+
)
|
|
125
|
+
.for("update")
|
|
126
|
+
.limit(1);
|
|
127
|
+
const expectedKind = input.purpose === "revision_component" ? "publish" : "suspend";
|
|
128
|
+
if (
|
|
129
|
+
!operation ||
|
|
130
|
+
operation.resourceKind !== "browser_session" ||
|
|
131
|
+
operation.resourceId !== input.sourceBrowserSessionId ||
|
|
132
|
+
operation.kind !== expectedKind ||
|
|
133
|
+
(operation.state !== "prepared" && operation.state !== "dispatched")
|
|
134
|
+
) {
|
|
135
|
+
throw new BrowserStateUploadStateError(
|
|
136
|
+
"Browser state upload does not belong to an active capture operation",
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const [existing] = await db
|
|
141
|
+
.select()
|
|
142
|
+
.from(schema.browserStateUploads)
|
|
143
|
+
.where(eq(schema.browserStateUploads.objectKey, input.objectKey))
|
|
144
|
+
.limit(1);
|
|
145
|
+
if (existing) {
|
|
146
|
+
if (
|
|
147
|
+
existing.accountId !== input.accountId ||
|
|
148
|
+
existing.workspaceId !== input.workspaceId ||
|
|
149
|
+
existing.operationId !== input.operationId ||
|
|
150
|
+
existing.sourceBrowserSessionId !== input.sourceBrowserSessionId ||
|
|
151
|
+
existing.purpose !== input.purpose ||
|
|
152
|
+
existing.state !== "prepared"
|
|
153
|
+
) {
|
|
154
|
+
throw new BrowserStateUploadStateError(
|
|
155
|
+
"Browser state object key belongs to another upload authority",
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
if (
|
|
159
|
+
existing.state === "prepared" &&
|
|
160
|
+
existing.cleanupAfter !== null &&
|
|
161
|
+
existing.cleanupAfter < input.cleanupAfter
|
|
162
|
+
) {
|
|
163
|
+
const [renewed] = await db
|
|
164
|
+
.update(schema.browserStateUploads)
|
|
165
|
+
.set({ cleanupAfter: input.cleanupAfter, updatedAt: new Date() })
|
|
166
|
+
.where(
|
|
167
|
+
and(
|
|
168
|
+
eq(schema.browserStateUploads.workspaceId, input.workspaceId),
|
|
169
|
+
eq(schema.browserStateUploads.id, existing.id),
|
|
170
|
+
eq(schema.browserStateUploads.state, "prepared"),
|
|
171
|
+
),
|
|
172
|
+
)
|
|
173
|
+
.returning({ id: schema.browserStateUploads.id });
|
|
174
|
+
if (!renewed) throw new Error("Browser state upload authority renewal was lost");
|
|
175
|
+
}
|
|
176
|
+
return { uploadId: existing.id, state: existing.state };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const [created] = await db
|
|
180
|
+
.insert(schema.browserStateUploads)
|
|
181
|
+
.values({
|
|
182
|
+
id: randomUUID(),
|
|
183
|
+
accountId: input.accountId,
|
|
184
|
+
workspaceId: input.workspaceId,
|
|
185
|
+
operationId: input.operationId,
|
|
186
|
+
sourceBrowserSessionId: input.sourceBrowserSessionId,
|
|
187
|
+
purpose: input.purpose,
|
|
188
|
+
objectKey: input.objectKey,
|
|
189
|
+
cleanupAfter: input.cleanupAfter,
|
|
190
|
+
})
|
|
191
|
+
.returning({ id: schema.browserStateUploads.id, state: schema.browserStateUploads.state });
|
|
192
|
+
if (!created) throw new Error("Browser state upload authority insert was lost");
|
|
193
|
+
return { uploadId: created.id, state: created.state };
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** Queue every uncommitted object owned by a terminal capture operation. */
|
|
197
|
+
export async function markBrowserStateUploadsDeletePendingInTransaction(
|
|
198
|
+
db: Database,
|
|
199
|
+
input: { workspaceId: string; operationId: string },
|
|
200
|
+
): Promise<void> {
|
|
201
|
+
const now = new Date();
|
|
202
|
+
await db
|
|
203
|
+
.update(schema.browserStateUploads)
|
|
204
|
+
.set({ state: "delete_pending", cleanupAfter: now, updatedAt: now })
|
|
205
|
+
.where(
|
|
206
|
+
and(
|
|
207
|
+
eq(schema.browserStateUploads.workspaceId, input.workspaceId),
|
|
208
|
+
eq(schema.browserStateUploads.operationId, input.operationId),
|
|
209
|
+
eq(schema.browserStateUploads.state, "prepared"),
|
|
210
|
+
),
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** Atomically turn one pre-upload authority into the exact artifact root. */
|
|
215
|
+
export async function commitBrowserStateUploadInTransaction(
|
|
216
|
+
db: Database,
|
|
217
|
+
input: {
|
|
218
|
+
workspaceId: string;
|
|
219
|
+
operationId: string;
|
|
220
|
+
sourceBrowserSessionId: string;
|
|
221
|
+
purpose: BrowserStateUploadPurpose;
|
|
222
|
+
objectKey: string;
|
|
223
|
+
artifactId: string;
|
|
224
|
+
},
|
|
225
|
+
): Promise<void> {
|
|
226
|
+
const [upload] = await db
|
|
227
|
+
.select({ id: schema.browserStateUploads.id, state: schema.browserStateUploads.state })
|
|
228
|
+
.from(schema.browserStateUploads)
|
|
229
|
+
.where(
|
|
230
|
+
and(
|
|
231
|
+
eq(schema.browserStateUploads.workspaceId, input.workspaceId),
|
|
232
|
+
eq(schema.browserStateUploads.operationId, input.operationId),
|
|
233
|
+
eq(schema.browserStateUploads.sourceBrowserSessionId, input.sourceBrowserSessionId),
|
|
234
|
+
eq(schema.browserStateUploads.purpose, input.purpose),
|
|
235
|
+
eq(schema.browserStateUploads.objectKey, input.objectKey),
|
|
236
|
+
),
|
|
237
|
+
)
|
|
238
|
+
.for("update")
|
|
239
|
+
.limit(1);
|
|
240
|
+
if (!upload || upload.state !== "prepared") {
|
|
241
|
+
throw new BrowserStateUploadStateError(
|
|
242
|
+
"Browser state upload is not available for durable publication",
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
const [committed] = await db
|
|
246
|
+
.update(schema.browserStateUploads)
|
|
247
|
+
.set({
|
|
248
|
+
state: "committed",
|
|
249
|
+
cleanupAfter: null,
|
|
250
|
+
committedArtifactId: input.artifactId,
|
|
251
|
+
updatedAt: new Date(),
|
|
252
|
+
})
|
|
253
|
+
.where(
|
|
254
|
+
and(
|
|
255
|
+
eq(schema.browserStateUploads.workspaceId, input.workspaceId),
|
|
256
|
+
eq(schema.browserStateUploads.id, upload.id),
|
|
257
|
+
eq(schema.browserStateUploads.state, "prepared"),
|
|
258
|
+
),
|
|
259
|
+
)
|
|
260
|
+
.returning({ id: schema.browserStateUploads.id });
|
|
261
|
+
if (!committed) throw new Error("Browser state upload publication was lost");
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Claim a bounded cross-workspace batch of retired private checkpoints. The
|
|
266
|
+
* privileged SQL function is the only FORCE-RLS bypass; stale claims are
|
|
267
|
+
* reclaimable and published revision components are structurally ineligible.
|
|
268
|
+
*/
|
|
269
|
+
export async function claimBrowserStateArtifactCleanup(
|
|
270
|
+
db: Database,
|
|
271
|
+
input: { claimTimeoutMs: number; limit: number },
|
|
272
|
+
): Promise<BrowserStateArtifactCleanupClaim[]> {
|
|
273
|
+
const rows = await rawRows<{
|
|
274
|
+
claim_id: string;
|
|
275
|
+
artifact_id: string;
|
|
276
|
+
account_id: string;
|
|
277
|
+
workspace_id: string;
|
|
278
|
+
object_key: string;
|
|
279
|
+
}>(
|
|
280
|
+
db,
|
|
281
|
+
sql`
|
|
282
|
+
select claim_id, artifact_id, account_id, workspace_id, object_key
|
|
283
|
+
from opengeni_private.claim_browser_state_artifact_cleanup(
|
|
284
|
+
${input.claimTimeoutMs},
|
|
285
|
+
${input.limit}
|
|
286
|
+
)
|
|
287
|
+
`,
|
|
288
|
+
);
|
|
289
|
+
return rows.map((row) => ({
|
|
290
|
+
claimId: row.claim_id,
|
|
291
|
+
artifactId: row.artifact_id,
|
|
292
|
+
accountId: row.account_id,
|
|
293
|
+
workspaceId: row.workspace_id,
|
|
294
|
+
objectKey: row.object_key,
|
|
295
|
+
}));
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Settle one exact claim only after its provider object was deleted. Completion
|
|
300
|
+
* is idempotent after a lost response and erases the wrapped data key.
|
|
301
|
+
*/
|
|
302
|
+
export async function completeBrowserStateArtifactCleanup(
|
|
303
|
+
db: Database,
|
|
304
|
+
input: BrowserStateArtifactCleanupClaim,
|
|
305
|
+
): Promise<boolean> {
|
|
306
|
+
return await withRlsContext(
|
|
307
|
+
db,
|
|
308
|
+
{ accountId: input.accountId, workspaceId: input.workspaceId },
|
|
309
|
+
async (scopedDb) =>
|
|
310
|
+
await scopedDb.transaction(async (tx) => {
|
|
311
|
+
const [artifact] = await tx
|
|
312
|
+
.select({
|
|
313
|
+
state: schema.browserStateArtifacts.state,
|
|
314
|
+
deleteClaimId: schema.browserStateArtifacts.deleteClaimId,
|
|
315
|
+
})
|
|
316
|
+
.from(schema.browserStateArtifacts)
|
|
317
|
+
.where(
|
|
318
|
+
and(
|
|
319
|
+
eq(schema.browserStateArtifacts.workspaceId, input.workspaceId),
|
|
320
|
+
eq(schema.browserStateArtifacts.id, input.artifactId),
|
|
321
|
+
eq(schema.browserStateArtifacts.purpose, "private_checkpoint"),
|
|
322
|
+
eq(schema.browserStateArtifacts.objectKey, input.objectKey),
|
|
323
|
+
),
|
|
324
|
+
)
|
|
325
|
+
.for("update")
|
|
326
|
+
.limit(1);
|
|
327
|
+
if (!artifact) return false;
|
|
328
|
+
if (artifact.state === "deleted") return true;
|
|
329
|
+
if (artifact.state !== "deleting" || artifact.deleteClaimId !== input.claimId) {
|
|
330
|
+
return false;
|
|
331
|
+
}
|
|
332
|
+
const [settled] = await tx
|
|
333
|
+
.update(schema.browserStateArtifacts)
|
|
334
|
+
.set({
|
|
335
|
+
state: "deleted",
|
|
336
|
+
encryptedDataKey: null,
|
|
337
|
+
deleteClaimId: null,
|
|
338
|
+
deleteClaimedAt: null,
|
|
339
|
+
deletedAt: new Date(),
|
|
340
|
+
})
|
|
341
|
+
.where(
|
|
342
|
+
and(
|
|
343
|
+
eq(schema.browserStateArtifacts.workspaceId, input.workspaceId),
|
|
344
|
+
eq(schema.browserStateArtifacts.id, input.artifactId),
|
|
345
|
+
eq(schema.browserStateArtifacts.state, "deleting"),
|
|
346
|
+
eq(schema.browserStateArtifacts.deleteClaimId, input.claimId),
|
|
347
|
+
),
|
|
348
|
+
)
|
|
349
|
+
.returning({ id: schema.browserStateArtifacts.id });
|
|
350
|
+
return settled !== undefined;
|
|
351
|
+
}),
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/** Claim orphan/failed upload objects after their safety horizon. */
|
|
356
|
+
export async function claimBrowserStateUploadCleanup(
|
|
357
|
+
db: Database,
|
|
358
|
+
input: { claimTimeoutMs: number; limit: number },
|
|
359
|
+
): Promise<BrowserStateUploadCleanupClaim[]> {
|
|
360
|
+
const rows = await rawRows<{
|
|
361
|
+
claim_id: string;
|
|
362
|
+
upload_id: string;
|
|
363
|
+
account_id: string;
|
|
364
|
+
workspace_id: string;
|
|
365
|
+
object_key: string;
|
|
366
|
+
}>(
|
|
367
|
+
db,
|
|
368
|
+
sql`
|
|
369
|
+
select claim_id, upload_id, account_id, workspace_id, object_key
|
|
370
|
+
from opengeni_private.claim_browser_state_upload_cleanup(
|
|
371
|
+
${input.claimTimeoutMs},
|
|
372
|
+
${input.limit}
|
|
373
|
+
)
|
|
374
|
+
`,
|
|
375
|
+
);
|
|
376
|
+
return rows.map((row) => ({
|
|
377
|
+
claimId: row.claim_id,
|
|
378
|
+
uploadId: row.upload_id,
|
|
379
|
+
accountId: row.account_id,
|
|
380
|
+
workspaceId: row.workspace_id,
|
|
381
|
+
objectKey: row.object_key,
|
|
382
|
+
}));
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/** Tombstone one exact orphan upload after its provider object is absent. */
|
|
386
|
+
export async function completeBrowserStateUploadCleanup(
|
|
387
|
+
db: Database,
|
|
388
|
+
input: BrowserStateUploadCleanupClaim,
|
|
389
|
+
): Promise<boolean> {
|
|
390
|
+
return await withRlsContext(
|
|
391
|
+
db,
|
|
392
|
+
{ accountId: input.accountId, workspaceId: input.workspaceId },
|
|
393
|
+
async (scopedDb) =>
|
|
394
|
+
scopedDb.transaction(async (tx) => {
|
|
395
|
+
const [upload] = await tx
|
|
396
|
+
.select({
|
|
397
|
+
state: schema.browserStateUploads.state,
|
|
398
|
+
deleteClaimId: schema.browserStateUploads.deleteClaimId,
|
|
399
|
+
})
|
|
400
|
+
.from(schema.browserStateUploads)
|
|
401
|
+
.where(
|
|
402
|
+
and(
|
|
403
|
+
eq(schema.browserStateUploads.workspaceId, input.workspaceId),
|
|
404
|
+
eq(schema.browserStateUploads.id, input.uploadId),
|
|
405
|
+
eq(schema.browserStateUploads.objectKey, input.objectKey),
|
|
406
|
+
),
|
|
407
|
+
)
|
|
408
|
+
.for("update")
|
|
409
|
+
.limit(1);
|
|
410
|
+
if (!upload) return false;
|
|
411
|
+
if (upload.state === "deleted") return true;
|
|
412
|
+
if (upload.state !== "deleting" || upload.deleteClaimId !== input.claimId) return false;
|
|
413
|
+
const [settled] = await tx
|
|
414
|
+
.update(schema.browserStateUploads)
|
|
415
|
+
.set({
|
|
416
|
+
state: "deleted",
|
|
417
|
+
deleteClaimId: null,
|
|
418
|
+
deleteClaimedAt: null,
|
|
419
|
+
deletedAt: new Date(),
|
|
420
|
+
updatedAt: new Date(),
|
|
421
|
+
})
|
|
422
|
+
.where(
|
|
423
|
+
and(
|
|
424
|
+
eq(schema.browserStateUploads.workspaceId, input.workspaceId),
|
|
425
|
+
eq(schema.browserStateUploads.id, input.uploadId),
|
|
426
|
+
eq(schema.browserStateUploads.state, "deleting"),
|
|
427
|
+
eq(schema.browserStateUploads.deleteClaimId, input.claimId),
|
|
428
|
+
),
|
|
429
|
+
)
|
|
430
|
+
.returning({ id: schema.browserStateUploads.id });
|
|
431
|
+
return settled !== undefined;
|
|
432
|
+
}),
|
|
433
|
+
);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function validBrowserStateObjectKey(workspaceId: string, objectKey: string): boolean {
|
|
437
|
+
const prefix = `workspaces/${workspaceId}/browser-state/`;
|
|
438
|
+
if (!objectKey.startsWith(prefix) || Buffer.byteLength(objectKey) > 2_048) return false;
|
|
439
|
+
const suffix = objectKey.slice(prefix.length);
|
|
440
|
+
return (
|
|
441
|
+
suffix.length > 0 &&
|
|
442
|
+
suffix
|
|
443
|
+
.split("/")
|
|
444
|
+
.every(
|
|
445
|
+
(segment) =>
|
|
446
|
+
OBJECT_KEY_SEGMENT_PATTERN.test(segment) && segment !== "." && segment !== "..",
|
|
447
|
+
)
|
|
448
|
+
);
|
|
449
|
+
}
|