diff --git a/sdk/core/core-util/package.json b/sdk/core/core-util/package.json index fd7632d9db05..f769d9dccfce 100644 --- a/sdk/core/core-util/package.json +++ b/sdk/core/core-util/package.json @@ -76,7 +76,8 @@ }, "dependencies": { "@azure/abort-controller": "^2.0.0", - "tslib": "^2.6.2" + "tslib": "^2.6.2", + "@typespec/ts-http-runtime": "^0.1.1" }, "devDependencies": { "@azure-tools/vite-plugin-browser-test-map": "^1.0.0", diff --git a/sdk/core/core-util/src/aborterUtils.ts b/sdk/core/core-util/src/aborterUtils.ts deleted file mode 100644 index ce29be9a281c..000000000000 --- a/sdk/core/core-util/src/aborterUtils.ts +++ /dev/null @@ -1,47 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -import type { AbortSignalLike } from "@azure/abort-controller"; - -/** - * Options related to abort controller. - */ -export interface AbortOptions { - /** - * The abortSignal associated with containing operation. - */ - abortSignal?: AbortSignalLike; - /** - * The abort error message associated with containing operation. - */ - abortErrorMsg?: string; -} - -/** - * Represents a function that returns a promise that can be aborted. - */ -export type AbortablePromiseBuilder = (abortOptions: { - abortSignal?: AbortSignalLike; -}) => Promise; - -/** - * promise.race() wrapper that aborts rest of promises as soon as the first promise settles. - */ -export async function cancelablePromiseRace( - abortablePromiseBuilders: AbortablePromiseBuilder[], - options?: { abortSignal?: AbortSignalLike }, -): Promise { - const aborter = new AbortController(); - function abortHandler(): void { - aborter.abort(); - } - options?.abortSignal?.addEventListener("abort", abortHandler); - try { - return await Promise.race( - abortablePromiseBuilders.map((p) => p({ abortSignal: aborter.signal })), - ); - } finally { - aborter.abort(); - options?.abortSignal?.removeEventListener("abort", abortHandler); - } -} diff --git a/sdk/core/core-util/src/bytesEncoding-browser.mts b/sdk/core/core-util/src/bytesEncoding-browser.mts deleted file mode 100644 index b01f15e0ed57..000000000000 --- a/sdk/core/core-util/src/bytesEncoding-browser.mts +++ /dev/null @@ -1,4 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -export * from "./bytesEncoding.common.js"; diff --git a/sdk/core/core-util/src/bytesEncoding-react-native.mts b/sdk/core/core-util/src/bytesEncoding-react-native.mts deleted file mode 100644 index b01f15e0ed57..000000000000 --- a/sdk/core/core-util/src/bytesEncoding-react-native.mts +++ /dev/null @@ -1,4 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -export * from "./bytesEncoding.common.js"; diff --git a/sdk/core/core-util/src/bytesEncoding.common.ts b/sdk/core/core-util/src/bytesEncoding.common.ts deleted file mode 100644 index 2f8191a26c24..000000000000 --- a/sdk/core/core-util/src/bytesEncoding.common.ts +++ /dev/null @@ -1,131 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -declare global { - // stub these out for the browser - function btoa(input: string): string; - function atob(input: string): string; -} - -/** The supported character encoding type */ -export type EncodingType = "utf-8" | "base64" | "base64url" | "hex"; - -/** - * The helper that transforms bytes with specific character encoding into string - * @param bytes - the uint8array bytes - * @param format - the format we use to encode the byte - * @returns a string of the encoded string - */ -export function uint8ArrayToString(bytes: Uint8Array, format: EncodingType): string { - switch (format) { - case "utf-8": - return uint8ArrayToUtf8String(bytes); - case "base64": - return uint8ArrayToBase64(bytes); - case "base64url": - return uint8ArrayToBase64Url(bytes); - case "hex": - return uint8ArrayToHexString(bytes); - } -} - -/** - * The helper that transforms string to specific character encoded bytes array. - * @param value - the string to be converted - * @param format - the format we use to decode the value - * @returns a uint8array - */ -export function stringToUint8Array(value: string, format: EncodingType): Uint8Array { - switch (format) { - case "utf-8": - return utf8StringToUint8Array(value); - case "base64": - return base64ToUint8Array(value); - case "base64url": - return base64UrlToUint8Array(value); - case "hex": - return hexStringToUint8Array(value); - } -} - -/** - * Decodes a Uint8Array into a Base64 string. - * @internal - */ -export function uint8ArrayToBase64(bytes: Uint8Array): string { - return btoa([...bytes].map((x) => String.fromCharCode(x)).join("")); -} - -/** - * Decodes a Uint8Array into a Base64Url string. - * @internal - */ -export function uint8ArrayToBase64Url(bytes: Uint8Array): string { - return uint8ArrayToBase64(bytes).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, ""); -} - -/** - * Decodes a Uint8Array into a javascript string. - * @internal - */ -export function uint8ArrayToUtf8String(bytes: Uint8Array): string { - const decoder = new TextDecoder(); - const dataString = decoder.decode(bytes); - return dataString; -} - -/** - * Decodes a Uint8Array into a hex string - * @internal - */ -export function uint8ArrayToHexString(bytes: Uint8Array): string { - return [...bytes].map((x) => x.toString(16).padStart(2, "0")).join(""); -} - -/** - * Encodes a JavaScript string into a Uint8Array. - * @internal - */ -export function utf8StringToUint8Array(value: string): Uint8Array { - return new TextEncoder().encode(value); -} - -/** - * Encodes a Base64 string into a Uint8Array. - * @internal - */ -export function base64ToUint8Array(value: string): Uint8Array { - return new Uint8Array([...atob(value)].map((x) => x.charCodeAt(0))); -} - -/** - * Encodes a Base64Url string into a Uint8Array. - * @internal - */ -export function base64UrlToUint8Array(value: string): Uint8Array { - const base64String = value.replace(/-/g, "+").replace(/_/g, "/"); - return base64ToUint8Array(base64String); -} - -const hexDigits = new Set("0123456789abcdefABCDEF"); - -/** - * Encodes a hex string into a Uint8Array - * @internal - */ -export function hexStringToUint8Array(value: string): Uint8Array { - // If value has odd length, the last character will be ignored, consistent with NodeJS Buffer behavior - const bytes = new Uint8Array(value.length / 2); - for (let i = 0; i < value.length / 2; ++i) { - const highNibble = value[2 * i]; - const lowNibble = value[2 * i + 1]; - if (!hexDigits.has(highNibble) || !hexDigits.has(lowNibble)) { - // Replicate Node Buffer behavior by exiting early when we encounter an invalid byte - return bytes.slice(0, i); - } - - bytes[i] = parseInt(`${highNibble}${lowNibble}`, 16); - } - - return bytes; -} diff --git a/sdk/core/core-util/src/bytesEncoding.ts b/sdk/core/core-util/src/bytesEncoding.ts deleted file mode 100644 index b6507afc6e0e..000000000000 --- a/sdk/core/core-util/src/bytesEncoding.ts +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -/** The supported character encoding type */ -export type EncodingType = "utf-8" | "base64" | "base64url" | "hex"; - -/** - * The helper that transforms bytes with specific character encoding into string - * @param bytes - the uint8array bytes - * @param format - the format we use to encode the byte - * @returns a string of the encoded string - */ -export function uint8ArrayToString(bytes: Uint8Array, format: EncodingType): string { - return Buffer.from(bytes).toString(format); -} - -/** - * The helper that transforms string to specific character encoded bytes array. - * @param value - the string to be converted - * @param format - the format we use to decode the value - * @returns a uint8array - */ -export function stringToUint8Array(value: string, format: EncodingType): Uint8Array { - return Buffer.from(value, format); -} diff --git a/sdk/core/core-util/src/checkEnvironment.ts b/sdk/core/core-util/src/checkEnvironment.ts deleted file mode 100644 index 153edf626443..000000000000 --- a/sdk/core/core-util/src/checkEnvironment.ts +++ /dev/null @@ -1,90 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -interface Window { - document: unknown; -} - -interface DedicatedWorkerGlobalScope { - constructor: { - name: string; - }; - - importScripts: (...paths: string[]) => void; -} - -interface Navigator { - product: string; -} - -interface DenoGlobal { - version: { - deno: string; - }; -} - -interface BunGlobal { - version: string; -} - -// eslint-disable-next-line @azure/azure-sdk/ts-no-window -declare const window: Window; -declare const self: DedicatedWorkerGlobalScope; -declare const Deno: DenoGlobal; -declare const Bun: BunGlobal; -declare const navigator: Navigator; - -/** - * A constant that indicates whether the environment the code is running is a Web Browser. - */ -// eslint-disable-next-line @azure/azure-sdk/ts-no-window -export const isBrowser = typeof window !== "undefined" && typeof window.document !== "undefined"; - -/** - * A constant that indicates whether the environment the code is running is a Web Worker. - */ -export const isWebWorker = - typeof self === "object" && - typeof self?.importScripts === "function" && - (self.constructor?.name === "DedicatedWorkerGlobalScope" || - self.constructor?.name === "ServiceWorkerGlobalScope" || - self.constructor?.name === "SharedWorkerGlobalScope"); - -/** - * A constant that indicates whether the environment the code is running is Deno. - */ -export const isDeno = - typeof Deno !== "undefined" && - typeof Deno.version !== "undefined" && - typeof Deno.version.deno !== "undefined"; - -/** - * A constant that indicates whether the environment the code is running is Bun.sh. - */ -export const isBun = typeof Bun !== "undefined" && typeof Bun.version !== "undefined"; - -/** - * A constant that indicates whether the environment the code is running is a Node.js compatible environment. - */ -export const isNodeLike = - typeof globalThis.process !== "undefined" && - Boolean(globalThis.process.version) && - Boolean(globalThis.process.versions?.node); - -/** - * A constant that indicates whether the environment the code is running is a Node.js compatible environment. - * @deprecated Use `isNodeLike` instead. - */ -export const isNode = isNodeLike; - -/** - * A constant that indicates whether the environment the code is running is Node.JS. - */ -export const isNodeRuntime = isNodeLike && !isBun && !isDeno; - -/** - * A constant that indicates whether the environment the code is running is in React-Native. - */ -// https://github.com/facebook/react-native/blob/main/packages/react-native/Libraries/Core/setUpNavigator.js -export const isReactNative = - typeof navigator !== "undefined" && navigator?.product === "ReactNative"; diff --git a/sdk/core/core-util/src/createAbortablePromise.ts b/sdk/core/core-util/src/createAbortablePromise.ts deleted file mode 100644 index 25cf55a97968..000000000000 --- a/sdk/core/core-util/src/createAbortablePromise.ts +++ /dev/null @@ -1,60 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -import { AbortError } from "@azure/abort-controller"; -import type { AbortOptions } from "./aborterUtils.js"; - -/** - * Options for the createAbortablePromise function. - */ -export interface CreateAbortablePromiseOptions extends AbortOptions { - /** A function to be called if the promise was aborted */ - cleanupBeforeAbort?: () => void; -} - -/** - * Creates an abortable promise. - * @param buildPromise - A function that takes the resolve and reject functions as parameters. - * @param options - The options for the abortable promise. - * @returns A promise that can be aborted. - */ -export function createAbortablePromise( - buildPromise: ( - resolve: (value: T | PromiseLike) => void, - reject: (reason?: any) => void, - ) => void, - options?: CreateAbortablePromiseOptions, -): Promise { - const { cleanupBeforeAbort, abortSignal, abortErrorMsg } = options ?? {}; - return new Promise((resolve, reject) => { - function rejectOnAbort(): void { - reject(new AbortError(abortErrorMsg ?? "The operation was aborted.")); - } - function removeListeners(): void { - abortSignal?.removeEventListener("abort", onAbort); - } - function onAbort(): void { - cleanupBeforeAbort?.(); - removeListeners(); - rejectOnAbort(); - } - if (abortSignal?.aborted) { - return rejectOnAbort(); - } - try { - buildPromise( - (x) => { - removeListeners(); - resolve(x); - }, - (x) => { - removeListeners(); - reject(x); - }, - ); - } catch (err) { - reject(err); - } - abortSignal?.addEventListener("abort", onAbort); - }); -} diff --git a/sdk/core/core-util/src/delay.ts b/sdk/core/core-util/src/delay.ts deleted file mode 100644 index 0b2729233736..000000000000 --- a/sdk/core/core-util/src/delay.ts +++ /dev/null @@ -1,60 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -import type { AbortOptions } from "./aborterUtils.js"; -import { createAbortablePromise } from "./createAbortablePromise.js"; -import { getRandomIntegerInclusive } from "./random.js"; - -const StandardAbortMessage = "The delay was aborted."; - -/** - * Options for support abort functionality for the delay method - */ -export interface DelayOptions extends AbortOptions {} - -/** - * A wrapper for setTimeout that resolves a promise after timeInMs milliseconds. - * @param timeInMs - The number of milliseconds to be delayed. - * @param options - The options for delay - currently abort options - * @returns Promise that is resolved after timeInMs - */ -export function delay(timeInMs: number, options?: DelayOptions): Promise { - let token: ReturnType; - const { abortSignal, abortErrorMsg } = options ?? {}; - return createAbortablePromise( - (resolve) => { - token = setTimeout(resolve, timeInMs); - }, - { - cleanupBeforeAbort: () => clearTimeout(token), - abortSignal, - abortErrorMsg: abortErrorMsg ?? StandardAbortMessage, - }, - ); -} - -/** - * Calculates the delay interval for retry attempts using exponential delay with jitter. - * @param retryAttempt - The current retry attempt number. - * @param config - The exponential retry configuration. - * @returns An object containing the calculated retry delay. - */ -export function calculateRetryDelay( - retryAttempt: number, - config: { - retryDelayInMs: number; - maxRetryDelayInMs: number; - }, -): { retryAfterInMs: number } { - // Exponentially increase the delay each time - const exponentialDelay = config.retryDelayInMs * Math.pow(2, retryAttempt); - - // Don't let the delay exceed the maximum - const clampedDelay = Math.min(config.maxRetryDelayInMs, exponentialDelay); - - // Allow the final value to have some "jitter" (within 50% of the delay size) so - // that retries across multiple clients don't occur simultaneously. - const retryAfterInMs = clampedDelay / 2 + getRandomIntegerInclusive(0, clampedDelay / 2); - - return { retryAfterInMs }; -} diff --git a/sdk/core/core-util/src/error.ts b/sdk/core/core-util/src/error.ts deleted file mode 100644 index 92687a5df18a..000000000000 --- a/sdk/core/core-util/src/error.ts +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -import { isObject } from "./object.js"; - -/** - * Typeguard for an error object shape (has name and message) - * @param e - Something caught by a catch clause. - */ -export function isError(e: unknown): e is Error { - if (isObject(e)) { - const hasName = typeof e.name === "string"; - const hasMessage = typeof e.message === "string"; - return hasName && hasMessage; - } - return false; -} - -/** - * Given what is thought to be an error object, return the message if possible. - * If the message is missing, returns a stringified version of the input. - * @param e - Something thrown from a try block - * @returns The error message or a string of the input - */ -export function getErrorMessage(e: unknown): string { - if (isError(e)) { - return e.message; - } else { - let stringified: string; - try { - if (typeof e === "object" && e) { - stringified = JSON.stringify(e); - } else { - stringified = String(e); - } - } catch (err: any) { - stringified = "[unable to stringify input]"; - } - return `Unknown error ${stringified}`; - } -} diff --git a/sdk/core/core-util/src/httpMethods.ts b/sdk/core/core-util/src/httpMethods.ts deleted file mode 100644 index 274803141c7c..000000000000 --- a/sdk/core/core-util/src/httpMethods.ts +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -/** - * @public - * Supported HTTP methods to use when making requests. - */ -export type HttpMethods = - | "GET" - | "PUT" - | "POST" - | "DELETE" - | "PATCH" - | "HEAD" - | "OPTIONS" - | "TRACE"; diff --git a/sdk/core/core-util/src/index.ts b/sdk/core/core-util/src/index.ts index e0af55ffae6a..9befa9df15ca 100644 --- a/sdk/core/core-util/src/index.ts +++ b/sdk/core/core-util/src/index.ts @@ -1,31 +1,327 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -export { delay, type DelayOptions, calculateRetryDelay } from "./delay.js"; -export { - type AbortOptions, - cancelablePromiseRace, - type AbortablePromiseBuilder, -} from "./aborterUtils.js"; -export { - createAbortablePromise, - type CreateAbortablePromiseOptions, -} from "./createAbortablePromise.js"; -export { getRandomIntegerInclusive } from "./random.js"; -export { isObject, type UnknownObject } from "./object.js"; -export { isError, getErrorMessage } from "./error.js"; -export { computeSha256Hash, computeSha256Hmac } from "./sha256.js"; -export { isDefined, isObjectWithProperties, objectHasProperty } from "./typeGuards.js"; -export { randomUUID } from "./uuidUtils.js"; -export { HttpMethods } from "./httpMethods.js"; -export { - isBrowser, - isBun, - isNode, - isNodeLike, - isNodeRuntime, - isDeno, - isReactNative, - isWebWorker, -} from "./checkEnvironment.js"; -export { uint8ArrayToString, stringToUint8Array, type EncodingType } from "./bytesEncoding.js"; +import type { AbortSignalLike } from "@azure/abort-controller"; +import { + __calculateRetryDelay, + __cancelablePromiseRace, + __computeSha256Hash, + __computeSha256Hmac, + __createAbortablePromise, + __delay, + __getErrorMessage, + __getRandomIntegerInclusive, + __isBrowser, + __isBun, + __isDefined, + __isDeno, + __isError, + __isNodeLike, + __isNodeRuntime, + __isObject, + __isObjectWithProperties, + __isReactNative, + __isWebWorker, + __objectHasProperty, + __randomUUID, + __isNode, + __stringToUint8Array, + __uint8ArrayToString, +} from "@typespec/ts-http-runtime/__internal/util"; + +/** + * Calculates the delay interval for retry attempts using exponential delay with jitter. + * + * @param retryAttempt - The current retry attempt number. + * + * @param config - The exponential retry configuration. + * + * @returns An object containing the calculated retry delay. + */ +export function calculateRetryDelay( + retryAttempt: number, + config: { + retryDelayInMs: number; + maxRetryDelayInMs: number; + }, +): { + retryAfterInMs: number; +} { + return __calculateRetryDelay(retryAttempt, config); +} + +/** + * promise.race() wrapper that aborts rest of promises as soon as the first promise settles. + */ +export function cancelablePromiseRace( + abortablePromiseBuilders: AbortablePromiseBuilder[], + options?: { + abortSignal?: AbortSignalLike; + }, +): Promise { + return __cancelablePromiseRace(abortablePromiseBuilders, options); +} + +/** + * Generates a SHA-256 hash. + * + * @param content - The data to be included in the hash. + * + * @param encoding - The textual encoding to use for the returned hash. + */ +export function computeSha256Hash(content: string, encoding: "base64" | "hex"): Promise { + return __computeSha256Hash(content, encoding); +} + +/** + * Generates a SHA-256 HMAC signature. + * + * @param key - The HMAC key represented as a base64 string, used to generate the cryptographic HMAC hash. + * + * @param stringToSign - The data to be signed. + * + * @param encoding - The textual encoding to use for the returned HMAC digest. + */ +export function computeSha256Hmac( + key: string, + stringToSign: string, + encoding: "base64" | "hex", +): Promise { + return __computeSha256Hmac(key, stringToSign, encoding); +} + +/** + * Creates an abortable promise. + * + * @param buildPromise - A function that takes the resolve and reject functions as parameters. + * + * @param options - The options for the abortable promise. + * + * @returns A promise that can be aborted. + */ +export function createAbortablePromise( + buildPromise: ( + resolve: (value: T | PromiseLike) => void, + reject: (reason?: any) => void, + ) => void, + options?: CreateAbortablePromiseOptions, +): Promise { + return __createAbortablePromise(buildPromise, options); +} + +/** + * A wrapper for setTimeout that resolves a promise after timeInMs milliseconds. + * + * @param timeInMs - The number of milliseconds to be delayed. + * + * @param options - The options for delay - currently abort options + * + * @returns Promise that is resolved after timeInMs + */ +export function delay(timeInMs: number, options?: DelayOptions): Promise { + return __delay(timeInMs, options); +} + +/** + * Given what is thought to be an error object, return the message if possible. If the message is missing, returns a stringified version of the input. + * + * @param e - Something thrown from a try block + * + * @returns The error message or a string of the input + */ +export function getErrorMessage(e: unknown): string { + return __getErrorMessage(e); +} + +/** + * Returns a random integer value between a lower and upper bound, inclusive of both bounds. Note that this uses Math.random and isn't secure. If you need to use this for any kind of security purpose, find a better source of random. + * + * @param min - The smallest integer value allowed. + * + * @param max - The largest integer value allowed. + */ +export function getRandomIntegerInclusive(min: number, max: number): number { + return __getRandomIntegerInclusive(min, max); +} + +/** + * Helper TypeGuard that checks if something is defined or not. + * + * @param thing - Anything + */ +export function isDefined(thing: T | undefined | null): thing is T { + return __isDefined(thing); +} + +/** + * Typeguard for an error object shape (has name and message) + * + * @param e - Something caught by a catch clause. + */ +export function isError(e: unknown): e is Error { + return __isError(e); +} + +/** + * Helper to determine when an input is a generic JS object. + * + * @returns true when input is an object type that is not null, Array, RegExp, or Date. + */ +export function isObject(input: unknown): input is UnknownObject { + return __isObject(input); +} + +/** + * Helper TypeGuard that checks if the input is an object with the specified properties. + * + * @param thing - Anything. + * + * @param properties - The name of the properties that should appear in the object. + */ +export function isObjectWithProperties( + thing: Thing, + properties: PropertyName[], +): thing is Thing & Record { + return __isObjectWithProperties(thing, properties); +} + +/** + * Helper TypeGuard that checks if the input is an object with the specified property. + * + * @param thing - Any object. + * + * @param property - The name of the property that should appear in the object. + */ +export function objectHasProperty( + thing: Thing, + property: PropertyName, +): thing is Thing & Record { + return __objectHasProperty(thing, property); +} + +/** + * Generated Universally Unique Identifier + * + * @returns RFC4122 v4 UUID. + */ +export function randomUUID(): string { + return __randomUUID(); +} + +/** + * Options related to abort controller. + */ +export interface AbortOptions { + /** + * The abort error message associated with containing operation. + */ + abortErrorMsg?: string; + /** + * The abortSignal associated with containing operation. + */ + abortSignal?: AbortSignalLike; +} + +/** + * Options for the createAbortablePromise function. + */ +export interface CreateAbortablePromiseOptions extends AbortOptions { + /** + * A function to be called if the promise was aborted + */ + cleanupBeforeAbort?: () => void; +} + +/** + * Options for support abort functionality for the delay method + */ +export interface DelayOptions extends AbortOptions {} + +/** + * Represents a function that returns a promise that can be aborted. + */ +export type AbortablePromiseBuilder = (abortOptions: { + abortSignal?: AbortSignalLike; +}) => Promise; + +/** + * Supported HTTP methods to use when making requests. + * + * @public + */ +export type HttpMethods = + | "GET" + | "PUT" + | "POST" + | "DELETE" + | "PATCH" + | "HEAD" + | "OPTIONS" + | "TRACE"; + +/** + * A generic shape for a plain JS object. + */ +export type UnknownObject = { + [s: string]: unknown; +}; + +/** + * A constant that indicates whether the environment the code is running is a Web Browser. + */ +export const isBrowser: boolean = __isBrowser; +/** + * A constant that indicates whether the environment the code is running is Bun.sh. + */ +export const isBun: boolean = __isBun; +/** + * A constant that indicates whether the environment the code is running is Deno. + */ +export const isDeno: boolean = __isDeno; +/** + * A constant that indicates whether the environment the code is running is a Node.js compatible environment. + * + * @deprecated + * + * Use `isNodeLike` instead. + */ +export const isNode: boolean = __isNode; +/** + * A constant that indicates whether the environment the code is running is a Node.js compatible environment. + */ +export const isNodeLike: boolean = __isNodeLike; +/** + * A constant that indicates whether the environment the code is running is Node.JS. + */ +export const isNodeRuntime: boolean = __isNodeRuntime; +/** + * A constant that indicates whether the environment the code is running is in React-Native. + */ +export const isReactNative: boolean = __isReactNative; +/** + * A constant that indicates whether the environment the code is running is a Web Worker. + */ +export const isWebWorker: boolean = __isWebWorker; + +/** The supported character encoding type */ +export type EncodingType = "utf-8" | "base64" | "base64url" | "hex"; + +/** + * The helper that transforms bytes with specific character encoding into string + * @param bytes - the uint8array bytes + * @param format - the format we use to encode the byte + * @returns a string of the encoded string + */ +export function uint8ArrayToString(bytes: Uint8Array, format: EncodingType): string { + return __uint8ArrayToString(bytes, format); +} + +/** + * The helper that transforms string to specific character encoded bytes array. + * @param value - the string to be converted + * @param format - the format we use to decode the value + * @returns a uint8array + */ +export function stringToUint8Array(value: string, format: EncodingType): Uint8Array { + return __stringToUint8Array(value, format); +} diff --git a/sdk/core/core-util/src/object.ts b/sdk/core/core-util/src/object.ts deleted file mode 100644 index bbc8d696c423..000000000000 --- a/sdk/core/core-util/src/object.ts +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -/** - * A generic shape for a plain JS object. - */ -export type UnknownObject = { [s: string]: unknown }; - -/** - * Helper to determine when an input is a generic JS object. - * @returns true when input is an object type that is not null, Array, RegExp, or Date. - */ -export function isObject(input: unknown): input is UnknownObject { - return ( - typeof input === "object" && - input !== null && - !Array.isArray(input) && - !(input instanceof RegExp) && - !(input instanceof Date) - ); -} diff --git a/sdk/core/core-util/src/random.ts b/sdk/core/core-util/src/random.ts deleted file mode 100644 index 5130ca671851..000000000000 --- a/sdk/core/core-util/src/random.ts +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -/** - * Returns a random integer value between a lower and upper bound, - * inclusive of both bounds. - * Note that this uses Math.random and isn't secure. If you need to use - * this for any kind of security purpose, find a better source of random. - * @param min - The smallest integer value allowed. - * @param max - The largest integer value allowed. - */ -export function getRandomIntegerInclusive(min: number, max: number): number { - // Make sure inputs are integers. - min = Math.ceil(min); - max = Math.floor(max); - // Pick a random offset from zero to the size of the range. - // Since Math.random() can never return 1, we have to make the range one larger - // in order to be inclusive of the maximum value after we take the floor. - const offset = Math.floor(Math.random() * (max - min + 1)); - return offset + min; -} diff --git a/sdk/core/core-util/src/sha256-browser.mts b/sdk/core/core-util/src/sha256-browser.mts deleted file mode 100644 index b24713ee7b13..000000000000 --- a/sdk/core/core-util/src/sha256-browser.mts +++ /dev/null @@ -1,4 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -export * from "./sha256.common.js"; diff --git a/sdk/core/core-util/src/sha256-react-native.mts b/sdk/core/core-util/src/sha256-react-native.mts deleted file mode 100644 index b24713ee7b13..000000000000 --- a/sdk/core/core-util/src/sha256-react-native.mts +++ /dev/null @@ -1,4 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -export * from "./sha256.common.js"; diff --git a/sdk/core/core-util/src/sha256.common.ts b/sdk/core/core-util/src/sha256.common.ts deleted file mode 100644 index 38429e23a403..000000000000 --- a/sdk/core/core-util/src/sha256.common.ts +++ /dev/null @@ -1,118 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -import { stringToUint8Array, uint8ArrayToString } from "./bytesEncoding.js"; - -// stubs for browser self.crypto -interface JsonWebKey {} -interface CryptoKey {} -type KeyUsage = - | "decrypt" - | "deriveBits" - | "deriveKey" - | "encrypt" - | "sign" - | "unwrapKey" - | "verify" - | "wrapKey"; -interface Algorithm { - name: string; -} -interface SubtleCrypto { - importKey( - format: string, - keyData: JsonWebKey, - algorithm: HmacImportParams, - extractable: boolean, - usage: KeyUsage[], - ): Promise; - sign( - algorithm: HmacImportParams, - key: CryptoKey, - data: ArrayBufferView | ArrayBuffer, - ): Promise; - digest(algorithm: Algorithm, data: ArrayBufferView | ArrayBuffer): Promise; -} -interface Crypto { - readonly subtle: SubtleCrypto; - getRandomValues(array: T): T; -} -declare const self: { - crypto: Crypto; -}; -interface HmacImportParams { - name: string; - hash: Algorithm; - length?: number; -} - -let subtleCrypto: SubtleCrypto | undefined; - -/** - * Returns a cached reference to the Web API crypto.subtle object. - * @internal - */ -function getCrypto(): SubtleCrypto { - if (subtleCrypto) { - return subtleCrypto; - } - - if (!self.crypto || !self.crypto.subtle) { - throw new Error("Your browser environment does not support cryptography functions."); - } - - subtleCrypto = self.crypto.subtle; - return subtleCrypto; -} - -/** - * Generates a SHA-256 HMAC signature. - * @param key - The HMAC key represented as a base64 string, used to generate the cryptographic HMAC hash. - * @param stringToSign - The data to be signed. - * @param encoding - The textual encoding to use for the returned HMAC digest. - */ -export async function computeSha256Hmac( - key: string, - stringToSign: string, - encoding: "base64" | "hex", -): Promise { - const crypto = getCrypto(); - const keyBytes = stringToUint8Array(key, "base64"); - const stringToSignBytes = stringToUint8Array(stringToSign, "utf-8"); - - const cryptoKey = await crypto.importKey( - "raw", - keyBytes, - { - name: "HMAC", - hash: { name: "SHA-256" }, - }, - false, - ["sign"], - ); - const signature = await crypto.sign( - { - name: "HMAC", - hash: { name: "SHA-256" }, - }, - cryptoKey, - stringToSignBytes, - ); - - return uint8ArrayToString(new Uint8Array(signature), encoding); -} - -/** - * Generates a SHA-256 hash. - * @param content - The data to be included in the hash. - * @param encoding - The textual encoding to use for the returned hash. - */ -export async function computeSha256Hash( - content: string, - encoding: "base64" | "hex", -): Promise { - const contentBytes = stringToUint8Array(content, "utf-8"); - const digest = await getCrypto().digest({ name: "SHA-256" }, contentBytes); - - return uint8ArrayToString(new Uint8Array(digest), encoding); -} diff --git a/sdk/core/core-util/src/sha256.ts b/sdk/core/core-util/src/sha256.ts deleted file mode 100644 index 80a1a2324915..000000000000 --- a/sdk/core/core-util/src/sha256.ts +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -import { createHash, createHmac } from "crypto"; - -/** - * Generates a SHA-256 HMAC signature. - * @param key - The HMAC key represented as a base64 string, used to generate the cryptographic HMAC hash. - * @param stringToSign - The data to be signed. - * @param encoding - The textual encoding to use for the returned HMAC digest. - */ -export async function computeSha256Hmac( - key: string, - stringToSign: string, - encoding: "base64" | "hex", -): Promise { - const decodedKey = Buffer.from(key, "base64"); - - return createHmac("sha256", decodedKey).update(stringToSign).digest(encoding); -} - -/** - * Generates a SHA-256 hash. - * @param content - The data to be included in the hash. - * @param encoding - The textual encoding to use for the returned hash. - */ -export async function computeSha256Hash( - content: string, - encoding: "base64" | "hex", -): Promise { - return createHash("sha256").update(content).digest(encoding); -} diff --git a/sdk/core/core-util/src/typeGuards.ts b/sdk/core/core-util/src/typeGuards.ts deleted file mode 100644 index 22a9cac90a3f..000000000000 --- a/sdk/core/core-util/src/typeGuards.ts +++ /dev/null @@ -1,46 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -/** - * Helper TypeGuard that checks if something is defined or not. - * @param thing - Anything - */ -export function isDefined(thing: T | undefined | null): thing is T { - return typeof thing !== "undefined" && thing !== null; -} - -/** - * Helper TypeGuard that checks if the input is an object with the specified properties. - * @param thing - Anything. - * @param properties - The name of the properties that should appear in the object. - */ -export function isObjectWithProperties( - thing: Thing, - properties: PropertyName[], -): thing is Thing & Record { - if (!isDefined(thing) || typeof thing !== "object") { - return false; - } - - for (const property of properties) { - if (!objectHasProperty(thing, property)) { - return false; - } - } - - return true; -} - -/** - * Helper TypeGuard that checks if the input is an object with the specified property. - * @param thing - Any object. - * @param property - The name of the property that should appear in the object. - */ -export function objectHasProperty( - thing: Thing, - property: PropertyName, -): thing is Thing & Record { - return ( - isDefined(thing) && typeof thing === "object" && property in (thing as Record) - ); -} diff --git a/sdk/core/core-util/src/uuidUtils-browser.mts b/sdk/core/core-util/src/uuidUtils-browser.mts deleted file mode 100644 index 0e8d3c7bd08f..000000000000 --- a/sdk/core/core-util/src/uuidUtils-browser.mts +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -import { generateUUID } from "./uuidUtils.common.js"; - -interface Crypto { - randomUUID(): string; -} - -declare const globalThis: { - crypto: Crypto; -}; - -// NOTE: This could be undefined if not used in a secure context -const uuidFunction = - typeof globalThis?.crypto?.randomUUID === "function" - ? globalThis.crypto.randomUUID.bind(globalThis.crypto) - : generateUUID; - -/** - * Generated Universally Unique Identifier - * - * @returns RFC4122 v4 UUID. - */ -export function randomUUID(): string { - return uuidFunction(); -} diff --git a/sdk/core/core-util/src/uuidUtils-react-native.mts b/sdk/core/core-util/src/uuidUtils-react-native.mts deleted file mode 100644 index cc49ba6e6154..000000000000 --- a/sdk/core/core-util/src/uuidUtils-react-native.mts +++ /dev/null @@ -1,4 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -export { randomUUID } from "./uuidUtils.common.js"; diff --git a/sdk/core/core-util/src/uuidUtils.common.ts b/sdk/core/core-util/src/uuidUtils.common.ts deleted file mode 100644 index 1fa8b8fd23d2..000000000000 --- a/sdk/core/core-util/src/uuidUtils.common.ts +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -/** - * Generated Universally Unique Identifier - * - * @returns RFC4122 v4 UUID. - */ -export function generateUUID(): string { - let uuid = ""; - for (let i = 0; i < 32; i++) { - // Generate a random number between 0 and 15 - const randomNumber = Math.floor(Math.random() * 16); - // Set the UUID version to 4 in the 13th position - if (i === 12) { - uuid += "4"; - } else if (i === 16) { - // Set the UUID variant to "10" in the 17th position - uuid += (randomNumber & 0x3) | 0x8; - } else { - // Add a random hexadecimal digit to the UUID string - uuid += randomNumber.toString(16); - } - // Add hyphens to the UUID string at the appropriate positions - if (i === 7 || i === 11 || i === 15 || i === 19) { - uuid += "-"; - } - } - return uuid; -} - -/** - * Generated Universally Unique Identifier - * - * @returns RFC4122 v4 UUID. - */ -export function randomUUID(): string { - return generateUUID(); -} diff --git a/sdk/core/core-util/src/uuidUtils.ts b/sdk/core/core-util/src/uuidUtils.ts deleted file mode 100644 index bd11d6132235..000000000000 --- a/sdk/core/core-util/src/uuidUtils.ts +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -import { randomUUID as v4RandomUUID } from "crypto"; - -interface Crypto { - randomUUID(): string; -} - -declare const globalThis: { - crypto: Crypto; -}; - -// NOTE: This is a workaround until we can use `globalThis.crypto.randomUUID` in Node.js 19+. -const uuidFunction = - typeof globalThis?.crypto?.randomUUID === "function" - ? globalThis.crypto.randomUUID.bind(globalThis.crypto) - : v4RandomUUID; - -/** - * Generated Universally Unique Identifier - * - * @returns RFC4122 v4 UUID. - */ -export function randomUUID(): string { - return uuidFunction(); -} diff --git a/sdk/core/core-util/test/public/bytesEncoding.spec.ts b/sdk/core/core-util/test/public/bytesEncoding.spec.ts index e39ce27c8651..3caddd7a7fab 100644 --- a/sdk/core/core-util/test/public/bytesEncoding.spec.ts +++ b/sdk/core/core-util/test/public/bytesEncoding.spec.ts @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import { stringToUint8Array, uint8ArrayToString } from "../../src/bytesEncoding.js"; +import { stringToUint8Array, uint8ArrayToString } from "../../src/index.js"; import { describe, it, assert } from "vitest"; describe("bytesEncoding", function () { diff --git a/sdk/core/core-util/test/public/uuidUtils.spec.ts b/sdk/core/core-util/test/public/uuidUtils.spec.ts index 76078248df54..b3068a04401c 100644 --- a/sdk/core/core-util/test/public/uuidUtils.spec.ts +++ b/sdk/core/core-util/test/public/uuidUtils.spec.ts @@ -2,7 +2,7 @@ // Licensed under the MIT License. import { describe, it, assert } from "vitest"; -import { randomUUID } from "../../src/uuidUtils.js"; +import { randomUUID } from "../../src/index.js"; describe("randomUUID", function () { it("should be a valid v4 UUID", function () { diff --git a/sdk/core/ts-http-runtime/CHANGELOG.md b/sdk/core/ts-http-runtime/CHANGELOG.md index 07f636e62bf4..4a73e059c584 100644 --- a/sdk/core/ts-http-runtime/CHANGELOG.md +++ b/sdk/core/ts-http-runtime/CHANGELOG.md @@ -1,5 +1,15 @@ # Release History +## 0.1.1 (Unreleased) + +### Features Added + +### Breaking Changes + +### Bugs Fixed + +### Other Changes + ## 0.1.0 (2024-12-03) ### Features Added diff --git a/sdk/core/ts-http-runtime/package.json b/sdk/core/ts-http-runtime/package.json index 9037381a88cb..ec5636ddec43 100644 --- a/sdk/core/ts-http-runtime/package.json +++ b/sdk/core/ts-http-runtime/package.json @@ -1,6 +1,6 @@ { "name": "@typespec/ts-http-runtime", - "version": "0.1.0", + "version": "0.1.1", "description": "Isomorphic client library for making HTTP requests in node.js and browser.", "sdk-type": "client", "type": "module", @@ -27,6 +27,24 @@ "types": "./dist/commonjs/index.d.ts", "default": "./dist/commonjs/index.js" } + }, + "./__internal/util": { + "browser": { + "types": "./dist/browser/util/internal.d.ts", + "default": "./dist/browser/util/internal.js" + }, + "react-native": { + "types": "./dist/react-native/util/internal.d.ts", + "default": "./dist/react-native/util/internal.js" + }, + "import": { + "types": "./dist/esm/util/internal.d.ts", + "default": "./dist/esm/util/internal.js" + }, + "require": { + "types": "./dist/commonjs/util/internal.d.ts", + "default": "./dist/commonjs/util/internal.js" + } } }, "files": [ @@ -112,7 +130,8 @@ "tshy": { "exports": { "./package.json": "./package.json", - ".": "./src/index.ts" + ".": "./src/index.ts", + "./__internal/util": "./src/util/internal.ts" }, "dialects": [ "esm", diff --git a/sdk/core/ts-http-runtime/review/azure-core-comparison.diff b/sdk/core/ts-http-runtime/review/azure-core-comparison.diff index f0efc4da479b..ff25f53e5af1 100644 --- a/sdk/core/ts-http-runtime/review/azure-core-comparison.diff +++ b/sdk/core/ts-http-runtime/review/azure-core-comparison.diff @@ -575,16 +575,6 @@ index 026f516..5d588ed 100644 /** * Endpoint for the client */ -diff --git a/src/client/dom.d.ts b/src/client/dom.d.ts -deleted file mode 100644 -index eabe718..0000000 ---- a/src/client/dom.d.ts -+++ /dev/null -@@ -1,4 +0,0 @@ --// Copyright (c) Microsoft Corporation. --// Licensed under the MIT License. -- --/// diff --git a/src/client/getClient.ts b/src/client/getClient.ts index f6415d8..2e4f603 100644 --- a/src/client/getClient.ts @@ -2910,18 +2900,342 @@ index 84999ab..0000000 - namespace?: string; -} diff --git a/src/util/aborterUtils.ts b/src/util/aborterUtils.ts -index ce29be9..cde2d52 100644 ---- a/src/util/aborterUtils.ts +new file mode 100644 +index 0000000..cde2d52 +--- /dev/null +++ b/src/util/aborterUtils.ts -@@ -1,7 +1,7 @@ - // Copyright (c) Microsoft Corporation. - // Licensed under the MIT License. - --import type { AbortSignalLike } from "@azure/abort-controller"; +@@ -0,0 +1,47 @@ ++// Copyright (c) Microsoft Corporation. ++// Licensed under the MIT License. ++ +import type { AbortSignalLike } from "../abort-controller/AbortSignalLike.js"; - - /** - * Options related to abort controller. ++ ++/** ++ * Options related to abort controller. ++ */ ++export interface AbortOptions { ++ /** ++ * The abortSignal associated with containing operation. ++ */ ++ abortSignal?: AbortSignalLike; ++ /** ++ * The abort error message associated with containing operation. ++ */ ++ abortErrorMsg?: string; ++} ++ ++/** ++ * Represents a function that returns a promise that can be aborted. ++ */ ++export type AbortablePromiseBuilder = (abortOptions: { ++ abortSignal?: AbortSignalLike; ++}) => Promise; ++ ++/** ++ * promise.race() wrapper that aborts rest of promises as soon as the first promise settles. ++ */ ++export async function cancelablePromiseRace( ++ abortablePromiseBuilders: AbortablePromiseBuilder[], ++ options?: { abortSignal?: AbortSignalLike }, ++): Promise { ++ const aborter = new AbortController(); ++ function abortHandler(): void { ++ aborter.abort(); ++ } ++ options?.abortSignal?.addEventListener("abort", abortHandler); ++ try { ++ return await Promise.race( ++ abortablePromiseBuilders.map((p) => p({ abortSignal: aborter.signal })), ++ ); ++ } finally { ++ aborter.abort(); ++ options?.abortSignal?.removeEventListener("abort", abortHandler); ++ } ++} +diff --git a/src/util/bytesEncoding-browser.mts b/src/util/bytesEncoding-browser.mts +new file mode 100644 +index 0000000..b01f15e +--- /dev/null ++++ b/src/util/bytesEncoding-browser.mts +@@ -0,0 +1,4 @@ ++// Copyright (c) Microsoft Corporation. ++// Licensed under the MIT License. ++ ++export * from "./bytesEncoding.common.js"; +diff --git a/src/util/bytesEncoding-react-native.mts b/src/util/bytesEncoding-react-native.mts +new file mode 100644 +index 0000000..b01f15e +--- /dev/null ++++ b/src/util/bytesEncoding-react-native.mts +@@ -0,0 +1,4 @@ ++// Copyright (c) Microsoft Corporation. ++// Licensed under the MIT License. ++ ++export * from "./bytesEncoding.common.js"; +diff --git a/src/util/bytesEncoding.common.ts b/src/util/bytesEncoding.common.ts +new file mode 100644 +index 0000000..2f8191a +--- /dev/null ++++ b/src/util/bytesEncoding.common.ts +@@ -0,0 +1,131 @@ ++// Copyright (c) Microsoft Corporation. ++// Licensed under the MIT License. ++ ++declare global { ++ // stub these out for the browser ++ function btoa(input: string): string; ++ function atob(input: string): string; ++} ++ ++/** The supported character encoding type */ ++export type EncodingType = "utf-8" | "base64" | "base64url" | "hex"; ++ ++/** ++ * The helper that transforms bytes with specific character encoding into string ++ * @param bytes - the uint8array bytes ++ * @param format - the format we use to encode the byte ++ * @returns a string of the encoded string ++ */ ++export function uint8ArrayToString(bytes: Uint8Array, format: EncodingType): string { ++ switch (format) { ++ case "utf-8": ++ return uint8ArrayToUtf8String(bytes); ++ case "base64": ++ return uint8ArrayToBase64(bytes); ++ case "base64url": ++ return uint8ArrayToBase64Url(bytes); ++ case "hex": ++ return uint8ArrayToHexString(bytes); ++ } ++} ++ ++/** ++ * The helper that transforms string to specific character encoded bytes array. ++ * @param value - the string to be converted ++ * @param format - the format we use to decode the value ++ * @returns a uint8array ++ */ ++export function stringToUint8Array(value: string, format: EncodingType): Uint8Array { ++ switch (format) { ++ case "utf-8": ++ return utf8StringToUint8Array(value); ++ case "base64": ++ return base64ToUint8Array(value); ++ case "base64url": ++ return base64UrlToUint8Array(value); ++ case "hex": ++ return hexStringToUint8Array(value); ++ } ++} ++ ++/** ++ * Decodes a Uint8Array into a Base64 string. ++ * @internal ++ */ ++export function uint8ArrayToBase64(bytes: Uint8Array): string { ++ return btoa([...bytes].map((x) => String.fromCharCode(x)).join("")); ++} ++ ++/** ++ * Decodes a Uint8Array into a Base64Url string. ++ * @internal ++ */ ++export function uint8ArrayToBase64Url(bytes: Uint8Array): string { ++ return uint8ArrayToBase64(bytes).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, ""); ++} ++ ++/** ++ * Decodes a Uint8Array into a javascript string. ++ * @internal ++ */ ++export function uint8ArrayToUtf8String(bytes: Uint8Array): string { ++ const decoder = new TextDecoder(); ++ const dataString = decoder.decode(bytes); ++ return dataString; ++} ++ ++/** ++ * Decodes a Uint8Array into a hex string ++ * @internal ++ */ ++export function uint8ArrayToHexString(bytes: Uint8Array): string { ++ return [...bytes].map((x) => x.toString(16).padStart(2, "0")).join(""); ++} ++ ++/** ++ * Encodes a JavaScript string into a Uint8Array. ++ * @internal ++ */ ++export function utf8StringToUint8Array(value: string): Uint8Array { ++ return new TextEncoder().encode(value); ++} ++ ++/** ++ * Encodes a Base64 string into a Uint8Array. ++ * @internal ++ */ ++export function base64ToUint8Array(value: string): Uint8Array { ++ return new Uint8Array([...atob(value)].map((x) => x.charCodeAt(0))); ++} ++ ++/** ++ * Encodes a Base64Url string into a Uint8Array. ++ * @internal ++ */ ++export function base64UrlToUint8Array(value: string): Uint8Array { ++ const base64String = value.replace(/-/g, "+").replace(/_/g, "/"); ++ return base64ToUint8Array(base64String); ++} ++ ++const hexDigits = new Set("0123456789abcdefABCDEF"); ++ ++/** ++ * Encodes a hex string into a Uint8Array ++ * @internal ++ */ ++export function hexStringToUint8Array(value: string): Uint8Array { ++ // If value has odd length, the last character will be ignored, consistent with NodeJS Buffer behavior ++ const bytes = new Uint8Array(value.length / 2); ++ for (let i = 0; i < value.length / 2; ++i) { ++ const highNibble = value[2 * i]; ++ const lowNibble = value[2 * i + 1]; ++ if (!hexDigits.has(highNibble) || !hexDigits.has(lowNibble)) { ++ // Replicate Node Buffer behavior by exiting early when we encounter an invalid byte ++ return bytes.slice(0, i); ++ } ++ ++ bytes[i] = parseInt(`${highNibble}${lowNibble}`, 16); ++ } ++ ++ return bytes; ++} +diff --git a/src/util/bytesEncoding.ts b/src/util/bytesEncoding.ts +new file mode 100644 +index 0000000..b6507af +--- /dev/null ++++ b/src/util/bytesEncoding.ts +@@ -0,0 +1,25 @@ ++// Copyright (c) Microsoft Corporation. ++// Licensed under the MIT License. ++ ++/** The supported character encoding type */ ++export type EncodingType = "utf-8" | "base64" | "base64url" | "hex"; ++ ++/** ++ * The helper that transforms bytes with specific character encoding into string ++ * @param bytes - the uint8array bytes ++ * @param format - the format we use to encode the byte ++ * @returns a string of the encoded string ++ */ ++export function uint8ArrayToString(bytes: Uint8Array, format: EncodingType): string { ++ return Buffer.from(bytes).toString(format); ++} ++ ++/** ++ * The helper that transforms string to specific character encoded bytes array. ++ * @param value - the string to be converted ++ * @param format - the format we use to decode the value ++ * @returns a uint8array ++ */ ++export function stringToUint8Array(value: string, format: EncodingType): Uint8Array { ++ return Buffer.from(value, format); ++} +diff --git a/src/util/checkEnvironment.ts b/src/util/checkEnvironment.ts +new file mode 100644 +index 0000000..153edf6 +--- /dev/null ++++ b/src/util/checkEnvironment.ts +@@ -0,0 +1,90 @@ ++// Copyright (c) Microsoft Corporation. ++// Licensed under the MIT License. ++ ++interface Window { ++ document: unknown; ++} ++ ++interface DedicatedWorkerGlobalScope { ++ constructor: { ++ name: string; ++ }; ++ ++ importScripts: (...paths: string[]) => void; ++} ++ ++interface Navigator { ++ product: string; ++} ++ ++interface DenoGlobal { ++ version: { ++ deno: string; ++ }; ++} ++ ++interface BunGlobal { ++ version: string; ++} ++ ++// eslint-disable-next-line @azure/azure-sdk/ts-no-window ++declare const window: Window; ++declare const self: DedicatedWorkerGlobalScope; ++declare const Deno: DenoGlobal; ++declare const Bun: BunGlobal; ++declare const navigator: Navigator; ++ ++/** ++ * A constant that indicates whether the environment the code is running is a Web Browser. ++ */ ++// eslint-disable-next-line @azure/azure-sdk/ts-no-window ++export const isBrowser = typeof window !== "undefined" && typeof window.document !== "undefined"; ++ ++/** ++ * A constant that indicates whether the environment the code is running is a Web Worker. ++ */ ++export const isWebWorker = ++ typeof self === "object" && ++ typeof self?.importScripts === "function" && ++ (self.constructor?.name === "DedicatedWorkerGlobalScope" || ++ self.constructor?.name === "ServiceWorkerGlobalScope" || ++ self.constructor?.name === "SharedWorkerGlobalScope"); ++ ++/** ++ * A constant that indicates whether the environment the code is running is Deno. ++ */ ++export const isDeno = ++ typeof Deno !== "undefined" && ++ typeof Deno.version !== "undefined" && ++ typeof Deno.version.deno !== "undefined"; ++ ++/** ++ * A constant that indicates whether the environment the code is running is Bun.sh. ++ */ ++export const isBun = typeof Bun !== "undefined" && typeof Bun.version !== "undefined"; ++ ++/** ++ * A constant that indicates whether the environment the code is running is a Node.js compatible environment. ++ */ ++export const isNodeLike = ++ typeof globalThis.process !== "undefined" && ++ Boolean(globalThis.process.version) && ++ Boolean(globalThis.process.versions?.node); ++ ++/** ++ * A constant that indicates whether the environment the code is running is a Node.js compatible environment. ++ * @deprecated Use `isNodeLike` instead. ++ */ ++export const isNode = isNodeLike; ++ ++/** ++ * A constant that indicates whether the environment the code is running is Node.JS. ++ */ ++export const isNodeRuntime = isNodeLike && !isBun && !isDeno; ++ ++/** ++ * A constant that indicates whether the environment the code is running is in React-Native. ++ */ ++// https://github.com/facebook/react-native/blob/main/packages/react-native/Libraries/Core/setUpNavigator.js ++export const isReactNative = ++ typeof navigator !== "undefined" && navigator?.product === "ReactNative"; diff --git a/src/util/concat.ts b/src/util/concat.ts index 457bc22..cbadccf 100644 --- a/src/util/concat.ts @@ -2938,18 +3252,184 @@ index 457bc22..cbadccf 100644 import { getRawContent } from "./file.js"; diff --git a/src/util/createAbortablePromise.ts b/src/util/createAbortablePromise.ts -index 25cf55a..685eaed 100644 ---- a/src/util/createAbortablePromise.ts +new file mode 100644 +index 0000000..685eaed +--- /dev/null +++ b/src/util/createAbortablePromise.ts -@@ -1,7 +1,7 @@ - // Copyright (c) Microsoft Corporation. - // Licensed under the MIT License. - --import { AbortError } from "@azure/abort-controller"; +@@ -0,0 +1,60 @@ ++// Copyright (c) Microsoft Corporation. ++// Licensed under the MIT License. ++ +import { AbortError } from "../abort-controller/AbortError.js"; - import type { AbortOptions } from "./aborterUtils.js"; - - /** ++import type { AbortOptions } from "./aborterUtils.js"; ++ ++/** ++ * Options for the createAbortablePromise function. ++ */ ++export interface CreateAbortablePromiseOptions extends AbortOptions { ++ /** A function to be called if the promise was aborted */ ++ cleanupBeforeAbort?: () => void; ++} ++ ++/** ++ * Creates an abortable promise. ++ * @param buildPromise - A function that takes the resolve and reject functions as parameters. ++ * @param options - The options for the abortable promise. ++ * @returns A promise that can be aborted. ++ */ ++export function createAbortablePromise( ++ buildPromise: ( ++ resolve: (value: T | PromiseLike) => void, ++ reject: (reason?: any) => void, ++ ) => void, ++ options?: CreateAbortablePromiseOptions, ++): Promise { ++ const { cleanupBeforeAbort, abortSignal, abortErrorMsg } = options ?? {}; ++ return new Promise((resolve, reject) => { ++ function rejectOnAbort(): void { ++ reject(new AbortError(abortErrorMsg ?? "The operation was aborted.")); ++ } ++ function removeListeners(): void { ++ abortSignal?.removeEventListener("abort", onAbort); ++ } ++ function onAbort(): void { ++ cleanupBeforeAbort?.(); ++ removeListeners(); ++ rejectOnAbort(); ++ } ++ if (abortSignal?.aborted) { ++ return rejectOnAbort(); ++ } ++ try { ++ buildPromise( ++ (x) => { ++ removeListeners(); ++ resolve(x); ++ }, ++ (x) => { ++ removeListeners(); ++ reject(x); ++ }, ++ ); ++ } catch (err) { ++ reject(err); ++ } ++ abortSignal?.addEventListener("abort", onAbort); ++ }); ++} +diff --git a/src/util/delay.ts b/src/util/delay.ts +new file mode 100644 +index 0000000..0b27292 +--- /dev/null ++++ b/src/util/delay.ts +@@ -0,0 +1,60 @@ ++// Copyright (c) Microsoft Corporation. ++// Licensed under the MIT License. ++ ++import type { AbortOptions } from "./aborterUtils.js"; ++import { createAbortablePromise } from "./createAbortablePromise.js"; ++import { getRandomIntegerInclusive } from "./random.js"; ++ ++const StandardAbortMessage = "The delay was aborted."; ++ ++/** ++ * Options for support abort functionality for the delay method ++ */ ++export interface DelayOptions extends AbortOptions {} ++ ++/** ++ * A wrapper for setTimeout that resolves a promise after timeInMs milliseconds. ++ * @param timeInMs - The number of milliseconds to be delayed. ++ * @param options - The options for delay - currently abort options ++ * @returns Promise that is resolved after timeInMs ++ */ ++export function delay(timeInMs: number, options?: DelayOptions): Promise { ++ let token: ReturnType; ++ const { abortSignal, abortErrorMsg } = options ?? {}; ++ return createAbortablePromise( ++ (resolve) => { ++ token = setTimeout(resolve, timeInMs); ++ }, ++ { ++ cleanupBeforeAbort: () => clearTimeout(token), ++ abortSignal, ++ abortErrorMsg: abortErrorMsg ?? StandardAbortMessage, ++ }, ++ ); ++} ++ ++/** ++ * Calculates the delay interval for retry attempts using exponential delay with jitter. ++ * @param retryAttempt - The current retry attempt number. ++ * @param config - The exponential retry configuration. ++ * @returns An object containing the calculated retry delay. ++ */ ++export function calculateRetryDelay( ++ retryAttempt: number, ++ config: { ++ retryDelayInMs: number; ++ maxRetryDelayInMs: number; ++ }, ++): { retryAfterInMs: number } { ++ // Exponentially increase the delay each time ++ const exponentialDelay = config.retryDelayInMs * Math.pow(2, retryAttempt); ++ ++ // Don't let the delay exceed the maximum ++ const clampedDelay = Math.min(config.maxRetryDelayInMs, exponentialDelay); ++ ++ // Allow the final value to have some "jitter" (within 50% of the delay size) so ++ // that retries across multiple clients don't occur simultaneously. ++ const retryAfterInMs = clampedDelay / 2 + getRandomIntegerInclusive(0, clampedDelay / 2); ++ ++ return { retryAfterInMs }; ++} +diff --git a/src/util/error.ts b/src/util/error.ts +new file mode 100644 +index 0000000..92687a5 +--- /dev/null ++++ b/src/util/error.ts +@@ -0,0 +1,41 @@ ++// Copyright (c) Microsoft Corporation. ++// Licensed under the MIT License. ++ ++import { isObject } from "./object.js"; ++ ++/** ++ * Typeguard for an error object shape (has name and message) ++ * @param e - Something caught by a catch clause. ++ */ ++export function isError(e: unknown): e is Error { ++ if (isObject(e)) { ++ const hasName = typeof e.name === "string"; ++ const hasMessage = typeof e.message === "string"; ++ return hasName && hasMessage; ++ } ++ return false; ++} ++ ++/** ++ * Given what is thought to be an error object, return the message if possible. ++ * If the message is missing, returns a stringified version of the input. ++ * @param e - Something thrown from a try block ++ * @returns The error message or a string of the input ++ */ ++export function getErrorMessage(e: unknown): string { ++ if (isError(e)) { ++ return e.message; ++ } else { ++ let stringified: string; ++ try { ++ if (typeof e === "object" && e) { ++ stringified = JSON.stringify(e); ++ } else { ++ stringified = String(e); ++ } ++ } catch (err: any) { ++ stringified = "[unable to stringify input]"; ++ } ++ return `Unknown error ${stringified}`; ++ } ++} diff --git a/src/util/file.ts b/src/util/file.ts index 48d09e6..65c0e25 100644 --- a/src/util/file.ts @@ -2977,65 +3457,111 @@ index f6819e8..7272d5d 100644 import type { PipelineResponse } from "../interfaces.js"; const StandardAbortMessage = "The operation was aborted."; -diff --git a/src/util/httpMethods.ts b/src/util/httpMethods.ts -deleted file mode 100644 -index 2748031..0000000 ---- a/src/util/httpMethods.ts -+++ /dev/null -@@ -1,16 +0,0 @@ --// Copyright (c) Microsoft Corporation. --// Licensed under the MIT License. -- --/** -- * @public -- * Supported HTTP methods to use when making requests. -- */ --export type HttpMethods = -- | "GET" -- | "PUT" -- | "POST" -- | "DELETE" -- | "PATCH" -- | "HEAD" -- | "OPTIONS" -- | "TRACE"; -diff --git a/src/util/index.ts b/src/util/index.ts -deleted file mode 100644 -index e0af55f..0000000 ---- a/src/util/index.ts -+++ /dev/null -@@ -1,31 +0,0 @@ --// Copyright (c) Microsoft Corporation. --// Licensed under the MIT License. -- --export { delay, type DelayOptions, calculateRetryDelay } from "./delay.js"; --export { -- type AbortOptions, -- cancelablePromiseRace, -- type AbortablePromiseBuilder, --} from "./aborterUtils.js"; --export { -- createAbortablePromise, -- type CreateAbortablePromiseOptions, --} from "./createAbortablePromise.js"; --export { getRandomIntegerInclusive } from "./random.js"; --export { isObject, type UnknownObject } from "./object.js"; --export { isError, getErrorMessage } from "./error.js"; --export { computeSha256Hash, computeSha256Hmac } from "./sha256.js"; --export { isDefined, isObjectWithProperties, objectHasProperty } from "./typeGuards.js"; --export { randomUUID } from "./uuidUtils.js"; --export { HttpMethods } from "./httpMethods.js"; --export { -- isBrowser, -- isBun, -- isNode, -- isNodeLike, -- isNodeRuntime, -- isDeno, -- isReactNative, -- isWebWorker, --} from "./checkEnvironment.js"; --export { uint8ArrayToString, stringToUint8Array, type EncodingType } from "./bytesEncoding.js"; +diff --git a/src/util/internal.ts b/src/util/internal.ts +new file mode 100644 +index 0000000..6ea819a +--- /dev/null ++++ b/src/util/internal.ts +@@ -0,0 +1,45 @@ ++// Copyright (c) Microsoft Corporation. ++// Licensed under the MIT License. ++ ++export { ++ delay as __delay, ++ type DelayOptions as __DelayOptions, ++ calculateRetryDelay as __calculateRetryDelay, ++} from "./delay.js"; ++export { ++ type AbortOptions as __AbortOptions, ++ cancelablePromiseRace as __cancelablePromiseRace, ++ type AbortablePromiseBuilder as __AbortablePromiseBuilder, ++} from "./aborterUtils.js"; ++export { ++ createAbortablePromise as __createAbortablePromise, ++ type CreateAbortablePromiseOptions as __CreateAbortablePromiseOptions, ++} from "./createAbortablePromise.js"; ++export { getRandomIntegerInclusive as __getRandomIntegerInclusive } from "./random.js"; ++export { isObject as __isObject, type UnknownObject as __UnknownObject } from "./object.js"; ++export { isError as __isError, getErrorMessage as __getErrorMessage } from "./error.js"; ++export { ++ computeSha256Hash as __computeSha256Hash, ++ computeSha256Hmac as __computeSha256Hmac, ++} from "./sha256.js"; ++export { ++ isDefined as __isDefined, ++ isObjectWithProperties as __isObjectWithProperties, ++ objectHasProperty as __objectHasProperty, ++} from "./typeGuards.js"; ++export { randomUUID as __randomUUID } from "./uuidUtils.js"; ++export { ++ isBrowser as __isBrowser, ++ isBun as __isBun, ++ isNode as __isNode, ++ isNodeLike as __isNodeLike, ++ isNodeRuntime as __isNodeRuntime, ++ isDeno as __isDeno, ++ isReactNative as __isReactNative, ++ isWebWorker as __isWebWorker, ++} from "./checkEnvironment.js"; ++export { ++ stringToUint8Array as __stringToUint8Array, ++ uint8ArrayToString as __uint8ArrayToString, ++ type EncodingType as __EncodingType, ++} from "./bytesEncoding.js"; +diff --git a/src/util/object.ts b/src/util/object.ts +new file mode 100644 +index 0000000..bbc8d69 +--- /dev/null ++++ b/src/util/object.ts +@@ -0,0 +1,21 @@ ++// Copyright (c) Microsoft Corporation. ++// Licensed under the MIT License. ++ ++/** ++ * A generic shape for a plain JS object. ++ */ ++export type UnknownObject = { [s: string]: unknown }; ++ ++/** ++ * Helper to determine when an input is a generic JS object. ++ * @returns true when input is an object type that is not null, Array, RegExp, or Date. ++ */ ++export function isObject(input: unknown): input is UnknownObject { ++ return ( ++ typeof input === "object" && ++ input !== null && ++ !Array.isArray(input) && ++ !(input instanceof RegExp) && ++ !(input instanceof Date) ++ ); ++} +diff --git a/src/util/random.ts b/src/util/random.ts +new file mode 100644 +index 0000000..5130ca6 +--- /dev/null ++++ b/src/util/random.ts +@@ -0,0 +1,21 @@ ++// Copyright (c) Microsoft Corporation. ++// Licensed under the MIT License. ++ ++/** ++ * Returns a random integer value between a lower and upper bound, ++ * inclusive of both bounds. ++ * Note that this uses Math.random and isn't secure. If you need to use ++ * this for any kind of security purpose, find a better source of random. ++ * @param min - The smallest integer value allowed. ++ * @param max - The largest integer value allowed. ++ */ ++export function getRandomIntegerInclusive(min: number, max: number): number { ++ // Make sure inputs are integers. ++ min = Math.ceil(min); ++ max = Math.floor(max); ++ // Pick a random offset from zero to the size of the range. ++ // Since Math.random() can never return 1, we have to make the range one larger ++ // in order to be inclusive of the maximum value after we take the floor. ++ const offset = Math.floor(Math.random() * (max - min + 1)); ++ return offset + min; ++} diff --git a/src/util/sanitizer.ts b/src/util/sanitizer.ts index 46654b9..be4f21e 100644 --- a/src/util/sanitizer.ts @@ -3049,19 +3575,191 @@ index 46654b9..be4f21e 100644 /** * @internal -diff --git a/src/util/sha256.ts b/src/util/sha256.ts -index 80a1a23..794d26a 100644 ---- a/src/util/sha256.ts -+++ b/src/util/sha256.ts -@@ -1,7 +1,7 @@ +diff --git a/src/client/dom.d.ts b/src/util/sha256-browser.mts +similarity index 67% +rename from src/client/dom.d.ts +rename to src/util/sha256-browser.mts +index eabe718..b24713e 100644 +--- a/src/client/dom.d.ts ++++ b/src/util/sha256-browser.mts +@@ -1,4 +1,4 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. --import { createHash, createHmac } from "crypto"; +-/// ++export * from "./sha256.common.js"; +diff --git a/src/util/sha256-react-native.mts b/src/util/sha256-react-native.mts +new file mode 100644 +index 0000000..b24713e +--- /dev/null ++++ b/src/util/sha256-react-native.mts +@@ -0,0 +1,4 @@ ++// Copyright (c) Microsoft Corporation. ++// Licensed under the MIT License. ++ ++export * from "./sha256.common.js"; +diff --git a/src/util/sha256.common.ts b/src/util/sha256.common.ts +new file mode 100644 +index 0000000..38429e2 +--- /dev/null ++++ b/src/util/sha256.common.ts +@@ -0,0 +1,118 @@ ++// Copyright (c) Microsoft Corporation. ++// Licensed under the MIT License. ++ ++import { stringToUint8Array, uint8ArrayToString } from "./bytesEncoding.js"; ++ ++// stubs for browser self.crypto ++interface JsonWebKey {} ++interface CryptoKey {} ++type KeyUsage = ++ | "decrypt" ++ | "deriveBits" ++ | "deriveKey" ++ | "encrypt" ++ | "sign" ++ | "unwrapKey" ++ | "verify" ++ | "wrapKey"; ++interface Algorithm { ++ name: string; ++} ++interface SubtleCrypto { ++ importKey( ++ format: string, ++ keyData: JsonWebKey, ++ algorithm: HmacImportParams, ++ extractable: boolean, ++ usage: KeyUsage[], ++ ): Promise; ++ sign( ++ algorithm: HmacImportParams, ++ key: CryptoKey, ++ data: ArrayBufferView | ArrayBuffer, ++ ): Promise; ++ digest(algorithm: Algorithm, data: ArrayBufferView | ArrayBuffer): Promise; ++} ++interface Crypto { ++ readonly subtle: SubtleCrypto; ++ getRandomValues(array: T): T; ++} ++declare const self: { ++ crypto: Crypto; ++}; ++interface HmacImportParams { ++ name: string; ++ hash: Algorithm; ++ length?: number; ++} ++ ++let subtleCrypto: SubtleCrypto | undefined; ++ ++/** ++ * Returns a cached reference to the Web API crypto.subtle object. ++ * @internal ++ */ ++function getCrypto(): SubtleCrypto { ++ if (subtleCrypto) { ++ return subtleCrypto; ++ } ++ ++ if (!self.crypto || !self.crypto.subtle) { ++ throw new Error("Your browser environment does not support cryptography functions."); ++ } ++ ++ subtleCrypto = self.crypto.subtle; ++ return subtleCrypto; ++} ++ ++/** ++ * Generates a SHA-256 HMAC signature. ++ * @param key - The HMAC key represented as a base64 string, used to generate the cryptographic HMAC hash. ++ * @param stringToSign - The data to be signed. ++ * @param encoding - The textual encoding to use for the returned HMAC digest. ++ */ ++export async function computeSha256Hmac( ++ key: string, ++ stringToSign: string, ++ encoding: "base64" | "hex", ++): Promise { ++ const crypto = getCrypto(); ++ const keyBytes = stringToUint8Array(key, "base64"); ++ const stringToSignBytes = stringToUint8Array(stringToSign, "utf-8"); ++ ++ const cryptoKey = await crypto.importKey( ++ "raw", ++ keyBytes, ++ { ++ name: "HMAC", ++ hash: { name: "SHA-256" }, ++ }, ++ false, ++ ["sign"], ++ ); ++ const signature = await crypto.sign( ++ { ++ name: "HMAC", ++ hash: { name: "SHA-256" }, ++ }, ++ cryptoKey, ++ stringToSignBytes, ++ ); ++ ++ return uint8ArrayToString(new Uint8Array(signature), encoding); ++} ++ ++/** ++ * Generates a SHA-256 hash. ++ * @param content - The data to be included in the hash. ++ * @param encoding - The textual encoding to use for the returned hash. ++ */ ++export async function computeSha256Hash( ++ content: string, ++ encoding: "base64" | "hex", ++): Promise { ++ const contentBytes = stringToUint8Array(content, "utf-8"); ++ const digest = await getCrypto().digest({ name: "SHA-256" }, contentBytes); ++ ++ return uint8ArrayToString(new Uint8Array(digest), encoding); ++} +diff --git a/src/util/sha256.ts b/src/util/sha256.ts +new file mode 100644 +index 0000000..794d26a +--- /dev/null ++++ b/src/util/sha256.ts +@@ -0,0 +1,32 @@ ++// Copyright (c) Microsoft Corporation. ++// Licensed under the MIT License. ++ +import { createHash, createHmac } from "node:crypto"; - - /** - * Generates a SHA-256 HMAC signature. ++ ++/** ++ * Generates a SHA-256 HMAC signature. ++ * @param key - The HMAC key represented as a base64 string, used to generate the cryptographic HMAC hash. ++ * @param stringToSign - The data to be signed. ++ * @param encoding - The textual encoding to use for the returned HMAC digest. ++ */ ++export async function computeSha256Hmac( ++ key: string, ++ stringToSign: string, ++ encoding: "base64" | "hex", ++): Promise { ++ const decodedKey = Buffer.from(key, "base64"); ++ ++ return createHmac("sha256", decodedKey).update(stringToSign).digest(encoding); ++} ++ ++/** ++ * Generates a SHA-256 hash. ++ * @param content - The data to be included in the hash. ++ * @param encoding - The textual encoding to use for the returned hash. ++ */ ++export async function computeSha256Hash( ++ content: string, ++ encoding: "base64" | "hex", ++): Promise { ++ return createHash("sha256").update(content).digest(encoding); ++} diff --git a/src/util/tokenCycler.ts b/src/util/tokenCycler.ts index 32e2343..723a36d 100644 --- a/src/util/tokenCycler.ts @@ -3076,26 +3774,64 @@ index 32e2343..723a36d 100644 /** diff --git a/src/util/typeGuards.ts b/src/util/typeGuards.ts -index 22a9cac..2b3f295 100644 +index 99d3fe0..2b3f295 100644 --- a/src/util/typeGuards.ts +++ b/src/util/typeGuards.ts -@@ -44,3 +44,41 @@ export function objectHasProperty( - isDefined(thing) && typeof thing === "object" && property in (thing as Record) - ); - } +@@ -1,6 +1,50 @@ + // Copyright (c) Microsoft Corporation. + // Licensed under the MIT License. + ++/** ++ * Helper TypeGuard that checks if something is defined or not. ++ * @param thing - Anything ++ */ ++export function isDefined(thing: T | undefined | null): thing is T { ++ return typeof thing !== "undefined" && thing !== null; ++} + -+export function isNodeReadableStream(x: unknown): x is NodeJS.ReadableStream { -+ return Boolean(x && typeof (x as NodeJS.ReadableStream)["pipe"] === "function"); ++/** ++ * Helper TypeGuard that checks if the input is an object with the specified properties. ++ * @param thing - Anything. ++ * @param properties - The name of the properties that should appear in the object. ++ */ ++export function isObjectWithProperties( ++ thing: Thing, ++ properties: PropertyName[], ++): thing is Thing & Record { ++ if (!isDefined(thing) || typeof thing !== "object") { ++ return false; ++ } ++ ++ for (const property of properties) { ++ if (!objectHasProperty(thing, property)) { ++ return false; ++ } ++ } ++ ++ return true; +} + -+export function isWebReadableStream(x: unknown): x is ReadableStream { -+ return Boolean( -+ x && -+ typeof (x as ReadableStream).getReader === "function" && -+ typeof (x as ReadableStream).tee === "function", ++/** ++ * Helper TypeGuard that checks if the input is an object with the specified property. ++ * @param thing - Any object. ++ * @param property - The name of the property that should appear in the object. ++ */ ++export function objectHasProperty( ++ thing: Thing, ++ property: PropertyName, ++): thing is Thing & Record { ++ return ( ++ isDefined(thing) && typeof thing === "object" && property in (thing as Record) + ); +} + + export function isNodeReadableStream(x: unknown): x is NodeJS.ReadableStream { + return Boolean(x && typeof (x as NodeJS.ReadableStream)["pipe"] === "function"); + } +@@ -13,6 +57,24 @@ export function isWebReadableStream(x: unknown): x is ReadableStream { + ); + } + +export function isBinaryBody( + body: unknown, +): body is @@ -3114,13 +3850,9 @@ index 22a9cac..2b3f295 100644 + ); +} + -+export function isReadableStream(x: unknown): x is ReadableStream | NodeJS.ReadableStream { -+ return isNodeReadableStream(x) || isWebReadableStream(x); -+} -+ -+export function isBlob(x: unknown): x is Blob { -+ return typeof (x as Blob).stream === "function"; -+} + export function isReadableStream(x: unknown): x is ReadableStream | NodeJS.ReadableStream { + return isNodeReadableStream(x) || isWebReadableStream(x); + } diff --git a/src/util/userAgent.ts b/src/util/userAgent.ts index b8950b5..3ef33c8 100644 --- a/src/util/userAgent.ts @@ -3134,19 +3866,127 @@ index b8950b5..3ef33c8 100644 await setPlatformSpecificData(runtimeInfo); const defaultAgent = getUserAgentString(runtimeInfo); const userAgentValue = prefix ? `${prefix} ${defaultAgent}` : defaultAgent; +diff --git a/src/util/uuidUtils-browser.mts b/src/util/uuidUtils-browser.mts +new file mode 100644 +index 0000000..0e8d3c7 +--- /dev/null ++++ b/src/util/uuidUtils-browser.mts +@@ -0,0 +1,27 @@ ++// Copyright (c) Microsoft Corporation. ++// Licensed under the MIT License. ++ ++import { generateUUID } from "./uuidUtils.common.js"; ++ ++interface Crypto { ++ randomUUID(): string; ++} ++ ++declare const globalThis: { ++ crypto: Crypto; ++}; ++ ++// NOTE: This could be undefined if not used in a secure context ++const uuidFunction = ++ typeof globalThis?.crypto?.randomUUID === "function" ++ ? globalThis.crypto.randomUUID.bind(globalThis.crypto) ++ : generateUUID; ++ ++/** ++ * Generated Universally Unique Identifier ++ * ++ * @returns RFC4122 v4 UUID. ++ */ ++export function randomUUID(): string { ++ return uuidFunction(); ++} +diff --git a/src/util/uuidUtils-react-native.mts b/src/util/uuidUtils-react-native.mts +new file mode 100644 +index 0000000..cc49ba6 +--- /dev/null ++++ b/src/util/uuidUtils-react-native.mts +@@ -0,0 +1,4 @@ ++// Copyright (c) Microsoft Corporation. ++// Licensed under the MIT License. ++ ++export { randomUUID } from "./uuidUtils.common.js"; +diff --git a/src/util/uuidUtils.common.ts b/src/util/uuidUtils.common.ts +new file mode 100644 +index 0000000..1fa8b8f +--- /dev/null ++++ b/src/util/uuidUtils.common.ts +@@ -0,0 +1,39 @@ ++// Copyright (c) Microsoft Corporation. ++// Licensed under the MIT License. ++ ++/** ++ * Generated Universally Unique Identifier ++ * ++ * @returns RFC4122 v4 UUID. ++ */ ++export function generateUUID(): string { ++ let uuid = ""; ++ for (let i = 0; i < 32; i++) { ++ // Generate a random number between 0 and 15 ++ const randomNumber = Math.floor(Math.random() * 16); ++ // Set the UUID version to 4 in the 13th position ++ if (i === 12) { ++ uuid += "4"; ++ } else if (i === 16) { ++ // Set the UUID variant to "10" in the 17th position ++ uuid += (randomNumber & 0x3) | 0x8; ++ } else { ++ // Add a random hexadecimal digit to the UUID string ++ uuid += randomNumber.toString(16); ++ } ++ // Add hyphens to the UUID string at the appropriate positions ++ if (i === 7 || i === 11 || i === 15 || i === 19) { ++ uuid += "-"; ++ } ++ } ++ return uuid; ++} ++ ++/** ++ * Generated Universally Unique Identifier ++ * ++ * @returns RFC4122 v4 UUID. ++ */ ++export function randomUUID(): string { ++ return generateUUID(); ++} diff --git a/src/util/uuidUtils.ts b/src/util/uuidUtils.ts -index bd11d61..3f6a12b 100644 ---- a/src/util/uuidUtils.ts +new file mode 100644 +index 0000000..3f6a12b +--- /dev/null +++ b/src/util/uuidUtils.ts -@@ -1,7 +1,7 @@ - // Copyright (c) Microsoft Corporation. - // Licensed under the MIT License. - --import { randomUUID as v4RandomUUID } from "crypto"; +@@ -0,0 +1,27 @@ ++// Copyright (c) Microsoft Corporation. ++// Licensed under the MIT License. ++ +import { randomUUID as v4RandomUUID } from "node:crypto"; - - interface Crypto { - randomUUID(): string; ++ ++interface Crypto { ++ randomUUID(): string; ++} ++ ++declare const globalThis: { ++ crypto: Crypto; ++}; ++ ++// NOTE: This is a workaround until we can use `globalThis.crypto.randomUUID` in Node.js 19+. ++const uuidFunction = ++ typeof globalThis?.crypto?.randomUUID === "function" ++ ? globalThis.crypto.randomUUID.bind(globalThis.crypto) ++ : v4RandomUUID; ++ ++/** ++ * Generated Universally Unique Identifier ++ * ++ * @returns RFC4122 v4 UUID. ++ */ ++export function randomUUID(): string { ++ return uuidFunction(); ++} diff --git a/src/xhrHttpClient.ts b/src/xhrHttpClient.ts index 71bc439..c82fcb7 100644 --- a/src/xhrHttpClient.ts diff --git a/sdk/core/ts-http-runtime/review/ts-http-runtime-__internal-util.api.md b/sdk/core/ts-http-runtime/review/ts-http-runtime-__internal-util.api.md new file mode 100644 index 000000000000..2ccda4702989 --- /dev/null +++ b/sdk/core/ts-http-runtime/review/ts-http-runtime-__internal-util.api.md @@ -0,0 +1,116 @@ +## API Report File for "@typespec/ts-http-runtime" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +// @public +export type __AbortablePromiseBuilder = (abortOptions: { + abortSignal?: AbortSignalLike; +}) => Promise; + +// @public +export interface __AbortOptions { + abortErrorMsg?: string; + abortSignal?: AbortSignalLike; +} + +// @public +export function __calculateRetryDelay(retryAttempt: number, config: { + retryDelayInMs: number; + maxRetryDelayInMs: number; +}): { + retryAfterInMs: number; +}; + +// @public +export function __cancelablePromiseRace(abortablePromiseBuilders: __AbortablePromiseBuilder[], options?: { + abortSignal?: AbortSignalLike; +}): Promise; + +// @public +export function __computeSha256Hash(content: string, encoding: "base64" | "hex"): Promise; + +// @public +export function __computeSha256Hmac(key: string, stringToSign: string, encoding: "base64" | "hex"): Promise; + +// @public +export function __createAbortablePromise(buildPromise: (resolve: (value: T | PromiseLike) => void, reject: (reason?: any) => void) => void, options?: __CreateAbortablePromiseOptions): Promise; + +// @public +export interface __CreateAbortablePromiseOptions extends __AbortOptions { + cleanupBeforeAbort?: () => void; +} + +// @public +export function __delay(timeInMs: number, options?: __DelayOptions): Promise; + +// @public +export interface __DelayOptions extends __AbortOptions { +} + +// @public +export type __EncodingType = "utf-8" | "base64" | "base64url" | "hex"; + +// @public +export function __getErrorMessage(e: unknown): string; + +// @public +export function __getRandomIntegerInclusive(min: number, max: number): number; + +// @public +export const __isBrowser: boolean; + +// @public +export const __isBun: boolean; + +// @public +export function __isDefined(thing: T | undefined | null): thing is T; + +// @public +export const __isDeno: boolean; + +// @public +export function __isError(e: unknown): e is Error; + +// @public @deprecated +export const __isNode: boolean; + +// @public +export const __isNodeLike: boolean; + +// @public +export const __isNodeRuntime: boolean; + +// @public +export function __isObject(input: unknown): input is __UnknownObject; + +// @public +export function __isObjectWithProperties(thing: Thing, properties: PropertyName[]): thing is Thing & Record; + +// @public +export const __isReactNative: boolean; + +// @public +export const __isWebWorker: boolean; + +// @public +export function __objectHasProperty(thing: Thing, property: PropertyName): thing is Thing & Record; + +// @public +export function __randomUUID(): string; + +// @public +export function __stringToUint8Array(value: string, format: __EncodingType): Uint8Array; + +// @public +export function __uint8ArrayToString(bytes: Uint8Array, format: __EncodingType): string; + +// @public +export type __UnknownObject = { + [s: string]: unknown; +}; + +// (No @packageDocumentation comment for this package) + +``` diff --git a/sdk/core/ts-http-runtime/scripts/azure-diff.ts b/sdk/core/ts-http-runtime/scripts/azure-diff.ts index 2101e94da22b..6c5f463112eb 100644 --- a/sdk/core/ts-http-runtime/scripts/azure-diff.ts +++ b/sdk/core/ts-http-runtime/scripts/azure-diff.ts @@ -11,7 +11,6 @@ const AZURE_SOURCES = { // core-rest-pipeline is placed at the root of the unbranded package; this also covers subfolders of // core-rest-pipeline including policies/ and retryStrategies/. "../core-rest-pipeline/src/": "./src/", - "../core-util/src/": "./src/util/", "../core-auth/src/": "./src/auth/", "../abort-controller/src/": "./src/abort-controller/", "../core-client-rest/src/": "./src/client/", diff --git a/sdk/core/ts-http-runtime/src/util/internal.ts b/sdk/core/ts-http-runtime/src/util/internal.ts new file mode 100644 index 000000000000..6ea819a9c979 --- /dev/null +++ b/sdk/core/ts-http-runtime/src/util/internal.ts @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +export { + delay as __delay, + type DelayOptions as __DelayOptions, + calculateRetryDelay as __calculateRetryDelay, +} from "./delay.js"; +export { + type AbortOptions as __AbortOptions, + cancelablePromiseRace as __cancelablePromiseRace, + type AbortablePromiseBuilder as __AbortablePromiseBuilder, +} from "./aborterUtils.js"; +export { + createAbortablePromise as __createAbortablePromise, + type CreateAbortablePromiseOptions as __CreateAbortablePromiseOptions, +} from "./createAbortablePromise.js"; +export { getRandomIntegerInclusive as __getRandomIntegerInclusive } from "./random.js"; +export { isObject as __isObject, type UnknownObject as __UnknownObject } from "./object.js"; +export { isError as __isError, getErrorMessage as __getErrorMessage } from "./error.js"; +export { + computeSha256Hash as __computeSha256Hash, + computeSha256Hmac as __computeSha256Hmac, +} from "./sha256.js"; +export { + isDefined as __isDefined, + isObjectWithProperties as __isObjectWithProperties, + objectHasProperty as __objectHasProperty, +} from "./typeGuards.js"; +export { randomUUID as __randomUUID } from "./uuidUtils.js"; +export { + isBrowser as __isBrowser, + isBun as __isBun, + isNode as __isNode, + isNodeLike as __isNodeLike, + isNodeRuntime as __isNodeRuntime, + isDeno as __isDeno, + isReactNative as __isReactNative, + isWebWorker as __isWebWorker, +} from "./checkEnvironment.js"; +export { + stringToUint8Array as __stringToUint8Array, + uint8ArrayToString as __uint8ArrayToString, + type EncodingType as __EncodingType, +} from "./bytesEncoding.js";