import type { Interceptor, ChannelCredentials } from "@grpc/grpc-js"; import { RoleCatalogClient } from "./client.js"; import type { CatalogPublishFailurePolicy, DefinedPermissionMap, PermissionDef, PermissionDefInput, PublishCatalogResponse, StarterRoleDef } from "./types.js"; /** Every field of {@link PublishCatalogAtBootOpts} EXCEPT the domain key. * Exported so consumers that need a plain extendable interface (an * intersection with a union cannot be `extends`-ed) still have one. */ export interface PublishCatalogAtBootOptsBase { catalogVersion: string; serviceOwner: string; serviceVersion?: string; /** Either the typed map from definePermissions OR a flat PermissionDef array * (for callers that constructed the catalog dynamically). */ permissions: DefinedPermissionMap> | readonly PermissionDef[]; starterRoles: readonly StarterRoleDef[]; /** Target gRPC URL — host:port. */ tenantServiceUrl: string; /** Optional channel credentials; defaults to insecure for local stack. */ channelCredentials?: ChannelCredentials; /** Optional gRPC client interceptors (S2S auth from @nodii/grpc-auth). */ interceptors?: Interceptor[]; /** When true, retry on UNAVAILABLE per § 5.5. Default: true. */ retryOnColdStart?: boolean; /** Max retry attempts. Default 30 (~5 min worst case). */ retryMaxAttempts?: number; /** Initial backoff base in ms. Default 100. */ retryBaseMs?: number; /** Cap on a single backoff in ms. Default 30000. */ retryMaxBackoffMs?: number; /** What a publish FAILURE does to the booting process. **Default * `"degrade"`** — the D410(3)-mandated posture. * * - `"degrade"` — record `degraded` state, log at ERROR, keep retrying in * the background, and let boot continue. The service serves. * - `"throw"` — rethrow out of {@link publishCatalogAtBoot} so an awaiting * boot sequence crashes before anything listens (the pre-0.9.0 default). * * Read `onPublishFailure: "degrade"` as fail-CLOSED, not lenient: a catalog * that never publishes means the domain's permission keys do not exist in * tenant-service, so the D544 roles->perms resolution yields nothing for * them and every guarded route 403s. There is no path where degrading grants * access that a successful publish would have withheld. See the "cannot fail * open" block in ./boot-publish.ts for the full argument. * * Ignored by {@link publishCatalogOnce}, which always throws — fatality is a * boot-layer policy, not a transport concern. */ onPublishFailure?: CatalogPublishFailurePolicy; /** @deprecated 0.9.0 — superseded by {@link onPublishFailure}, and its * default INVERTED to match D410(3). * * `true` ≡ `onPublishFailure: "throw"` * `false` ≡ `onPublishFailure: "degrade"` * * Still honored so the services that pass it explicitly keep their exact * current behaviour across the bump. Passing BOTH this and * `onPublishFailure` with conflicting meanings is a loud error rather than a * silent pick. * * Historical note: before 0.9.0 this ALSO mis-advertised itself. Setting it * to `false` did not produce a non-fatal publish — the branch fell through * to a bare `throw err`. Every consumer therefore had to hand-roll its own * try/catch, and the one that didn't crash-looped in production. Removed in * 1.0.0. */ failFastOnVersionMismatch?: boolean; /** Only for nodii-tenant-service publishing platform_ops under reserved namespace. */ bypassReservedNamespaceCheck?: boolean; /** Declares the namespaces a SUBSTRATE publish owns (07-rbac § 5.5.2 / D513). * Only meaningful when `domainKey` ∈ SUBSTRATE_DOMAIN_KEYS (e.g. * "observability"). When set on such a publish, each permission key's * first-segment is validated against this set INSTEAD of the strict * first-segment == domainKey rule. Ignored for a non-substrate domainKey. */ ownedNamespaces?: readonly string[]; /** Optional injected sleep — for tests. */ sleepFn?: (ms: number) => Promise; /** Optional rng for jitter; defaults to Math.random. */ randomFn?: () => number; /** Optional pre-built client (test seam). When supplied, tenantServiceUrl * is still required for telemetry/log context but no new client is made. */ clientOverride?: Pick; /** ⚠️ TELEMETRY-LABEL BRIDGE for the 0.7.0 rename. Default `false`. * * 0.7.0 renamed the `ValidationFailureKind` telemetry label * `"invalid_module_key"` -> `"invalid_domain_key"`. Any dashboard panel or * alert rule filtering on the OLD label goes SILENTLY DEAD after the bump: * it still renders, it just matches nothing — which is indistinguishable * from "no validation failures". Nothing errors, nothing logs, no CI check * catches it. * * Set this to `true` to emit BOTH labels for a single failure while the * panels are migrated, then remove the flag. It is OFF by default precisely * because dual-emit DOUBLE-COUNTS the failure counter — opt in only if you * have live dashboards keyed on the old label, and prefer double-counting a * boot-time validation error over a dead panel. Removed in 1.0.0. */ emitLegacyValidationFailureKind?: boolean; /** Where the boot layer writes its ERROR / INFO lines. Defaults to a REAL * console-backed logger — never a silent no-op. A degraded catalog that * logged nothing would be exactly the invisible failure this whole change * exists to prevent, so there is deliberately no "quiet" default. * * Inject the host's structured logger (pino etc.) to get the events into the * normal log pipeline. */ logger?: CatalogPublishLogger; /** Background re-attempt ladder in ms, used after the awaited boot attempt * fails. Default `[5_000, 15_000, 30_000, 60_000, 120_000]`. */ retryDelaysMs?: readonly number[]; /** After the ladder is exhausted, keep re-attempting on this interval so a * repair made hours later still lands WITHOUT a redeploy. Default 15 min. * Set `0` to stop after the ladder. */ refreshIntervalMs?: number; /** Test seam — the boot layer's own timer sleep (the ladder + refresh), * distinct from {@link sleepFn} which paces the in-attempt UNAVAILABLE * backoff. Real timers are `unref`'d so they never hold the process open. */ bootSleepFn?: (ms: number) => Promise; } /** Minimal structured-logger shape the boot layer needs. Deliberately tiny so * pino / bunyan / console all satisfy it without an adapter. */ export interface CatalogPublishLogger { error(fields: Record, msg: string): void; info(fields: Record, msg: string): void; } /** Options for {@link publishCatalogAtBoot}. * * 0.11.0 deleted the 0.7.0 `moduleKey` compat arm outright: `domainKey` is the * only spelling. Every live publish call site in the fleet already passes it * (grep-verified across all ten publishers); the remaining `moduleKey:` hits * are structured-LOG fields, not options. */ export interface PublishCatalogAtBootOpts extends PublishCatalogAtBootOptsBase { /** RBAC permission namespace — the first segment of every permission key. */ domainKey: string; } export declare function computeBackoff(attempt: number, baseMs: number, maxMs: number, randomFn?: () => number): number; /** * ONE publish attempt. Per spec § 5.3. * * THROWS on every failure, always with a typed {@link RoleCatalogError} * subclass. It applies no fatality policy and keeps no state — it is the * transport, and it is the unit under test. * * Most services should NOT call this directly: call * {@link publishCatalogAtBoot}, which adds the D410(3) non-fatal posture, the * queryable degraded state and the background retry. Reach for this one only * when you genuinely want to handle every failure yourself. * * @throws {CatalogPublishFailedPrecondition} peer rejected the catalog * (domain not registered / service_owner mismatch / version regression) * @throws {CatalogPublishUnavailable} peer down and `retryOnColdStart: false` * @throws {CatalogPublishTimedOut} cold-start retries exhausted * @throws {RoleCatalogError} local Layer-2 validation rejected the catalog */ export declare function publishCatalogOnce(opts: PublishCatalogAtBootOpts): Promise; //# sourceMappingURL=publish.d.ts.map