@atproto/oauth-scopes
Version:
A library for manipulating and validating ATproto OAuth scopes in TypeScript.
153 lines • 5.98 kB
JavaScript
import { isNsid } from '../lib/nsid.js';
import { Parser } from '../lib/parser.js';
import { LexPermissionSyntax } from '../lib/syntax-lexicon.js';
import { ScopeStringSyntax } from '../lib/syntax-string.js';
import { isScopeStringFor, isScopeSyntaxFor, } from '../lib/syntax.js';
import { RepoPermission } from './repo-permission.js';
import { RpcPermission, isAtprotoDidRefAbsolute, } from './rpc-permission.js';
export { isNsid };
/**
* This is used to handle "include:" oauth scope values, used to include
* permissions from a lexicon defined permission set. Not being a resource
* permission, it does not implement `Matchable`.
*/
export class IncludeScope {
constructor(nsid, aud = undefined) {
this.nsid = nsid;
this.aud = aud;
}
toString() {
return IncludeScope.parser.format(this);
}
toPermissions(permissionSet) {
return Array.from(this.buildPermissions(permissionSet));
}
toScopes(permissionSet) {
return Array.from(this.buildPermissions(permissionSet), (p) => p.toString());
}
/**
* Converts an "include:" to the list of permissions it includes, based on the
* lexicon defined permission set.
*/
*buildPermissions(permissionSet) {
for (const lexPermission of permissionSet.permissions) {
const syntax = this.parseLexPermission(lexPermission);
if (!syntax)
continue;
const resourcePermission = toResourcePermission(syntax);
if (!resourcePermission)
continue;
if (this.isAllowedPermission(resourcePermission)) {
yield resourcePermission;
}
}
}
parseLexPermission(permission) {
// This function converts permissions listed in the permission set into
// their respective ScopeSyntax representations, handling special cases as
// needed.
if (isLexPermissionForResource(permission, 'repo')) {
return new LexPermissionSyntax(permission);
}
if (isLexPermissionForResource(permission, 'rpc')) {
// "rpc" permissions with a defined audience are not allowed in permission
// sets
if (permission.aud !== undefined && permission.aud !== '*') {
return null;
}
// "rpc" permissions can "inherit" their audience from "aud" param defined
// in the "include:<nsid>?aud=<audience>" scope the permission set was
// loaded from.
if (permission.inheritAud === true &&
permission.aud === undefined &&
this.aud !== undefined) {
const { inheritAud, ...rest } = permission;
return new LexPermissionSyntax({ aud: this.aud, ...rest });
}
return new LexPermissionSyntax(permission);
}
return null;
}
/**
* Verifies that a permission included through a lexicon permission set is
* allowed in the context of the `include:` scope. This basically checks that
* the permission is "under" the namespace authority of the `include:` scope,
* and that it only contains "repo:", "rpc:", or "blob:" permissions.
*/
isAllowedPermission(permission) {
if (permission instanceof RpcPermission) {
return permission.lxm.every(this.isParentAuthorityOf, this);
}
if (permission instanceof RepoPermission) {
return permission.collection.every(this.isParentAuthorityOf, this);
}
throw new TypeError(`Unexpected permission ${permission}`);
}
/**
* Verifies that a permission item's nsid is under the same authority as the
* nsid of the lexicon itself (which is the same as the nsid of the `include:`
* scope).
*/
isParentAuthorityOf(otherNsid) {
if (otherNsid === '*') {
return false;
}
const lexiconNsid = this.nsid;
const groupPrefixEnd = lexiconNsid.lastIndexOf('.');
// There should always be a dot, but since this is a security feature, let's
// be strict about it.
if (groupPrefixEnd === -1) {
throw new TypeError('Dot character (".") missing from lexicon NSID');
}
// Make sure that otherNsid is at least as long as the "group prefix"
if (groupPrefixEnd >= otherNsid.length - 1) {
return false;
}
// Make sure that the "otherNsid" starts with the group of the lexiconNsid,
// up to the dot itself. We check in reverse order as nsids tend to have
// long common prefixes.
for (let i = groupPrefixEnd; i >= 0; i--) {
if (lexiconNsid.charCodeAt(i) !== otherNsid.charCodeAt(i)) {
return false;
}
}
return true;
}
static { this.parser = new Parser('include', {
nsid: {
multiple: false,
required: true,
validate: isNsid,
},
aud: {
multiple: false,
required: false,
validate: isAtprotoDidRefAbsolute,
},
}, 'nsid'); }
static fromString(scope) {
if (!isScopeStringFor(scope, 'include'))
return null;
const syntax = ScopeStringSyntax.fromString(scope);
return IncludeScope.fromSyntax(syntax);
}
static fromSyntax(syntax) {
const result = IncludeScope.parser.parse(syntax);
if (!result)
return null;
return new IncludeScope(result.nsid, result.aud);
}
}
function toResourcePermission(syntax) {
if (isScopeSyntaxFor(syntax, 'repo')) {
return RepoPermission.fromSyntax(syntax);
}
if (isScopeSyntaxFor(syntax, 'rpc')) {
return RpcPermission.fromSyntax(syntax);
}
return null;
}
function isLexPermissionForResource(permission, type) {
return permission.resource === type;
}
//# sourceMappingURL=include-scope.js.map