Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export * from './lib/utils'
export * from './lib/section-categories'
export { CreditVocabTerm } from './lib/credit-roles'
export * from './lib/citeproc'
export * from './lib/access-control'
export * from './schema'
export { migrateFor } from './schema/migration/migrate'
export * from './transformer'
Expand Down
98 changes: 98 additions & 0 deletions src/lib/access-control.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
/*!
* © 2026 Atypon Systems LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { NodeType } from 'prosemirror-model'

import { CommentNode, ManuscriptActions, ManuscriptNode } from '../schema'

/**
* Attribute-Based Access Control (ABAC) policies for manuscript nodes.
* Used by manuscript-api to evaluate incoming ProseMirror steps against these policies.
*/
Comment thread
asouqi marked this conversation as resolved.

export type AccessContext = {
userId: string
actions: Record<ManuscriptActions, boolean>
}

/** Nodes with restricted access */
type ProtectedResources = {
comment: CommentNode
}

/** A rule that evaluates whether a subject can perform an operation on a specific node */
type NodeRule<T extends ManuscriptNode> = (
node: T,
context: AccessContext
) => boolean

/** Policy for a single node type */
type NodePolicy<T extends ManuscriptNode> = {
/** can subject add this node */
insert?: NodeRule<T>
/** can subject delete this node */
delete?: NodeRule<T>
/** can subject modify attributes:
* - As an object per-attribute rules
* - As a function one rule applied to all attribute changes*/
attrs?: Partial<Record<keyof T['attrs'], NodeRule<T>>> | NodeRule<T>
}

type NodesPolicy = {
[K in keyof ProtectedResources]?: NodePolicy<ProtectedResources[K]>
}

/**
* Access policy to manuscript content as node-based.
* @example add access policy to citation node based on ManuscriptActions which is resolved at manuscript-api by user role
* citation: {
* insert: (_, context) => context.actions.editCitationsAndRefs,
* delete: (_, context) => context.actions.editCitationsAndRefs,
* attrs: (_, context) => context.actions.editCitationsAndRefs,
* }
*/
const nodesPolicy: NodesPolicy = {
Comment thread
mbartenev-atypon marked this conversation as resolved.
comment: {
insert: (_, context) => context.actions.createComment,
delete: (node, context) => {
const isOwn = node.attrs.userID === context.userId
return isOwn
? context.actions.handleOwnComments
: context.actions.handleOthersComments
},
attrs: {
contents: (node, context) => {
const isOwn = node.attrs.userID === context.userId
return isOwn
? context.actions.handleOwnComments
: context.actions.handleOthersComments
},
resolved: (node, context) => {
const isOwn = node.attrs.userID === context.userId
return isOwn
? context.actions.resolveOwnComment
: context.actions.resolveOthersComment
},
},
Comment thread
asouqi marked this conversation as resolved.
},
}

export function getNodeAccessPolicy(nodeType: NodeType) {
if (nodeType.name in nodesPolicy) {
Comment thread
asouqi marked this conversation as resolved.
return nodesPolicy[
nodeType.name as keyof NodesPolicy
] as NodePolicy<ManuscriptNode>
}
}
4 changes: 4 additions & 0 deletions src/schema/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -238,3 +238,7 @@ export function isNodeOfType<T extends ManuscriptNode>(
): node is T {
return node.type === type
}

export type ExposedSlice<T, F> = T & {
insertAt: (pos: number, fragment: F) => T
}