@appstrate/core 2.0.0 → 2.1.1

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/package.json CHANGED
@@ -1,8 +1,10 @@
1
1
  {
2
2
  "name": "@appstrate/core",
3
- "version": "2.0.0",
3
+ "version": "2.1.1",
4
4
  "type": "module",
5
- "files": ["src"],
5
+ "files": [
6
+ "src"
7
+ ],
6
8
  "exports": {
7
9
  "./logger": "./src/logger.ts",
8
10
  "./rate-limit": "./src/rate-limit.ts",
@@ -17,7 +19,10 @@
17
19
  "./semver": "./src/semver.ts",
18
20
  "./registry-deps": "./src/registry-deps.ts",
19
21
  "./update-check": "./src/update-check.ts",
20
- "./publish-manifest": "./src/publish-manifest.ts"
22
+ "./publish-manifest": "./src/publish-manifest.ts",
23
+ "./dist-tags": "./src/dist-tags.ts",
24
+ "./version-policy": "./src/version-policy.ts",
25
+ "./download": "./src/download.ts"
21
26
  },
22
27
  "dependencies": {
23
28
  "fflate": "^0.8.0",
@@ -0,0 +1,12 @@
1
+ /** Regex for valid dist-tag names: lowercase alphanumeric, dots, hyphens, underscores. */
2
+ export const DIST_TAG_REGEX = /^[a-z][a-z0-9._-]*$/;
3
+
4
+ /** Check whether `tag` is a valid dist-tag name. */
5
+ export function isValidDistTag(tag: string): boolean {
6
+ return DIST_TAG_REGEX.test(tag);
7
+ }
8
+
9
+ /** Check whether `tag` is a protected tag that cannot be manually set or removed. */
10
+ export function isProtectedTag(tag: string): boolean {
11
+ return tag === "latest";
12
+ }
@@ -0,0 +1,54 @@
1
+ import { computeIntegrity } from "./integrity.ts";
2
+
3
+ // ─────────────────────────────────────────────
4
+ // Integrity verification
5
+ // ─────────────────────────────────────────────
6
+
7
+ export interface IntegrityCheckResult {
8
+ valid: boolean;
9
+ computed: string;
10
+ }
11
+
12
+ /** Verify artifact integrity by computing SHA256 hash and comparing to expected value. */
13
+ export function verifyArtifactIntegrity(
14
+ data: Uint8Array,
15
+ expectedIntegrity: string,
16
+ ): IntegrityCheckResult {
17
+ const computed = computeIntegrity(data);
18
+ return { valid: computed === expectedIntegrity, computed };
19
+ }
20
+
21
+ // ─────────────────────────────────────────────
22
+ // Download filename
23
+ // ─────────────────────────────────────────────
24
+
25
+ /** Build a consistent download filename: scope-name-version.zip */
26
+ export function buildDownloadFilename(scope: string, name: string, version: string): string {
27
+ const cleanScope = scope.replace(/^@/, "");
28
+ return `${cleanScope}-${name}-${version}.zip`;
29
+ }
30
+
31
+ // ─────────────────────────────────────────────
32
+ // Download response headers
33
+ // ─────────────────────────────────────────────
34
+
35
+ export interface DownloadHeadersInput {
36
+ integrity: string;
37
+ yanked: boolean;
38
+ scope: string;
39
+ name: string;
40
+ version: string;
41
+ }
42
+
43
+ /** Build standard download response headers (Content-Type, Content-Disposition, X-Integrity, X-Yanked). */
44
+ export function buildDownloadHeaders(meta: DownloadHeadersInput): Record<string, string> {
45
+ const headers: Record<string, string> = {
46
+ "Content-Type": "application/zip",
47
+ "X-Integrity": meta.integrity,
48
+ "Content-Disposition": `attachment; filename="${buildDownloadFilename(meta.scope, meta.name, meta.version)}"`,
49
+ };
50
+ if (meta.yanked) {
51
+ headers["X-Yanked"] = "true";
52
+ }
53
+ return headers;
54
+ }
package/src/semver.ts CHANGED
@@ -34,6 +34,11 @@ export function isPrerelease(v: string): boolean {
34
34
  return semver.prerelease(v) !== null;
35
35
  }
36
36
 
37
+ /** Auto-bump the patch segment of `currentVersion`. Returns null if invalid semver. */
38
+ export function bumpPatch(currentVersion: string): string | null {
39
+ return semver.inc(currentVersion, "patch");
40
+ }
41
+
37
42
  /** Returns true if `remote` is a strictly newer version than `installed`.
38
43
  * Returns false if either is null/undefined or invalid. */
39
44
  export function hasNewerVersion(
@@ -71,3 +76,51 @@ export function resolveLatestVersion(
71
76
 
72
77
  return versions.at(-1)?.version ?? null;
73
78
  }
79
+
80
+ export interface CatalogVersion {
81
+ id: number;
82
+ version: string;
83
+ yanked: boolean;
84
+ }
85
+
86
+ /**
87
+ * Resolve a version query against a catalog of versions and dist-tags.
88
+ * 3-step resolution: exact match → dist-tag → semver range.
89
+ *
90
+ * - Exact match includes yanked versions (like npm/crates.io: exact pins always resolve).
91
+ * - Dist-tag lookup excludes yanked versions.
92
+ * - Semver range excludes yanked versions.
93
+ *
94
+ * Returns the version id, or null if no match.
95
+ */
96
+ export function resolveVersionFromCatalog(
97
+ query: string,
98
+ versions: CatalogVersion[],
99
+ distTags: DistTagEntry[],
100
+ ): number | null {
101
+ // 1. Exact match — includes yanked
102
+ if (isValidVersion(query)) {
103
+ const exact = versions.find((v) => v.version === query);
104
+ if (exact) return exact.id;
105
+ return null;
106
+ }
107
+
108
+ // 2. Dist-tag — excludes yanked
109
+ const tagEntry = distTags.find((t) => t.tag === query);
110
+ if (tagEntry) {
111
+ const tagged = versions.find((v) => v.id === tagEntry.versionId && !v.yanked);
112
+ if (tagged) return tagged.id;
113
+ }
114
+
115
+ // 3. Semver range — excludes yanked
116
+ if (isValidRange(query)) {
117
+ const nonYanked = versions.filter((v) => !v.yanked);
118
+ const versionStrings = nonYanked.map((v) => v.version).filter(isValidVersion);
119
+ const best = matchVersion(versionStrings, query);
120
+ if (!best) return null;
121
+ const match = nonYanked.find((v) => v.version === best);
122
+ return match?.id ?? null;
123
+ }
124
+
125
+ return null;
126
+ }
@@ -0,0 +1,66 @@
1
+ import { isValidVersion, versionGt, compareVersionsDesc, isPrerelease } from "./semver.ts";
2
+
3
+ export type ForwardVersionError = "VERSION_EXISTS" | "VERSION_NOT_HIGHER";
4
+
5
+ export interface ForwardVersionResult {
6
+ ok: boolean;
7
+ error?: ForwardVersionError;
8
+ highest?: string;
9
+ }
10
+
11
+ /**
12
+ * Validate that `newVersion` is strictly higher than all existing versions.
13
+ * Callers must pass ALL versions (including yanked) to prevent re-publishing.
14
+ * Rejects duplicates and versions not higher than the current maximum.
15
+ */
16
+ export function validateForwardVersion(
17
+ newVersion: string,
18
+ existingVersions: string[],
19
+ ): ForwardVersionResult {
20
+ if (existingVersions.includes(newVersion)) {
21
+ return { ok: false, error: "VERSION_EXISTS" };
22
+ }
23
+
24
+ const validVersions = existingVersions.filter(isValidVersion);
25
+ if (validVersions.length === 0) {
26
+ return { ok: true };
27
+ }
28
+
29
+ const highest = validVersions.sort(compareVersionsDesc)[0]!;
30
+ if (!versionGt(newVersion, highest)) {
31
+ return { ok: false, error: "VERSION_NOT_HIGHER", highest };
32
+ }
33
+
34
+ return { ok: true };
35
+ }
36
+
37
+ /**
38
+ * Find the best candidate for dist-tag reassignment after a yank:
39
+ * highest non-yanked, non-prerelease version.
40
+ */
41
+ export function findBestStableVersion(
42
+ candidates: Array<{ id: number; version: string }>,
43
+ ): { id: number; version: string } | null {
44
+ const stable = candidates.filter((v) => !isPrerelease(v.version));
45
+ if (stable.length === 0) return null;
46
+
47
+ const sorted = stable.sort((a, b) => compareVersionsDesc(a.version, b.version));
48
+ return sorted[0]!;
49
+ }
50
+
51
+ /**
52
+ * Determine whether a newly published version should replace the "latest" dist-tag.
53
+ * Prereleases never update "latest". A stable version updates "latest" only if
54
+ * there is no current latest or the new version is >= the current one.
55
+ */
56
+ export function shouldUpdateLatestTag(
57
+ newVersion: string,
58
+ currentLatestVersion: string | null,
59
+ ): boolean {
60
+ if (isPrerelease(newVersion)) return false;
61
+ if (!currentLatestVersion) return true;
62
+ return !versionGt(currentLatestVersion, newVersion);
63
+ }
64
+
65
+ // Re-export bumpPatch from semver.ts for backward compatibility
66
+ export { bumpPatch } from "./semver.ts";