@microbitsclub/microbits-client
Version:
Microbits API client
636 lines (629 loc) • 17.6 kB
JavaScript
"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
});