334 lines
12 KiB
JavaScript
334 lines
12 KiB
JavaScript
"use strict";
|
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
if (k2 === undefined) k2 = k;
|
|
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
}
|
|
Object.defineProperty(o, k2, desc);
|
|
}) : (function(o, m, k, k2) {
|
|
if (k2 === undefined) k2 = k;
|
|
o[k2] = m[k];
|
|
}));
|
|
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
}) : function(o, v) {
|
|
o["default"] = v;
|
|
});
|
|
var __importStar = (this && this.__importStar) || function (mod) {
|
|
if (mod && mod.__esModule) return mod;
|
|
var result = {};
|
|
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
__setModuleDefault(result, mod);
|
|
return result;
|
|
};
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.getAlgorithm = exports.validateExp = exports.parseResponseAPIVersion = exports.getCodeChallengeAndMethod = exports.generatePKCEChallenge = exports.generatePKCEVerifier = exports.retryable = exports.sleep = exports.decodeJWT = exports.Deferred = exports.removeItemAsync = exports.getItemAsync = exports.setItemAsync = exports.looksLikeFetchResponse = exports.resolveFetch = exports.parseParametersFromURL = exports.supportsLocalStorage = exports.isBrowser = exports.uuid = exports.expiresAt = void 0;
|
|
const constants_1 = require("./constants");
|
|
const errors_1 = require("./errors");
|
|
const base64url_1 = require("./base64url");
|
|
function expiresAt(expiresIn) {
|
|
const timeNow = Math.round(Date.now() / 1000);
|
|
return timeNow + expiresIn;
|
|
}
|
|
exports.expiresAt = expiresAt;
|
|
function uuid() {
|
|
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
|
|
const r = (Math.random() * 16) | 0, v = c == 'x' ? r : (r & 0x3) | 0x8;
|
|
return v.toString(16);
|
|
});
|
|
}
|
|
exports.uuid = uuid;
|
|
const isBrowser = () => typeof window !== 'undefined' && typeof document !== 'undefined';
|
|
exports.isBrowser = isBrowser;
|
|
const localStorageWriteTests = {
|
|
tested: false,
|
|
writable: false,
|
|
};
|
|
/**
|
|
* Checks whether localStorage is supported on this browser.
|
|
*/
|
|
const supportsLocalStorage = () => {
|
|
if (!(0, exports.isBrowser)()) {
|
|
return false;
|
|
}
|
|
try {
|
|
if (typeof globalThis.localStorage !== 'object') {
|
|
return false;
|
|
}
|
|
}
|
|
catch (e) {
|
|
// DOM exception when accessing `localStorage`
|
|
return false;
|
|
}
|
|
if (localStorageWriteTests.tested) {
|
|
return localStorageWriteTests.writable;
|
|
}
|
|
const randomKey = `lswt-${Math.random()}${Math.random()}`;
|
|
try {
|
|
globalThis.localStorage.setItem(randomKey, randomKey);
|
|
globalThis.localStorage.removeItem(randomKey);
|
|
localStorageWriteTests.tested = true;
|
|
localStorageWriteTests.writable = true;
|
|
}
|
|
catch (e) {
|
|
// localStorage can't be written to
|
|
// https://www.chromium.org/for-testers/bug-reporting-guidelines/uncaught-securityerror-failed-to-read-the-localstorage-property-from-window-access-is-denied-for-this-document
|
|
localStorageWriteTests.tested = true;
|
|
localStorageWriteTests.writable = false;
|
|
}
|
|
return localStorageWriteTests.writable;
|
|
};
|
|
exports.supportsLocalStorage = supportsLocalStorage;
|
|
/**
|
|
* Extracts parameters encoded in the URL both in the query and fragment.
|
|
*/
|
|
function parseParametersFromURL(href) {
|
|
const result = {};
|
|
const url = new URL(href);
|
|
if (url.hash && url.hash[0] === '#') {
|
|
try {
|
|
const hashSearchParams = new URLSearchParams(url.hash.substring(1));
|
|
hashSearchParams.forEach((value, key) => {
|
|
result[key] = value;
|
|
});
|
|
}
|
|
catch (e) {
|
|
// hash is not a query string
|
|
}
|
|
}
|
|
// search parameters take precedence over hash parameters
|
|
url.searchParams.forEach((value, key) => {
|
|
result[key] = value;
|
|
});
|
|
return result;
|
|
}
|
|
exports.parseParametersFromURL = parseParametersFromURL;
|
|
const resolveFetch = (customFetch) => {
|
|
let _fetch;
|
|
if (customFetch) {
|
|
_fetch = customFetch;
|
|
}
|
|
else if (typeof fetch === 'undefined') {
|
|
_fetch = (...args) => Promise.resolve().then(() => __importStar(require('@supabase/node-fetch'))).then(({ default: fetch }) => fetch(...args));
|
|
}
|
|
else {
|
|
_fetch = fetch;
|
|
}
|
|
return (...args) => _fetch(...args);
|
|
};
|
|
exports.resolveFetch = resolveFetch;
|
|
const looksLikeFetchResponse = (maybeResponse) => {
|
|
return (typeof maybeResponse === 'object' &&
|
|
maybeResponse !== null &&
|
|
'status' in maybeResponse &&
|
|
'ok' in maybeResponse &&
|
|
'json' in maybeResponse &&
|
|
typeof maybeResponse.json === 'function');
|
|
};
|
|
exports.looksLikeFetchResponse = looksLikeFetchResponse;
|
|
// Storage helpers
|
|
const setItemAsync = async (storage, key, data) => {
|
|
await storage.setItem(key, JSON.stringify(data));
|
|
};
|
|
exports.setItemAsync = setItemAsync;
|
|
const getItemAsync = async (storage, key) => {
|
|
const value = await storage.getItem(key);
|
|
if (!value) {
|
|
return null;
|
|
}
|
|
try {
|
|
return JSON.parse(value);
|
|
}
|
|
catch (_a) {
|
|
return value;
|
|
}
|
|
};
|
|
exports.getItemAsync = getItemAsync;
|
|
const removeItemAsync = async (storage, key) => {
|
|
await storage.removeItem(key);
|
|
};
|
|
exports.removeItemAsync = removeItemAsync;
|
|
/**
|
|
* A deferred represents some asynchronous work that is not yet finished, which
|
|
* may or may not culminate in a value.
|
|
* Taken from: https://github.com/mike-north/types/blob/master/src/async.ts
|
|
*/
|
|
class Deferred {
|
|
constructor() {
|
|
// eslint-disable-next-line @typescript-eslint/no-extra-semi
|
|
;
|
|
this.promise = new Deferred.promiseConstructor((res, rej) => {
|
|
// eslint-disable-next-line @typescript-eslint/no-extra-semi
|
|
;
|
|
this.resolve = res;
|
|
this.reject = rej;
|
|
});
|
|
}
|
|
}
|
|
exports.Deferred = Deferred;
|
|
Deferred.promiseConstructor = Promise;
|
|
function decodeJWT(token) {
|
|
const parts = token.split('.');
|
|
if (parts.length !== 3) {
|
|
throw new errors_1.AuthInvalidJwtError('Invalid JWT structure');
|
|
}
|
|
// Regex checks for base64url format
|
|
for (let i = 0; i < parts.length; i++) {
|
|
if (!constants_1.BASE64URL_REGEX.test(parts[i])) {
|
|
throw new errors_1.AuthInvalidJwtError('JWT not in base64url format');
|
|
}
|
|
}
|
|
const data = {
|
|
// using base64url lib
|
|
header: JSON.parse((0, base64url_1.stringFromBase64URL)(parts[0])),
|
|
payload: JSON.parse((0, base64url_1.stringFromBase64URL)(parts[1])),
|
|
signature: (0, base64url_1.base64UrlToUint8Array)(parts[2]),
|
|
raw: {
|
|
header: parts[0],
|
|
payload: parts[1],
|
|
},
|
|
};
|
|
return data;
|
|
}
|
|
exports.decodeJWT = decodeJWT;
|
|
/**
|
|
* Creates a promise that resolves to null after some time.
|
|
*/
|
|
async function sleep(time) {
|
|
return await new Promise((accept) => {
|
|
setTimeout(() => accept(null), time);
|
|
});
|
|
}
|
|
exports.sleep = sleep;
|
|
/**
|
|
* Converts the provided async function into a retryable function. Each result
|
|
* or thrown error is sent to the isRetryable function which should return true
|
|
* if the function should run again.
|
|
*/
|
|
function retryable(fn, isRetryable) {
|
|
const promise = new Promise((accept, reject) => {
|
|
// eslint-disable-next-line @typescript-eslint/no-extra-semi
|
|
;
|
|
(async () => {
|
|
for (let attempt = 0; attempt < Infinity; attempt++) {
|
|
try {
|
|
const result = await fn(attempt);
|
|
if (!isRetryable(attempt, null, result)) {
|
|
accept(result);
|
|
return;
|
|
}
|
|
}
|
|
catch (e) {
|
|
if (!isRetryable(attempt, e)) {
|
|
reject(e);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
})();
|
|
});
|
|
return promise;
|
|
}
|
|
exports.retryable = retryable;
|
|
function dec2hex(dec) {
|
|
return ('0' + dec.toString(16)).substr(-2);
|
|
}
|
|
// Functions below taken from: https://stackoverflow.com/questions/63309409/creating-a-code-verifier-and-challenge-for-pkce-auth-on-spotify-api-in-reactjs
|
|
function generatePKCEVerifier() {
|
|
const verifierLength = 56;
|
|
const array = new Uint32Array(verifierLength);
|
|
if (typeof crypto === 'undefined') {
|
|
const charSet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~';
|
|
const charSetLen = charSet.length;
|
|
let verifier = '';
|
|
for (let i = 0; i < verifierLength; i++) {
|
|
verifier += charSet.charAt(Math.floor(Math.random() * charSetLen));
|
|
}
|
|
return verifier;
|
|
}
|
|
crypto.getRandomValues(array);
|
|
return Array.from(array, dec2hex).join('');
|
|
}
|
|
exports.generatePKCEVerifier = generatePKCEVerifier;
|
|
async function sha256(randomString) {
|
|
const encoder = new TextEncoder();
|
|
const encodedData = encoder.encode(randomString);
|
|
const hash = await crypto.subtle.digest('SHA-256', encodedData);
|
|
const bytes = new Uint8Array(hash);
|
|
return Array.from(bytes)
|
|
.map((c) => String.fromCharCode(c))
|
|
.join('');
|
|
}
|
|
async function generatePKCEChallenge(verifier) {
|
|
const hasCryptoSupport = typeof crypto !== 'undefined' &&
|
|
typeof crypto.subtle !== 'undefined' &&
|
|
typeof TextEncoder !== 'undefined';
|
|
if (!hasCryptoSupport) {
|
|
console.warn('WebCrypto API is not supported. Code challenge method will default to use plain instead of sha256.');
|
|
return verifier;
|
|
}
|
|
const hashed = await sha256(verifier);
|
|
return btoa(hashed).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
|
}
|
|
exports.generatePKCEChallenge = generatePKCEChallenge;
|
|
async function getCodeChallengeAndMethod(storage, storageKey, isPasswordRecovery = false) {
|
|
const codeVerifier = generatePKCEVerifier();
|
|
let storedCodeVerifier = codeVerifier;
|
|
if (isPasswordRecovery) {
|
|
storedCodeVerifier += '/PASSWORD_RECOVERY';
|
|
}
|
|
await (0, exports.setItemAsync)(storage, `${storageKey}-code-verifier`, storedCodeVerifier);
|
|
const codeChallenge = await generatePKCEChallenge(codeVerifier);
|
|
const codeChallengeMethod = codeVerifier === codeChallenge ? 'plain' : 's256';
|
|
return [codeChallenge, codeChallengeMethod];
|
|
}
|
|
exports.getCodeChallengeAndMethod = getCodeChallengeAndMethod;
|
|
/** Parses the API version which is 2YYY-MM-DD. */
|
|
const API_VERSION_REGEX = /^2[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|1[0-9]|2[0-9]|3[0-1])$/i;
|
|
function parseResponseAPIVersion(response) {
|
|
const apiVersion = response.headers.get(constants_1.API_VERSION_HEADER_NAME);
|
|
if (!apiVersion) {
|
|
return null;
|
|
}
|
|
if (!apiVersion.match(API_VERSION_REGEX)) {
|
|
return null;
|
|
}
|
|
try {
|
|
const date = new Date(`${apiVersion}T00:00:00.0Z`);
|
|
return date;
|
|
}
|
|
catch (e) {
|
|
return null;
|
|
}
|
|
}
|
|
exports.parseResponseAPIVersion = parseResponseAPIVersion;
|
|
function validateExp(exp) {
|
|
if (!exp) {
|
|
throw new Error('Missing exp claim');
|
|
}
|
|
const timeNow = Math.floor(Date.now() / 1000);
|
|
if (exp <= timeNow) {
|
|
throw new Error('JWT has expired');
|
|
}
|
|
}
|
|
exports.validateExp = validateExp;
|
|
function getAlgorithm(alg) {
|
|
switch (alg) {
|
|
case 'RS256':
|
|
return {
|
|
name: 'RSASSA-PKCS1-v1_5',
|
|
hash: { name: 'SHA-256' },
|
|
};
|
|
case 'ES256':
|
|
return {
|
|
name: 'ECDSA',
|
|
namedCurve: 'P-256',
|
|
hash: { name: 'SHA-256' },
|
|
};
|
|
default:
|
|
throw new Error('Invalid alg claim');
|
|
}
|
|
}
|
|
exports.getAlgorithm = getAlgorithm;
|
|
//# sourceMappingURL=helpers.js.map
|