@@ -176,9 +176,7 @@ function githubInstallationAccessCondition(scope: KnowledgeAccessScope): SQL {
176176export function knowledgeAccessCondition ( scope : KnowledgeAccessScope | SystemAccessScope ) : SQL {
177177 return storedKnowledgeAccessCondition (
178178 scope ,
179- scope . kind === 'system'
180- ? sql `true`
181- : sql `(${ githubInstallationAccessCondition ( scope ) } AND ${ confluenceSiteAccessCondition ( scope ) } )`
179+ scope . kind === 'system' ? sql `true` : liveSourceAccessCondition ( scope )
182180 )
183181}
184182
@@ -193,6 +191,103 @@ export function knowledgeMetadataCandidateAccessCondition(
193191 return storedKnowledgeAccessCondition ( scope , sql `true` )
194192}
195193
194+ /**
195+ * Mirrored permissions are current while the row says so. Every writer of an ACL records when its
196+ * evidence expires, so a candidate costs one comparison instead of a lookup per document into its
197+ * connector's members and observations. The stored expiry is never later than the evidence it was
198+ * written from: a members-mode document expires with the earliest observation its ACL names, so
199+ * this can hide a document the evidence would still admit, and can never admit one it would
200+ * refuse.
201+ *
202+ * A row without an expiry is one no current writer has touched — a backfill has not reached it, or
203+ * an older app version wrote its ACL during a deploy — so it falls back to proving freshness from
204+ * the evidence itself. That branch is dead once every row carries an expiry.
205+ */
206+ function isCurrentFrom ( evidence : SQL ) : SQL {
207+ return sql `(
208+ ${ document . aclValidUntil } > statement_timestamp()
209+ OR (${ document . aclValidUntil } IS NULL AND ${ evidence } )
210+ )`
211+ }
212+
213+ /** Every requirement clause must reach the caller, which preserves source permission intersections. */
214+ function aclRequirementsSatisfied ( tokens : SQL ) : SQL {
215+ return sql `NOT EXISTS (
216+ SELECT 1 FROM jsonb_array_elements(${ document . aclRequirements } ) AS required_clause(tokens)
217+ WHERE NOT (required_clause.tokens ?| ${ tokens } )
218+ )`
219+ }
220+
221+ /** The live source proofs this request carries, as the connector-scoped clause both shapes apply. */
222+ function liveSourceAccessCondition ( scope : KnowledgeAccessScope ) : SQL {
223+ return sql `(${ githubInstallationAccessCondition ( scope ) } AND ${ confluenceSiteAccessCondition ( scope ) } )`
224+ }
225+
226+ /**
227+ * The connectors a search may read from, resolved once per query: their ids grouped by the shape
228+ * their documents' ACLs take, and separately those whose reader access is proven live per request.
229+ */
230+ export interface KnowledgeConnectorEligibility {
231+ /** Documents carry the workspace ACL. */
232+ workspace : readonly string [ ]
233+ /** Documents carry mirrored source permissions verified as a whole. */
234+ admin : readonly string [ ]
235+ /** Documents carry the subject tokens of the members who observe them. */
236+ members : readonly string [ ]
237+ /** Of the above, those that additionally require this request's live source proof. */
238+ liveProofRequired : readonly string [ ]
239+ }
240+
241+ /**
242+ * The candidate predicate with connector state resolved ahead of the query instead of per row.
243+ *
244+ * Deletion, archival, a pending access rewrite, the organization's integration approval and the
245+ * access mode are facts about a connector, not a document, so checking them once per query leaves
246+ * each candidate an id comparison plus its own columns. A connector that still needs this
247+ * request's live source proof keeps the per-row lookup, which is where that proof is expressed.
248+ *
249+ * It narrows exactly as {@link knowledgeMetadataCandidateAccessCondition} does: the eligible ids
250+ * are the connectors that predicate's `EXISTS` would admit, and every document-level clause is
251+ * carried over unchanged.
252+ */
253+ export function knowledgeCandidateAccessConditionForConnectors (
254+ scope : KnowledgeAccessScope | SystemAccessScope ,
255+ eligibility : KnowledgeConnectorEligibility
256+ ) : SQL {
257+ if ( scope . kind === 'system' ) return documentConnectorIsActive ( )
258+ if ( scope . tokens . length === 0 ) return sql `false`
259+ const tokens = textArrayLiteral ( scope . tokens )
260+ const cutoff = aclFreshnessCutoff ( )
261+ const liveProof = new Set ( eligibility . liveProofRequired )
262+ const inConnectors = ( ids : readonly string [ ] ) : SQL =>
263+ ids . length === 0
264+ ? sql `false`
265+ : sql `${ document . connectorId } = ANY(${ textArrayLiteral ( [ ...ids ] ) } )`
266+ const mirrored = ( ids : readonly string [ ] , evidence : SQL ) : SQL => {
267+ const direct = ids . filter ( ( id ) => ! liveProof . has ( id ) )
268+ const gated = ids . filter ( ( id ) => liveProof . has ( id ) )
269+ const current = sql `${ document . acl } <> ARRAY['ws']::text[] AND ${ isCurrentFrom ( evidence ) } `
270+ return sql `(
271+ (${ inConnectors ( direct ) } AND ${ current } )
272+ OR (${ inConnectors ( gated ) } AND ${ current } AND EXISTS (
273+ SELECT 1 FROM ${ knowledgeConnector }
274+ WHERE ${ knowledgeConnector . id } = ${ document . connectorId }
275+ AND ${ liveSourceAccessCondition ( scope ) }
276+ ))
277+ )`
278+ }
279+ return sql `(
280+ ${ aclOverlap ( tokens ) }
281+ AND ${ aclRequirementsSatisfied ( tokens ) }
282+ AND (
283+ ((${ document . connectorId } IS NULL OR ${ inConnectors ( eligibility . workspace ) } )
284+ AND ${ document . acl } = ARRAY['ws']::text[])
285+ OR ${ mirrored ( eligibility . admin , sql `${ document . aclVerifiedAt } > ${ cutoff } ` ) }
286+ OR ${ mirrored ( eligibility . members , memberObservationCondition ( tokens , cutoff ) ) }
287+ )
288+ )`
289+ }
290+
196291/**
197292 * A members-mode document is readable while one of the caller's active member identities on its
198293 * connector still observes it, freshly. Correlated on the document so each check is a lookup on
@@ -221,29 +316,9 @@ function storedKnowledgeAccessCondition(
221316 if ( scope . tokens . length === 0 ) return sql `false`
222317 const tokens = textArrayLiteral ( scope . tokens )
223318 const cutoff = aclFreshnessCutoff ( )
224- /**
225- * Mirrored permissions are current while the row says so. Every writer of an ACL records when
226- * its evidence expires, so a candidate costs one comparison instead of a lookup per document
227- * into its connector's members and observations. The stored expiry is never later than the
228- * evidence it was written from: a members-mode document expires with the earliest observation
229- * its ACL names, so this can hide a document the evidence would still admit, and can never
230- * admit one it would refuse.
231- *
232- * A row without an expiry is one no current writer has touched — the rollout's backfill has not
233- * reached it, or an older app version wrote its ACL during a deploy — so it falls back to
234- * proving freshness from the evidence itself. That branch is dead once every row carries an
235- * expiry.
236- */
237- const isCurrent = ( evidence : SQL ) : SQL => sql `(
238- ${ document . aclValidUntil } > statement_timestamp()
239- OR (${ document . aclValidUntil } IS NULL AND ${ evidence } )
240- )`
241319 return sql `(
242320 ${ aclOverlap ( tokens ) }
243- AND NOT EXISTS (
244- SELECT 1 FROM jsonb_array_elements(${ document . aclRequirements } ) AS required_clause(tokens)
245- WHERE NOT (required_clause.tokens ?| ${ tokens } )
246- )
321+ AND ${ aclRequirementsSatisfied ( tokens ) }
247322 AND (
248323 (${ document . connectorId } IS NULL AND ${ document . acl } = ARRAY['ws']::text[])
249324 OR EXISTS (
@@ -257,8 +332,8 @@ function storedKnowledgeAccessCondition(
257332 AND (
258333 (${ knowledgeConnector . accessMode } = 'workspace' AND ${ document . acl } = ARRAY['ws']::text[])
259334 OR (${ document . acl } <> ARRAY['ws']::text[] AND (
260- (${ knowledgeConnector . accessMode } = 'admin' AND ${ isCurrent ( sql `${ document . aclVerifiedAt } > ${ cutoff } ` ) } )
261- OR (${ knowledgeConnector . accessMode } = 'members' AND ${ isCurrent ( memberObservationCondition ( tokens , cutoff ) ) } )
335+ (${ knowledgeConnector . accessMode } = 'admin' AND ${ isCurrentFrom ( sql `${ document . aclVerifiedAt } > ${ cutoff } ` ) } )
336+ OR (${ knowledgeConnector . accessMode } = 'members' AND ${ isCurrentFrom ( memberObservationCondition ( tokens , cutoff ) ) } )
262337 ))
263338 )
264339 )
0 commit comments