dev-flow-codex 0.1.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.
@@ -0,0 +1,1133 @@
1
+ import { execFile as execFileCallback } from "node:child_process";
2
+ import { createHash, randomBytes } from "node:crypto";
3
+ import { constants as fsConstants } from "node:fs";
4
+ import {
5
+ access,
6
+ chmod,
7
+ lstat,
8
+ mkdir,
9
+ readFile,
10
+ realpath,
11
+ rename,
12
+ stat,
13
+ unlink,
14
+ writeFile,
15
+ } from "node:fs/promises";
16
+ import { basename, delimiter, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
17
+ import { promisify } from "node:util";
18
+
19
+ import { containedPath } from "./paths.mjs";
20
+
21
+ const execFile = promisify(execFileCallback);
22
+
23
+ export const CODEX_COMPATIBILITY_RANGE = ">=0.147.0 <0.148.0";
24
+ export const MARKETPLACE_NAME = "dev-flow-local";
25
+ export const PLUGIN_NAME = "dev-flow-codex";
26
+ export const PLUGIN_SELECTOR = `${PLUGIN_NAME}@${MARKETPLACE_NAME}`;
27
+
28
+ const MCP_SCHEMA_URI = "https://agent-plugins.org/schemas/1.0.0/mcp.schema.json";
29
+ const EXPLICIT_SKILL_POLICY = "policy:\n allow_implicit_invocation: false";
30
+
31
+ const semverPattern = /^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/;
32
+ const digestPattern = /^[0-9a-f]{64}$/;
33
+
34
+ export async function runCodexJSON(
35
+ arguments_,
36
+ {
37
+ codexExecutable = "codex",
38
+ environment = process.env,
39
+ currentDirectory = process.cwd(),
40
+ } = {},
41
+ ) {
42
+ if (!Array.isArray(arguments_) || arguments_.some((argument) => typeof argument !== "string" || argument.includes("\0"))) {
43
+ throw new Error("Codex arguments must be a closed string array");
44
+ }
45
+ let stdout;
46
+ try {
47
+ ({ stdout } = await execFile(codexExecutable, arguments_, {
48
+ cwd: currentDirectory,
49
+ env: environment,
50
+ encoding: "utf8",
51
+ maxBuffer: 1024 * 1024,
52
+ windowsHide: true,
53
+ }));
54
+ } catch (error) {
55
+ const detail = String(error?.stderr ?? "").trim();
56
+ throw new Error(
57
+ `Codex command failed (${arguments_.join(" ")})${detail ? `: ${detail}` : ""}`,
58
+ { cause: error },
59
+ );
60
+ }
61
+
62
+ try {
63
+ return JSON.parse(stdout);
64
+ } catch (error) {
65
+ throw new Error(`Codex command did not return valid JSON (${arguments_.join(" ")})`, {
66
+ cause: error,
67
+ });
68
+ }
69
+ }
70
+
71
+ export async function inspectCoreVersion(
72
+ runtimePath,
73
+ {
74
+ environment = process.env,
75
+ currentDirectory = dirname(runtimePath),
76
+ } = {},
77
+ ) {
78
+ await assertExecutableFile(runtimePath, "packaged Core");
79
+ let stdout;
80
+ try {
81
+ ({ stdout } = await execFile(runtimePath, ["version"], {
82
+ cwd: currentDirectory,
83
+ env: environment,
84
+ encoding: "utf8",
85
+ maxBuffer: 64 * 1024,
86
+ windowsHide: true,
87
+ }));
88
+ } catch (error) {
89
+ throw new Error("packaged Core version preflight failed", { cause: error });
90
+ }
91
+ const match = /^dev-flow (\S+)\n?$/.exec(stdout);
92
+ if (!match) throw new Error("packaged Core returned an invalid version line");
93
+ parseSemver(match[1], "packaged Core version");
94
+ return match[1];
95
+ }
96
+
97
+ export async function setupRegistration({
98
+ paths,
99
+ packageVersion,
100
+ codexExecutable = "codex",
101
+ environment = process.env,
102
+ now = () => new Date(),
103
+ } = {}) {
104
+ const preflight = await preflightSetup({
105
+ paths,
106
+ packageVersion,
107
+ codexExecutable,
108
+ environment,
109
+ });
110
+ const commandOptions = {
111
+ codexExecutable,
112
+ environment,
113
+ currentDirectory: paths.packageRoot,
114
+ };
115
+ const existingReceipt = await readReceipt(paths.receiptPath);
116
+ const initialState = await readRegistrationState(commandOptions);
117
+ const expectedReceipt = createReceipt({
118
+ paths,
119
+ packageVersion: preflight.packageVersion,
120
+ coreVersion: preflight.coreVersion,
121
+ codexVersion: preflight.codexVersion,
122
+ resourceDigests: preflight.resourceDigests,
123
+ installedAt: now().toISOString(),
124
+ });
125
+
126
+ if (existingReceipt) {
127
+ if (receiptOwnershipMatches(existingReceipt, expectedReceipt)) {
128
+ assertMatchingRegistrationState(initialState, paths, preflight.packageVersion);
129
+ return {
130
+ status: "already-installed",
131
+ changed: false,
132
+ receipt: existingReceipt,
133
+ };
134
+ }
135
+
136
+ assertCompatibleReceiptUpgrade(existingReceipt, expectedReceipt);
137
+ const registrationMatchesPrevious = registrationStateMatches(
138
+ initialState,
139
+ paths,
140
+ existingReceipt.product.version,
141
+ );
142
+ const registrationMatchesCurrent = registrationStateMatches(
143
+ initialState,
144
+ paths,
145
+ preflight.packageVersion,
146
+ );
147
+ if (!registrationMatchesPrevious && !registrationMatchesCurrent) {
148
+ throw new Error("registration state conflicts with the owned upgrade; setup made no changes");
149
+ }
150
+ if (registrationMatchesPrevious) {
151
+ const pluginAddResult = await runCodexJSON(
152
+ ["plugin", "add", PLUGIN_SELECTOR, "--json"],
153
+ commandOptions,
154
+ );
155
+ assertPluginAddResult(pluginAddResult, paths, preflight.packageVersion);
156
+ const finalState = await readRegistrationState(commandOptions);
157
+ assertMatchingRegistrationState(finalState, paths, preflight.packageVersion);
158
+ }
159
+ await writeReceiptAtomic(paths.receiptPath, expectedReceipt, {
160
+ ownedRoot: paths.productSupportRoot,
161
+ });
162
+ return {
163
+ status: "installed",
164
+ changed: true,
165
+ receipt: expectedReceipt,
166
+ };
167
+ }
168
+
169
+ assertRegistrationAbsent(initialState, paths);
170
+ let marketplaceCreated = false;
171
+ try {
172
+ const marketplaceAddResult = await runCodexJSON(
173
+ ["plugin", "marketplace", "add", paths.marketplaceRoot, "--json"],
174
+ commandOptions,
175
+ );
176
+ assertMarketplaceAddResult(marketplaceAddResult, paths);
177
+ marketplaceCreated = true;
178
+ const pluginAddResult = await runCodexJSON(
179
+ ["plugin", "add", PLUGIN_SELECTOR, "--json"],
180
+ commandOptions,
181
+ );
182
+ assertPluginAddResult(pluginAddResult, paths, preflight.packageVersion);
183
+ const finalState = await readRegistrationState(commandOptions);
184
+ assertMatchingRegistrationState(finalState, paths, preflight.packageVersion);
185
+ await writeReceiptAtomic(paths.receiptPath, expectedReceipt, {
186
+ ownedRoot: paths.productSupportRoot,
187
+ });
188
+ return {
189
+ status: "installed",
190
+ changed: true,
191
+ receipt: expectedReceipt,
192
+ };
193
+ } catch (error) {
194
+ if (marketplaceCreated) {
195
+ const rollback = await rollbackCreatedMarketplace(paths, commandOptions);
196
+ if (rollback.error) {
197
+ throw new Error(
198
+ `setup failed and bounded marketplace rollback could not complete: ${rollback.error.message}`,
199
+ { cause: error },
200
+ );
201
+ }
202
+ }
203
+ throw error;
204
+ }
205
+ }
206
+
207
+ export async function removeRegistration({
208
+ paths,
209
+ packageVersion,
210
+ codexExecutable = "codex",
211
+ environment = process.env,
212
+ } = {}) {
213
+ const commandOptions = {
214
+ codexExecutable,
215
+ environment,
216
+ currentDirectory: paths.packageRoot,
217
+ };
218
+ await assertNoSymbolicLinkComponents(paths.productSupportRoot, dirname(paths.receiptPath));
219
+ await rejectSymbolicLink(paths.receiptPath);
220
+ const receipt = await readReceipt(paths.receiptPath);
221
+ let state = await readRegistrationState(commandOptions);
222
+
223
+ if (!receipt) {
224
+ assertRegistrationAbsent(state, paths);
225
+ return { status: "already-absent", changed: false };
226
+ }
227
+
228
+ assertRemovalReceipt(receipt, paths, packageVersion);
229
+ let owned = reconcileRemovalState(state, receipt);
230
+
231
+ if (owned.plugin) {
232
+ const pluginRemoveResult = await runCodexJSON(
233
+ ["plugin", "remove", receipt.registration.plugin_selector, "--json"],
234
+ commandOptions,
235
+ );
236
+ assertPluginRemoveResult(pluginRemoveResult, receipt.registration);
237
+ state = await readRegistrationState(commandOptions);
238
+ owned = reconcileRemovalState(state, receipt);
239
+ if (owned.plugin) throw new Error("Codex plugin remains after removal readback");
240
+ }
241
+
242
+ if (owned.marketplace) {
243
+ const marketplaceRemoveResult = await runCodexJSON(
244
+ ["plugin", "marketplace", "remove", receipt.registration.marketplace_name, "--json"],
245
+ commandOptions,
246
+ );
247
+ assertMarketplaceRemoveResult(
248
+ marketplaceRemoveResult,
249
+ receipt.registration.marketplace_name,
250
+ );
251
+ state = await readRegistrationState(commandOptions);
252
+ owned = reconcileRemovalState(state, receipt);
253
+ if (owned.marketplace) throw new Error("Codex marketplace remains after removal readback");
254
+ if (owned.plugin) throw new Error("Codex plugin reappeared during marketplace removal");
255
+ }
256
+
257
+ try {
258
+ await unlink(paths.receiptPath);
259
+ } catch (error) {
260
+ throw new Error(`delete exact registration receipt ${paths.receiptPath}: ${error.message}`, {
261
+ cause: error,
262
+ });
263
+ }
264
+ if (await readReceipt(paths.receiptPath)) {
265
+ throw new Error("registration receipt remains after exact cleanup");
266
+ }
267
+ return { status: "removed", changed: true };
268
+ }
269
+
270
+ async function preflightSetup({ paths, packageVersion, codexExecutable, environment }) {
271
+ assertObject(paths, "product paths");
272
+ if (paths.runtimeKey !== "darwin-arm64") {
273
+ throw new Error(`unsupported platform ${paths.runtimeKey ?? "unknown"}; Feature 003 supports darwin-arm64`);
274
+ }
275
+ parseSemver(packageVersion, "package version");
276
+ await assertPackageResources(paths, packageVersion);
277
+ const launcherPath = await assertExecutableOnPath("dev-flow-codex", environment?.PATH ?? "");
278
+ const expectedLauncherPath = join(paths.packageRoot, "bin", "dev-flow-codex.mjs");
279
+ let canonicalLauncher;
280
+ let canonicalExpectedLauncher;
281
+ try {
282
+ [canonicalLauncher, canonicalExpectedLauncher] = await Promise.all([
283
+ realpath(launcherPath),
284
+ realpath(expectedLauncherPath),
285
+ ]);
286
+ } catch (error) {
287
+ throw new Error("resolve the package-owned dev-flow-codex launcher on PATH", { cause: error });
288
+ }
289
+ if (canonicalLauncher !== canonicalExpectedLauncher) {
290
+ throw new Error("dev-flow-codex on PATH does not resolve to this installed package");
291
+ }
292
+ const coreVersion = await inspectCoreVersion(paths.runtimePath, {
293
+ environment,
294
+ currentDirectory: paths.packageRoot,
295
+ });
296
+ if (coreVersion !== packageVersion) {
297
+ throw new Error(`packaged Core version ${coreVersion} does not match package version ${packageVersion}`);
298
+ }
299
+ const codexVersion = await inspectCodexVersion(codexExecutable, {
300
+ environment,
301
+ currentDirectory: paths.packageRoot,
302
+ });
303
+ if (!versionSatisfiesRange(codexVersion)) {
304
+ throw new Error(`Codex version ${codexVersion} does not satisfy ${CODEX_COMPATIBILITY_RANGE}`);
305
+ }
306
+ return {
307
+ packageVersion,
308
+ coreVersion,
309
+ codexVersion,
310
+ resourceDigests: await digestResources(resourcePaths(paths)),
311
+ };
312
+ }
313
+
314
+ async function inspectCodexVersion(codexExecutable, { environment, currentDirectory }) {
315
+ let stdout;
316
+ try {
317
+ ({ stdout } = await execFile(codexExecutable, ["--version"], {
318
+ cwd: currentDirectory,
319
+ env: environment,
320
+ encoding: "utf8",
321
+ maxBuffer: 64 * 1024,
322
+ windowsHide: true,
323
+ }));
324
+ } catch (error) {
325
+ throw new Error("Codex version preflight failed", { cause: error });
326
+ }
327
+ const match = /^codex(?:-cli)? (\S+)\n?$/.exec(stdout);
328
+ if (!match) throw new Error("Codex returned an invalid version line");
329
+ parseSemver(match[1], "Codex version");
330
+ return match[1];
331
+ }
332
+
333
+ async function assertPackageResources(paths, packageVersion) {
334
+ const packageManifest = await readJSON(join(paths.packageRoot, "package.json"), "package manifest");
335
+ const privateContract = !Object.hasOwn(packageManifest, "private") || packageManifest.private === false;
336
+ const platformContract = stableJSON(packageManifest.os) === stableJSON(["darwin"]) &&
337
+ stableJSON(packageManifest.cpu) === stableJSON(["arm64"]);
338
+ const publishContract = packageManifest.publishConfig?.access === "public" &&
339
+ packageManifest.publishConfig?.registry === "https://registry.npmjs.org/" &&
340
+ stableJSON(Object.keys(packageManifest.publishConfig).sort()) === stableJSON(["access", "registry"]);
341
+ if (
342
+ packageManifest.name !== PLUGIN_NAME ||
343
+ !privateContract ||
344
+ !platformContract ||
345
+ !publishContract ||
346
+ packageManifest.license !== "Apache-2.0"
347
+ ) {
348
+ throw new Error("package manifest does not satisfy the fixed public package contract");
349
+ }
350
+ if (packageManifest.version !== packageVersion) {
351
+ throw new Error("package manifest version does not match the requested package version");
352
+ }
353
+
354
+ const marketplace = await readJSON(
355
+ join(paths.marketplaceRoot, ".agents", "plugins", "marketplace.json"),
356
+ "marketplace catalog",
357
+ );
358
+ if (marketplace.name !== MARKETPLACE_NAME || !Array.isArray(marketplace.plugins) || marketplace.plugins.length !== 1) {
359
+ throw new Error("marketplace catalog must contain exactly the Dev Flow marketplace entry");
360
+ }
361
+ const marketplacePlugin = marketplace.plugins[0];
362
+ if (
363
+ marketplacePlugin?.name !== PLUGIN_NAME ||
364
+ marketplacePlugin?.source?.source !== "local" ||
365
+ marketplacePlugin?.source?.path !== "./plugin"
366
+ ) {
367
+ throw new Error("marketplace catalog has an invalid local plugin source");
368
+ }
369
+
370
+ const pluginManifest = await readJSON(
371
+ join(paths.pluginRoot, ".codex-plugin", "plugin.json"),
372
+ "plugin manifest",
373
+ );
374
+ if (
375
+ pluginManifest.name !== PLUGIN_NAME ||
376
+ pluginManifest.version !== packageVersion ||
377
+ pluginManifest.skills !== "./skills/" ||
378
+ pluginManifest.mcpServers !== "./.mcp.json"
379
+ ) {
380
+ throw new Error("plugin manifest identity or resource paths do not match the package");
381
+ }
382
+
383
+ const mcpConfiguration = await readJSON(
384
+ join(paths.pluginRoot, ".mcp.json"),
385
+ "MCP configuration",
386
+ );
387
+ assertExactKeys(mcpConfiguration, ["$schema", "mcpServers"], "MCP configuration");
388
+ if (mcpConfiguration.$schema !== MCP_SCHEMA_URI) {
389
+ throw new Error(`MCP configuration schema must equal ${MCP_SCHEMA_URI}`);
390
+ }
391
+ assertObject(mcpConfiguration.mcpServers, "MCP servers");
392
+ assertExactKeys(mcpConfiguration.mcpServers, ["dev-flow"], "MCP servers");
393
+ const server = mcpConfiguration.mcpServers["dev-flow"];
394
+ assertObject(server, "Dev Flow MCP server");
395
+ assertExactKeys(server, ["type", "command", "args"], "Dev Flow MCP server");
396
+ if (
397
+ server.type !== "stdio" ||
398
+ server.command !== "dev-flow-codex" ||
399
+ stableJSON(server.args) !== stableJSON(["mcp"])
400
+ ) {
401
+ throw new Error("Dev Flow MCP server must invoke exactly dev-flow-codex mcp");
402
+ }
403
+
404
+ const skillPath = join(paths.pluginRoot, "skills", "dev-flow", "SKILL.md");
405
+ let skill;
406
+ try {
407
+ skill = await readFile(skillPath, "utf8");
408
+ } catch (error) {
409
+ throw new Error("Dev Flow Skill is unavailable", { cause: error });
410
+ }
411
+ if (skill.trim() === "") throw new Error("Dev Flow Skill must be non-empty");
412
+ if (/^allow_implicit_invocation\s*:/m.test(skill.match(/^---\n([\s\S]*?)\n---\n/)?.[1] ?? "")) {
413
+ throw new Error("Dev Flow Skill frontmatter must not carry Codex invocation policy");
414
+ }
415
+
416
+ let skillMetadata;
417
+ try {
418
+ skillMetadata = await readFile(
419
+ join(paths.pluginRoot, "skills", "dev-flow", "agents", "openai.yaml"),
420
+ "utf8",
421
+ );
422
+ } catch (error) {
423
+ throw new Error("Dev Flow explicit-only Skill policy is unavailable", { cause: error });
424
+ }
425
+ if (skillMetadata.trim() !== EXPLICIT_SKILL_POLICY) {
426
+ throw new Error("Dev Flow explicit-only Skill policy must disable implicit invocation");
427
+ }
428
+ }
429
+
430
+ async function readRegistrationState(commandOptions) {
431
+ const marketplaceResponse = await runCodexJSON(
432
+ ["plugin", "marketplace", "list", "--json"],
433
+ commandOptions,
434
+ );
435
+ const pluginResponse = await runCodexJSON(["plugin", "list", "--json"], commandOptions);
436
+ assertObject(marketplaceResponse, "Codex marketplace readback");
437
+ assertExactKeys(marketplaceResponse, ["marketplaces"], "Codex marketplace readback");
438
+ if (!Array.isArray(marketplaceResponse.marketplaces)) {
439
+ throw new Error("Codex marketplace readback marketplaces must be an array");
440
+ }
441
+ assertObject(pluginResponse, "Codex plugin readback");
442
+ assertExactKeys(pluginResponse, ["installed", "available"], "Codex plugin readback");
443
+ if (!Array.isArray(pluginResponse.installed) || !Array.isArray(pluginResponse.available)) {
444
+ throw new Error("Codex plugin readback installed and available must be arrays");
445
+ }
446
+ if (pluginResponse.available.length !== 0) {
447
+ throw new Error("Codex plugin readback available must be empty without --available");
448
+ }
449
+ return { marketplaces: marketplaceResponse.marketplaces, plugins: pluginResponse.installed };
450
+ }
451
+
452
+ function assertRegistrationAbsent(state, paths) {
453
+ const marketplaceCollision = state.marketplaces.find(
454
+ (entry) =>
455
+ entry?.name === MARKETPLACE_NAME ||
456
+ entry?.root === paths.marketplaceRoot ||
457
+ entry?.marketplaceSource?.source === paths.marketplaceRoot,
458
+ );
459
+ if (marketplaceCollision) {
460
+ throw new Error("marketplace ownership conflict without a matching registration receipt");
461
+ }
462
+ const pluginCollision = state.plugins.find(
463
+ (entry) =>
464
+ entry?.name === PLUGIN_NAME ||
465
+ entry?.pluginId === PLUGIN_SELECTOR ||
466
+ entry?.source?.path === paths.pluginRoot,
467
+ );
468
+ if (pluginCollision) {
469
+ throw new Error("plugin ownership conflict without a matching registration receipt");
470
+ }
471
+ }
472
+
473
+ function assertRemovalReceipt(receipt, paths, packageVersion) {
474
+ const matches =
475
+ receipt.product.version === packageVersion &&
476
+ receipt.product.core_version === packageVersion &&
477
+ receipt.registration.marketplace_name === MARKETPLACE_NAME &&
478
+ receipt.registration.marketplace_root === paths.marketplaceRoot &&
479
+ receipt.registration.plugin_name === PLUGIN_NAME &&
480
+ receipt.registration.plugin_selector === PLUGIN_SELECTOR &&
481
+ receipt.registration.plugin_root === paths.pluginRoot &&
482
+ receipt.paths.package_root === paths.packageRoot &&
483
+ receipt.paths.runtime_path === paths.runtimePath &&
484
+ receipt.paths.data_dir === paths.dataDirectory &&
485
+ receipt.paths.receipt_path === paths.receiptPath;
486
+ if (!matches) {
487
+ throw new Error("registration receipt ownership conflict; removal made no changes");
488
+ }
489
+ }
490
+
491
+ function reconcileRemovalState(state, receipt) {
492
+ const matchingMarketplaces = state.marketplaces.filter(
493
+ (entry) =>
494
+ entry?.name === receipt.registration.marketplace_name ||
495
+ entry?.root === receipt.registration.marketplace_root ||
496
+ entry?.marketplaceSource?.source === receipt.registration.marketplace_root,
497
+ );
498
+ if (matchingMarketplaces.length > 1) {
499
+ throw new Error("Codex marketplace ownership conflict during removal");
500
+ }
501
+ const marketplace = matchingMarketplaces[0] ?? null;
502
+ if (marketplace) {
503
+ assertMarketplaceReadback(
504
+ marketplace,
505
+ receipt.registration.marketplace_name,
506
+ receipt.registration.marketplace_root,
507
+ "marketplace removal readback",
508
+ );
509
+ }
510
+
511
+ const matchingPlugins = state.plugins.filter(
512
+ (entry) =>
513
+ entry?.name === receipt.registration.plugin_name ||
514
+ entry?.pluginId === receipt.registration.plugin_selector ||
515
+ entry?.source?.path === receipt.registration.plugin_root,
516
+ );
517
+ if (matchingPlugins.length > 1) {
518
+ throw new Error("Codex plugin ownership conflict during removal");
519
+ }
520
+ const plugin = matchingPlugins[0] ?? null;
521
+ if (plugin) {
522
+ assertPluginReadback(
523
+ plugin,
524
+ {
525
+ pluginId: receipt.registration.plugin_selector,
526
+ name: receipt.registration.plugin_name,
527
+ marketplaceName: receipt.registration.marketplace_name,
528
+ pluginRoot: receipt.registration.plugin_root,
529
+ marketplaceRoot: receipt.registration.marketplace_root,
530
+ version: receipt.product.version,
531
+ requireEnabled: false,
532
+ },
533
+ "plugin removal readback",
534
+ );
535
+ }
536
+
537
+ return { marketplace, plugin };
538
+ }
539
+
540
+ function assertMatchingRegistrationState(state, paths, packageVersion) {
541
+ const matchingMarketplaces = state.marketplaces.filter(
542
+ (entry) =>
543
+ entry?.name === MARKETPLACE_NAME ||
544
+ entry?.root === paths.marketplaceRoot ||
545
+ entry?.marketplaceSource?.source === paths.marketplaceRoot,
546
+ );
547
+ if (matchingMarketplaces.length !== 1) {
548
+ throw new Error("Codex readback must contain exactly one Dev Flow marketplace identity");
549
+ }
550
+ const marketplace = matchingMarketplaces[0];
551
+ assertMarketplaceReadback(
552
+ marketplace,
553
+ MARKETPLACE_NAME,
554
+ paths.marketplaceRoot,
555
+ "marketplace readback",
556
+ );
557
+
558
+ const matchingPlugins = state.plugins.filter(
559
+ (entry) =>
560
+ entry?.name === PLUGIN_NAME ||
561
+ entry?.pluginId === PLUGIN_SELECTOR ||
562
+ entry?.source?.path === paths.pluginRoot,
563
+ );
564
+ if (matchingPlugins.length !== 1) {
565
+ throw new Error("Codex readback must contain exactly one Dev Flow plugin identity");
566
+ }
567
+ const plugin = matchingPlugins[0];
568
+ assertPluginReadback(
569
+ plugin,
570
+ {
571
+ pluginId: PLUGIN_SELECTOR,
572
+ name: PLUGIN_NAME,
573
+ marketplaceName: MARKETPLACE_NAME,
574
+ pluginRoot: paths.pluginRoot,
575
+ marketplaceRoot: paths.marketplaceRoot,
576
+ version: packageVersion,
577
+ requireEnabled: true,
578
+ },
579
+ "plugin readback",
580
+ );
581
+ }
582
+
583
+ function registrationStateMatches(state, paths, packageVersion) {
584
+ try {
585
+ assertMatchingRegistrationState(state, paths, packageVersion);
586
+ return true;
587
+ } catch {
588
+ return false;
589
+ }
590
+ }
591
+
592
+ async function rollbackCreatedMarketplace(paths, commandOptions) {
593
+ try {
594
+ const state = await readRegistrationState(commandOptions);
595
+ if (state.plugins.some((entry) => entry?.name === PLUGIN_NAME || entry?.pluginId === PLUGIN_SELECTOR)) {
596
+ return { preserved: true };
597
+ }
598
+ const marketplace = state.marketplaces.find((entry) => entry?.name === MARKETPLACE_NAME);
599
+ if (!marketplace || marketplace.root !== paths.marketplaceRoot) return { preserved: true };
600
+ assertMarketplaceReadback(marketplace, MARKETPLACE_NAME, paths.marketplaceRoot, "rollback marketplace readback");
601
+ const removeResult = await runCodexJSON(
602
+ ["plugin", "marketplace", "remove", MARKETPLACE_NAME, "--json"],
603
+ commandOptions,
604
+ );
605
+ assertMarketplaceRemoveResult(removeResult, MARKETPLACE_NAME);
606
+ const after = await readRegistrationState(commandOptions);
607
+ if (after.marketplaces.some((entry) => entry?.name === MARKETPLACE_NAME)) {
608
+ throw new Error("marketplace remains after rollback readback");
609
+ }
610
+ return { removed: true };
611
+ } catch (error) {
612
+ return { error };
613
+ }
614
+ }
615
+
616
+ function assertMarketplaceReadback(marketplace, expectedName, expectedRoot, label) {
617
+ assertObject(marketplace, label);
618
+ assertExactKeys(marketplace, ["name", "root", "marketplaceSource"], label);
619
+ assertObject(marketplace.marketplaceSource, `${label} source`);
620
+ assertExactKeys(marketplace.marketplaceSource, ["sourceType", "source"], `${label} source`);
621
+ if (
622
+ marketplace.name !== expectedName ||
623
+ marketplace.root !== expectedRoot ||
624
+ marketplace.marketplaceSource.sourceType !== "local" ||
625
+ marketplace.marketplaceSource.source !== expectedRoot
626
+ ) {
627
+ throw new Error(`${label} conflicts with the expected local marketplace identity`);
628
+ }
629
+ }
630
+
631
+ function assertPluginReadback(plugin, expected, label) {
632
+ assertObject(plugin, label);
633
+ assertExactKeys(
634
+ plugin,
635
+ [
636
+ "pluginId",
637
+ "name",
638
+ "marketplaceName",
639
+ "version",
640
+ "installed",
641
+ "enabled",
642
+ "source",
643
+ "marketplaceSource",
644
+ "installPolicy",
645
+ "authPolicy",
646
+ ],
647
+ label,
648
+ );
649
+ assertObject(plugin.source, `${label} plugin source`);
650
+ assertExactKeys(plugin.source, ["source", "path"], `${label} plugin source`);
651
+ assertObject(plugin.marketplaceSource, `${label} marketplace source`);
652
+ assertExactKeys(
653
+ plugin.marketplaceSource,
654
+ ["sourceType", "source"],
655
+ `${label} marketplace source`,
656
+ );
657
+ if (
658
+ plugin.pluginId !== expected.pluginId ||
659
+ plugin.name !== expected.name ||
660
+ plugin.marketplaceName !== expected.marketplaceName ||
661
+ plugin.version !== expected.version ||
662
+ plugin.installed !== true ||
663
+ typeof plugin.enabled !== "boolean" ||
664
+ (expected.requireEnabled && plugin.enabled !== true) ||
665
+ plugin.source.source !== "local" ||
666
+ plugin.source.path !== expected.pluginRoot ||
667
+ plugin.marketplaceSource.sourceType !== "local" ||
668
+ plugin.marketplaceSource.source !== expected.marketplaceRoot ||
669
+ plugin.installPolicy !== "AVAILABLE" ||
670
+ plugin.authPolicy !== "ON_INSTALL"
671
+ ) {
672
+ throw new Error(`${label} conflicts with the expected installed plugin identity`);
673
+ }
674
+ }
675
+
676
+ function assertMarketplaceAddResult(result, paths) {
677
+ assertObject(result, "marketplace add result");
678
+ assertExactKeys(result, ["marketplaceName", "installedRoot", "alreadyAdded"], "marketplace add result");
679
+ if (
680
+ result.marketplaceName !== MARKETPLACE_NAME ||
681
+ result.installedRoot !== paths.marketplaceRoot ||
682
+ typeof result.alreadyAdded !== "boolean"
683
+ ) {
684
+ throw new Error("marketplace add result conflicts with the requested local marketplace");
685
+ }
686
+ if (result.alreadyAdded) {
687
+ throw new Error(
688
+ "marketplace add reported alreadyAdded after absent readback; concurrent marketplace ownership is not rollback-owned",
689
+ );
690
+ }
691
+ }
692
+
693
+ function assertPluginAddResult(result, paths, packageVersion) {
694
+ assertObject(result, "plugin add result");
695
+ assertExactKeys(
696
+ result,
697
+ ["pluginId", "name", "marketplaceName", "version", "installedPath", "authPolicy"],
698
+ "plugin add result",
699
+ );
700
+ assertCanonicalAbsolutePath(result.installedPath, "plugin add result installedPath");
701
+ if (
702
+ result.pluginId !== PLUGIN_SELECTOR ||
703
+ result.name !== PLUGIN_NAME ||
704
+ result.marketplaceName !== MARKETPLACE_NAME ||
705
+ result.version !== packageVersion ||
706
+ result.authPolicy !== "ON_INSTALL" ||
707
+ result.installedPath === paths.pluginRoot
708
+ ) {
709
+ throw new Error("plugin add result conflicts with the requested plugin identity");
710
+ }
711
+ }
712
+
713
+ function assertPluginRemoveResult(result, registration) {
714
+ assertObject(result, "plugin remove result");
715
+ assertExactKeys(result, ["pluginId", "name", "marketplaceName"], "plugin remove result");
716
+ if (
717
+ result.pluginId !== registration.plugin_selector ||
718
+ result.name !== registration.plugin_name ||
719
+ result.marketplaceName !== registration.marketplace_name
720
+ ) {
721
+ throw new Error("plugin remove result conflicts with the recorded plugin identity");
722
+ }
723
+ }
724
+
725
+ function assertMarketplaceRemoveResult(result, marketplaceName) {
726
+ assertObject(result, "marketplace remove result");
727
+ assertExactKeys(result, ["marketplaceName", "installedRoot"], "marketplace remove result");
728
+ if (result.marketplaceName !== marketplaceName || result.installedRoot !== null) {
729
+ throw new Error("marketplace remove result conflicts with the recorded local marketplace");
730
+ }
731
+ }
732
+
733
+ function createReceipt({
734
+ paths,
735
+ packageVersion,
736
+ coreVersion,
737
+ codexVersion,
738
+ resourceDigests,
739
+ installedAt,
740
+ }) {
741
+ return validateReceipt({
742
+ schema_version: 3,
743
+ product: {
744
+ name: PLUGIN_NAME,
745
+ version: packageVersion,
746
+ core_version: coreVersion,
747
+ codex_compatibility: CODEX_COMPATIBILITY_RANGE,
748
+ },
749
+ host: {
750
+ surface: "codex-cli",
751
+ version: codexVersion,
752
+ os: "darwin",
753
+ arch: "arm64",
754
+ },
755
+ registration: {
756
+ marketplace_name: MARKETPLACE_NAME,
757
+ marketplace_root: paths.marketplaceRoot,
758
+ plugin_name: PLUGIN_NAME,
759
+ plugin_selector: PLUGIN_SELECTOR,
760
+ plugin_root: paths.pluginRoot,
761
+ },
762
+ paths: {
763
+ package_root: paths.packageRoot,
764
+ runtime_path: paths.runtimePath,
765
+ data_dir: paths.dataDirectory,
766
+ receipt_path: paths.receiptPath,
767
+ },
768
+ resource_digests: resourceDigests,
769
+ installed_at: installedAt,
770
+ });
771
+ }
772
+
773
+ function resourcePaths(paths) {
774
+ return {
775
+ pluginManifest: join(paths.pluginRoot, ".codex-plugin", "plugin.json"),
776
+ skill: join(paths.pluginRoot, "skills", "dev-flow", "SKILL.md"),
777
+ skillMetadata: join(paths.pluginRoot, "skills", "dev-flow", "agents", "openai.yaml"),
778
+ mcpConfiguration: join(paths.pluginRoot, ".mcp.json"),
779
+ };
780
+ }
781
+
782
+ async function assertExecutableFile(path, label) {
783
+ let info;
784
+ try {
785
+ info = await stat(path);
786
+ await access(path, fsConstants.X_OK);
787
+ } catch (error) {
788
+ throw new Error(`${label} must exist and be executable`, { cause: error });
789
+ }
790
+ if (!info.isFile() || (info.mode & 0o111) === 0) {
791
+ throw new Error(`${label} must exist and be executable`);
792
+ }
793
+ }
794
+
795
+ async function assertExecutableOnPath(name, pathValue) {
796
+ for (const directory of pathValue.split(delimiter).filter(Boolean)) {
797
+ const candidate = join(directory, name);
798
+ try {
799
+ await assertExecutableFile(candidate, name);
800
+ return candidate;
801
+ } catch {
802
+ // Continue through the closed PATH list.
803
+ }
804
+ }
805
+ throw new Error(`${name} must be executable and discoverable on PATH`);
806
+ }
807
+
808
+ async function readJSON(path, label) {
809
+ let contents;
810
+ try {
811
+ contents = await readFile(path, "utf8");
812
+ } catch (error) {
813
+ throw new Error(`${label} is unavailable at ${path}`, { cause: error });
814
+ }
815
+ try {
816
+ const parsed = JSON.parse(contents);
817
+ assertObject(parsed, label);
818
+ return parsed;
819
+ } catch (error) {
820
+ throw new Error(`${label} is not valid JSON`, { cause: error });
821
+ }
822
+ }
823
+
824
+ export function versionSatisfiesRange(version, range = CODEX_COMPATIBILITY_RANGE) {
825
+ const match = /^>=(\S+)\s+<(\S+)$/.exec(range);
826
+ if (!match) throw new Error(`unsupported compatibility range ${JSON.stringify(range)}`);
827
+ const candidate = parseSemver(version, "Codex version");
828
+ const minimum = parseSemver(match[1], "compatibility minimum");
829
+ const maximum = parseSemver(match[2], "compatibility maximum");
830
+ return compareSemver(candidate, minimum) >= 0 && compareSemver(candidate, maximum) < 0;
831
+ }
832
+
833
+ export function validateReceipt(receipt, { compatibilityRange = CODEX_COMPATIBILITY_RANGE } = {}) {
834
+ assertObject(receipt, "registration receipt");
835
+ assertExactKeys(
836
+ receipt,
837
+ ["schema_version", "product", "host", "registration", "paths", "resource_digests", "installed_at"],
838
+ "registration receipt",
839
+ );
840
+ if (receipt.schema_version !== 3) throw new Error("registration receipt schema_version must equal 3");
841
+
842
+ assertObject(receipt.product, "product");
843
+ assertExactKeys(receipt.product, ["name", "version", "core_version", "codex_compatibility"], "product");
844
+ assertEqual(receipt.product.name, "dev-flow-codex", "product.name");
845
+ parseSemver(receipt.product.version, "product.version");
846
+ parseSemver(receipt.product.core_version, "product.core_version");
847
+ assertEqual(receipt.product.core_version, receipt.product.version, "product Core version");
848
+ assertEqual(receipt.product.codex_compatibility, compatibilityRange, "product compatibility range");
849
+
850
+ assertObject(receipt.host, "host");
851
+ assertExactKeys(receipt.host, ["surface", "version", "os", "arch"], "host");
852
+ assertEqual(receipt.host.surface, "codex-cli", "host.surface");
853
+ assertEqual(receipt.host.os, "darwin", "host.os");
854
+ assertEqual(receipt.host.arch, "arm64", "host.arch");
855
+ if (!versionSatisfiesRange(receipt.host.version, compatibilityRange)) {
856
+ throw new Error(`host.version ${receipt.host.version} does not satisfy ${compatibilityRange}`);
857
+ }
858
+
859
+ assertObject(receipt.registration, "registration");
860
+ assertExactKeys(
861
+ receipt.registration,
862
+ ["marketplace_name", "marketplace_root", "plugin_name", "plugin_selector", "plugin_root"],
863
+ "registration",
864
+ );
865
+ assertEqual(receipt.registration.marketplace_name, "dev-flow-local", "registration.marketplace_name");
866
+ assertEqual(receipt.registration.plugin_name, "dev-flow-codex", "registration.plugin_name");
867
+ assertEqual(
868
+ receipt.registration.plugin_selector,
869
+ "dev-flow-codex@dev-flow-local",
870
+ "registration.plugin_selector",
871
+ );
872
+ assertCanonicalAbsolutePath(receipt.registration.marketplace_root, "registration.marketplace_root");
873
+ assertCanonicalAbsolutePath(receipt.registration.plugin_root, "registration.plugin_root");
874
+
875
+ assertObject(receipt.paths, "paths");
876
+ assertExactKeys(receipt.paths, ["package_root", "runtime_path", "data_dir", "receipt_path"], "paths");
877
+ for (const field of ["package_root", "runtime_path", "data_dir", "receipt_path"]) {
878
+ assertCanonicalAbsolutePath(receipt.paths[field], `paths.${field}`);
879
+ }
880
+
881
+ assertObject(receipt.resource_digests, "resource_digests");
882
+ assertExactKeys(
883
+ receipt.resource_digests,
884
+ ["plugin_manifest", "skill", "skill_metadata", "mcp_configuration"],
885
+ "resource_digests",
886
+ );
887
+ for (const field of ["plugin_manifest", "skill", "skill_metadata", "mcp_configuration"]) {
888
+ if (!digestPattern.test(receipt.resource_digests[field])) {
889
+ throw new Error(`resource_digests.${field} must be a lowercase SHA-256 digest`);
890
+ }
891
+ }
892
+
893
+ if (typeof receipt.installed_at !== "string" || !Number.isFinite(Date.parse(receipt.installed_at))) {
894
+ throw new Error("installed_at must be an RFC 3339 date-time string");
895
+ }
896
+ return structuredClone(receipt);
897
+ }
898
+
899
+ export async function readReceipt(receiptPath, options) {
900
+ let contents;
901
+ try {
902
+ contents = await readFile(receiptPath, "utf8");
903
+ } catch (error) {
904
+ if (error?.code === "ENOENT") return null;
905
+ throw new Error(`read registration receipt ${receiptPath}: ${error.message}`, { cause: error });
906
+ }
907
+
908
+ let receipt;
909
+ try {
910
+ receipt = JSON.parse(contents);
911
+ } catch (error) {
912
+ throw new Error(`parse registration receipt ${receiptPath}: invalid JSON`, { cause: error });
913
+ }
914
+ try {
915
+ return validateReceipt(receipt, options);
916
+ } catch (error) {
917
+ throw new Error(`registration receipt ${receiptPath} is invalid: ${error.message}`, { cause: error });
918
+ }
919
+ }
920
+
921
+ export async function writeReceiptAtomic(receiptPath, receipt, { ownedRoot, compatibilityRange } = {}) {
922
+ if (!ownedRoot) throw new Error("receipt owned root is required");
923
+ assertCanonicalAbsolutePath(receiptPath, "receipt path");
924
+ assertCanonicalAbsolutePath(ownedRoot, "receipt owned root");
925
+ const expectedPath = join(ownedRoot, "registrations", "codex.json");
926
+ if (receiptPath !== expectedPath) {
927
+ throw new Error(`receipt path must equal the product-owned path ${expectedPath}`);
928
+ }
929
+ containedPath(ownedRoot, receiptPath, "receipt path");
930
+
931
+ const validated = validateReceipt(receipt, { compatibilityRange });
932
+ if (validated.paths.receipt_path !== receiptPath) {
933
+ throw new Error("receipt payload path does not match the write target");
934
+ }
935
+
936
+ await mkdir(ownedRoot, { recursive: true, mode: 0o700 });
937
+ const canonicalOwnedRoot = await realpath(ownedRoot);
938
+ if (canonicalOwnedRoot !== ownedRoot) {
939
+ throw new Error("receipt owned root must be canonical and may not use a symbolic link");
940
+ }
941
+ const parent = dirname(receiptPath);
942
+ await assertNoSymbolicLinkComponents(ownedRoot, parent);
943
+ await mkdir(parent, { recursive: true, mode: 0o700 });
944
+ await assertNoSymbolicLinkComponents(ownedRoot, parent);
945
+ await chmod(parent, 0o700);
946
+ await rejectSymbolicLink(receiptPath);
947
+
948
+ const temporaryPath = join(parent, `.${basename(receiptPath)}.tmp-${process.pid}-${randomBytes(8).toString("hex")}`);
949
+ try {
950
+ await writeFile(temporaryPath, `${JSON.stringify(validated, null, 2)}\n`, {
951
+ encoding: "utf8",
952
+ mode: 0o600,
953
+ flag: "wx",
954
+ });
955
+ await rename(temporaryPath, receiptPath);
956
+ await chmod(receiptPath, 0o600);
957
+ } catch (error) {
958
+ await unlink(temporaryPath).catch(() => {});
959
+ throw error;
960
+ }
961
+ }
962
+
963
+ export async function digestResources({ pluginManifest, skill, skillMetadata, mcpConfiguration }) {
964
+ const entries = {
965
+ plugin_manifest: pluginManifest,
966
+ skill,
967
+ skill_metadata: skillMetadata,
968
+ mcp_configuration: mcpConfiguration,
969
+ };
970
+ const result = {};
971
+ for (const [name, path] of Object.entries(entries)) {
972
+ if (!path) throw new Error(`resource path ${name} is required`);
973
+ result[name] = createHash("sha256").update(await readFile(path)).digest("hex");
974
+ }
975
+ return result;
976
+ }
977
+
978
+ export function receiptOwnershipMatches(left, right) {
979
+ try {
980
+ const first = validateReceipt(left);
981
+ const second = validateReceipt(right);
982
+ return stableJSON(ownershipProjection(first)) === stableJSON(ownershipProjection(second));
983
+ } catch {
984
+ return false;
985
+ }
986
+ }
987
+
988
+ function assertCompatibleReceiptUpgrade(previousReceipt, currentReceipt) {
989
+ const previous = validateReceipt(previousReceipt);
990
+ const current = validateReceipt(currentReceipt);
991
+ const order = compareSemver(
992
+ parseSemver(current.product.version, "current product version"),
993
+ parseSemver(previous.product.version, "previous product version"),
994
+ );
995
+ if (order < 0) {
996
+ throw new Error("package downgrade is not allowed; setup made no changes");
997
+ }
998
+ if (order === 0) {
999
+ throw new Error("registration receipt ownership conflict; setup made no changes");
1000
+ }
1001
+ if (stableJSON(upgradeOwnershipProjection(previous)) !== stableJSON(upgradeOwnershipProjection(current))) {
1002
+ throw new Error("registration receipt ownership conflict; setup made no changes");
1003
+ }
1004
+ }
1005
+
1006
+ function upgradeOwnershipProjection(receipt) {
1007
+ return {
1008
+ schema_version: receipt.schema_version,
1009
+ product: {
1010
+ name: receipt.product.name,
1011
+ codex_compatibility: receipt.product.codex_compatibility,
1012
+ },
1013
+ host: {
1014
+ surface: receipt.host.surface,
1015
+ os: receipt.host.os,
1016
+ arch: receipt.host.arch,
1017
+ },
1018
+ registration: receipt.registration,
1019
+ paths: receipt.paths,
1020
+ };
1021
+ }
1022
+
1023
+ function ownershipProjection(receipt) {
1024
+ return {
1025
+ schema_version: receipt.schema_version,
1026
+ product: receipt.product,
1027
+ host: receipt.host,
1028
+ registration: receipt.registration,
1029
+ paths: receipt.paths,
1030
+ resource_digests: receipt.resource_digests,
1031
+ };
1032
+ }
1033
+
1034
+ function stableJSON(value) {
1035
+ if (Array.isArray(value)) return `[${value.map(stableJSON).join(",")}]`;
1036
+ if (value && typeof value === "object") {
1037
+ return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableJSON(value[key])}`).join(",")}}`;
1038
+ }
1039
+ return JSON.stringify(value);
1040
+ }
1041
+
1042
+ function assertObject(value, label) {
1043
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
1044
+ throw new Error(`${label} must be an object`);
1045
+ }
1046
+ }
1047
+
1048
+ function assertExactKeys(value, requiredKeys, label) {
1049
+ for (const field of requiredKeys) {
1050
+ if (!Object.hasOwn(value, field)) throw new Error(`${label} missing field ${field}`);
1051
+ }
1052
+ const allowed = new Set(requiredKeys);
1053
+ for (const field of Object.keys(value)) {
1054
+ if (!allowed.has(field)) throw new Error(`${label} has unexpected field ${field}`);
1055
+ }
1056
+ }
1057
+
1058
+ function assertEqual(actual, expected, label) {
1059
+ if (actual !== expected) {
1060
+ throw new Error(`${label} must equal ${JSON.stringify(expected)}; got ${JSON.stringify(actual)}`);
1061
+ }
1062
+ }
1063
+
1064
+ function assertCanonicalAbsolutePath(value, label) {
1065
+ if (typeof value !== "string" || !isAbsolute(value)) {
1066
+ throw new Error(`${label} must be an absolute path`);
1067
+ }
1068
+ if (resolve(value) !== value) throw new Error(`${label} must be canonical`);
1069
+ }
1070
+
1071
+ function parseSemver(value, label) {
1072
+ if (typeof value !== "string") throw new Error(`${label} must be a SemVer string`);
1073
+ const match = semverPattern.exec(value);
1074
+ if (!match) throw new Error(`${label} must be valid SemVer`);
1075
+ const prerelease = match[4] ? match[4].split(".") : [];
1076
+ for (const identifier of prerelease) {
1077
+ if (/^[0-9]+$/.test(identifier) && identifier.length > 1 && identifier.startsWith("0")) {
1078
+ throw new Error(`${label} has an invalid numeric prerelease identifier`);
1079
+ }
1080
+ }
1081
+ return { major: Number(match[1]), minor: Number(match[2]), patch: Number(match[3]), prerelease };
1082
+ }
1083
+
1084
+ function compareSemver(left, right) {
1085
+ for (const field of ["major", "minor", "patch"]) {
1086
+ if (left[field] !== right[field]) return left[field] < right[field] ? -1 : 1;
1087
+ }
1088
+ if (left.prerelease.length === 0 || right.prerelease.length === 0) {
1089
+ if (left.prerelease.length === right.prerelease.length) return 0;
1090
+ return left.prerelease.length === 0 ? 1 : -1;
1091
+ }
1092
+ const count = Math.max(left.prerelease.length, right.prerelease.length);
1093
+ for (let index = 0; index < count; index += 1) {
1094
+ const a = left.prerelease[index];
1095
+ const b = right.prerelease[index];
1096
+ if (a === undefined || b === undefined) return a === undefined ? -1 : 1;
1097
+ if (a === b) continue;
1098
+ const aNumeric = /^[0-9]+$/.test(a);
1099
+ const bNumeric = /^[0-9]+$/.test(b);
1100
+ if (aNumeric && bNumeric) return Number(a) < Number(b) ? -1 : 1;
1101
+ if (aNumeric !== bNumeric) return aNumeric ? -1 : 1;
1102
+ return a < b ? -1 : 1;
1103
+ }
1104
+ return 0;
1105
+ }
1106
+
1107
+ async function assertNoSymbolicLinkComponents(root, candidate) {
1108
+ const offset = relative(root, candidate);
1109
+ if (offset === ".." || offset.startsWith(`..${sep}`) || isAbsolute(offset)) {
1110
+ throw new Error("receipt path escapes its owned root");
1111
+ }
1112
+ let current = root;
1113
+ for (const component of offset.split(sep).filter(Boolean)) {
1114
+ current = join(current, component);
1115
+ try {
1116
+ const info = await lstat(current);
1117
+ if (info.isSymbolicLink()) throw new Error(`receipt path contains a symbolic link: ${current}`);
1118
+ } catch (error) {
1119
+ if (error?.code === "ENOENT") return;
1120
+ throw error;
1121
+ }
1122
+ }
1123
+ }
1124
+
1125
+ async function rejectSymbolicLink(path) {
1126
+ try {
1127
+ if ((await lstat(path)).isSymbolicLink()) {
1128
+ throw new Error(`receipt target is a symbolic link: ${path}`);
1129
+ }
1130
+ } catch (error) {
1131
+ if (error?.code !== "ENOENT") throw error;
1132
+ }
1133
+ }