Skip to content

Releases: keyqcloud/kyte-php

kyte-php v4.4.4

Choose a tag to compare

@github-actions github-actions released this 22 May 17:17
2775902

Bug Fix: /jwt/login still 500s on apps with custom user_model (continuation of 4.4.3)

v4.4.3 fixed the Undefined constant "User" fatal by calling Api::loadAppModels($app) in resolveAuthContext. That uncovered a second gap: the app-scoped model definition has appId set, which causes ModelObject->retrieve() to auto-switch to the app DB via Api::dbswitch(true). But the app-DB credentials (host/user/password) were never configured because Api::dbappconnect() is called only by the normal MVC pipeline (Api.php:690) — which JWT bypasses.

Result: mysqli received null host/user/password and tried a Unix-socket connection → No such file or directory → HTTP 500.

Fix: resolveAuthContext now also calls Api::dbappconnect($app->db_name, $app->db_username, $app->db_password) immediately after loadAppModels. The HMAC pipeline does both calls back-to-back at Api.php:688-690; JWT now matches.

Apps without a custom user_model (default KyteUser path) are unaffected — they were never in the app-DB branch.

No schema changes. Composer upgrade is sufficient.

kyte-php v4.4.3

Choose a tag to compare

@github-actions github-actions released this 22 May 17:08
349483e

Bug Fix: /jwt/login fatals on apps with custom user_model

When an Application's user_model is set to an app-specific DataModel (e.g. "User" rather than the default "KyteUser"), JwtEndpoint::resolveAuthContext called constant($app->user_model) to resolve the model definition. But the JWT endpoint dispatches before Api::loadAppModels() runs in the normal MVC pipeline, so the app-scoped constant wasn't yet defined. In PHP 8+ that's a fatal: Undefined constant "User". Surface to the client was HTTP 500.

HMAC /Session login did not hit this because Api::route() calls loadAppModels before reaching the session controller. JWT's whole point is to bypass that pipeline (login can't require auth), which is what created the gap.

Fix: resolveAuthContext now calls Api::loadAppModels($app) immediately after retrieving the Application, before referencing the constant. If the app references a name that no DataModel row defines, falls back to KyteUser (with an error_log breadcrumb) rather than fatal — login then naturally fails at password_verify, which is a safer surface than a 500.

No schema changes. Composer upgrade is sufficient.

kyte-php v4.4.2

Choose a tag to compare

@github-actions github-actions released this 22 May 14:36
5b79e5e

Bug Fix: ErrorHandler crash when apiContext->key is a ModelObject

The enhanced v4.4 ErrorHandler bound $this->apiContext->key directly into the KyteError.api_key string column. $this->key is set in Api.php as new \Kyte\Core\ModelObject(KyteAPIKey) — an object, not a string. mysqli_stmt::execute() then threw Object of class Kyte\Core\ModelObject could not be converted to string inside the error handler. That secondary fatal swallowed the original error and surfaced to clients as a blank HTTP 500 with no log row written and no stack trace recoverable.

Customer-visible impact: any request that triggered the error handler — including expected exceptions on routes like POST /Session, GET on a controller hitting a runtime error, etc. — returned 500 with no body. Production traffic at ETOM was affected from the v4.4.0 deploy onward.

Fix: 'api_key' => isset($this->apiContext->key->public_key) ? (string)$this->apiContext->key->public_key : null — extract the scalar public_key string (which is the audit-relevant value anyway). Falls back to null when the ModelObject hasn't been retrieved or when there's no key context at all.

Regression test ErrorHandlerSensitivityTest::testApiContextKeyAsModelObjectLogsPublicKeyString exercises the previously-broken ModelObject path end to end. Pre-existing tests set $context->key = null and never hit the crash, which is why the regression slipped through 4.4.0 and 4.4.1.

No schema changes. Composer upgrade is sufficient.

kyte-php v4.4.1

Choose a tag to compare

@github-actions github-actions released this 22 May 13:58
ab57213

Bug Fix: CORS preflight on /jwt/* endpoints

Browser CORS preflight requests (OPTIONS /jwt/login) were being routed
through JwtEndpoint::process(), which only accepts POST and returned
405 method_not_allowed with no CORS headers. Browsers then blocked
the actual POST /jwt/login, breaking JWT login for any same-page web
client (Shipyard 2.0+ in JWT mode, kyte-api-js v2 JWT consumers).

Root cause: Api::cors() runs inside validateRequest(), which lives
downstream of the /jwt dispatch in Api::route(). So /jwt/* skipped
CORS entirely. (Same is technically true for /mcp but MCP is
server-to-server and doesn't trigger browser preflight.)

Fix: JwtEndpoint::handle() now emits CORS headers on every response
and replies to OPTIONS with 204 No Content + CORS preflight headers
before falling through to process(). Mirrors the permissive Origin
policy in Api::cors().

Regression test: JwtEndpointTest::testHandleAnswersOptionsPreflightWith204
exercises the OPTIONS path through handle() end-to-end with output
buffering and asserts the 204 response.

No schema changes. Composer upgrade is sufficient. Customers running
Shipyard 2.0+ in JWT mode must update before JWT login will succeed
in a browser.

kyte-php v4.4.0

Choose a tag to compare

@kennethphough kennethphough released this 22 May 11:45
2f7d49b

Phase 2 (MCP server) + Phase 2.5 (sensitive-data flag) + Phase 3 (JWT auth) all ship together. No breaking changes — every addition is opt-in, defaults preserve v4.3.x behavior bit-for-bit. Existing customers can upgrade in place without code changes.

New Feature: Embedded MCP server (Phase 2)

Each Kyte install can now expose a Model Context Protocol endpoint that lets AI clients (Claude Code, Claude.ai) inspect controllers, models, pages, and functions over a tenant-scoped bearer.

  • New endpoint: POST /mcp — handled by Mcp\Endpoint outside the standard MVC pipeline. JSON-RPC over HTTP, protocol version 2025-06-18. Bypasses Kyte's HMAC envelope.
  • New strategy: McpTokenStrategy registered with the auth dispatcher. Strict Authorization: Bearer kmcp_live_… prefix match.
  • New model: KyteMCPToken — opaque bearer tokens. Stored as sha256 hash, displayed as 16-char prefix for identification. Scoped (read / draft / commit), revokable, optional expiry, optional CIDR allowlist, optional application-binding.
  • New controller: KyteMCPTokenController — issue, list, revoke. Generates the raw token at issuance (returned once, never recoverable). Force-overrides kyte_account from auth context to close a privilege-escalation vector. Emits MCP_TOKEN_ISSUE / MCP_TOKEN_REVOKE / MCP_TOKEN_USE / MCP_SCOPE_VIOLATION audit rows.
  • 10 read tools shipped: list_applications, list_controllers, read_controller, list_functions, read_function (with optional version_number), list_models, read_model, list_sites, list_pages, read_page (with optional version_number).
  • Scope enforcement: ScopedCallToolHandler registered ahead of the SDK's default handler. Tools declare required scope via #[RequiresScope('read'|'draft'|'commit')] attribute. Fail-closed default — a tool without the attribute is unreachable.
  • Account isolation: every tool re-asserts entity.kyte_account === api.account.id. Foreign ids return null / [], never the foreign record.
  • bzip2 decompression: Controller.code, Function.code, and KytePageVersionContent.{html,stylesheet,javascript} are stored compressed; Bz2Codec::decompressIfBz2 wraps the read tools so source is returned in plaintext to the client.
  • Origin validation on /mcp: per spec § Security, mitigates DNS rebinding. Policy is "no Origin → allow, Origin present → check MCP_ALLOWED_ORIGINS constant (CSV)". CLI clients (Claude Code) unaffected; restrictive default forces operator opt-in for browser origins.
  • Proxy-aware client IP: Kyte\Mcp\Util\ClientIp reads CF-Connecting-IP then X-Forwarded-For first hop then falls back to REMOTE_ADDR. Gated on KYTE_TRUST_PROXY_IP_HEADERS constant — default-off so installs without a proxy aren't exposed to header spoofing. Wired into McpTokenStrategy::clientIp() for IP allowlist enforcement and audit fields.

New Feature: Sensitive-data flag (Phase 2.5)

A three-tier opt-in flag that prevents activity/error logs from capturing request bodies, and gates MCP read tools from exposing source for flagged entities. Designed for pass-through controllers whose payload contents are regulated data the platform should not store.

  • New columns (default 0, no behavior change unless flipped):
    • Controller.sensitive — blanket flag. When 1, drops body+response from logs entirely. Handles virtual (no-model) pass-through controllers.
    • DataModel.sensitive — same blanket treatment when the model is the request target.
    • ModelAttribute.sensitive — per-field redaction. Distinct from existing .protected (which only blanks values in GET responses). Set both for both behaviors.
    • KytePage.sensitive — MCP-only; withholds html/stylesheet/javascript from read_page. Pages don't write to activity logs.
  • New service: SensitivityPolicy (src/Core/SensitivityPolicy.php) — single source of truth, per-request singleton with in-memory cache keyed by (scope, name, account). One DB hit per tuple per request. Fail-permissive on lookup error so transient DB issues degrade to existing SENSITIVE_FIELDS baseline rather than to no redaction at all.
  • ActivityLogger consults the policy before persisting request_data and the PUT changes diff. Blanket-sensitive → both fields null. Field-sensitive → flagged fields replaced with [REDACTED], other fields pass through. The hardcoded SENSITIVE_FIELDS list (password / token / secret_key / etc.) still runs as a baseline on top.
  • ErrorHandler previously captured request body AND response payload into KyteError with zero redaction — closed in this release. handleException, handleError, and outputBufferCallback all consult the policy now. AI error-correction queue (AIErrorCorrection::queueForAnalysis) gains its own defense-in-depth check at the top: skips any sensitive-origin row, with audit-log breadcrumb. Regulated data never reaches Anthropic for analysis.
  • MCP read tools gated by the same flag: sensitive controller → read_controller returns code: null + sensitive: true; sensitive function (via parent controller) → same; sensitive model → read_model returns definition: null + sensitive: true; sensitive field → stripped from definition.struct, listed separately in sensitive_fields; sensitive page → content fields null. List tools (list_controllers, list_models, list_pages) surface a sensitive: bool on each row so AI clients see up front which entities are gated.
  • Runtime API responses unaffected — a sensitive controller still returns its normal response to the caller. The flag governs log / MCP / AI exposure only, not the live contract.

New Feature: JWT bearer authentication (Phase 3)

Modern auth path that coexists with the legacy HMAC sign/rotate. Customers can run a mix of HMAC apps and JWT apps on the same install.

  • New strategy: JwtSessionStrategy — HS256 access tokens with a configurable secret, claims {iss, sub, aud, exp, iat, nbf, jti, email, app}. Strict Authorization: Bearer eyJ… prefix match (won't clash with MCP's kmcp_live_…). Decode + verify in preAuth, sets $api->user, $api->account, and $api->session->hasSession = true so the standard ModelController auth gate accepts the request.
  • New endpoint family: POST /jwt/login, POST /jwt/refresh, POST /jwt/logout, POST /jwt/logout-all — handled by JwtEndpoint (same pattern as Mcp\Endpoint, bypasses the MVC pipeline so login can run pre-auth). Login posts {email, password, app_identifier?} and returns {access_token, refresh_token, expires_in, token_type:'Bearer', refresh_expires_at}.
  • Refresh tokens: opaque (kref_v1_… prefix), stored as sha256 hash in new KyteRefreshToken table. Single-use rotation per RFC 6819 — every successful refresh revokes the presented token and issues a new one in the same family. Reuse detection: presenting a revoked token revokes the entire family (likely leak signal). Expiration without revocation does not trigger family kill.
  • Multilogon: separate logins always create separate families. A user signing in from laptop and phone gets two independent families — revoking one device does not affect the other. Distinct from HMAC's ALLOW_MULTILOGON flag.
  • AuthDispatcher::buildDefault() now registers three strategies: McpToken → JwtSession → Hmac. Each matches() is strict so order doesn't affect correctness; order is documented for review clarity. HMAC clients continue working unchanged via HmacSessionStrategy.
  • Application.auth_mode column added: 'hmac' (default) or 'jwt'. Drives whether Shipyard's page generator emits the v1.x HMAC constructor or the v2 JWT constructor for kyte-api-js consumers.
  • Configuration constants (define in config.php to enable JWT):
    • KYTE_JWT_SECRET — required for JWT mode. At least 256 bits of entropy. Never commit to version control.
    • KYTE_JWT_ISSUER — optional, defaults to 'kyte'. Mismatched tokens are rejected at preAuth.
    • KYTE_JWT_ACCESS_TTL — optional, default 900 seconds.
    • KYTE_JWT_REFRESH_TTL — optional, default 604800 seconds (7 days).
  • Dependency: firebase/php-jwt ^7.0 — lightweight, well-known, no transitive deps.

Bundled migrations

Three new SQL files in migrations/. Apply in order. All are additive (new columns / new table) — safe to apply ahead of code; new columns default to 0 / 'hmac' so legacy code sees no behavior change until the matching feature is opted into.

migrations/4.4.0_sensitive_columns.sql       # Controller / DataModel / ModelAttribute / KytePage .sensitive
migrations/4.4.0_jwt_refresh_tokens.sql      # KyteRefreshToken table
migrations/4.4.0_application_auth_mode.sql   # Application.auth_mode

CI hardening

  • PHP matrix in .github/workflows/php.yml: tests now run on PHP 8.2 AND 8.3 against MariaDB 10.5.29. Catches version-specific syntax / deprecation issues.
  • PHPStan static analysis at level 1 with a baseline of pre-existing findings (phpstan-baseline.neon). New code is held to zero level-1 violations; baseline only shrinks.
  • Composer audit step on every push — fails CI on a new advisory in production dependencies.

Test coverage

  • 180 unit tests, 531 assertions. Up from 109 at end of Phase 2.
  • New test files: SensitivityPolicyTest, ActivityLoggerSensitivityTest, ErrorHandlerSensitivityTest, McpSensitivityTest, JwtSessionStrategyTest, JwtEndpointTest, RefreshTokenStoreTest.
  • Includes a regression test for the hasSession integration bug surfaced during dev rollout: JwtSessionStrategy must mark $api->session->hasSession = true after validating the bearer, otherwise ModelController::authenticate() rejects every protected endpoint despite a valid JWT.

Upgrade notes

  1. Apply the three migrations above to your database.
  2. If using JWT mode: define KYTE_JWT_SECRET in config.php (generate with `openssl rand...
Read more