@httpc/kit
Version:
httpc toolbox for building function-based API with minimal code and end-to-end type safety
360 lines (359 loc) • 13.5 kB
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;
};
var __exportStar = (this && this.__exportStar) || function(m, exports) {
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.PermissionsChecker = exports.Assertion = exports.AssertionBuilder = exports.Authorization = exports.AuthorizationBuilder = exports.InvalidClaim = exports.PermissionSerializer = exports.PermissionParser = exports.PermissionToken = void 0;
const Parser_1 = __importDefault(require("./Parser"));
exports.PermissionParser = Parser_1.default;
const Token = __importStar(require("./Token"));
exports.PermissionToken = Token;
const Serializer_1 = __importDefault(require("./Serializer"));
exports.PermissionSerializer = Serializer_1.default;
const assert_1 = __importDefault(require("assert"));
__exportStar(require("./Parser"), exports);
__exportStar(require("./Serializer"), exports);
__exportStar(require("./model"), exports);
class InvalidClaim extends Error {
constructor(claim, message) {
super(message || "Invalid claim");
this.claim = claim;
}
}
exports.InvalidClaim = InvalidClaim;
class AuthorizationBuilder {
constructor(extend) {
this._claims = [];
if (extend) {
this.add(extend);
}
}
add(claim) {
if (typeof claim === "string") {
Parser_1.default.parseAuthorization(claim).forEach(x => this.add(x));
return this;
}
if (Array.isArray(claim)) {
claim.forEach(x => this.add(x));
return this;
}
if (claim instanceof AuthorizationBuilder) {
claim._claims.forEach(x => this.add(x));
return this;
}
if (claim instanceof Authorization) {
claim.claims.forEach(x => this.add(x));
return this;
}
this._claims.push({
token: Array.isArray(claim.token) ? claim.token.slice() : claim.token,
scope: Array.isArray(claim.scope) ? claim.scope.slice() : claim.scope,
});
return this;
}
build() {
return new Authorization(this._claims);
}
}
exports.AuthorizationBuilder = AuthorizationBuilder;
class Authorization {
constructor(claims) {
this.claims = claims;
}
*[Symbol.iterator]() {
yield* this.claims;
}
merge(auth) {
return new AuthorizationBuilder(this).add(auth).build();
}
toString() {
return Serializer_1.default.serialize(this);
}
static parse(claims) {
return new AuthorizationBuilder(claims).build();
}
}
exports.Authorization = Authorization;
class AssertionBuilder {
constructor(extend) {
this._claims = [];
if (extend) {
this.add(extend);
}
}
add(claim) {
if (typeof claim === "string") {
Parser_1.default.parseAssertion(claim).forEach(x => this.add(x));
return this;
}
if (Array.isArray(claim)) {
claim.forEach(x => this.add(x));
return this;
}
if (claim instanceof AssertionBuilder) {
claim._claims.forEach(x => this.add(x));
return this;
}
if (claim instanceof Assertion) {
claim.claims.forEach(x => this.add(x));
return this;
}
this._claims.push({
token: Array.isArray(claim.token) ? claim.token.slice() : claim.token,
scope: Array.isArray(claim.scope) ? claim.scope.slice() : claim.scope,
negative: claim.negative,
});
return this;
}
build() {
return new Assertion(this._claims);
}
}
exports.AssertionBuilder = AssertionBuilder;
class Assertion {
constructor(claims) {
this.claims = claims;
}
*[Symbol.iterator]() {
yield* this.claims;
}
toString() {
return Serializer_1.default.serialize(this);
}
test(auth) {
if (this.claims.length === 0)
return { success: true };
for (const assertion of this.claims) {
let isPass = false;
for (const claim of auth) {
const isTokenPass = Token.match(claim.token, assertion.token);
const isScopePass = assertion.scope ? claim.scope ? Token.match(claim.scope, assertion.scope) : false : true;
if (isTokenPass && isScopePass) {
isPass = true;
break;
}
}
isPass = isPass !== assertion.negative;
if (!isPass) {
return { success: false, failed: assertion };
}
}
return { success: true };
}
static parse(claims) {
return new AssertionBuilder(claims).build();
}
}
exports.Assertion = Assertion;
class PermissionsChecker {
constructor(options) {
this._model = options?.model;
this._cache = options?.cache === true ? new Map() : options?.cache || undefined;
}
can(authorization, assertion) {
return this.test(authorization, assertion).success;
}
test(authorization, assertion) {
[authorization] = this._getAuthorization(authorization);
authorization = this.validate(authorization);
[assertion] = this._getAssertion(assertion);
assertion = this.validate(assertion);
if (assertion.claims.length === 0) {
return { success: true };
}
const model = this._model;
if (!model) {
return assertion.test(authorization);
}
for (const assertClaim of assertion) {
let isPass = false;
for (const authClaim of authorization) {
const isTokenPass = tokenMatch(authClaim.token, assertClaim.token);
const isScopePass = assertClaim.scope ? authClaim.scope ? Token.match(authClaim.scope, assertClaim.scope) : false : true;
if (isTokenPass && isScopePass) {
isPass = true;
break;
}
}
isPass = isPass !== assertClaim.negative;
if (!isPass) {
return { success: false, failed: assertClaim };
}
}
return { success: true };
function tokenMatch(source, target) {
(0, assert_1.default)(model, "model");
if (Token.match(source, target)) {
return true;
}
const def = model.find(source, "exact");
// must be defined, because it already passed validation, if error -> this is a bug
(0, assert_1.default)(def, "token definition not found");
if (def.includes && def.includes.length > 0) {
for (const include of def.includes) {
if (tokenMatch(Parser_1.default.parseToken(include), target)) {
return true;
}
}
}
return false;
}
}
supports(authorization) {
const model = this._model;
if (!model)
return true;
[authorization] = this._getAuthorization(authorization);
for (const { token } of authorization) {
if (!model.find(token, "exact")) {
return false;
}
}
return true;
}
parse(what, value) {
const model = this._model;
let [instance, validated] = what === "assertion"
? this._getAssertion(value)
: this._getAuthorization(value);
if (!model || validated) {
return instance;
}
instance = this.validate(instance);
if (this._cache) {
const key = this._getCacheKey(what, value);
this._cache.set(key, [instance, true]);
}
return instance;
}
validate(what) {
const model = this._model;
if (!model)
return what;
for (const claim of what) {
if (!model.find(claim.token, "exact")) {
throw new InvalidClaim(what instanceof Assertion
? Serializer_1.default.serializeAssertionClaim(claim)
: Serializer_1.default.serializeAuthorizationClaim(claim));
}
}
return this.consolidate(what);
}
consolidate(what) {
const model = this._model;
// try to reduce the instance
// - alias substitution (model only)
// - eliminating duplicate
if (what instanceof Authorization) {
let isConsolidated = false;
let claims = [];
for (let claim of what) {
// alias substitution
const deAliasedToken = replaceAlias(claim);
if (deAliasedToken) {
isConsolidated = true;
claim = {
token: deAliasedToken,
scope: claim.scope,
};
}
// look for already present
const existing = claims.find(x => x.scope === claim.scope && Token.equals(x.token, claim.token));
if (existing) {
isConsolidated = true;
continue;
}
claims.push(claim);
}
return isConsolidated ? new Authorization(claims) : what;
}
else if (what instanceof Assertion) {
let isConsolidated = false;
let claims = [];
for (let claim of what) {
// alias substitution
const deAliasedToken = replaceAlias(claim);
if (deAliasedToken) {
isConsolidated = true;
claim = {
token: deAliasedToken,
scope: claim.scope,
negative: claim.negative,
};
}
// look for already present
const existing = claims.find(x => x.scope === claim.scope && Token.equals(x.token, claim.token) && !!x.negative === !!claim.negative);
if (existing) {
isConsolidated = true;
continue;
}
claims.push(claim);
}
return isConsolidated ? new Assertion(claims) : what;
}
throw new Error("Invalid param: authorization or assertion required");
function replaceAlias(claim) {
if (!model)
return;
// find works with alias
const def = model.find(claim.token, "exact");
if (!def) {
throw new InvalidClaim(Serializer_1.default.serializeTokenClaim(claim.token));
}
// if token are different, it means it matched an alias
if (!Token.equals(claim.token, def.fullToken)) {
return Token.simplify(def.fullToken);
}
}
}
_getAuthorization(authorization) {
return this._getCachedOrCreate(authorization, "authorization", authorization => Authorization.parse(authorization));
}
_getAssertion(assertion) {
return this._getCachedOrCreate(assertion, "assertion", assertion => Assertion.parse(assertion));
}
_getCachedOrCreate(value, prefix, factory) {
if (typeof value !== "string") {
return [value, false];
}
if (!this._cache) {
return [factory(value), false];
}
const key = this._getCacheKey(prefix, value);
let item = this._cache.get(key);
if (!item) {
this._cache.set(key, item = [factory(value), false]);
}
return item;
}
_getCacheKey(prefix, value) {
return `${prefix}/${value}`;
}
}
exports.PermissionsChecker = PermissionsChecker;