@red-hat-developer-hub/backstage-plugin-ai-skills-common 0.0.0-stage → 0.3.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 ADDED
@@ -0,0 +1,27 @@
1
+ # @red-hat-developer-hub/backstage-plugin-ai-skills-common
2
+
3
+ ## 0.3.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 910c3a6: Discover all active tags in public Quay repositories when `quayDiscovery.tag` is
8
+ omitted or blank. Set `tag: latest` explicitly to retain the previous default;
9
+ other supplied tags remain exact filters. Bound repository and tag pagination
10
+ together, and preserve distinct tagged references even when they share a digest.
11
+ Update shared contract fixtures and documentation for a separate catalog identity
12
+ per valid OCI skill-image tag.
13
+
14
+ OCI record producers adopting this contract must construct keys as
15
+ `<registry>/<repository>:<tag>` instead of `<registry>/<repository>`, preserving
16
+ the exact tag case. Consumers with persisted repository-only identities must
17
+ plan for replacement catalog identities and update references when adopting
18
+ tagged keys. The shared hashing algorithm and validators are unchanged: the same
19
+ identity tuple still yields the same catalog name. Upgrading the common library
20
+ alone does not rewrite caller-supplied keys or migrate existing entities.
21
+
22
+ ## 0.2.0
23
+
24
+ ### Minor Changes
25
+
26
+ - d69098f: Add stable identity, tag normalization, SemVer fallback, and OCI/npx reference helpers per design D5 (OpenSpec task 1.3)
27
+ - dc2f19a: Add ai-skills-common shared library with v1 SkillRecord and SkillSnapshot contract, runtime validators, deterministic bounded snapshot construction, REST contract documentation, and reusable OCI/npx contract fixtures
package/README.md CHANGED
@@ -1,3 +1,127 @@
1
- # Temporary Holding Version
1
+ # ai-skills-common
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Shared v1 skill record and snapshot contract for RHDH skill connectors
4
+ and the common catalog provider.
5
+
6
+ This library owns schemas, validation, and pure helpers. It performs
7
+ no network, scheduler, database, or Catalog operations.
8
+
9
+ ## Overview
10
+
11
+ The `ai-skills-common` package defines the versioned normalized REST contract
12
+ (`SkillRecord`, `SkillSnapshot`) that OCI and npx skill connectors expose
13
+ at `GET /skills/:sourceId`. The common catalog provider validates and
14
+ consumes these snapshots to produce `AiResource` entities.
15
+
16
+ ## Types
17
+
18
+ | Type | Description |
19
+ | ---------------- | ----------------------------------------------------------------- |
20
+ | `SkillRecord` | Base normalized v1 skill record with required and optional fields |
21
+ | `OciSkillRecord` | OCI-source record with optional `extensions.oci` container |
22
+ | `NpxSkillRecord` | npx-source record with optional `extensions.npx` container |
23
+ | `SkillSnapshot` | Versioned snapshot containing records, status, and metadata |
24
+ | `SnapshotSource` | Source identity (`id` and `type`) |
25
+ | `SnapshotStatus` | `'loading' \| 'ready' \| 'partial' \| 'failed'` |
26
+
27
+ OCI discovery records use `<registry>/<repository>:<tag>` as their stable key;
28
+ the registry host is lowercase and the tag retains its exact case. Two tags may
29
+ share the same manifest digest and `sourceUri` while retaining distinct keys and
30
+ catalog identities. A tag moving to different content keeps the same identity.
31
+ The connector constructs these keys; the shared helpers consume them as opaque
32
+ strings and continue to reject duplicate snapshot keys. Digest-addressed OCI
33
+ reference construction and declared-version mapping are unchanged.
34
+
35
+ ## Constants
36
+
37
+ | Constant | Value | Description |
38
+ | -------------------------- | ----------------- | ------------------------------------------------ |
39
+ | `MAX_SNAPSHOT_RECORDS` | `1_000` | Maximum number of records in a single snapshot |
40
+ | `MAX_SNAPSHOT_BYTES` | `5 * 1024 * 1024` | Maximum serialized snapshot size (5 MiB) |
41
+ | `MAX_RESPONSE_BYTES` | `5 * 1024 * 1024` | Maximum response read size for consumers (5 MiB) |
42
+ | `SUPPORTED_SCHEMA_VERSION` | `'1'` | Supported schema version |
43
+
44
+ ## Validation
45
+
46
+ - `validateSnapshot(snapshot)` — validates a complete v1 snapshot
47
+ including schema version, source identity, status invariants,
48
+ record fields, source-discriminated extensions, unique keys,
49
+ disjoint failed keys, and count bounds.
50
+ - `validateSnapshotSize(snapshot)` — validates serialized byte size
51
+ against the 5 MiB limit.
52
+ - `isValidDigest(digest)` — checks `sha256:<64 lowercase hex>` format.
53
+ - `isValidUtcTimestamp(value)` — checks for valid ISO 8601 UTC
54
+ timestamps (strings ending in `Z` or `+00:00`).
55
+
56
+ ## Snapshot construction
57
+
58
+ - `boundSnapshot(options)` — creates a bounded v1 snapshot with
59
+ deterministic stable-key ordering and longest-prefix selection
60
+ within count (1,000) and byte-size (5 MiB) limits.
61
+ - `sortRecordsByKey(records)` — sorts skill records by stable `key`
62
+ in ascending Unicode code-point order.
63
+ - `createLoadingSnapshot(source)` — creates a loading snapshot for
64
+ initial state.
65
+ - `createFailedSnapshot(source, observedAt)` — creates a failed
66
+ snapshot.
67
+
68
+ ## REST contract
69
+
70
+ Each connector exposes `GET /skills/:sourceId` relative to its
71
+ Backstage plugin base URL. The endpoint uses Backstage's default
72
+ backend service-to-service authentication. Responses contain a single
73
+ bounded `SkillSnapshot` v1 JSON body.
74
+
75
+ | Status | Meaning |
76
+ | ------ | ---------------------------- |
77
+ | 200 | Returns a `SkillSnapshot` v1 |
78
+ | 404 | Unknown `sourceId` |
79
+
80
+ ## Catalog helpers
81
+
82
+ Pure identity, tag, version, and source-reference helpers for catalog
83
+ entity mapping (design D5). These helpers have no network, database, or
84
+ Catalog side effects.
85
+
86
+ ### Types
87
+
88
+ | Type | Description |
89
+ | --------------------- | -------------------------------------------------------------------- |
90
+ | `IdentityTuple` | Identity tuple (`type`, `id`, `key`) for deterministic entity naming |
91
+ | `NormalizeTagsResult` | Result of tag normalization: valid tags and diagnostics |
92
+ | `OciRef` | Parsed OCI skill reference (registry, repository, digest, URI) |
93
+ | `NpxRef` | Parsed npx skill reference (sourceUri, digest, ref) |
94
+
95
+ ### Functions
96
+
97
+ - `computeCatalogName(tuple)` — computes the deterministic catalog entity
98
+ name from an identity tuple via SHA-256 of the compact JSON encoding
99
+ `[type, id, key]`, prefixed with `skill-` (first 56 hex digits).
100
+ - `normalizeTags(tags)` — trims, lowercases, deduplicates, and validates
101
+ tags against an extended catalog tag pattern (per design D3/D5) that
102
+ allows colons, plus signs, and hash characters beyond the standard
103
+ Backstage tag validator. Returns valid tags and diagnostics for dropped
104
+ entries.
105
+ - `resolveVersion(declaredVersion, digest)` — strips at most one leading
106
+ `v`, validates as SemVer, and falls back to `0.0.0+<first 12 hex digits
107
+ of digest>` when no valid SemVer is present.
108
+ - `buildOciRef(registry, repository, digest)` — constructs a
109
+ digest-addressed OCI URI (`oci://<registry>/<repository>@<digest>`).
110
+ - `parseOciRef(uri, recordDigest?)` — parses an OCI URI and optionally
111
+ validates digest agreement with a record digest.
112
+ - `buildNpxRef(sourceUri, digest)` — constructs an npx reference
113
+ (`<sourceUri>#<digest>`) from an HTTPS URL, rejecting credentials,
114
+ fragments, and sensitive query parameters.
115
+ - `parseNpxRef(ref)` — parses an npx reference, validating the URL and
116
+ digest.
117
+
118
+ ## Fixtures
119
+
120
+ The package exports valid and invalid OCI/npx fixtures for connector
121
+ and provider contract tests. Import them from the package root.
122
+
123
+ ## Installation
124
+
125
+ ```bash
126
+ yarn add @red-hat-developer-hub/backstage-plugin-ai-skills-common
127
+ ```
@@ -0,0 +1,198 @@
1
+ 'use strict';
2
+
3
+ var crypto = require('crypto');
4
+ var validation = require('./validation.cjs.js');
5
+
6
+ function computeCatalogName(tuple) {
7
+ const payload = JSON.stringify([tuple.type, tuple.id, tuple.key]);
8
+ const hash = crypto.createHash("sha256").update(payload, "utf8").digest("hex");
9
+ return `skill-${hash.substring(0, 56)}`;
10
+ }
11
+ const CATALOG_TAG_RE = /^[a-z0-9:+#]+(-[a-z0-9:+#]+)*$/;
12
+ const MAX_TAG_LENGTH = 63;
13
+ function normalizeTags(tags) {
14
+ const seen = /* @__PURE__ */ new Set();
15
+ const result = [];
16
+ const diagnostics = [];
17
+ for (const raw of tags) {
18
+ const trimmed = raw.trim().toLocaleLowerCase("en-US");
19
+ if (trimmed.length === 0) {
20
+ diagnostics.push(`tag '${raw}': empty after trim, skipped`);
21
+ continue;
22
+ }
23
+ if (seen.has(trimmed)) {
24
+ diagnostics.push(`tag '${raw}': duplicate '${trimmed}', skipped`);
25
+ continue;
26
+ }
27
+ if (trimmed.length > MAX_TAG_LENGTH) {
28
+ diagnostics.push(
29
+ `tag '${raw}': length ${trimmed.length} exceeds maximum ${MAX_TAG_LENGTH}, skipped`
30
+ );
31
+ seen.add(trimmed);
32
+ continue;
33
+ }
34
+ if (!CATALOG_TAG_RE.test(trimmed)) {
35
+ diagnostics.push(
36
+ `tag '${raw}': '${trimmed}' does not match catalog tag pattern, skipped`
37
+ );
38
+ seen.add(trimmed);
39
+ continue;
40
+ }
41
+ seen.add(trimmed);
42
+ result.push(trimmed);
43
+ }
44
+ return { tags: result, diagnostics };
45
+ }
46
+ const SEMVER_RE = /^(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-]+)*))?$/;
47
+ function resolveVersion(declaredVersion, digest) {
48
+ if (!validation.isValidDigest(digest)) {
49
+ throw new Error(`invalid digest: ${digest}`);
50
+ }
51
+ if (declaredVersion !== void 0 && declaredVersion !== "") {
52
+ let candidate = declaredVersion;
53
+ if (candidate.startsWith("v")) {
54
+ candidate = candidate.substring(1);
55
+ }
56
+ if (SEMVER_RE.test(candidate)) {
57
+ return candidate;
58
+ }
59
+ }
60
+ const hex = digest.substring(7, 19);
61
+ return `0.0.0+${hex}`;
62
+ }
63
+ const OCI_URI_RE = /^oci:\/\/([^/]+)\/(.+)@(sha256:[0-9a-f]{64})$/;
64
+ function buildOciRef(registry, repository, digest) {
65
+ if (!validation.isValidDigest(digest)) {
66
+ throw new Error(`invalid digest: ${digest}`);
67
+ }
68
+ if (!registry || !repository) {
69
+ throw new Error(
70
+ `registry and repository must be non-empty: registry=${JSON.stringify(
71
+ registry
72
+ )}, repository=${JSON.stringify(repository)}`
73
+ );
74
+ }
75
+ return `oci://${registry}/${repository}@${digest}`;
76
+ }
77
+ function parseOciRef(uri, recordDigest) {
78
+ const match = OCI_URI_RE.exec(uri);
79
+ if (!match) {
80
+ throw new Error(`malformed OCI URI: ${uri}`);
81
+ }
82
+ const [, registry, repository, digest] = match;
83
+ if (recordDigest !== void 0 && digest !== recordDigest) {
84
+ throw new Error(
85
+ `OCI URI digest '${digest}' does not match record digest '${recordDigest}'`
86
+ );
87
+ }
88
+ return { uri, registry, repository, digest };
89
+ }
90
+ const SENSITIVE_QUERY_PARAMS = /* @__PURE__ */ new Set([
91
+ "sig",
92
+ "signature",
93
+ "token",
94
+ "access_token",
95
+ "api_key",
96
+ "apikey",
97
+ "secret",
98
+ "password",
99
+ "private_token",
100
+ "auth_token",
101
+ "bearer_token",
102
+ "x-amz-credential",
103
+ "x-amz-signature",
104
+ "x-amz-security-token",
105
+ "x-goog-signature",
106
+ "client_secret",
107
+ "refresh_token"
108
+ ]);
109
+ const AZURE_SAS_PARAMS = /* @__PURE__ */ new Set([
110
+ "se",
111
+ // expiry
112
+ "sp",
113
+ // permissions
114
+ "spr",
115
+ // protocol
116
+ "sv",
117
+ // version
118
+ "ss",
119
+ // services
120
+ "srt",
121
+ // resource types
122
+ "st"
123
+ // start time
124
+ ]);
125
+ const AZURE_SAS_CO_OCCURRENCE_THRESHOLD = 3;
126
+ function rejectSensitiveUrl(url, context) {
127
+ const prefix = `${context}: ` ;
128
+ if (url.username || url.password) {
129
+ throw new Error(`${prefix}URL contains credentials`);
130
+ }
131
+ if (url.hash) {
132
+ throw new Error(`${prefix}URL contains a fragment`);
133
+ }
134
+ let azureSasCount = 0;
135
+ for (const [param] of url.searchParams) {
136
+ const lower = param.toLocaleLowerCase("en-US");
137
+ if (SENSITIVE_QUERY_PARAMS.has(lower)) {
138
+ throw new Error(
139
+ `${prefix}URL contains sensitive query parameter '${param}'`
140
+ );
141
+ }
142
+ if (AZURE_SAS_PARAMS.has(lower)) {
143
+ azureSasCount++;
144
+ }
145
+ }
146
+ if (azureSasCount >= AZURE_SAS_CO_OCCURRENCE_THRESHOLD) {
147
+ throw new Error(
148
+ `${prefix}URL contains Azure SAS token parameters (${azureSasCount} of ${AZURE_SAS_PARAMS.size} present)`
149
+ );
150
+ }
151
+ }
152
+ function buildNpxRef(sourceUri, digest) {
153
+ if (!validation.isValidDigest(digest)) {
154
+ throw new Error(`invalid digest: ${digest}`);
155
+ }
156
+ let url;
157
+ try {
158
+ url = new URL(sourceUri);
159
+ } catch {
160
+ throw new Error("invalid URL");
161
+ }
162
+ if (url.protocol !== "https:") {
163
+ throw new Error("npx reference: URL must use HTTPS");
164
+ }
165
+ rejectSensitiveUrl(url, "npx reference");
166
+ return `${url.href}#${digest}`;
167
+ }
168
+ function parseNpxRef(ref) {
169
+ const hashIndex = ref.lastIndexOf("#");
170
+ if (hashIndex === -1) {
171
+ throw new Error("malformed npx reference (no '#' separator)");
172
+ }
173
+ const sourceUri = ref.substring(0, hashIndex);
174
+ const digest = ref.substring(hashIndex + 1);
175
+ if (!validation.isValidDigest(digest)) {
176
+ throw new Error("malformed npx reference: invalid digest");
177
+ }
178
+ let url;
179
+ try {
180
+ url = new URL(sourceUri);
181
+ } catch {
182
+ throw new Error("malformed npx reference: invalid URL");
183
+ }
184
+ if (url.protocol !== "https:") {
185
+ throw new Error("npx reference: URL must use HTTPS");
186
+ }
187
+ rejectSensitiveUrl(url, "npx reference");
188
+ return { ref, sourceUri, digest };
189
+ }
190
+
191
+ exports.buildNpxRef = buildNpxRef;
192
+ exports.buildOciRef = buildOciRef;
193
+ exports.computeCatalogName = computeCatalogName;
194
+ exports.normalizeTags = normalizeTags;
195
+ exports.parseNpxRef = parseNpxRef;
196
+ exports.parseOciRef = parseOciRef;
197
+ exports.resolveVersion = resolveVersion;
198
+ //# sourceMappingURL=catalog-helpers.cjs.js.map
@@ -0,0 +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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"}
@@ -0,0 +1,190 @@
1
+ import { createHash } from 'crypto';
2
+ import { isValidDigest } from './validation.esm.js';
3
+
4
+ function computeCatalogName(tuple) {
5
+ const payload = JSON.stringify([tuple.type, tuple.id, tuple.key]);
6
+ const hash = createHash("sha256").update(payload, "utf8").digest("hex");
7
+ return `skill-${hash.substring(0, 56)}`;
8
+ }
9
+ const CATALOG_TAG_RE = /^[a-z0-9:+#]+(-[a-z0-9:+#]+)*$/;
10
+ const MAX_TAG_LENGTH = 63;
11
+ function normalizeTags(tags) {
12
+ const seen = /* @__PURE__ */ new Set();
13
+ const result = [];
14
+ const diagnostics = [];
15
+ for (const raw of tags) {
16
+ const trimmed = raw.trim().toLocaleLowerCase("en-US");
17
+ if (trimmed.length === 0) {
18
+ diagnostics.push(`tag '${raw}': empty after trim, skipped`);
19
+ continue;
20
+ }
21
+ if (seen.has(trimmed)) {
22
+ diagnostics.push(`tag '${raw}': duplicate '${trimmed}', skipped`);
23
+ continue;
24
+ }
25
+ if (trimmed.length > MAX_TAG_LENGTH) {
26
+ diagnostics.push(
27
+ `tag '${raw}': length ${trimmed.length} exceeds maximum ${MAX_TAG_LENGTH}, skipped`
28
+ );
29
+ seen.add(trimmed);
30
+ continue;
31
+ }
32
+ if (!CATALOG_TAG_RE.test(trimmed)) {
33
+ diagnostics.push(
34
+ `tag '${raw}': '${trimmed}' does not match catalog tag pattern, skipped`
35
+ );
36
+ seen.add(trimmed);
37
+ continue;
38
+ }
39
+ seen.add(trimmed);
40
+ result.push(trimmed);
41
+ }
42
+ return { tags: result, diagnostics };
43
+ }
44
+ const SEMVER_RE = /^(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-]+)*))?$/;
45
+ function resolveVersion(declaredVersion, digest) {
46
+ if (!isValidDigest(digest)) {
47
+ throw new Error(`invalid digest: ${digest}`);
48
+ }
49
+ if (declaredVersion !== void 0 && declaredVersion !== "") {
50
+ let candidate = declaredVersion;
51
+ if (candidate.startsWith("v")) {
52
+ candidate = candidate.substring(1);
53
+ }
54
+ if (SEMVER_RE.test(candidate)) {
55
+ return candidate;
56
+ }
57
+ }
58
+ const hex = digest.substring(7, 19);
59
+ return `0.0.0+${hex}`;
60
+ }
61
+ const OCI_URI_RE = /^oci:\/\/([^/]+)\/(.+)@(sha256:[0-9a-f]{64})$/;
62
+ function buildOciRef(registry, repository, digest) {
63
+ if (!isValidDigest(digest)) {
64
+ throw new Error(`invalid digest: ${digest}`);
65
+ }
66
+ if (!registry || !repository) {
67
+ throw new Error(
68
+ `registry and repository must be non-empty: registry=${JSON.stringify(
69
+ registry
70
+ )}, repository=${JSON.stringify(repository)}`
71
+ );
72
+ }
73
+ return `oci://${registry}/${repository}@${digest}`;
74
+ }
75
+ function parseOciRef(uri, recordDigest) {
76
+ const match = OCI_URI_RE.exec(uri);
77
+ if (!match) {
78
+ throw new Error(`malformed OCI URI: ${uri}`);
79
+ }
80
+ const [, registry, repository, digest] = match;
81
+ if (recordDigest !== void 0 && digest !== recordDigest) {
82
+ throw new Error(
83
+ `OCI URI digest '${digest}' does not match record digest '${recordDigest}'`
84
+ );
85
+ }
86
+ return { uri, registry, repository, digest };
87
+ }
88
+ const SENSITIVE_QUERY_PARAMS = /* @__PURE__ */ new Set([
89
+ "sig",
90
+ "signature",
91
+ "token",
92
+ "access_token",
93
+ "api_key",
94
+ "apikey",
95
+ "secret",
96
+ "password",
97
+ "private_token",
98
+ "auth_token",
99
+ "bearer_token",
100
+ "x-amz-credential",
101
+ "x-amz-signature",
102
+ "x-amz-security-token",
103
+ "x-goog-signature",
104
+ "client_secret",
105
+ "refresh_token"
106
+ ]);
107
+ const AZURE_SAS_PARAMS = /* @__PURE__ */ new Set([
108
+ "se",
109
+ // expiry
110
+ "sp",
111
+ // permissions
112
+ "spr",
113
+ // protocol
114
+ "sv",
115
+ // version
116
+ "ss",
117
+ // services
118
+ "srt",
119
+ // resource types
120
+ "st"
121
+ // start time
122
+ ]);
123
+ const AZURE_SAS_CO_OCCURRENCE_THRESHOLD = 3;
124
+ function rejectSensitiveUrl(url, context) {
125
+ const prefix = `${context}: ` ;
126
+ if (url.username || url.password) {
127
+ throw new Error(`${prefix}URL contains credentials`);
128
+ }
129
+ if (url.hash) {
130
+ throw new Error(`${prefix}URL contains a fragment`);
131
+ }
132
+ let azureSasCount = 0;
133
+ for (const [param] of url.searchParams) {
134
+ const lower = param.toLocaleLowerCase("en-US");
135
+ if (SENSITIVE_QUERY_PARAMS.has(lower)) {
136
+ throw new Error(
137
+ `${prefix}URL contains sensitive query parameter '${param}'`
138
+ );
139
+ }
140
+ if (AZURE_SAS_PARAMS.has(lower)) {
141
+ azureSasCount++;
142
+ }
143
+ }
144
+ if (azureSasCount >= AZURE_SAS_CO_OCCURRENCE_THRESHOLD) {
145
+ throw new Error(
146
+ `${prefix}URL contains Azure SAS token parameters (${azureSasCount} of ${AZURE_SAS_PARAMS.size} present)`
147
+ );
148
+ }
149
+ }
150
+ function buildNpxRef(sourceUri, digest) {
151
+ if (!isValidDigest(digest)) {
152
+ throw new Error(`invalid digest: ${digest}`);
153
+ }
154
+ let url;
155
+ try {
156
+ url = new URL(sourceUri);
157
+ } catch {
158
+ throw new Error("invalid URL");
159
+ }
160
+ if (url.protocol !== "https:") {
161
+ throw new Error("npx reference: URL must use HTTPS");
162
+ }
163
+ rejectSensitiveUrl(url, "npx reference");
164
+ return `${url.href}#${digest}`;
165
+ }
166
+ function parseNpxRef(ref) {
167
+ const hashIndex = ref.lastIndexOf("#");
168
+ if (hashIndex === -1) {
169
+ throw new Error("malformed npx reference (no '#' separator)");
170
+ }
171
+ const sourceUri = ref.substring(0, hashIndex);
172
+ const digest = ref.substring(hashIndex + 1);
173
+ if (!isValidDigest(digest)) {
174
+ throw new Error("malformed npx reference: invalid digest");
175
+ }
176
+ let url;
177
+ try {
178
+ url = new URL(sourceUri);
179
+ } catch {
180
+ throw new Error("malformed npx reference: invalid URL");
181
+ }
182
+ if (url.protocol !== "https:") {
183
+ throw new Error("npx reference: URL must use HTTPS");
184
+ }
185
+ rejectSensitiveUrl(url, "npx reference");
186
+ return { ref, sourceUri, digest };
187
+ }
188
+
189
+ export { buildNpxRef, buildOciRef, computeCatalogName, normalizeTags, parseNpxRef, parseOciRef, resolveVersion };
190
+ //# sourceMappingURL=catalog-helpers.esm.js.map
@@ -0,0 +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":"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"}