Skip to content
KAMOUniverse

Changelog

What's new.

Every improvement to the platform, as it ships.

  1. · kamo-shared-library

    Add a global alias-uniqueness check

    **************** scopes by provider, and that was right while an alias was only ever resolved as (security_provider_id, alias) — the pair behind a <alias>.<parentdomain> host. Two orgs under different providers could share one without ambiguity. An organization can now be named directly by its alias, with ?org=, and that lookup has no provider to scope by. A repeated alias makes the reference ambiguous platform-wide, so OrgResolutionService refuses it rather than picking one — which leaves BOTH organizations unreachable by alias. The live estate already contains repeats (four orgs share "acme-corp", two share "kia-kaha"), which is why the resolver refuses; this is what stops more being created. excludeId lets an organization keep its own alias while other fields are saved; pass null when creating. 1597 tests pass.

  2. · KlusterServices

    Dispatch a bug report into sage's open VS Code

    URI handler that creates a conversation in a RUNNING window and pre-fills the input, which is the only way to reach the editor sage already has open instead of starting a second one beside it. The URI carries a prompt and nothing else, so model and effort go through coderProcessWrapper — a wrapper that maps a validated enum to fixed flags and never takes argv from the file, so a forged file can choose a different model and nothing more. The URI pre-fills but does not submit, so submission is a synthetic Return and the agent then waits for a transcript before reporting success. The needle it looks for is JSON-escaped: transcripts are JSONL, and searching for raw newlines would mark every delivered message as failed.

  3. · InitializerService

    Create the System Bugs tables

    No CHECK constraint on the ordinal columns: support_ticket_status_check is what happens when Hibernate freezes one at an enum's size and the enum then grows, and SystemBugStatusType is expected to grow. Applied to yb-tserver:5433 by hand as well, since this service has no CI.

  4. · kamo-internal

    Manage a custom domain from Company settings

    A domain used to be the doorway into an organization: DNS had to be configured before anyone could sign in at all, which is why /setup/dns was reached from the org-creation wizard and from a "needs setup" banner rather than from settings. It is optional now, so it belongs with the rest of the white-labelling. Adds a Custom Domain section to Settings → Essential → Behavior & Labeling, between Company Info and Branding. It shows the org's current domain, or says plainly that there is not one, and opens /setup/dns to add or change it. The button reads "Set up a domain" or "Manage domain" depending on which of those it is. The copy leads with the workspace already being reachable at kamocrm.com, so an owner who never wants their own address knows this is something they can ignore. Uses orgIdStr rather than organization.id: that field is a JS number and truncates int64 org ids past MAX_SAFE_INTEGER, which would open the DNS page for a different organization. The button is disabled until it resolves rather than navigating with a wrong id. Copy landed in kamo-translation-dictionary first (98ee7cb6) — CI clones it unpinned at image-build time, so app-first is a red build. 2921 tests and all 8 guards pass, i18n included.

  5. · SecurityService

    Send the tree on the app-catalog endpoint too

    Branch Types → Allowed Apps builds its editor from /features/app-catalog, not from the tenant features listing, so it was the one surface that could not see which apps are children of which. A parent decides what its children can even be offered, so the editor needs the same shape. 947 tests green.

  6. · kamo-internal

    Render the app tree, and confirm what a parent switch-off takes

    Children now sit under their parent in the Apps & Features list, each with its own switch. Nesting is expressed by ORDER plus an indent rather than a nested container, so the existing card, motion and gating code is untouched — and a child whose parent is filtered out of the current tab still renders at top level rather than vanishing. Two behaviours the cascade made necessary: Toggling now applies the server's whole feature list instead of flipping the clicked row. Enabling a child switches its parent on and disabling a parent switches every child off, so a local single-row update would leave the UI disagreeing with the server about everything else the cascade touched. Both endpoints already returned the full list; nothing new was needed server-side. The old single-row path stays as a fallback for a SecurityService that sends no list. Switching a parent off asks first, naming the children it will take with it. That is a bigger action than the toggle looks. The tree comes from the features endpoint rather than a TypeScript mirror of ServiceType — those have drifted from Java twice already, and the server is the only place that knows the operator catalog. 203 test files / 2921 tests green, build clean.

  7. · InitializerService

    Price the commerce children and the EHR, with a totality guard

    Neither pricing map had a totality check, and the two failure modes are different and both silent. Missing from CATALOG, an app lands nowhere on the pricing matrix. Missing from TIERS, every tier writes is_included=false — an explicit DENIAL that entitlement fail-open does NOT rescue, because fail-open only applies when a plan has no row at all and this writes one. So a forgotten app is either given away to everyone or padlocked for everyone, and which one depends on a detail nobody remembers at the time. The five commerce verticals sit on exactly POS's rung. They were not separately purchasable before the app tree — anything an org holding POS could do, it could do — so putting them anywhere else would retroactively charge for capability people already have. A test pins that to POS rather than to a literal, so moving POS moves them. EHR is enterprise-negotiated like the origination systems: no list price, CUSTOM plans only, absent from every tier. Onboarding a PHI customer means a BAA and a compliance review, not a self-serve checkout. 159 tests green.

  8. · kamo-marketing

    Stop counting a member's visits to their own link

    A member checking that their own tracking link works is the most common first visit any code ever gets, and it is worse than useless: it inflates the number they are measured on, it is indistinguishable from a real prospect in the visitor drawer, and it leaves them holding a permanent visitor cookie so every later visit counts again. A code showing three visits, all of them the person who minted it, is a payment record with a lie in it. This site is the only origin that can answer the question — it has no session and no member, and the internal console cannot set a cookie on www.<apex> from internal.<apex>. What crosses that gap is a link: arriving with ?mrt_self=1 mints the exclusion, and from then on the browser is excluded on every visit whether or not the parameter is there. The internal Tracking Codes tab grows the control that produces that link. Not minting is not enough, so an excluded visit DELETES the visitor and source cookies too. A member who previewed their link before this shipped, or who opted out on a browser already counted, is still carrying a GUID — and the beacon fires for anyone who has one. Without the delete the opt-out would only ever have worked for people who did not need it. The exclusion is deliberately NOT in TRACKING_COOKIES: that set is what the cookie policy's delete control clears, and sweeping an opt-OUT away with the identifiers would silently re-enrol the one visitor who asked not to be counted — on the control most likely to be pressed by exactly that person. check-discount-cookie grows from 50 checks to 72, covering the value matching, the parameter spellings, the delete-set exclusion, and the two source invariants (middleware must delete the identity, the beacon must bail locally because middleware runs on document requests while the beacon runs on every client-side navigation).

  9. · kamo-internal

    The Affiliate Sales Scoreboard widget

    One grid column wide and two rows tall: the ten people in the organisation bringing in the most through their tracking links, over today, this month, this year or all time, opening on visitors this month. The reorder is the feature. Every other way of showing four metrics over four windows is sixteen separate lists; this is one list that rearranges itself. Rows are keyed by member and carry a layout animation, so switching category slides the same people past each other rather than blanking the pane — and the hook keeps every board it has fetched, so the press that triggers that animation has data to animate to on the same frame. The selectors are stored against the member's PROFILE, not localStorage, so the board someone set up at their desk is the board they get on their laptop. Both readers downgrade an unrecognised value to the default rather than refusing it — the server applies the identical rule and echoes back what it used, so a preference written by an older build can never leave a member on a blank pane. A member outside the top ten gets their own line pinned below the list, and a member with nothing yet gets it at rank zero. A board that can only say "you are not on it" is the one thing a scoreboard must not do to the people it exists to motivate. Also here, because it is the same problem from the other end: a "preview" control on the Tracking Codes tab that opens a link WITHOUT counting the visit. Checking where your own link lands is the most common first visit any code ever gets, and counting it puts the member's own clicks into the record they are measured on. buildTrackingPreviewLink is the real link plus the marker the marketing site reads — a cross-repo contract, so the test pins the parameter name.

  10. · MediaService

    The Affiliate Sales Scoreboard endpoint

    GET **************** answers this organisation's top ten for one metric over one window, plus the caller's own standing when they are not on it. The counters on MARKETING_TRACKING_CODES cannot back this: they are all-time roll-ups with no time dimension, and there is nothing in a running total that says when any of it happened. The windowed truth is in the rows — **************** for acquisition, and each conversion's own *_AT stamp on MARKETING_VISITORS. Which is also why All Time agrees with the code grid: same data, counted the same way, not a second bookkeeping. Four decisions worth stating: - Acquisition is dated by FIRST contact, not by activity. Counting visitors who were merely active in the window would re-credit the same person every month, so work done once would out-rank work done now, forever. - Conversions rank by attributedTrackingCodeId alone. That column freezes the moment an account exists; lastTrackingCodeId keeps moving, and ranking off it would let one member take another's closed conversion with an extra click. - The window is cut in the MEMBER's calendar and compared in UTC. "This month" differs by up to a day depending on whose calendar is asked, and every column in this schema is an offset-free LocalDateTime meaning UTC. - All four metrics are computed per request, so switching category is an instant re-sort of rows already on screen rather than a spinner. Gated on AFFILIATE_SCOREBOARD_WIDGET and nothing else — see the right's own javadoc. It buys no drill-down: there is no code id, no visitor id and no member parameter on the endpoint, and every by-id analytics endpoint still goes through MarketingTrackingScopeResolver unchanged. The four aggregations use the comma-join rather than JOIN..ON because neither foreign key is a JPA association and the comma form parses on every Hibernate version the fleet runs — MarketingTrackingHqlTest now parses all four against the real entity model on 6.2.13, and pins the acquisition, attribution and tenant predicates that fail silently if they drift.

  11. · kamo-shared-library

    Add the Affiliate Sales Scoreboard widget rights

    Five rights, 234-238, in the MARKETING group and hung off VIEW_REFERRAL_CODES: a leaderboard of referral numbers is tracking-code data, and a member who may not see the Tracking Codes tab has no business on it. The widget right — not VIEW_ALL_MEMBER_TRACKING — is what discloses other members' totals, and that is deliberate. 200/201 gate the ANALYTICS: whose visitors, whose addresses, whose timelines, resolved per target member by the scope resolver. A scoreboard discloses a name, a rank and one number, to everyone at once. Requiring the org-wide analytics right would leave it visible only to the administrators who hold one, which is a report rather than a board. Deliberately NOT seeded, for the same reason 200 and 201 are not: seeding would publish every existing holder's referral totals to their colleagues on the boot that shipped it, which is a disclosure decision made by a deploy rather than by an administrator. 229/166/32/31 in the audited shape — all five are children and VIEW_REFERRAL_CODES is itself a child of VIEW_MARKETING, so neither root bucket moves.

  12. · kamo-login

    Brand and scope the sign-in screen with ?org=

    login.kamocrm.com is the sign-in screen for every organization that has no host of its own, so the hostname cannot say which one is signing in. ?org= does: it names the organization by id or alias, selects the branding to paint, and travels with the credentials as the organization they are checked against. A hint, not a credential. Membership is proven server-side either way, so naming an organization you do not belong to gets the same refusal as naming a fictional one. The worst a wrong reference achieves is the wrong logo on a public page. Branding comes from SecurityService's /org/public/{ref}, relayed rather than reimplemented so the rule about what a guessable reference may expose stays in one place. The theme base then follows the organization instead of the host, used as given: dropping its first label the way the host path does would turn acme.kamocrm.com into kamocrm.com and paint a tenant in the platform's branding. The first paint necessarily uses the host's branding, because the reference has not been resolved yet; KAMO_THEME_REAPPLY repaints once it has. A visit that names no organization clears any reference a previous visit recorded in this tab, so the platform's own screen does not keep someone else's logo.

  13. · SecurityService

    Serve an org's branding to a screen with no session

    Login, registration and the meeting lobby paint an organization's colours before any session exists. They knew which one from the hostname, which is exactly what an organization without a hostname of its own cannot supply. GET /api/security/org/public/{ref} turns an explicit ?org= reference -- the id as digits, or the alias -- into something to paint. Branding ONLY: name, logo, colours, locale, theme host. Not features, not rights, not domains, not members. The first attempt at this reused the projected organization record and the endpoint ratchet refused it, correctly -- aliases are guessable, so a full record behind a guessable reference enumerates every organization's configuration. What is left is what anyone opening that organization's sign-in page already sees, and a test names the excluded fields individually so putting one back has to be deliberate. Read-only. The sibling /org/domain/{host} auto-provisions app features as a side effect; an endpoint reachable by guessing must not write. An unknown reference 404s, and so does an ambiguous alias -- byPublicRef already refuses one naming several orgs, and this pins that the refusal reaches the caller as "no such organization" rather than as someone else's branding. The caller falls back to the platform's own. themeDomain carries the same value the shell and the theme provisioner use, so a pre-session screen reads an org's branding from the folder it was actually written to -- {alias}.kamocrm.com for an org with no domain. 947 tests pass.

  14. · MediaService

    Refuse meeting recording for a tenant that handles PHI

    The capability split made ServiceType.MEET permitted — a live conference on self-hosted Meet never leaves the cluster — while MEETING_RECORDING stayed blocked, because this pipeline posts the entire transcript to the multi-provider AI router and then emails the result to every participant including external guests. That is an unrecallable third-party copy. The guard sits in the private ingest() both public entry points funnel through, so it refuses before any bytes reach MinIO, and again at the top of runSummaryPipeline, which is public and @Async. 200 tests green.

  15. · VOIPService

    The blocked capability now outranks the right, and god mode

    PhiPlaybackAuthorizer gated PHI call playback on VIEW_CALL_LOGS. That was the whole check because ServiceType.VOIP mapped to the blocked VOIP_RECORDING module, so a PHI tenant could not have the phone system at all and this class never ran for one. The capability split made VOIP itself permitted — call handling is in-cluster — while VOIP_RECORDING stayed BLOCKED_PENDING_WORK for reasons that have not changed: playback performs no clearance check, transcripts ride in metadata, and there is no retention or deletion path. Left alone that would have been a straight weakening: a PHI tenant could enable the phone system and, with one right granted, play back everything. So PhiCapabilityGuard runs FIRST and its refusal is final. It takes no caller parameter, so for a PHI tenant no right lifts it — and neither does an active god session, which is the property that matters most. **************** is now **************** Three tests encoded the old contract and are rewritten rather than deleted, each recording why the expectation flipped. The voicemail refusal message no longer blames the missing right, which is usually not the reason any more. 152 tests green.

  16. · SecurityService

    Serve the app tree, and fix the unguarded auto-enable

    FeatureController now sends parent/children/isParent on every serialized app so the client can render children nested under their parent. Served rather than mirrored in TypeScript on purpose: the ServiceType mirrors have drifted from Java twice already and a second copy of the tree would be one more thing to drift. No new response shape was needed for the cascade — /enable and /disable already re-serialize the whole feature list, so knock-on changes arrive without the client reconciling anything. Both auto-provision loops now skip a child whose parent the platform does not offer, since enableFeature cascades the parent on and would otherwise switch on a parent nobody published. OrganizationController's loop had NO PHI check while FeatureController's equivalent did, on a public unauthenticated endpoint. For a PHI tenant the first blocked module threw, the surrounding catch logged a warning, and effectiveFeatures silently fell back to raw rows with no applied-model projection. It now asks rather than catches, exactly as the features listing does. /api/security/ehr registered. The commerce prefixes stay mapped to POS: the parent is the right coarse gate and **************** already refines per market. 938 tests green.

  17. · kamo-internal

    Mirror the six commerce child apps, and stop the mirrors drifting

    Both TS mirrors are now GENERATED from ServiceType.java rather than hand-maintained. They had drifted badly from Java and from each other: stale "RAG integration" and "Falls back to parent organization SMTP" copy that Java removed in 2026-08, POS marked PLANNING in ServiceType.ts while AppType.ts said COMPLETED, and different descriptions for the same app in each file. Worth knowing before trusting them: displayName and description here are read by NOTHING. Every consumer uses these constants for identity only — hasFeature() matches on id, appServiceIcons keys on name. Copy comes from GET /api/security/features, which honours PLATFORM_APP_CATALOG overrides the mirrors cannot see. The strings are synced so a reader is not misled, and the file header says not to start rendering them. The parent/child tree is deliberately NOT mirrored — the features endpoint sends parent/children/isParent per app, and a second copy of the shape would be one more thing to drift. Also: an icon per new vertical rather than borrowing the parent's storefront, and Organization getters including isEhr.

  18. · kamo-internal

    One scrollbar, three times as wide, down every conversation

    The bar beside the messages was 8px in the member chat and SMS, 6px in support tickets and social, and in the AI chat it was nothing at all: that list declared `scrollbar-width` inline beside its ::-webkit-scrollbar rules, and since Chrome 121 a non-initial `scrollbar-width` or `scrollbar-color` makes the browser discard the whole -webkit- block — so it asked for 12px and got the platform hairline. globals.css documents that rule for the page scroller; nothing enforced it, and the AI chat had quietly been on the wrong side of it. All five now read one CHAT_SCROLLBAR constant: a 24px gutter — three times the member chat's old width — with the thumb inset 6px either side by a transparent border, so a bar that wide still reads as a pill in a trough rather than a slab welded to the window edge. The AI chat's standard properties move into `@supports not selector(::-webkit-scrollbar)`, which is where globals.css puts them and the only place Firefox reads them without costing Chrome its rules. The test is source-level because the subject is a stylesheet and jsdom implements no scrollbars: it pins that no window sizes its bar from a literal, and that none of them sets the standard property unguarded again.

  19. · InitializerService

    Extend market grandfathering into the app-tree backfill

    grandfatherExistingGatedMarkets already walked each org's active markets, resolved getRequiredService() and enabled the missing app — it was just restricted to isGated() verticals, i.e. MORTGAGE and PERSONAL_LOANS. Now that every CommerceType names its own child app the restriction is gone, so the same loop seeds RETAIL / RENTAL / SERVICE / SUBSCRIPTION / PROCUREMENT rows. No new migration was needed. Also seeds RETAIL for orgs that hold POS with no markets at all, which had working commerce before the tree because retail needed no app of its own. Top-level orgs are no longer skipped, and that is the fix that matters. The exemption was right when this only granted the two lending apps — a platform-operator org should not silently acquire MLOS — but it is wrong for the app tree, because isAppEffectivelyEnabled requires an OrgFeature ROW and has no top-level exemption. The first run reported success while leaving KamoCRM, which runs active RETAIL and SUBSCRIPTION markets, with no rows for either; it would have lost both from /commerce on the next SecurityService deploy. Caught by querying the database, not by reading the log. enableFeature now cascades a child to its parent, so seeding a child also guarantees the POS row. RUN against prod 2026-08-24. Verified: every active commerce_markets row now has an active child OrgFeature row, zero missing.

  20. · SecurityService

    Entitlement-check every vertical, not just the gated two

    Every CommerceType now names its own child app under POS, so the isGated() short-circuit that waved retail, rentals, service, subscriptions and procurement straight through is gone. Those markets are now checked against their own app — which is the point: a vertical can be sold, and switched off, on its own. Depends on the KamoInitializer backfill having run.

  21. · kamo-internal

    The toolbar's search is a glyph that opens into a field

    The member chat carried a permanent search box across the top of the window — the width of the whole toolbar, spent on a control that is empty almost all of the time and used in bursts. It is now a 28px magnifier that opens into the field on click, and the toolbar reads as four glyphs and air. The idea it is built around: the magnifier never moves and is never swapped out. Collapsed, the pill is 28px and the icon sits at its centre, 14px from the left edge; open, the pill has 6px of padding before a 16px icon, which puts the icon's centre at 14px again. The pill therefore grows out from behind a glyph that holds still, and the control reads as one object opening rather than two states crossfading. The width springs with a little overshoot on the way open and none on the way closed; the placeholder trails the growth by 60ms so the text arrives in a box that already has room for it; the clear control springs in only once there is something to clear. Closing always clears the query, and the field cannot be put away while it holds one: a collapsed magnifier still filtering the conversation would be the window lying about what it is showing. Blur leaves it open with text in it, and a stray click on the glyph focuses the field rather than discarding the search. Reduced motion is honoured throughout, the glyph carries the focus ring for a Tab as well as a click, and the 50px spacer the old box needed is gone with it.

  22. · kamo-shared-library

    Every vertical names its own child app

    requiredService becomes total now that each vertical has a child app under ServiceType.POS, which deletes the "null means POS alone is enough" special case and isGated() with it. Each vertical can now be switched on, licensed and PHI-classified independently — which is what lets a medical practice use commerce at all, since RETAIL stays blocked for a PHI tenant while the rest of commerce does not. HEALTHCARE(ServiceType.EHR) is what makes the EHR appear at **************** when the app is on, with no new gating code: assertCommerceTypeAllowed, getAllowedCommerceTypes, the allowed-types endpoint behind both market pickers, **************** and the market list filter all already resolve through getRequiredService(). NOT PUSHED until the KamoInitializer backfill has run. assertCommerceTypeAllowed reads getRequiredService() directly, so the moment SecurityService sees this every org with a retail or rental market needs a child OrgFeature row it does not have yet.

  23. · MediaService

    Let the public config be addressed by org id, not only by host

    The meet sidecar looks up an org's meet config to decide whether a guest needs a meeting password. It asked by hostname, which on the shared meet host resolves to the platform organization -- so a tenant that requires a password would have had that requirement silently dropped for every guest joining by link. A guest has no session, so the only thing naming their tenant is the room, which now carries an org token. Both public endpoints therefore accept an explicit orgId and fall back to the domain lookup when none is given, so nothing that asks by host today changes. Ids arrive as digits and are parsed as a long: they are past 2^53, so one routed through a double names a different organization. 200 tests pass.

  24. · kamo-shared-library

    Cascade feature enable/disable across the app tree

    Enabling a child enables its parent; disabling a parent disables every child; disabling the last child disables the parent. Enabling a parent leaves children alone, so a parent switched on deliberately with no children stays on. The cascade is event-driven and there is deliberately no reconciliation sweep. That is a safety property: a continuously-enforced "a parent with no enabled children is off" invariant would switch commerce off for every existing org the moment this shipped, because no child rows exist until the KamoInitializer backfill runs. A child asserts the PHI guard on its parent's module as well as its own, before any write — otherwise a permitted child under a blocked parent would either be stranded or would silently switch the blocked parent on.

  25. · kamo-shared-library

    Add PhiCapabilityGuard for call-time capability checks

    PhiTenantGuard runs at feature-enable time and nowhere else — three call sites, all in FeatureService. Now that PhiModule is capability-grained, an app whose core is permitted can still contain a capability that is not, and enable-time checking cannot see the difference. Same deliberate signature as PhiTenantGuard: a pure function of (status, module) with no caller, right or god-mode parameter, so there is nothing to elevate. Plain class rather than a stereotype — a shared-lib @Component needing an unscanned repository has crash-looped services four times; each consuming service declares its own @Bean. NOTE: MEETING_RECORDING and VOIP_RECORDING are now unbound from any ServiceType but not yet guarded at their call sites — that is the next commit touching MediaService and VOIPService. This is inert in production meanwhile because every org currently has handlesPhi=false, but the call sites must land before any tenant is flagged.

  26. · kamo-shared-library

    Split PhiModule from app-grained to capability-grained

    POS mapped to ECOMMERCE_SYNC (BLOCKED_NO_BAA) and the whole commerce surface hangs off POS, so an org with handlesPhi=true could not switch commerce on at all — no market, no /commerce, and no clinical vertical built on it. The ten retail sync adapters are the thing that leaves the boundary, not the commerce spine. POS now maps to COMMERCE_CORE (permitted) and ECOMMERCE_SYNC scopes down to the RETAIL child. Same correction for MEET (live conference vs the summary pipeline that mails transcripts to external inference) and VOIP (telephony vs stored recordings and transcripts). MEETING_RECORDING and VOIP_RECORDING now have no ServiceType at all: they are capabilities reachable only through code, and PhiModule's javadoc now states the rule that such a constant MUST be checked by PhiCapabilityGuard at its call site. Reclassifying an app as permitted without guarding the capability inside it reads as compliant while enforcing nothing. AI_ASSISTANT and RAG_SEARCH deliberately stay blocked: KlusterServices has embeddings, whisper-asr and translation but no generative LLM, and RAG shares one Qdrant collection isolated by a payload filter. Splitting those would be a paper change. Two tests encoded the app-grained model and are updated rather than extended: PhiServiceTypeMappingTest's blocked-app assertion, and FeatureServicePhiGateTest, whose blocked exemplar moves from VOIP to RETAIL — VOIP is no longer refusable at enable time.

  27. · kamo-internal

    Land inside a newly created org instead of at DNS setup

    Creating an org routed the creator to /setup/dns, and the reason was real: entering meant an OTK redirect to internal.<new-domain>, and a brand-new org has no DNS, so that host was unreachable. Configuring DNS was therefore the price of opening the thing you had just made -- the exact wall this work removes. The create call already returns a one-time key; the wizard just had nowhere to spend it. It now spends it on the current origin, where the session names the org, so there is no host to wait for. A domain is still worth having and Settings is where it lives now: an optional white-label step rather than a doorway. Branding warnings are carried through either way, and if no key comes back the org still exists, so the wizard lands on the estate view rather than failing. check-client-navigation correctly flagged the window.location.href. Its own docstring names /validate with a one-time key as a genuine session-boundary jump, so this is allowlisted with that reason rather than worked around -- the router would keep the creator's OLD session, which is precisely what is being replaced. 2857 tests and all 7 guards pass.

  28. · kamo-shared-library

    Give ServiceType a two-level parent/child tree

    Six commerce children at new ids 26-31, wired to POS in a static block on the RoleRightType pattern (an enum constant cannot reference another from its own constructor arguments). MLOS and LOS reparent without changing id, because ORG_FEATURES persists the id via ServiceTypeConverter and moving one would reassign every existing row. POS's copy becomes "Lines of Business" now that it parents lending and patient records; the constant name stays POS because **************** persists names as strings. Its old description moves to RETAIL, where it is accurate and already vetted by ServiceTypeCustomerCopyTest. New constants ship PLANNING/disabled so FeatureController.list's auto-provision loop cannot switch them on for every org. PhiServiceTypeMappingTest fails by design until the next commit maps the new constants; both land in one push.

  29. · kamo-internal

    Make an org without a domain enterable

    Entering an org meant navigating to **************** With no domain that URL rendered as the literal **************** so Enter was disabled and owners were routed to /setup/dns instead -- DNS was the doorway, not a white-label option. The workspace now opens on the CURRENT origin when there is no live custom host, and the session names the org. A tenant with a working custom domain still opens on its own host, so nothing changes for anyone white-labelled. An unfinished custom domain is treated like no domain: that HOST cannot serve yet, which is not the same as the org being unreachable. canEnterOrg therefore keys on the thing that was always the real precondition -- a usable membership -- rather than on a domain. needsDomainSetup and orgStatus deliberately keep reporting an unfinished domain: it is no longer a barrier, but it is still somebody's outstanding task, and the estate view is where that gets noticed. The shell also had to stop asking the hostname which org it is in. The root layout stripped "internal." and looked the host up, which answers "who owns this name" -- the same question as "which workspace am I in" only while every tenant had a host to itself. On the platform host it returns the platform owner, so a member entering a domainless org would have been shown KamoCRM Inc.'s branding, features and rights. It now asks the session first via /api/org/current and falls back to the host when there is no session, which keeps the signed-out shell rendering. The *** cookie is what makes that reachable at all -- sessionStorage is invisible to a document request. 2857 tests and all 7 guard scripts pass.

  30. · SecurityService

    Let the shell fetch its org from the session, not the host

    kamo-internal's root layout reads the Host header, strips "internal." and fetches /org/domain/<that>. On a host serving one tenant that is the same question as "which org am I"; on a shared host it is a different question with a wrong answer -- it returns whoever owns the host, so a member who entered a domainless org would be handed KamoCRM Inc.'s branding, features and flags. Adds GET /api/security/org/current, returning the projected record for the organization the caller's SESSION is in. The layout can reach it because it already forwards the Cookie header and a *** cookie exists, so the server can see a session even though sessionStorage is invisible to a document request. Takes no parameter, deliberately. The first draft accepted an id or alias and **************** correctly refused it: that shape makes every org's record enumerable by guessing an alias, and aliases are guessable. The session already knows which org it is in, so a parameter could only ever ask about a different one. With the session resolved the ratchet passes on its own rather than needing a baseline entry. The projection is extracted rather than copied. Two endpoints returning two shapes of the same record is the failure worth preventing -- a client cannot tell which it holds, and they drift the first time only one is updated. 928 tests pass.

Your universe is waiting.

Start free in minutes. Add products whenever you are ready — no migration, no new logins.

Free forever — no credit card required