Type result of parsing JSON as unknown until narrowed

This commit is contained in:
Michael B. Gale
2026-03-10 13:49:08 +00:00
parent c6e75ac1e8
commit c92efdb98d
3 changed files with 137 additions and 47 deletions
+51 -19
View File
@@ -47749,12 +47749,12 @@ var require_concat_map = __commonJS({
var res = [];
for (var i = 0; i < xs.length; i++) {
var x = fn(xs[i], i);
if (isArray(x)) res.push.apply(res, x);
if (isArray2(x)) res.push.apply(res, x);
else res.push(x);
}
return res;
};
var isArray = Array.isArray || function(xs) {
var isArray2 = Array.isArray || function(xs) {
return Object.prototype.toString.call(xs) === "[object Array]";
};
}
@@ -52097,8 +52097,8 @@ var require_object = __commonJS({
"node_modules/@typespec/ts-http-runtime/dist/commonjs/util/object.js"(exports2) {
"use strict";
Object.defineProperty(exports2, "__esModule", { value: true });
exports2.isObject = isObject2;
function isObject2(input) {
exports2.isObject = isObject3;
function isObject3(input) {
return typeof input === "object" && input !== null && !Array.isArray(input) && !(input instanceof RegExp) && !(input instanceof Date);
}
}
@@ -56584,7 +56584,7 @@ var require_commonjs4 = __commonJS({
exports2.computeSha256Hmac = computeSha256Hmac;
exports2.getRandomIntegerInclusive = getRandomIntegerInclusive;
exports2.isError = isError;
exports2.isObject = isObject2;
exports2.isObject = isObject3;
exports2.randomUUID = randomUUID;
exports2.uint8ArrayToString = uint8ArrayToString;
exports2.stringToUint8Array = stringToUint8Array;
@@ -56631,7 +56631,7 @@ var require_commonjs4 = __commonJS({
function isError(e) {
return tspRuntime.isError(e);
}
function isObject2(input) {
function isObject3(input) {
return tspRuntime.isObject(input);
}
function randomUUID() {
@@ -121350,6 +121350,23 @@ function isAuthToken(value, patterns = GITHUB_TOKEN_PATTERNS) {
return void 0;
}
// src/json/index.ts
function parseString(data) {
return JSON.parse(data);
}
function isObject2(value) {
return typeof value === "object";
}
function isArray(value) {
return Array.isArray(value);
}
function isString(value) {
return typeof value === "string";
}
function isStringOrUndefined(value) {
return value === void 0 || isString(value);
}
// src/languages.ts
var KnownLanguage = /* @__PURE__ */ ((KnownLanguage2) => {
KnownLanguage2["actions"] = "actions";
@@ -121373,10 +121390,10 @@ function isUsernamePassword(config) {
return hasUsername(config) && "password" in config;
}
function isToken(config) {
return "token" in config;
return "token" in config && isStringOrUndefined(config.token);
}
function isAzureConfig(config) {
return "tenant_id" in config && "client_id" in config && isDefined2(config.tenant_id) && isDefined2(config.client_id);
return "tenant_id" in config && "client_id" in config && isDefined2(config.tenant_id) && isDefined2(config.client_id) && isString(config.tenant_id) && isString(config.client_id);
}
function isAWSConfig(config) {
const requiredProperties = [
@@ -121387,14 +121404,23 @@ function isAWSConfig(config) {
"domain_owner"
];
for (const property of requiredProperties) {
if (!(property in config) || !isDefined2(config[property])) {
if (!(property in config) || !isDefined2(config[property]) || !isString(config[property])) {
return false;
}
}
if ("audience" in config && !isStringOrUndefined(config.audience)) {
return false;
}
return true;
}
function isJFrogConfig(config) {
return "jfrog_oidc_provider_name" in config && isDefined2(config.jfrog_oidc_provider_name);
if ("audience" in config && !isStringOrUndefined(config.audience)) {
return false;
}
if ("identity_mapping_name" in config && !isStringOrUndefined(config.identity_mapping_name)) {
return false;
}
return "jfrog_oidc_provider_name" in config && isDefined2(config.jfrog_oidc_provider_name) && isString(config.jfrog_oidc_provider_name);
}
function credentialToStr(credential) {
let result = `Type: ${credential.type};`;
@@ -121829,12 +121855,12 @@ var NEW_LANGUAGE_TO_REGISTRY_TYPE = {
go: ["goproxy_server", "git_source"]
};
function getRegistryAddress(registry) {
if (isDefined2(registry.url)) {
if (isDefined2(registry.url) && isString(registry.url) && isStringOrUndefined(registry.host)) {
return {
url: registry.url,
host: registry.host
};
} else if (isDefined2(registry.host)) {
} else if (isDefined2(registry.host) && isString(registry.host)) {
return {
url: void 0,
host: registry.host
@@ -121872,12 +121898,18 @@ function getAuthConfig(config) {
}
return { username: config.username, token: config.token };
} else {
if ("password" in config && isDefined2(config.password)) {
let username = void 0;
let password = void 0;
if ("password" in config && isString(config.password)) {
core10.setSecret(config.password);
password = config.password;
}
if ("username" in config && isString(config.username)) {
username = config.username;
}
return {
username: "username" in config ? config.username : void 0,
password: "password" in config ? config.password : void 0
username,
password
};
}
}
@@ -121897,22 +121929,22 @@ function getCredentials(logger, registrySecrets, registriesCredentials, language
}
let parsed;
try {
parsed = JSON.parse(credentialsStr);
parsed = parseString(credentialsStr);
} catch {
logger.error("Failed to parse the credentials data.");
throw new ConfigurationError("Invalid credentials format.");
}
if (!Array.isArray(parsed)) {
if (!isArray(parsed)) {
throw new ConfigurationError(
"Expected credentials data to be an array of configurations, but it is not."
);
}
const out = [];
for (const e of parsed) {
if (e === null || typeof e !== "object") {
if (e === null || !isObject2(e)) {
throw new ConfigurationError("Invalid credentials - must be an object");
}
if (!isDefined2(e.type)) {
if (!isDefined2(e.type) || !isString(e.type)) {
throw new ConfigurationError("Invalid credentials - must have a type");
}
const authConfig = getAuthConfig(e);
+46 -18
View File
@@ -17,11 +17,11 @@ import {
Feature,
FeatureEnablement,
} from "./feature-flags";
import * as json from "./json";
import { KnownLanguage } from "./languages";
import { Logger } from "./logging";
import {
Address,
RawCredential,
Registry,
Credential,
AuthConfig,
@@ -36,6 +36,7 @@ import {
JFrogConfig,
isUsernamePassword,
hasUsername,
RawCredential,
} from "./start-proxy/types";
import {
ActionName,
@@ -267,13 +268,19 @@ const NEW_LANGUAGE_TO_REGISTRY_TYPE: Required<RegistryMapping> = {
*
* @throws A `ConfigurationError` if the `Registry` value contains neither a `url` or `host` field.
*/
function getRegistryAddress(registry: Partial<Registry>): Address {
if (isDefined(registry.url)) {
function getRegistryAddress(
registry: json.UnvalidatedObject<Registry>,
): Address {
if (
isDefined(registry.url) &&
json.isString(registry.url) &&
json.isStringOrUndefined(registry.host)
) {
return {
url: registry.url,
host: registry.host,
};
} else if (isDefined(registry.host)) {
} else if (isDefined(registry.host) && json.isString(registry.host)) {
return {
url: undefined,
host: registry.host,
@@ -287,7 +294,9 @@ function getRegistryAddress(registry: Partial<Registry>): Address {
}
/** Extracts an `AuthConfig` value from `config`. */
export function getAuthConfig(config: Partial<AuthConfig>): AuthConfig {
export function getAuthConfig(
config: json.UnvalidatedObject<AuthConfig>,
): AuthConfig {
// Start by checking for the OIDC configurations, since they have required properties
// which we can use to identify them.
if (isAzureConfig(config)) {
@@ -311,25 +320,44 @@ export function getAuthConfig(config: Partial<AuthConfig>): AuthConfig {
audience: config.audience,
} satisfies JFrogConfig;
} else if (isToken(config)) {
// For token-based authentication, both the token and username are optional.
// If the token is absent, then it doesn't matter if we end up treating it
// as a `UsernamePassword` object internally.
// There are three scenarios for non-OIDC authentication based on the registry type:
//
// 1. `username`+`token`
// 2. A `token` that combines the username and actual token, seperated by ':'.
// 3. `username`+`password`
//
// In all three cases, all fields are optional. If the `token` field is present,
// we accept the configuration as a `Token` typed configuration, with the `token`
// value and an optional `username`. Otherwise, we accept the configuration
// typed as `UsernamePassword` (in the `else` clause below) with optional
// username and password. I.e. a private registry type that uses 1. or 2.,
// but has no `token` configured, will get accepted as `UsernamePassword` here.
// Mask token to reduce chance of accidental leakage in logs, if we have one.
if (isDefined(config.token)) {
// Mask token to reduce chance of accidental leakage in logs, if we have one.
core.setSecret(config.token);
}
return { username: config.username, token: config.token } satisfies Token;
} else {
// Mask password to reduce chance of accidental leakage in logs, if we have one.
if ("password" in config && isDefined(config.password)) {
let username: string | undefined = undefined;
let password: string | undefined = undefined;
// Both "username" and "password" are optional. If we have reached this point, we need
// to validate which of them are present and that they have the correct type if so.
if ("password" in config && json.isString(config.password)) {
// Mask password to reduce chance of accidental leakage in logs, if we have one.
core.setSecret(config.password);
password = config.password;
}
if ("username" in config && json.isString(config.username)) {
username = config.username;
}
// Return the `UsernamePassword` object. Both username and password may be undefined.
return {
username: "username" in config ? config.username : undefined,
password: "password" in config ? config.password : undefined,
username,
password,
} satisfies UsernamePassword;
}
}
@@ -364,9 +392,9 @@ export function getCredentials(
}
// Parse and validate the credentials
let parsed: RawCredential[];
let parsed: unknown;
try {
parsed = JSON.parse(credentialsStr) as RawCredential[];
parsed = json.parseString(credentialsStr);
} catch {
// Don't log the error since it might contain sensitive information.
logger.error("Failed to parse the credentials data.");
@@ -374,7 +402,7 @@ export function getCredentials(
}
// Check that the parsed data is indeed an array.
if (!Array.isArray(parsed)) {
if (!json.isArray(parsed)) {
throw new ConfigurationError(
"Expected credentials data to be an array of configurations, but it is not.",
);
@@ -382,12 +410,12 @@ export function getCredentials(
const out: Credential[] = [];
for (const e of parsed) {
if (e === null || typeof e !== "object") {
if (e === null || !json.isObject<RawCredential>(e)) {
throw new ConfigurationError("Invalid credentials - must be an object");
}
// The configuration must have a type.
if (!isDefined(e.type)) {
if (!isDefined(e.type) || !json.isString(e.type)) {
throw new ConfigurationError("Invalid credentials - must have a type");
}
+40 -10
View File
@@ -1,3 +1,5 @@
import type { UnvalidatedObject } from "../json";
import * as json from "../json";
import { isDefined } from "../util";
/**
@@ -5,7 +7,7 @@ import { isDefined } from "../util";
* present or not. This type is used to represent such values, which we expect to be
* `Credential` values, but haven't validated yet.
*/
export type RawCredential = Partial<Credential>;
export type RawCredential = UnvalidatedObject<Credential>;
/** Usernames may be present for both authentication with tokens or passwords. */
export type Username = {
@@ -14,7 +16,7 @@ export type Username = {
};
/** Decides whether `config` has a username. */
export function hasUsername(config: Partial<AuthConfig>): config is Username {
export function hasUsername(config: AuthConfig): config is Username {
return "username" in config;
}
@@ -44,8 +46,10 @@ export type Token = {
} & Username;
/** Decides whether `config` is token-based. */
export function isToken(config: Partial<AuthConfig>): config is Token {
return "token" in config;
export function isToken(
config: UnvalidatedObject<AuthConfig>,
): config is Token {
return "token" in config && json.isStringOrUndefined(config.token);
}
/** Configuration for Azure OIDC. */
@@ -53,13 +57,15 @@ export type AzureConfig = { tenant_id: string; client_id: string };
/** Decides whether `config` is an Azure OIDC configuration. */
export function isAzureConfig(
config: Partial<AuthConfig>,
config: UnvalidatedObject<AuthConfig>,
): config is AzureConfig {
return (
"tenant_id" in config &&
"client_id" in config &&
isDefined(config.tenant_id) &&
isDefined(config.client_id)
isDefined(config.client_id) &&
json.isString(config.tenant_id) &&
json.isString(config.client_id)
);
}
@@ -74,7 +80,9 @@ export type AWSConfig = {
};
/** Decides whether `config` is an AWS OIDC configuration. */
export function isAWSConfig(config: Partial<AuthConfig>): config is AWSConfig {
export function isAWSConfig(
config: UnvalidatedObject<AuthConfig>,
): config is AWSConfig {
// All of these properties are required.
const requiredProperties = [
"aws_region",
@@ -85,10 +93,20 @@ export function isAWSConfig(config: Partial<AuthConfig>): config is AWSConfig {
];
for (const property of requiredProperties) {
if (!(property in config) || !isDefined(config[property])) {
if (
!(property in config) ||
!isDefined(config[property]) ||
!json.isString(config[property])
) {
return false;
}
}
// The "audience" field is optional, but should be a string if present.
if ("audience" in config && !json.isStringOrUndefined(config.audience)) {
return false;
}
return true;
}
@@ -101,11 +119,23 @@ export type JFrogConfig = {
/** Decides whether `config` is a JFrog OIDC configuration. */
export function isJFrogConfig(
config: Partial<AuthConfig>,
config: UnvalidatedObject<AuthConfig>,
): config is JFrogConfig {
// The "audience" and "identity_mapping_name" fields is optional, but should be strings if present.
if ("audience" in config && !json.isStringOrUndefined(config.audience)) {
return false;
}
if (
"identity_mapping_name" in config &&
!json.isStringOrUndefined(config.identity_mapping_name)
) {
return false;
}
return (
"jfrog_oidc_provider_name" in config &&
isDefined(config.jfrog_oidc_provider_name)
isDefined(config.jfrog_oidc_provider_name) &&
json.isString(config.jfrog_oidc_provider_name)
);
}