@open-condo/miniapp-utils
Version:
A set of helper functions / components / hooks used to build new condo apps fast
587 lines (575 loc) • 22.4 kB
JavaScript
;
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/helpers/messaging/index.ts
var messaging_exports = {};
__export(messaging_exports, {
PostMessageController: () => PostMessageController,
PostMessageProvider: () => PostMessageProvider,
typeCheckerToValidator: () => typeCheckerToValidator,
usePostMessageContext: () => usePostMessageContext,
zodSchemaToValidator: () => zodSchemaToValidator
});
module.exports = __toCommonJS(messaging_exports);
// src/helpers/messaging/provider.tsx
var import_react = __toESM(require("react"));
// src/helpers/messaging/controller.ts
var import_zod3 = require("zod");
// src/helpers/messaging/errors.ts
function getClientErrorMessage(reason, code, message, requestId, eventName) {
return {
type: eventName ? `${eventName}Error` : "CondoWebAppCommonError",
data: {
errorType: "client",
errorCode: code,
errorReason: reason,
errorMessage: message,
...typeof requestId !== "undefined" ? { requestId } : null
}
};
}
// src/helpers/messaging/events/bridge.ts
var import_zod2 = require("zod");
// src/helpers/ip/utils.ts
var v4Seg = "(?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])";
var v4Str = `(${v4Seg}[.]){3}${v4Seg}`;
var IPv4Reg = new RegExp(`^${v4Str}$`);
var v6Seg = "(?:[0-9a-fA-F]{1,4})";
var IPv6Reg = new RegExp(
`^((?:${v6Seg}:){7}(?:${v6Seg}|:)|(?:${v6Seg}:){6}(?:${v4Str}|:${v6Seg}|:)|(?:${v6Seg}:){5}(?::${v4Str}|(:${v6Seg}){1,2}|:)|(?:${v6Seg}:){4}(?:(:${v6Seg}){0,1}:${v4Str}|(:${v6Seg}){1,3}|:)|(?:${v6Seg}:){3}(?:(:${v6Seg}){0,2}:${v4Str}|(:${v6Seg}){1,4}|:)|(?:${v6Seg}:){2}(?:(:${v6Seg}){0,3}:${v4Str}|(:${v6Seg}){1,5}|:)|(?:${v6Seg}:){1}(?:(:${v6Seg}){0,4}:${v4Str}|(:${v6Seg}){1,6}|:)|(?::((?::${v6Seg}){0,5}:${v4Str}|(?::${v6Seg}){1,7}|:)))(%[0-9a-zA-Z]{1,})?$`
);
// src/helpers/urls.ts
var REGEXP_ESCAPE_CHARS = /[\\^$.*+?()[\]{}|]/g;
var WILDCARD_REGEXP_PART = "([a-zA-Z0-9-]{1,63})";
var WILDCARD_REGEXP_PART_ESCAPED = _escapeRegexp(WILDCARD_REGEXP_PART);
function isSafeUrl(url) {
if (!url || typeof url !== "string") return false;
let decodedUrl;
try {
decodedUrl = decodeURI(url);
} catch (error) {
return false;
}
const normalizedUrl = decodedUrl.replace(/[\u0000-\u001F\s]/g, "").toLowerCase();
return !normalizedUrl.includes("javascript:");
}
function _escapeRegexp(source) {
return source.replace(REGEXP_ESCAPE_CHARS, "\\$&");
}
// src/helpers/uuid.ts
var import_crypto = require("crypto");
function generateUUIDv4() {
let randomValues;
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
return crypto.randomUUID();
} else if (typeof window !== "undefined" && window.crypto && window.crypto.getRandomValues) {
randomValues = new Uint8Array(16);
window.crypto.getRandomValues(randomValues);
} else {
randomValues = (0, import_crypto.randomBytes)(16);
}
randomValues[6] = randomValues[6] & 15 | 64;
randomValues[8] = randomValues[8] & 63 | 128;
return [...randomValues].map((value, index) => {
const hex = value.toString(16).padStart(2, "0");
if (index === 4 || index === 6 || index === 8 || index === 10) {
return `-${hex}`;
}
return hex;
}).join("");
}
// src/helpers/messaging/utils.ts
var import_zod = require("zod");
function typeCheckerToValidator(typeChecker) {
return (params) => {
if (!typeChecker(params)) {
return { success: false, error: "Invalid params" };
}
return { success: true, data: params };
};
}
function zodSchemaToValidator(schema) {
return (params) => {
const result = schema.safeParse(params);
if (!result.success) {
return { success: false, error: import_zod.z.prettifyError(result.error) };
}
return { success: true, data: result.data };
};
}
function sortedMiddlewares(middlewares) {
return middlewares.sort((a, b) => {
const orderDiff = (a.order || 0) - (b.order || 0);
if (orderDiff !== 0) return orderDiff;
const aIsGlobal = a.scope === "*" ? 1 : 0;
const bIsGlobal = b.scope === "*" ? 1 : 0;
if (aIsGlobal !== bIsGlobal) return aIsGlobal - bIsGlobal;
const aHasEventName = a.eventName ? 0 : 1;
const bHasEventName = b.eventName ? 0 : 1;
if (aHasEventName !== bHasEventName) return bHasEventName - aHasEventName;
const aHasEventType = a.eventType ? 0 : 1;
const bHasEventType = b.eventType ? 0 : 1;
return bHasEventType - aHasEventType;
});
}
function isServiceWorker(source) {
return typeof ServiceWorker !== "undefined" && source instanceof ServiceWorker;
}
function sendResponseMessage({ data, target, origin }) {
if (isServiceWorker(target)) {
target.postMessage(data);
} else {
target.postMessage(data, origin);
}
}
// src/helpers/messaging/events/bridge.ts
function registerBridgeEvents({
addHandler,
router,
notificationsApi,
modalsApi
}) {
addHandler("condo-bridge", "CondoWebAppResizeWindow", "*", zodSchemaToValidator(import_zod2.z.strictObject({
height: import_zod2.z.number()
})), ({ params, source }) => {
if (source.type !== "frame") {
throw new Error("Forbidden source type. Resize window is only available for registered iframes");
}
source.ref.height = `${params.height}px`;
return { height: params.height };
});
addHandler("condo-bridge", "CondoWebAppGetFragment", "*", zodSchemaToValidator(import_zod2.z.strictObject({})), () => {
const hash = window.location.hash;
const rawFragment = hash.startsWith("#") ? hash.substring(1) : hash;
const fragment = decodeURIComponent(rawFragment);
return { fragment };
});
addHandler("condo-bridge", "CondoWebAppRedirect", "*", zodSchemaToValidator(import_zod2.z.strictObject({
url: import_zod2.z.url(),
target: import_zod2.z.union([import_zod2.z.literal("_blank"), import_zod2.z.literal("_self")])
})), async ({
params: { url, target }
}) => {
if (!isSafeUrl(url)) {
throw new Error("Forbidden url. Your url is probably injected");
}
if (target === "_blank") {
window.open(url, target);
} else {
const urlOrigin = new URL(url).origin;
if (window.origin !== urlOrigin) {
throw new Error("The redirect url must have the same origin as parent window, if target is not _blank");
}
if (router) {
router.push(url);
} else {
window.open(url, target);
}
}
return { success: true };
});
addHandler("condo-bridge", "CondoWebAppRequestAuth", "*", zodSchemaToValidator(import_zod2.z.strictObject({
url: import_zod2.z.url()
})), async ({
params: { url }
}) => {
if (!isSafeUrl(url)) {
throw new Error("Forbidden url. Your url is probably injected");
}
const response = await fetch(url, { credentials: "include" });
const body = await response.text();
return { response: { status: response.status, body, url: response.url } };
});
if (notificationsApi) {
addHandler("condo-bridge", "CondoWebAppShowNotification", "*", zodSchemaToValidator(import_zod2.z.strictObject({
message: import_zod2.z.string(),
description: import_zod2.z.string().optional(),
type: import_zod2.z.enum(["success", "error", "warning", "info"])
})), ({ params }) => {
notificationsApi(params);
return { success: true };
});
}
if (modalsApi) {
addHandler("condo-bridge", "CondoWebAppShowModalWindow", "*", zodSchemaToValidator(import_zod2.z.strictObject({
title: import_zod2.z.string(),
url: import_zod2.z.url(),
size: import_zod2.z.enum(["big", "small"]).optional(),
initialHeight: import_zod2.z.number().optional()
})), ({
source,
params,
storage
}) => {
if (source.type === "worker") {
throw new Error("Forbidden source type. Modals cannot be opened from service workers");
}
const modalId = generateUUIDv4();
if (!isSafeUrl(params.url)) {
throw new Error("Forbidden url. Your url is probably injected");
}
const originalSrc = new URL(params.url);
originalSrc.searchParams.set("modalId", modalId);
const sourceOrigin = new URL(source.type === "frame" ? source.ref.src : window.location.href).origin;
const sourceTarget = source.type === "frame" ? source.ref.contentWindow : source.ref;
if (sourceOrigin && originalSrc.origin !== sourceOrigin) {
throw new Error("Forbidden url. Url must have same origin as sender");
}
const closeEventData = {
type: "CondoWebAppCloseModalWindowResult",
data: {
success: true,
modalId
}
};
const onCancel = () => {
storage.events.delete(`modals:${modalId}`);
if (sourceTarget) {
sendResponseMessage({
data: closeEventData,
target: sourceTarget,
origin: sourceOrigin
});
}
};
storage.events.set(`modals:${modalId}`, modalsApi({
...params,
url: originalSrc.toString(),
onCancel,
metadata: source.type === "frame" ? source.metadata : void 0
}));
return { modalId };
});
addHandler("condo-bridge", "CondoWebAppUpdateModalWindow", "*", zodSchemaToValidator(import_zod2.z.strictObject({
modalId: import_zod2.z.string(),
data: import_zod2.z.strictObject({
title: import_zod2.z.string().optional(),
size: import_zod2.z.enum(["big", "small"]).optional()
})
})), ({ params, storage }) => {
const modalActions = storage.events.get(`modals:${params.modalId}`);
if (!modalActions) {
return { updated: false };
}
modalActions.update(params.data);
return { updated: true };
});
addHandler("condo-bridge", "CondoWebAppCloseModalWindow", "*", zodSchemaToValidator(import_zod2.z.strictObject({
modalId: import_zod2.z.string()
})), ({ params, storage }) => {
const modalActions = storage.events.get(`modals:${params.modalId}`);
if (!modalActions) {
return { modalId: params.modalId, success: false };
}
modalActions.destroy();
storage.events.delete(`modals:${params.modalId}`);
return { modalId: params.modalId, success: true };
});
}
}
// src/helpers/messaging/controller.ts
var SEMVER_REGEXP = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/;
var MESSAGE_SCHEMA = import_zod3.z.object({
handler: import_zod3.z.string(),
params: import_zod3.z.record(import_zod3.z.string(), import_zod3.z.unknown()).and(import_zod3.z.object({
requestId: import_zod3.z.union([import_zod3.z.string(), import_zod3.z.number()]).optional()
})),
type: import_zod3.z.string(),
version: import_zod3.z.string().regex(SEMVER_REGEXP)
}).strict();
var PostMessageController = class extends EventTarget {
constructor() {
super();
this.#registeredFrames = {};
this.#registeredHandlers = {};
this.#registeredMiddlewares = {};
this.#middlewaresIdsMap = {};
this.#storage = {};
this.state = { isBridgeReady: false };
this.addFrame = this.addFrame.bind(this);
this.removeFrame = this.removeFrame.bind(this);
this.addHandler = this.addHandler.bind(this);
this.eventListener = this.eventListener.bind(this);
this.registerBridgeEvents = this.registerBridgeEvents.bind(this);
this.addMiddleware = this.addMiddleware.bind(this);
this.removeMiddleware = this.removeMiddleware.bind(this);
}
#registeredFrames;
#registeredHandlers;
#registeredMiddlewares;
#middlewaresIdsMap;
#storage;
// ---- PRIVATE UTILITIES METHODS ----
updateState(state) {
this.state = { ...this.state, ...state };
this.dispatchEvent(new CustomEvent("statechange", { detail: this.state }));
}
getWrappedHandler(eventType, eventName, scope, handler) {
const globalMiddlewares = (this.#registeredMiddlewares["*"] ?? []).filter(
(mw) => (!mw.eventType || mw.eventType === eventType) && (!mw.eventName || mw.eventName === eventName) && mw.scope === "*"
);
const scopedMiddlewares = (this.#registeredMiddlewares[scope] ?? []).filter(
(mw) => (!mw.eventType || mw.eventType === eventType) && (!mw.eventName || mw.eventName === eventName) && mw.scope === scope
);
const middlewares = sortedMiddlewares([...globalMiddlewares, ...scopedMiddlewares]);
return middlewares.reduceRight(
(nextHandler, mw) => {
return async (args) => {
const nextMiddlewareFn = (nextArgs) => {
return nextHandler({
...args,
params: (nextArgs == null ? void 0 : nextArgs.params) ?? args.params
});
};
return mw.fn({
...args,
next: nextMiddlewareFn
});
};
},
handler
);
}
getMessageSource(source) {
if (isServiceWorker(source)) {
if (source !== navigator.serviceWorker.controller) return null;
return {
ref: source,
id: "worker",
type: "worker"
};
}
if (source === window) {
return {
ref: source,
id: "parent",
type: "window"
};
}
const registeredFrame = Object.entries(this.#registeredFrames).find(([, frame]) => frame.ref.contentWindow === source);
if (!registeredFrame) return null;
return {
ref: registeredFrame[1].ref,
id: registeredFrame[0],
type: "frame",
metadata: registeredFrame[1].metadata
};
}
// ---- SOURCE REGISTRATION METHODS ----
addFrame(frame, metadata) {
const registeredFrame = Object.entries(this.#registeredFrames).find(([, existingFrame]) => existingFrame.ref === frame);
if (registeredFrame) {
this.#registeredFrames[registeredFrame[0]].metadata = metadata;
return registeredFrame[0];
}
const frameId = generateUUIDv4();
this.#registeredFrames[frameId] = { ref: frame, metadata };
return frameId;
}
removeFrame(frameId) {
(this.#registeredMiddlewares[frameId] ?? []).forEach((mw) => {
delete this.#middlewaresIdsMap[mw.id];
});
delete this.#registeredFrames[frameId];
delete this.#registeredHandlers[frameId];
delete this.#registeredMiddlewares[frameId];
}
// ---- HANDLER REGISTRATION METHODS ----
addHandler(eventType, eventName, handlerScope, validator, handler) {
if (!this.#registeredHandlers[handlerScope]) {
this.#registeredHandlers[handlerScope] = {};
}
const scopedHandlers = this.#registeredHandlers[handlerScope];
if (!scopedHandlers[eventType]) {
scopedHandlers[eventType] = {};
}
const eventHandlers = scopedHandlers[eventType];
eventHandlers[eventName] = { validator, handler };
}
addMiddleware(mw) {
const { scope } = mw;
if (!this.#registeredMiddlewares[scope]) {
this.#registeredMiddlewares[scope] = [];
}
const id = generateUUIDv4();
this.#registeredMiddlewares[scope].push({
...mw,
fn: mw.fn,
id
});
this.#middlewaresIdsMap[id] = scope;
return id;
}
removeMiddleware(id) {
const scope = this.#middlewaresIdsMap[id];
if (scope && this.#registeredMiddlewares[scope]) {
this.#registeredMiddlewares[scope] = this.#registeredMiddlewares[scope].filter((mw) => mw.id !== id);
}
delete this.#middlewaresIdsMap[id];
}
// ---- EVENT LISTENERS ----
async eventListener(event) {
var _a, _b, _c, _d;
if (typeof window === "undefined") return;
if (!event.isTrusted || !event.source || !("self" in event.source) && !isServiceWorker(event.source)) return;
const { success: isValidMessage, data: message } = MESSAGE_SCHEMA.safeParse(event.data);
if (!isValidMessage) return;
const { handler: eventName, params: { requestId, ...handlerParams }, type: eventType } = message;
const messageSource = this.getMessageSource(event.source);
if (!messageSource) {
return sendResponseMessage({
data: getClientErrorMessage("ACCESS_DENIED", 0, "Message was received from unregistered origin / iframe", requestId, eventName),
target: event.source,
origin: event.origin
});
}
const handlerMethods = ((_b = (_a = this.#registeredHandlers[messageSource.id]) == null ? void 0 : _a[eventType]) == null ? void 0 : _b[eventName]) ?? ((_d = (_c = this.#registeredHandlers["*"]) == null ? void 0 : _c[eventType]) == null ? void 0 : _d[eventName]) ?? {};
const { handler, validator } = handlerMethods;
if (!handler || !validator) {
return sendResponseMessage({
data: getClientErrorMessage("UNKNOWN_METHOD", 2, "Unknown method was provided. Make sure your runtime environment supports it.", requestId),
origin: event.origin,
target: event.source
});
}
const validationResult = validator(handlerParams);
if (!validationResult.success) {
return sendResponseMessage({
data: getClientErrorMessage("INVALID_PARAMETERS", 3, validationResult.error, requestId, eventName),
origin: event.origin,
target: event.source
});
}
const validatedParams = validationResult.data;
this.#storage[eventType] ??= /* @__PURE__ */ new Map();
const eventsStorage = this.#storage[eventType];
const wrappedHandler = this.getWrappedHandler(eventType, eventName, messageSource.id, handler);
try {
const result = await wrappedHandler({
eventType,
eventName,
params: validatedParams,
storage: { events: eventsStorage },
source: messageSource
});
return sendResponseMessage({
data: {
type: `${eventName}Result`,
data: {
...result,
requestId
}
},
target: event.source,
origin: event.origin
});
} catch (err) {
const errorMessage = err instanceof Error ? err.message : String(err);
return sendResponseMessage({
data: getClientErrorMessage("HANDLER_ERROR", 4, errorMessage, requestId, eventName),
target: event.source,
origin: event.origin
});
}
}
// ---- COMMON HANDLERS METHODS ----
registerBridgeEvents(options) {
registerBridgeEvents({
...options,
addHandler: this.addHandler
});
this.updateState({ isBridgeReady: true });
}
};
// src/helpers/messaging/provider.tsx
function useControllerState(controller) {
const [controllerState, setControllerState] = (0, import_react.useState)(controller.state);
(0, import_react.useEffect)(() => {
const handleBridgeReadyChange = (event) => {
setControllerState(event.detail);
};
controller.addEventListener("statechange", handleBridgeReadyChange);
return () => {
controller.removeEventListener("statechange", handleBridgeReadyChange);
};
}, [controller]);
return controllerState;
}
var PostMessageContext = (0, import_react.createContext)({
addFrame: () => "",
removeFrame: () => {
},
addHandler: () => {
},
addMiddleware: () => "",
removeMiddleware: () => {
},
isBridgeReady: false
});
var PostMessageProvider = ({ children, router, notificationsApi, modalsApi }) => {
const [controller] = (0, import_react.useState)(() => new PostMessageController());
const controllerState = useControllerState(controller);
(0, import_react.useEffect)(() => {
controller.registerBridgeEvents({ router, notificationsApi, modalsApi });
}, [controller, modalsApi, notificationsApi, router]);
(0, import_react.useEffect)(() => {
window.addEventListener("message", controller.eventListener);
return () => {
window.removeEventListener("message", controller.eventListener);
};
}, [controller.eventListener]);
(0, import_react.useEffect)(() => {
var _a, _b;
(_b = (_a = window == null ? void 0 : window.navigator) == null ? void 0 : _a.serviceWorker) == null ? void 0 : _b.addEventListener("message", controller.eventListener);
return () => {
var _a2, _b2;
(_b2 = (_a2 = window == null ? void 0 : window.navigator) == null ? void 0 : _a2.serviceWorker) == null ? void 0 : _b2.removeEventListener("message", controller.eventListener);
};
}, [controller.eventListener]);
const contextValue = (0, import_react.useMemo)(() => ({
...controllerState,
addFrame: controller.addFrame,
removeFrame: controller.removeFrame,
addHandler: controller.addHandler,
addMiddleware: controller.addMiddleware,
removeMiddleware: controller.removeMiddleware
}), [controller, controllerState]);
return /* @__PURE__ */ import_react.default.createElement(PostMessageContext.Provider, { value: contextValue }, children);
};
function usePostMessageContext() {
return (0, import_react.useContext)(PostMessageContext);
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
PostMessageController,
PostMessageProvider,
typeCheckerToValidator,
usePostMessageContext,
zodSchemaToValidator
});
//# sourceMappingURL=index.js.map