diff --git a/client/creation.ts b/client/creation.ts index bbea0f3..28d5204 100644 --- a/client/creation.ts +++ b/client/creation.ts @@ -1,4 +1,5 @@ import { RpcRequest } from "../rpc_types.ts"; +import { generateUlid } from "./deps.ts"; export type CreateRequestInput = { method: string; @@ -7,10 +8,6 @@ export type CreateRequestInput = { id?: RpcRequest["id"]; }; -function generateId() { - return crypto.getRandomValues(new Uint32Array(1))[0].toString(16); -} - export function createRequest( { method, params, isNotification = false, id }: CreateRequestInput, ): RpcRequest { @@ -19,7 +16,7 @@ export function createRequest( method, }; params && (rpcRequest.params = params); - id = isNotification ? undefined : id !== undefined ? id : generateId(); + id = isNotification ? undefined : id !== undefined ? id : generateUlid(); id !== undefined && (rpcRequest.id = id); return rpcRequest; } diff --git a/client/deps.ts b/client/deps.ts new file mode 100644 index 0000000..b76d7a5 --- /dev/null +++ b/client/deps.ts @@ -0,0 +1 @@ +export { generateUlid } from "https://dev.zaubrik.com/sorcery@v0.1.5/util/id.js"; diff --git a/client/dist/mod.js b/client/dist/mod.js index 801e274..0110da7 100644 --- a/client/dist/mod.js +++ b/client/dist/mod.js @@ -2,8 +2,68 @@ // deno-lint-ignore-file // This code was bundled using `deno bundle` and it's not recommended to edit it manually -function generateId() { - return crypto.getRandomValues(new Uint32Array(1))[0].toString(16); +const ENCODING = "0123456789ABCDEFGHJKMNPQRSTVWXYZ"; +const ENCODING_LEN = ENCODING.length; +const TIME_MAX = Math.pow(2, 48) - 1; +const RANDOM_LEN = 16; +function replaceCharAt(str, index, __char) { + return str.substring(0, index) + __char + str.substring(index + 1); +} +function encodeTime(now, len = 10) { + if (!Number.isInteger(now) || now < 0 || now > TIME_MAX) { + throw new Error("Time must be a positive integer less than " + TIME_MAX); + } + let str = ""; + for(; len > 0; len--){ + const mod = now % ENCODING_LEN; + str = ENCODING[mod] + str; + now = (now - mod) / ENCODING_LEN; + } + return str; +} +function encodeRandom(len) { + let str = ""; + const randomBytes = crypto.getRandomValues(new Uint8Array(len)); + for (const randomByte of randomBytes){ + str += ENCODING[randomByte % ENCODING_LEN]; + } + return str; +} +function incrementBase32(str) { + let index = str.length; + let __char; + let charIndex; + const maxCharIndex = ENCODING_LEN - 1; + while(--index >= 0){ + __char = str[index]; + charIndex = ENCODING.indexOf(__char); + if (charIndex === -1) { + throw new Error("incorrectly encoded string"); + } + if (charIndex === maxCharIndex) { + str = replaceCharAt(str, index, ENCODING[0]); + continue; + } + return replaceCharAt(str, index, ENCODING[charIndex + 1]); + } + throw new Error("cannot increment this string"); +} +function monotonicFactory(encodeRand = encodeRandom) { + let lastTime = 0; + let lastRandom; + return function ulid(seedTime = Date.now()) { + if (seedTime <= lastTime) { + const incrementedRandom = lastRandom = incrementBase32(lastRandom); + return encodeTime(lastTime, 10) + incrementedRandom; + } + lastTime = seedTime; + const newRandom = lastRandom = encodeRand(RANDOM_LEN); + return encodeTime(seedTime, 10) + newRandom; + }; +} +monotonicFactory(); +function ulid(seedTime = Date.now()) { + return encodeTime(seedTime, 10) + encodeRandom(16); } function createRequest({ method, params, isNotification = false, id }) { const rpcRequest = { @@ -11,7 +71,7 @@ function createRequest({ method, params, isNotification = false, id }) { method }; params && (rpcRequest.params = params); - id = isNotification ? undefined : id !== undefined ? id : generateId(); + id = isNotification ? undefined : id !== undefined ? id : ulid(); id !== undefined && (rpcRequest.id = id); return rpcRequest; } diff --git a/server/auth.ts b/server/auth.ts index 70fb6f9..590fd9a 100644 --- a/server/auth.ts +++ b/server/auth.ts @@ -1,18 +1,14 @@ import { authErrorData } from "./error_data.ts"; -import { getJwtFromBearer, type Payload } from "./deps.ts"; +import { getJwtFromBearer, isArray, isPresent, type Payload } from "./deps.ts"; import { type CreationInput } from "./creation.ts"; -import { type AuthInputAndVerify } from "./response.ts"; +import { type AuthInput } from "./response.ts"; import { type ValidationSuccess } from "./validation.ts"; -export type AuthData = AuthInputAndVerify & { headers: Headers }; +export type AuthData = AuthInput & { headers: Headers }; type VerifyJwtForSelectedMethodsReturnType = CreationInput & { payload?: Payload; }; -function isPresent(input: T | undefined | null): input is T { - return input !== undefined && input !== null; -} - export async function verifyJwtForSelectedMethods( { validationObject, methods, options, authDataArray }: CreationInput & { authDataArray?: AuthData[]; @@ -47,14 +43,21 @@ function processAuthData( return async (authData: AuthData) => { const authMethods = authData.methods; if ( - Array.isArray(authMethods) + isArray(authMethods) ? authMethods.includes(validationObject.method) : authMethods?.test(validationObject.method) ) { try { - const jwt = getJwtFromBearer(authData.headers); - const payload = await authData.verify(jwt, authData.options); - return { validationObject, methods, options, payload }; + const verify = authData.verification; + if (typeof verify === "function") { + const jwt = getJwtFromBearer(authData.headers); + const payload = await verify(jwt, authData.options); + return { validationObject, methods, options, payload }; + } else { + throw new Error( + "There is no verify function. This error should never happen!", + ); + } } catch (err) { return { validationObject: { diff --git a/server/creation.ts b/server/creation.ts index 405fc6e..f3047d7 100644 --- a/server/creation.ts +++ b/server/creation.ts @@ -4,7 +4,7 @@ import { type AuthData, verifyJwtForSelectedMethods } from "./auth.ts"; import { type RpcBatchResponse, type RpcResponse } from "../rpc_types.ts"; import { type ValidationObject } from "./validation.ts"; import { type Methods, type Options } from "./response.ts"; -import { type Payload } from "./deps.ts"; +import { isArray, type Payload } from "./deps.ts"; export type CreationInput = { validationObject: ValidationObject; @@ -93,7 +93,7 @@ export async function createRpcResponseOrBatch( options: Options, authDataArray?: AuthData[], ): Promise { - return Array.isArray(validationObjectOrBatch) + return isArray(validationObjectOrBatch) ? await cleanBatch( validationObjectOrBatch.map(async (validationObject) => createRpcResponse( diff --git a/server/deps.ts b/server/deps.ts index bf3587d..512bca9 100644 --- a/server/deps.ts +++ b/server/deps.ts @@ -8,3 +8,9 @@ export { getJwtFromBearer, verifyJwt, } from "https://dev.zaubrik.com/portal@v0.2.7/functions/mod.ts"; +export { + isArray, + isFunction, + isObject, + isPresent, +} from "https://dev.zaubrik.com/sorcery@v0.1.5/type.js"; diff --git a/server/object.js b/server/object.js new file mode 100644 index 0000000..8113fe3 --- /dev/null +++ b/server/object.js @@ -0,0 +1,11 @@ +/** + * isObject. + * @param {unknown} input + * @returns {input is Record} + */ +export function isObject(input) { + return ( + input !== null && typeof input === "object" && + Array.isArray(input) === false + ); +} diff --git a/server/response.ts b/server/response.ts index 86b1363..e2fe219 100644 --- a/server/response.ts +++ b/server/response.ts @@ -27,15 +27,15 @@ export type AuthInput = { options?: VerifyOptions; }; export type VerifyInput = CryptoKeyOrUpdateInput | Verify; -export type AuthInputAndVerify = Omit & { - verify: Verify; -}; -function addVerifyFunctions(authInput: AuthInput) { - const verify = typeof authInput.verification === "function" - ? authInput.verification - : verifyJwt(authInput.verification); - return { ...authInput, verify }; +export function ensureVerify(authInput: AuthInput): AuthInput { + const verification = authInput.verification; + return { + ...authInput, + verification: typeof verification === "function" + ? verification + : verifyJwt(verification), + }; } export function respond( @@ -45,7 +45,7 @@ export function respond( ) { const authInputArray = [authInput].flat(); const authInputArrayIsNotEmpty = authInputArray.length > 0; - const authInputAndVeryifyArray = authInputArray.map(addVerifyFunctions); + const authInputAndVeryifyArray = authInputArray.map(ensureVerify); return async (request: Request): Promise => { const authData = authInputArrayIsNotEmpty ? authInputAndVeryifyArray.map((authInput) => ({ diff --git a/server/validation.ts b/server/validation.ts index 932d11f..e717906 100644 --- a/server/validation.ts +++ b/server/validation.ts @@ -1,3 +1,4 @@ +import { isArray, isObject } from "./deps.ts"; import { invalidParamsErrorData, invalidRequestErrorData, @@ -55,13 +56,6 @@ function isRpcId(input: unknown): input is RpcId { } } -// deno-lint-ignore no-explicit-any -function isObject(obj: unknown): obj is Record { - return ( - obj !== null && typeof obj === "object" && Array.isArray(obj) === false - ); -} - function tryToParse(json: string) { try { return [JSON.parse(json), null];