"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.CREDENTIAL_BEARING_DSN_SCHEMES = exports.SECRET_KEY_PATTERN = exports.PHYSICAL_DATABASE_INSTANCE_STATUSES = exports.CREDENTIAL_RESOLVERS = exports.DATABASE_LIFECYCLE_TAG_CAPABILITIES = exports.DATABASE_LIFECYCLE_TAG_ENVIRONMENT_PROFILES = exports.DATABASE_LIFECYCLE_TAG_ENGINES = exports.DATABASE_LIFECYCLE_TAG_OPERATIONS = exports.DATABASE_LIFECYCLE_CAPABILITY_MANAGED_IAM_V1 = exports.NATIVE_DB_IDENTIFIER_HASH_DOMAIN = exports.NATIVE_DB_IAM_AUTH_MODE = exports.NATIVE_DB_CONNECTOR_ROLE_NAME_PREFIX = exports.NATIVE_DB_AUTH_DESCRIPTOR_VERSIONS = exports.NATIVE_DB_AUTH_DESCRIPTOR_VERSION = exports.DATABASE_LIFECYCLE_MANAGED_BY = exports.DATABASE_ENGINES = exports.LIFECYCLE_ENVIRONMENTS = exports.LIFECYCLE_OPERATIONS = exports.LIFECYCLE_MIGRATION_STATES = exports.LIFECYCLE_STATES = exports.LIFECYCLE_NON_TERMINAL_STATES = exports.LIFECYCLE_TERMINAL_STATES = void 0;
exports.isReadyDatabaseBinding = isReadyDatabaseBinding;
exports.computeRequirementKey = computeRequirementKey;
exports.computeBindingKey = computeBindingKey;
exports.computeLegacyBindingKeyWithoutOrganization = computeLegacyBindingKeyWithoutOrganization;
exports.computeDesiredSpecHash = computeDesiredSpecHash;
exports.nativeDbIamSessionPolicy = nativeDbIamSessionPolicy;
exports.parseNativeDbIamDescriptor = parseNativeDbIamDescriptor;
exports.computeNativeDbApplicationToken = computeNativeDbApplicationToken;
exports.computeNativeDbBindingToken = computeNativeDbBindingToken;
exports.computeNativeDbDatabaseName = computeNativeDbDatabaseName;
exports.computeResourceKey = computeResourceKey;
exports.isTerminalLifecycleState = isTerminalLifecycleState;
exports.redactLifecycleSecrets = redactLifecycleSecrets;
exports.isSecretKey = isSecretKey;
exports.containsCredentialMaterial = containsCredentialMaterial;
const hashing_js_1 = require("../signing/hashing.js");
exports.LIFECYCLE_TERMINAL_STATES = ['ready', 'failed', 'cancelled'];
exports.LIFECYCLE_NON_TERMINAL_STATES = ['pending', 'provisioning', 'migrating', 'retiring'];
exports.LIFECYCLE_STATES = [...exports.LIFECYCLE_NON_TERMINAL_STATES, ...exports.LIFECYCLE_TERMINAL_STATES];
exports.LIFECYCLE_MIGRATION_STATES = ['pending', 'migrated', 'failed'];
// The environment axis (edit/preview/deployed) lives on the binding, so a
// single `ensure_database` covers what used to be split into
// ensure_dev_database / ensure_prod_database. `migrate_schema` and
// `retire_database` were always environment-agnostic.
exports.LIFECYCLE_OPERATIONS = ['ensure_database', 'migrate_schema', 'retire_database'];
// Mirrors the platform's view modes (proto api.v1.ViewMode: VIEW_MODE_EDIT /
// VIEW_MODE_PREVIEW / VIEW_MODE_DEPLOYED). Working-state migrations and the
// shared workspace database serve edit/preview; deploy-commit migrations and
// the deploy gate concern deployed.
exports.LIFECYCLE_ENVIRONMENTS = ['edit', 'preview', 'deployed'];
exports.DATABASE_ENGINES = ['postgres', 'snowflake', 'snowflake_postgres', 'lakebase'];
exports.DATABASE_LIFECYCLE_MANAGED_BY = 'database_lifecycle';
exports.NATIVE_DB_AUTH_DESCRIPTOR_VERSION = 2;
exports.NATIVE_DB_AUTH_DESCRIPTOR_VERSIONS = [1, exports.NATIVE_DB_AUTH_DESCRIPTOR_VERSION];
exports.NATIVE_DB_CONNECTOR_ROLE_NAME_PREFIX = 'superblocks-native-db-connector';
exports.NATIVE_DB_IAM_AUTH_MODE = 'aws_iam_role';
exports.NATIVE_DB_IDENTIFIER_HASH_DOMAIN = 'superblocks-native-db:v1';
exports.DATABASE_LIFECYCLE_CAPABILITY_MANAGED_IAM_V1 = 'managed-IAM-v1';
// Agent capability tag keys. A lifecycle worker publishes these in the
// `tags` map of its agent registration (merged into — never replacing — the
// tag map the agent already publishes; the existing `profile` tag carries
// datatag coverage, same meaning it has for execution routing. Lifecycle
// environment/profile coverage is published as pairs so edit-only staging
// profiles do not imply deployed staging support. The server matches pending
// lifecycle requests against these at poll/claim time and gates task creation
// on "some active org agent supports this".
exports.DATABASE_LIFECYCLE_TAG_OPERATIONS = 'databaseLifecycle:operations';
exports.DATABASE_LIFECYCLE_TAG_ENGINES = 'databaseLifecycle:engines';
exports.DATABASE_LIFECYCLE_TAG_ENVIRONMENT_PROFILES = 'databaseLifecycle:environmentProfiles';
exports.DATABASE_LIFECYCLE_TAG_CAPABILITIES = 'databaseLifecycle:capabilities';
exports.CREDENTIAL_RESOLVERS = ['opa_local', 'aws_secrets_manager', 'gcp_secret_manager', 'vault', 'kubernetes_secret'];
function isReadyDatabaseBinding(binding) {
    return binding.lifecycleState === 'ready';
}
// Physical database instances back the M2 shared-RDS allocation pattern: dev-DB
// provisioning issues `CREATE DATABASE`/`CREATE ROLE` against a pre-existing
// physical database instance instead of spinning up a fresh RDS per binding.
// The control plane is the dumb org-scoped state store (registry + atomic
// capacity counter); ALL selection and provisioning logic lives in the worker.
exports.PHYSICAL_DATABASE_INSTANCE_STATUSES = ['active', 'draining', 'retired'];
function computeRequirementKey(requirement) {
    return `${slugify(requirement.logicalName)}~${encodeURIComponent(requirement.logicalName)}:${requirement.engine}`;
}
// binding_key is the product identity of a database in the control plane, and
// is deliberately scoped to (organization, application, environment, profile)
// only — NOT the logical name. This is what makes "one database per profile"
// enforceable by the plain UNIQUE (binding_key) constraint: asking for another
// database under a different logical name resolves to the same binding_key
// instead of minting a new one. The logical name still lives on the binding row
// and in resource_key (physical identity); it just doesn't grant a second slot.
function computeBindingKey(input) {
    return [input.organizationId, ...bindingKeySegments(input)].join(':');
}
function computeLegacyBindingKeyWithoutOrganization(input) {
    return bindingKeySegments(input).join(':');
}
function bindingKeySegments(input) {
    return [input.applicationId, input.environment, `${slugify(input.profile)}~${encodeURIComponent(input.profile)}`];
}
// Hashes only the infrastructure spec. migrationDirectory is deliberately
// excluded: it says where the SQL files live, not what database to provision,
// so changing it must not make a ready binding look drifted and re-dispatch
// ensure_database. Callers that persist the requirement must still rewrite
// desiredSpec when migrationDirectory changes (same hash is not "no-op").
// Specs without a migrationDirectory keep their existing hash.
async function computeDesiredSpecHash(requirement) {
    const infrastructureSpec = { ...requirement };
    delete infrastructureSpec.migrationDirectory;
    return await (0, hashing_js_1.sha256Base64)(JSON.stringify(canonicalize(infrastructureSpec)));
}
function nativeDbIamSessionPolicy(descriptor) {
    return JSON.stringify({
        Version: '2012-10-17',
        Statement: [
            {
                Sid: 'ConnectToThisNativeDatabaseUser',
                Effect: 'Allow',
                Action: 'rds-db:connect',
                Resource: `arn:aws:rds-db:${descriptor.region}:${descriptor.aws_account_id}:dbuser:${descriptor.cluster_resource_id}/${descriptor.username}`
            }
        ]
    });
}
function parseNativeDbIamDescriptor(value) {
    if (!isUnknownRecord(value)) {
        return undefined;
    }
    const databaseIdentifier = typeof value.database === 'string' ? /^sbndb_([0-9a-f]{12})_([0-9a-f]{24})$/.exec(value.database) : null;
    const usernameIdentifier = typeof value.username === 'string' ? /^sbndb_([0-9a-f]{12})_([0-9a-f]{24})_runtime$/.exec(value.username) : null;
    const connectorRoleArn = typeof value.connector_role_arn === 'string'
        ? /^arn:aws:iam::(\d{12}):role\/(?:[\w+=,.@-]+\/)*[\w+=,.@-]+$/.exec(value.connector_role_arn)
        : null;
    if (!isNonEmptyString(value.application_id) ||
        !isNativeDbAuthDescriptorVersion(value.auth_descriptor_version) ||
        value.auth_mode !== exports.NATIVE_DB_IAM_AUTH_MODE ||
        !isNonEmptyString(value.aws_account_id) ||
        !/^\d{12}$/.test(value.aws_account_id) ||
        !isNonEmptyString(value.binding_id) ||
        !isNonEmptyString(value.cluster_resource_id) ||
        !/^(cluster|db)-[A-Za-z0-9-]+$/.test(value.cluster_resource_id) ||
        !connectorRoleArn ||
        connectorRoleArn[1] !== value.aws_account_id ||
        !databaseIdentifier ||
        !usernameIdentifier ||
        databaseIdentifier[1] !== usernameIdentifier[1] ||
        (value.auth_descriptor_version === exports.NATIVE_DB_AUTH_DESCRIPTOR_VERSION && databaseIdentifier[2] !== usernameIdentifier[2]) ||
        !isValidNativeDbRdsHostname(value.host, value.region) ||
        typeof value.port !== 'number' ||
        !Number.isInteger(value.port) ||
        value.port < 1 ||
        value.port > 65535 ||
        !isCommercialAwsRegion(value.region)) {
        return undefined;
    }
    return {
        application_id: value.application_id,
        auth_descriptor_version: value.auth_descriptor_version,
        auth_mode: exports.NATIVE_DB_IAM_AUTH_MODE,
        aws_account_id: value.aws_account_id,
        binding_id: value.binding_id,
        cluster_resource_id: value.cluster_resource_id,
        connector_role_arn: connectorRoleArn[0],
        database: databaseIdentifier[0],
        host: value.host,
        port: value.port,
        region: value.region,
        username: usernameIdentifier[0]
    };
}
async function computeNativeDbApplicationToken(trustedApplicationId) {
    return (await computeNativeDbIdentifierToken('application', trustedApplicationId)).slice(0, 24);
}
async function computeNativeDbBindingToken(trustedBindingId) {
    return (await computeNativeDbIdentifierToken('database', trustedBindingId)).slice(0, 24);
}
async function computeNativeDbDatabaseName(deploymentToken, trustedDatabaseId) {
    if (!/^[0-9a-f]{12}$/.test(deploymentToken)) {
        throw new Error('Native database deployment token must be 12 lowercase hexadecimal characters');
    }
    return `sbndb_${deploymentToken}_${await computeNativeDbBindingToken(trustedDatabaseId)}`;
}
async function computeNativeDbIdentifierToken(kind, trustedId) {
    if (!isNonEmptyString(trustedId)) {
        throw new Error(`Trusted native database ${kind} ID must be a non-empty string`);
    }
    const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(`${exports.NATIVE_DB_IDENTIFIER_HASH_DOMAIN}:${kind}:${trustedId}`));
    return Array.from(new Uint8Array(digest))
        .map((byte) => byte.toString(16).padStart(2, '0'))
        .join('');
}
// resource_key identifies the physical resource a binding maps to in
// customer infrastructure, and is the unit of locking inside the lifecycle
// worker. Distinct from binding_key (product identity in the control plane)
// because the worker derives infrastructure identity from it — shape
// mirrors planning doc §15.
function computeResourceKey(input) {
    return [
        input.organizationId,
        input.applicationId,
        input.requirementKey,
        input.environment,
        `${slugify(input.profile)}~${encodeURIComponent(input.profile)}`,
        input.actorScope ?? 'default'
    ].join('/');
}
function isTerminalLifecycleState(state) {
    return exports.LIFECYCLE_TERMINAL_STATES.includes(state);
}
function redactLifecycleSecrets(value) {
    return redact(value);
}
function slugify(value) {
    return value
        .trim()
        .toLowerCase()
        .replace(/[^a-z0-9]+/g, '-')
        .replace(/^-|-$/g, '');
}
function canonicalize(value) {
    if (Array.isArray(value)) {
        return value.map(canonicalize).sort((left, right) => codepointCompare(JSON.stringify(left), JSON.stringify(right)));
    }
    if (value && typeof value === 'object') {
        return Object.fromEntries(Object.entries(value)
            .filter(([, entryValue]) => entryValue !== undefined)
            .sort(([left], [right]) => codepointCompare(left, right))
            .map(([key, entryValue]) => [key, canonicalize(entryValue)]));
    }
    return value;
}
function codepointCompare(left, right) {
    if (left < right) {
        return -1;
    }
    if (left > right) {
        return 1;
    }
    return 0;
}
function isCommercialAwsRegion(value) {
    return typeof value === 'string' && /^[a-z]{2}-[a-z]+-[0-9]+$/.test(value) && !value.startsWith('cn-') && !value.startsWith('us-gov-');
}
function isNativeDbAuthDescriptorVersion(value) {
    return value === 1 || value === exports.NATIVE_DB_AUTH_DESCRIPTOR_VERSION;
}
function isNonEmptyString(value) {
    return typeof value === 'string' && value.trim().length > 0;
}
function isUnknownRecord(value) {
    return value !== null && typeof value === 'object' && !Array.isArray(value);
}
function isValidNativeDbRdsHostname(host, region) {
    if (typeof host !== 'string' || typeof region !== 'string') {
        return false;
    }
    const suffix = `.${region}.rds.amazonaws.com`;
    const prefix = host.endsWith(suffix) ? host.slice(0, -suffix.length) : '';
    const validLabel = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
    return host.length <= 253 && host === host.toLowerCase() && prefix !== '' && host.split('.').every((label) => validLabel.test(label));
}
function redact(value, preserveCredentialRefs = false) {
    if (Array.isArray(value)) {
        return value.map((entry) => redact(entry, preserveCredentialRefs));
    }
    if (value && typeof value === 'object') {
        return Object.fromEntries(Object.entries(value).map(([key, entryValue]) => {
            if (isCredentialRefKey(key)) {
                return [key, redact(entryValue, true)];
            }
            if (!preserveCredentialRefs && exports.SECRET_KEY_PATTERN.test(key)) {
                return [key, '[REDACTED]'];
            }
            return [key, redact(entryValue, preserveCredentialRefs)];
        }));
    }
    return value;
}
// Shared secret-key tripwire: keys whose name strongly suggests a raw secret
// value. Used by `redactLifecycleSecrets` to scrub logs/error reporting and by
// the server-side `assertNoRawCredentialMaterial` guard to refuse callbacks
// that violate the typed-CredentialRef contract. NOT a security boundary --
// callers must still send `Record<string, string | number | boolean>` for
// `connectionMetadata` and `CredentialRef` for `credentialRefs`; this catches
// obvious misuse only.
exports.SECRET_KEY_PATTERN = /password|secret|token|private[_-]?key|dsn/i;
// Schemes whose URI form embeds credentials before `@`. Used as a value-side
// tripwire alongside `SECRET_KEY_PATTERN` -- callers should never send
// connection strings; the typed `connectionMetadata` shape is the contract.
exports.CREDENTIAL_BEARING_DSN_SCHEMES = [
    'postgres',
    'postgresql',
    'mysql',
    'mariadb',
    'mongodb',
    'mongodb+srv',
    'redis',
    'rediss',
    'amqp',
    'amqps',
    'kafka'
];
function isSecretKey(key) {
    return exports.SECRET_KEY_PATTERN.test(key);
}
function containsCredentialMaterial(value) {
    if (DSN_WITH_INLINE_AUTH.test(value)) {
        return true;
    }
    if (CREDENTIAL_KV_PATTERN.test(value)) {
        return true;
    }
    if (JSON_ENCODED_SECRET_PATTERN.test(value)) {
        return true;
    }
    return false;
}
const DSN_SCHEME_GROUP = exports.CREDENTIAL_BEARING_DSN_SCHEMES.join('|').replace(/\+/g, '\\+');
const DSN_WITH_INLINE_AUTH = new RegExp(`(?:${DSN_SCHEME_GROUP}):\\/\\/[^/@\\s:]+:[^/@\\s]+@`, 'i');
const CREDENTIAL_KV_PATTERN = /(?:password|pwd|token|secret|private[_-]?key|api[_-]?key)\s*=\s*[^;&\s"]+/i;
const JSON_ENCODED_SECRET_PATTERN = /["']\s*(?:password|secret|token|private[_-]?key|dsn)\s*["']\s*:/i;
function isCredentialRefKey(key) {
    return /credential[_-]?ref/i.test(key);
}
//# sourceMappingURL=index.js.map