@opengeni/db 0.9.3 → 0.10.7
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-4LG5NBTC.js → chunk-P6PKXY5W.js} +93 -1
- package/dist/chunk-P6PKXY5W.js.map +1 -0
- package/dist/index.d.ts +3 -2
- package/dist/index.js +1332 -178
- package/dist/index.js.map +1 -1
- package/dist/provision-roles.d.ts +406 -32
- package/dist/{schema-CdPGTHlD.d.ts → schema-CqkzrBRS.d.ts} +513 -2
- package/dist/schema.d.ts +1 -1
- package/dist/schema.js +3 -1
- package/drizzle/0053_codex_credential_leases.sql +2 -2
- package/drizzle/0057_durable_queue_control.sql +1 -1
- package/drizzle/0061_session_workflow_wake_outbox.sql +1 -1
- package/drizzle/0062_session_list_snapshot_reaper.sql +1 -1
- package/drizzle/0063_session_control_mega_foundation.sql +1 -1
- package/drizzle/0064_rotation_strategy_sharded_backfill.sql +1 -1
- package/drizzle/0065_codex_subscription_overview.sql +168 -0
- package/drizzle/0065_session_tool_policy.sql +38 -0
- package/drizzle/0067_session_event_payload_bounds.sql +2 -2
- package/drizzle/0068_workspace_control_event_bounds.sql +2 -2
- package/drizzle/0069_session_event_history_backfill.sql +2 -2
- package/drizzle/0074_session_activity_revisions.sql +2 -2
- package/drizzle/0106_session_attempt_mcp_approval_policies.sql +29 -0
- package/drizzle/0107_host_export_lineage_contract.sql +381 -0
- package/drizzle/0108_fence_invalidated_warming_epochs.sql +76 -0
- package/package.json +5 -4
- package/src/codex-token-resolver.ts +175 -14
- package/src/connection-token-resolver.ts +143 -120
- package/src/event-payload-sanitizer.ts +32 -2
- package/src/index.ts +1888 -205
- package/src/schema.ts +107 -1
- package/src/session-control.ts +2 -0
- package/src/session-queue-commands.ts +94 -21
- package/dist/chunk-4LG5NBTC.js.map +0 -1
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
-- deployment-mode: rolling
|
|
2
|
+
-- Fence provider creates that outlive a failed or expired warming acquisition.
|
|
3
|
+
--
|
|
4
|
+
-- A warming row can be invalidated while a provider create is still
|
|
5
|
+
-- non-abortably in flight. Advancing lease_epoch in the same transaction that
|
|
6
|
+
-- exposes cold/draining means a successor can never reuse the old acquisition
|
|
7
|
+
-- epoch, so late record/commit/fail/cleanup callbacks fail their existing CAS.
|
|
8
|
+
|
|
9
|
+
CREATE OR REPLACE FUNCTION opengeni_private.reap_sandbox_leases(
|
|
10
|
+
p_viewer_holder_ttl_ms bigint,
|
|
11
|
+
p_turn_holder_ttl_ms bigint,
|
|
12
|
+
p_idle_grace_ms bigint
|
|
13
|
+
)
|
|
14
|
+
RETURNS TABLE (workspace_id uuid, sandbox_group_id uuid, instance_id text, lease_epoch integer)
|
|
15
|
+
LANGUAGE plpgsql
|
|
16
|
+
SECURITY DEFINER
|
|
17
|
+
AS $$
|
|
18
|
+
BEGIN
|
|
19
|
+
DELETE FROM sandbox_lease_holders h
|
|
20
|
+
WHERE h.kind = 'viewer'
|
|
21
|
+
AND h.last_heartbeat_at < now() - make_interval(secs => p_viewer_holder_ttl_ms / 1000.0);
|
|
22
|
+
|
|
23
|
+
IF p_turn_holder_ttl_ms > 0 THEN
|
|
24
|
+
DELETE FROM sandbox_lease_holders h
|
|
25
|
+
WHERE h.kind = 'turn'
|
|
26
|
+
AND h.last_heartbeat_at < now() - make_interval(secs => p_turn_holder_ttl_ms / 1000.0);
|
|
27
|
+
END IF;
|
|
28
|
+
|
|
29
|
+
UPDATE sandbox_leases L SET
|
|
30
|
+
refcount = c.total,
|
|
31
|
+
turn_holders = c.turns,
|
|
32
|
+
viewer_holders = c.viewers,
|
|
33
|
+
liveness = CASE WHEN L.liveness = 'warm' AND c.total = 0 AND c.turns = 0
|
|
34
|
+
THEN 'draining' ELSE L.liveness END,
|
|
35
|
+
expires_at = CASE WHEN L.liveness = 'warm' AND c.total = 0 AND c.turns = 0
|
|
36
|
+
THEN now() + make_interval(secs => p_idle_grace_ms / 1000.0)
|
|
37
|
+
ELSE L.expires_at END,
|
|
38
|
+
updated_at = now()
|
|
39
|
+
FROM (
|
|
40
|
+
SELECT L2.id,
|
|
41
|
+
(SELECT count(*) FROM sandbox_lease_holders h WHERE h.lease_id = L2.id)::int AS total,
|
|
42
|
+
(SELECT count(*) FROM sandbox_lease_holders h WHERE h.lease_id = L2.id AND h.kind = 'turn')::int AS turns,
|
|
43
|
+
(SELECT count(*) FROM sandbox_lease_holders h WHERE h.lease_id = L2.id AND h.kind = 'viewer')::int AS viewers
|
|
44
|
+
FROM sandbox_leases L2
|
|
45
|
+
) c
|
|
46
|
+
WHERE L.id = c.id;
|
|
47
|
+
|
|
48
|
+
-- The old warming epoch is permanently closed before a successor can acquire.
|
|
49
|
+
UPDATE sandbox_leases AS L SET
|
|
50
|
+
liveness = 'cold', instance_id = NULL,
|
|
51
|
+
resume_backend_id = NULL, resume_state = NULL,
|
|
52
|
+
data_plane_url = NULL, terminal_data_plane_url = NULL,
|
|
53
|
+
lease_epoch = L.lease_epoch + 1,
|
|
54
|
+
updated_at = now()
|
|
55
|
+
WHERE L.liveness = 'warming' AND L.expires_at < now() AND L.instance_id IS NULL;
|
|
56
|
+
|
|
57
|
+
-- Keep an attributed provider id for the drain/terminate path, but fence the
|
|
58
|
+
-- expired creator before exposing the row as drainable.
|
|
59
|
+
UPDATE sandbox_leases AS L SET
|
|
60
|
+
liveness = 'draining',
|
|
61
|
+
refcount = 0,
|
|
62
|
+
turn_holders = 0,
|
|
63
|
+
viewer_holders = 0,
|
|
64
|
+
data_plane_url = NULL,
|
|
65
|
+
terminal_data_plane_url = NULL,
|
|
66
|
+
lease_epoch = L.lease_epoch + 1,
|
|
67
|
+
expires_at = now() - interval '1 millisecond',
|
|
68
|
+
updated_at = now()
|
|
69
|
+
WHERE L.liveness = 'warming' AND L.expires_at < now() AND L.instance_id IS NOT NULL;
|
|
70
|
+
|
|
71
|
+
RETURN QUERY
|
|
72
|
+
SELECT L.workspace_id, L.sandbox_group_id, L.instance_id, L.lease_epoch
|
|
73
|
+
FROM sandbox_leases L
|
|
74
|
+
WHERE L.liveness = 'draining' AND L.expires_at < now() AND L.refcount = 0;
|
|
75
|
+
END;
|
|
76
|
+
$$;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opengeni/db",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.7",
|
|
4
4
|
"description": "OpenGeni persistence: Drizzle schema, RLS-scoped query layer, the SQL migration runner, and role provisioning.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -49,9 +49,10 @@
|
|
|
49
49
|
"prepublishOnly": "bash ../../scripts/prepublish-guard"
|
|
50
50
|
},
|
|
51
51
|
"dependencies": {
|
|
52
|
-
"@opengeni/codex": "^0.2.
|
|
53
|
-
"@opengeni/config": "^0.6.
|
|
54
|
-
"@opengeni/contracts": "^0.
|
|
52
|
+
"@opengeni/codex": "^0.2.7",
|
|
53
|
+
"@opengeni/config": "^0.6.9",
|
|
54
|
+
"@opengeni/contracts": "^0.18.0",
|
|
55
|
+
"@opengeni/network": "^0.1.1",
|
|
55
56
|
"drizzle-orm": "^0.45.2",
|
|
56
57
|
"postgres": "^3.4.7"
|
|
57
58
|
},
|
|
@@ -26,6 +26,10 @@ import {
|
|
|
26
26
|
CodexReloginRequired,
|
|
27
27
|
type CodexTokenSnapshot,
|
|
28
28
|
type CodexUsagePayload,
|
|
29
|
+
type CodexFetch,
|
|
30
|
+
type CodexRateLimitResetCreditsDetails,
|
|
31
|
+
type ResetCreditFetchFailureReason,
|
|
32
|
+
fetchCodexRateLimitResetCredits,
|
|
29
33
|
fetchCodexUsage,
|
|
30
34
|
normalizeCodexUsage,
|
|
31
35
|
refreshCodexToken,
|
|
@@ -49,6 +53,92 @@ import {
|
|
|
49
53
|
// connected credential. Concurrent calls for the SAME credential still coalesce,
|
|
50
54
|
// so the one-time refresh token is never double-spent.
|
|
51
55
|
const inflight = new Map<string, Promise<CodexTokenSnapshot>>();
|
|
56
|
+
const CODEX_TOKEN_REFRESH_TIMEOUT_MS = 6_000;
|
|
57
|
+
|
|
58
|
+
export type CodexTokenDeadlineClock = {
|
|
59
|
+
setTimeout: (callback: () => void, delayMs: number) => ReturnType<typeof globalThis.setTimeout>;
|
|
60
|
+
clearTimeout: (handle: ReturnType<typeof globalThis.setTimeout>) => void;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export type CodexTokenDeadlineOptions = {
|
|
64
|
+
timeoutMs?: number | undefined;
|
|
65
|
+
signal?: AbortSignal | undefined;
|
|
66
|
+
clock?: CodexTokenDeadlineClock | undefined;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const systemCodexTokenDeadlineClock: CodexTokenDeadlineClock = {
|
|
70
|
+
setTimeout: (callback, delayMs) => globalThis.setTimeout(callback, delayMs),
|
|
71
|
+
clearTimeout: (handle) => globalThis.clearTimeout(handle),
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Bound a refresh promise without abandoning its rejection handler when the
|
|
76
|
+
* deadline or cancellation wins. The provider promise is observed exactly
|
|
77
|
+
* once, while the observer itself always fulfills, so a late provider failure
|
|
78
|
+
* cannot become an unhandled rejection or replace the authoritative outcome.
|
|
79
|
+
*/
|
|
80
|
+
export async function withCodexTokenDeadline<T>(
|
|
81
|
+
operation: Promise<T>,
|
|
82
|
+
options: CodexTokenDeadlineOptions = {},
|
|
83
|
+
): Promise<T> {
|
|
84
|
+
const timeoutMs = options.timeoutMs ?? CODEX_TOKEN_REFRESH_TIMEOUT_MS;
|
|
85
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
|
|
86
|
+
throw new Error("Codex token refresh timeout must be positive");
|
|
87
|
+
}
|
|
88
|
+
const clock = options.clock ?? systemCodexTokenDeadlineClock;
|
|
89
|
+
const signal = options.signal;
|
|
90
|
+
|
|
91
|
+
return await new Promise<T>((resolve, reject) => {
|
|
92
|
+
let settled = false;
|
|
93
|
+
let timeout: ReturnType<typeof globalThis.setTimeout> | undefined;
|
|
94
|
+
|
|
95
|
+
const cleanup = (): void => {
|
|
96
|
+
if (timeout !== undefined) {
|
|
97
|
+
clock.clearTimeout(timeout);
|
|
98
|
+
timeout = undefined;
|
|
99
|
+
}
|
|
100
|
+
signal?.removeEventListener("abort", onAbort);
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const settle = (
|
|
104
|
+
outcome: { kind: "resolve"; value: T } | { kind: "reject"; error: unknown },
|
|
105
|
+
) => {
|
|
106
|
+
if (settled) return;
|
|
107
|
+
settled = true;
|
|
108
|
+
cleanup();
|
|
109
|
+
if (outcome.kind === "resolve") {
|
|
110
|
+
resolve(outcome.value);
|
|
111
|
+
} else {
|
|
112
|
+
reject(outcome.error);
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const onAbort = (): void => {
|
|
117
|
+
settle({
|
|
118
|
+
kind: "reject",
|
|
119
|
+
error: signal?.reason ?? new Error("Codex token refresh cancelled"),
|
|
120
|
+
});
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
if (signal?.aborted) {
|
|
124
|
+
onAbort();
|
|
125
|
+
} else {
|
|
126
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
127
|
+
timeout = clock.setTimeout(
|
|
128
|
+
() => settle({ kind: "reject", error: new Error("Codex token refresh timed out") }),
|
|
129
|
+
timeoutMs,
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Do not use Promise.race here. Its derived promise can obscure which
|
|
134
|
+
// branch owns settlement, while this fulfillment-only observer makes the
|
|
135
|
+
// losing provider branch explicitly consumed after timeout/cancellation.
|
|
136
|
+
void Promise.resolve(operation).then(
|
|
137
|
+
(value) => settle({ kind: "resolve", value }),
|
|
138
|
+
(error) => settle({ kind: "reject", error }),
|
|
139
|
+
);
|
|
140
|
+
});
|
|
141
|
+
}
|
|
52
142
|
|
|
53
143
|
// Dependencies are injectable so the lifecycle logic (single-flight, staleness,
|
|
54
144
|
// needs_relogin transition) is unit-testable without a database. Production uses
|
|
@@ -96,7 +186,10 @@ export function buildCodexTokenResolver(
|
|
|
96
186
|
cred: CodexCredentialForRun,
|
|
97
187
|
): Promise<CodexTokenSnapshot> => {
|
|
98
188
|
try {
|
|
99
|
-
|
|
189
|
+
// Bound even injected/custom refresh implementations that ignore abort
|
|
190
|
+
// signals. The provider client has its own AbortController timeout; this
|
|
191
|
+
// outer fence ensures the DB advisory transaction cannot be held forever.
|
|
192
|
+
const next = await withCodexTokenDeadline(deps.refresh(cred.tokens.refreshToken));
|
|
100
193
|
const tokens = {
|
|
101
194
|
access_token: next.accessToken ?? cred.tokens.accessToken,
|
|
102
195
|
refresh_token: next.refreshToken ?? cred.tokens.refreshToken,
|
|
@@ -219,6 +312,7 @@ function errorUsagePayload(reason?: "needs_relogin"): CodexUsagePayload {
|
|
|
219
312
|
weekly: null,
|
|
220
313
|
limitReached: false,
|
|
221
314
|
fetchedAt: new Date().toISOString(),
|
|
315
|
+
rateLimitResetCredits: null,
|
|
222
316
|
...(reason ? { reason } : {}),
|
|
223
317
|
};
|
|
224
318
|
}
|
|
@@ -242,6 +336,7 @@ export async function fetchCodexUsageForAccount(
|
|
|
242
336
|
settings: Settings,
|
|
243
337
|
workspaceId: string,
|
|
244
338
|
credentialId: string,
|
|
339
|
+
fetchImpl: CodexFetch = fetch,
|
|
245
340
|
): Promise<CodexUsagePayload> {
|
|
246
341
|
const resolver = buildCodexTokenResolver(db, settings, workspaceId, credentialId);
|
|
247
342
|
let token: CodexTokenSnapshot;
|
|
@@ -253,12 +348,15 @@ export async function fetchCodexUsageForAccount(
|
|
|
253
348
|
|
|
254
349
|
let normalized: CodexUsagePayload;
|
|
255
350
|
try {
|
|
256
|
-
const usage = await fetchCodexUsage(
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
351
|
+
const usage = await fetchCodexUsage(
|
|
352
|
+
{
|
|
353
|
+
accessToken: token.accessToken,
|
|
354
|
+
chatgptAccountId: token.chatgptAccountId,
|
|
355
|
+
isFedramp: token.isFedramp,
|
|
356
|
+
clientVersion: CODEX_CLIENT_VERSION,
|
|
357
|
+
},
|
|
358
|
+
fetchImpl,
|
|
359
|
+
);
|
|
262
360
|
normalized = normalizeCodexUsage(usage.status, usage.payload);
|
|
263
361
|
} catch {
|
|
264
362
|
// A network throw on the /wham/usage read must surface as an error PAYLOAD
|
|
@@ -266,17 +364,80 @@ export async function fetchCodexUsageForAccount(
|
|
|
266
364
|
return errorUsagePayload();
|
|
267
365
|
}
|
|
268
366
|
|
|
269
|
-
|
|
367
|
+
const parsedQuota =
|
|
368
|
+
normalized.status !== "error" && (normalized.fiveHour != null || normalized.weekly != null);
|
|
369
|
+
if (parsedQuota || normalized.rateLimitResetCredits) {
|
|
370
|
+
const checkedAt = new Date();
|
|
371
|
+
// Quota windows and reset-summary freshness are independent. A malformed
|
|
372
|
+
// usage body can still carry a syntactically valid count; that count may be
|
|
373
|
+
// cached without erasing or falsely refreshing the last valid quota truth.
|
|
270
374
|
// Cache-write is best-effort: a disconnect under us (false) or a transient
|
|
271
|
-
// write error must NOT sink the freshly-read
|
|
375
|
+
// write error must NOT sink the freshly-read result we are about to return.
|
|
272
376
|
await recordCodexAccountUsage(db, workspaceId, credentialId, {
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
377
|
+
...(parsedQuota
|
|
378
|
+
? {
|
|
379
|
+
primaryUsedPercent: normalized.fiveHour?.percent ?? null,
|
|
380
|
+
primaryResetAt: normalized.fiveHour?.resetAt
|
|
381
|
+
? new Date(normalized.fiveHour.resetAt)
|
|
382
|
+
: null,
|
|
383
|
+
secondaryUsedPercent: normalized.weekly?.percent ?? null,
|
|
384
|
+
secondaryResetAt: normalized.weekly?.resetAt
|
|
385
|
+
? new Date(normalized.weekly.resetAt)
|
|
386
|
+
: null,
|
|
387
|
+
checkedAt,
|
|
388
|
+
}
|
|
389
|
+
: {}),
|
|
390
|
+
...(normalized.rateLimitResetCredits
|
|
391
|
+
? {
|
|
392
|
+
resetCreditAvailableCount: normalized.rateLimitResetCredits.availableCount,
|
|
393
|
+
resetCreditsCheckedAt: checkedAt,
|
|
394
|
+
}
|
|
395
|
+
: {}),
|
|
278
396
|
}).catch(() => undefined);
|
|
279
397
|
}
|
|
280
398
|
|
|
281
399
|
return normalized;
|
|
282
400
|
}
|
|
401
|
+
|
|
402
|
+
export type CodexRateLimitResetCreditsAccountResult =
|
|
403
|
+
| { ok: true; status: number; details: CodexRateLimitResetCreditsDetails }
|
|
404
|
+
| {
|
|
405
|
+
ok: false;
|
|
406
|
+
status: number;
|
|
407
|
+
reason: ResetCreditFetchFailureReason | "needs_relogin";
|
|
408
|
+
};
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Fresh detailed reset-credit inventory for one exact workspace credential.
|
|
412
|
+
* The token is refreshed through the same resolver as usage and never escapes
|
|
413
|
+
* this server-side function. Detailed rows are returned to the route only and
|
|
414
|
+
* are never persisted as redemption authority.
|
|
415
|
+
*/
|
|
416
|
+
export async function fetchCodexRateLimitResetCreditsForAccount(
|
|
417
|
+
db: Database,
|
|
418
|
+
settings: Settings,
|
|
419
|
+
workspaceId: string,
|
|
420
|
+
credentialId: string,
|
|
421
|
+
fetchImpl: CodexFetch = fetch,
|
|
422
|
+
): Promise<CodexRateLimitResetCreditsAccountResult> {
|
|
423
|
+
const resolver = buildCodexTokenResolver(db, settings, workspaceId, credentialId);
|
|
424
|
+
let token: CodexTokenSnapshot;
|
|
425
|
+
try {
|
|
426
|
+
token = await resolver.getToken();
|
|
427
|
+
} catch (error) {
|
|
428
|
+
return {
|
|
429
|
+
ok: false,
|
|
430
|
+
status: 0,
|
|
431
|
+
reason: error instanceof CodexReloginRequired ? "needs_relogin" : "network_error",
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
return await fetchCodexRateLimitResetCredits(
|
|
435
|
+
{
|
|
436
|
+
accessToken: token.accessToken,
|
|
437
|
+
chatgptAccountId: token.chatgptAccountId,
|
|
438
|
+
isFedramp: token.isFedramp,
|
|
439
|
+
clientVersion: CODEX_CLIENT_VERSION,
|
|
440
|
+
},
|
|
441
|
+
fetchImpl,
|
|
442
|
+
);
|
|
443
|
+
}
|