yam-flow 2.7.0 → 2.8.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/AGENTS.md +1 -0
- package/CHANGELOG.md +29 -0
- package/COMMANDS.md +42 -5
- package/README.md +40 -8
- package/ROADMAP.md +17 -6
- package/dist/bin/yam.js +441 -70
- package/dist/lib/external-updates.d.ts +9 -0
- package/dist/lib/external-updates.js +232 -4
- package/dist/lib/next-step.d.ts +61 -0
- package/dist/lib/next-step.js +340 -0
- package/dist/lib/release-auth-readiness.d.ts +81 -0
- package/dist/lib/release-auth-readiness.js +106 -0
- package/dist/lib/release-registry-status.d.ts +18 -0
- package/dist/lib/release-registry-status.js +37 -0
- package/dist/lib/scout-receipt.d.ts +89 -0
- package/dist/lib/scout-receipt.js +542 -0
- package/dist/lib/skill-installation.d.ts +30 -0
- package/dist/lib/skill-installation.js +470 -82
- package/dist/lib/trust-kernel.d.ts +2 -0
- package/dist/lib/trust-kernel.js +21 -2
- package/package.json +22 -3
- package/references/final-report.md +15 -1
- package/references/next-step.md +67 -0
- package/references/scout-wide-scan.md +16 -0
- package/references/study-note.md +4 -2
- package/references/tool-trust-layer.md +2 -0
- package/skills/deep/SKILL.md +2 -0
- package/skills/mission/SKILL.md +2 -0
- package/skills/quick/SKILL.md +1 -0
- package/skills/scout/SKILL.md +19 -9
- package/skills/ueye/SKILL.md +1 -0
- package/src/bin/yam.ts +445 -68
- package/src/lib/external-updates.ts +262 -4
- package/src/lib/next-step.ts +397 -0
- package/src/lib/release-auth-readiness.ts +148 -0
- package/src/lib/release-registry-status.ts +55 -0
- package/src/lib/scout-receipt.ts +593 -0
- package/src/lib/skill-installation.ts +542 -87
- package/src/lib/trust-kernel.ts +25 -2
- package/yam.manifest.json +4 -3
|
@@ -54,6 +54,14 @@ export interface ExternalComponentCheck {
|
|
|
54
54
|
drift: boolean;
|
|
55
55
|
note: string;
|
|
56
56
|
};
|
|
57
|
+
source_receipt?: {
|
|
58
|
+
canonical_url: string;
|
|
59
|
+
retrieved_at: string;
|
|
60
|
+
access_path: string;
|
|
61
|
+
version: string;
|
|
62
|
+
revision: string;
|
|
63
|
+
integrity: string[];
|
|
64
|
+
};
|
|
57
65
|
executable?: string;
|
|
58
66
|
error?: string;
|
|
59
67
|
}
|
|
@@ -68,6 +76,7 @@ export interface ExternalComponentReceipt {
|
|
|
68
76
|
installed_version: string;
|
|
69
77
|
source: string;
|
|
70
78
|
source_revision: JsonRecord | null;
|
|
79
|
+
source_receipt: JsonRecord | null;
|
|
71
80
|
checks: Array<{
|
|
72
81
|
id: string;
|
|
73
82
|
status: 'passed' | 'failed' | 'skipped';
|
|
@@ -129,7 +138,9 @@ interface YamRollbackResult {
|
|
|
129
138
|
}
|
|
130
139
|
|
|
131
140
|
const YAM_REGISTRY_URL = 'https://registry.npmjs.org/yam-flow/latest';
|
|
141
|
+
const NPM_REGISTRY = 'https://registry.npmjs.org/';
|
|
132
142
|
const SCRAPLING_PYPI_URL = 'https://pypi.org/pypi/scrapling/json';
|
|
143
|
+
const SCRAPLING_PYPI_INDEX = 'https://pypi.org/simple';
|
|
133
144
|
const INSANE_MARKETPLACE = 'gptaku-codex';
|
|
134
145
|
const INSANE_PLUGIN_ID = 'insane-search-codex@gptaku-codex';
|
|
135
146
|
const INSANE_MARKETPLACE_SOURCE = 'https://github.com/fivetaku/gptaku-plugins-codex.git';
|
|
@@ -402,7 +413,19 @@ async function checkYam(currentYamVersion: string, deps: ResolvedDependencies):
|
|
|
402
413
|
const data = await deps.fetchJson(YAM_REGISTRY_URL);
|
|
403
414
|
const latest = requireStableVersion(data?.version, 'yam registry version');
|
|
404
415
|
const installed = requireStableVersion(currentYamVersion, 'installed yam version');
|
|
405
|
-
|
|
416
|
+
const integrity = requireIntegrityValue(data?.dist?.integrity, 'yam registry integrity');
|
|
417
|
+
const revision = requireGitRevision(data?.gitHead, 'yam registry gitHead');
|
|
418
|
+
return {
|
|
419
|
+
...componentCheck('yam', installed, latest, 'npm:yam-flow'),
|
|
420
|
+
source_receipt: {
|
|
421
|
+
canonical_url: YAM_REGISTRY_URL,
|
|
422
|
+
retrieved_at: deps.now().toISOString(),
|
|
423
|
+
access_path: 'npm registry latest endpoint',
|
|
424
|
+
version: latest,
|
|
425
|
+
revision,
|
|
426
|
+
integrity: [integrity]
|
|
427
|
+
}
|
|
428
|
+
};
|
|
406
429
|
} catch (error) {
|
|
407
430
|
return failedCheck('yam', currentYamVersion, 'npm:yam-flow', error);
|
|
408
431
|
}
|
|
@@ -412,6 +435,7 @@ async function checkScrapling(deps: ResolvedDependencies): Promise<ExternalCompo
|
|
|
412
435
|
try {
|
|
413
436
|
const data = await deps.fetchJson(SCRAPLING_PYPI_URL);
|
|
414
437
|
const latest = requireStableVersion(data?.info?.version, 'Scrapling PyPI version');
|
|
438
|
+
const releaseIntegrity = pypiReleaseIntegrity(data?.urls, latest);
|
|
415
439
|
const executable = deps.paths.scraplingBin;
|
|
416
440
|
const versionResult = await deps.run(executable, ['--version'], { env: deps.env, timeout: 30000 });
|
|
417
441
|
if (!versionResult.ok) {
|
|
@@ -422,12 +446,28 @@ async function checkScrapling(deps: ResolvedDependencies): Promise<ExternalCompo
|
|
|
422
446
|
update_available: true,
|
|
423
447
|
status: 'not_installed',
|
|
424
448
|
source: 'pypi:scrapling',
|
|
449
|
+
source_receipt: {
|
|
450
|
+
canonical_url: SCRAPLING_PYPI_URL,
|
|
451
|
+
retrieved_at: deps.now().toISOString(),
|
|
452
|
+
access_path: 'PyPI project JSON endpoint',
|
|
453
|
+
version: latest,
|
|
454
|
+
revision: '',
|
|
455
|
+
integrity: releaseIntegrity
|
|
456
|
+
},
|
|
425
457
|
executable
|
|
426
458
|
};
|
|
427
459
|
}
|
|
428
460
|
const installed = requireStableVersion(extractVersion(versionResult.stdout), 'installed Scrapling version');
|
|
429
461
|
return {
|
|
430
462
|
...componentCheck('scrapling', installed, latest, 'pypi:scrapling'),
|
|
463
|
+
source_receipt: {
|
|
464
|
+
canonical_url: SCRAPLING_PYPI_URL,
|
|
465
|
+
retrieved_at: deps.now().toISOString(),
|
|
466
|
+
access_path: 'PyPI project JSON endpoint',
|
|
467
|
+
version: latest,
|
|
468
|
+
revision: '',
|
|
469
|
+
integrity: releaseIntegrity
|
|
470
|
+
},
|
|
431
471
|
executable
|
|
432
472
|
};
|
|
433
473
|
} catch (error) {
|
|
@@ -510,6 +550,14 @@ async function checkInsaneSearch(deps: ResolvedDependencies): Promise<ExternalCo
|
|
|
510
550
|
note: installed && compareVersions(installed, latest) === 0
|
|
511
551
|
? 'marketplace revision drift alone is not treated as an Insane Search update'
|
|
512
552
|
: 'plugin update availability is based on the official plugin manifest version'
|
|
553
|
+
},
|
|
554
|
+
source_receipt: {
|
|
555
|
+
canonical_url: manifestUrl,
|
|
556
|
+
retrieved_at: deps.now().toISOString(),
|
|
557
|
+
access_path: 'Git-pinned raw marketplace manifest',
|
|
558
|
+
version: latest,
|
|
559
|
+
revision: remoteRevision,
|
|
560
|
+
integrity: [`git_commit:${remoteRevision.toLowerCase()}`]
|
|
513
561
|
}
|
|
514
562
|
};
|
|
515
563
|
} catch (error) {
|
|
@@ -565,7 +613,13 @@ async function applyYam(currentYamVersion: string, deps: ResolvedDependencies) {
|
|
|
565
613
|
const checks: ExternalComponentReceipt['checks'] = [];
|
|
566
614
|
const sideEffects: string[] = [];
|
|
567
615
|
const exactPackage = `yam-flow@${check.latest_version}`;
|
|
568
|
-
const install = await deps.run('npm', [
|
|
616
|
+
const install = await deps.run('npm', [
|
|
617
|
+
'install',
|
|
618
|
+
'-g',
|
|
619
|
+
exactPackage,
|
|
620
|
+
'--registry',
|
|
621
|
+
NPM_REGISTRY
|
|
622
|
+
], {
|
|
569
623
|
env: deps.env,
|
|
570
624
|
timeout: 600000
|
|
571
625
|
});
|
|
@@ -649,7 +703,13 @@ async function rollbackYam(
|
|
|
649
703
|
checks: ExternalComponentReceipt['checks']
|
|
650
704
|
): Promise<YamRollbackResult> {
|
|
651
705
|
if (!previousVersion) return { ok: false };
|
|
652
|
-
const result = await deps.run('npm', [
|
|
706
|
+
const result = await deps.run('npm', [
|
|
707
|
+
'install',
|
|
708
|
+
'-g',
|
|
709
|
+
`yam-flow@${previousVersion}`,
|
|
710
|
+
'--registry',
|
|
711
|
+
NPM_REGISTRY
|
|
712
|
+
], {
|
|
653
713
|
env: deps.env,
|
|
654
714
|
timeout: 600000
|
|
655
715
|
});
|
|
@@ -831,7 +891,15 @@ async function applyScrapling(deps: ResolvedDependencies) {
|
|
|
831
891
|
const candidatePython = path.join(candidate, 'bin', 'python');
|
|
832
892
|
const candidateScrapling = path.join(candidate, 'bin', 'scrapling');
|
|
833
893
|
const packageSpec = `scrapling[fetchers]==${check.latest_version}`;
|
|
834
|
-
const install = await deps.run(candidatePython, [
|
|
894
|
+
const install = await deps.run(candidatePython, [
|
|
895
|
+
'-m',
|
|
896
|
+
'pip',
|
|
897
|
+
'install',
|
|
898
|
+
'--isolated',
|
|
899
|
+
'--index-url',
|
|
900
|
+
SCRAPLING_PYPI_INDEX,
|
|
901
|
+
packageSpec
|
|
902
|
+
], {
|
|
835
903
|
env: deps.env,
|
|
836
904
|
timeout: 600000
|
|
837
905
|
});
|
|
@@ -970,6 +1038,16 @@ async function applyInsaneSearch(deps: ResolvedDependencies) {
|
|
|
970
1038
|
}), deps);
|
|
971
1039
|
}
|
|
972
1040
|
|
|
1041
|
+
const pinnedSource = await verifyUpgradedInsaneSource(check, deps, 'pre_add_source_revision');
|
|
1042
|
+
checks.push(pinnedSource.check);
|
|
1043
|
+
if (!pinnedSource.ok) {
|
|
1044
|
+
return persistReceipt(baseReceipt(check, 'failed', checks, insaneRollback(check.installed_version), {
|
|
1045
|
+
installed_version: check.installed_version,
|
|
1046
|
+
side_effects: ['The official Codex marketplace snapshot was upgraded; plugin add was blocked because the post-upgrade source did not match the checked revision.'],
|
|
1047
|
+
error: pinnedSource.error
|
|
1048
|
+
}), deps);
|
|
1049
|
+
}
|
|
1050
|
+
|
|
973
1051
|
let observation = await installedInsaneVersion(deps, checks, 'plugin_list_after_marketplace_upgrade');
|
|
974
1052
|
if (!observation.ok) {
|
|
975
1053
|
return persistReceipt(baseReceipt(check, 'failed', checks, insaneRollback(check.installed_version), {
|
|
@@ -1011,6 +1089,21 @@ async function applyInsaneSearch(deps: ResolvedDependencies) {
|
|
|
1011
1089
|
}), deps);
|
|
1012
1090
|
}
|
|
1013
1091
|
|
|
1092
|
+
const finalPinnedSource = await verifyUpgradedInsaneSource(check, deps, 'final_source_revision');
|
|
1093
|
+
checks.push(finalPinnedSource.check);
|
|
1094
|
+
if (!finalPinnedSource.ok) {
|
|
1095
|
+
return persistReceipt(baseReceipt(check, 'failed', checks, insaneRollback(check.installed_version), {
|
|
1096
|
+
installed_version: installedAfter,
|
|
1097
|
+
side_effects: [
|
|
1098
|
+
`upgraded official marketplace ${INSANE_MARKETPLACE}`,
|
|
1099
|
+
installedAfter === check.latest_version
|
|
1100
|
+
? `the plugin reached ${installedAfter}, but final source provenance changed and completion was blocked`
|
|
1101
|
+
: 'plugin state did not reach the checked version'
|
|
1102
|
+
],
|
|
1103
|
+
error: finalPinnedSource.error
|
|
1104
|
+
}), deps);
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1014
1107
|
return persistReceipt(baseReceipt(check, 'updated', checks, insaneRollback(check.installed_version), {
|
|
1015
1108
|
installed_version: installedAfter,
|
|
1016
1109
|
side_effects: [
|
|
@@ -1021,6 +1114,131 @@ async function applyInsaneSearch(deps: ResolvedDependencies) {
|
|
|
1021
1114
|
}), deps);
|
|
1022
1115
|
}
|
|
1023
1116
|
|
|
1117
|
+
async function verifyUpgradedInsaneSource(
|
|
1118
|
+
check: ExternalComponentCheck,
|
|
1119
|
+
deps: ResolvedDependencies,
|
|
1120
|
+
id: string
|
|
1121
|
+
) {
|
|
1122
|
+
try {
|
|
1123
|
+
const expectedRevision = requireGitRevision(
|
|
1124
|
+
check.source_receipt?.revision,
|
|
1125
|
+
'checked Insane Search source revision'
|
|
1126
|
+
);
|
|
1127
|
+
const marketplaceList = await deps.run('codex', ['plugin', 'marketplace', 'list', '--json'], {
|
|
1128
|
+
env: deps.env,
|
|
1129
|
+
timeout: 30000
|
|
1130
|
+
});
|
|
1131
|
+
if (!marketplaceList.ok) {
|
|
1132
|
+
throw new Error(`post-upgrade marketplace state could not be observed: ${commandNote(marketplaceList)}`);
|
|
1133
|
+
}
|
|
1134
|
+
const parsedMarketplaces = parseJsonOutput(marketplaceList.stdout, 'post-upgrade codex plugin marketplace list');
|
|
1135
|
+
const marketplaceEntry = arrayValue(parsedMarketplaces?.marketplaces).find((item) => item?.name === INSANE_MARKETPLACE);
|
|
1136
|
+
if (!marketplaceEntry) throw new Error(`post-upgrade marketplace is missing: ${INSANE_MARKETPLACE}`);
|
|
1137
|
+
const configuredSource = String(marketplaceEntry?.marketplaceSource?.source || '');
|
|
1138
|
+
if (
|
|
1139
|
+
marketplaceEntry?.marketplaceSource?.sourceType !== 'git'
|
|
1140
|
+
|| configuredSource !== INSANE_MARKETPLACE_SOURCE
|
|
1141
|
+
) {
|
|
1142
|
+
throw new Error(`refusing unexpected post-upgrade ${INSANE_MARKETPLACE} marketplace source`);
|
|
1143
|
+
}
|
|
1144
|
+
const marketplaceRoot = String(marketplaceEntry.root || '');
|
|
1145
|
+
if (!marketplaceRoot || !isSafeMarketplaceRoot(marketplaceRoot, deps.homeDir)) {
|
|
1146
|
+
throw new Error(`refusing unsafe post-upgrade ${INSANE_MARKETPLACE} marketplace root`);
|
|
1147
|
+
}
|
|
1148
|
+
const localRevisionResult = await deps.run('git', ['-C', marketplaceRoot, 'rev-parse', 'HEAD'], {
|
|
1149
|
+
env: deps.env,
|
|
1150
|
+
timeout: 30000
|
|
1151
|
+
});
|
|
1152
|
+
if (!localRevisionResult.ok) {
|
|
1153
|
+
throw new Error(`post-upgrade marketplace revision could not be read: ${commandNote(localRevisionResult)}`);
|
|
1154
|
+
}
|
|
1155
|
+
const actualRevision = requireGitRevision(
|
|
1156
|
+
localRevisionResult.stdout.trim(),
|
|
1157
|
+
'post-upgrade Insane Search marketplace revision'
|
|
1158
|
+
);
|
|
1159
|
+
const statusResult = await deps.run('git', [
|
|
1160
|
+
'-C',
|
|
1161
|
+
marketplaceRoot,
|
|
1162
|
+
'status',
|
|
1163
|
+
'--porcelain=v1',
|
|
1164
|
+
'--untracked-files=all',
|
|
1165
|
+
'--ignored=matching'
|
|
1166
|
+
], { env: deps.env, timeout: 30000 });
|
|
1167
|
+
if (!statusResult.ok) {
|
|
1168
|
+
throw new Error(`post-upgrade marketplace cleanliness could not be observed: ${commandNote(statusResult)}`);
|
|
1169
|
+
}
|
|
1170
|
+
if (statusResult.stdout.trim()) {
|
|
1171
|
+
throw new Error('post-upgrade Insane Search marketplace working tree is dirty; refusing to bind plugin install to a clean commit receipt');
|
|
1172
|
+
}
|
|
1173
|
+
const trackedManifest = await deps.run('git', [
|
|
1174
|
+
'-C',
|
|
1175
|
+
marketplaceRoot,
|
|
1176
|
+
'ls-files',
|
|
1177
|
+
'--error-unmatch',
|
|
1178
|
+
'--',
|
|
1179
|
+
INSANE_MANIFEST_PATH
|
|
1180
|
+
], { env: deps.env, timeout: 30000 });
|
|
1181
|
+
if (!trackedManifest.ok) {
|
|
1182
|
+
throw new Error(`local Insane Search manifest is not tracked at the checked commit: ${commandNote(trackedManifest)}`);
|
|
1183
|
+
}
|
|
1184
|
+
const localManifestPath = path.join(marketplaceRoot, INSANE_MANIFEST_PATH);
|
|
1185
|
+
assertChildPath(marketplaceRoot, localManifestPath, 'local Insane Search manifest');
|
|
1186
|
+
const localManifestStat = await fsp.lstat(localManifestPath);
|
|
1187
|
+
if (!localManifestStat.isFile() || localManifestStat.isSymbolicLink() || localManifestStat.size > 1024 * 1024) {
|
|
1188
|
+
throw new Error('local Insane Search manifest must be a regular tracked file no larger than 1 MiB');
|
|
1189
|
+
}
|
|
1190
|
+
const localManifestBytes = await fsp.readFile(localManifestPath, 'utf8');
|
|
1191
|
+
const committedManifest = await deps.run('git', [
|
|
1192
|
+
'-C',
|
|
1193
|
+
marketplaceRoot,
|
|
1194
|
+
'show',
|
|
1195
|
+
`${actualRevision}:${INSANE_MANIFEST_PATH}`
|
|
1196
|
+
], { env: deps.env, timeout: 30000 });
|
|
1197
|
+
if (!committedManifest.ok) {
|
|
1198
|
+
throw new Error(`checked Insane Search manifest bytes could not be read from Git: ${commandNote(committedManifest)}`);
|
|
1199
|
+
}
|
|
1200
|
+
if (localManifestBytes !== committedManifest.stdout) {
|
|
1201
|
+
throw new Error('local Insane Search manifest bytes do not match the checked Git commit');
|
|
1202
|
+
}
|
|
1203
|
+
const localManifest = parseJsonOutput(localManifestBytes, 'local Insane Search manifest');
|
|
1204
|
+
const localVersion = requireStableVersion(localManifest?.version, 'local Insane Search manifest version');
|
|
1205
|
+
const manifestUrl = `${INSANE_MANIFEST_ROOT}/${actualRevision}/${INSANE_MANIFEST_PATH}`;
|
|
1206
|
+
const manifest = await deps.fetchJson(manifestUrl);
|
|
1207
|
+
const actualVersion = requireStableVersion(manifest?.version, 'post-upgrade Insane Search manifest version');
|
|
1208
|
+
if (actualRevision !== expectedRevision) {
|
|
1209
|
+
throw new Error(
|
|
1210
|
+
`Insane Search source changed between check and upgrade: checked ${expectedRevision}, observed ${actualRevision}; rerun the read-only check before retrying`
|
|
1211
|
+
);
|
|
1212
|
+
}
|
|
1213
|
+
if (actualVersion !== check.latest_version) {
|
|
1214
|
+
throw new Error(
|
|
1215
|
+
`post-upgrade Insane Search manifest version ${actualVersion} does not match checked ${check.latest_version}`
|
|
1216
|
+
);
|
|
1217
|
+
}
|
|
1218
|
+
if (localVersion !== actualVersion) {
|
|
1219
|
+
throw new Error(
|
|
1220
|
+
`local Insane Search manifest version ${localVersion} does not match pinned source version ${actualVersion}`
|
|
1221
|
+
);
|
|
1222
|
+
}
|
|
1223
|
+
return {
|
|
1224
|
+
ok: true,
|
|
1225
|
+
error: '',
|
|
1226
|
+
check: {
|
|
1227
|
+
id,
|
|
1228
|
+
status: 'passed' as const,
|
|
1229
|
+
note: `clean tracked marketplace remained pinned to ${actualRevision} with local and remote manifest version ${actualVersion}`
|
|
1230
|
+
}
|
|
1231
|
+
};
|
|
1232
|
+
} catch (error) {
|
|
1233
|
+
const note = boundedNote(errorMessage(error));
|
|
1234
|
+
return {
|
|
1235
|
+
ok: false,
|
|
1236
|
+
error: note,
|
|
1237
|
+
check: { id, status: 'failed' as const, note }
|
|
1238
|
+
};
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1024
1242
|
async function installedInsaneVersion(
|
|
1025
1243
|
deps: ResolvedDependencies,
|
|
1026
1244
|
checks: ExternalComponentReceipt['checks'],
|
|
@@ -1069,6 +1287,7 @@ function baseReceipt(
|
|
|
1069
1287
|
installed_version: check.installed_version,
|
|
1070
1288
|
source: check.source,
|
|
1071
1289
|
source_revision: receiptSourceRevision(check),
|
|
1290
|
+
source_receipt: check.source_receipt || null,
|
|
1072
1291
|
checks,
|
|
1073
1292
|
rollback_hint: rollbackHint,
|
|
1074
1293
|
side_effects: [],
|
|
@@ -1408,6 +1627,45 @@ function requireStableVersion(value: unknown, label: string) {
|
|
|
1408
1627
|
return version;
|
|
1409
1628
|
}
|
|
1410
1629
|
|
|
1630
|
+
function requireIntegrityValue(value: unknown, label: string) {
|
|
1631
|
+
const integrity = String(value || '').trim();
|
|
1632
|
+
if (!/^sha(?:256|384|512)-[A-Za-z0-9+/=]+$/.test(integrity)) {
|
|
1633
|
+
throw new Error(`${label} is missing or malformed`);
|
|
1634
|
+
}
|
|
1635
|
+
return integrity;
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1638
|
+
function requireGitRevision(value: unknown, label: string) {
|
|
1639
|
+
const revision = String(value || '').trim().toLowerCase();
|
|
1640
|
+
if (!/^[a-f0-9]{40}$/.test(revision)) throw new Error(`${label} is not a 40-character Git revision`);
|
|
1641
|
+
return revision;
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1644
|
+
function pypiReleaseIntegrity(value: unknown, version: string) {
|
|
1645
|
+
if (!Array.isArray(value) || value.length === 0) {
|
|
1646
|
+
throw new Error(`Scrapling PyPI ${version} has no SHA-256 release artifact inventory`);
|
|
1647
|
+
}
|
|
1648
|
+
if (value.length > 64) {
|
|
1649
|
+
throw new Error(`Scrapling PyPI ${version} release artifact inventory exceeds the 64-item provenance limit`);
|
|
1650
|
+
}
|
|
1651
|
+
const artifacts = value.map((item, index) => {
|
|
1652
|
+
const filename = typeof item?.filename === 'string' ? item.filename.trim() : '';
|
|
1653
|
+
const rawSha256 = item?.digests?.sha256;
|
|
1654
|
+
const sha256 = typeof rawSha256 === 'string' ? rawSha256.trim().toLowerCase() : '';
|
|
1655
|
+
if (!filename || filename.length > 512 || /[\r\n\u0000-\u001f\u007f]/.test(filename)) {
|
|
1656
|
+
throw new Error(`Scrapling PyPI ${version} release artifact ${index} has a malformed filename`);
|
|
1657
|
+
}
|
|
1658
|
+
if (!/^[a-f0-9]{64}$/.test(sha256)) {
|
|
1659
|
+
throw new Error(`Scrapling PyPI ${version} release artifact ${index} has a missing or malformed SHA-256 digest`);
|
|
1660
|
+
}
|
|
1661
|
+
return `sha256:${sha256} filename:${filename}`;
|
|
1662
|
+
}).sort((left, right) => left < right ? -1 : left > right ? 1 : 0);
|
|
1663
|
+
if (new Set(artifacts).size !== artifacts.length) {
|
|
1664
|
+
throw new Error(`Scrapling PyPI ${version} release artifact inventory contains duplicates`);
|
|
1665
|
+
}
|
|
1666
|
+
return artifacts;
|
|
1667
|
+
}
|
|
1668
|
+
|
|
1411
1669
|
function extractVersion(text: string) {
|
|
1412
1670
|
return String(text || '').match(/\b\d+(?:\.\d+){1,3}\b/)?.[0] || '';
|
|
1413
1671
|
}
|