UNPKG

@microbitsclub/microbits-client

Version:
636 lines (629 loc) 17.6 kB
"use strict"; 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 __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; 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( isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); var __publicField = (obj, key, value) => { __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); return value; }; // src/index.ts var src_exports = {}; __export(src_exports, { APIKeyRequiredError: () => APIKeyRequiredError, DEFAULTS: () => DEFAULTS, MicrobitsClient: () => MicrobitsClient, getClientRequestHandler: () => getClientRequestHandler, getContentRequestHandler: () => getContentRequestHandler }); module.exports = __toCommonJS(src_exports); // src/errors.ts var _APIKeyRequiredError = class extends Error { constructor() { super(_APIKeyRequiredError.message); } }; var APIKeyRequiredError = _APIKeyRequiredError; __publicField(APIKeyRequiredError, "message", "An API key is required for the requested endpoint"); // src/url.ts var isCharCodeRange = (c, a, b) => { const u = c.charCodeAt(0); return u >= a && u <= b; }; var isCharRange = (c, a, b) => { return isCharCodeRange(c, a.charCodeAt(0), b.charCodeAt(0)); }; var isNumeric = (c) => { const u = c.charCodeAt(0); return u >= 48 && u <= 57; }; var isAlpha = (c) => { const u = c.charCodeAt(0); return u >= 65 && u <= 90 || u >= 97 && u <= 122; }; var parseProto = (s) => { const l = s.length; for (let i = 0; i < l; ++i) { const c = s[i]; if (c === ":") { if (s[i + 1] === "/" && s[i + 2] === "/") { return s.slice(0, i); } else { break; } } if (!isAlpha(c)) { break; } } return void 0; }; var parseHostname = (s) => { const l = s.length + 1; let isIp6 = s[0] === "["; for (let i = 0; i < l; ++i) { const c = s[i]; if (isIp6) { if (i === 0) continue; if (c === ":") continue; if (isNumeric(c)) continue; if (isCharRange(c, "a", "f")) continue; if (isCharRange(c, "A", "F")) continue; if (c === "]") { if (i === 1) return void 0; return [s.slice(i + 1), s.slice(1, i)]; } break; } else { if (c === ":" || c === "/" || c === "?" || c === void 0) { if (i === 0) return void 0; return [s.slice(i), s.slice(0, i)]; } } } return void 0; }; var parsePort = (s) => { if (s[0] !== ":") return void 0; const l = s.length + 1; for (let i = 1; i < l; ++i) { const c = s[i]; if (c === void 0 || !isNumeric(c)) { if (i === 1) break; return s.slice(1, i); } } return void 0; }; var parsePath = (s) => { if (s[0] !== "/") return void 0; const l = s.length + 1; const path = []; let part = ""; let trailing = true; for (let i = 1; i < l; ++i) { const c = s[i]; if (c === void 0 || c === "?") { if (part.length > 0) path.push(part); return [s.slice(i), { path, trailing }]; } if (c === "/") { trailing = true; if (part.length === 0) break; path.push(part); part = ""; continue; } part += c; trailing = false; } return void 0; }; var parseQuery = (s) => { if (s[0] !== "?") return void 0; const l = s.length + 1; const query = {}; let pivot = 0; let count = 0; for (let i = 1; i < l; ++i) { const c = s[i]; if (c === void 0 || c === "&") { const [key, val] = s.slice(pivot + 1, i).split("="); if (val !== void 0) { const prevVal = query[key]; if (prevVal === void 0) { query[key] = decodeURIComponent(val ?? ""); count += 1; } else if (typeof prevVal === "string") { query[key] = [prevVal, decodeURIComponent(val ?? "")]; } else { prevVal.push(decodeURIComponent(val ?? "")); } } pivot = i; } } return count === 0 ? void 0 : query; }; var parseUrl = (s) => { const proto = parseProto(s); if (proto !== void 0) { s = s.slice(proto.length + 3); } let hostname; const hostnameResult = parseHostname(s); if (hostnameResult !== void 0) { s = hostnameResult[0]; hostname = hostnameResult[1]; } let port; const portResult = parsePort(s); if (portResult !== void 0) { s = s.slice(portResult.length + 1); port = Number.parseInt(portResult); } let path = []; let trailing; const pathResult = parsePath(s); if (pathResult !== void 0) { s = pathResult[0]; path = pathResult[1].path; trailing = pathResult[1].trailing; } const query = parseQuery(s); const url = { proto, hostname, port, path, trailing: trailing ?? false, query }; if (url.proto === void 0) delete url.proto; if (url.hostname === void 0) delete url.hostname; if (url.port === void 0) delete url.port; if (url.query === void 0) delete url.query; return url; }; var stringifyUrlPath = ({ path, trailing }) => { let s = ""; if (path) s += "/" + path.join("/"); if (trailing === true) s += "/"; return s; }; var stringifyUrl = (url) => { let s = ""; if (url.proto) s += url.proto + "://"; if (url.hostname) s += url.hostname; if (url.port) s += `:${url.port}`; s += stringifyUrlPath(url); if (url.query !== void 0) { const parts = []; for (const k in url.query) { const v = url.query[k]; if (v !== void 0) { if (typeof v === "string") { parts.push(`${k}=${encodeURIComponent(v)}`); } else { for (const x of v) { parts.push(`${k}=${encodeURIComponent(x)}`); } } } } s += `?${parts.join("&")}`; } return s; }; // src/microbits-client-config.ts var NODE_ENV = "development"; var LOADED_MICROBITS_URL = NODE_ENV === "development" ? "https://microbits.localhost" : "https://microbits.club"; var DEFAULTS = { MICROBITS_URL: parseUrl(LOADED_MICROBITS_URL), USER_SESSION_COOKIE: "microbits.user_session_id", MERCHANT_ROUTE_PREFIX: "/microbits" }; // src/microbits-client.ts var import_cross_fetch = __toESM(require("cross-fetch"), 1); var MicrobitsClient = class { constructor(config) { this.config = config; this.fetch = config.fetchFunction ?? import_cross_fetch.default; if (this.config.logging) { const params = { ...config }; if (params.apiKey !== void 0) { params.apiKey = "<api-key>"; } this._logParams("INFO", "constructor", params); if (this.config.logging === true) { this._log( "WARN", "using console.log as default logger; replace with production-ready alternative" ); } } } fetch; _authorizationHeader; _getAuthorizationHeader() { if (this._authorizationHeader === void 0) { if (this.config.apiKey === void 0) { this._log("FATAL", APIKeyRequiredError.message); throw new APIKeyRequiredError(); } this._log("INFO", "api key set"); this._authorizationHeader = `Bearer ${this.config.apiKey}`; } return this._authorizationHeader; } _log(level, message) { if (this.config.logging === true) { console.log({ level, message }); } else if (this.config.logging) { this.config.logging(level, message); } } _logParams(level, op, params) { const s = []; for (const k in params) { s.push(`${k}=${params[k]}`); } this._log(level, `${op}: ${s.join(", ")}`); } _responseLogger = (op) => async (resp) => { if (this.config.logging) { if (resp.ok) { const { status } = resp; this._logParams("INFO", op, { status }); } else { const { status, statusText } = resp; this._logParams(resp.status === 500 ? "FATAL" : "ERROR", op, { status, statusText }); } } return resp; }; async getPaywallById(id) { const url = stringifyUrl({ ...DEFAULTS.MICROBITS_URL, path: ["api", "paywall", id] }); const init = { method: "GET", headers: { Authorization: this._getAuthorizationHeader() } }; this._logParams("INFO", "getPaywallById", { url, id }); return this.fetch(url, init).then(this._responseLogger("getPaywallById")).then((resp) => resp.json()).then((x) => x); } async getPaywallByContentUrl(contentUrl) { const url = stringifyUrl({ ...DEFAULTS.MICROBITS_URL, path: ["api", "paywall"], query: { merchantId: this.config.merchantId, contentUrl } }); const init = { method: "GET", headers: { Authorization: this._getAuthorizationHeader() } }; this._logParams("INFO", "getPaywallByContentUrl", { url, contentUrl }); return this.fetch(url, init).then(this._responseLogger("getPaywallByContentUrl")).then((resp) => resp.json()).then((x) => x); } async getUserSessionById(id) { const url = stringifyUrl({ ...DEFAULTS.MICROBITS_URL, path: ["api", "user-session", id] }); const init = { method: "GET", headers: { Authorization: this._getAuthorizationHeader() } }; this._logParams("INFO", "getUserSessionById", { url, id }); return this.fetch(url, init).then(this._responseLogger("getUserSessionById")).then((resp) => resp.json()).then((x) => x); } async isUserSessionAuthorizedForPaywall(userSessionId, contentUrl, paywallId) { const url = stringifyUrl({ ...DEFAULTS.MICROBITS_URL, path: ["api", "user-session", userSessionId, "authorized"], query: { contentUrl, paywallId } }); const init = { method: "GET", headers: { Authorization: this._getAuthorizationHeader() } }; this._logParams("INFO", "isUserSessionAuthorizedForPaywall", { url, userSessionId, contentUrl, paywallId }); return this.fetch(url, init).then(this._responseLogger("isUserSessionAuthorizedForPaywall")).then((resp) => resp.json()).then((x) => x); } async activateUserSession(userSessionId) { const url = stringifyUrl({ ...DEFAULTS.MICROBITS_URL, path: ["api", "user-session", userSessionId, "activate"] }); const init = { method: "POST", headers: { Authorization: this._getAuthorizationHeader() } }; this._logParams("INFO", "activateUserSession", { url, userSessionId }); return this.fetch(url, init).then(this._responseLogger("activateUserSession")).then((resp) => resp.json()).then((x) => x); } async createPaywallPurchase(paywallId, userSessionId, contentUrl) { const url = stringifyUrl({ ...DEFAULTS.MICROBITS_URL, path: ["api", "paywall-purchase"] }); const init = { method: "POST", headers: { Authorization: this._getAuthorizationHeader(), ["Content-Type"]: "application/json" }, body: JSON.stringify({ paywallId, userSessionId, contentUrl }) }; this._logParams("INFO", "createPaywallPurchase", { url, paywallId, userSessionId, contentUrl }); return this.fetch(url, init).then(this._responseLogger("createPaywallPurchase")).then((resp) => resp.json()).then((x) => x); } async getPaywallPurchase(paywallId, userSessionId) { const url = stringifyUrl({ ...DEFAULTS.MICROBITS_URL, path: ["api", "paywall-purchase"], query: { paywallId, userSessionId } }); const init = { method: "GET", headers: { Authorization: this._getAuthorizationHeader() } }; this._logParams("INFO", "getPaywallPurchase", { url, paywallId, userSessionId }); return this.fetch(url, init).then(this._responseLogger("getPaywallPurchase")).then((resp) => resp.json()).then((x) => x); } }; // src/request-handler.ts var getContentRequestHandler = (config) => async (ctx, requestUrl) => { const { microbits: client } = config; const url = parseUrl(requestUrl); let { userSessionId, shouldPurchase, ...otherQueryParams } = url.query ?? {}; if (Array.isArray(userSessionId)) { userSessionId = userSessionId[userSessionId.length - 1]; } const contentUrl = stringifyUrlPath(url); const paywallResponse = await client.getPaywallByContentUrl(contentUrl); if (paywallResponse.type === "error") { return { type: "error", error: { cause: "getPaywallByContentUrl", statusCode: 500, message: paywallResponse.error } }; } if (paywallResponse.ok === null) { return { type: "error", error: { cause: "getPaywallByContentUrl", statusCode: 404, message: "Paywall not found" } }; } console.log({ userSessionId }); if (!userSessionId) { const userSessionId2 = await Promise.resolve( config.getUserSessionIdCookie(ctx, DEFAULTS.USER_SESSION_COOKIE) ); if (userSessionId2 === void 0) { return { type: "error", error: { cause: "getUserSessionIdCookie", statusCode: 401, message: "No user session to authenticate" } }; } const authResponse = await client.isUserSessionAuthorizedForPaywall( userSessionId2, contentUrl, paywallResponse.ok.id ); if (authResponse.type === "error") { return { type: "error", error: { cause: "isUserSessionAuthorizedForPaywall", statusCode: 500, message: authResponse.error } }; } if (authResponse.ok === false) { return { type: "ok", ok: { type: "access-denied", hardPaywallUrl: stringifyUrl({ ...DEFAULTS.MICROBITS_URL, path: ["a", "merchant-paywall"], query: { merchantId: config.microbits.config.merchantId, contentUrl } }) } }; } return { type: "ok", ok: { type: "serve-content", contentUrl, userSessionId: userSessionId2 } }; } else { const userSessionResponse = await client.activateUserSession(userSessionId); if (userSessionResponse.type === "error") { return { type: "error", error: { cause: "activateUserSession", statusCode: 401, message: "Invalid user session ID" } }; } await Promise.resolve( config.setUserSessionIdCookie( ctx, DEFAULTS.USER_SESSION_COOKIE, userSessionResponse.ok.id ) ); if (shouldPurchase) { const purchaseResponse = await client.createPaywallPurchase( paywallResponse.ok.id, userSessionResponse.ok.id, contentUrl ); if (purchaseResponse.type === "error") { return { type: "error", error: { cause: "createPaywallPurchase", statusCode: 500, message: purchaseResponse.error } }; } } return { type: "ok", ok: { type: "content-redirect", statusCode: 301, contentUrl, query: otherQueryParams } }; } }; var getClientRequestHandler = (config) => async (ctx, requestUrl) => { const { microbits: client } = config; const url = parseUrl(requestUrl); const endpoint = url.path[url.path.length - 1]; let contentUrl = url.query && url.query["contentUrl"]; if (Array.isArray(contentUrl)) { contentUrl = contentUrl[contentUrl.length - 1]; } if (!contentUrl) { return { type: "error", error: 'Validation error: missing query paramter "contentUrl"' }; } if (endpoint === "get-paywall") { return client.getPaywallByContentUrl(contentUrl); } if (endpoint === "is-content-authorized") { const userSessionId = await Promise.resolve( config.getUserSessionIdCookie(ctx, DEFAULTS.USER_SESSION_COOKIE) ); if (!userSessionId) { return { type: "error", error: "No user session to authenticate" }; } return client.isUserSessionAuthorizedForPaywall(userSessionId, contentUrl); } return { type: "error", error: `Endpoint not found: ${stringifyUrlPath(url)}` }; }; // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { APIKeyRequiredError, DEFAULTS, MicrobitsClient, getClientRequestHandler, getContentRequestHandler });