@red-hat-developer-hub/backstage-plugin-ai-skills-common 0.3.0 → 0.4.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/CHANGELOG.md +12 -0
- package/README.md +69 -4
- package/dist/catalog-helpers.cjs.js +7 -8
- package/dist/catalog-helpers.cjs.js.map +1 -1
- package/dist/catalog-helpers.esm.js +6 -7
- package/dist/catalog-helpers.esm.js.map +1 -1
- package/dist/fixtures.cjs.js +126 -0
- package/dist/fixtures.cjs.js.map +1 -1
- package/dist/fixtures.esm.js +110 -1
- package/dist/fixtures.esm.js.map +1 -1
- package/dist/index.cjs.js +23 -0
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +457 -8
- package/dist/index.esm.js +3 -1
- package/dist/index.esm.js.map +1 -1
- package/dist/normalizer.cjs.js +344 -0
- package/dist/normalizer.cjs.js.map +1 -0
- package/dist/normalizer.esm.js +341 -0
- package/dist/normalizer.esm.js.map +1 -0
- package/dist/tag-constants.cjs.js +8 -0
- package/dist/tag-constants.cjs.js.map +1 -0
- package/dist/tag-constants.esm.js +5 -0
- package/dist/tag-constants.esm.js.map +1 -0
- package/package.json +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @red-hat-developer-hub/backstage-plugin-ai-skills-common
|
|
2
2
|
|
|
3
|
+
## 0.4.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- e526768: Add pure OCI and npx metadata normalizers implementing design D3 native-field mappings and precedence, with input types, normalization diagnostics, shared catalog-valid tag processing, author string normalization, allowlisted extension mapping, and reusable native metadata fixtures including conflicting-version scenarios.
|
|
8
|
+
|
|
9
|
+
Both metadata normalizers and the existing `normalizeTags` helper share Backstage's catalog tag rules. Dots, underscores, consecutive hyphens, and tags longer than 63 characters are omitted with diagnostics. For example, `rhdh:2`, `version+beta`, and `tag#1` remain valid; `rhdh:2.2`, `has_underscore`, and `in--valid` are omitted. Development-only parity tests compare all three normalization paths with Backstage's catalog validator.
|
|
10
|
+
|
|
11
|
+
The OCI trusted-input fixture uses the tagged key `quay.io/octo/hello-world-skill:1.0.0-draft`, consistent with the per-tag identity contract. Its digest-addressed source URI is unchanged.
|
|
12
|
+
|
|
13
|
+
The `normalizeTags` diagnostic messages now use "omitted" instead of "skipped". Their wording is informational and may change.
|
|
14
|
+
|
|
3
15
|
## 0.3.0
|
|
4
16
|
|
|
5
17
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -41,6 +41,69 @@ reference construction and declared-version mapping are unchanged.
|
|
|
41
41
|
| `MAX_RESPONSE_BYTES` | `5 * 1024 * 1024` | Maximum response read size for consumers (5 MiB) |
|
|
42
42
|
| `SUPPORTED_SCHEMA_VERSION` | `'1'` | Supported schema version |
|
|
43
43
|
|
|
44
|
+
## Normalization
|
|
45
|
+
|
|
46
|
+
Pure metadata normalizers implementing design D3 native-field mappings
|
|
47
|
+
and precedence. These functions accept already-parsed, verified inputs
|
|
48
|
+
and return schema-valid `SkillRecord`s or explicit per-record
|
|
49
|
+
diagnostics. They perform no network, scheduler, database, or catalog
|
|
50
|
+
operations.
|
|
51
|
+
|
|
52
|
+
### Functions
|
|
53
|
+
|
|
54
|
+
| Function | Description |
|
|
55
|
+
| ---------------------- | ---------------------------------------------------------- |
|
|
56
|
+
| `normalizeOciMetadata` | Normalizes OCI SkillCard and Markdown frontmatter metadata |
|
|
57
|
+
| `normalizeNpxMetadata` | Normalizes npx discovery entry and Markdown frontmatter |
|
|
58
|
+
|
|
59
|
+
### Input types
|
|
60
|
+
|
|
61
|
+
| Type | Description |
|
|
62
|
+
| ------------------------ | ----------------------------------------------------------- |
|
|
63
|
+
| `OciNativeInput` | Combined OCI SkillCard and frontmatter for normalization |
|
|
64
|
+
| `OciSkillCard` | Parsed OCI SkillCard document |
|
|
65
|
+
| `OciSkillCardMetadata` | Parsed OCI SkillCard metadata fields |
|
|
66
|
+
| `OciSkillCardSpec` | Parsed OCI SkillCard spec fields |
|
|
67
|
+
| `OciMarkdownFrontmatter` | Parsed OCI Markdown frontmatter |
|
|
68
|
+
| `NpxNativeInput` | Combined npx discovery entry and frontmatter |
|
|
69
|
+
| `NpxDiscoveryEntry` | Parsed npx discovery entry |
|
|
70
|
+
| `NpxFrontmatter` | Parsed npx Markdown frontmatter |
|
|
71
|
+
| `TrustedSourceInput` | Trusted source identity/integrity fields from the connector |
|
|
72
|
+
|
|
73
|
+
### Result types
|
|
74
|
+
|
|
75
|
+
| Type | Description |
|
|
76
|
+
| ------------------------- | -------------------------------------------------------- |
|
|
77
|
+
| `NormalizationResult<T>` | Contains the normalized record (or null) and diagnostics |
|
|
78
|
+
| `NormalizationDiagnostic` | A single diagnostic with field name and message |
|
|
79
|
+
|
|
80
|
+
The metadata normalizers return diagnostics as `{ field, message }` objects
|
|
81
|
+
because they process multiple metadata fields. The existing `normalizeTags`
|
|
82
|
+
catalog helper returns diagnostic strings for its single tag input. Both shapes
|
|
83
|
+
are intentional. Diagnostic message text is informational and may change.
|
|
84
|
+
|
|
85
|
+
### Constants
|
|
86
|
+
|
|
87
|
+
| Constant | Value | Description |
|
|
88
|
+
| --------------------- | ----- | --------------------------------------------- |
|
|
89
|
+
| `MAX_TAG_LENGTH` | `63` | Maximum tag length for Backstage catalog tags |
|
|
90
|
+
| `CATALOG_TAG_PATTERN` | regex | Backstage catalog tag validation pattern |
|
|
91
|
+
|
|
92
|
+
### Behavior
|
|
93
|
+
|
|
94
|
+
- **D3 precedence**: Each field has an ordered list of candidates.
|
|
95
|
+
The first non-empty, correctly typed value wins.
|
|
96
|
+
- **Tag processing**: Tags are trimmed, lowercased, deduplicated, and
|
|
97
|
+
validated against `CATALOG_TAG_PATTERN`. Invalid or overlength tags
|
|
98
|
+
are omitted with diagnostics.
|
|
99
|
+
- **Author normalization**: String authors are normalized to
|
|
100
|
+
`[{ name: value }]`. Array authors require a non-empty `name`.
|
|
101
|
+
Invalid entries are omitted with diagnostics.
|
|
102
|
+
- **Extension allowlisting**: Only `extensions.oci.namespace`,
|
|
103
|
+
`extensions.oci.prompt`, and `extensions.npx.type` are preserved.
|
|
104
|
+
- **Diagnostics**: Non-string candidates at any priority level emit
|
|
105
|
+
a diagnostic even when a valid lower-priority candidate is selected.
|
|
106
|
+
|
|
44
107
|
## Validation
|
|
45
108
|
|
|
46
109
|
- `validateSnapshot(snapshot)` — validates a complete v1 snapshot
|
|
@@ -98,10 +161,12 @@ Catalog side effects.
|
|
|
98
161
|
name from an identity tuple via SHA-256 of the compact JSON encoding
|
|
99
162
|
`[type, id, key]`, prefixed with `skill-` (first 56 hex digits).
|
|
100
163
|
- `normalizeTags(tags)` — trims, lowercases, deduplicates, and validates
|
|
101
|
-
tags against
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
164
|
+
tags against `CATALOG_TAG_PATTERN` and `MAX_TAG_LENGTH`. The tag
|
|
165
|
+
pattern matches Backstage's default catalog validator: lowercase letters,
|
|
166
|
+
digits, colons (`:`), plus signs (`+`), and hash characters (`#`), separated
|
|
167
|
+
by single hyphens. Dots, underscores, and consecutive hyphens are invalid.
|
|
168
|
+
Parity tests use Backstage's validator to detect drift. Returns valid tags
|
|
169
|
+
and diagnostics for dropped entries.
|
|
105
170
|
- `resolveVersion(declaredVersion, digest)` — strips at most one leading
|
|
106
171
|
`v`, validates as SemVer, and falls back to `0.0.0+<first 12 hex digits
|
|
107
172
|
of digest>` when no valid SemVer is present.
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
var crypto = require('crypto');
|
|
4
|
+
var tagConstants = require('./tag-constants.cjs.js');
|
|
4
5
|
var validation = require('./validation.cjs.js');
|
|
5
6
|
|
|
6
7
|
function computeCatalogName(tuple) {
|
|
@@ -8,8 +9,6 @@ function computeCatalogName(tuple) {
|
|
|
8
9
|
const hash = crypto.createHash("sha256").update(payload, "utf8").digest("hex");
|
|
9
10
|
return `skill-${hash.substring(0, 56)}`;
|
|
10
11
|
}
|
|
11
|
-
const CATALOG_TAG_RE = /^[a-z0-9:+#]+(-[a-z0-9:+#]+)*$/;
|
|
12
|
-
const MAX_TAG_LENGTH = 63;
|
|
13
12
|
function normalizeTags(tags) {
|
|
14
13
|
const seen = /* @__PURE__ */ new Set();
|
|
15
14
|
const result = [];
|
|
@@ -17,23 +16,23 @@ function normalizeTags(tags) {
|
|
|
17
16
|
for (const raw of tags) {
|
|
18
17
|
const trimmed = raw.trim().toLocaleLowerCase("en-US");
|
|
19
18
|
if (trimmed.length === 0) {
|
|
20
|
-
diagnostics.push(`tag '${raw}': empty after trim,
|
|
19
|
+
diagnostics.push(`tag '${raw}': empty after trim, omitted`);
|
|
21
20
|
continue;
|
|
22
21
|
}
|
|
23
22
|
if (seen.has(trimmed)) {
|
|
24
|
-
diagnostics.push(`tag '${raw}': duplicate '${trimmed}',
|
|
23
|
+
diagnostics.push(`tag '${raw}': duplicate '${trimmed}', omitted`);
|
|
25
24
|
continue;
|
|
26
25
|
}
|
|
27
|
-
if (trimmed.length > MAX_TAG_LENGTH) {
|
|
26
|
+
if (trimmed.length > tagConstants.MAX_TAG_LENGTH) {
|
|
28
27
|
diagnostics.push(
|
|
29
|
-
`tag '${raw}': length ${trimmed.length} exceeds maximum ${MAX_TAG_LENGTH},
|
|
28
|
+
`tag '${raw}': length ${trimmed.length} exceeds maximum ${tagConstants.MAX_TAG_LENGTH}, omitted`
|
|
30
29
|
);
|
|
31
30
|
seen.add(trimmed);
|
|
32
31
|
continue;
|
|
33
32
|
}
|
|
34
|
-
if (!
|
|
33
|
+
if (!tagConstants.CATALOG_TAG_PATTERN.test(trimmed)) {
|
|
35
34
|
diagnostics.push(
|
|
36
|
-
`tag '${raw}': '${trimmed}' does not match catalog tag pattern,
|
|
35
|
+
`tag '${raw}': '${trimmed}' does not match catalog tag pattern, omitted`
|
|
37
36
|
);
|
|
38
37
|
seen.add(trimmed);
|
|
39
38
|
continue;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"catalog-helpers.cjs.js","sources":["../src/catalog-helpers.ts"],"sourcesContent":["/*\n * Copyright Red Hat, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Pure identity, tag, version, and source-reference helpers for\n * catalog entity mapping (design D5).\n *\n * These helpers are consumed by the common catalog provider and have\n * no network, database, or Catalog side effects.\n *\n * @packageDocumentation\n */\n\nimport { createHash } from 'crypto';\n\nimport type { SkillSourceType } from './types';\nimport { isValidDigest } from './validation';\n\n// ─── Identity ────────────────────────────────────────────────────────\n\n/**\n * The identity tuple used for deterministic catalog entity naming.\n *\n * @public\n */\nexport interface IdentityTuple {\n /** Source type discriminator. */\n type: SkillSourceType;\n /** Configured source ID. */\n id: string;\n /** Stable record key within the source. */\n key: string;\n}\n\n/**\n * Computes the deterministic catalog entity name from an identity tuple.\n *\n * Per design D5, the name is `skill-` followed by the first 56 lowercase\n * hex digits of the SHA-256 hash of the compact JSON UTF-8 encoding of\n * `[source.type, source.id, record.key]`.\n *\n * Names, versions, tags, and content digests do not affect entity\n * identity. A source ID or native key change creates a different name.\n *\n * @remarks\n * The hashing algorithm (SHA-256 of compact JSON identity tuple, truncated\n * to 56 hex digits) is part of the stable public contract. Persisted\n * entities depend on this producing the same output for the same input.\n * Any change to the algorithm constitutes a breaking (major) change.\n *\n * @public\n */\nexport function computeCatalogName(tuple: IdentityTuple): string {\n const payload = JSON.stringify([tuple.type, tuple.id, tuple.key]);\n const hash = createHash('sha256').update(payload, 'utf8').digest('hex');\n return `skill-${hash.substring(0, 56)}`;\n}\n\n// ─── Tag normalization ───────────────────────────────────────────────\n\n/**\n * Backstage catalog tag validation pattern: one or more segments of\n * `[a-z0-9:+#]` separated by hyphens, maximum 63 characters.\n *\n * This pattern intentionally extends the standard Backstage catalog tag\n * pattern (which uses `[a-z0-9]` segments) to also allow colons, plus\n * signs, and hash characters per design D3/D5 requirements.\n */\nconst CATALOG_TAG_RE = /^[a-z0-9:+#]+(-[a-z0-9:+#]+)*$/;\n\n/** Maximum length for a Backstage catalog tag. */\nconst MAX_TAG_LENGTH = 63;\n\n/**\n * Result of tag normalization.\n *\n * @public\n */\nexport interface NormalizeTagsResult {\n /** Valid, deduplicated tags in lowercase. */\n tags: string[];\n /** Diagnostic messages for tags that were dropped. */\n diagnostics: string[];\n}\n\n/**\n * Normalizes tags for catalog use per design D3/D5.\n *\n * Trims whitespace, lowercases, deduplicates, and retains only values\n * accepted by an extended catalog tag pattern (per design D3/D5) that\n * allows colons, plus signs, and hash characters beyond the standard\n * Backstage tag validator. Invalid or overlength values are omitted\n * with a diagnostic — tags are never truncated and replacement\n * characters are never synthesized.\n *\n * @public\n */\nexport function normalizeTags(tags: string[]): NormalizeTagsResult {\n const seen = new Set<string>();\n const result: string[] = [];\n const diagnostics: string[] = [];\n\n for (const raw of tags) {\n const trimmed = raw.trim().toLocaleLowerCase('en-US');\n\n if (trimmed.length === 0) {\n diagnostics.push(`tag '${raw}': empty after trim, skipped`);\n continue;\n }\n\n if (seen.has(trimmed)) {\n diagnostics.push(`tag '${raw}': duplicate '${trimmed}', skipped`);\n continue;\n }\n\n if (trimmed.length > MAX_TAG_LENGTH) {\n diagnostics.push(\n `tag '${raw}': length ${trimmed.length} exceeds maximum ${MAX_TAG_LENGTH}, skipped`,\n );\n seen.add(trimmed);\n continue;\n }\n\n if (!CATALOG_TAG_RE.test(trimmed)) {\n diagnostics.push(\n `tag '${raw}': '${trimmed}' does not match catalog tag pattern, skipped`,\n );\n seen.add(trimmed);\n continue;\n }\n\n seen.add(trimmed);\n result.push(trimmed);\n }\n\n return { tags: result, diagnostics };\n}\n\n// ─── SemVer fallback ─────────────────────────────────────────────────\n\n/**\n * SemVer validation regex per https://semver.org/#is-there-a-suggested-regular-expression-to-check-a-semver-string\n *\n * Matches: MAJOR.MINOR.PATCH with optional pre-release and build metadata.\n */\nconst SEMVER_RE =\n /^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$/;\n\n/**\n * Resolves a catalog version from a declared version string and record\n * digest, per design D5.\n *\n * Removes at most one leading `v`, validates the result as SemVer, and\n * falls back to `0.0.0+<first 12 hex digits of record.digest>` when no\n * valid SemVer is present.\n *\n * @param declaredVersion - The raw version string from the record, or undefined.\n * @param digest - The record's content digest in `sha256:<64 hex>` format.\n * @returns A valid SemVer string.\n *\n * @public\n */\nexport function resolveVersion(\n declaredVersion: string | undefined,\n digest: string,\n): string {\n // Validate digest upfront, consistent with buildOciRef and buildNpxRef.\n if (!isValidDigest(digest)) {\n throw new Error(`invalid digest: ${digest}`);\n }\n\n if (declaredVersion !== undefined && declaredVersion !== '') {\n let candidate = declaredVersion;\n if (candidate.startsWith('v')) {\n candidate = candidate.substring(1);\n }\n if (SEMVER_RE.test(candidate)) {\n return candidate;\n }\n }\n\n // Fallback: 0.0.0+<first 12 hex digits of digest>\n // digest format is sha256:<64 hex>, so hex starts at index 7\n const hex = digest.substring(7, 19);\n return `0.0.0+${hex}`;\n}\n\n// ─── OCI references ──────────────────────────────────────────────────\n\n/**\n * A parsed OCI skill reference.\n *\n * @public\n */\nexport interface OciRef {\n /** Full OCI URI: `oci://<registry>/<repository>@sha256:<hex>`. */\n uri: string;\n /** Registry host (and optional port). */\n registry: string;\n /** Repository path (may contain slashes). */\n repository: string;\n /** Content digest in `sha256:<64 hex>` format. */\n digest: string;\n}\n\n/**\n * OCI URI pattern: `oci://<registry>/<repository>@sha256:<64 hex digits>`.\n */\nconst OCI_URI_RE = /^oci:\\/\\/([^/]+)\\/(.+)@(sha256:[0-9a-f]{64})$/;\n\n/**\n * Constructs a digest-addressed OCI reference URI.\n *\n * The returned URI has the form `oci://<registry>/<repository>@sha256:<hex>`.\n * The provided digest must be a valid SHA-256 digest string.\n *\n * @param registry - Registry host (e.g., `quay.io`).\n * @param repository - Repository path (e.g., `octo/hello-world-skill`).\n * @param digest - Content digest in `sha256:<64 hex>` format.\n * @returns The constructed OCI URI.\n * @throws If the digest is invalid.\n *\n * @public\n */\nexport function buildOciRef(\n registry: string,\n repository: string,\n digest: string,\n): string {\n if (!isValidDigest(digest)) {\n throw new Error(`invalid digest: ${digest}`);\n }\n if (!registry || !repository) {\n throw new Error(\n `registry and repository must be non-empty: registry=${JSON.stringify(\n registry,\n )}, repository=${JSON.stringify(repository)}`,\n );\n }\n return `oci://${registry}/${repository}@${digest}`;\n}\n\n/**\n * Parses a digest-addressed OCI reference URI.\n *\n * Rejects URIs that do not match the `oci://<registry>/<repository>@sha256:<hex>`\n * pattern. When a `recordDigest` is provided, validates that the URI digest\n * agrees with the record digest.\n *\n * @param uri - The OCI URI to parse.\n * @param recordDigest - Optional record digest to verify agreement.\n * @returns The parsed OCI reference.\n * @throws If the URI is malformed or the digest does not agree.\n *\n * @public\n */\nexport function parseOciRef(uri: string, recordDigest?: string): OciRef {\n const match = OCI_URI_RE.exec(uri);\n if (!match) {\n throw new Error(`malformed OCI URI: ${uri}`);\n }\n\n const [, registry, repository, digest] = match;\n\n if (recordDigest !== undefined && digest !== recordDigest) {\n throw new Error(\n `OCI URI digest '${digest}' does not match record digest '${recordDigest}'`,\n );\n }\n\n return { uri, registry, repository, digest };\n}\n\n// ─── npx references ─────────────────────────────────────────────────\n\n/**\n * A parsed npx skill reference.\n *\n * @public\n */\nexport interface NpxRef {\n /** Serialized reference: `<sourceUri>#<digest>`. */\n ref: string;\n /** The artifact source URL (HTTPS, no credentials or fragments). */\n sourceUri: string;\n /** Content digest in `sha256:<64 hex>` format. */\n digest: string;\n}\n\n/**\n * Query parameter names that indicate credentials or signed access tokens.\n * Individually rejected regardless of context.\n */\nconst SENSITIVE_QUERY_PARAMS: ReadonlySet<string> = new Set([\n 'sig',\n 'signature',\n 'token',\n 'access_token',\n 'api_key',\n 'apikey',\n 'secret',\n 'password',\n 'private_token',\n 'auth_token',\n 'bearer_token',\n 'x-amz-credential',\n 'x-amz-signature',\n 'x-amz-security-token',\n 'x-goog-signature',\n 'client_secret',\n 'refresh_token',\n]);\n\n/**\n * Azure SAS token query parameters. These short, common names (`se`, `sp`,\n * `sv`, etc.) can appear in non-credential contexts, so they are only\n * rejected when {@link AZURE_SAS_CO_OCCURRENCE_THRESHOLD} or more appear\n * together — a strong signal of a signed URL.\n */\nconst AZURE_SAS_PARAMS: ReadonlySet<string> = new Set([\n 'se', // expiry\n 'sp', // permissions\n 'spr', // protocol\n 'sv', // version\n 'ss', // services\n 'srt', // resource types\n 'st', // start time\n]);\n\n/** Minimum number of Azure SAS params that must co-occur to trigger rejection. */\nconst AZURE_SAS_CO_OCCURRENCE_THRESHOLD = 3;\n\n/**\n * Checks whether a URL contains credentials (userinfo), fragments,\n * query-string credentials, or signed access tokens.\n *\n * @param url - The URL to inspect.\n * @param context - Optional context prefix for error messages (e.g., `\"npx reference\"`).\n * @throws If the URL contains sensitive elements.\n */\nfunction rejectSensitiveUrl(url: URL, context?: string): void {\n const prefix = context ? `${context}: ` : '';\n\n if (url.username || url.password) {\n throw new Error(`${prefix}URL contains credentials`);\n }\n\n if (url.hash) {\n throw new Error(`${prefix}URL contains a fragment`);\n }\n\n let azureSasCount = 0;\n for (const [param] of url.searchParams) {\n const lower = param.toLocaleLowerCase('en-US');\n if (SENSITIVE_QUERY_PARAMS.has(lower)) {\n throw new Error(\n `${prefix}URL contains sensitive query parameter '${param}'`,\n );\n }\n if (AZURE_SAS_PARAMS.has(lower)) {\n azureSasCount++;\n }\n }\n\n if (azureSasCount >= AZURE_SAS_CO_OCCURRENCE_THRESHOLD) {\n throw new Error(\n `${prefix}URL contains Azure SAS token parameters (${azureSasCount} of ${AZURE_SAS_PARAMS.size} present)`,\n );\n }\n}\n\n/**\n * Constructs an npx skill reference from a verified artifact URL and digest.\n *\n * The reference has the form `<sourceUri>#<digest>` where `sourceUri` is an\n * absolute HTTPS URL without credentials or fragments, and `digest` is a\n * lowercase SHA-256 digest.\n *\n * Non-sensitive query parameters retain their original order.\n *\n * @param sourceUri - Absolute HTTPS artifact URL.\n * @param digest - Content digest in `sha256:<64 hex>` format.\n * @returns The serialized npx reference string.\n * @throws If the URL contains credentials, fragments, or sensitive params,\n * or if the digest is invalid, or the URL is not HTTPS.\n *\n * @public\n */\nexport function buildNpxRef(sourceUri: string, digest: string): string {\n if (!isValidDigest(digest)) {\n throw new Error(`invalid digest: ${digest}`);\n }\n\n let url: URL;\n try {\n url = new URL(sourceUri);\n } catch {\n throw new Error('invalid URL');\n }\n\n if (url.protocol !== 'https:') {\n throw new Error('npx reference: URL must use HTTPS');\n }\n\n rejectSensitiveUrl(url, 'npx reference');\n\n // Serialize using URL API to normalize, preserving query order\n return `${url.href}#${digest}`;\n}\n\n/**\n * Parses an npx skill reference.\n *\n * Splits at the single literal `#` separator. Validates that the URL is\n * HTTPS, contains no credentials or fragments, and that the digest is a\n * valid SHA-256 digest.\n *\n * @param ref - The serialized npx reference (`<sourceUri>#<digest>`).\n * @returns The parsed npx reference.\n * @throws If the reference is malformed or the URL is sensitive.\n *\n * @public\n */\nexport function parseNpxRef(ref: string): NpxRef {\n // Split at the last '#' to separate URL from digest\n const hashIndex = ref.lastIndexOf('#');\n if (hashIndex === -1) {\n throw new Error(\"malformed npx reference (no '#' separator)\");\n }\n\n const sourceUri = ref.substring(0, hashIndex);\n const digest = ref.substring(hashIndex + 1);\n\n if (!isValidDigest(digest)) {\n throw new Error('malformed npx reference: invalid digest');\n }\n\n let url: URL;\n try {\n url = new URL(sourceUri);\n } catch {\n throw new Error('malformed npx reference: invalid URL');\n }\n\n if (url.protocol !== 'https:') {\n throw new Error('npx reference: URL must use HTTPS');\n }\n\n rejectSensitiveUrl(url, 'npx reference');\n\n return { ref, sourceUri, digest };\n}\n"],"names":["createHash","isValidDigest"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"catalog-helpers.cjs.js","sources":["../src/catalog-helpers.ts"],"sourcesContent":["/*\n * Copyright Red Hat, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Pure identity, tag, version, and source-reference helpers for\n * catalog entity mapping (design D5).\n *\n * These helpers are consumed by the common catalog provider and have\n * no network, database, or Catalog side effects.\n *\n * @packageDocumentation\n */\n\nimport { createHash } from 'crypto';\n\nimport { CATALOG_TAG_PATTERN, MAX_TAG_LENGTH } from './tag-constants';\nimport type { SkillSourceType } from './types';\nimport { isValidDigest } from './validation';\n\n// ─── Identity ────────────────────────────────────────────────────────\n\n/**\n * The identity tuple used for deterministic catalog entity naming.\n *\n * @public\n */\nexport interface IdentityTuple {\n /** Source type discriminator. */\n type: SkillSourceType;\n /** Configured source ID. */\n id: string;\n /** Stable record key within the source. */\n key: string;\n}\n\n/**\n * Computes the deterministic catalog entity name from an identity tuple.\n *\n * Per design D5, the name is `skill-` followed by the first 56 lowercase\n * hex digits of the SHA-256 hash of the compact JSON UTF-8 encoding of\n * `[source.type, source.id, record.key]`.\n *\n * Names, versions, tags, and content digests do not affect entity\n * identity. A source ID or native key change creates a different name.\n *\n * @remarks\n * The hashing algorithm (SHA-256 of compact JSON identity tuple, truncated\n * to 56 hex digits) is part of the stable public contract. Persisted\n * entities depend on this producing the same output for the same input.\n * Any change to the algorithm constitutes a breaking (major) change.\n *\n * @public\n */\nexport function computeCatalogName(tuple: IdentityTuple): string {\n const payload = JSON.stringify([tuple.type, tuple.id, tuple.key]);\n const hash = createHash('sha256').update(payload, 'utf8').digest('hex');\n return `skill-${hash.substring(0, 56)}`;\n}\n\n// ─── Tag normalization ───────────────────────────────────────────────\n\n/**\n * Result of tag normalization.\n *\n * @public\n */\nexport interface NormalizeTagsResult {\n /** Valid, deduplicated tags in lowercase. */\n tags: string[];\n /**\n * Diagnostic messages for tags that were dropped.\n *\n * @remarks\n * Message text is informational and may change. Consumers must not\n * treat wording as a stable API or use it for programmatic decisions.\n */\n diagnostics: string[];\n}\n\n/**\n * Normalizes tags for catalog use per design D3/D5.\n *\n * Trims whitespace, lowercases, deduplicates, and retains only values\n * accepted by {@link CATALOG_TAG_PATTERN} (per design D3/D5). Invalid or\n * overlength values are omitted with a diagnostic — tags are never\n * truncated and replacement characters are never synthesized.\n *\n * @public\n */\nexport function normalizeTags(tags: string[]): NormalizeTagsResult {\n const seen = new Set<string>();\n const result: string[] = [];\n const diagnostics: string[] = [];\n\n for (const raw of tags) {\n const trimmed = raw.trim().toLocaleLowerCase('en-US');\n\n if (trimmed.length === 0) {\n diagnostics.push(`tag '${raw}': empty after trim, omitted`);\n continue;\n }\n\n if (seen.has(trimmed)) {\n diagnostics.push(`tag '${raw}': duplicate '${trimmed}', omitted`);\n continue;\n }\n\n if (trimmed.length > MAX_TAG_LENGTH) {\n diagnostics.push(\n `tag '${raw}': length ${trimmed.length} exceeds maximum ${MAX_TAG_LENGTH}, omitted`,\n );\n seen.add(trimmed);\n continue;\n }\n\n if (!CATALOG_TAG_PATTERN.test(trimmed)) {\n diagnostics.push(\n `tag '${raw}': '${trimmed}' does not match catalog tag pattern, omitted`,\n );\n seen.add(trimmed);\n continue;\n }\n\n seen.add(trimmed);\n result.push(trimmed);\n }\n\n return { tags: result, diagnostics };\n}\n\n// ─── SemVer fallback ─────────────────────────────────────────────────\n\n/**\n * SemVer validation regex per https://semver.org/#is-there-a-suggested-regular-expression-to-check-a-semver-string\n *\n * Matches: MAJOR.MINOR.PATCH with optional pre-release and build metadata.\n */\nconst SEMVER_RE =\n /^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$/;\n\n/**\n * Resolves a catalog version from a declared version string and record\n * digest, per design D5.\n *\n * Removes at most one leading `v`, validates the result as SemVer, and\n * falls back to `0.0.0+<first 12 hex digits of record.digest>` when no\n * valid SemVer is present.\n *\n * @param declaredVersion - The raw version string from the record, or undefined.\n * @param digest - The record's content digest in `sha256:<64 hex>` format.\n * @returns A valid SemVer string.\n *\n * @public\n */\nexport function resolveVersion(\n declaredVersion: string | undefined,\n digest: string,\n): string {\n // Validate digest upfront, consistent with buildOciRef and buildNpxRef.\n if (!isValidDigest(digest)) {\n throw new Error(`invalid digest: ${digest}`);\n }\n\n if (declaredVersion !== undefined && declaredVersion !== '') {\n let candidate = declaredVersion;\n if (candidate.startsWith('v')) {\n candidate = candidate.substring(1);\n }\n if (SEMVER_RE.test(candidate)) {\n return candidate;\n }\n }\n\n // Fallback: 0.0.0+<first 12 hex digits of digest>\n // digest format is sha256:<64 hex>, so hex starts at index 7\n const hex = digest.substring(7, 19);\n return `0.0.0+${hex}`;\n}\n\n// ─── OCI references ──────────────────────────────────────────────────\n\n/**\n * A parsed OCI skill reference.\n *\n * @public\n */\nexport interface OciRef {\n /** Full OCI URI: `oci://<registry>/<repository>@sha256:<hex>`. */\n uri: string;\n /** Registry host (and optional port). */\n registry: string;\n /** Repository path (may contain slashes). */\n repository: string;\n /** Content digest in `sha256:<64 hex>` format. */\n digest: string;\n}\n\n/**\n * OCI URI pattern: `oci://<registry>/<repository>@sha256:<64 hex digits>`.\n */\nconst OCI_URI_RE = /^oci:\\/\\/([^/]+)\\/(.+)@(sha256:[0-9a-f]{64})$/;\n\n/**\n * Constructs a digest-addressed OCI reference URI.\n *\n * The returned URI has the form `oci://<registry>/<repository>@sha256:<hex>`.\n * The provided digest must be a valid SHA-256 digest string.\n *\n * @param registry - Registry host (e.g., `quay.io`).\n * @param repository - Repository path (e.g., `octo/hello-world-skill`).\n * @param digest - Content digest in `sha256:<64 hex>` format.\n * @returns The constructed OCI URI.\n * @throws If the digest is invalid.\n *\n * @public\n */\nexport function buildOciRef(\n registry: string,\n repository: string,\n digest: string,\n): string {\n if (!isValidDigest(digest)) {\n throw new Error(`invalid digest: ${digest}`);\n }\n if (!registry || !repository) {\n throw new Error(\n `registry and repository must be non-empty: registry=${JSON.stringify(\n registry,\n )}, repository=${JSON.stringify(repository)}`,\n );\n }\n return `oci://${registry}/${repository}@${digest}`;\n}\n\n/**\n * Parses a digest-addressed OCI reference URI.\n *\n * Rejects URIs that do not match the `oci://<registry>/<repository>@sha256:<hex>`\n * pattern. When a `recordDigest` is provided, validates that the URI digest\n * agrees with the record digest.\n *\n * @param uri - The OCI URI to parse.\n * @param recordDigest - Optional record digest to verify agreement.\n * @returns The parsed OCI reference.\n * @throws If the URI is malformed or the digest does not agree.\n *\n * @public\n */\nexport function parseOciRef(uri: string, recordDigest?: string): OciRef {\n const match = OCI_URI_RE.exec(uri);\n if (!match) {\n throw new Error(`malformed OCI URI: ${uri}`);\n }\n\n const [, registry, repository, digest] = match;\n\n if (recordDigest !== undefined && digest !== recordDigest) {\n throw new Error(\n `OCI URI digest '${digest}' does not match record digest '${recordDigest}'`,\n );\n }\n\n return { uri, registry, repository, digest };\n}\n\n// ─── npx references ─────────────────────────────────────────────────\n\n/**\n * A parsed npx skill reference.\n *\n * @public\n */\nexport interface NpxRef {\n /** Serialized reference: `<sourceUri>#<digest>`. */\n ref: string;\n /** The artifact source URL (HTTPS, no credentials or fragments). */\n sourceUri: string;\n /** Content digest in `sha256:<64 hex>` format. */\n digest: string;\n}\n\n/**\n * Query parameter names that indicate credentials or signed access tokens.\n * Individually rejected regardless of context.\n */\nconst SENSITIVE_QUERY_PARAMS: ReadonlySet<string> = new Set([\n 'sig',\n 'signature',\n 'token',\n 'access_token',\n 'api_key',\n 'apikey',\n 'secret',\n 'password',\n 'private_token',\n 'auth_token',\n 'bearer_token',\n 'x-amz-credential',\n 'x-amz-signature',\n 'x-amz-security-token',\n 'x-goog-signature',\n 'client_secret',\n 'refresh_token',\n]);\n\n/**\n * Azure SAS token query parameters. These short, common names (`se`, `sp`,\n * `sv`, etc.) can appear in non-credential contexts, so they are only\n * rejected when {@link AZURE_SAS_CO_OCCURRENCE_THRESHOLD} or more appear\n * together — a strong signal of a signed URL.\n */\nconst AZURE_SAS_PARAMS: ReadonlySet<string> = new Set([\n 'se', // expiry\n 'sp', // permissions\n 'spr', // protocol\n 'sv', // version\n 'ss', // services\n 'srt', // resource types\n 'st', // start time\n]);\n\n/** Minimum number of Azure SAS params that must co-occur to trigger rejection. */\nconst AZURE_SAS_CO_OCCURRENCE_THRESHOLD = 3;\n\n/**\n * Checks whether a URL contains credentials (userinfo), fragments,\n * query-string credentials, or signed access tokens.\n *\n * @param url - The URL to inspect.\n * @param context - Optional context prefix for error messages (e.g., `\"npx reference\"`).\n * @throws If the URL contains sensitive elements.\n */\nfunction rejectSensitiveUrl(url: URL, context?: string): void {\n const prefix = context ? `${context}: ` : '';\n\n if (url.username || url.password) {\n throw new Error(`${prefix}URL contains credentials`);\n }\n\n if (url.hash) {\n throw new Error(`${prefix}URL contains a fragment`);\n }\n\n let azureSasCount = 0;\n for (const [param] of url.searchParams) {\n const lower = param.toLocaleLowerCase('en-US');\n if (SENSITIVE_QUERY_PARAMS.has(lower)) {\n throw new Error(\n `${prefix}URL contains sensitive query parameter '${param}'`,\n );\n }\n if (AZURE_SAS_PARAMS.has(lower)) {\n azureSasCount++;\n }\n }\n\n if (azureSasCount >= AZURE_SAS_CO_OCCURRENCE_THRESHOLD) {\n throw new Error(\n `${prefix}URL contains Azure SAS token parameters (${azureSasCount} of ${AZURE_SAS_PARAMS.size} present)`,\n );\n }\n}\n\n/**\n * Constructs an npx skill reference from a verified artifact URL and digest.\n *\n * The reference has the form `<sourceUri>#<digest>` where `sourceUri` is an\n * absolute HTTPS URL without credentials or fragments, and `digest` is a\n * lowercase SHA-256 digest.\n *\n * Non-sensitive query parameters retain their original order.\n *\n * @param sourceUri - Absolute HTTPS artifact URL.\n * @param digest - Content digest in `sha256:<64 hex>` format.\n * @returns The serialized npx reference string.\n * @throws If the URL contains credentials, fragments, or sensitive params,\n * or if the digest is invalid, or the URL is not HTTPS.\n *\n * @public\n */\nexport function buildNpxRef(sourceUri: string, digest: string): string {\n if (!isValidDigest(digest)) {\n throw new Error(`invalid digest: ${digest}`);\n }\n\n let url: URL;\n try {\n url = new URL(sourceUri);\n } catch {\n throw new Error('invalid URL');\n }\n\n if (url.protocol !== 'https:') {\n throw new Error('npx reference: URL must use HTTPS');\n }\n\n rejectSensitiveUrl(url, 'npx reference');\n\n // Serialize using URL API to normalize, preserving query order\n return `${url.href}#${digest}`;\n}\n\n/**\n * Parses an npx skill reference.\n *\n * Splits at the single literal `#` separator. Validates that the URL is\n * HTTPS, contains no credentials or fragments, and that the digest is a\n * valid SHA-256 digest.\n *\n * @param ref - The serialized npx reference (`<sourceUri>#<digest>`).\n * @returns The parsed npx reference.\n * @throws If the reference is malformed or the URL is sensitive.\n *\n * @public\n */\nexport function parseNpxRef(ref: string): NpxRef {\n // Split at the last '#' to separate URL from digest\n const hashIndex = ref.lastIndexOf('#');\n if (hashIndex === -1) {\n throw new Error(\"malformed npx reference (no '#' separator)\");\n }\n\n const sourceUri = ref.substring(0, hashIndex);\n const digest = ref.substring(hashIndex + 1);\n\n if (!isValidDigest(digest)) {\n throw new Error('malformed npx reference: invalid digest');\n }\n\n let url: URL;\n try {\n url = new URL(sourceUri);\n } catch {\n throw new Error('malformed npx reference: invalid URL');\n }\n\n if (url.protocol !== 'https:') {\n throw new Error('npx reference: URL must use HTTPS');\n }\n\n rejectSensitiveUrl(url, 'npx reference');\n\n return { ref, sourceUri, digest };\n}\n"],"names":["createHash","MAX_TAG_LENGTH","CATALOG_TAG_PATTERN","isValidDigest"],"mappings":"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"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createHash } from 'crypto';
|
|
2
|
+
import { MAX_TAG_LENGTH, CATALOG_TAG_PATTERN } from './tag-constants.esm.js';
|
|
2
3
|
import { isValidDigest } from './validation.esm.js';
|
|
3
4
|
|
|
4
5
|
function computeCatalogName(tuple) {
|
|
@@ -6,8 +7,6 @@ function computeCatalogName(tuple) {
|
|
|
6
7
|
const hash = createHash("sha256").update(payload, "utf8").digest("hex");
|
|
7
8
|
return `skill-${hash.substring(0, 56)}`;
|
|
8
9
|
}
|
|
9
|
-
const CATALOG_TAG_RE = /^[a-z0-9:+#]+(-[a-z0-9:+#]+)*$/;
|
|
10
|
-
const MAX_TAG_LENGTH = 63;
|
|
11
10
|
function normalizeTags(tags) {
|
|
12
11
|
const seen = /* @__PURE__ */ new Set();
|
|
13
12
|
const result = [];
|
|
@@ -15,23 +14,23 @@ function normalizeTags(tags) {
|
|
|
15
14
|
for (const raw of tags) {
|
|
16
15
|
const trimmed = raw.trim().toLocaleLowerCase("en-US");
|
|
17
16
|
if (trimmed.length === 0) {
|
|
18
|
-
diagnostics.push(`tag '${raw}': empty after trim,
|
|
17
|
+
diagnostics.push(`tag '${raw}': empty after trim, omitted`);
|
|
19
18
|
continue;
|
|
20
19
|
}
|
|
21
20
|
if (seen.has(trimmed)) {
|
|
22
|
-
diagnostics.push(`tag '${raw}': duplicate '${trimmed}',
|
|
21
|
+
diagnostics.push(`tag '${raw}': duplicate '${trimmed}', omitted`);
|
|
23
22
|
continue;
|
|
24
23
|
}
|
|
25
24
|
if (trimmed.length > MAX_TAG_LENGTH) {
|
|
26
25
|
diagnostics.push(
|
|
27
|
-
`tag '${raw}': length ${trimmed.length} exceeds maximum ${MAX_TAG_LENGTH},
|
|
26
|
+
`tag '${raw}': length ${trimmed.length} exceeds maximum ${MAX_TAG_LENGTH}, omitted`
|
|
28
27
|
);
|
|
29
28
|
seen.add(trimmed);
|
|
30
29
|
continue;
|
|
31
30
|
}
|
|
32
|
-
if (!
|
|
31
|
+
if (!CATALOG_TAG_PATTERN.test(trimmed)) {
|
|
33
32
|
diagnostics.push(
|
|
34
|
-
`tag '${raw}': '${trimmed}' does not match catalog tag pattern,
|
|
33
|
+
`tag '${raw}': '${trimmed}' does not match catalog tag pattern, omitted`
|
|
35
34
|
);
|
|
36
35
|
seen.add(trimmed);
|
|
37
36
|
continue;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"catalog-helpers.esm.js","sources":["../src/catalog-helpers.ts"],"sourcesContent":["/*\n * Copyright Red Hat, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Pure identity, tag, version, and source-reference helpers for\n * catalog entity mapping (design D5).\n *\n * These helpers are consumed by the common catalog provider and have\n * no network, database, or Catalog side effects.\n *\n * @packageDocumentation\n */\n\nimport { createHash } from 'crypto';\n\nimport type { SkillSourceType } from './types';\nimport { isValidDigest } from './validation';\n\n// ─── Identity ────────────────────────────────────────────────────────\n\n/**\n * The identity tuple used for deterministic catalog entity naming.\n *\n * @public\n */\nexport interface IdentityTuple {\n /** Source type discriminator. */\n type: SkillSourceType;\n /** Configured source ID. */\n id: string;\n /** Stable record key within the source. */\n key: string;\n}\n\n/**\n * Computes the deterministic catalog entity name from an identity tuple.\n *\n * Per design D5, the name is `skill-` followed by the first 56 lowercase\n * hex digits of the SHA-256 hash of the compact JSON UTF-8 encoding of\n * `[source.type, source.id, record.key]`.\n *\n * Names, versions, tags, and content digests do not affect entity\n * identity. A source ID or native key change creates a different name.\n *\n * @remarks\n * The hashing algorithm (SHA-256 of compact JSON identity tuple, truncated\n * to 56 hex digits) is part of the stable public contract. Persisted\n * entities depend on this producing the same output for the same input.\n * Any change to the algorithm constitutes a breaking (major) change.\n *\n * @public\n */\nexport function computeCatalogName(tuple: IdentityTuple): string {\n const payload = JSON.stringify([tuple.type, tuple.id, tuple.key]);\n const hash = createHash('sha256').update(payload, 'utf8').digest('hex');\n return `skill-${hash.substring(0, 56)}`;\n}\n\n// ─── Tag normalization ───────────────────────────────────────────────\n\n/**\n * Backstage catalog tag validation pattern: one or more segments of\n * `[a-z0-9:+#]` separated by hyphens, maximum 63 characters.\n *\n * This pattern intentionally extends the standard Backstage catalog tag\n * pattern (which uses `[a-z0-9]` segments) to also allow colons, plus\n * signs, and hash characters per design D3/D5 requirements.\n */\nconst CATALOG_TAG_RE = /^[a-z0-9:+#]+(-[a-z0-9:+#]+)*$/;\n\n/** Maximum length for a Backstage catalog tag. */\nconst MAX_TAG_LENGTH = 63;\n\n/**\n * Result of tag normalization.\n *\n * @public\n */\nexport interface NormalizeTagsResult {\n /** Valid, deduplicated tags in lowercase. */\n tags: string[];\n /** Diagnostic messages for tags that were dropped. */\n diagnostics: string[];\n}\n\n/**\n * Normalizes tags for catalog use per design D3/D5.\n *\n * Trims whitespace, lowercases, deduplicates, and retains only values\n * accepted by an extended catalog tag pattern (per design D3/D5) that\n * allows colons, plus signs, and hash characters beyond the standard\n * Backstage tag validator. Invalid or overlength values are omitted\n * with a diagnostic — tags are never truncated and replacement\n * characters are never synthesized.\n *\n * @public\n */\nexport function normalizeTags(tags: string[]): NormalizeTagsResult {\n const seen = new Set<string>();\n const result: string[] = [];\n const diagnostics: string[] = [];\n\n for (const raw of tags) {\n const trimmed = raw.trim().toLocaleLowerCase('en-US');\n\n if (trimmed.length === 0) {\n diagnostics.push(`tag '${raw}': empty after trim, skipped`);\n continue;\n }\n\n if (seen.has(trimmed)) {\n diagnostics.push(`tag '${raw}': duplicate '${trimmed}', skipped`);\n continue;\n }\n\n if (trimmed.length > MAX_TAG_LENGTH) {\n diagnostics.push(\n `tag '${raw}': length ${trimmed.length} exceeds maximum ${MAX_TAG_LENGTH}, skipped`,\n );\n seen.add(trimmed);\n continue;\n }\n\n if (!CATALOG_TAG_RE.test(trimmed)) {\n diagnostics.push(\n `tag '${raw}': '${trimmed}' does not match catalog tag pattern, skipped`,\n );\n seen.add(trimmed);\n continue;\n }\n\n seen.add(trimmed);\n result.push(trimmed);\n }\n\n return { tags: result, diagnostics };\n}\n\n// ─── SemVer fallback ─────────────────────────────────────────────────\n\n/**\n * SemVer validation regex per https://semver.org/#is-there-a-suggested-regular-expression-to-check-a-semver-string\n *\n * Matches: MAJOR.MINOR.PATCH with optional pre-release and build metadata.\n */\nconst SEMVER_RE =\n /^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$/;\n\n/**\n * Resolves a catalog version from a declared version string and record\n * digest, per design D5.\n *\n * Removes at most one leading `v`, validates the result as SemVer, and\n * falls back to `0.0.0+<first 12 hex digits of record.digest>` when no\n * valid SemVer is present.\n *\n * @param declaredVersion - The raw version string from the record, or undefined.\n * @param digest - The record's content digest in `sha256:<64 hex>` format.\n * @returns A valid SemVer string.\n *\n * @public\n */\nexport function resolveVersion(\n declaredVersion: string | undefined,\n digest: string,\n): string {\n // Validate digest upfront, consistent with buildOciRef and buildNpxRef.\n if (!isValidDigest(digest)) {\n throw new Error(`invalid digest: ${digest}`);\n }\n\n if (declaredVersion !== undefined && declaredVersion !== '') {\n let candidate = declaredVersion;\n if (candidate.startsWith('v')) {\n candidate = candidate.substring(1);\n }\n if (SEMVER_RE.test(candidate)) {\n return candidate;\n }\n }\n\n // Fallback: 0.0.0+<first 12 hex digits of digest>\n // digest format is sha256:<64 hex>, so hex starts at index 7\n const hex = digest.substring(7, 19);\n return `0.0.0+${hex}`;\n}\n\n// ─── OCI references ──────────────────────────────────────────────────\n\n/**\n * A parsed OCI skill reference.\n *\n * @public\n */\nexport interface OciRef {\n /** Full OCI URI: `oci://<registry>/<repository>@sha256:<hex>`. */\n uri: string;\n /** Registry host (and optional port). */\n registry: string;\n /** Repository path (may contain slashes). */\n repository: string;\n /** Content digest in `sha256:<64 hex>` format. */\n digest: string;\n}\n\n/**\n * OCI URI pattern: `oci://<registry>/<repository>@sha256:<64 hex digits>`.\n */\nconst OCI_URI_RE = /^oci:\\/\\/([^/]+)\\/(.+)@(sha256:[0-9a-f]{64})$/;\n\n/**\n * Constructs a digest-addressed OCI reference URI.\n *\n * The returned URI has the form `oci://<registry>/<repository>@sha256:<hex>`.\n * The provided digest must be a valid SHA-256 digest string.\n *\n * @param registry - Registry host (e.g., `quay.io`).\n * @param repository - Repository path (e.g., `octo/hello-world-skill`).\n * @param digest - Content digest in `sha256:<64 hex>` format.\n * @returns The constructed OCI URI.\n * @throws If the digest is invalid.\n *\n * @public\n */\nexport function buildOciRef(\n registry: string,\n repository: string,\n digest: string,\n): string {\n if (!isValidDigest(digest)) {\n throw new Error(`invalid digest: ${digest}`);\n }\n if (!registry || !repository) {\n throw new Error(\n `registry and repository must be non-empty: registry=${JSON.stringify(\n registry,\n )}, repository=${JSON.stringify(repository)}`,\n );\n }\n return `oci://${registry}/${repository}@${digest}`;\n}\n\n/**\n * Parses a digest-addressed OCI reference URI.\n *\n * Rejects URIs that do not match the `oci://<registry>/<repository>@sha256:<hex>`\n * pattern. When a `recordDigest` is provided, validates that the URI digest\n * agrees with the record digest.\n *\n * @param uri - The OCI URI to parse.\n * @param recordDigest - Optional record digest to verify agreement.\n * @returns The parsed OCI reference.\n * @throws If the URI is malformed or the digest does not agree.\n *\n * @public\n */\nexport function parseOciRef(uri: string, recordDigest?: string): OciRef {\n const match = OCI_URI_RE.exec(uri);\n if (!match) {\n throw new Error(`malformed OCI URI: ${uri}`);\n }\n\n const [, registry, repository, digest] = match;\n\n if (recordDigest !== undefined && digest !== recordDigest) {\n throw new Error(\n `OCI URI digest '${digest}' does not match record digest '${recordDigest}'`,\n );\n }\n\n return { uri, registry, repository, digest };\n}\n\n// ─── npx references ─────────────────────────────────────────────────\n\n/**\n * A parsed npx skill reference.\n *\n * @public\n */\nexport interface NpxRef {\n /** Serialized reference: `<sourceUri>#<digest>`. */\n ref: string;\n /** The artifact source URL (HTTPS, no credentials or fragments). */\n sourceUri: string;\n /** Content digest in `sha256:<64 hex>` format. */\n digest: string;\n}\n\n/**\n * Query parameter names that indicate credentials or signed access tokens.\n * Individually rejected regardless of context.\n */\nconst SENSITIVE_QUERY_PARAMS: ReadonlySet<string> = new Set([\n 'sig',\n 'signature',\n 'token',\n 'access_token',\n 'api_key',\n 'apikey',\n 'secret',\n 'password',\n 'private_token',\n 'auth_token',\n 'bearer_token',\n 'x-amz-credential',\n 'x-amz-signature',\n 'x-amz-security-token',\n 'x-goog-signature',\n 'client_secret',\n 'refresh_token',\n]);\n\n/**\n * Azure SAS token query parameters. These short, common names (`se`, `sp`,\n * `sv`, etc.) can appear in non-credential contexts, so they are only\n * rejected when {@link AZURE_SAS_CO_OCCURRENCE_THRESHOLD} or more appear\n * together — a strong signal of a signed URL.\n */\nconst AZURE_SAS_PARAMS: ReadonlySet<string> = new Set([\n 'se', // expiry\n 'sp', // permissions\n 'spr', // protocol\n 'sv', // version\n 'ss', // services\n 'srt', // resource types\n 'st', // start time\n]);\n\n/** Minimum number of Azure SAS params that must co-occur to trigger rejection. */\nconst AZURE_SAS_CO_OCCURRENCE_THRESHOLD = 3;\n\n/**\n * Checks whether a URL contains credentials (userinfo), fragments,\n * query-string credentials, or signed access tokens.\n *\n * @param url - The URL to inspect.\n * @param context - Optional context prefix for error messages (e.g., `\"npx reference\"`).\n * @throws If the URL contains sensitive elements.\n */\nfunction rejectSensitiveUrl(url: URL, context?: string): void {\n const prefix = context ? `${context}: ` : '';\n\n if (url.username || url.password) {\n throw new Error(`${prefix}URL contains credentials`);\n }\n\n if (url.hash) {\n throw new Error(`${prefix}URL contains a fragment`);\n }\n\n let azureSasCount = 0;\n for (const [param] of url.searchParams) {\n const lower = param.toLocaleLowerCase('en-US');\n if (SENSITIVE_QUERY_PARAMS.has(lower)) {\n throw new Error(\n `${prefix}URL contains sensitive query parameter '${param}'`,\n );\n }\n if (AZURE_SAS_PARAMS.has(lower)) {\n azureSasCount++;\n }\n }\n\n if (azureSasCount >= AZURE_SAS_CO_OCCURRENCE_THRESHOLD) {\n throw new Error(\n `${prefix}URL contains Azure SAS token parameters (${azureSasCount} of ${AZURE_SAS_PARAMS.size} present)`,\n );\n }\n}\n\n/**\n * Constructs an npx skill reference from a verified artifact URL and digest.\n *\n * The reference has the form `<sourceUri>#<digest>` where `sourceUri` is an\n * absolute HTTPS URL without credentials or fragments, and `digest` is a\n * lowercase SHA-256 digest.\n *\n * Non-sensitive query parameters retain their original order.\n *\n * @param sourceUri - Absolute HTTPS artifact URL.\n * @param digest - Content digest in `sha256:<64 hex>` format.\n * @returns The serialized npx reference string.\n * @throws If the URL contains credentials, fragments, or sensitive params,\n * or if the digest is invalid, or the URL is not HTTPS.\n *\n * @public\n */\nexport function buildNpxRef(sourceUri: string, digest: string): string {\n if (!isValidDigest(digest)) {\n throw new Error(`invalid digest: ${digest}`);\n }\n\n let url: URL;\n try {\n url = new URL(sourceUri);\n } catch {\n throw new Error('invalid URL');\n }\n\n if (url.protocol !== 'https:') {\n throw new Error('npx reference: URL must use HTTPS');\n }\n\n rejectSensitiveUrl(url, 'npx reference');\n\n // Serialize using URL API to normalize, preserving query order\n return `${url.href}#${digest}`;\n}\n\n/**\n * Parses an npx skill reference.\n *\n * Splits at the single literal `#` separator. Validates that the URL is\n * HTTPS, contains no credentials or fragments, and that the digest is a\n * valid SHA-256 digest.\n *\n * @param ref - The serialized npx reference (`<sourceUri>#<digest>`).\n * @returns The parsed npx reference.\n * @throws If the reference is malformed or the URL is sensitive.\n *\n * @public\n */\nexport function parseNpxRef(ref: string): NpxRef {\n // Split at the last '#' to separate URL from digest\n const hashIndex = ref.lastIndexOf('#');\n if (hashIndex === -1) {\n throw new Error(\"malformed npx reference (no '#' separator)\");\n }\n\n const sourceUri = ref.substring(0, hashIndex);\n const digest = ref.substring(hashIndex + 1);\n\n if (!isValidDigest(digest)) {\n throw new Error('malformed npx reference: invalid digest');\n }\n\n let url: URL;\n try {\n url = new URL(sourceUri);\n } catch {\n throw new Error('malformed npx reference: invalid URL');\n }\n\n if (url.protocol !== 'https:') {\n throw new Error('npx reference: URL must use HTTPS');\n }\n\n rejectSensitiveUrl(url, 'npx reference');\n\n return { ref, sourceUri, digest };\n}\n"],"names":[],"mappings":";;;AAiEO,SAAS,mBAAmB,KAAA,EAA8B;AAC/D,EAAA,MAAM,OAAA,GAAU,IAAA,CAAK,SAAA,CAAU,CAAC,KAAA,CAAM,MAAM,KAAA,CAAM,EAAA,EAAI,KAAA,CAAM,GAAG,CAAC,CAAA;AAChE,EAAA,MAAM,IAAA,GAAO,WAAW,QAAQ,CAAA,CAAE,OAAO,OAAA,EAAS,MAAM,CAAA,CAAE,MAAA,CAAO,KAAK,CAAA;AACtE,EAAA,OAAO,CAAA,MAAA,EAAS,IAAA,CAAK,SAAA,CAAU,CAAA,EAAG,EAAE,CAAC,CAAA,CAAA;AACvC;AAYA,MAAM,cAAA,GAAiB,gCAAA;AAGvB,MAAM,cAAA,GAAiB,EAAA;AA0BhB,SAAS,cAAc,IAAA,EAAqC;AACjE,EAAA,MAAM,IAAA,uBAAW,GAAA,EAAY;AAC7B,EAAA,MAAM,SAAmB,EAAC;AAC1B,EAAA,MAAM,cAAwB,EAAC;AAE/B,EAAA,KAAA,MAAW,OAAO,IAAA,EAAM;AACtB,IAAA,MAAM,OAAA,GAAU,GAAA,CAAI,IAAA,EAAK,CAAE,kBAAkB,OAAO,CAAA;AAEpD,IAAA,IAAI,OAAA,CAAQ,WAAW,CAAA,EAAG;AACxB,MAAA,WAAA,CAAY,IAAA,CAAK,CAAA,KAAA,EAAQ,GAAG,CAAA,4BAAA,CAA8B,CAAA;AAC1D,MAAA;AAAA,IACF;AAEA,IAAA,IAAI,IAAA,CAAK,GAAA,CAAI,OAAO,CAAA,EAAG;AACrB,MAAA,WAAA,CAAY,IAAA,CAAK,CAAA,KAAA,EAAQ,GAAG,CAAA,cAAA,EAAiB,OAAO,CAAA,UAAA,CAAY,CAAA;AAChE,MAAA;AAAA,IACF;AAEA,IAAA,IAAI,OAAA,CAAQ,SAAS,cAAA,EAAgB;AACnC,MAAA,WAAA,CAAY,IAAA;AAAA,QACV,QAAQ,GAAG,CAAA,UAAA,EAAa,OAAA,CAAQ,MAAM,oBAAoB,cAAc,CAAA,SAAA;AAAA,OAC1E;AACA,MAAA,IAAA,CAAK,IAAI,OAAO,CAAA;AAChB,MAAA;AAAA,IACF;AAEA,IAAA,IAAI,CAAC,cAAA,CAAe,IAAA,CAAK,OAAO,CAAA,EAAG;AACjC,MAAA,WAAA,CAAY,IAAA;AAAA,QACV,CAAA,KAAA,EAAQ,GAAG,CAAA,IAAA,EAAO,OAAO,CAAA,6CAAA;AAAA,OAC3B;AACA,MAAA,IAAA,CAAK,IAAI,OAAO,CAAA;AAChB,MAAA;AAAA,IACF;AAEA,IAAA,IAAA,CAAK,IAAI,OAAO,CAAA;AAChB,IAAA,MAAA,CAAO,KAAK,OAAO,CAAA;AAAA,EACrB;AAEA,EAAA,OAAO,EAAE,IAAA,EAAM,MAAA,EAAQ,WAAA,EAAY;AACrC;AASA,MAAM,SAAA,GACJ,qLAAA;AAgBK,SAAS,cAAA,CACd,iBACA,MAAA,EACQ;AAER,EAAA,IAAI,CAAC,aAAA,CAAc,MAAM,CAAA,EAAG;AAC1B,IAAA,MAAM,IAAI,KAAA,CAAM,CAAA,gBAAA,EAAmB,MAAM,CAAA,CAAE,CAAA;AAAA,EAC7C;AAEA,EAAA,IAAI,eAAA,KAAoB,MAAA,IAAa,eAAA,KAAoB,EAAA,EAAI;AAC3D,IAAA,IAAI,SAAA,GAAY,eAAA;AAChB,IAAA,IAAI,SAAA,CAAU,UAAA,CAAW,GAAG,CAAA,EAAG;AAC7B,MAAA,SAAA,GAAY,SAAA,CAAU,UAAU,CAAC,CAAA;AAAA,IACnC;AACA,IAAA,IAAI,SAAA,CAAU,IAAA,CAAK,SAAS,CAAA,EAAG;AAC7B,MAAA,OAAO,SAAA;AAAA,IACT;AAAA,EACF;AAIA,EAAA,MAAM,GAAA,GAAM,MAAA,CAAO,SAAA,CAAU,CAAA,EAAG,EAAE,CAAA;AAClC,EAAA,OAAO,SAAS,GAAG,CAAA,CAAA;AACrB;AAuBA,MAAM,UAAA,GAAa,+CAAA;AAgBZ,SAAS,WAAA,CACd,QAAA,EACA,UAAA,EACA,MAAA,EACQ;AACR,EAAA,IAAI,CAAC,aAAA,CAAc,MAAM,CAAA,EAAG;AAC1B,IAAA,MAAM,IAAI,KAAA,CAAM,CAAA,gBAAA,EAAmB,MAAM,CAAA,CAAE,CAAA;AAAA,EAC7C;AACA,EAAA,IAAI,CAAC,QAAA,IAAY,CAAC,UAAA,EAAY;AAC5B,IAAA,MAAM,IAAI,KAAA;AAAA,MACR,uDAAuD,IAAA,CAAK,SAAA;AAAA,QAC1D;AAAA,OACD,CAAA,aAAA,EAAgB,IAAA,CAAK,SAAA,CAAU,UAAU,CAAC,CAAA;AAAA,KAC7C;AAAA,EACF;AACA,EAAA,OAAO,CAAA,MAAA,EAAS,QAAQ,CAAA,CAAA,EAAI,UAAU,IAAI,MAAM,CAAA,CAAA;AAClD;AAgBO,SAAS,WAAA,CAAY,KAAa,YAAA,EAA+B;AACtE,EAAA,MAAM,KAAA,GAAQ,UAAA,CAAW,IAAA,CAAK,GAAG,CAAA;AACjC,EAAA,IAAI,CAAC,KAAA,EAAO;AACV,IAAA,MAAM,IAAI,KAAA,CAAM,CAAA,mBAAA,EAAsB,GAAG,CAAA,CAAE,CAAA;AAAA,EAC7C;AAEA,EAAA,MAAM,GAAG,QAAA,EAAU,UAAA,EAAY,MAAM,CAAA,GAAI,KAAA;AAEzC,EAAA,IAAI,YAAA,KAAiB,MAAA,IAAa,MAAA,KAAW,YAAA,EAAc;AACzD,IAAA,MAAM,IAAI,KAAA;AAAA,MACR,CAAA,gBAAA,EAAmB,MAAM,CAAA,gCAAA,EAAmC,YAAY,CAAA,CAAA;AAAA,KAC1E;AAAA,EACF;AAEA,EAAA,OAAO,EAAE,GAAA,EAAK,QAAA,EAAU,UAAA,EAAY,MAAA,EAAO;AAC7C;AAsBA,MAAM,sBAAA,uBAAkD,GAAA,CAAI;AAAA,EAC1D,KAAA;AAAA,EACA,WAAA;AAAA,EACA,OAAA;AAAA,EACA,cAAA;AAAA,EACA,SAAA;AAAA,EACA,QAAA;AAAA,EACA,QAAA;AAAA,EACA,UAAA;AAAA,EACA,eAAA;AAAA,EACA,YAAA;AAAA,EACA,cAAA;AAAA,EACA,kBAAA;AAAA,EACA,iBAAA;AAAA,EACA,sBAAA;AAAA,EACA,kBAAA;AAAA,EACA,eAAA;AAAA,EACA;AACF,CAAC,CAAA;AAQD,MAAM,gBAAA,uBAA4C,GAAA,CAAI;AAAA,EACpD,IAAA;AAAA;AAAA,EACA,IAAA;AAAA;AAAA,EACA,KAAA;AAAA;AAAA,EACA,IAAA;AAAA;AAAA,EACA,IAAA;AAAA;AAAA,EACA,KAAA;AAAA;AAAA,EACA;AAAA;AACF,CAAC,CAAA;AAGD,MAAM,iCAAA,GAAoC,CAAA;AAU1C,SAAS,kBAAA,CAAmB,KAAU,OAAA,EAAwB;AAC5D,EAAA,MAAM,MAAA,GAAmB,CAAA,EAAG,OAAO,CAAA,EAAA,CAAA,CAAO;AAE1C,EAAA,IAAI,GAAA,CAAI,QAAA,IAAY,GAAA,CAAI,QAAA,EAAU;AAChC,IAAA,MAAM,IAAI,KAAA,CAAM,CAAA,EAAG,MAAM,CAAA,wBAAA,CAA0B,CAAA;AAAA,EACrD;AAEA,EAAA,IAAI,IAAI,IAAA,EAAM;AACZ,IAAA,MAAM,IAAI,KAAA,CAAM,CAAA,EAAG,MAAM,CAAA,uBAAA,CAAyB,CAAA;AAAA,EACpD;AAEA,EAAA,IAAI,aAAA,GAAgB,CAAA;AACpB,EAAA,KAAA,MAAW,CAAC,KAAK,CAAA,IAAK,GAAA,CAAI,YAAA,EAAc;AACtC,IAAA,MAAM,KAAA,GAAQ,KAAA,CAAM,iBAAA,CAAkB,OAAO,CAAA;AAC7C,IAAA,IAAI,sBAAA,CAAuB,GAAA,CAAI,KAAK,CAAA,EAAG;AACrC,MAAA,MAAM,IAAI,KAAA;AAAA,QACR,CAAA,EAAG,MAAM,CAAA,wCAAA,EAA2C,KAAK,CAAA,CAAA;AAAA,OAC3D;AAAA,IACF;AACA,IAAA,IAAI,gBAAA,CAAiB,GAAA,CAAI,KAAK,CAAA,EAAG;AAC/B,MAAA,aAAA,EAAA;AAAA,IACF;AAAA,EACF;AAEA,EAAA,IAAI,iBAAiB,iCAAA,EAAmC;AACtD,IAAA,MAAM,IAAI,KAAA;AAAA,MACR,GAAG,MAAM,CAAA,yCAAA,EAA4C,aAAa,CAAA,IAAA,EAAO,iBAAiB,IAAI,CAAA,SAAA;AAAA,KAChG;AAAA,EACF;AACF;AAmBO,SAAS,WAAA,CAAY,WAAmB,MAAA,EAAwB;AACrE,EAAA,IAAI,CAAC,aAAA,CAAc,MAAM,CAAA,EAAG;AAC1B,IAAA,MAAM,IAAI,KAAA,CAAM,CAAA,gBAAA,EAAmB,MAAM,CAAA,CAAE,CAAA;AAAA,EAC7C;AAEA,EAAA,IAAI,GAAA;AACJ,EAAA,IAAI;AACF,IAAA,GAAA,GAAM,IAAI,IAAI,SAAS,CAAA;AAAA,EACzB,CAAA,CAAA,MAAQ;AACN,IAAA,MAAM,IAAI,MAAM,aAAa,CAAA;AAAA,EAC/B;AAEA,EAAA,IAAI,GAAA,CAAI,aAAa,QAAA,EAAU;AAC7B,IAAA,MAAM,IAAI,MAAM,mCAAmC,CAAA;AAAA,EACrD;AAEA,EAAA,kBAAA,CAAmB,KAAK,eAAe,CAAA;AAGvC,EAAA,OAAO,CAAA,EAAG,GAAA,CAAI,IAAI,CAAA,CAAA,EAAI,MAAM,CAAA,CAAA;AAC9B;AAeO,SAAS,YAAY,GAAA,EAAqB;AAE/C,EAAA,MAAM,SAAA,GAAY,GAAA,CAAI,WAAA,CAAY,GAAG,CAAA;AACrC,EAAA,IAAI,cAAc,EAAA,EAAI;AACpB,IAAA,MAAM,IAAI,MAAM,4CAA4C,CAAA;AAAA,EAC9D;AAEA,EAAA,MAAM,SAAA,GAAY,GAAA,CAAI,SAAA,CAAU,CAAA,EAAG,SAAS,CAAA;AAC5C,EAAA,MAAM,MAAA,GAAS,GAAA,CAAI,SAAA,CAAU,SAAA,GAAY,CAAC,CAAA;AAE1C,EAAA,IAAI,CAAC,aAAA,CAAc,MAAM,CAAA,EAAG;AAC1B,IAAA,MAAM,IAAI,MAAM,yCAAyC,CAAA;AAAA,EAC3D;AAEA,EAAA,IAAI,GAAA;AACJ,EAAA,IAAI;AACF,IAAA,GAAA,GAAM,IAAI,IAAI,SAAS,CAAA;AAAA,EACzB,CAAA,CAAA,MAAQ;AACN,IAAA,MAAM,IAAI,MAAM,sCAAsC,CAAA;AAAA,EACxD;AAEA,EAAA,IAAI,GAAA,CAAI,aAAa,QAAA,EAAU;AAC7B,IAAA,MAAM,IAAI,MAAM,mCAAmC,CAAA;AAAA,EACrD;AAEA,EAAA,kBAAA,CAAmB,KAAK,eAAe,CAAA;AAEvC,EAAA,OAAO,EAAE,GAAA,EAAK,SAAA,EAAW,MAAA,EAAO;AAClC;;;;"}
|
|
1
|
+
{"version":3,"file":"catalog-helpers.esm.js","sources":["../src/catalog-helpers.ts"],"sourcesContent":["/*\n * Copyright Red Hat, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Pure identity, tag, version, and source-reference helpers for\n * catalog entity mapping (design D5).\n *\n * These helpers are consumed by the common catalog provider and have\n * no network, database, or Catalog side effects.\n *\n * @packageDocumentation\n */\n\nimport { createHash } from 'crypto';\n\nimport { CATALOG_TAG_PATTERN, MAX_TAG_LENGTH } from './tag-constants';\nimport type { SkillSourceType } from './types';\nimport { isValidDigest } from './validation';\n\n// ─── Identity ────────────────────────────────────────────────────────\n\n/**\n * The identity tuple used for deterministic catalog entity naming.\n *\n * @public\n */\nexport interface IdentityTuple {\n /** Source type discriminator. */\n type: SkillSourceType;\n /** Configured source ID. */\n id: string;\n /** Stable record key within the source. */\n key: string;\n}\n\n/**\n * Computes the deterministic catalog entity name from an identity tuple.\n *\n * Per design D5, the name is `skill-` followed by the first 56 lowercase\n * hex digits of the SHA-256 hash of the compact JSON UTF-8 encoding of\n * `[source.type, source.id, record.key]`.\n *\n * Names, versions, tags, and content digests do not affect entity\n * identity. A source ID or native key change creates a different name.\n *\n * @remarks\n * The hashing algorithm (SHA-256 of compact JSON identity tuple, truncated\n * to 56 hex digits) is part of the stable public contract. Persisted\n * entities depend on this producing the same output for the same input.\n * Any change to the algorithm constitutes a breaking (major) change.\n *\n * @public\n */\nexport function computeCatalogName(tuple: IdentityTuple): string {\n const payload = JSON.stringify([tuple.type, tuple.id, tuple.key]);\n const hash = createHash('sha256').update(payload, 'utf8').digest('hex');\n return `skill-${hash.substring(0, 56)}`;\n}\n\n// ─── Tag normalization ───────────────────────────────────────────────\n\n/**\n * Result of tag normalization.\n *\n * @public\n */\nexport interface NormalizeTagsResult {\n /** Valid, deduplicated tags in lowercase. */\n tags: string[];\n /**\n * Diagnostic messages for tags that were dropped.\n *\n * @remarks\n * Message text is informational and may change. Consumers must not\n * treat wording as a stable API or use it for programmatic decisions.\n */\n diagnostics: string[];\n}\n\n/**\n * Normalizes tags for catalog use per design D3/D5.\n *\n * Trims whitespace, lowercases, deduplicates, and retains only values\n * accepted by {@link CATALOG_TAG_PATTERN} (per design D3/D5). Invalid or\n * overlength values are omitted with a diagnostic — tags are never\n * truncated and replacement characters are never synthesized.\n *\n * @public\n */\nexport function normalizeTags(tags: string[]): NormalizeTagsResult {\n const seen = new Set<string>();\n const result: string[] = [];\n const diagnostics: string[] = [];\n\n for (const raw of tags) {\n const trimmed = raw.trim().toLocaleLowerCase('en-US');\n\n if (trimmed.length === 0) {\n diagnostics.push(`tag '${raw}': empty after trim, omitted`);\n continue;\n }\n\n if (seen.has(trimmed)) {\n diagnostics.push(`tag '${raw}': duplicate '${trimmed}', omitted`);\n continue;\n }\n\n if (trimmed.length > MAX_TAG_LENGTH) {\n diagnostics.push(\n `tag '${raw}': length ${trimmed.length} exceeds maximum ${MAX_TAG_LENGTH}, omitted`,\n );\n seen.add(trimmed);\n continue;\n }\n\n if (!CATALOG_TAG_PATTERN.test(trimmed)) {\n diagnostics.push(\n `tag '${raw}': '${trimmed}' does not match catalog tag pattern, omitted`,\n );\n seen.add(trimmed);\n continue;\n }\n\n seen.add(trimmed);\n result.push(trimmed);\n }\n\n return { tags: result, diagnostics };\n}\n\n// ─── SemVer fallback ─────────────────────────────────────────────────\n\n/**\n * SemVer validation regex per https://semver.org/#is-there-a-suggested-regular-expression-to-check-a-semver-string\n *\n * Matches: MAJOR.MINOR.PATCH with optional pre-release and build metadata.\n */\nconst SEMVER_RE =\n /^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$/;\n\n/**\n * Resolves a catalog version from a declared version string and record\n * digest, per design D5.\n *\n * Removes at most one leading `v`, validates the result as SemVer, and\n * falls back to `0.0.0+<first 12 hex digits of record.digest>` when no\n * valid SemVer is present.\n *\n * @param declaredVersion - The raw version string from the record, or undefined.\n * @param digest - The record's content digest in `sha256:<64 hex>` format.\n * @returns A valid SemVer string.\n *\n * @public\n */\nexport function resolveVersion(\n declaredVersion: string | undefined,\n digest: string,\n): string {\n // Validate digest upfront, consistent with buildOciRef and buildNpxRef.\n if (!isValidDigest(digest)) {\n throw new Error(`invalid digest: ${digest}`);\n }\n\n if (declaredVersion !== undefined && declaredVersion !== '') {\n let candidate = declaredVersion;\n if (candidate.startsWith('v')) {\n candidate = candidate.substring(1);\n }\n if (SEMVER_RE.test(candidate)) {\n return candidate;\n }\n }\n\n // Fallback: 0.0.0+<first 12 hex digits of digest>\n // digest format is sha256:<64 hex>, so hex starts at index 7\n const hex = digest.substring(7, 19);\n return `0.0.0+${hex}`;\n}\n\n// ─── OCI references ──────────────────────────────────────────────────\n\n/**\n * A parsed OCI skill reference.\n *\n * @public\n */\nexport interface OciRef {\n /** Full OCI URI: `oci://<registry>/<repository>@sha256:<hex>`. */\n uri: string;\n /** Registry host (and optional port). */\n registry: string;\n /** Repository path (may contain slashes). */\n repository: string;\n /** Content digest in `sha256:<64 hex>` format. */\n digest: string;\n}\n\n/**\n * OCI URI pattern: `oci://<registry>/<repository>@sha256:<64 hex digits>`.\n */\nconst OCI_URI_RE = /^oci:\\/\\/([^/]+)\\/(.+)@(sha256:[0-9a-f]{64})$/;\n\n/**\n * Constructs a digest-addressed OCI reference URI.\n *\n * The returned URI has the form `oci://<registry>/<repository>@sha256:<hex>`.\n * The provided digest must be a valid SHA-256 digest string.\n *\n * @param registry - Registry host (e.g., `quay.io`).\n * @param repository - Repository path (e.g., `octo/hello-world-skill`).\n * @param digest - Content digest in `sha256:<64 hex>` format.\n * @returns The constructed OCI URI.\n * @throws If the digest is invalid.\n *\n * @public\n */\nexport function buildOciRef(\n registry: string,\n repository: string,\n digest: string,\n): string {\n if (!isValidDigest(digest)) {\n throw new Error(`invalid digest: ${digest}`);\n }\n if (!registry || !repository) {\n throw new Error(\n `registry and repository must be non-empty: registry=${JSON.stringify(\n registry,\n )}, repository=${JSON.stringify(repository)}`,\n );\n }\n return `oci://${registry}/${repository}@${digest}`;\n}\n\n/**\n * Parses a digest-addressed OCI reference URI.\n *\n * Rejects URIs that do not match the `oci://<registry>/<repository>@sha256:<hex>`\n * pattern. When a `recordDigest` is provided, validates that the URI digest\n * agrees with the record digest.\n *\n * @param uri - The OCI URI to parse.\n * @param recordDigest - Optional record digest to verify agreement.\n * @returns The parsed OCI reference.\n * @throws If the URI is malformed or the digest does not agree.\n *\n * @public\n */\nexport function parseOciRef(uri: string, recordDigest?: string): OciRef {\n const match = OCI_URI_RE.exec(uri);\n if (!match) {\n throw new Error(`malformed OCI URI: ${uri}`);\n }\n\n const [, registry, repository, digest] = match;\n\n if (recordDigest !== undefined && digest !== recordDigest) {\n throw new Error(\n `OCI URI digest '${digest}' does not match record digest '${recordDigest}'`,\n );\n }\n\n return { uri, registry, repository, digest };\n}\n\n// ─── npx references ─────────────────────────────────────────────────\n\n/**\n * A parsed npx skill reference.\n *\n * @public\n */\nexport interface NpxRef {\n /** Serialized reference: `<sourceUri>#<digest>`. */\n ref: string;\n /** The artifact source URL (HTTPS, no credentials or fragments). */\n sourceUri: string;\n /** Content digest in `sha256:<64 hex>` format. */\n digest: string;\n}\n\n/**\n * Query parameter names that indicate credentials or signed access tokens.\n * Individually rejected regardless of context.\n */\nconst SENSITIVE_QUERY_PARAMS: ReadonlySet<string> = new Set([\n 'sig',\n 'signature',\n 'token',\n 'access_token',\n 'api_key',\n 'apikey',\n 'secret',\n 'password',\n 'private_token',\n 'auth_token',\n 'bearer_token',\n 'x-amz-credential',\n 'x-amz-signature',\n 'x-amz-security-token',\n 'x-goog-signature',\n 'client_secret',\n 'refresh_token',\n]);\n\n/**\n * Azure SAS token query parameters. These short, common names (`se`, `sp`,\n * `sv`, etc.) can appear in non-credential contexts, so they are only\n * rejected when {@link AZURE_SAS_CO_OCCURRENCE_THRESHOLD} or more appear\n * together — a strong signal of a signed URL.\n */\nconst AZURE_SAS_PARAMS: ReadonlySet<string> = new Set([\n 'se', // expiry\n 'sp', // permissions\n 'spr', // protocol\n 'sv', // version\n 'ss', // services\n 'srt', // resource types\n 'st', // start time\n]);\n\n/** Minimum number of Azure SAS params that must co-occur to trigger rejection. */\nconst AZURE_SAS_CO_OCCURRENCE_THRESHOLD = 3;\n\n/**\n * Checks whether a URL contains credentials (userinfo), fragments,\n * query-string credentials, or signed access tokens.\n *\n * @param url - The URL to inspect.\n * @param context - Optional context prefix for error messages (e.g., `\"npx reference\"`).\n * @throws If the URL contains sensitive elements.\n */\nfunction rejectSensitiveUrl(url: URL, context?: string): void {\n const prefix = context ? `${context}: ` : '';\n\n if (url.username || url.password) {\n throw new Error(`${prefix}URL contains credentials`);\n }\n\n if (url.hash) {\n throw new Error(`${prefix}URL contains a fragment`);\n }\n\n let azureSasCount = 0;\n for (const [param] of url.searchParams) {\n const lower = param.toLocaleLowerCase('en-US');\n if (SENSITIVE_QUERY_PARAMS.has(lower)) {\n throw new Error(\n `${prefix}URL contains sensitive query parameter '${param}'`,\n );\n }\n if (AZURE_SAS_PARAMS.has(lower)) {\n azureSasCount++;\n }\n }\n\n if (azureSasCount >= AZURE_SAS_CO_OCCURRENCE_THRESHOLD) {\n throw new Error(\n `${prefix}URL contains Azure SAS token parameters (${azureSasCount} of ${AZURE_SAS_PARAMS.size} present)`,\n );\n }\n}\n\n/**\n * Constructs an npx skill reference from a verified artifact URL and digest.\n *\n * The reference has the form `<sourceUri>#<digest>` where `sourceUri` is an\n * absolute HTTPS URL without credentials or fragments, and `digest` is a\n * lowercase SHA-256 digest.\n *\n * Non-sensitive query parameters retain their original order.\n *\n * @param sourceUri - Absolute HTTPS artifact URL.\n * @param digest - Content digest in `sha256:<64 hex>` format.\n * @returns The serialized npx reference string.\n * @throws If the URL contains credentials, fragments, or sensitive params,\n * or if the digest is invalid, or the URL is not HTTPS.\n *\n * @public\n */\nexport function buildNpxRef(sourceUri: string, digest: string): string {\n if (!isValidDigest(digest)) {\n throw new Error(`invalid digest: ${digest}`);\n }\n\n let url: URL;\n try {\n url = new URL(sourceUri);\n } catch {\n throw new Error('invalid URL');\n }\n\n if (url.protocol !== 'https:') {\n throw new Error('npx reference: URL must use HTTPS');\n }\n\n rejectSensitiveUrl(url, 'npx reference');\n\n // Serialize using URL API to normalize, preserving query order\n return `${url.href}#${digest}`;\n}\n\n/**\n * Parses an npx skill reference.\n *\n * Splits at the single literal `#` separator. Validates that the URL is\n * HTTPS, contains no credentials or fragments, and that the digest is a\n * valid SHA-256 digest.\n *\n * @param ref - The serialized npx reference (`<sourceUri>#<digest>`).\n * @returns The parsed npx reference.\n * @throws If the reference is malformed or the URL is sensitive.\n *\n * @public\n */\nexport function parseNpxRef(ref: string): NpxRef {\n // Split at the last '#' to separate URL from digest\n const hashIndex = ref.lastIndexOf('#');\n if (hashIndex === -1) {\n throw new Error(\"malformed npx reference (no '#' separator)\");\n }\n\n const sourceUri = ref.substring(0, hashIndex);\n const digest = ref.substring(hashIndex + 1);\n\n if (!isValidDigest(digest)) {\n throw new Error('malformed npx reference: invalid digest');\n }\n\n let url: URL;\n try {\n url = new URL(sourceUri);\n } catch {\n throw new Error('malformed npx reference: invalid URL');\n }\n\n if (url.protocol !== 'https:') {\n throw new Error('npx reference: URL must use HTTPS');\n }\n\n rejectSensitiveUrl(url, 'npx reference');\n\n return { ref, sourceUri, digest };\n}\n"],"names":[],"mappings":"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"}
|
package/dist/fixtures.cjs.js
CHANGED
|
@@ -236,6 +236,115 @@ const invalidSnapshotNonUtcTimestamp = {
|
|
|
236
236
|
skills: [],
|
|
237
237
|
failedSkillKeys: []
|
|
238
238
|
};
|
|
239
|
+
const ociSkillCardFull = {
|
|
240
|
+
metadata: {
|
|
241
|
+
"display-name": "Hello World Skill",
|
|
242
|
+
name: "hello-world-skill",
|
|
243
|
+
description: "A demo OCI skill image",
|
|
244
|
+
version: "1.0.0",
|
|
245
|
+
license: "Apache-2.0",
|
|
246
|
+
authors: [{ name: "OCTO Team", email: "octo@example.com" }],
|
|
247
|
+
tags: ["Demo", "hello-world"],
|
|
248
|
+
compatibility: "rhdh-2.2",
|
|
249
|
+
namespace: "octo-skills"
|
|
250
|
+
},
|
|
251
|
+
spec: {
|
|
252
|
+
prompt: "You are a helpful assistant."
|
|
253
|
+
}
|
|
254
|
+
};
|
|
255
|
+
const ociMarkdownFrontmatterFull = {
|
|
256
|
+
name: "Hello World (Markdown)",
|
|
257
|
+
description: "A demo OCI skill from Markdown",
|
|
258
|
+
version: "0.9.0",
|
|
259
|
+
license: "MIT",
|
|
260
|
+
compatibility: "rhdh-2.1",
|
|
261
|
+
metadata: {
|
|
262
|
+
version: "1.0",
|
|
263
|
+
author: "Markdown Author",
|
|
264
|
+
tags: ["markdown-tag"],
|
|
265
|
+
owner: "team-octo",
|
|
266
|
+
lifecycle: "production"
|
|
267
|
+
}
|
|
268
|
+
};
|
|
269
|
+
const ociSkillCardConflictingVersion = {
|
|
270
|
+
metadata: {
|
|
271
|
+
name: "version-test-skill",
|
|
272
|
+
version: "1.0.0"
|
|
273
|
+
}
|
|
274
|
+
};
|
|
275
|
+
const ociMarkdownFrontmatterConflictingVersion = {
|
|
276
|
+
name: "Version Test (Markdown)",
|
|
277
|
+
metadata: {
|
|
278
|
+
version: "1.0"
|
|
279
|
+
},
|
|
280
|
+
version: "0.9.0"
|
|
281
|
+
};
|
|
282
|
+
const ociSkillCardMinimal = {
|
|
283
|
+
metadata: {
|
|
284
|
+
name: "Minimal Skill"
|
|
285
|
+
}
|
|
286
|
+
};
|
|
287
|
+
const ociNativeInputFull = {
|
|
288
|
+
skillCard: ociSkillCardFull,
|
|
289
|
+
frontmatter: ociMarkdownFrontmatterFull
|
|
290
|
+
};
|
|
291
|
+
const ociNativeInputConflictingVersion = {
|
|
292
|
+
skillCard: ociSkillCardConflictingVersion,
|
|
293
|
+
frontmatter: ociMarkdownFrontmatterConflictingVersion
|
|
294
|
+
};
|
|
295
|
+
const ociNativeInputMinimal = {
|
|
296
|
+
skillCard: ociSkillCardMinimal
|
|
297
|
+
};
|
|
298
|
+
const ociTrustedInput = {
|
|
299
|
+
key: "quay.io/octo/hello-world-skill:1.0.0-draft",
|
|
300
|
+
sourceUri: "oci://quay.io/octo/hello-world-skill@sha256:abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789",
|
|
301
|
+
digest: "sha256:abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789"
|
|
302
|
+
};
|
|
303
|
+
const npxDiscoveryEntryFull = {
|
|
304
|
+
name: "summarize-text",
|
|
305
|
+
description: "Summarizes input text using an LLM",
|
|
306
|
+
type: "skill-md"
|
|
307
|
+
};
|
|
308
|
+
const npxFrontmatterFull = {
|
|
309
|
+
name: "Summarize Text",
|
|
310
|
+
description: "A skill for summarizing text content",
|
|
311
|
+
version: "0.2.0",
|
|
312
|
+
license: "MIT",
|
|
313
|
+
compatibility: "agent-skills-v0.2",
|
|
314
|
+
metadata: {
|
|
315
|
+
version: "0.2.0",
|
|
316
|
+
author: "Agent Team",
|
|
317
|
+
tags: ["summarization", "text"],
|
|
318
|
+
owner: "team-agents",
|
|
319
|
+
lifecycle: "experimental"
|
|
320
|
+
}
|
|
321
|
+
};
|
|
322
|
+
const npxDiscoveryEntryMinimal = {
|
|
323
|
+
name: "basic-skill"
|
|
324
|
+
};
|
|
325
|
+
const npxFrontmatterConflictingVersion = {
|
|
326
|
+
name: "Version Conflict Skill",
|
|
327
|
+
metadata: {
|
|
328
|
+
version: "2.0.0"
|
|
329
|
+
},
|
|
330
|
+
version: "1.5.0"
|
|
331
|
+
};
|
|
332
|
+
const npxNativeInputFull = {
|
|
333
|
+
entry: npxDiscoveryEntryFull,
|
|
334
|
+
frontmatter: npxFrontmatterFull
|
|
335
|
+
};
|
|
336
|
+
const npxNativeInputConflictingVersion = {
|
|
337
|
+
entry: npxDiscoveryEntryMinimal,
|
|
338
|
+
frontmatter: npxFrontmatterConflictingVersion
|
|
339
|
+
};
|
|
340
|
+
const npxNativeInputMinimal = {
|
|
341
|
+
entry: npxDiscoveryEntryMinimal
|
|
342
|
+
};
|
|
343
|
+
const npxTrustedInput = {
|
|
344
|
+
key: "summarize-text",
|
|
345
|
+
sourceUri: "https://registry.example.com/skills/summarize-text/SKILL.md",
|
|
346
|
+
digest: "sha256:fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210"
|
|
347
|
+
};
|
|
239
348
|
|
|
240
349
|
exports.invalidFailedWithSkills = invalidFailedWithSkills;
|
|
241
350
|
exports.invalidLoadingWithObservedAt = invalidLoadingWithObservedAt;
|
|
@@ -251,8 +360,25 @@ exports.invalidSnapshotDuplicateKeys = invalidSnapshotDuplicateKeys;
|
|
|
251
360
|
exports.invalidSnapshotNonUtcTimestamp = invalidSnapshotNonUtcTimestamp;
|
|
252
361
|
exports.invalidSnapshotOverlappingKeys = invalidSnapshotOverlappingKeys;
|
|
253
362
|
exports.invalidSnapshotReadyWithFailedKeys = invalidSnapshotReadyWithFailedKeys;
|
|
363
|
+
exports.npxDiscoveryEntryFull = npxDiscoveryEntryFull;
|
|
364
|
+
exports.npxDiscoveryEntryMinimal = npxDiscoveryEntryMinimal;
|
|
365
|
+
exports.npxFrontmatterConflictingVersion = npxFrontmatterConflictingVersion;
|
|
366
|
+
exports.npxFrontmatterFull = npxFrontmatterFull;
|
|
367
|
+
exports.npxNativeInputConflictingVersion = npxNativeInputConflictingVersion;
|
|
368
|
+
exports.npxNativeInputFull = npxNativeInputFull;
|
|
369
|
+
exports.npxNativeInputMinimal = npxNativeInputMinimal;
|
|
254
370
|
exports.npxSource = npxSource;
|
|
371
|
+
exports.npxTrustedInput = npxTrustedInput;
|
|
372
|
+
exports.ociMarkdownFrontmatterConflictingVersion = ociMarkdownFrontmatterConflictingVersion;
|
|
373
|
+
exports.ociMarkdownFrontmatterFull = ociMarkdownFrontmatterFull;
|
|
374
|
+
exports.ociNativeInputConflictingVersion = ociNativeInputConflictingVersion;
|
|
375
|
+
exports.ociNativeInputFull = ociNativeInputFull;
|
|
376
|
+
exports.ociNativeInputMinimal = ociNativeInputMinimal;
|
|
377
|
+
exports.ociSkillCardConflictingVersion = ociSkillCardConflictingVersion;
|
|
378
|
+
exports.ociSkillCardFull = ociSkillCardFull;
|
|
379
|
+
exports.ociSkillCardMinimal = ociSkillCardMinimal;
|
|
255
380
|
exports.ociSource = ociSource;
|
|
381
|
+
exports.ociTrustedInput = ociTrustedInput;
|
|
256
382
|
exports.validFailedSnapshot = validFailedSnapshot;
|
|
257
383
|
exports.validLoadingSnapshot = validLoadingSnapshot;
|
|
258
384
|
exports.validNpxRecordFull = validNpxRecordFull;
|