import { SmrtCollection } from '@happyvertical/smrt-core'; import { FieldPolicySuggestion } from '../models/FieldPolicySuggestion.js'; import type { AcceptFieldPolicySuggestionResult, DismissFieldPolicySuggestionResult, PendingFieldPolicySuggestionsResult } from '../types.js'; /** * Raised when a pending→settled transition loses its race: another caller * (accept or dismiss) already settled the suggestion — whether the loss is * detected by the compare-and-set or by the load that preceded it, so * overlapping decisions get ONE error type regardless of timing. * * Carries BOTH `httpStatus` and `status` = 409. `httpStatus` is the property * core's generated REST error mapping honors (an integer OWN property, checked * with `Object.hasOwn`); `status` mirrors the users-style shape this package * already uses on `FieldPolicyPermissionError`. NOTE: that mapping arrived with * #2049 and is NOT in this branch's core yet, so until #2049 lands ahead of * this work the generated routes still surface these as 500s — the property is * set now so the 409 becomes real the moment it does. */ export declare class FieldPolicySuggestionConflictError extends Error { /** Core's generated-REST status contract (own integer property). */ readonly httpStatus = 409; /** The users-style shape mirrored by this package's authorization errors. */ readonly status = 409; constructor(operation: string, suggestionId: string); } /** * Raised when a non-transactional accept could neither complete its policy * write NOR undo its claim. Both causes are carried: the row may be sitting * `accepted` without a policy, which an operator must reconcile — silently * reporting only the policy error would imply the suggestion is still pending. */ export declare class FieldPolicySuggestionCompensationError extends Error { readonly httpStatus = 500; readonly status = 500; readonly revertCause: unknown; constructor(suggestionId: string, cause: unknown, revertCause: unknown); } /** * Stable id for the org-scope policy row an acceptance creates, derived from * the policy natural key. * * With a random id, two concurrent acceptances for the same field would each * mint a row and the natural-key upsert would resolve them by REPLACING one * (dropping its column and dangling its id). A deterministic id turns that into * a primary-key collision the loser can detect and fall back from. */ export declare function fieldPolicyRowId(tenantId: string, objectRef: string, fieldName: string): Promise; /** Default cool-down a dismissal applies (30 days). */ export declare const DEFAULT_SUGGESTION_COOL_DOWN_MS: number; /** Bounds for a caller-supplied dismissal cool-down. */ export declare const MIN_SUGGESTION_COOL_DOWN_MS: number; export declare const MAX_SUGGESTION_COOL_DOWN_MS: number; /** * Collection surface for {@link FieldPolicySuggestion} plus the three * manage-gated actions the gear badge / control-panel queue consume (#2051; * UI integration is #2050-follow-up territory — this is the minimal seam). * * All three actions are custom collection-scoped routes (the resolveBatch * mechanism — single-segment paths, both transports dispatch). Identity is * ambient-context-only and every action requires `fields.policy.manage` (or * super-admin bypass) within an ambient tenant: pending suggestions describe * org-wide usage, and accept/dismiss are org policy decisions. */ export declare class FieldPolicySuggestionCollection extends SmrtCollection { static readonly _itemClass: typeof FieldPolicySuggestion; /** * The caller's tenant's PENDING suggestions (optionally filtered to a set * of objectRefs), newest first, plus the total for the gear badge. */ pendingSuggestions(options?: { objectRefs?: string[]; }): Promise; /** * Accept a pending suggestion: CLAIM the pending→accepted transition, then * write the corresponding org-scope `FieldPolicy` row THROUGH NORMAL * VALIDATION (registry check, type check, security rail, required-field * invariant, ownership boundary, and the #2049 permission split — the * ambient caller must hold `fields.policy.manage`, which this action also * asserts up front). * * An existing tenant row for the same `(objectRef, fieldName)` is UPDATED * (read-modify-write), preserving its other sparse columns — never * duplicated through the natural-key upsert. * * Concurrency: the claim is a compare-and-set on `status` (the jobs * `writeOwnedJob` precedent — a guarded UPDATE with `RETURNING id`), so an * overlapping accept/dismiss pair cannot both win; the loser throws * {@link FieldPolicySuggestionConflictError} (409). Claim and policy write * run in ONE transaction when the driver supports it, so a rejected policy * write can never leave the suggestion settled without its policy (and vice * versa). Drivers without transactions get an explicit compensating revert. */ acceptSuggestion(options?: { id?: string; }): Promise; /** * Dismiss a pending suggestion: set status + a cool-down until which the * generation job will NOT regenerate the same * `(objectRef, fieldName, tenantId, kind)` suggestion. * * Validates OWNERSHIP and PENDING STATUS ONLY — deliberately NOT continued * proposal eligibility. A queued suggestion whose field has since been * removed, turned sensitive/`readPermission`-gated/transient, or retyped is * exactly the row an operator most needs to clear; re-running proposal * validation on the way out would reject the dismissal and, because pending * rows are never pruned, wedge it in the queue forever. The claim is the same * compare-and-set as accept, so it also bypasses the model's proposal * validation by construction. */ dismissSuggestion(options?: { id?: string; coolDownMs?: number; }): Promise; /** * Write the org-scope policy column an accepted suggestion implies, bound to * `db` so it participates in the caller's transaction. * * A `promote` and a `default` suggestion for the SAME field share one sparse * `FieldPolicy` row but own DIFFERENT columns, and both may be accepted * concurrently. A read-modify-save of the whole row therefore loses updates: * each acceptance reads the row (or its absence) before the other's save * lands, and the later full-row write erases the sibling's column — and, via * the natural-key upsert, can even replace the row id, dangling the earlier * caller's `policyRowId`. (Distinct from the suggestion-row race fixed * earlier: that one guards the pending→settled transition; this one is on the * shared policy row.) So this NEVER writes a whole row over an existing one: * * 1. An ATOMIC partial UPDATE sets only this acceptance's column, keyed by * the policy natural key and `RETURNING id` — one statement, so no * interleaving can drop the sibling's column, and the returned id is * always the surviving row. * 2. Only when no row exists yet does it CREATE through the model, so full * validation (registry, type check, security rail, required-field * invariant, ownership + permission split) runs on the row that is * actually inserted. The row carries a DETERMINISTIC id under strict * insert, so two concurrent creates cannot each mint a row: the loser * collides and falls back to step 1, applying only its own column. * * The update path skips the model, so BOTH kinds re-check live target * addressability through the shared helpers (a field can turn sensitive, * gated, transient, or disappear between queueing and acceptance). Defaults * additionally re-check their value/type rail; a promotion to `basic` has no * value payload, and the required-field invariant restricts only * advanced/hidden. Org locks constrain the user tier, not org rows. */ private applyAcceptedPolicy; /** * A driver transaction handle, or `null` when the driver has none (the * `FieldPolicy.saveAfterIdentityChange` probe, same shape). */ private beginTransactionIfSupported; /** * Ambient manage gate shared by the three actions: an ambient tenant * context is required (no context ⇒ no identity ⇒ fail closed — the * suggestion queue is never an anonymous surface), and the caller must hold * `fields.policy.manage` unless running under super-admin bypass. */ private requireManageContext; private loadOwnedPendingSuggestion; } //# sourceMappingURL=FieldPolicySuggestionCollection.d.ts.map