@bitkyc08/opencodex 2.37.0 → 2.38.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/bin/ocx.mjs +69 -10
- package/gui/dist/assets/{index-CowztZdo.js → index-C14iCj_Q.js} +13 -13
- package/gui/dist/assets/index-D7PIz7_g.css +1 -0
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/aside.svg +3 -0
- package/gui/dist/provider-icons/deepseek-harness.svg +3 -0
- package/gui/dist/provider-icons/oh-my-pi.svg +11 -0
- package/gui/dist/provider-icons/openclaw.svg +54 -0
- package/gui/dist/provider-icons/prime-agent.svg +21 -0
- package/gui/dist/provider-icons/zcode.svg +219 -0
- package/package.json +1 -1
- package/src/adapters/cursor/protobuf-request.ts +4 -1
- package/src/adapters/cursor/tool-definitions.ts +36 -4
- package/src/cli/capabilities.ts +14 -0
- package/src/cli/codex-cli-update.ts +96 -0
- package/src/cli/codex-shim-autorestore.ts +3 -0
- package/src/cli/export-command.ts +18 -17
- package/src/cli/help.ts +2 -2
- package/src/cli/index.ts +3 -2
- package/src/cli/launcher-context.ts +53 -2
- package/src/cli/opencode.ts +126 -33
- package/src/cli/registry.ts +16 -10
- package/src/cli/system-command.ts +6 -1
- package/src/clients/config-export.ts +293 -28
- package/src/codex/account-store.ts +10 -4
- package/src/codex/autostart-health.ts +3 -3
- package/src/codex/catalog/provider-fetch.ts +20 -1
- package/src/codex/catalog/sync.ts +4 -3
- package/src/codex/cli-install-provenance.ts +795 -0
- package/src/codex/convergence.ts +4 -3
- package/src/codex/credential-mutation-epoch.ts +11 -0
- package/src/codex/main-account.ts +2 -0
- package/src/codex/model-entitlements.ts +430 -27
- package/src/codex/native-profile-manager.ts +4 -0
- package/src/codex/reset-credit-operation-ledger.ts +1411 -0
- package/src/codex/reset-credit-recovery.ts +20 -2
- package/src/codex/shim.ts +204 -18
- package/src/codex/user-identity.ts +2 -1
- package/src/config/paths.ts +18 -3
- package/src/config.ts +23 -0
- package/src/generated/compatibility-version.json +81 -45
- package/src/integrations/registry.ts +112 -0
- package/src/integrations/state.ts +67 -5
- package/src/integrations/writer.ts +25 -9
- package/src/lib/bounded-subprocess.ts +36 -0
- package/src/lib/strict-semver.ts +47 -0
- package/src/lib/windows-elevation.ts +32 -1
- package/src/lib/windows-secret-acl.ts +47 -25
- package/src/lib/windows-service-mutation-lock.ts +133 -0
- package/src/lib/windows-user-principal.ts +15 -17
- package/src/responses/spill-store.ts +334 -29
- package/src/responses/state.ts +488 -7
- package/src/server/index.ts +4 -3
- package/src/server/lifecycle.ts +5 -1
- package/src/server/management/model-rows.ts +11 -2
- package/src/server/management/provider-routes.ts +4 -0
- package/src/server/management/system-restart.ts +5 -5
- package/src/server/management-api.ts +7 -2
- package/src/server/startup-action-control.ts +3 -2
- package/src/service.ts +594 -33
- package/src/sidecar/candidates.ts +1 -1
- package/src/update/codex-cli-update-launch-policy.d.mts +18 -0
- package/src/update/codex-cli-update-launch-policy.mjs +30 -0
- package/src/update/index.ts +3 -2
- package/src/update/job.ts +10 -11
- package/gui/dist/assets/index-jqE_VOKI.css +0 -1
|
@@ -0,0 +1,1411 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { chmodSync } from "node:fs";
|
|
3
|
+
import { Database } from "bun:sqlite";
|
|
4
|
+
import { NestedConfigMutationError, prepareConfigMutationDatabasePathForWrite } from "../config";
|
|
5
|
+
import { initializeConfigGeneration } from "./generation";
|
|
6
|
+
import {
|
|
7
|
+
compareCodexResetCreditRecoveryGenerationOrder,
|
|
8
|
+
isCodexResetCreditOperationId,
|
|
9
|
+
snapshotCodexResetCreditRecoveryGeneration,
|
|
10
|
+
type CodexResetCreditConsumeCode,
|
|
11
|
+
type CodexResetCreditRecoveryGeneration,
|
|
12
|
+
type CodexReservedOperationId,
|
|
13
|
+
} from "./reset-credit-recovery";
|
|
14
|
+
import { isValidCodexAccountId, MAIN_CODEX_ACCOUNT_ID } from "./account-id";
|
|
15
|
+
|
|
16
|
+
export const MAX_RESET_CREDIT_OPERATION_ACCOUNTS = 128;
|
|
17
|
+
export const MAX_MANUAL_RESET_CREDIT_OPERATION_IDS = 4_096;
|
|
18
|
+
const MANUAL_RESET_CREDIT_HISTORY_HIGH_WATER_MARK = Math.ceil(
|
|
19
|
+
MAX_MANUAL_RESET_CREDIT_OPERATION_IDS * 0.9,
|
|
20
|
+
);
|
|
21
|
+
let reportedManualHistoryLevel = 0;
|
|
22
|
+
type ResetCreditOperationMigrationFaultForTests = "after_first_write" | null;
|
|
23
|
+
let migrationFaultForTests: ResetCreditOperationMigrationFaultForTests = null;
|
|
24
|
+
const ACCOUNT_KEY_PATTERN = /^[0-9a-f]{64}$/;
|
|
25
|
+
const TERMINAL_STATE_BY_CODE: Readonly<Record<
|
|
26
|
+
CodexResetCreditConsumeCode,
|
|
27
|
+
"confirmed" | "stopped"
|
|
28
|
+
>> = Object.freeze({
|
|
29
|
+
reset: "confirmed",
|
|
30
|
+
already_redeemed: "confirmed",
|
|
31
|
+
nothing_to_reset: "stopped",
|
|
32
|
+
no_credit: "stopped",
|
|
33
|
+
});
|
|
34
|
+
const STATES: ReadonlySet<string> = new Set(["pending", "ambiguous", "confirmed", "stopped"]);
|
|
35
|
+
|
|
36
|
+
type ResetCreditOperationState = "pending" | "ambiguous" | "confirmed" | "stopped";
|
|
37
|
+
type ResetCreditOperationKind = "recovery" | "manual";
|
|
38
|
+
|
|
39
|
+
type ResetCreditOperationRecord = Readonly<{
|
|
40
|
+
accountKey: string;
|
|
41
|
+
operationKind: ResetCreditOperationKind;
|
|
42
|
+
credentialGeneration?: number;
|
|
43
|
+
exhaustionGeneration?: number;
|
|
44
|
+
operationId: string;
|
|
45
|
+
joinedOperationId?: string;
|
|
46
|
+
state: ResetCreditOperationState;
|
|
47
|
+
code?: CodexResetCreditConsumeCode;
|
|
48
|
+
createdAt: number;
|
|
49
|
+
updatedAt: number;
|
|
50
|
+
}>;
|
|
51
|
+
|
|
52
|
+
type ResetCreditOperationRow = {
|
|
53
|
+
account_key: unknown;
|
|
54
|
+
operation_kind: unknown;
|
|
55
|
+
credential_generation: unknown;
|
|
56
|
+
exhaustion_generation: unknown;
|
|
57
|
+
operation_id: unknown;
|
|
58
|
+
joined_operation_id: unknown;
|
|
59
|
+
state: unknown;
|
|
60
|
+
code: unknown;
|
|
61
|
+
created_at: unknown;
|
|
62
|
+
updated_at: unknown;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
type ManualResetCreditOperationIdRecord = Readonly<{
|
|
66
|
+
operationId: string;
|
|
67
|
+
accountKey: string;
|
|
68
|
+
canonicalOperationId: string;
|
|
69
|
+
terminalCode?: CodexResetCreditConsumeCode;
|
|
70
|
+
createdAt: number;
|
|
71
|
+
updatedAt: number;
|
|
72
|
+
}>;
|
|
73
|
+
|
|
74
|
+
type ManualResetCreditOperationIdRow = {
|
|
75
|
+
operation_id: unknown;
|
|
76
|
+
account_key: unknown;
|
|
77
|
+
canonical_operation_id: unknown;
|
|
78
|
+
terminal_code: unknown;
|
|
79
|
+
created_at: unknown;
|
|
80
|
+
updated_at: unknown;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
export type OpenResetCreditOperationResult =
|
|
84
|
+
| Readonly<{ kind: "execute"; operationId: CodexReservedOperationId; resumed: boolean }>
|
|
85
|
+
| Readonly<{ kind: "terminal"; operationId: CodexReservedOperationId; code: CodexResetCreditConsumeCode }>
|
|
86
|
+
| Readonly<{ kind: "stale-generation" | "unresolved-prior-generation" | "capacity" | "unavailable" }>;
|
|
87
|
+
|
|
88
|
+
export type UpdateResetCreditOperationResult =
|
|
89
|
+
| Readonly<{ kind: "updated" }>
|
|
90
|
+
| Readonly<{ kind: "mismatch" | "unavailable" }>;
|
|
91
|
+
|
|
92
|
+
export type ManualResetCreditOperationIdentity = Readonly<{
|
|
93
|
+
accountId: string;
|
|
94
|
+
chatgptAccountId: string;
|
|
95
|
+
operationId: string;
|
|
96
|
+
}>;
|
|
97
|
+
|
|
98
|
+
export type OpenManualResetCreditOperationResult =
|
|
99
|
+
| Readonly<{ kind: "execute"; operationId: CodexReservedOperationId; resumed: boolean }>
|
|
100
|
+
| Readonly<{ kind: "terminal"; operationId: CodexReservedOperationId; code: CodexResetCreditConsumeCode }>
|
|
101
|
+
| Readonly<{ kind: "capacity" | "identity-mismatch" | "unavailable" }>;
|
|
102
|
+
|
|
103
|
+
const TABLE_NAME = "reset_credit_operations";
|
|
104
|
+
const CREATE_TABLE = `CREATE TABLE main.reset_credit_operations (
|
|
105
|
+
account_key TEXT PRIMARY KEY,
|
|
106
|
+
operation_kind TEXT NOT NULL CHECK (operation_kind IN ('recovery', 'manual')),
|
|
107
|
+
credential_generation INTEGER,
|
|
108
|
+
exhaustion_generation INTEGER,
|
|
109
|
+
operation_id TEXT NOT NULL,
|
|
110
|
+
joined_operation_id TEXT,
|
|
111
|
+
state TEXT NOT NULL,
|
|
112
|
+
code TEXT,
|
|
113
|
+
created_at INTEGER NOT NULL CHECK (created_at >= 0),
|
|
114
|
+
updated_at INTEGER NOT NULL CHECK (updated_at >= created_at),
|
|
115
|
+
CHECK (
|
|
116
|
+
(operation_kind = 'recovery'
|
|
117
|
+
AND credential_generation IS NOT NULL AND credential_generation >= 0
|
|
118
|
+
AND exhaustion_generation IS NOT NULL AND exhaustion_generation >= 0
|
|
119
|
+
AND joined_operation_id IS NULL)
|
|
120
|
+
OR
|
|
121
|
+
(operation_kind = 'manual'
|
|
122
|
+
AND credential_generation IS NULL AND exhaustion_generation IS NULL)
|
|
123
|
+
),
|
|
124
|
+
CHECK (joined_operation_id IS NULL OR joined_operation_id <> operation_id)
|
|
125
|
+
) STRICT, WITHOUT ROWID`;
|
|
126
|
+
const EXPECTED_SCHEMA_SQL = CREATE_TABLE.replace("main.", "");
|
|
127
|
+
const MANUAL_ID_TABLE_NAME = "reset_credit_manual_operation_ids";
|
|
128
|
+
const CREATE_MANUAL_ID_TABLE = `CREATE TABLE main.reset_credit_manual_operation_ids (
|
|
129
|
+
operation_id TEXT PRIMARY KEY,
|
|
130
|
+
account_key TEXT NOT NULL,
|
|
131
|
+
canonical_operation_id TEXT NOT NULL,
|
|
132
|
+
terminal_code TEXT CHECK (
|
|
133
|
+
terminal_code IS NULL OR terminal_code IN (
|
|
134
|
+
'reset', 'already_redeemed', 'nothing_to_reset', 'no_credit'
|
|
135
|
+
)
|
|
136
|
+
),
|
|
137
|
+
created_at INTEGER NOT NULL CHECK (created_at >= 0),
|
|
138
|
+
updated_at INTEGER NOT NULL CHECK (updated_at >= created_at)
|
|
139
|
+
) STRICT, WITHOUT ROWID`;
|
|
140
|
+
const EXPECTED_MANUAL_ID_SCHEMA_SQL = CREATE_MANUAL_ID_TABLE.replace("main.", "");
|
|
141
|
+
const PRIOR_TABLE_NAME = "reset_credit_operations_legacy_v2";
|
|
142
|
+
const PRIOR_CREATE_TABLE = `CREATE TABLE reset_credit_operations (
|
|
143
|
+
account_key TEXT PRIMARY KEY,
|
|
144
|
+
operation_kind TEXT NOT NULL CHECK (operation_kind IN ('recovery', 'manual')),
|
|
145
|
+
credential_generation INTEGER,
|
|
146
|
+
exhaustion_generation INTEGER,
|
|
147
|
+
operation_id TEXT NOT NULL,
|
|
148
|
+
state TEXT NOT NULL,
|
|
149
|
+
code TEXT,
|
|
150
|
+
created_at INTEGER NOT NULL CHECK (created_at >= 0),
|
|
151
|
+
updated_at INTEGER NOT NULL CHECK (updated_at >= created_at),
|
|
152
|
+
CHECK (
|
|
153
|
+
(operation_kind = 'recovery'
|
|
154
|
+
AND credential_generation IS NOT NULL AND credential_generation >= 0
|
|
155
|
+
AND exhaustion_generation IS NOT NULL AND exhaustion_generation >= 0)
|
|
156
|
+
OR
|
|
157
|
+
(operation_kind = 'manual'
|
|
158
|
+
AND credential_generation IS NULL AND exhaustion_generation IS NULL)
|
|
159
|
+
)
|
|
160
|
+
) STRICT, WITHOUT ROWID`;
|
|
161
|
+
const LEGACY_TABLE_NAME = "reset_credit_operations_legacy_v1";
|
|
162
|
+
const LEGACY_CREATE_TABLE = `CREATE TABLE reset_credit_operations (
|
|
163
|
+
account_key TEXT PRIMARY KEY,
|
|
164
|
+
credential_generation INTEGER NOT NULL CHECK (credential_generation >= 0),
|
|
165
|
+
exhaustion_generation INTEGER NOT NULL CHECK (exhaustion_generation >= 0),
|
|
166
|
+
operation_id TEXT NOT NULL,
|
|
167
|
+
state TEXT NOT NULL,
|
|
168
|
+
code TEXT,
|
|
169
|
+
created_at INTEGER NOT NULL CHECK (created_at >= 0),
|
|
170
|
+
updated_at INTEGER NOT NULL CHECK (updated_at >= created_at)
|
|
171
|
+
) STRICT, WITHOUT ROWID`;
|
|
172
|
+
const SELECT_ALL = `
|
|
173
|
+
SELECT account_key, operation_kind, credential_generation,
|
|
174
|
+
exhaustion_generation, operation_id, joined_operation_id,
|
|
175
|
+
state, code, created_at, updated_at
|
|
176
|
+
FROM main.reset_credit_operations
|
|
177
|
+
ORDER BY account_key
|
|
178
|
+
LIMIT ${MAX_RESET_CREDIT_OPERATION_ACCOUNTS + 1}`;
|
|
179
|
+
const SELECT_BY_KEY = `
|
|
180
|
+
SELECT account_key, operation_kind, credential_generation,
|
|
181
|
+
exhaustion_generation, operation_id, joined_operation_id,
|
|
182
|
+
state, code, created_at, updated_at
|
|
183
|
+
FROM main.reset_credit_operations
|
|
184
|
+
WHERE account_key = ?
|
|
185
|
+
LIMIT 2`;
|
|
186
|
+
const SELECT_KEY_BY_OPERATION_ID = `
|
|
187
|
+
SELECT account_key FROM (
|
|
188
|
+
SELECT account_key
|
|
189
|
+
FROM main.reset_credit_operations
|
|
190
|
+
WHERE operation_id = ? OR joined_operation_id = ?
|
|
191
|
+
UNION
|
|
192
|
+
SELECT account_key
|
|
193
|
+
FROM main.reset_credit_manual_operation_ids
|
|
194
|
+
WHERE operation_id = ?
|
|
195
|
+
)
|
|
196
|
+
LIMIT 2`;
|
|
197
|
+
const SELECT_ALL_MANUAL_IDS = `
|
|
198
|
+
SELECT operation_id, account_key, canonical_operation_id,
|
|
199
|
+
terminal_code, created_at, updated_at
|
|
200
|
+
FROM main.reset_credit_manual_operation_ids
|
|
201
|
+
ORDER BY operation_id
|
|
202
|
+
LIMIT ${MAX_MANUAL_RESET_CREDIT_OPERATION_IDS + 1}`;
|
|
203
|
+
const SELECT_BOUNDED_MANUAL_ID_COUNT = `
|
|
204
|
+
SELECT COUNT(*) AS count
|
|
205
|
+
FROM (
|
|
206
|
+
SELECT 1
|
|
207
|
+
FROM main.reset_credit_manual_operation_ids
|
|
208
|
+
LIMIT ${MAX_MANUAL_RESET_CREDIT_OPERATION_IDS + 1}
|
|
209
|
+
)`;
|
|
210
|
+
const SELECT_DUPLICATE_RECOVERY_MANUAL_ID = `
|
|
211
|
+
SELECT operations.operation_id
|
|
212
|
+
FROM main.reset_credit_operations AS operations
|
|
213
|
+
JOIN main.reset_credit_manual_operation_ids AS manual_ids
|
|
214
|
+
ON manual_ids.operation_id = operations.operation_id
|
|
215
|
+
WHERE operations.operation_kind = 'recovery'
|
|
216
|
+
LIMIT 1`;
|
|
217
|
+
const SELECT_MANUAL_ID = `
|
|
218
|
+
SELECT operation_id, account_key, canonical_operation_id,
|
|
219
|
+
terminal_code, created_at, updated_at
|
|
220
|
+
FROM main.reset_credit_manual_operation_ids
|
|
221
|
+
WHERE operation_id = ?
|
|
222
|
+
LIMIT 2`;
|
|
223
|
+
const SELECT_MANUAL_IDS_BY_CANONICAL = `
|
|
224
|
+
SELECT operation_id, account_key, canonical_operation_id,
|
|
225
|
+
terminal_code, created_at, updated_at
|
|
226
|
+
FROM main.reset_credit_manual_operation_ids
|
|
227
|
+
WHERE account_key = ? AND canonical_operation_id = ?
|
|
228
|
+
ORDER BY operation_id
|
|
229
|
+
LIMIT ${MAX_MANUAL_RESET_CREDIT_OPERATION_IDS + 1}`;
|
|
230
|
+
const INSERT_MANUAL_ID = `
|
|
231
|
+
INSERT INTO main.reset_credit_manual_operation_ids (
|
|
232
|
+
operation_id, account_key, canonical_operation_id,
|
|
233
|
+
terminal_code, created_at, updated_at
|
|
234
|
+
) VALUES (?, ?, ?, ?, ?, ?)`;
|
|
235
|
+
const SETTLE_MANUAL_IDS = `
|
|
236
|
+
UPDATE main.reset_credit_manual_operation_ids
|
|
237
|
+
SET terminal_code = ?, updated_at = ?
|
|
238
|
+
WHERE account_key = ? AND canonical_operation_id = ?
|
|
239
|
+
AND (terminal_code IS NULL OR terminal_code = ?)`;
|
|
240
|
+
const INSERT_RECORD = `
|
|
241
|
+
INSERT INTO main.reset_credit_operations (
|
|
242
|
+
account_key, operation_kind, credential_generation, exhaustion_generation,
|
|
243
|
+
operation_id, joined_operation_id, state, code, created_at, updated_at
|
|
244
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`;
|
|
245
|
+
const REPLACE_RECORD = `
|
|
246
|
+
UPDATE main.reset_credit_operations
|
|
247
|
+
SET operation_kind = ?, credential_generation = ?, exhaustion_generation = ?,
|
|
248
|
+
operation_id = ?, joined_operation_id = ?, state = ?, code = ?,
|
|
249
|
+
created_at = ?, updated_at = ?
|
|
250
|
+
WHERE account_key = ?`;
|
|
251
|
+
const UPDATE_RECORD = `
|
|
252
|
+
UPDATE main.reset_credit_operations
|
|
253
|
+
SET state = ?, code = ?, updated_at = ?
|
|
254
|
+
WHERE account_key = ? AND operation_kind = ? AND operation_id = ?
|
|
255
|
+
AND credential_generation IS ? AND exhaustion_generation IS ?`;
|
|
256
|
+
const JOIN_MANUAL_OPERATION = `
|
|
257
|
+
UPDATE main.reset_credit_operations
|
|
258
|
+
SET joined_operation_id = ?, updated_at = ?
|
|
259
|
+
WHERE account_key = ? AND operation_kind = 'manual' AND operation_id = ?
|
|
260
|
+
AND joined_operation_id IS NULL AND state IN ('pending', 'ambiguous')`;
|
|
261
|
+
const TOUCH_MANUAL_OPERATION = `
|
|
262
|
+
UPDATE main.reset_credit_operations
|
|
263
|
+
SET updated_at = ?
|
|
264
|
+
WHERE account_key = ? AND operation_kind = 'manual' AND operation_id = ?
|
|
265
|
+
AND joined_operation_id IS NOT NULL AND state IN ('pending', 'ambiguous')`;
|
|
266
|
+
|
|
267
|
+
type SchemaObjectRow = {
|
|
268
|
+
type: unknown;
|
|
269
|
+
name: unknown;
|
|
270
|
+
tbl_name: unknown;
|
|
271
|
+
sql: unknown;
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
type TableListRow = {
|
|
275
|
+
schema: unknown;
|
|
276
|
+
name: unknown;
|
|
277
|
+
type: unknown;
|
|
278
|
+
ncol: unknown;
|
|
279
|
+
wr: unknown;
|
|
280
|
+
strict: unknown;
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
type TableColumnRow = {
|
|
284
|
+
cid: unknown;
|
|
285
|
+
name: unknown;
|
|
286
|
+
type: unknown;
|
|
287
|
+
notnull: unknown;
|
|
288
|
+
dflt_value: unknown;
|
|
289
|
+
pk: unknown;
|
|
290
|
+
hidden: unknown;
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
const EXPECTED_COLUMNS = Object.freeze([
|
|
294
|
+
Object.freeze({ name: "account_key", type: "TEXT", notnull: 1, pk: 1 }),
|
|
295
|
+
Object.freeze({ name: "operation_kind", type: "TEXT", notnull: 1, pk: 0 }),
|
|
296
|
+
Object.freeze({ name: "credential_generation", type: "INTEGER", notnull: 0, pk: 0 }),
|
|
297
|
+
Object.freeze({ name: "exhaustion_generation", type: "INTEGER", notnull: 0, pk: 0 }),
|
|
298
|
+
Object.freeze({ name: "operation_id", type: "TEXT", notnull: 1, pk: 0 }),
|
|
299
|
+
Object.freeze({ name: "joined_operation_id", type: "TEXT", notnull: 0, pk: 0 }),
|
|
300
|
+
Object.freeze({ name: "state", type: "TEXT", notnull: 1, pk: 0 }),
|
|
301
|
+
Object.freeze({ name: "code", type: "TEXT", notnull: 0, pk: 0 }),
|
|
302
|
+
Object.freeze({ name: "created_at", type: "INTEGER", notnull: 1, pk: 0 }),
|
|
303
|
+
Object.freeze({ name: "updated_at", type: "INTEGER", notnull: 1, pk: 0 }),
|
|
304
|
+
]);
|
|
305
|
+
|
|
306
|
+
const MANUAL_ID_COLUMNS = Object.freeze([
|
|
307
|
+
Object.freeze({ name: "operation_id", type: "TEXT", notnull: 1, pk: 1 }),
|
|
308
|
+
Object.freeze({ name: "account_key", type: "TEXT", notnull: 1, pk: 0 }),
|
|
309
|
+
Object.freeze({ name: "canonical_operation_id", type: "TEXT", notnull: 1, pk: 0 }),
|
|
310
|
+
Object.freeze({ name: "terminal_code", type: "TEXT", notnull: 0, pk: 0 }),
|
|
311
|
+
Object.freeze({ name: "created_at", type: "INTEGER", notnull: 1, pk: 0 }),
|
|
312
|
+
Object.freeze({ name: "updated_at", type: "INTEGER", notnull: 1, pk: 0 }),
|
|
313
|
+
]);
|
|
314
|
+
|
|
315
|
+
const LEGACY_COLUMNS = Object.freeze([
|
|
316
|
+
Object.freeze({ name: "account_key", type: "TEXT", notnull: 1, pk: 1 }),
|
|
317
|
+
Object.freeze({ name: "credential_generation", type: "INTEGER", notnull: 1, pk: 0 }),
|
|
318
|
+
Object.freeze({ name: "exhaustion_generation", type: "INTEGER", notnull: 1, pk: 0 }),
|
|
319
|
+
Object.freeze({ name: "operation_id", type: "TEXT", notnull: 1, pk: 0 }),
|
|
320
|
+
Object.freeze({ name: "state", type: "TEXT", notnull: 1, pk: 0 }),
|
|
321
|
+
Object.freeze({ name: "code", type: "TEXT", notnull: 0, pk: 0 }),
|
|
322
|
+
Object.freeze({ name: "created_at", type: "INTEGER", notnull: 1, pk: 0 }),
|
|
323
|
+
Object.freeze({ name: "updated_at", type: "INTEGER", notnull: 1, pk: 0 }),
|
|
324
|
+
]);
|
|
325
|
+
const PRIOR_COLUMNS = Object.freeze([
|
|
326
|
+
Object.freeze({ name: "account_key", type: "TEXT", notnull: 1, pk: 1 }),
|
|
327
|
+
Object.freeze({ name: "operation_kind", type: "TEXT", notnull: 1, pk: 0 }),
|
|
328
|
+
Object.freeze({ name: "credential_generation", type: "INTEGER", notnull: 0, pk: 0 }),
|
|
329
|
+
Object.freeze({ name: "exhaustion_generation", type: "INTEGER", notnull: 0, pk: 0 }),
|
|
330
|
+
Object.freeze({ name: "operation_id", type: "TEXT", notnull: 1, pk: 0 }),
|
|
331
|
+
Object.freeze({ name: "state", type: "TEXT", notnull: 1, pk: 0 }),
|
|
332
|
+
Object.freeze({ name: "code", type: "TEXT", notnull: 0, pk: 0 }),
|
|
333
|
+
Object.freeze({ name: "created_at", type: "INTEGER", notnull: 1, pk: 0 }),
|
|
334
|
+
Object.freeze({ name: "updated_at", type: "INTEGER", notnull: 1, pk: 0 }),
|
|
335
|
+
]);
|
|
336
|
+
|
|
337
|
+
function accountKey(accountId: string): string {
|
|
338
|
+
return createHash("sha256").update(`codex-reset-credit-operation\0${accountId}`).digest("hex");
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function validateManualAccountId(accountId: string): void {
|
|
342
|
+
if (accountId !== MAIN_CODEX_ACCOUNT_ID && !isValidCodexAccountId(accountId)) {
|
|
343
|
+
throw new TypeError("invalid manual reset-credit account");
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function manualPhysicalAccountKey(chatgptAccountId: string): string {
|
|
348
|
+
const normalized = chatgptAccountId.trim();
|
|
349
|
+
if (!normalized) throw new TypeError("invalid manual reset-credit credential identity");
|
|
350
|
+
return createHash("sha256")
|
|
351
|
+
.update(`codex-reset-credit-manual-physical\0${normalized}`)
|
|
352
|
+
.digest("hex");
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function isGenerationNumber(value: unknown): value is number {
|
|
356
|
+
return Number.isSafeInteger(value) && Number(value) >= 0;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function parseRecord(row: ResetCreditOperationRow | null): ResetCreditOperationRecord | undefined {
|
|
360
|
+
if (!row) return undefined;
|
|
361
|
+
const state = row.state;
|
|
362
|
+
const code = row.code;
|
|
363
|
+
const joinedOperationId = row.joined_operation_id;
|
|
364
|
+
if (typeof row.account_key !== "string" || !ACCOUNT_KEY_PATTERN.test(row.account_key)
|
|
365
|
+
|| (row.operation_kind !== "recovery" && row.operation_kind !== "manual")
|
|
366
|
+
|| !isCodexResetCreditOperationId(row.operation_id)
|
|
367
|
+
|| (joinedOperationId !== null
|
|
368
|
+
&& (!isCodexResetCreditOperationId(joinedOperationId) || joinedOperationId === row.operation_id))
|
|
369
|
+
|| typeof state !== "string" || !STATES.has(state)
|
|
370
|
+
|| !isGenerationNumber(row.created_at)
|
|
371
|
+
|| !isGenerationNumber(row.updated_at)
|
|
372
|
+
|| row.updated_at < row.created_at) {
|
|
373
|
+
return undefined;
|
|
374
|
+
}
|
|
375
|
+
const recovery = row.operation_kind === "recovery";
|
|
376
|
+
const manual = row.operation_kind === "manual";
|
|
377
|
+
if (recovery !== (isGenerationNumber(row.credential_generation)
|
|
378
|
+
&& isGenerationNumber(row.exhaustion_generation))
|
|
379
|
+
|| manual !== (row.credential_generation === null
|
|
380
|
+
&& row.exhaustion_generation === null)
|
|
381
|
+
|| (recovery && joinedOperationId !== null)) {
|
|
382
|
+
return undefined;
|
|
383
|
+
}
|
|
384
|
+
const terminal = state === "confirmed" || state === "stopped";
|
|
385
|
+
if (!terminal && code !== null) return undefined;
|
|
386
|
+
const terminalState = typeof code === "string"
|
|
387
|
+
&& Object.prototype.hasOwnProperty.call(TERMINAL_STATE_BY_CODE, code)
|
|
388
|
+
? TERMINAL_STATE_BY_CODE[code as CodexResetCreditConsumeCode]
|
|
389
|
+
: undefined;
|
|
390
|
+
if (terminal !== (terminalState !== undefined)) return undefined;
|
|
391
|
+
if (terminal && state !== terminalState) return undefined;
|
|
392
|
+
return Object.freeze({
|
|
393
|
+
accountKey: row.account_key,
|
|
394
|
+
operationKind: row.operation_kind,
|
|
395
|
+
...(recovery
|
|
396
|
+
? {
|
|
397
|
+
credentialGeneration: row.credential_generation as number,
|
|
398
|
+
exhaustionGeneration: row.exhaustion_generation as number,
|
|
399
|
+
}
|
|
400
|
+
: {}),
|
|
401
|
+
operationId: row.operation_id,
|
|
402
|
+
...(joinedOperationId === null ? {} : { joinedOperationId }),
|
|
403
|
+
state: state as ResetCreditOperationState,
|
|
404
|
+
...(terminal ? { code: code as CodexResetCreditConsumeCode } : {}),
|
|
405
|
+
createdAt: row.created_at,
|
|
406
|
+
updatedAt: row.updated_at,
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function parseManualIdRecord(
|
|
411
|
+
row: ManualResetCreditOperationIdRow | null,
|
|
412
|
+
): ManualResetCreditOperationIdRecord | undefined {
|
|
413
|
+
if (!row) return undefined;
|
|
414
|
+
const terminalCode = row.terminal_code;
|
|
415
|
+
if (!isCodexResetCreditOperationId(row.operation_id)
|
|
416
|
+
|| typeof row.account_key !== "string" || !ACCOUNT_KEY_PATTERN.test(row.account_key)
|
|
417
|
+
|| !isCodexResetCreditOperationId(row.canonical_operation_id)
|
|
418
|
+
|| (terminalCode !== null
|
|
419
|
+
&& (typeof terminalCode !== "string"
|
|
420
|
+
|| !Object.prototype.hasOwnProperty.call(TERMINAL_STATE_BY_CODE, terminalCode)))
|
|
421
|
+
|| !isGenerationNumber(row.created_at)
|
|
422
|
+
|| !isGenerationNumber(row.updated_at)
|
|
423
|
+
|| row.updated_at < row.created_at) {
|
|
424
|
+
return undefined;
|
|
425
|
+
}
|
|
426
|
+
return Object.freeze({
|
|
427
|
+
operationId: row.operation_id,
|
|
428
|
+
accountKey: row.account_key,
|
|
429
|
+
canonicalOperationId: row.canonical_operation_id,
|
|
430
|
+
...(terminalCode === null ? {} : { terminalCode: terminalCode as CodexResetCreditConsumeCode }),
|
|
431
|
+
createdAt: row.created_at,
|
|
432
|
+
updatedAt: row.updated_at,
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function assertColumnLayout(
|
|
437
|
+
database: Database,
|
|
438
|
+
tableName: string,
|
|
439
|
+
expectedColumns: readonly Readonly<{ name: string; type: string; notnull: number; pk: number }>[],
|
|
440
|
+
): void {
|
|
441
|
+
const tableRows = database.query<TableListRow, []>("PRAGMA main.table_list").all()
|
|
442
|
+
.filter(row => row.name === tableName);
|
|
443
|
+
if (tableRows.length !== 1) throw new Error("invalid reset-credit operation ledger table");
|
|
444
|
+
const table = tableRows[0]!;
|
|
445
|
+
if (table.schema !== "main" || table.type !== "table" || table.ncol !== expectedColumns.length
|
|
446
|
+
|| table.wr !== 1 || table.strict !== 1) {
|
|
447
|
+
throw new Error("invalid reset-credit operation ledger table");
|
|
448
|
+
}
|
|
449
|
+
const columns = database.query<TableColumnRow, []>(
|
|
450
|
+
`PRAGMA main.table_xinfo(${tableName})`,
|
|
451
|
+
).all();
|
|
452
|
+
if (columns.length !== expectedColumns.length) {
|
|
453
|
+
throw new Error("invalid reset-credit operation ledger columns");
|
|
454
|
+
}
|
|
455
|
+
for (let index = 0; index < expectedColumns.length; index += 1) {
|
|
456
|
+
const actual = columns[index]!;
|
|
457
|
+
const expected = expectedColumns[index]!;
|
|
458
|
+
if (actual.cid !== index || actual.name !== expected.name || actual.type !== expected.type
|
|
459
|
+
|| actual.notnull !== expected.notnull || actual.dflt_value !== null
|
|
460
|
+
|| actual.pk !== expected.pk || actual.hidden !== 0) {
|
|
461
|
+
throw new Error("invalid reset-credit operation ledger columns");
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function assertNoLedgerTriggers(database: Database, tableName: string): void {
|
|
467
|
+
const mainTrigger = database.query<{ name: unknown }, [string]>(`
|
|
468
|
+
SELECT name FROM main.sqlite_schema
|
|
469
|
+
WHERE type = 'trigger' AND tbl_name = ? COLLATE NOCASE LIMIT 1
|
|
470
|
+
`).get(tableName);
|
|
471
|
+
const tempTrigger = database.query<{ name: unknown }, [string]>(`
|
|
472
|
+
SELECT name FROM temp.sqlite_schema
|
|
473
|
+
WHERE type = 'trigger' AND tbl_name = ? COLLATE NOCASE LIMIT 1
|
|
474
|
+
`).get(tableName);
|
|
475
|
+
if (mainTrigger || tempTrigger) throw new Error("reset-credit operation ledger triggers are forbidden");
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
function failMigrationAfterFirstWriteForTests(): void {
|
|
479
|
+
if (migrationFaultForTests === "after_first_write") {
|
|
480
|
+
throw new Error("synthetic reset-credit operation migration failure");
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/** @internal Test-only fault injection after the first transactional migration write. */
|
|
485
|
+
export function setResetCreditOperationMigrationFaultForTests(
|
|
486
|
+
fault: ResetCreditOperationMigrationFaultForTests,
|
|
487
|
+
): void {
|
|
488
|
+
if (process.env.OCX_TEST_HOME_GUARD !== "1") {
|
|
489
|
+
throw new Error("reset-credit operation migration faults require the repository test preload");
|
|
490
|
+
}
|
|
491
|
+
migrationFaultForTests = fault;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
function migrateLegacyTable(database: Database): void {
|
|
495
|
+
assertColumnLayout(database, TABLE_NAME, LEGACY_COLUMNS);
|
|
496
|
+
assertNoLedgerTriggers(database, TABLE_NAME);
|
|
497
|
+
const legacyRows = database.query<{
|
|
498
|
+
account_key: unknown;
|
|
499
|
+
credential_generation: unknown;
|
|
500
|
+
exhaustion_generation: unknown;
|
|
501
|
+
operation_id: unknown;
|
|
502
|
+
state: unknown;
|
|
503
|
+
code: unknown;
|
|
504
|
+
created_at: unknown;
|
|
505
|
+
updated_at: unknown;
|
|
506
|
+
}, []>(`
|
|
507
|
+
SELECT account_key, credential_generation, exhaustion_generation, operation_id,
|
|
508
|
+
state, code, created_at, updated_at
|
|
509
|
+
FROM main.reset_credit_operations
|
|
510
|
+
ORDER BY account_key
|
|
511
|
+
LIMIT ${MAX_RESET_CREDIT_OPERATION_ACCOUNTS + 1}
|
|
512
|
+
`).all();
|
|
513
|
+
if (legacyRows.length > MAX_RESET_CREDIT_OPERATION_ACCOUNTS) {
|
|
514
|
+
throw new Error("invalid reset-credit operation ledger capacity");
|
|
515
|
+
}
|
|
516
|
+
const keys = new Set<string>();
|
|
517
|
+
const operations = new Set<string>();
|
|
518
|
+
for (const row of legacyRows) {
|
|
519
|
+
const record = parseRecord({
|
|
520
|
+
...row,
|
|
521
|
+
operation_kind: "recovery",
|
|
522
|
+
joined_operation_id: null,
|
|
523
|
+
});
|
|
524
|
+
if (!record || keys.has(record.accountKey) || operations.has(record.operationId)) {
|
|
525
|
+
throw new Error("invalid reset-credit operation ledger state");
|
|
526
|
+
}
|
|
527
|
+
keys.add(record.accountKey);
|
|
528
|
+
operations.add(record.operationId);
|
|
529
|
+
}
|
|
530
|
+
database.exec(`ALTER TABLE main.${TABLE_NAME} RENAME TO ${LEGACY_TABLE_NAME}`);
|
|
531
|
+
failMigrationAfterFirstWriteForTests();
|
|
532
|
+
database.exec(CREATE_TABLE);
|
|
533
|
+
database.exec(`
|
|
534
|
+
INSERT INTO main.${TABLE_NAME} (
|
|
535
|
+
account_key, operation_kind, credential_generation, exhaustion_generation,
|
|
536
|
+
operation_id, joined_operation_id, state, code, created_at, updated_at
|
|
537
|
+
)
|
|
538
|
+
SELECT account_key, 'recovery', credential_generation, exhaustion_generation,
|
|
539
|
+
operation_id, NULL, state, code, created_at, updated_at
|
|
540
|
+
FROM main.${LEGACY_TABLE_NAME}
|
|
541
|
+
`);
|
|
542
|
+
database.exec(`DROP TABLE main.${LEGACY_TABLE_NAME}`);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
function migratePriorTable(database: Database): void {
|
|
546
|
+
assertColumnLayout(database, TABLE_NAME, PRIOR_COLUMNS);
|
|
547
|
+
assertNoLedgerTriggers(database, TABLE_NAME);
|
|
548
|
+
const priorRows = database.query<Omit<ResetCreditOperationRow, "joined_operation_id">, []>(`
|
|
549
|
+
SELECT account_key, operation_kind, credential_generation, exhaustion_generation,
|
|
550
|
+
operation_id, state, code, created_at, updated_at
|
|
551
|
+
FROM main.reset_credit_operations
|
|
552
|
+
ORDER BY account_key
|
|
553
|
+
LIMIT ${MAX_RESET_CREDIT_OPERATION_ACCOUNTS + 1}
|
|
554
|
+
`).all();
|
|
555
|
+
if (priorRows.length > MAX_RESET_CREDIT_OPERATION_ACCOUNTS) {
|
|
556
|
+
throw new Error("invalid reset-credit operation ledger capacity");
|
|
557
|
+
}
|
|
558
|
+
const keys = new Set<string>();
|
|
559
|
+
const operations = new Set<string>();
|
|
560
|
+
for (const row of priorRows) {
|
|
561
|
+
const record = parseRecord({ ...row, joined_operation_id: null });
|
|
562
|
+
if (!record || keys.has(record.accountKey) || operations.has(record.operationId)) {
|
|
563
|
+
throw new Error("invalid reset-credit operation ledger state");
|
|
564
|
+
}
|
|
565
|
+
keys.add(record.accountKey);
|
|
566
|
+
operations.add(record.operationId);
|
|
567
|
+
}
|
|
568
|
+
database.exec(`ALTER TABLE main.${TABLE_NAME} RENAME TO ${PRIOR_TABLE_NAME}`);
|
|
569
|
+
failMigrationAfterFirstWriteForTests();
|
|
570
|
+
database.exec(CREATE_TABLE);
|
|
571
|
+
database.exec(`
|
|
572
|
+
INSERT INTO main.${TABLE_NAME} (
|
|
573
|
+
account_key, operation_kind, credential_generation, exhaustion_generation,
|
|
574
|
+
operation_id, joined_operation_id, state, code, created_at, updated_at
|
|
575
|
+
)
|
|
576
|
+
SELECT account_key, operation_kind, credential_generation, exhaustion_generation,
|
|
577
|
+
operation_id, NULL, state, code, created_at, updated_at
|
|
578
|
+
FROM main.${PRIOR_TABLE_NAME}
|
|
579
|
+
`);
|
|
580
|
+
database.exec(`DROP TABLE main.${PRIOR_TABLE_NAME}`);
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
function isExactLegacySchema(database: Database, schema: SchemaObjectRow): boolean {
|
|
584
|
+
if (schema.type !== "table" || schema.name !== TABLE_NAME || schema.tbl_name !== TABLE_NAME
|
|
585
|
+
|| schema.sql !== LEGACY_CREATE_TABLE) return false;
|
|
586
|
+
try {
|
|
587
|
+
assertColumnLayout(database, TABLE_NAME, LEGACY_COLUMNS);
|
|
588
|
+
assertNoLedgerTriggers(database, TABLE_NAME);
|
|
589
|
+
return true;
|
|
590
|
+
} catch {
|
|
591
|
+
return false;
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
function isExactPriorSchema(database: Database, schema: SchemaObjectRow): boolean {
|
|
596
|
+
if (schema.type !== "table" || schema.name !== TABLE_NAME || schema.tbl_name !== TABLE_NAME
|
|
597
|
+
|| schema.sql !== PRIOR_CREATE_TABLE) return false;
|
|
598
|
+
try {
|
|
599
|
+
assertColumnLayout(database, TABLE_NAME, PRIOR_COLUMNS);
|
|
600
|
+
assertNoLedgerTriggers(database, TABLE_NAME);
|
|
601
|
+
return true;
|
|
602
|
+
} catch {
|
|
603
|
+
return false;
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
type PrimaryTableInitialization = "created" | "migrated" | "existing";
|
|
608
|
+
|
|
609
|
+
function assertCanonicalTable(database: Database): PrimaryTableInitialization {
|
|
610
|
+
const schemaRows = database.query<SchemaObjectRow, [string, string]>(`
|
|
611
|
+
SELECT type, name, tbl_name, sql
|
|
612
|
+
FROM main.sqlite_schema
|
|
613
|
+
WHERE name = ? COLLATE NOCASE OR tbl_name = ? COLLATE NOCASE
|
|
614
|
+
ORDER BY type, name
|
|
615
|
+
LIMIT 4
|
|
616
|
+
`).all(TABLE_NAME, TABLE_NAME);
|
|
617
|
+
let initialization: PrimaryTableInitialization;
|
|
618
|
+
if (schemaRows.length === 0) {
|
|
619
|
+
database.exec(CREATE_TABLE);
|
|
620
|
+
initialization = "created";
|
|
621
|
+
} else if (schemaRows.length === 1 && isExactLegacySchema(database, schemaRows[0]!)) {
|
|
622
|
+
migrateLegacyTable(database);
|
|
623
|
+
initialization = "migrated";
|
|
624
|
+
} else if (schemaRows.length === 1 && isExactPriorSchema(database, schemaRows[0]!)) {
|
|
625
|
+
migratePriorTable(database);
|
|
626
|
+
initialization = "migrated";
|
|
627
|
+
} else if (schemaRows.length !== 1
|
|
628
|
+
|| schemaRows[0]?.type !== "table"
|
|
629
|
+
|| schemaRows[0]?.name !== TABLE_NAME
|
|
630
|
+
|| schemaRows[0]?.tbl_name !== TABLE_NAME
|
|
631
|
+
|| schemaRows[0]?.sql !== EXPECTED_SCHEMA_SQL) {
|
|
632
|
+
throw new Error("invalid reset-credit operation ledger schema");
|
|
633
|
+
} else {
|
|
634
|
+
initialization = "existing";
|
|
635
|
+
}
|
|
636
|
+
assertColumnLayout(database, TABLE_NAME, EXPECTED_COLUMNS);
|
|
637
|
+
assertNoLedgerTriggers(database, TABLE_NAME);
|
|
638
|
+
return initialization;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
function ensureManualIdTable(database: Database, allowCreate: boolean): boolean {
|
|
642
|
+
const schemaRows = database.query<SchemaObjectRow, [string, string]>(`
|
|
643
|
+
SELECT type, name, tbl_name, sql
|
|
644
|
+
FROM main.sqlite_schema
|
|
645
|
+
WHERE name = ? COLLATE NOCASE OR tbl_name = ? COLLATE NOCASE
|
|
646
|
+
ORDER BY type, name
|
|
647
|
+
LIMIT 4
|
|
648
|
+
`).all(MANUAL_ID_TABLE_NAME, MANUAL_ID_TABLE_NAME);
|
|
649
|
+
let created = false;
|
|
650
|
+
if (schemaRows.length === 0) {
|
|
651
|
+
if (!allowCreate) {
|
|
652
|
+
throw new Error("missing manual reset-credit operation identity schema");
|
|
653
|
+
}
|
|
654
|
+
database.exec(CREATE_MANUAL_ID_TABLE);
|
|
655
|
+
created = true;
|
|
656
|
+
} else if (schemaRows.length !== 1
|
|
657
|
+
|| schemaRows[0]?.type !== "table"
|
|
658
|
+
|| schemaRows[0]?.name !== MANUAL_ID_TABLE_NAME
|
|
659
|
+
|| schemaRows[0]?.tbl_name !== MANUAL_ID_TABLE_NAME
|
|
660
|
+
|| schemaRows[0]?.sql !== EXPECTED_MANUAL_ID_SCHEMA_SQL) {
|
|
661
|
+
throw new Error("invalid manual reset-credit operation identity schema");
|
|
662
|
+
}
|
|
663
|
+
assertColumnLayout(database, MANUAL_ID_TABLE_NAME, MANUAL_ID_COLUMNS);
|
|
664
|
+
assertNoLedgerTriggers(database, MANUAL_ID_TABLE_NAME);
|
|
665
|
+
return created;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
function initializeTable(
|
|
669
|
+
database: Database,
|
|
670
|
+
validationScope: ResetCreditOperationKind,
|
|
671
|
+
): Readonly<{
|
|
672
|
+
recordCount: number;
|
|
673
|
+
manualIdCount: number;
|
|
674
|
+
}> {
|
|
675
|
+
const manualSchemaPresentBefore = database.query<{ present: number }, [string, string]>(`
|
|
676
|
+
SELECT 1 AS present
|
|
677
|
+
FROM main.sqlite_schema
|
|
678
|
+
WHERE name = ? COLLATE NOCASE OR tbl_name = ? COLLATE NOCASE
|
|
679
|
+
LIMIT 1
|
|
680
|
+
`).get(MANUAL_ID_TABLE_NAME, MANUAL_ID_TABLE_NAME) !== null;
|
|
681
|
+
const primaryInitialization = assertCanonicalTable(database);
|
|
682
|
+
if (primaryInitialization !== "existing" && manualSchemaPresentBefore) {
|
|
683
|
+
throw new Error("invalid partial reset-credit operation ledger schema");
|
|
684
|
+
}
|
|
685
|
+
const rows = database.query<ResetCreditOperationRow, []>(SELECT_ALL).all();
|
|
686
|
+
if (rows.length > MAX_RESET_CREDIT_OPERATION_ACCOUNTS) {
|
|
687
|
+
throw new Error("invalid reset-credit operation ledger capacity");
|
|
688
|
+
}
|
|
689
|
+
const accountKeys = new Set<string>();
|
|
690
|
+
const operationIds = new Set<string>();
|
|
691
|
+
const records = new Map<string, ResetCreditOperationRecord>();
|
|
692
|
+
for (const row of rows) {
|
|
693
|
+
const record = parseRecord(row);
|
|
694
|
+
const ids = record ? [record.operationId, ...(record.joinedOperationId ? [record.joinedOperationId] : [])] : [];
|
|
695
|
+
if (!record || accountKeys.has(record.accountKey) || ids.some(id => operationIds.has(id))) {
|
|
696
|
+
throw new Error("invalid reset-credit operation ledger state");
|
|
697
|
+
}
|
|
698
|
+
accountKeys.add(record.accountKey);
|
|
699
|
+
records.set(record.accountKey, record);
|
|
700
|
+
for (const id of ids) operationIds.add(id);
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
const manualTableCreated = ensureManualIdTable(database, primaryInitialization !== "existing");
|
|
704
|
+
if (manualTableCreated) {
|
|
705
|
+
for (const record of records.values()) {
|
|
706
|
+
if (record.operationKind !== "manual") continue;
|
|
707
|
+
const ids = [record.operationId, ...(record.joinedOperationId ? [record.joinedOperationId] : [])];
|
|
708
|
+
for (const operationId of ids) {
|
|
709
|
+
insertManualIdRecord(database, Object.freeze({
|
|
710
|
+
operationId,
|
|
711
|
+
accountKey: record.accountKey,
|
|
712
|
+
canonicalOperationId: record.operationId,
|
|
713
|
+
...(record.code === undefined ? {} : { terminalCode: record.code }),
|
|
714
|
+
createdAt: record.createdAt,
|
|
715
|
+
updatedAt: record.updatedAt,
|
|
716
|
+
}));
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
const manualIdCount = database.query<{ count: unknown }, []>(SELECT_BOUNDED_MANUAL_ID_COUNT)
|
|
722
|
+
.get()?.count;
|
|
723
|
+
if (typeof manualIdCount !== "number" || !Number.isSafeInteger(manualIdCount) || manualIdCount < 0
|
|
724
|
+
|| manualIdCount > MAX_MANUAL_RESET_CREDIT_OPERATION_IDS) {
|
|
725
|
+
throw new Error("invalid manual reset-credit operation identity capacity");
|
|
726
|
+
}
|
|
727
|
+
if (validationScope === "recovery") {
|
|
728
|
+
if (database.query<{ operation_id: unknown }, []>(SELECT_DUPLICATE_RECOVERY_MANUAL_ID).get()) {
|
|
729
|
+
throw new Error("duplicate reset-credit operation ids");
|
|
730
|
+
}
|
|
731
|
+
return Object.freeze({ recordCount: rows.length, manualIdCount });
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
const manualRows = database.query<ManualResetCreditOperationIdRow, []>(SELECT_ALL_MANUAL_IDS).all();
|
|
735
|
+
if (manualRows.length !== manualIdCount) throw new Error("invalid manual reset-credit operation identity state");
|
|
736
|
+
const manualIds = new Map<string, ManualResetCreditOperationIdRecord>();
|
|
737
|
+
for (const row of manualRows) {
|
|
738
|
+
const record = parseManualIdRecord(row);
|
|
739
|
+
if (!record || manualIds.has(record.operationId)) {
|
|
740
|
+
throw new Error("invalid manual reset-credit operation identity state");
|
|
741
|
+
}
|
|
742
|
+
manualIds.set(record.operationId, record);
|
|
743
|
+
}
|
|
744
|
+
for (const record of manualIds.values()) {
|
|
745
|
+
const canonical = manualIds.get(record.canonicalOperationId);
|
|
746
|
+
if (!canonical || canonical.operationId !== canonical.canonicalOperationId
|
|
747
|
+
|| canonical.accountKey !== record.accountKey
|
|
748
|
+
|| canonical.terminalCode !== record.terminalCode) {
|
|
749
|
+
throw new Error("invalid manual reset-credit operation identity state");
|
|
750
|
+
}
|
|
751
|
+
if (record.terminalCode === undefined) {
|
|
752
|
+
const current = records.get(record.accountKey);
|
|
753
|
+
if (!current || current.operationKind !== "manual" || isTerminal(current)
|
|
754
|
+
|| current.operationId !== record.canonicalOperationId) {
|
|
755
|
+
throw new Error("invalid manual reset-credit operation identity state");
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
for (const record of records.values()) {
|
|
760
|
+
if (record.operationKind === "recovery") {
|
|
761
|
+
if (manualIds.has(record.operationId)) {
|
|
762
|
+
throw new Error("duplicate reset-credit operation ids");
|
|
763
|
+
}
|
|
764
|
+
continue;
|
|
765
|
+
}
|
|
766
|
+
const expectedIds = [record.operationId, ...(record.joinedOperationId ? [record.joinedOperationId] : [])];
|
|
767
|
+
for (const operationId of expectedIds) {
|
|
768
|
+
const identity = manualIds.get(operationId);
|
|
769
|
+
if (!identity || identity.accountKey !== record.accountKey
|
|
770
|
+
|| identity.canonicalOperationId !== record.operationId
|
|
771
|
+
|| identity.terminalCode !== record.code) {
|
|
772
|
+
throw new Error("invalid manual reset-credit operation identity state");
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
return Object.freeze({ recordCount: rows.length, manualIdCount });
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
function readRecord(database: Database, key: string): ResetCreditOperationRecord | undefined {
|
|
780
|
+
const rows = database.query<ResetCreditOperationRow, [string]>(SELECT_BY_KEY).all(key);
|
|
781
|
+
if (rows.length > 1) throw new Error("duplicate reset-credit operation records");
|
|
782
|
+
const row = rows[0];
|
|
783
|
+
const record = parseRecord(row ?? null);
|
|
784
|
+
if (row && !record) throw new Error("invalid reset-credit operation record");
|
|
785
|
+
return record;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
function readManualIdRecord(
|
|
789
|
+
database: Database,
|
|
790
|
+
operationId: string,
|
|
791
|
+
): ManualResetCreditOperationIdRecord | undefined {
|
|
792
|
+
const rows = database.query<ManualResetCreditOperationIdRow, [string]>(SELECT_MANUAL_ID)
|
|
793
|
+
.all(operationId);
|
|
794
|
+
if (rows.length > 1) throw new Error("duplicate manual reset-credit operation ids");
|
|
795
|
+
const row = rows[0];
|
|
796
|
+
const record = parseManualIdRecord(row ?? null);
|
|
797
|
+
if (row && !record) throw new Error("invalid manual reset-credit operation identity");
|
|
798
|
+
return record;
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
function sameManualIdRecord(
|
|
802
|
+
left: ManualResetCreditOperationIdRecord,
|
|
803
|
+
right: ManualResetCreditOperationIdRecord,
|
|
804
|
+
): boolean {
|
|
805
|
+
return left.operationId === right.operationId
|
|
806
|
+
&& left.accountKey === right.accountKey
|
|
807
|
+
&& left.canonicalOperationId === right.canonicalOperationId
|
|
808
|
+
&& left.terminalCode === right.terminalCode
|
|
809
|
+
&& left.createdAt === right.createdAt
|
|
810
|
+
&& left.updatedAt === right.updatedAt;
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
function assertStoredManualIdRecord(
|
|
814
|
+
database: Database,
|
|
815
|
+
expected: ManualResetCreditOperationIdRecord,
|
|
816
|
+
): void {
|
|
817
|
+
const stored = readManualIdRecord(database, expected.operationId);
|
|
818
|
+
if (!stored || !sameManualIdRecord(stored, expected)) {
|
|
819
|
+
throw new Error("manual reset-credit operation identity write did not persist");
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
function insertManualIdRecord(
|
|
824
|
+
database: Database,
|
|
825
|
+
record: ManualResetCreditOperationIdRecord,
|
|
826
|
+
): void {
|
|
827
|
+
const result = database.query(INSERT_MANUAL_ID).run(
|
|
828
|
+
record.operationId,
|
|
829
|
+
record.accountKey,
|
|
830
|
+
record.canonicalOperationId,
|
|
831
|
+
record.terminalCode ?? null,
|
|
832
|
+
record.createdAt,
|
|
833
|
+
record.updatedAt,
|
|
834
|
+
);
|
|
835
|
+
if (result.changes !== 1) throw new Error("manual reset-credit operation identity insert failed");
|
|
836
|
+
assertStoredManualIdRecord(database, record);
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
function operationOwner(database: Database, operationId: string): string | undefined {
|
|
840
|
+
const rows = database.query<{ account_key: unknown }, [string, string, string]>(SELECT_KEY_BY_OPERATION_ID)
|
|
841
|
+
.all(operationId, operationId, operationId);
|
|
842
|
+
if (rows.length > 1) throw new Error("duplicate reset-credit operation ids");
|
|
843
|
+
const owner = rows[0]?.account_key;
|
|
844
|
+
if (owner !== undefined && (typeof owner !== "string" || !ACCOUNT_KEY_PATTERN.test(owner))) {
|
|
845
|
+
throw new Error("invalid reset-credit operation owner");
|
|
846
|
+
}
|
|
847
|
+
return owner;
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
function sameRecord(left: ResetCreditOperationRecord, right: ResetCreditOperationRecord): boolean {
|
|
851
|
+
return left.accountKey === right.accountKey
|
|
852
|
+
&& left.operationKind === right.operationKind
|
|
853
|
+
&& left.credentialGeneration === right.credentialGeneration
|
|
854
|
+
&& left.exhaustionGeneration === right.exhaustionGeneration
|
|
855
|
+
&& left.operationId === right.operationId
|
|
856
|
+
&& left.joinedOperationId === right.joinedOperationId
|
|
857
|
+
&& left.state === right.state
|
|
858
|
+
&& left.code === right.code
|
|
859
|
+
&& left.createdAt === right.createdAt
|
|
860
|
+
&& left.updatedAt === right.updatedAt;
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
function assertStoredRecord(
|
|
864
|
+
database: Database,
|
|
865
|
+
expected: ResetCreditOperationRecord,
|
|
866
|
+
): void {
|
|
867
|
+
const stored = readRecord(database, expected.accountKey);
|
|
868
|
+
if (!stored || !sameRecord(stored, expected)) {
|
|
869
|
+
throw new Error("reset-credit operation write did not persist the expected record");
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
function compareGeneration(
|
|
874
|
+
record: ResetCreditOperationRecord,
|
|
875
|
+
generation: CodexResetCreditRecoveryGeneration,
|
|
876
|
+
): -1 | 0 | 1 {
|
|
877
|
+
return compareCodexResetCreditRecoveryGenerationOrder({
|
|
878
|
+
accountId: generation.accountId,
|
|
879
|
+
credentialGeneration: record.credentialGeneration!,
|
|
880
|
+
exhaustionGeneration: record.exhaustionGeneration!,
|
|
881
|
+
}, generation);
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
function isTerminal(record: ResetCreditOperationRecord): boolean {
|
|
885
|
+
return record.state === "confirmed" || record.state === "stopped";
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
function isThenable(value: unknown): boolean {
|
|
889
|
+
return (typeof value === "object" && value !== null) || typeof value === "function"
|
|
890
|
+
? typeof (value as { then?: unknown }).then === "function"
|
|
891
|
+
: false;
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
type Synchronous<T> = T extends PromiseLike<unknown> ? never : T;
|
|
895
|
+
|
|
896
|
+
function withLedger<T>(validationScope: ResetCreditOperationKind, operation: (
|
|
897
|
+
database: Database,
|
|
898
|
+
recordCount: number,
|
|
899
|
+
manualIdCount: number,
|
|
900
|
+
) => Synchronous<T>): T {
|
|
901
|
+
const path = prepareConfigMutationDatabasePathForWrite();
|
|
902
|
+
let database: Database | undefined;
|
|
903
|
+
let transactionOpen = false;
|
|
904
|
+
try {
|
|
905
|
+
database = new Database(path, { create: true });
|
|
906
|
+
try { chmodSync(path, 0o600); } catch { /* platform may ignore chmod */ }
|
|
907
|
+
database.exec("PRAGMA trusted_schema = OFF; PRAGMA busy_timeout = 0; PRAGMA synchronous = FULL; BEGIN IMMEDIATE");
|
|
908
|
+
transactionOpen = true;
|
|
909
|
+
initializeConfigGeneration(database);
|
|
910
|
+
const counts = initializeTable(database, validationScope);
|
|
911
|
+
const value = operation(database, counts.recordCount, counts.manualIdCount);
|
|
912
|
+
if (isThenable(value) || !database.inTransaction) {
|
|
913
|
+
throw new Error("reset-credit operation ledger work escaped its synchronous transaction");
|
|
914
|
+
}
|
|
915
|
+
database.exec("COMMIT");
|
|
916
|
+
transactionOpen = false;
|
|
917
|
+
return value;
|
|
918
|
+
} catch (error) {
|
|
919
|
+
if (transactionOpen) {
|
|
920
|
+
try { database?.exec("ROLLBACK"); } catch { /* close still releases the write lock */ }
|
|
921
|
+
transactionOpen = false;
|
|
922
|
+
}
|
|
923
|
+
throw error;
|
|
924
|
+
} finally {
|
|
925
|
+
try { database?.close(); } catch { /* operation already completed */ }
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
function isLedgerBusyError(error: unknown): boolean {
|
|
930
|
+
const code = error && typeof error === "object" && "code" in error
|
|
931
|
+
? String((error as { code?: unknown }).code)
|
|
932
|
+
: "";
|
|
933
|
+
const message = error instanceof Error ? error.message : "";
|
|
934
|
+
return code === "SQLITE_BUSY" || code === "SQLITE_LOCKED"
|
|
935
|
+
|| /database (?:is|table is) locked/i.test(message);
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
function warnLedgerUnavailable(error: unknown): void {
|
|
939
|
+
if (isLedgerBusyError(error)) return;
|
|
940
|
+
const nested = error instanceof NestedConfigMutationError;
|
|
941
|
+
// Native SQLite and filesystem errors may contain absolute, account-bearing
|
|
942
|
+
// paths. Keep this warning categorical rather than forwarding error.message.
|
|
943
|
+
console.warn(nested
|
|
944
|
+
? "[opencodex] Reset-credit operation ledger refused a nested config mutation."
|
|
945
|
+
: "[opencodex] Reset-credit operation ledger is unavailable.");
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
function reportManualHistoryCapacity(count: number): void {
|
|
949
|
+
const level = count >= MAX_MANUAL_RESET_CREDIT_OPERATION_IDS
|
|
950
|
+
? MAX_MANUAL_RESET_CREDIT_OPERATION_IDS
|
|
951
|
+
: count >= MANUAL_RESET_CREDIT_HISTORY_HIGH_WATER_MARK
|
|
952
|
+
? MANUAL_RESET_CREDIT_HISTORY_HIGH_WATER_MARK
|
|
953
|
+
: 0;
|
|
954
|
+
if (level === 0 || level <= reportedManualHistoryLevel) return;
|
|
955
|
+
reportedManualHistoryLevel = level;
|
|
956
|
+
try {
|
|
957
|
+
console.warn(
|
|
958
|
+
`[opencodex] Reset-credit manual operation history is at ${count}/${MAX_MANUAL_RESET_CREDIT_OPERATION_IDS} entries${
|
|
959
|
+
level === MAX_MANUAL_RESET_CREDIT_OPERATION_IDS
|
|
960
|
+
? "; new manual operation IDs, including aliases, are disabled until a maintainer expands capacity or applies an approved retirement policy."
|
|
961
|
+
: "."
|
|
962
|
+
}`,
|
|
963
|
+
);
|
|
964
|
+
} catch {
|
|
965
|
+
// Count-only operational reporting must never weaken the fail-closed result.
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
/**
|
|
970
|
+
* Throws `TypeError` for a malformed generation or timestamp. Runtime storage
|
|
971
|
+
* and contention failures are represented by a result kind.
|
|
972
|
+
*/
|
|
973
|
+
export function openResetCreditOperation(
|
|
974
|
+
generation: CodexResetCreditRecoveryGeneration,
|
|
975
|
+
now = Date.now(),
|
|
976
|
+
): OpenResetCreditOperationResult {
|
|
977
|
+
const generationSnapshot = snapshotCodexResetCreditRecoveryGeneration(generation);
|
|
978
|
+
if (!Number.isSafeInteger(now) || now < 0) throw new TypeError("invalid reset-credit operation timestamp");
|
|
979
|
+
try {
|
|
980
|
+
return withLedger("recovery", (database, recordCount) => {
|
|
981
|
+
const key = accountKey(generationSnapshot.accountId);
|
|
982
|
+
const current = readRecord(database, key);
|
|
983
|
+
if (current) {
|
|
984
|
+
if (current.operationKind !== "recovery") {
|
|
985
|
+
return Object.freeze({ kind: "unresolved-prior-generation" as const });
|
|
986
|
+
}
|
|
987
|
+
const comparison = compareGeneration(current, generationSnapshot);
|
|
988
|
+
if (comparison > 0) return Object.freeze({ kind: "stale-generation" as const });
|
|
989
|
+
if (comparison === 0) {
|
|
990
|
+
if (isTerminal(current)) {
|
|
991
|
+
return Object.freeze({
|
|
992
|
+
kind: "terminal" as const,
|
|
993
|
+
operationId: current.operationId as CodexReservedOperationId,
|
|
994
|
+
code: current.code!,
|
|
995
|
+
});
|
|
996
|
+
}
|
|
997
|
+
return Object.freeze({
|
|
998
|
+
kind: "execute" as const,
|
|
999
|
+
operationId: current.operationId as CodexReservedOperationId,
|
|
1000
|
+
resumed: true,
|
|
1001
|
+
});
|
|
1002
|
+
}
|
|
1003
|
+
if (!isTerminal(current)) return Object.freeze({ kind: "unresolved-prior-generation" as const });
|
|
1004
|
+
} else if (recordCount >= MAX_RESET_CREDIT_OPERATION_ACCOUNTS) {
|
|
1005
|
+
return Object.freeze({ kind: "capacity" as const });
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
const operationId = randomUUID();
|
|
1009
|
+
if (!isCodexResetCreditOperationId(operationId)) throw new Error("runtime generated invalid UUID");
|
|
1010
|
+
if (operationOwner(database, operationId) !== undefined) {
|
|
1011
|
+
throw new Error("duplicate reset-credit operation ids");
|
|
1012
|
+
}
|
|
1013
|
+
const values = [
|
|
1014
|
+
"recovery",
|
|
1015
|
+
generationSnapshot.credentialGeneration,
|
|
1016
|
+
generationSnapshot.exhaustionGeneration,
|
|
1017
|
+
operationId,
|
|
1018
|
+
null,
|
|
1019
|
+
"pending",
|
|
1020
|
+
null,
|
|
1021
|
+
now,
|
|
1022
|
+
now,
|
|
1023
|
+
] as const;
|
|
1024
|
+
const result = current
|
|
1025
|
+
? database.query(REPLACE_RECORD).run(...values, key)
|
|
1026
|
+
: database.query(INSERT_RECORD).run(key, ...values);
|
|
1027
|
+
if (result.changes !== 1) throw new Error("reset-credit operation reservation lost ownership");
|
|
1028
|
+
assertStoredRecord(database, Object.freeze({
|
|
1029
|
+
accountKey: key,
|
|
1030
|
+
operationKind: "recovery",
|
|
1031
|
+
credentialGeneration: generationSnapshot.credentialGeneration,
|
|
1032
|
+
exhaustionGeneration: generationSnapshot.exhaustionGeneration,
|
|
1033
|
+
operationId,
|
|
1034
|
+
state: "pending",
|
|
1035
|
+
createdAt: now,
|
|
1036
|
+
updatedAt: now,
|
|
1037
|
+
}));
|
|
1038
|
+
return Object.freeze({
|
|
1039
|
+
kind: "execute" as const,
|
|
1040
|
+
operationId: operationId as CodexReservedOperationId,
|
|
1041
|
+
resumed: false,
|
|
1042
|
+
});
|
|
1043
|
+
});
|
|
1044
|
+
} catch (error) {
|
|
1045
|
+
warnLedgerUnavailable(error);
|
|
1046
|
+
return Object.freeze({ kind: "unavailable" });
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
function updateOperation(
|
|
1051
|
+
owner: Readonly<{
|
|
1052
|
+
accountKey: string;
|
|
1053
|
+
operationKind: ResetCreditOperationKind;
|
|
1054
|
+
credentialGeneration?: number;
|
|
1055
|
+
exhaustionGeneration?: number;
|
|
1056
|
+
}>,
|
|
1057
|
+
operationId: string,
|
|
1058
|
+
update: (record: ResetCreditOperationRecord) => ResetCreditOperationRecord | undefined,
|
|
1059
|
+
afterWrite?: (database: Database, updated: ResetCreditOperationRecord) => void,
|
|
1060
|
+
): UpdateResetCreditOperationResult {
|
|
1061
|
+
if (!isCodexResetCreditOperationId(operationId)) return Object.freeze({ kind: "mismatch" });
|
|
1062
|
+
try {
|
|
1063
|
+
return withLedger(owner.operationKind, database => {
|
|
1064
|
+
const current = readRecord(database, owner.accountKey);
|
|
1065
|
+
if (!current
|
|
1066
|
+
|| current.operationKind !== owner.operationKind
|
|
1067
|
+
|| current.credentialGeneration !== owner.credentialGeneration
|
|
1068
|
+
|| current.exhaustionGeneration !== owner.exhaustionGeneration
|
|
1069
|
+
|| current.operationId !== operationId) {
|
|
1070
|
+
return Object.freeze({ kind: "mismatch" as const });
|
|
1071
|
+
}
|
|
1072
|
+
const updated = update(current);
|
|
1073
|
+
if (!updated) return Object.freeze({ kind: "mismatch" as const });
|
|
1074
|
+
const result = database.query(UPDATE_RECORD).run(
|
|
1075
|
+
updated.state,
|
|
1076
|
+
updated.code ?? null,
|
|
1077
|
+
updated.updatedAt,
|
|
1078
|
+
owner.accountKey,
|
|
1079
|
+
owner.operationKind,
|
|
1080
|
+
operationId,
|
|
1081
|
+
owner.credentialGeneration ?? null,
|
|
1082
|
+
owner.exhaustionGeneration ?? null,
|
|
1083
|
+
);
|
|
1084
|
+
if (result.changes !== 1) throw new Error("reset-credit operation update lost ownership");
|
|
1085
|
+
assertStoredRecord(database, updated);
|
|
1086
|
+
afterWrite?.(database, updated);
|
|
1087
|
+
return Object.freeze({ kind: "updated" as const });
|
|
1088
|
+
});
|
|
1089
|
+
} catch (error) {
|
|
1090
|
+
warnLedgerUnavailable(error);
|
|
1091
|
+
return Object.freeze({ kind: "unavailable" });
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
/**
|
|
1096
|
+
* Throws `TypeError` for a malformed generation or timestamp. An invalid
|
|
1097
|
+
* operation id returns `mismatch`; runtime storage failures return `unavailable`.
|
|
1098
|
+
*/
|
|
1099
|
+
export function markResetCreditOperationAmbiguous(
|
|
1100
|
+
generation: CodexResetCreditRecoveryGeneration,
|
|
1101
|
+
operationId: string,
|
|
1102
|
+
now = Date.now(),
|
|
1103
|
+
): UpdateResetCreditOperationResult {
|
|
1104
|
+
if (!Number.isSafeInteger(now) || now < 0) throw new TypeError("invalid reset-credit operation timestamp");
|
|
1105
|
+
const generationSnapshot = snapshotCodexResetCreditRecoveryGeneration(generation);
|
|
1106
|
+
return updateOperation({
|
|
1107
|
+
accountKey: accountKey(generationSnapshot.accountId),
|
|
1108
|
+
operationKind: "recovery",
|
|
1109
|
+
credentialGeneration: generationSnapshot.credentialGeneration,
|
|
1110
|
+
exhaustionGeneration: generationSnapshot.exhaustionGeneration,
|
|
1111
|
+
}, operationId, record => {
|
|
1112
|
+
if (isTerminal(record)) return undefined;
|
|
1113
|
+
return Object.freeze({
|
|
1114
|
+
...record,
|
|
1115
|
+
state: "ambiguous",
|
|
1116
|
+
code: undefined,
|
|
1117
|
+
updatedAt: Math.max(record.updatedAt, now),
|
|
1118
|
+
});
|
|
1119
|
+
});
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
/**
|
|
1123
|
+
* Throws `TypeError` for a malformed generation or timestamp. An invalid
|
|
1124
|
+
* operation id or non-terminal code returns `mismatch`; runtime storage
|
|
1125
|
+
* failures return `unavailable`.
|
|
1126
|
+
*/
|
|
1127
|
+
export function settleResetCreditOperation(
|
|
1128
|
+
generation: CodexResetCreditRecoveryGeneration,
|
|
1129
|
+
operationId: string,
|
|
1130
|
+
code: CodexResetCreditConsumeCode,
|
|
1131
|
+
now = Date.now(),
|
|
1132
|
+
): UpdateResetCreditOperationResult {
|
|
1133
|
+
if (!Number.isSafeInteger(now) || now < 0) throw new TypeError("invalid reset-credit operation timestamp");
|
|
1134
|
+
if (!Object.prototype.hasOwnProperty.call(TERMINAL_STATE_BY_CODE, code)) {
|
|
1135
|
+
return Object.freeze({ kind: "mismatch" });
|
|
1136
|
+
}
|
|
1137
|
+
const generationSnapshot = snapshotCodexResetCreditRecoveryGeneration(generation);
|
|
1138
|
+
return updateOperation({
|
|
1139
|
+
accountKey: accountKey(generationSnapshot.accountId),
|
|
1140
|
+
operationKind: "recovery",
|
|
1141
|
+
credentialGeneration: generationSnapshot.credentialGeneration,
|
|
1142
|
+
exhaustionGeneration: generationSnapshot.exhaustionGeneration,
|
|
1143
|
+
}, operationId, record => {
|
|
1144
|
+
if (isTerminal(record)) return record.code === code ? record : undefined;
|
|
1145
|
+
return Object.freeze({
|
|
1146
|
+
...record,
|
|
1147
|
+
state: TERMINAL_STATE_BY_CODE[code],
|
|
1148
|
+
code,
|
|
1149
|
+
updatedAt: Math.max(record.updatedAt, now),
|
|
1150
|
+
});
|
|
1151
|
+
});
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
function snapshotManualIdentity(identity: ManualResetCreditOperationIdentity): {
|
|
1155
|
+
accountKey: string;
|
|
1156
|
+
operationId: string;
|
|
1157
|
+
} {
|
|
1158
|
+
if (!identity || typeof identity !== "object" || Array.isArray(identity)) {
|
|
1159
|
+
throw new TypeError("manual reset-credit identity must be an object");
|
|
1160
|
+
}
|
|
1161
|
+
const value = identity as unknown as Record<string, unknown>;
|
|
1162
|
+
const hasOwn = Object.prototype.hasOwnProperty;
|
|
1163
|
+
if (!hasOwn.call(value, "accountId")
|
|
1164
|
+
|| !hasOwn.call(value, "chatgptAccountId")
|
|
1165
|
+
|| !hasOwn.call(value, "operationId")) {
|
|
1166
|
+
throw new TypeError("manual reset-credit identity fields must be own properties");
|
|
1167
|
+
}
|
|
1168
|
+
const accountId = value.accountId;
|
|
1169
|
+
const chatgptAccountId = value.chatgptAccountId;
|
|
1170
|
+
const operationId = value.operationId;
|
|
1171
|
+
if (!isCodexResetCreditOperationId(operationId)) {
|
|
1172
|
+
throw new TypeError("invalid manual reset-credit operation id");
|
|
1173
|
+
}
|
|
1174
|
+
if (typeof accountId !== "string") throw new TypeError("invalid manual reset-credit account");
|
|
1175
|
+
validateManualAccountId(accountId);
|
|
1176
|
+
if (typeof chatgptAccountId !== "string") {
|
|
1177
|
+
throw new TypeError("invalid manual reset-credit credential identity");
|
|
1178
|
+
}
|
|
1179
|
+
return Object.freeze({
|
|
1180
|
+
accountKey: manualPhysicalAccountKey(chatgptAccountId),
|
|
1181
|
+
operationId,
|
|
1182
|
+
});
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
/**
|
|
1186
|
+
* Reserve or restore one explicit manual redemption intent.
|
|
1187
|
+
*
|
|
1188
|
+
* Throws `TypeError` for malformed identity fields or `now`; these are caller
|
|
1189
|
+
* contract violations. Durable-state and runtime failures return a result kind.
|
|
1190
|
+
*/
|
|
1191
|
+
export function openManualResetCreditOperation(
|
|
1192
|
+
identity: ManualResetCreditOperationIdentity,
|
|
1193
|
+
now = Date.now(),
|
|
1194
|
+
): OpenManualResetCreditOperationResult {
|
|
1195
|
+
const owner = snapshotManualIdentity(identity);
|
|
1196
|
+
if (!Number.isSafeInteger(now) || now < 0) throw new TypeError("invalid reset-credit operation timestamp");
|
|
1197
|
+
try {
|
|
1198
|
+
return withLedger("manual", (database, recordCount, manualIdCount) => {
|
|
1199
|
+
reportManualHistoryCapacity(manualIdCount);
|
|
1200
|
+
const admitNewCallerId = () => {
|
|
1201
|
+
if (manualIdCount >= MAX_MANUAL_RESET_CREDIT_OPERATION_IDS) {
|
|
1202
|
+
return Object.freeze({ kind: "capacity" as const });
|
|
1203
|
+
}
|
|
1204
|
+
const existingOwner = operationOwner(database, owner.operationId);
|
|
1205
|
+
if (existingOwner !== undefined) {
|
|
1206
|
+
return Object.freeze({
|
|
1207
|
+
kind: existingOwner === owner.accountKey ? "unavailable" as const : "identity-mismatch" as const,
|
|
1208
|
+
});
|
|
1209
|
+
}
|
|
1210
|
+
return undefined;
|
|
1211
|
+
};
|
|
1212
|
+
|
|
1213
|
+
const reserve = (replaceCurrent: boolean): OpenManualResetCreditOperationResult => {
|
|
1214
|
+
const rejected = admitNewCallerId();
|
|
1215
|
+
if (rejected) return rejected;
|
|
1216
|
+
|
|
1217
|
+
const record: ResetCreditOperationRecord = Object.freeze({
|
|
1218
|
+
accountKey: owner.accountKey,
|
|
1219
|
+
operationKind: "manual",
|
|
1220
|
+
operationId: owner.operationId,
|
|
1221
|
+
state: "pending",
|
|
1222
|
+
createdAt: now,
|
|
1223
|
+
updatedAt: now,
|
|
1224
|
+
});
|
|
1225
|
+
const values = [
|
|
1226
|
+
"manual",
|
|
1227
|
+
null,
|
|
1228
|
+
null,
|
|
1229
|
+
owner.operationId,
|
|
1230
|
+
null,
|
|
1231
|
+
"pending",
|
|
1232
|
+
null,
|
|
1233
|
+
now,
|
|
1234
|
+
now,
|
|
1235
|
+
] as const;
|
|
1236
|
+
const result = replaceCurrent
|
|
1237
|
+
? database.query(REPLACE_RECORD).run(...values, owner.accountKey)
|
|
1238
|
+
: database.query(INSERT_RECORD).run(owner.accountKey, ...values);
|
|
1239
|
+
if (result.changes !== 1) throw new Error("manual reset-credit reservation lost ownership");
|
|
1240
|
+
assertStoredRecord(database, record);
|
|
1241
|
+
insertManualIdRecord(database, Object.freeze({
|
|
1242
|
+
operationId: owner.operationId,
|
|
1243
|
+
accountKey: owner.accountKey,
|
|
1244
|
+
canonicalOperationId: owner.operationId,
|
|
1245
|
+
createdAt: now,
|
|
1246
|
+
updatedAt: now,
|
|
1247
|
+
}));
|
|
1248
|
+
return Object.freeze({
|
|
1249
|
+
kind: "execute" as const,
|
|
1250
|
+
operationId: owner.operationId as CodexReservedOperationId,
|
|
1251
|
+
resumed: false,
|
|
1252
|
+
});
|
|
1253
|
+
};
|
|
1254
|
+
|
|
1255
|
+
const knownIdentity = readManualIdRecord(database, owner.operationId);
|
|
1256
|
+
if (knownIdentity) {
|
|
1257
|
+
if (knownIdentity.accountKey !== owner.accountKey) {
|
|
1258
|
+
return Object.freeze({ kind: "identity-mismatch" as const });
|
|
1259
|
+
}
|
|
1260
|
+
if (knownIdentity.terminalCode !== undefined) {
|
|
1261
|
+
return Object.freeze({
|
|
1262
|
+
kind: "terminal" as const,
|
|
1263
|
+
operationId: knownIdentity.canonicalOperationId as CodexReservedOperationId,
|
|
1264
|
+
code: knownIdentity.terminalCode,
|
|
1265
|
+
});
|
|
1266
|
+
}
|
|
1267
|
+
const current = readRecord(database, owner.accountKey);
|
|
1268
|
+
if (!current || current.operationKind !== "manual" || isTerminal(current)
|
|
1269
|
+
|| current.operationId !== knownIdentity.canonicalOperationId) {
|
|
1270
|
+
throw new Error("manual reset-credit operation identity lost its active owner");
|
|
1271
|
+
}
|
|
1272
|
+
return Object.freeze({
|
|
1273
|
+
kind: "execute" as const,
|
|
1274
|
+
operationId: current.operationId as CodexReservedOperationId,
|
|
1275
|
+
resumed: true,
|
|
1276
|
+
});
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
const current = readRecord(database, owner.accountKey);
|
|
1280
|
+
if (current) {
|
|
1281
|
+
if (current.operationKind !== "manual") {
|
|
1282
|
+
return Object.freeze({ kind: "unavailable" as const });
|
|
1283
|
+
}
|
|
1284
|
+
if (!isTerminal(current)) {
|
|
1285
|
+
const rejected = admitNewCallerId();
|
|
1286
|
+
if (rejected) return rejected;
|
|
1287
|
+
insertManualIdRecord(database, Object.freeze({
|
|
1288
|
+
operationId: owner.operationId,
|
|
1289
|
+
accountKey: owner.accountKey,
|
|
1290
|
+
canonicalOperationId: current.operationId,
|
|
1291
|
+
createdAt: now,
|
|
1292
|
+
updatedAt: now,
|
|
1293
|
+
}));
|
|
1294
|
+
const joined: ResetCreditOperationRecord = Object.freeze({
|
|
1295
|
+
...current,
|
|
1296
|
+
...(current.joinedOperationId === undefined
|
|
1297
|
+
? { joinedOperationId: owner.operationId }
|
|
1298
|
+
: {}),
|
|
1299
|
+
updatedAt: Math.max(current.updatedAt, now),
|
|
1300
|
+
});
|
|
1301
|
+
const result = current.joinedOperationId === undefined
|
|
1302
|
+
? database.query(JOIN_MANUAL_OPERATION).run(
|
|
1303
|
+
owner.operationId,
|
|
1304
|
+
joined.updatedAt,
|
|
1305
|
+
owner.accountKey,
|
|
1306
|
+
current.operationId,
|
|
1307
|
+
)
|
|
1308
|
+
: database.query(TOUCH_MANUAL_OPERATION).run(
|
|
1309
|
+
joined.updatedAt,
|
|
1310
|
+
owner.accountKey,
|
|
1311
|
+
current.operationId,
|
|
1312
|
+
);
|
|
1313
|
+
if (result.changes !== 1) {
|
|
1314
|
+
throw new Error("manual reset-credit join lost ownership");
|
|
1315
|
+
}
|
|
1316
|
+
assertStoredRecord(database, joined);
|
|
1317
|
+
// The upstream request keeps the original durable id. Every caller id
|
|
1318
|
+
// is retained in the identity history; the first alias is also kept on
|
|
1319
|
+
// the current row for compatibility with the previous schema.
|
|
1320
|
+
return Object.freeze({
|
|
1321
|
+
kind: "execute" as const,
|
|
1322
|
+
operationId: current.operationId as CodexReservedOperationId,
|
|
1323
|
+
resumed: true,
|
|
1324
|
+
});
|
|
1325
|
+
}
|
|
1326
|
+
// Deliberate: a distinct caller id after a settled intent represents a
|
|
1327
|
+
// new explicit redemption. Prior ids remain immutable in the history,
|
|
1328
|
+
// so a delayed retry can never be reclassified as this new intent.
|
|
1329
|
+
return reserve(true);
|
|
1330
|
+
}
|
|
1331
|
+
if (recordCount >= MAX_RESET_CREDIT_OPERATION_ACCOUNTS) {
|
|
1332
|
+
return Object.freeze({ kind: "capacity" as const });
|
|
1333
|
+
}
|
|
1334
|
+
return reserve(false);
|
|
1335
|
+
});
|
|
1336
|
+
} catch (error) {
|
|
1337
|
+
warnLedgerUnavailable(error);
|
|
1338
|
+
return Object.freeze({ kind: "unavailable" });
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
/**
|
|
1343
|
+
* Mark a reserved manual redemption as ambiguous.
|
|
1344
|
+
*
|
|
1345
|
+
* Throws `TypeError` for malformed identity fields or `now`. A missing or
|
|
1346
|
+
* incompatible durable record returns the existing result kind.
|
|
1347
|
+
*/
|
|
1348
|
+
export function markManualResetCreditOperationAmbiguous(
|
|
1349
|
+
identity: ManualResetCreditOperationIdentity,
|
|
1350
|
+
now = Date.now(),
|
|
1351
|
+
): UpdateResetCreditOperationResult {
|
|
1352
|
+
const owner = snapshotManualIdentity(identity);
|
|
1353
|
+
if (!Number.isSafeInteger(now) || now < 0) throw new TypeError("invalid reset-credit operation timestamp");
|
|
1354
|
+
return updateOperation({ accountKey: owner.accountKey, operationKind: "manual" }, owner.operationId, record => {
|
|
1355
|
+
if (isTerminal(record)) return undefined;
|
|
1356
|
+
return Object.freeze({ ...record, state: "ambiguous", code: undefined, updatedAt: Math.max(record.updatedAt, now) });
|
|
1357
|
+
});
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
/**
|
|
1361
|
+
* Settle a reserved manual redemption with one terminal consume code.
|
|
1362
|
+
*
|
|
1363
|
+
* Throws `TypeError` for malformed identity fields or `now`; an unsupported
|
|
1364
|
+
* code or incompatible durable record returns `mismatch`.
|
|
1365
|
+
*/
|
|
1366
|
+
export function settleManualResetCreditOperation(
|
|
1367
|
+
identity: ManualResetCreditOperationIdentity,
|
|
1368
|
+
code: CodexResetCreditConsumeCode,
|
|
1369
|
+
now = Date.now(),
|
|
1370
|
+
): UpdateResetCreditOperationResult {
|
|
1371
|
+
const owner = snapshotManualIdentity(identity);
|
|
1372
|
+
if (!Number.isSafeInteger(now) || now < 0) throw new TypeError("invalid reset-credit operation timestamp");
|
|
1373
|
+
if (!Object.prototype.hasOwnProperty.call(TERMINAL_STATE_BY_CODE, code)) {
|
|
1374
|
+
return Object.freeze({ kind: "mismatch" });
|
|
1375
|
+
}
|
|
1376
|
+
return updateOperation({ accountKey: owner.accountKey, operationKind: "manual" }, owner.operationId, record => {
|
|
1377
|
+
if (isTerminal(record)) return record.code === code ? record : undefined;
|
|
1378
|
+
return Object.freeze({
|
|
1379
|
+
...record,
|
|
1380
|
+
state: TERMINAL_STATE_BY_CODE[code],
|
|
1381
|
+
code,
|
|
1382
|
+
updatedAt: Math.max(record.updatedAt, now),
|
|
1383
|
+
});
|
|
1384
|
+
}, (database, updated) => {
|
|
1385
|
+
const result = database.query(SETTLE_MANUAL_IDS).run(
|
|
1386
|
+
code,
|
|
1387
|
+
updated.updatedAt,
|
|
1388
|
+
owner.accountKey,
|
|
1389
|
+
owner.operationId,
|
|
1390
|
+
code,
|
|
1391
|
+
);
|
|
1392
|
+
if (result.changes < 1) {
|
|
1393
|
+
throw new Error("manual reset-credit terminal identity update lost ownership");
|
|
1394
|
+
}
|
|
1395
|
+
const rows = database.query<ManualResetCreditOperationIdRow, [string, string]>(
|
|
1396
|
+
SELECT_MANUAL_IDS_BY_CANONICAL,
|
|
1397
|
+
).all(owner.accountKey, owner.operationId);
|
|
1398
|
+
if (rows.length < 1 || rows.length > MAX_MANUAL_RESET_CREDIT_OPERATION_IDS) {
|
|
1399
|
+
throw new Error("invalid manual reset-credit terminal identity set");
|
|
1400
|
+
}
|
|
1401
|
+
for (const row of rows) {
|
|
1402
|
+
const stored = parseManualIdRecord(row);
|
|
1403
|
+
if (!stored || stored.accountKey !== owner.accountKey
|
|
1404
|
+
|| stored.canonicalOperationId !== owner.operationId
|
|
1405
|
+
|| stored.terminalCode !== code
|
|
1406
|
+
|| stored.updatedAt !== updated.updatedAt) {
|
|
1407
|
+
throw new Error("manual reset-credit terminal identity write did not persist");
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
});
|
|
1411
|
+
}
|