osdy-pi 1.11.11 → 1.11.13
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/README.md +33 -12
- package/extensions/osdy-pi/assets/PROVENANCE.md +21 -0
- package/extensions/osdy-pi/assets/notification-default.wav +0 -0
- package/extensions/osdy-pi/audio-default-sound.test.ts +130 -0
- package/extensions/osdy-pi/audio-default-wizard.test.ts +35 -0
- package/extensions/osdy-pi/audio-master-mute.test.ts +103 -0
- package/extensions/osdy-pi/audio-notification-config.ts +15 -2
- package/extensions/osdy-pi/audio-notification-service.ts +6 -3
- package/extensions/osdy-pi/audio-notification-types.ts +2 -1
- package/extensions/osdy-pi/audio-sound-settings.ts +3 -1
- package/extensions/osdy-pi/codex-reset-store.test.ts +236 -0
- package/extensions/osdy-pi/codex-reset-store.ts +174 -0
- package/extensions/osdy-pi/codex-reset-ui.test.ts +70 -0
- package/extensions/osdy-pi/codex-reset-ui.ts +52 -0
- package/extensions/osdy-pi/codex-reset.test.ts +525 -0
- package/extensions/osdy-pi/codex-reset.ts +213 -0
- package/extensions/osdy-pi/codex-usage-ui.test.ts +132 -2
- package/extensions/osdy-pi/codex-usage-ui.ts +75 -30
- package/extensions/osdy-pi/codex-usage.test.ts +370 -1
- package/extensions/osdy-pi/codex-usage.ts +198 -19
- package/extensions/osdy-pi/control-center-git.test.ts +2 -0
- package/extensions/osdy-pi/control-center-sounds.test.ts +25 -8
- package/extensions/osdy-pi/control-center-sounds.ts +25 -16
- package/extensions/osdy-pi/control-center-usage.test.ts +96 -3
- package/extensions/osdy-pi/control-center-usage.ts +15 -4
- package/extensions/osdy-pi/control-center.test.ts +51 -2
- package/extensions/osdy-pi/control-center.ts +16 -3
- package/extensions/osdy-pi/profile-codex-usage.test.ts +34 -0
- package/extensions/osdy-pi/runtime.test.ts +289 -0
- package/extensions/osdy-pi/runtime.ts +98 -2
- package/extensions/osdy-pi/sound-setup-wizard.test.ts +33 -0
- package/extensions/osdy-pi/sound-setup-wizard.ts +9 -7
- package/package.json +2 -2
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { existsSync } from "node:fs";
|
|
4
|
+
import { chmod, mkdtemp, open, readFile, readdir, rm, stat, symlink, writeFile } from "node:fs/promises";
|
|
5
|
+
import type { FileHandle } from "node:fs/promises";
|
|
6
|
+
import { registerHooks } from "node:module";
|
|
7
|
+
import { tmpdir } from "node:os";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
import test from "node:test";
|
|
10
|
+
import { fileURLToPath } from "node:url";
|
|
11
|
+
|
|
12
|
+
registerHooks({
|
|
13
|
+
resolve(specifier, context, nextResolve) {
|
|
14
|
+
if (specifier.startsWith(".") && specifier.endsWith(".js") && context.parentURL?.endsWith(".ts")) {
|
|
15
|
+
const url = new URL(`${specifier.slice(0, -3)}.ts`, context.parentURL);
|
|
16
|
+
if (existsSync(fileURLToPath(url))) return { shortCircuit: true, url: url.href };
|
|
17
|
+
}
|
|
18
|
+
return nextResolve(specifier, context);
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const { createCodexResetStore } = await import("./codex-reset-store.js");
|
|
23
|
+
const accountId = "synthetic-account/../not-a-path";
|
|
24
|
+
const pending = {
|
|
25
|
+
version: 1 as const, accountId, creditId: "synthetic-credit",
|
|
26
|
+
requestId: "11111111-1111-4111-8111-111111111111", expiresAt: 2_000, status: "pending" as const,
|
|
27
|
+
};
|
|
28
|
+
const basename = createHash("sha256").update(accountId).digest("hex");
|
|
29
|
+
|
|
30
|
+
async function fixture(t: test.TestContext) {
|
|
31
|
+
const directory = await mkdtemp(join(tmpdir(), "osdy-reset-test-"));
|
|
32
|
+
t.after(() => rm(directory, { recursive: true, force: true })); // Only this synthetic fixture.
|
|
33
|
+
const root = join(directory, "journal");
|
|
34
|
+
return { root, store: createCodexResetStore(root) };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
void test("private hashed journals round-trip and resolved attempts permit a new request", async (t) => {
|
|
38
|
+
const { root, store } = await fixture(t);
|
|
39
|
+
const lease = await store.acquire(accountId);
|
|
40
|
+
assert.equal(await lease.read(), undefined);
|
|
41
|
+
await lease.write(pending);
|
|
42
|
+
assert.deepEqual(await lease.read(), pending);
|
|
43
|
+
assert.equal((await stat(root)).mode & 0o777, 0o700);
|
|
44
|
+
for (const name of await readdir(root)) {
|
|
45
|
+
assert.match(name, /^[a-f0-9]{64}\.(json|lock)$/);
|
|
46
|
+
assert.equal((await stat(join(root, name))).mode & 0o777, 0o600);
|
|
47
|
+
}
|
|
48
|
+
assert.equal((await readFile(join(root, `${basename}.json`), "utf8")).includes("accessToken"), false);
|
|
49
|
+
await lease.write({ ...pending, status: "resolved", confirmedCode: "reset" });
|
|
50
|
+
await lease.write({ ...pending, requestId: "22222222-2222-4222-8222-222222222222" });
|
|
51
|
+
await lease.release();
|
|
52
|
+
const restored = await store.acquire(accountId);
|
|
53
|
+
assert.equal((await restored.read())?.requestId, "22222222-2222-4222-8222-222222222222");
|
|
54
|
+
await restored.release();
|
|
55
|
+
assert.deepEqual(await readdir(root), [`${basename}.json`]);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
void test("account leases are exclusive, isolate accounts, and never reclaim existing stale locks", async (t) => {
|
|
59
|
+
const { root, store } = await fixture(t);
|
|
60
|
+
const lease = await store.acquire(accountId);
|
|
61
|
+
await assert.rejects(store.acquire(accountId), /^Error: Codex reset journal is unavailable$/);
|
|
62
|
+
const other = await store.acquire("synthetic-other");
|
|
63
|
+
assert.equal(await other.read(), undefined);
|
|
64
|
+
await lease.write(pending);
|
|
65
|
+
assert.equal(await other.read(), undefined);
|
|
66
|
+
await other.release();
|
|
67
|
+
await lease.release();
|
|
68
|
+
await writeFile(join(root, `${basename}.lock`), "", { mode: 0o600 });
|
|
69
|
+
await assert.rejects(store.acquire(accountId), /^Error: Codex reset journal is unavailable$/);
|
|
70
|
+
assert.equal(await readFile(join(root, `${basename}.lock`), "utf8"), "");
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
for (const content of [
|
|
74
|
+
"{broken", "x".repeat(8_193), JSON.stringify({ ...pending, version: 2 }),
|
|
75
|
+
JSON.stringify({ ...pending, accountId: "wrong-account" }), JSON.stringify({ ...pending, requestId: "bad" }),
|
|
76
|
+
JSON.stringify({ ...pending, expiresAt: -1 }), JSON.stringify({ ...pending, accessToken: "forbidden" }),
|
|
77
|
+
JSON.stringify({ ...pending, status: "resolved", confirmedCode: "invented" }),
|
|
78
|
+
JSON.stringify({ ...pending, status: "pending", confirmedCode: "reset" }),
|
|
79
|
+
]) {
|
|
80
|
+
void test(`rejects corrupt or unbounded journal fixture ${content.length}:${content.slice(0, 12)}`, async (t) => {
|
|
81
|
+
const { root, store } = await fixture(t);
|
|
82
|
+
const lease = await store.acquire(accountId);
|
|
83
|
+
await writeFile(join(root, `${basename}.json`), content, { mode: 0o600 });
|
|
84
|
+
await assert.rejects(lease.read(), /^Error: Codex reset journal is unavailable$/);
|
|
85
|
+
await lease.release();
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
void test("refuses final-file symlinks on both read and atomic write, preserving the target", async (t) => {
|
|
90
|
+
const { root, store } = await fixture(t);
|
|
91
|
+
const lease = await store.acquire(accountId);
|
|
92
|
+
const target = join(root, "synthetic-target");
|
|
93
|
+
await writeFile(target, "untouched", { mode: 0o600 });
|
|
94
|
+
await symlink(target, join(root, `${basename}.json`));
|
|
95
|
+
await assert.rejects(lease.read());
|
|
96
|
+
await assert.rejects(lease.write(pending));
|
|
97
|
+
assert.equal(await readFile(target, "utf8"), "untouched");
|
|
98
|
+
await lease.release();
|
|
99
|
+
assert.equal((await readdir(root)).some((name) => name.endsWith(".tmp")), false);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
void test("refuses symlink roots and non-private journal files or directories", async (t) => {
|
|
103
|
+
const { root, store } = await fixture(t);
|
|
104
|
+
const lease = await store.acquire(accountId);
|
|
105
|
+
await lease.write(pending);
|
|
106
|
+
await chmod(join(root, `${basename}.json`), 0o644);
|
|
107
|
+
await assert.rejects(lease.read());
|
|
108
|
+
await lease.release();
|
|
109
|
+
await chmod(root, 0o755);
|
|
110
|
+
await assert.rejects(store.acquire(accountId));
|
|
111
|
+
const link = `${root}-link`;
|
|
112
|
+
await symlink(root, link);
|
|
113
|
+
await assert.rejects(createCodexResetStore(link).acquire(accountId));
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
void test("validation/write failure cannot replace pending; terminal outcomes must match the logical attempt", async (t) => {
|
|
117
|
+
const { root, store } = await fixture(t);
|
|
118
|
+
const lease = await store.acquire(accountId);
|
|
119
|
+
await lease.write(pending);
|
|
120
|
+
await assert.rejects(lease.write({ ...pending, creditId: "different" }));
|
|
121
|
+
await assert.rejects(lease.write({ ...pending, status: "resolved", requestId: "22222222-2222-4222-8222-222222222222" }));
|
|
122
|
+
await assert.rejects(lease.write({ ...pending, creditId: "x".repeat(8_193) }));
|
|
123
|
+
assert.deepEqual(await lease.read(), pending);
|
|
124
|
+
assert.equal((await readdir(root)).some((name) => name.endsWith(".tmp")), false);
|
|
125
|
+
await lease.write({ ...pending, status: "resolved" }); // Definitely not sent.
|
|
126
|
+
await lease.release();
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
void test("invalid UTF-8 must not silently change an opaque stored credit ID", async (t) => {
|
|
130
|
+
const { root, store } = await fixture(t);
|
|
131
|
+
const lease = await store.acquire(accountId);
|
|
132
|
+
const content = Buffer.from(JSON.stringify(pending));
|
|
133
|
+
content[content.indexOf("synthetic-credit")] = 0xff;
|
|
134
|
+
await writeFile(join(root, `${basename}.json`), content, { mode: 0o600 });
|
|
135
|
+
await assert.rejects(lease.read());
|
|
136
|
+
await lease.release();
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
void test("resolved request IDs cannot be reused for a new logical attempt", async (t) => {
|
|
140
|
+
const { store } = await fixture(t);
|
|
141
|
+
const lease = await store.acquire(accountId);
|
|
142
|
+
await lease.write(pending);
|
|
143
|
+
await lease.write({ ...pending, status: "resolved", confirmedCode: "reset" });
|
|
144
|
+
await assert.rejects(lease.write(pending));
|
|
145
|
+
await lease.release();
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
async function fileHandlePrototype(root: string): Promise<FileHandle> {
|
|
149
|
+
const probe = await open(join(root, "synthetic-probe"), "wx", 0o600);
|
|
150
|
+
const prototype = Object.getPrototypeOf(probe) as FileHandle;
|
|
151
|
+
await probe.close();
|
|
152
|
+
return prototype;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
void test("failed temporary write leaves pending intact and cleans only its own temporary file", async (t) => {
|
|
156
|
+
const { root, store } = await fixture(t);
|
|
157
|
+
const lease = await store.acquire(accountId);
|
|
158
|
+
await lease.write(pending);
|
|
159
|
+
const prototype = await fileHandlePrototype(root);
|
|
160
|
+
const fault = t.mock.method(prototype, "writeFile", () => Promise.reject(new Error("synthetic write fault")));
|
|
161
|
+
await assert.rejects(lease.write({ ...pending, status: "resolved", confirmedCode: "reset" }));
|
|
162
|
+
fault.mock.restore();
|
|
163
|
+
assert.deepEqual(await lease.read(), pending);
|
|
164
|
+
assert.equal((await readdir(root)).some((name) => name.endsWith(".tmp")), false);
|
|
165
|
+
await lease.release();
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
for (const restoreFails of [false, true]) {
|
|
169
|
+
void test(`post-rename terminal fsync failure restores pending or retains lock (${restoreFails})`, async (t) => {
|
|
170
|
+
const { root, store } = await fixture(t);
|
|
171
|
+
const lease = await store.acquire(accountId);
|
|
172
|
+
await lease.write(pending);
|
|
173
|
+
const prototype = await fileHandlePrototype(root);
|
|
174
|
+
const original = Reflect.get(prototype, "sync");
|
|
175
|
+
let directorySyncs = 0;
|
|
176
|
+
const fault = t.mock.method(prototype, "sync", async function (this: FileHandle) {
|
|
177
|
+
if ((await this.stat()).isDirectory() && (++directorySyncs === 1 || restoreFails)) throw new Error("synthetic fsync fault");
|
|
178
|
+
await original.call(this);
|
|
179
|
+
});
|
|
180
|
+
await assert.rejects(lease.write({ ...pending, status: "resolved", confirmedCode: "reset" }));
|
|
181
|
+
fault.mock.restore();
|
|
182
|
+
assert.deepEqual(JSON.parse(await readFile(join(root, `${basename}.json`), "utf8")) as unknown, pending);
|
|
183
|
+
if (restoreFails) {
|
|
184
|
+
await assert.rejects(lease.release());
|
|
185
|
+
await assert.rejects(store.acquire(accountId));
|
|
186
|
+
} else {
|
|
187
|
+
await lease.release();
|
|
188
|
+
const next = await store.acquire(accountId);
|
|
189
|
+
assert.deepEqual(await next.read(), pending);
|
|
190
|
+
await next.release();
|
|
191
|
+
}
|
|
192
|
+
assert.equal((await readdir(root)).some((name) => name.endsWith(".tmp")), false);
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
void test("accepts the byte limit but never reads beyond it", async (t) => {
|
|
197
|
+
const { root, store } = await fixture(t);
|
|
198
|
+
const lease = await store.acquire(accountId);
|
|
199
|
+
const serialized = JSON.stringify(pending);
|
|
200
|
+
await writeFile(join(root, `${basename}.json`), serialized.padEnd(8_192, " "), { mode: 0o600 });
|
|
201
|
+
assert.deepEqual(await lease.read(), pending);
|
|
202
|
+
await writeFile(join(root, `${basename}.json`), serialized.padEnd(8_193, " "), { mode: 0o600 });
|
|
203
|
+
await assert.rejects(lease.read());
|
|
204
|
+
await lease.release();
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
void test("no terminal record without pending, no operations after lease release", async (t) => {
|
|
208
|
+
const { store } = await fixture(t);
|
|
209
|
+
const lease = await store.acquire(accountId);
|
|
210
|
+
await assert.rejects(lease.write({ ...pending, status: "resolved", confirmedCode: "reset" }));
|
|
211
|
+
assert.equal(await lease.read(), undefined);
|
|
212
|
+
await lease.release();
|
|
213
|
+
await assert.rejects(lease.read());
|
|
214
|
+
await assert.rejects(lease.write(pending));
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
void test("write snapshots validated fields before asynchronous IO and cannot serialize inherited credentials", async (t) => {
|
|
218
|
+
const { root, store } = await fixture(t);
|
|
219
|
+
const lease = await store.acquire(accountId);
|
|
220
|
+
const input = { ...pending };
|
|
221
|
+
Object.setPrototypeOf(input, { toJSON: () => ({ ...pending, accessToken: "synthetic-forbidden" }) });
|
|
222
|
+
const saving = lease.write(input);
|
|
223
|
+
Object.assign(input, { creditId: "x".repeat(8_193), accessToken: "synthetic-forbidden" });
|
|
224
|
+
await saving;
|
|
225
|
+
assert.deepEqual(await lease.read(), pending);
|
|
226
|
+
assert.equal((await readFile(join(root, `${basename}.json`), "utf8")).includes("synthetic-forbidden"), false);
|
|
227
|
+
await lease.release();
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
void test("lock release failure is generic and refuses a subsequent lease", async (t) => {
|
|
231
|
+
const { root, store } = await fixture(t);
|
|
232
|
+
const lease = await store.acquire(accountId);
|
|
233
|
+
await chmod(join(root, `${basename}.lock`), 0o644);
|
|
234
|
+
await assert.rejects(lease.release(), /^Error: Codex reset journal is unavailable$/);
|
|
235
|
+
await assert.rejects(store.acquire(accountId));
|
|
236
|
+
});
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { constants } from "node:fs";
|
|
3
|
+
import { lstat, mkdir, open, rename, unlink } from "node:fs/promises";
|
|
4
|
+
import type { FileHandle } from "node:fs/promises";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import type { ConsumeCodexResetCode } from "./codex-usage.js";
|
|
7
|
+
|
|
8
|
+
/** Resolved without confirmedCode means definitely not sent, never inferred from GET. */
|
|
9
|
+
export type CodexResetRecord = {
|
|
10
|
+
version: 1;
|
|
11
|
+
accountId: string;
|
|
12
|
+
creditId: string;
|
|
13
|
+
requestId: string;
|
|
14
|
+
expiresAt?: number;
|
|
15
|
+
} & ({ status: "pending" } | { status: "resolved"; confirmedCode?: ConsumeCodexResetCode });
|
|
16
|
+
|
|
17
|
+
export type CodexResetLease = {
|
|
18
|
+
read(): Promise<CodexResetRecord | undefined>;
|
|
19
|
+
write(record: CodexResetRecord): Promise<void>;
|
|
20
|
+
release(): Promise<void>;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export type CodexResetStore = { acquire(accountId: string): Promise<CodexResetLease> };
|
|
24
|
+
|
|
25
|
+
const MAX_BYTES = 8_192;
|
|
26
|
+
const UUID = /^[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}$/i;
|
|
27
|
+
const CODES = new Set(["reset", "nothing_to_reset", "no_credit", "already_redeemed"]);
|
|
28
|
+
const unavailable = () => new Error("Codex reset journal is unavailable");
|
|
29
|
+
const isMissing = (error: unknown) => !!error && typeof error === "object" && "code" in error && error.code === "ENOENT";
|
|
30
|
+
const validId = (value: unknown): value is string => typeof value === "string" && value.length <= 512 && !!value.trim() &&
|
|
31
|
+
!Array.from(value).some((character) => character.charCodeAt(0) < 32 || character.charCodeAt(0) === 127);
|
|
32
|
+
|
|
33
|
+
function validateRecord(value: unknown, accountId: string): CodexResetRecord {
|
|
34
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw unavailable();
|
|
35
|
+
// Capture own values once; never serialize caller prototypes, toJSON hooks, or later mutations.
|
|
36
|
+
const record = { ...value } as Record<string, unknown>;
|
|
37
|
+
const keys = ["version", "accountId", "creditId", "requestId", "expiresAt", "status", "confirmedCode"];
|
|
38
|
+
if (Object.getOwnPropertyNames(value).some((key) => !keys.includes(key)) || record.version !== 1 || record.accountId !== accountId ||
|
|
39
|
+
!validId(record.creditId) || typeof record.requestId !== "string" || !UUID.test(record.requestId) ||
|
|
40
|
+
(record.expiresAt !== undefined && (typeof record.expiresAt !== "number" || !Number.isSafeInteger(record.expiresAt) || record.expiresAt < 0)) ||
|
|
41
|
+
(record.status !== "pending" && record.status !== "resolved") ||
|
|
42
|
+
(record.confirmedCode !== undefined && (record.status !== "resolved" || typeof record.confirmedCode !== "string" || !CODES.has(record.confirmedCode))))
|
|
43
|
+
throw unavailable();
|
|
44
|
+
return record as CodexResetRecord;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function sameAttempt(left: CodexResetRecord, right: CodexResetRecord): boolean {
|
|
48
|
+
return left.accountId === right.accountId && left.creditId === right.creditId &&
|
|
49
|
+
left.requestId === right.requestId && left.expiresAt === right.expiresAt;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** The injected root must have trusted parents. No auth material enters this edge. */
|
|
53
|
+
export function createCodexResetStore(rootDirectory: string): CodexResetStore {
|
|
54
|
+
return {
|
|
55
|
+
async acquire(accountId) {
|
|
56
|
+
let lock: FileHandle | undefined;
|
|
57
|
+
try {
|
|
58
|
+
if (!validId(accountId)) throw unavailable();
|
|
59
|
+
await mkdir(rootDirectory, { recursive: true, mode: 0o700 });
|
|
60
|
+
const root = await lstat(rootDirectory);
|
|
61
|
+
if (!root.isDirectory() || root.isSymbolicLink() || (root.mode & 0o777) !== 0o700) throw unavailable();
|
|
62
|
+
const hash = createHash("sha256").update(accountId).digest("hex");
|
|
63
|
+
const journalPath = join(rootDirectory, `${hash}.json`);
|
|
64
|
+
const lockPath = join(rootDirectory, `${hash}.lock`);
|
|
65
|
+
// Existing locks (including empty, stale, or symlink locks) are never reclaimed.
|
|
66
|
+
lock = await open(lockPath, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o600);
|
|
67
|
+
const heldLock = lock;
|
|
68
|
+
const lockStat = await heldLock.stat();
|
|
69
|
+
let released = false;
|
|
70
|
+
let poisoned = false;
|
|
71
|
+
|
|
72
|
+
async function checkRoot() {
|
|
73
|
+
if (released || poisoned) throw unavailable();
|
|
74
|
+
const current = await lstat(rootDirectory);
|
|
75
|
+
if (!current.isDirectory() || current.isSymbolicLink() || current.dev !== root.dev || current.ino !== root.ino ||
|
|
76
|
+
(current.mode & 0o777) !== 0o700) throw unavailable();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function read(): Promise<CodexResetRecord | undefined> {
|
|
80
|
+
let file: FileHandle | undefined;
|
|
81
|
+
try {
|
|
82
|
+
await checkRoot();
|
|
83
|
+
try { file = await open(journalPath, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK); }
|
|
84
|
+
catch (error) { if (isMissing(error)) return undefined; throw error; }
|
|
85
|
+
const info = await file.stat();
|
|
86
|
+
if (!info.isFile() || (info.mode & 0o777) !== 0o600 || info.size > MAX_BYTES) throw unavailable();
|
|
87
|
+
// A fixed buffer bounds reads even if another writer grows the file after stat.
|
|
88
|
+
const buffer = Buffer.alloc(MAX_BYTES + 1);
|
|
89
|
+
let size = 0;
|
|
90
|
+
while (size < buffer.length) {
|
|
91
|
+
const { bytesRead } = await file.read(buffer, size, buffer.length - size, size);
|
|
92
|
+
if (bytesRead === 0) break;
|
|
93
|
+
size += bytesRead;
|
|
94
|
+
}
|
|
95
|
+
if (size > MAX_BYTES) throw unavailable();
|
|
96
|
+
const text = new TextDecoder("utf-8", { fatal: true }).decode(buffer.subarray(0, size));
|
|
97
|
+
return validateRecord(JSON.parse(text) as unknown, accountId);
|
|
98
|
+
} catch { throw unavailable(); }
|
|
99
|
+
finally { await file?.close().catch(() => { throw unavailable(); }); }
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function syncDirectory() {
|
|
103
|
+
const directory = await open(rootDirectory, constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW);
|
|
104
|
+
try { await directory.sync(); }
|
|
105
|
+
finally { await directory.close(); }
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function write(record: CodexResetRecord): Promise<void> {
|
|
109
|
+
let previous: CodexResetRecord | undefined;
|
|
110
|
+
let published = false;
|
|
111
|
+
async function atomicSave(next: CodexResetRecord) {
|
|
112
|
+
const tempPath = join(rootDirectory, `${hash}.${randomUUID()}.tmp`);
|
|
113
|
+
let temp: FileHandle | undefined;
|
|
114
|
+
let created = false;
|
|
115
|
+
try {
|
|
116
|
+
await checkRoot();
|
|
117
|
+
temp = await open(tempPath, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o600);
|
|
118
|
+
created = true;
|
|
119
|
+
await temp.writeFile(JSON.stringify(next), "utf8");
|
|
120
|
+
await temp.sync();
|
|
121
|
+
await temp.close();
|
|
122
|
+
temp = undefined;
|
|
123
|
+
await checkRoot();
|
|
124
|
+
try {
|
|
125
|
+
const final = await lstat(journalPath);
|
|
126
|
+
if (!final.isFile() || final.isSymbolicLink() || (final.mode & 0o777) !== 0o600) throw unavailable();
|
|
127
|
+
} catch (error) { if (!isMissing(error)) throw error; }
|
|
128
|
+
await rename(tempPath, journalPath);
|
|
129
|
+
created = false;
|
|
130
|
+
published = true;
|
|
131
|
+
await syncDirectory();
|
|
132
|
+
} finally {
|
|
133
|
+
try { await temp?.close(); }
|
|
134
|
+
finally { if (created) await unlink(tempPath); }
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
try {
|
|
138
|
+
record = validateRecord(record, accountId);
|
|
139
|
+
if (Buffer.byteLength(JSON.stringify(record)) > MAX_BYTES) throw unavailable();
|
|
140
|
+
previous = await read();
|
|
141
|
+
if ((previous?.status === "pending" && !sameAttempt(previous, record)) ||
|
|
142
|
+
(previous?.status === "resolved" && record.status === "pending" && previous.requestId === record.requestId) ||
|
|
143
|
+
(record.status === "resolved" && (previous?.status !== "pending" || !sameAttempt(previous, record)))) throw unavailable();
|
|
144
|
+
await atomicSave(record);
|
|
145
|
+
} catch {
|
|
146
|
+
// A post-rename fsync failure must not expose a resolved gate. Restore pending;
|
|
147
|
+
// if even that fails, retain the lock rather than permit another attempt.
|
|
148
|
+
if (published && record.status === "resolved" && previous?.status === "pending") {
|
|
149
|
+
try { await atomicSave(previous); }
|
|
150
|
+
catch { poisoned = true; }
|
|
151
|
+
}
|
|
152
|
+
throw unavailable();
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async function release(): Promise<void> {
|
|
157
|
+
try {
|
|
158
|
+
await checkRoot();
|
|
159
|
+
const current = await lstat(lockPath);
|
|
160
|
+
if (!current.isFile() || (current.mode & 0o777) !== 0o600 || current.dev !== lockStat.dev || current.ino !== lockStat.ino)
|
|
161
|
+
throw unavailable();
|
|
162
|
+
await heldLock.close();
|
|
163
|
+
await unlink(lockPath);
|
|
164
|
+
} catch { throw unavailable(); }
|
|
165
|
+
finally { released = true; await heldLock.close().catch(() => {}); }
|
|
166
|
+
}
|
|
167
|
+
return { read, write, release };
|
|
168
|
+
} catch {
|
|
169
|
+
await lock?.close().catch(() => {}); // Never remove an uncertain or pre-existing lock.
|
|
170
|
+
throw unavailable();
|
|
171
|
+
}
|
|
172
|
+
},
|
|
173
|
+
};
|
|
174
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { registerHooks } from "node:module";
|
|
4
|
+
import test from "node:test";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import type { CodexResetResult } from "./codex-reset.js";
|
|
7
|
+
registerHooks({ resolve(specifier, context, next) {
|
|
8
|
+
if (specifier.startsWith(".") && specifier.endsWith(".js") && context.parentURL?.endsWith(".ts")) {
|
|
9
|
+
const url = new URL(`${specifier.slice(0, -3)}.ts`, context.parentURL);
|
|
10
|
+
if (existsSync(fileURLToPath(url))) return { shortCircuit: true, url: url.href };
|
|
11
|
+
}
|
|
12
|
+
return next(specifier, context);
|
|
13
|
+
} });
|
|
14
|
+
|
|
15
|
+
void test("choice labels map indexed local expiry to exact IDs, never render opaque identities", async () => {
|
|
16
|
+
const { createCodexResetInteraction } = await import("./codex-reset-ui.js");
|
|
17
|
+
const expiry = Date.parse("2030-07-17T12:30:00Z");
|
|
18
|
+
const choices = [{ creditId: "opaque-first", expiresAt: expiry }, { creditId: "opaque-second", expiresAt: undefined }];
|
|
19
|
+
let answer: number | string | undefined = 1;
|
|
20
|
+
const interaction = createCodexResetInteraction({ select: (_title, options) => {
|
|
21
|
+
assert.deepEqual(options, [`Reset 1 — expires ${new Date(expiry).toLocaleString()} (local time)`, "Reset 2 — Expiry not provided"]);
|
|
22
|
+
assert.doesNotMatch(options.join(""), /opaque-/);
|
|
23
|
+
return Promise.resolve(typeof answer === "number" ? options[answer] : answer);
|
|
24
|
+
} });
|
|
25
|
+
assert.equal(await interaction.choose(choices), "opaque-second");
|
|
26
|
+
for (answer of [undefined, "invented", "opaque-first"]) assert.equal(await interaction.choose(choices), undefined);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
void test("new and recovery confirmation use Cancel-first select with honest scope/effects/date", async () => {
|
|
30
|
+
const { createCodexResetInteraction } = await import("./codex-reset-ui.js");
|
|
31
|
+
for (const kind of ["new", "recovery"] as const) {
|
|
32
|
+
for (const accept of [false, true, undefined]) {
|
|
33
|
+
const interaction = createCodexResetInteraction({ select: (title, options) => {
|
|
34
|
+
assert.match(title, /active Codex account/);
|
|
35
|
+
assert.match(title, /Expiry not provided/);
|
|
36
|
+
assert.doesNotMatch(title, /opaque-/);
|
|
37
|
+
assert.deepEqual(options, ["Cancel", kind === "new" ? "Use reset" : "Check pending attempt"]);
|
|
38
|
+
if (kind === "new") assert.match(title, /one banked reset/);
|
|
39
|
+
else assert.match(title, /same previous request.*may finish.*not a new attempt/i);
|
|
40
|
+
return Promise.resolve(accept === undefined ? undefined : options[accept ? 1 : 0]);
|
|
41
|
+
} });
|
|
42
|
+
const base = { scope: "active-account", cancelFirst: true, creditId: "opaque-credit", expiresAt: undefined } as const;
|
|
43
|
+
const prompt = kind === "recovery" ? { ...base, kind, requestId: "opaque-request" } : { ...base, kind };
|
|
44
|
+
assert.equal(await interaction.confirm(prompt), accept === true);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
void test("confirmed codes remain definite and distinguish refresh/journal failures", async () => {
|
|
50
|
+
const { notifyCodexResetResult } = await import("./codex-reset-ui.js");
|
|
51
|
+
for (const code of ["reset", "already_redeemed", "no_credit", "nothing_to_reset"] as const) {
|
|
52
|
+
const notices: string[] = [];
|
|
53
|
+
notifyCodexResetResult({ notify: text => { notices.push(text); } }, { kind: "confirmed", code, windowsReset: 2, refreshFailed: true, journalFailed: true });
|
|
54
|
+
assert.match(notices[0] ?? "", code === "reset" ? /consumed.*2.*windows/i : code === "already_redeemed" ? /previous.*succeeded/i : /not consumed/i);
|
|
55
|
+
assert.match(notices[0] ?? "", /Quota refresh failed/);
|
|
56
|
+
assert.match(notices[0] ?? "", /Local attempt bookkeeping failed/);
|
|
57
|
+
assert.doesNotMatch(notices[0] ?? "", /reset failed|Outcome unknown/i);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
void test("unknown and blocked feedback never suggests another reset or automatic lock cleanup", async () => {
|
|
62
|
+
const { notifyCodexResetResult } = await import("./codex-reset-ui.js");
|
|
63
|
+
const results: CodexResetResult[] = [{ kind: "unknown", refreshFailed: false, journalFailed: false },
|
|
64
|
+
{ kind: "blocked", journalFailed: true }, { kind: "busy" }, { kind: "unavailable" }, { kind: "cancelled" }];
|
|
65
|
+
const notices: string[] = [];
|
|
66
|
+
for (const result of results) notifyCodexResetResult({ notify: text => { notices.push(text); } }, result);
|
|
67
|
+
assert.equal(notices[0], "Outcome unknown; do not start another reset. Use check to confirm the same pending attempt.");
|
|
68
|
+
assert.match(notices[1] ?? "", /blocked.*manual verification/i);
|
|
69
|
+
assert.doesNotMatch(notices.join(""), /opaque|token|\.json|auto.*reclaim/i);
|
|
70
|
+
});
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { CodexResetInteraction, CodexResetResult } from "./codex-reset.js";
|
|
3
|
+
|
|
4
|
+
export type CodexResetAction = () => Promise<boolean>;
|
|
5
|
+
const expiryLabel = (expiresAt: number | undefined): string => expiresAt === undefined
|
|
6
|
+
? "Expiry not provided" : `expires ${new Date(expiresAt).toLocaleString()} (local time)`;
|
|
7
|
+
|
|
8
|
+
/** Presentation only. Opaque IDs stay in the indexed mapping, not dialog text. */
|
|
9
|
+
export function createCodexResetInteraction(ui: Pick<ExtensionContext["ui"], "select">): CodexResetInteraction {
|
|
10
|
+
return {
|
|
11
|
+
async choose(choices) {
|
|
12
|
+
const labels = choices.map((choice, index) => `Reset ${index + 1} — ${expiryLabel(choice.expiresAt)}`);
|
|
13
|
+
const selected = await ui.select("Choose a banked reset for the active Codex account", labels);
|
|
14
|
+
const index = selected === undefined ? -1 : labels.indexOf(selected);
|
|
15
|
+
return choices[index]?.creditId;
|
|
16
|
+
},
|
|
17
|
+
async confirm(prompt) {
|
|
18
|
+
const recovery = prompt.kind === "recovery";
|
|
19
|
+
const effect = recovery
|
|
20
|
+
? "Check the same previous request; this may finish the previous unconfirmed reset, not a new attempt."
|
|
21
|
+
: "Use one banked reset; one banked reset will be consumed if rate limits are reset.";
|
|
22
|
+
const accept = recovery ? "Check pending attempt" : "Use reset";
|
|
23
|
+
return await ui.select(`For the active Codex account: ${effect}\nReset ${expiryLabel(prompt.expiresAt)}.`, ["Cancel", accept]) === accept;
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Definite server outcomes are never relabelled as failures by later local work. */
|
|
29
|
+
export function notifyCodexResetResult(ui: Pick<ExtensionContext["ui"], "notify">, result: CodexResetResult): void {
|
|
30
|
+
let message: string;
|
|
31
|
+
switch (result.kind) {
|
|
32
|
+
case "busy": message = "A banked reset action is already in progress."; break;
|
|
33
|
+
case "cancelled": message = "Banked reset action cancelled; no new reset was sent."; break;
|
|
34
|
+
case "unavailable": message = "No currently available banked reset could be selected. Refresh usage to check availability."; break;
|
|
35
|
+
case "blocked": message = "Banked reset action blocked. Local attempt storage or its lock is unavailable; manual verification is required. No automatic cleanup or retry."; break;
|
|
36
|
+
case "unknown": message = "Outcome unknown; do not start another reset. Use check to confirm the same pending attempt."; break;
|
|
37
|
+
case "confirmed":
|
|
38
|
+
switch (result.code) {
|
|
39
|
+
case "reset": message = `Banked reset consumed; ${result.windowsReset} rate-limit windows reset.`; break;
|
|
40
|
+
case "already_redeemed": message = "The previous reset already succeeded; this check did not consume another reset."; break;
|
|
41
|
+
case "no_credit": message = "No banked reset was available; a reset was not consumed."; break;
|
|
42
|
+
case "nothing_to_reset": message = "No rate limits needed resetting; a banked reset was not consumed."; break;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
if (result.kind === "unknown" || result.kind === "confirmed") {
|
|
46
|
+
if (result.refreshFailed) message += " Quota refresh failed; the consumption outcome above is unchanged.";
|
|
47
|
+
if (result.journalFailed) message += " Local attempt bookkeeping failed; do not start another reset before manual verification.";
|
|
48
|
+
}
|
|
49
|
+
const warning = result.kind === "unknown" || result.kind === "blocked" || result.kind === "busy" || result.kind === "unavailable" ||
|
|
50
|
+
(result.kind === "confirmed" && (result.refreshFailed || result.journalFailed));
|
|
51
|
+
ui.notify(message, warning ? "warning" : "info");
|
|
52
|
+
}
|