diff --git a/packages/superdoc/src/core/types/index.ts b/packages/superdoc/src/core/types/index.ts index 2580c44bb8..41f1fe5029 100644 --- a/packages/superdoc/src/core/types/index.ts +++ b/packages/superdoc/src/core/types/index.ts @@ -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 @@ -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`. */ diff --git a/packages/superdoc/src/public/index.ts b/packages/superdoc/src/public/index.ts index 8d918d087d..1c8699774c 100644 --- a/packages/superdoc/src/public/index.ts +++ b/packages/superdoc/src/public/index.ts @@ -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'; diff --git a/tests/consumer-typecheck/snapshots/superdoc-root-classification.json b/tests/consumer-typecheck/snapshots/superdoc-root-classification.json index d3e2fa5a76..383b8236d3 100644 --- a/tests/consumer-typecheck/snapshots/superdoc-root-classification.json +++ b/tests/consumer-typecheck/snapshots/superdoc-root-classification.json @@ -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 } @@ -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", diff --git a/tests/consumer-typecheck/snapshots/superdoc-root-classification.md b/tests/consumer-typecheck/snapshots/superdoc-root-classification.md index 4179c95eec..32d0f654e3 100644 --- a/tests/consumer-typecheck/snapshots/superdoc-root-classification.md +++ b/tests/consumer-typecheck/snapshots/superdoc-root-classification.md @@ -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 | |---|---|---|---| @@ -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. | diff --git a/tests/consumer-typecheck/snapshots/superdoc-root-exports.json b/tests/consumer-typecheck/snapshots/superdoc-root-exports.json index 5ee156da06..f900446ee9 100644 --- a/tests/consumer-typecheck/snapshots/superdoc-root-exports.json +++ b/tests/consumer-typecheck/snapshots/superdoc-root-exports.json @@ -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": { @@ -125,6 +125,7 @@ "PasswordPromptRenderContext", "PasswordPromptResolution", "PermissionParams", + "PermissionResolverParams", "PositionHit", "PresenceOptions", "PresentationEditor", @@ -344,6 +345,7 @@ "PasswordPromptRenderContext", "PasswordPromptResolution", "PermissionParams", + "PermissionResolverParams", "PositionHit", "PresenceOptions", "PresentationEditor", @@ -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": { @@ -662,6 +664,7 @@ "PasswordPromptRenderContext", "PasswordPromptResolution", "PermissionParams", + "PermissionResolverParams", "PositionHit", "PresenceOptions", "PresentationEditorOptions", diff --git a/tests/consumer-typecheck/snapshots/superdoc-root-exports.md b/tests/consumer-typecheck/snapshots/superdoc-root-exports.md index bd5e282847..49225b0f75 100644 --- a/tests/consumer-typecheck/snapshots/superdoc-root-exports.md +++ b/tests/consumer-typecheck/snapshots/superdoc-root-exports.md @@ -1,17 +1,17 @@ # 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 @@ -19,7 +19,7 @@ Source: packed and installed `tests/consumer-typecheck/node_modules/superdoc` - 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) @@ -124,6 +124,7 @@ Source: packed and installed `tests/consumer-typecheck/node_modules/superdoc` - `PasswordPromptRenderContext` - `PasswordPromptResolution` - `PermissionParams` +- `PermissionResolverParams` - `PositionHit` - `PresenceOptions` - `PresentationEditorOptions` @@ -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 | | @@ -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 | | @@ -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 | ✓ | @@ -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 | ✓ | diff --git a/tests/consumer-typecheck/src/all-public-types.ts b/tests/consumer-typecheck/src/all-public-types.ts index 38e0938f2e..f53ac804d7 100644 --- a/tests/consumer-typecheck/src/all-public-types.ts +++ b/tests/consumer-typecheck/src/all-public-types.ts @@ -124,6 +124,7 @@ import type { PasswordPromptRenderContext, PasswordPromptResolution, PermissionParams, + PermissionResolverParams, PositionHit, PresenceOptions, PresentationEditorOptions, @@ -305,6 +306,7 @@ const _real_PasswordPromptHandle: AssertNotAny = true; const _real_PasswordPromptRenderContext: AssertNotAny = true; const _real_PasswordPromptResolution: AssertNotAny = true; const _real_PermissionParams: AssertNotAny = true; +const _real_PermissionResolverParams: AssertNotAny = true; const _real_PositionHit: AssertNotAny = true; const _real_PresenceOptions: AssertNotAny = true; const _real_PresentationEditorOptions: AssertNotAny = true; diff --git a/tests/consumer-typecheck/src/permission-resolver-params-apis.ts b/tests/consumer-typecheck/src/permission-resolver-params-apis.ts new file mode 100644 index 0000000000..26d8794a95 --- /dev/null +++ b/tests/consumer-typecheck/src/permission-resolver-params-apis.ts @@ -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>[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 = (() => T extends A ? 1 : 2) extends () => T extends B ? 1 : 2 ? true : false; +type AssertEqual = Equal extends true ? true : never; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type ParamOf any) | undefined> = Parameters>[0]; + +// ─── Config.permissionResolver ────────────────────────────────────── +const _topLevelResolverParamsOk: AssertEqual, PermissionResolverParams> = true; + +// ─── Modules.comments.permissionResolver ──────────────────────────── +// Modules.comments is `false | object`. Narrow to the object form +// before pulling the resolver slot out. +type CommentsModule = Exclude, false>; +const _commentsResolverParamsOk: AssertEqual< + ParamOf, + 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 = (params) => { + // defaultDecision is now visible on the typed payload. + return params.defaultDecision; +}; +void _resolver; + +void [_topLevelResolverParamsOk, _commentsResolverParamsOk];