From 7972cb3e0722b39477723878bc1ab911683463a8 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 27 Jul 2026 13:16:39 +0000 Subject: [PATCH 1/3] feat(spec): publish ISecurityService and enforce it at both ends MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `security` service registers seven cross-package methods but had no contract in `@objectstack/spec/contracts`. Consumers duck-typed it and each invented its own fallback for a missing method or an "empty" answer. Adds `ISecurityService` and types BOTH ends against it, so the surface is enforced rather than declared: plugin-security assigns its registration to the interface (a renamed, dropped, or re-typed method fails that build), and the REST layer resolves the service as `Partial` (call sites must keep feature-detecting rather than assume the full surface). The contract states the one thing consumers cannot guess — the methods do not share a failure convention. `getReadFilter` fails CLOSED (a failure is a deny filter, never `undefined`; `undefined` means "no row restriction" and nothing else). `getReadableFields` fails SOFT, and its two empty answers are opposites: `undefined` is "no answer, use your own projection", `[]` is authoritative "no field is readable". Typing the producer immediately caught one real discrepancy, fixed here: getReadFilter declared `Record | null | undefined` while every return path yields a filter or `undefined` (`filter ?? undefined` normalizes the null away). Dropping the dead `| null` leaves "no restriction" with exactly one representation. Type-level only. Tests: spec 6665, plugin-security 593, rest 399 — all passing; the three packages build clean (CJS/ESM/DTS). Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01KZ2BGusRo58ZW8FMkDhGTb --- .changeset/security-service-contract.md | 36 ++++ .../plugin-security/src/security-plugin.ts | 19 +- packages/rest/src/rest-server.ts | 10 +- packages/spec/src/contracts/index.ts | 1 + .../src/contracts/security-service.test.ts | 98 +++++++++ .../spec/src/contracts/security-service.ts | 198 ++++++++++++++++++ 6 files changed, 356 insertions(+), 6 deletions(-) create mode 100644 .changeset/security-service-contract.md create mode 100644 packages/spec/src/contracts/security-service.test.ts create mode 100644 packages/spec/src/contracts/security-service.ts diff --git a/.changeset/security-service-contract.md b/.changeset/security-service-contract.md new file mode 100644 index 000000000..a47873596 --- /dev/null +++ b/.changeset/security-service-contract.md @@ -0,0 +1,36 @@ +--- +"@objectstack/spec": minor +"@objectstack/plugin-security": patch +"@objectstack/rest": patch +--- + +feat(spec): publish `ISecurityService` — the `security` service surface becomes an enforced contract + +The `security` service registers seven cross-package methods (`getReadFilter`, +`getReadableFields`, `resolvePermissionSetNames`, `explain`, and the three +audience-binding suggestion calls) but had no contract in +`@objectstack/spec/contracts`. Consumers duck-typed it, and each one invented its +own fallback for a missing method or an "empty" answer — with more consumers +arriving, that is a drift surface. + +`ISecurityService` now documents the surface, and both ends are typed against it +so it is **enforced rather than declared**: `plugin-security` assigns its +registration to `ISecurityService` (a renamed, dropped, or re-typed method fails +that build), and the REST layer resolves the service as a `Partial` +(so call sites must keep feature-detecting instead of assuming the full surface). + +The contract makes explicit the one thing consumers cannot guess — that the +methods do **not** share a failure convention: + +- `getReadFilter` fails **CLOSED**: a resolution failure yields a deny filter + matching zero rows, never `undefined`. `undefined` means "no row restriction", + and nothing else. +- `getReadableFields` fails **SOFT**: `undefined` means "no answer, use your own + projection", while `[]` is authoritative and means "no field is readable" — + opposite instructions that a consumer must not conflate. + +Typing the producer immediately caught one real discrepancy, fixed here: +`getReadFilter` declared `Promise | null | undefined>` +while every return path yields a filter or `undefined` (`filter ?? undefined` +normalizes the null away). The dead `| null` is removed, so "no restriction" has +exactly one representation. Type-level only — no runtime behaviour changes. diff --git a/packages/plugins/plugin-security/src/security-plugin.ts b/packages/plugins/plugin-security/src/security-plugin.ts index e7e997c0f..a697c9ca9 100644 --- a/packages/plugins/plugin-security/src/security-plugin.ts +++ b/packages/plugins/plugin-security/src/security-plugin.ts @@ -47,6 +47,7 @@ import { RLS_MEMBERSHIP_RESOLVER_SERVICE, RESERVED_RLS_MEMBERSHIP_KEYS, type IRlsMembershipResolver, + type ISecurityService, } from '@objectstack/spec/contracts'; import { matchesFilterCondition } from '@objectstack/formula'; import { FieldMasker } from './field-masker.js'; @@ -611,7 +612,11 @@ export class SecurityPlugin implements Plugin { resolveSets: (context: any) => this.resolvePermissionSetsForContext(context), logger: ctx.logger, }; - ctx.registerService('security', { + // Typed against the published contract so the registered surface cannot + // drift from what cross-package consumers are promised: a renamed method, + // a dropped one, or a changed return type fails THIS build rather than + // silently degrading a consumer's feature detection at runtime. + const securityService: ISecurityService = { getReadFilter: (object: string, context?: any) => this.getReadFilter(object, context), // [#3547] Readable-field projection for a context — the authoritative // column set for a read-derived export (`export ⊆ list`, #3391). @@ -632,8 +637,11 @@ export class SecurityPlugin implements Plugin { // [ADR-0090 D6] First-class access explanation. Same code paths as // the middleware (resolution/evaluator/RLS compiler) — explained by // construction. Explaining ANOTHER user requires `manage_users`. - explain: (request: { object: string; operation: string; userId?: string }, callerContext?: any) => - this.explainAccessForCaller(request, callerContext), + explain: (request, callerContext?: any) => + this.explainAccessForCaller( + { ...request, operation: String(request.operation) }, + callerContext, + ), // [ADR-0090 D5/D9] Install-time suggestion surface: packages suggest // audience-anchor bindings; a tenant admin confirms (the binding is // written under the anchor + delegated-admin gates) or dismisses. @@ -643,7 +651,8 @@ export class SecurityPlugin implements Plugin { confirmAudienceBindingSuggestion(suggestionDeps, callerContext, id), dismissAudienceBindingSuggestion: (callerContext: any, id: string) => dismissAudienceBindingSuggestion(suggestionDeps, callerContext, id), - }); + }; + ctx.registerService('security', securityService); ctx.logger.info('[security] registered "security" service (getReadFilter, getReadableFields, explain, audience-binding suggestions) — ADR-0021 D-C / ADR-0090 D5/D6/D9 / #3547'); } catch (e) { ctx.logger.warn?.('[security] failed to register "security" service', { @@ -2093,7 +2102,7 @@ export class SecurityPlugin implements Plugin { async getReadFilter( object: string, context?: any, - ): Promise | null | undefined> { + ): Promise | undefined> { // System operations bypass scoping (mirrors the middleware's isSystem skip). if (context?.isSystem) return undefined; const positions = context?.positions ?? []; diff --git a/packages/rest/src/rest-server.ts b/packages/rest/src/rest-server.ts index 4ba30f606..71ce19b33 100644 --- a/packages/rest/src/rest-server.ts +++ b/packages/rest/src/rest-server.ts @@ -11,6 +11,7 @@ import { RestServerConfig, RestApiConfig, CrudEndpointsConfig, MetadataEndpoints import { DataProtocol, MetadataProtocol } from '@objectstack/spec/api'; import { PUBLIC_FORM_SERVER_MANAGED_FIELDS } from '@objectstack/spec/security'; import type { DroppedFieldsEvent } from '@objectstack/spec/data'; +import type { ISecurityService } from '@objectstack/spec/contracts'; import { resolveEffectiveApiMethods, isApiOperationAllowed, @@ -4542,8 +4543,15 @@ export class RestServer { * Mirrors the resolver in registerSecurityExplainEndpoints. Returns * `undefined` when no security service is reachable (no plugin-security / * single-kernel without a provider), so callers degrade gracefully. + * + * Typed as a PARTIAL {@link ISecurityService}: an implementation may omit a + * method it cannot honour, so every call site must keep feature-detecting + * (`typeof svc.x === 'function'`) rather than assume the full surface. */ - private async resolveSecurityService(environmentId?: string, req?: any): Promise { + private async resolveSecurityService( + environmentId?: string, + req?: any, + ): Promise | undefined> { try { const envId = await this.resolveRequestEnvironmentId(environmentId, req); if (envId && envId !== 'platform' && this.kernelManager) { diff --git a/packages/spec/src/contracts/index.ts b/packages/spec/src/contracts/index.ts index 18b9af53b..1ee60813a 100644 --- a/packages/spec/src/contracts/index.ts +++ b/packages/spec/src/contracts/index.ts @@ -34,6 +34,7 @@ export * from './workflow-service.js'; export * from './export-service.js'; export * from './email-service.js'; export * from './sms-service.js'; +export * from './security-service.js'; export * from './sharing-service.js'; export * from './rls-membership-resolver.js'; export * from './share-link-service.js'; diff --git a/packages/spec/src/contracts/security-service.test.ts b/packages/spec/src/contracts/security-service.test.ts new file mode 100644 index 000000000..9b8be0fa6 --- /dev/null +++ b/packages/spec/src/contracts/security-service.test.ts @@ -0,0 +1,98 @@ +// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. + +import { describe, it, expect } from 'vitest'; +import type { ISecurityService } from './security-service'; + +/** + * These tests pin the two things a consumer of the `security` service reasons + * about and that a refactor could silently change: the SHAPE of the surface + * (compile-time) and the MEANING of each "empty" answer (runtime). The second + * matters more than it looks — `undefined` and `[]` from getReadableFields are + * opposite instructions, and a consumer that conflates them either leaks a + * column set or blanks one out. + */ + +/** Minimal stub implementing the full surface. */ +function makeService(overrides: Partial = {}): ISecurityService { + return { + getReadFilter: async () => undefined, + getReadableFields: async () => [], + resolvePermissionSetNames: async () => [], + explain: async () => ({}) as any, + listAudienceBindingSuggestions: async () => ({ + suggestions: [], + synced: { created: 0, confirmedObserved: 0, pruned: 0 }, + }), + confirmAudienceBindingSuggestion: async () => ({ suggestion: {}, bindingCreated: true }), + dismissAudienceBindingSuggestion: async () => ({ suggestion: {} }), + ...overrides, + }; +} + +describe('Security Service Contract', () => { + it('a full implementation satisfies the surface', () => { + const service = makeService(); + for (const m of [ + 'getReadFilter', + 'getReadableFields', + 'resolvePermissionSetNames', + 'explain', + 'listAudienceBindingSuggestions', + 'confirmAudienceBindingSuggestion', + 'dismissAudienceBindingSuggestion', + ] as const) { + expect(typeof service[m]).toBe('function'); + } + }); + + it('getReadFilter: undefined means NO row restriction — the only thing it may mean', async () => { + // A deny is expressed as a filter that matches nothing, never as `undefined`, + // so a consumer can safely read `undefined` as "apply no filter". + const open = makeService({ getReadFilter: async () => undefined }); + await expect(open.getReadFilter('deal', { userId: 'u1' })).resolves.toBeUndefined(); + + const denied = makeService({ getReadFilter: async () => ({ id: { $eq: null } }) as any }); + await expect(denied.getReadFilter('deal', { userId: 'u1' })).resolves.toBeDefined(); + }); + + it('getReadableFields: undefined (no answer) and [] (nothing readable) are opposite answers', async () => { + const noAnswer = makeService({ getReadableFields: async () => undefined }); + // `undefined` → the caller must fall back to its own projection… + await expect(noAnswer.getReadableFields('deal', { userId: 'u1' })).resolves.toBeUndefined(); + + const nothingReadable = makeService({ getReadableFields: async () => [] }); + // …whereas `[]` is authoritative: expose no columns at all. + await expect(nothingReadable.getReadableFields('deal', { userId: 'u1' })).resolves.toEqual([]); + }); + + it('a system context is a full field-level bypass', async () => { + const service = makeService({ + getReadableFields: async (_object, context) => + context?.isSystem ? ['id', 'name', 'secret'] : ['id', 'name'], + }); + await expect(service.getReadableFields('deal', { isSystem: true })) + .resolves.toEqual(['id', 'name', 'secret']); + await expect(service.getReadableFields('deal', { userId: 'u1' })) + .resolves.toEqual(['id', 'name']); + }); + + it('a partial implementation is feature-detectable rather than wrong', () => { + // Consumers probe (`typeof svc.getReadableFields === 'function'`) so an + // implementation may omit a method it cannot honour and still be usable. + const partial: Partial = { getReadFilter: async () => undefined }; + expect(typeof partial.getReadFilter).toBe('function'); + expect(typeof partial.getReadableFields).toBe('undefined'); + }); + + it('explain accepts a record-scoped request and an explicit target user', async () => { + const seen: unknown[] = []; + const service = makeService({ + explain: async (request) => { seen.push(request); return {} as any; }, + }); + await service.explain( + { object: 'deal', operation: 'update', userId: 'u2', recordId: 'r1' }, + { userId: 'admin' }, + ); + expect(seen[0]).toEqual({ object: 'deal', operation: 'update', userId: 'u2', recordId: 'r1' }); + }); +}); diff --git a/packages/spec/src/contracts/security-service.ts b/packages/spec/src/contracts/security-service.ts new file mode 100644 index 000000000..f465f5404 --- /dev/null +++ b/packages/spec/src/contracts/security-service.ts @@ -0,0 +1,198 @@ +// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * @objectstack/spec/contracts/security-service + * + * Cross-package contract for the `security` service — the query surface that + * lets code OUTSIDE the ObjectQL engine middleware ask the same questions the + * middleware answers when it enforces access. + * + * The point of this surface is **non-drift**. Every method here is required to + * be computed from the SAME resolution the enforcement path uses (permission-set + * resolution → evaluator → RLS compiler / FieldMasker). A consumer that + * re-derives any of these answers locally — by reading permission sets itself, + * or by inferring them from already-enforced output — will drift the moment the + * enforcement path changes. Ask this service instead. + * + * Registered under the service name `security` by `@objectstack/plugin-security` + * (`ctx.registerService('security', …)`), which is also the reference + * implementation. Related lower-level handles the same plugin registers — + * `security.permissions`, `security.rls`, `security.fieldMasker` — are + * implementation internals and deliberately NOT part of this contract. + * + * ## Two failure stances, and how to tell which applies + * + * The methods here do NOT share a single failure convention, because they do + * not carry the same risk. Read each method's doc before choosing a fallback: + * + * - **Access-narrowing answers fail CLOSED.** {@link ISecurityService.getReadFilter} + * answers "which rows may this caller see"; a resolution failure returns a + * DENY filter (zero rows), never `undefined`. A consumer must never treat a + * thrown error or a deny filter as "no restriction". + * - **Advisory projections fail SOFT, explicitly.** {@link ISecurityService.getReadableFields} + * answers "which columns may this caller see" for presentation purposes; when + * the object schema cannot be resolved it returns `undefined`, meaning + * "no answer — use your own fallback", NOT "no fields are readable". An empty + * array is a real answer and means the opposite: nothing is readable. + * + * That distinction is load-bearing: the field projection is only ever a + * cosmetic narrowing on top of enforcement that already happened (the read path + * has already deleted unreadable keys from the data), so degrading to a wider + * column set cannot leak VALUES. A row filter has no such backstop. + * + * ## Availability + * + * The service is absent in deployments without `plugin-security`, and a kernel + * may resolve it per environment. Consumers MUST tolerate absence — resolve it + * defensively and keep a fallback path — rather than assume registration. + */ + +import type { FilterCondition } from '../data/filter.zod.js'; +import type { ExecutionContext } from '../kernel/execution-context.zod.js'; +import type { ExplainDecision, ExplainOperation } from '../security/explain.zod.js'; + +/** + * The context shape these methods accept. + * + * Callers routinely hold only part of a full {@link ExecutionContext} (a REST + * route may have `{ userId, permissions }`; a platform-internal writer may have + * only `{ isSystem: true }`), so every field is optional. Implementations read + * `positions` / `permissions` for set resolution, `isSystem` as a full bypass, + * `principalKind` to distinguish agent principals, and `onBehalfOf` for + * delegated (on-behalf-of) access. + */ +export type SecurityContext = Partial; + +/** Selector for {@link ISecurityService.explain}. */ +export interface ExplainAccessRequest { + /** Object API name to explain access for. */ + object: string; + /** Operation being explained (`read`, `create`, `update`, `delete`, …). */ + operation: ExplainOperation | string; + /** + * Explain access for ANOTHER user. Omit to explain the caller's own access; + * naming a different user is an administrative action and implementations are + * expected to gate it (the reference implementation requires `manage_users`). + */ + userId?: string; + /** Narrow the explanation to a single record (adds record-level layers). */ + recordId?: string; +} + +/** Filter accepted by the audience-binding suggestion list. */ +export interface AudienceBindingSuggestionFilter { + status?: 'pending' | 'confirmed' | 'dismissed'; + packageId?: string; +} + +/** + * Counters from the implicit re-sync the list call performs before reading. + * Surfaced so an admin UI can report what changed underneath the list. + */ +export interface AudienceBindingSuggestionSync { + created: number; + confirmedObserved: number; + pruned: number; +} + +/** + * A suggestion row as stored. The column set is owned by the implementation's + * backing object rather than this contract, so it stays open — consumers should + * read the fields they know (`id`, `status`, `package_id`) and pass the rest + * through. + */ +export type AudienceBindingSuggestion = Record; + +/** + * Public contract for the `security` service. + * + * Every method is expected to be derived from the same permission-set + * resolution the enforcement path uses. Implementations that cannot honour a + * method should omit it rather than answer approximately — consumers feature- + * detect (`typeof svc.getReadableFields === 'function'`) precisely so a partial + * implementation degrades instead of lying. + */ +export interface ISecurityService { + /** + * The row-level READ scope for `object` under `context` — the same filter the + * engine middleware AND-s into every find, exposed for paths that bypass the + * middleware (notably the analytics raw-SQL path, which must apply it to the + * base object AND every joined object). + * + * **Fails CLOSED.** A resolution failure, or an on-behalf-of context on a path + * that cannot compute the delegator intersection, returns a DENY filter that + * matches zero rows — never `undefined`. `undefined` means one thing only: + * this caller has no row restriction on this object. + */ + getReadFilter(object: string, context?: SecurityContext): Promise; + + /** + * The field names `context` may READ on `object` — the authoritative column + * projection for anything that must present "the columns list shows", such as + * a read-derived export header. + * + * Computed from schema + context, never from data rows: it is therefore immune + * to an all-null column (which a driver may omit from every row) and to an + * empty result set. The returned set is the exact complement of what the read + * path's field mask deletes, so a header built from it cannot drift from the + * values the same caller receives. + * + * **Fails SOFT, and the two empty answers are NOT the same:** + * - `undefined` — no answer (e.g. the object schema could not be resolved). + * Fall back to your own projection. + * - `[]` — a real answer: this caller may read NO field of this object. + * + * A system context bypasses field-level security and yields the full field set. + */ + getReadableFields(object: string, context?: SecurityContext): Promise; + + /** + * The effective permission-set NAMES for `context` — positions expanded and + * the additive baseline applied, i.e. the same set the middleware enforces + * with. The primitive for evaluating a permission-set-gated audience without + * re-implementing set resolution. + * + * **Throws** on resolution failure; callers must fail CLOSED on a throw rather + * than treating it as "no sets". + */ + resolvePermissionSetNames(context?: SecurityContext): Promise; + + /** + * Explain WHY access is granted or denied — the decision plus the layers that + * produced it (permission sets, object permissions, RLS, sharing, field mask). + * + * Runs the same resolution/evaluator/compiler the enforcement path uses, so + * the explanation matches enforcement by construction rather than by + * maintenance. + */ + explain(request: ExplainAccessRequest, callerContext?: SecurityContext): Promise; + + /** + * List install-time audience-binding suggestions (packages propose an + * audience anchor; a tenant admin confirms or dismisses). Re-syncs before + * reading and reports what that sync changed. + * + * Administrative: implementations gate this on tenant-admin and throw + * otherwise. + */ + listAudienceBindingSuggestions( + callerContext?: SecurityContext, + filter?: AudienceBindingSuggestionFilter, + ): Promise<{ suggestions: AudienceBindingSuggestion[]; synced: AudienceBindingSuggestionSync }>; + + /** + * Confirm a pending suggestion — writes the binding under the anchor and + * delegated-admin gates. Throws when the suggestion is unknown or no longer + * pending. `bindingCreated` is `false` when the binding already existed. + */ + confirmAudienceBindingSuggestion( + callerContext: SecurityContext, + id: string, + ): Promise<{ suggestion: AudienceBindingSuggestion; bindingCreated: boolean }>; + + /** Dismiss a pending suggestion. Throws when it is unknown or not pending. */ + dismissAudienceBindingSuggestion( + callerContext: SecurityContext, + id: string, + ): Promise<{ suggestion: AudienceBindingSuggestion }>; +} From 793ee5e4bc0ebde3a1021afb1884cdf8e4821668 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 27 Jul 2026 13:18:49 +0000 Subject: [PATCH 2/3] docs(kernel): list ISecurityService in the contracts index The contracts index table and the runtime-services source-of-truth list enumerate every published service contract; a new contract missing from them makes the index quietly wrong. Adds the Security Service row and its source path. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01KZ2BGusRo58ZW8FMkDhGTb --- content/docs/kernel/contracts/index.mdx | 1 + content/docs/kernel/runtime-services/index.mdx | 1 + 2 files changed, 2 insertions(+) diff --git a/content/docs/kernel/contracts/index.mdx b/content/docs/kernel/contracts/index.mdx index 1dd213da8..65dac74a1 100644 --- a/content/docs/kernel/contracts/index.mdx +++ b/content/docs/kernel/contracts/index.mdx @@ -41,6 +41,7 @@ Service contracts are **TypeScript interfaces** that define the boundaries betwe | Contract | Interface | Description | |:---|:---|:---| | Auth Service | `IAuthService` | Authentication — login, verify, logout, session management | +| Security Service | `ISecurityService` | Query surface for access decisions — row-level read scope, readable-field projection, effective permission sets, access explanation | | Sharing Service | `ISharingService` | Record sharing rules and access grants | ### Storage & Caching Contracts diff --git a/content/docs/kernel/runtime-services/index.mdx b/content/docs/kernel/runtime-services/index.mdx index bae7ba7bf..f070f8ca2 100644 --- a/content/docs/kernel/runtime-services/index.mdx +++ b/content/docs/kernel/runtime-services/index.mdx @@ -36,6 +36,7 @@ This chapter documents the runtime `services.*` APIs used in hook/action/flow/pl Each page links the canonical TypeScript source used to derive signatures. - Data: `packages/client/src/index.ts` +- Security: `packages/spec/src/contracts/security-service.ts` - Sharing: `packages/spec/src/contracts/sharing-service.ts` - Queue: `packages/spec/src/contracts/queue-service.ts` - Email: `packages/spec/src/contracts/email-service.ts` From f9100bf6851db9c5322fa170ca18f6a65fc46327 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 27 Jul 2026 13:25:26 +0000 Subject: [PATCH 3/3] chore(spec): regenerate the public API surface snapshot MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adding ISecurityService and its companion types exports six new names from `@objectstack/spec/contracts`; the api-surface gate pins the public surface so additions have to be acknowledged. Diff is exactly those six additions — 0 removed, 0 narrowed. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01KZ2BGusRo58ZW8FMkDhGTb --- packages/spec/api-surface.json | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/spec/api-surface.json b/packages/spec/api-surface.json index 29ddafa93..a5234c641 100644 --- a/packages/spec/api-surface.json +++ b/packages/spec/api-surface.json @@ -3425,6 +3425,9 @@ "ApprovalSendBackResult (interface)", "ApprovalStatus (type)", "AssistantModelMessage (type)", + "AudienceBindingSuggestion (type)", + "AudienceBindingSuggestionFilter (interface)", + "AudienceBindingSuggestionSync (interface)", "AuthResult (interface)", "AuthSession (interface)", "AuthUser (interface)", @@ -3452,6 +3455,7 @@ "EmailAttachment (interface)", "EmailDeliveryStatus (type)", "ExecuteUpgradeInput (interface)", + "ExplainAccessRequest (interface)", "ExportJobDownload (interface)", "ExportJobListResult (interface)", "FinishReason (type)", @@ -3508,6 +3512,7 @@ "ISchemaDiffService (interface)", "ISchemaDriver (interface)", "ISearchService (interface)", + "ISecurityService (interface)", "ISeedLoaderService (interface)", "IServiceRegistry (interface)", "IShareLinkService (interface)", @@ -3611,6 +3616,7 @@ "SearchHit (interface)", "SearchOptions (interface)", "SearchResult (interface)", + "SecurityContext (type)", "SendEmailInput (interface)", "SendEmailResult (interface)", "SendSmsInput (interface)",