Skip to content
Merged
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
66 changes: 54 additions & 12 deletions packages/superdoc/src/core/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1007,16 +1007,57 @@ export interface FindReplaceConfig {
// Modules
// ---------------------------------------------------------------------------

/** Permission resolver shared by the top-level Config and the comments module. */
type PermissionResolverParams = {
/**
* Payload passed to a permission resolver callback. SuperDoc invokes
* the resolver when a consumer registers one via
* `Config.permissionResolver` or `Modules.comments.permissionResolver`,
* forwarding the in-flight check so the resolver can decide whether
* to override the built-in policy.
*
* Returning `boolean` from the resolver overrides the default;
* returning `undefined` (or any non-boolean) falls through to
* `defaultDecision`, which the resolver receives so it can mirror or
* branch off the built-in policy without re-deriving it.
*
* `comment` and `trackedChange` are typed as `object | null` because
* consumer comment / tracked-change shapes vary; resolvers that read
* fields on those payloads should narrow before use.
*
* Distinct from `CanPerformPermissionParams`, which is the input
* shape consumers pass _to_ `SuperDoc#canPerformPermission`. That
* input becomes part of this resolver payload after SuperDoc resolves
* `currentUser`, `superdoc`, and `defaultDecision`.
*/
export interface PermissionResolverParams {
/** The permission key being checked (e.g. `'comment.create'`). */
permission: string;
role?: string;
isInternal?: boolean;
comment?: object | null;
trackedChange?: object | null;
currentUser?: User | null;
superdoc?: SuperDoc | null;
};
/**
* The effective role (consumer-supplied or falling back to
* `Config.role`). The key is always present on the payload; the
* value is `undefined` when `Config.role` was never set.
*/
role: string | undefined;
/**
* The effective internal/external flag (consumer-supplied or
* `Config.isInternal`). The key is always present; the value is
* `undefined` when `Config.isInternal` was never set.
*/
isInternal: boolean | undefined;
/**
* What the built-in policy would return if the resolver does not
* override. Resolvers can return this value to defer to the
* default, or branch off it.
*/
defaultDecision: boolean;
/** The comment object being acted on, if any. Shape is consumer-defined. */
comment: object | null;
/** The tracked-change payload (as emitted by the editor) being acted on, if any. */
trackedChange: object | null;
/** The active user performing the action; resolved from `Config.user`. */
currentUser: User | null;
/** The SuperDoc instance the check ran against. */
superdoc: SuperDoc | null;
}

/**
* Input shape for `SuperDoc#canPerformPermission`. All fields are
Expand All @@ -1026,9 +1067,10 @@ type PermissionResolverParams = {
* because the runtime forwards the full payload to the resolver
* context, and consumer comment / tracked-change shapes vary; the
* named fields below are the ones the method itself reads. Distinct
* from the non-exported `PermissionResolverParams` helper, which
* models the resolver callback payload with resolved `currentUser`
* and `superdoc` context attached.
* from `PermissionResolverParams`, which is the exported resolver
* callback payload SuperDoc passes to configured permission resolvers
* (with resolved `currentUser`, `superdoc`, and `defaultDecision`
* context attached).
*/
export interface CanPerformPermissionParams {
/** The permission key to check (e.g. `'comment.create'`). Required at runtime; omitting returns `false`. */
Expand Down
1 change: 1 addition & 0 deletions packages/superdoc/src/public/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ export type { PasswordPromptContext } from '../core/types/index.js';
export type { PasswordPromptHandle } from '../core/types/index.js';
export type { PasswordPromptRenderContext } from '../core/types/index.js';
export type { PasswordPromptResolution } from '../core/types/index.js';
export type { PermissionResolverParams } from '../core/types/index.js';
export type { ResolvedFindReplaceTexts } from '../core/types/index.js';
export type { ResolvedPasswordPromptTexts } from '../core/types/index.js';
export type { SearchMatch } from '../core/types/index.js';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
{
"generatedAt": "2026-05-19T11:33:50.546Z",
"summary": {
"total": 213,
"total": 214,
"byBucket": {
"legacy-root": 60,
"internal-candidate": 8,
"supported-root": 145
"supported-root": 146
},
"byConfidence": {
"high": 110,
"high": 111,
"medium": 101,
"low": 2
}
Expand Down Expand Up @@ -1235,6 +1235,17 @@
"inEsm": false,
"inCjs": false
},
{
"name": "PermissionResolverParams",
"bucket": "supported-root",
"rationale": "Payload passed to permission resolver callbacks registered via Config.permissionResolver or Modules.comments.permissionResolver; promoted from a non-exported helper to a named public type so resolver authors can import the contract.",
"confidence": "high",
"source": "config-supported",
"inDts": true,
"inDcts": true,
"inEsm": false,
"inCjs": false
},
{
"name": "PositionHit",
"bucket": "legacy-root",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,16 @@ Input: tests/consumer-typecheck/snapshots/superdoc-root-exports.json (205 names,

| Bucket | Count |
|---|---|
| supported-root | 145 |
| supported-root | 146 |
| legacy-root | 60 |
| move-to-subpath | 0 |
| internal-candidate | 8 |
| NEEDS-REVIEW | 0 |
| **total** | **213** |
| **total** | **214** |

Confidence: high=110, medium=101, needs-review=0.
Confidence: high=111, medium=101, needs-review=0.

## supported-root (145)
## supported-root (146)

| Name | Confidence | Source | Rationale |
|---|---|---|---|
Expand Down Expand Up @@ -90,6 +90,7 @@ Confidence: high=110, medium=101, needs-review=0.
| `PasswordPromptRenderContext` | medium | password-prompt | PasswordPrompt surface API type. Public. |
| `PasswordPromptResolution` | medium | password-prompt | PasswordPrompt surface API type. Public. |
| `PermissionParams` | medium | core | Customer-facing core API type or runtime export. Type-reachable through documented config / callback / event / method surfaces; runtime exports are documented utilities. |
| `PermissionResolverParams` | high | config-supported | Payload passed to permission resolver callbacks registered via Config.permissionResolver or Modules.comments.permissionResolver; promoted from a non-exported helper to a named public type so resolver authors can import the contract. |
| `ProofingCapabilities` | medium | proofing | Proofing module type. Public for proofing-provider integrations. |
| `ProofingCheckRequest` | medium | proofing | Proofing module type. Public for proofing-provider integrations. |
| `ProofingCheckResult` | medium | proofing | Proofing module type. Public for proofing-provider integrations. |
Expand Down
11 changes: 7 additions & 4 deletions tests/consumer-typecheck/snapshots/superdoc-root-exports.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"generatedAt": "2026-05-26T10:45:50.855Z",
"generatedAt": "2026-05-26T11:53:56.060Z",
"ticket": "SD-3212 PR A0",
"package": "superdoc",
"rootExport": {
Expand Down Expand Up @@ -125,6 +125,7 @@
"PasswordPromptRenderContext",
"PasswordPromptResolution",
"PermissionParams",
"PermissionResolverParams",
"PositionHit",
"PresenceOptions",
"PresentationEditor",
Expand Down Expand Up @@ -344,6 +345,7 @@
"PasswordPromptRenderContext",
"PasswordPromptResolution",
"PermissionParams",
"PermissionResolverParams",
"PositionHit",
"PresenceOptions",
"PresentationEditor",
Expand Down Expand Up @@ -545,11 +547,11 @@
}
},
"counts": {
"types.import": 213,
"types.require": 213,
"types.import": 214,
"types.require": 214,
"import": 41,
"require": 41,
"union": 213
"union": 214
},
"divergences": {
"typesImportVsRequire": {
Expand Down Expand Up @@ -662,6 +664,7 @@
"PasswordPromptRenderContext",
"PasswordPromptResolution",
"PermissionParams",
"PermissionResolverParams",
"PositionHit",
"PresenceOptions",
"PresentationEditorOptions",
Expand Down
18 changes: 10 additions & 8 deletions tests/consumer-typecheck/snapshots/superdoc-root-exports.md
Original file line number Diff line number Diff line change
@@ -1,25 +1,25 @@
# superdoc root export inventory (SD-3212 PR A0)

Generated: 2026-05-26T10:45:50.855Z
Generated: 2026-05-26T11:53:56.060Z
Source: packed and installed `tests/consumer-typecheck/node_modules/superdoc`

## Counts

| Source | Path | Count |
|---|---|---|
| types.import | `./dist/superdoc/src/public/index.d.ts` | 213 |
| types.require | `./dist/superdoc/src/public/index.d.cts` | 213 |
| types.import | `./dist/superdoc/src/public/index.d.ts` | 214 |
| types.require | `./dist/superdoc/src/public/index.d.cts` | 214 |
| import | `./dist/superdoc.es.js` | 41 |
| require | `./dist/superdoc.cjs` | 41 |
| **union** | | **213** |
| **union** | | **214** |

## Divergences

- types.import only (not in types.require): 0
- types.require only (not in types.import): 0
- ESM only (not in CJS): 0
- CJS only (not in ESM): 0
- typed but no runtime export (phantom risk): 172
- typed but no runtime export (phantom risk): 173
- runtime export but not typed (silent shadow on root): 0

### Type-only names (no runtime)
Expand Down Expand Up @@ -124,6 +124,7 @@ Source: packed and installed `tests/consumer-typecheck/node_modules/superdoc`
- `PasswordPromptRenderContext`
- `PasswordPromptResolution`
- `PermissionParams`
- `PermissionResolverParams`
- `PositionHit`
- `PresenceOptions`
- `PresentationEditorOptions`
Expand Down Expand Up @@ -228,7 +229,7 @@ Source: packed and installed `tests/consumer-typecheck/node_modules/superdoc`
| `CommentsPayload` | ✓ | ✓ | | | 2 | ✓ | 0 | 0 | 0 | |
| `CommentsPluginKey` | ✓ | ✓ | ✓ | ✓ | 2 | | 0 | 0 | 1 | ✓ |
| `CommentsType` | ✓ | ✓ | | | 1 | ✓ | 0 | 0 | 0 | |
| `Config` | ✓ | ✓ | | | 7 | ✓ | 2 | 1 | 2 | ✓ |
| `Config` | ✓ | ✓ | | | 8 | ✓ | 2 | 1 | 2 | ✓ |
| `ContextMenu` | ✓ | ✓ | ✓ | ✓ | 1 | | 7 | 0 | 31 | |
| `ContextMenuConfig` | ✓ | ✓ | | | 2 | ✓ | 0 | 0 | 0 | |
| `ContextMenuContext` | ✓ | ✓ | | | 2 | ✓ | 0 | 0 | 0 | |
Expand All @@ -253,7 +254,7 @@ Source: packed and installed `tests/consumer-typecheck/node_modules/superdoc`
| `EditorState` | ✓ | ✓ | | | 4 | ✓ | 7 | 0 | 1 | ✓ |
| `EditorSurface` | ✓ | ✓ | | | 1 | ✓ | 0 | 0 | 0 | |
| `EditorTransactionEvent` | ✓ | ✓ | | | 1 | ✓ | 0 | 0 | 0 | |
| `EditorUpdateEvent` | ✓ | ✓ | | | 1 | ✓ | 0 | 0 | 0 | |
| `EditorUpdateEvent` | ✓ | ✓ | | | 2 | ✓ | 0 | 0 | 0 | |
| `EditorView` | ✓ | ✓ | | | 4 | ✓ | 2 | 0 | 0 | ✓ |
| `EntityAddress` | ✓ | ✓ | | | 2 | ✓ | 276 | 0 | 8 | |
| `ExportDocxParams` | ✓ | ✓ | | | 2 | ✓ | 0 | 0 | 0 | |
Expand Down Expand Up @@ -293,7 +294,7 @@ Source: packed and installed `tests/consumer-typecheck/node_modules/superdoc`
| `LinkPopoverResolver` | ✓ | ✓ | | | 1 | ✓ | 0 | 0 | 0 | |
| `ListDefinitionsPayload` | ✓ | ✓ | | | 2 | ✓ | 0 | 0 | 0 | |
| `Measure` | ✓ | ✓ | | | 3 | ✓ | 0 | 0 | 1 | |
| `Modules` | ✓ | ✓ | | | 1 | ✓ | 4 | 0 | 0 | |
| `Modules` | ✓ | ✓ | | | 2 | ✓ | 4 | 0 | 0 | |
| `NavigableAddress` | ✓ | ✓ | | | 2 | ✓ | 0 | 0 | 0 | |
| `OpenOptions` | ✓ | ✓ | | | 3 | ✓ | 1 | 0 | 0 | |
| `PDF` | ✓ | ✓ | ✓ | ✓ | 2 | | 35 | 0 | 1 | ✓ |
Expand All @@ -312,6 +313,7 @@ Source: packed and installed `tests/consumer-typecheck/node_modules/superdoc`
| `PasswordPromptRenderContext` | ✓ | ✓ | | | 1 | ✓ | 2 | 0 | 0 | |
| `PasswordPromptResolution` | ✓ | ✓ | | | 1 | ✓ | 1 | 0 | 0 | |
| `PermissionParams` | ✓ | ✓ | | | 1 | ✓ | 0 | 0 | 0 | |
| `PermissionResolverParams` | ✓ | ✓ | | | 2 | | 0 | 0 | 0 | |
| `PositionHit` | ✓ | ✓ | | | 3 | ✓ | 0 | 0 | 0 | |
| `PresenceOptions` | ✓ | ✓ | | | 3 | ✓ | 0 | 0 | 0 | |
| `PresentationEditor` | ✓ | ✓ | ✓ | ✓ | 3 | | 0 | 0 | 40 | ✓ |
Expand Down
2 changes: 2 additions & 0 deletions tests/consumer-typecheck/src/all-public-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ import type {
PasswordPromptRenderContext,
PasswordPromptResolution,
PermissionParams,
PermissionResolverParams,
PositionHit,
PresenceOptions,
PresentationEditorOptions,
Expand Down Expand Up @@ -305,6 +306,7 @@ const _real_PasswordPromptHandle: AssertNotAny<PasswordPromptHandle> = true;
const _real_PasswordPromptRenderContext: AssertNotAny<PasswordPromptRenderContext> = true;
const _real_PasswordPromptResolution: AssertNotAny<PasswordPromptResolution> = true;
const _real_PermissionParams: AssertNotAny<PermissionParams> = true;
const _real_PermissionResolverParams: AssertNotAny<PermissionResolverParams> = true;
const _real_PositionHit: AssertNotAny<PositionHit> = true;
const _real_PresenceOptions: AssertNotAny<PresenceOptions> = true;
const _real_PresentationEditorOptions: AssertNotAny<PresentationEditorOptions> = true;
Expand Down
76 changes: 76 additions & 0 deletions tests/consumer-typecheck/src/permission-resolver-params-apis.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/**
* Consumer typecheck: `PermissionResolverParams` export and resolver
* callback contracts.
*
* Locks two things against the emitted `.d.ts`:
*
* 1. `PermissionResolverParams` is reachable as a named public type
* from `superdoc` (was a non-exported helper before this PR).
* Resolver authors can now `import type { PermissionResolverParams }
* from 'superdoc'` and write `(params: PermissionResolverParams)`
* explicitly, instead of relying on inferred shape from
* `Parameters<NonNullable<Config['permissionResolver']>>[0]`.
*
* 2. Both `Config.permissionResolver` and
* `Modules.comments.permissionResolver` use the named type, with
* identical signatures. Drift between the two resolver slots
* would slip past method-coverage fixtures (callbacks are not
* gate-tracked) but fails here on `AssertEqual`.
*
* Promoted as part of the same DX initiative that exported
* `CanPerformPermissionParams` (the consumer input shape). The two
* types overlap on `permission` / `role` / `isInternal` / `comment` /
* `trackedChange` but serve opposite directions of the flow:
*
* - `CanPerformPermissionParams` = what consumers pass INTO
* `SuperDoc#canPerformPermission`.
* - `PermissionResolverParams` = what consumer resolvers RECEIVE,
* enriched with `defaultDecision`, `currentUser`, and `superdoc`.
*
* One real omission landed alongside this export: the runtime always
* forwards `defaultDecision: boolean` to the resolver, but the old
* non-exported helper omitted it. Adding it lets resolvers branch off
* (or defer to) the built-in policy without re-deriving it. Existing
* resolvers that didn't read `defaultDecision` are unaffected.
*/
import type { Config, Modules, PermissionResolverParams } from 'superdoc';

type Equal<A, B> = (<T>() => T extends A ? 1 : 2) extends <T>() => T extends B ? 1 : 2 ? true : false;
type AssertEqual<A, B> = Equal<A, B> extends true ? true : never;

// eslint-disable-next-line @typescript-eslint/no-explicit-any
type ParamOf<F extends ((...args: any) => any) | undefined> = Parameters<NonNullable<F>>[0];

// ─── Config.permissionResolver ──────────────────────────────────────
const _topLevelResolverParamsOk: AssertEqual<ParamOf<Config['permissionResolver']>, PermissionResolverParams> = true;

// ─── Modules.comments.permissionResolver ────────────────────────────
// Modules.comments is `false | object`. Narrow to the object form
// before pulling the resolver slot out.
type CommentsModule = Exclude<NonNullable<Modules['comments']>, false>;
const _commentsResolverParamsOk: AssertEqual<
ParamOf<CommentsModule['permissionResolver']>,
PermissionResolverParams
> = true;

// ─── Resolver authors construct payloads against the named type ─────
const sample: PermissionResolverParams = {
permission: 'comment.create',
role: 'editor',
isInternal: true,
defaultDecision: true,
comment: { id: 'c-1' },
trackedChange: { id: 'tc-1' },
currentUser: { name: 'A', email: 'a@x.com' },
superdoc: null,
};
void sample;

// Returning `boolean | undefined` matches what isAllowed accepts.
const _resolver: NonNullable<Config['permissionResolver']> = (params) => {
// defaultDecision is now visible on the typed payload.
return params.defaultDecision;
};
void _resolver;

void [_topLevelResolverParamsOk, _commentsResolverParamsOk];
Loading