Skip to content
KAMOUniverse

Changelog

What's new.

Every improvement to the platform, as it ships.

  1. · DocsService

    Outbound email client and a Redis notification ledger

    Idempotency with NO schema change. The reminder bucket is a pure function of (dueDate, today) and never of "when we last sent", so the only durable state a notification needs is a per-(assignment, bucket, dueDate, channel) claim — and SET NX in the Redis DocsService already runs (WopiLockService, KSessionService) has exactly the semantics of the UNIQUE constraint the design asked a table for. It is atomic across the RollingUpdate replicas, the due date is IN the key so extending a due date legitimately restarts the ladder, and it leaves the sweep entirely READ-ONLY against CockroachDB: no write transaction is ever open while EmailService blocks on SMTP. Claim policy is at-most-once by default. An observed failure shortens the claim's TTL so the next tick retries; a crash between send and mark does NOT retry, because a missed reminder is recovered by the next bucket and a duplicate compliance email to every employee of every tenant is not recoverable at all. Redis being unreachable refuses the send rather than sending unclaimed. packageTitle and voidReason are HTML-escaped Java-side: TemplateRenderer is a flat regex replace with no escaping, so HR-authored prose otherwise reaches every employee's inbox as markup, sent from the org's own verified domain. Every varying sentence is pre-rendered as one whole-sentence variable because there are no conditionals and no pluralization either. Persistence is one seam (LegalNotificationRecorder) writing the Redis ledger plus one structured legal-notify line. The durable evidence table is REPORTED in the plan, not invented: a new shared-lib entity is a KamoInitializer run across every consumer.

  2. · DocsService

    DocsService's first inbound internal-auth filter

    DocsService has no service-to-service authentication: ResourceServerConfig is anyRequest().permitAll() and OTKPreAuthFilter CONTINUES the chain when X-OTK is absent. Authorization is per-controller — an HR right on LegalPackageController, identity on LegalMemberController — and both need a session a daemon does not have. A mapping that fits neither funnel is therefore fully anonymous through APIService's /api/docs/** forward, so the filter ships BEFORE the endpoint it guards, not after. Copied from **************** including the two behaviours that matter: an unconfigured internal.auth.secret answers 503 rather than passing through (reading "no secret" as "no check" is how a control becomes a comment), and the comparison is MessageDigest.isEqual, which does not return early on the first differing byte the way String.equals does — against a freely callable endpoint that is a byte-at-a-time oracle for a cluster-wide secret. Ownership is segment-exact rather than a bare prefix, so /api/docs/legal/internal and everything under it is guarded while an unrelated future sibling is not 403'd by accident. The property is internal.auth.secret (public-chat), NOT mlos.internal-auth-secret.

  3. · kamo-shared-library

    Four canonical HR legal-package templates

    hr.package_assigned, hr.package_reminder, hr.package_voided and hr.package_completed, as {json,html} pairs plus four CANONICAL_KEYS entries. The CONSTANT is not the templateKey and that seam is where the live QUOTE_SENT bug lives: the loader derives the FILENAME from the constant **************** and reads templateKey out of the JSON, while EmailTemplateService.sendToUser looks the org row up by the key the CALLER passed and, on a miss, seeds rows keyed by the JSON value and retries the same caller key. QUOTE_SENT -> quote-sent.json -> commerce.quote_sent therefore seeds a template no caller can reach. HrPackageTemplateTest pins all three legs for the four HR keys, and pins a file pair for EVERY canonical constant. Rendering is flat {{var}} regex substitution with no escaping, no conditionals and no pluralization, so every sentence that varies is pre-rendered Java-side as one variable (documentSummary, dueLine, duePhrase, reasonLine, receiptLine) and the tests refuse {{#if}}/{{/}}/{{^}}. Every body names {{teamMemberLabel}} and none contains the literal "Team Member": the org's label does not reach EmailService any other way — sendToUser injects only orgName, logoUrl and logoSimpleUrl. No entity, no repository, no enum, no schema. pom.xml stays 1.5.0.

  4. · kamo-internal

    Signature button with a live legal-package badge

    Hand-written into the fixed left cluster between My Profile and Log out. NAV_REGISTRY cannot express a position between two fixed controls and its badgeKey field is dead code, so this is not a registry entry. Gated on getMemberType() === 'TEAM_MEMBER', never on checkRight(): checkRight returns true for every right under active god mode, and this button leads to the evidence of what a specific person personally agreed to. getMemberType() defaults to 'MEMBER', so the gate fails closed. Badge idiom copied from Chat Now — 99+ cap, color="error", invisible at zero. Icon-only with an aria-label, and navigation through <Link component={NextLink}> because check-client-navigation.mjs fails the build on window.location.href. The tooltip reuses hr.tabs.legalCompliance, the same key the member page heading already calls, rather than inventing one.

  5. · kamo-internal

    Live pending-package count over the EXISTING chat socket

    LegalPackageCountContext consumes **************** rather than opening a fourteenth STOMP connection for one integer, and subscribes to **************** — the destination MediaService relays legal.packages.{memberId} onto. The REST seed is mandatory, not an optimisation: that subject is core NATS with no stream, so a frame published while the socket was down is gone. Seed on mount, on every reconnect, and on a 60s floor. With no nats block in the DocsService configmap — today's state — those three paths ARE the feature. Everything testable is a pure function in app/lib/legalPendingCount.ts; vitest runs in node and collects only .test.ts, so a context test would not run. parsePendingCount never throws (an exception in a stompjs handler takes the frame loop down and every other subscription on the shared client with it) and refuses a payload addressed to another member, as defence in depth behind the MediaService SUBSCRIBE guard. Ids are compared as strings and never coerced: getMemberIdString(), not the lossy getMemberId().

  6. · MediaService

    Relay the legal-package count and guard per-member SUBSCRIBEs

    legal.packages.> -> **************** core NATS, no stream — the same shape SupportStompRelayController and PlatformStompRelayController already use. topicFor is a package-private static so the contract between DocsService and kamo-internal is testable without a broker, and it refuses a multi-token tail so the guard only ever sees single-token member ids. The guard is the other half, and it closes a real gap: only /topic/social/feed/{orgId} and /topic/leads/rt/{orgId} were checked, so every per-member topic this broker carries was subscribable by any authenticated member. Relaying a per-member compliance count onto an unguarded topic would have let any employee watch a named colleague's backlog. The decision moves into a pure StompSubscribeAuthz.allow so it can be tested at all — the interceptor is anonymous inside a @Configuration. Both existing org rules are pinned by test before the move, and the function stays an allowlist of guarded prefixes: eleven other topics run unguarded today and flipping the default would take chat, presence and unread down together. Comparison is String.valueOf on both sides. This service stores memberId as a Long and EmailService as a String; a guard copied across without that silently never matches.

  7. · kamo-internal

    Proxy the member's pending-package count

    A static segment sibling to [uid] so App Router matches it first. PATH ONLY — forwardToApi owns the query string — and one upstream call, because the OTK is single-use.

  8. · DocsService

    Serve the caller's own pending-package count

    GET **************** through memberGuarded() and the identity funnel. No memberId and no orgId parameter — both come from the session, because an id in a path is an id somebody can change. Mandatory, not optional: legal.packages.{memberId} is core NATS with no stream, so a frame published while a member's socket was down is gone. This is the only correct value after a reconnect. The response shape is IDENTICAL to the pushed frame, so the client parses both through one function and the seed cannot drift from the push. memberId is a string; count is a number.

  9. · DocsService

    Emit the badge count on every transition that moves it

    Four sites: delivery (none -> PENDING), void (open -> VOIDED), redelivery (open -> SUPERSEDED) and finish (open -> EXECUTED). Each passes the status it read BEFORE the write and the one it wrote, and LegalPendingCountTransition decides — so the rule lives in one testable place instead of four. Two status writes deliberately do NOT publish. PENDING -> IN_PROGRESS is stamped every time a member opens a package and both states are OPEN, so it would put a STOMP frame (logged at INFO with full payload) on the wire per page view. The filer's EXECUTED <-> EXECUTION_INCOMPLETE cannot cross the boundary at all: its FILEABLE set is exactly those two. Finish into AWAITING_OTHERS publishes nothing by the same rule. The member signed, the instrument is not complete, and clearing the badge would say otherwise.

  10. · DocsService

    Publish the member's open-package count to core NATS

    Connection.publish on legal.packages.{memberId}, NOT NatsMessageService: that service pins .expectedStream(CHAT_MESSAGES) in both publish and publishSync, so a JetStream publish on a legal subject is refused by the server and every count update would vanish silently. The count is read inside the caller's transaction (Hibernate auto-flushes before the count query, so the value is the post-commit value) and only the send is deferred through LegalAfterCommit. Reading from afterCommit would query a completed transaction; a rollback needs no handling because afterCommit never fires. Nothing here throws. Spring propagates an afterCommit exception to the caller, and a dead broker must not turn a Finish that already wrote an append-only attestation into a 500. natsConnection is required=false and is null in production today — DocsService has no nats block in its configmap — so the publisher degrades to a no-op and the REST seed carries the badge. memberId is a STRING in the payload. It is a unique_rowid() past Number.MAX_SAFE_INTEGER and the browser parses this with JSON.parse.

  11. · kamo-internal

    Give the executed-package repair an actual button

    POST **************** existed upstream and was tested, but nothing in the product could reach it: no proxy route, no helper, no button. Every failure branch in LegalExecutedPackageFiler is designed around "HR repairs it" — a flatten that never landed, bytes that drifted since delivery, a subject below Team Member, a conversion mark that failed — and the Assigned Packages grid rendered all of them as an "Error" chip with nothing beside it to click. The action sits in the slot Download vacates, so a row shows exactly one of the two. canFileExecutedPackage mirrors the backend's FILEABLE list exactly, so no button can exist whose only outcome is a 409, and it keys on executedImgId being absent rather than on status alone — an EXECUTED row whose artifact never attached is the silent shape, the one case an operator would never think to retry. The upstream status and reason are preserved end to end rather than flattened into "Load failed": 409 here says WHICH repair is needed, and "that member's security level is below Team Member" is something HR fixes and files again while "a document is still missing" is not. PATH ONLY through forwardToApi, one upstream call for the single-use OTK, existing i18n keys only (retry/success/error, all three verified present and non-empty in all 22 locales), ids stay strings.

  12. · kamo-login

    Forward the login host so recovery can resolve the org

    Password recovery now accepts a member's primary mailbox address, which only names an account within one org. This proxy reaches SecurityService over cluster DNS, so Host is the service name and the browser's host has to travel in X-Forwarded-Host — the only header the backend reads it from. `login` is a registered alias, so login.acme.com resolves to the acme.com org as-is; no rewrite to api.*, which is the APIService gateway's requirement and this route bypasses it.

  13. · SecurityService

    Accept a member's primary mailbox address as a login identifier

    The address a member gives out is usually the org mailbox flagged Primary on their member settings, not whatever is on their user account — so typing it into the login form failed, and forgot-password for it silently did nothing. Both now fall back to it, after the account username/email has had its say, so account credentials still win and no existing login changes meaning. A mailbox address only names an account within one org, so the fallback is scoped to the org resolved from the login host; login already did that resolution, which now lives in OrgHostResolver so recovery can share it. The account query re-runs by resolved id, leaving every gate — disabled, locked, expired, membership active, password — exactly where it was. An address that names two accounts resolves to neither. The recover proxy in kamo-login has to forward the browser's host for any of this to reach the backend; EMAIL_NOT_VERIFIED now carries the userId, since the verification hand-off could no longer re-find the account from what was typed.

  14. · kamo-internal

    Download the executed package from the grid and the member's own page

    Both surfaces go through the LEGAL endpoints, keyed on the assignment, not /api/imaging/download/{imgId}: that path additionally needs organization.isDocs and VIEW_DOCUMENTS and checks a session security level baked in at login, and design §8 is explicit that DocManager is the convenience copy and not the access path. executedImgId stays the presence signal — the action is ABSENT, not disabled, while nothing has been filed, because a Download button that 404s reads as a lost document. assignmentStatusKey stops mapping EXECUTION_INCOMPLETE to common.states.incomplete, which is semantically INVERTED in pt. It has been harmless because nothing in the platform has ever written that status; Phase 6 is the first thing that can, and a Portuguese member would have read "complete" over a package with no artifact behind it, on their own employment record. The file name is built from data and sanitised: a package title is free text an HR admin typed, and it reaches both a Content-Disposition header and the user's filesystem, where "/" makes a path and a quote terminates the header.

  15. · kamo-internal

    Draw non-image attachments with the document manager's file-type icons

    A staged .docx or .xlsx was a grey MUI outline with the extension lettered underneath — a shape invented for the tray, and one that says less than the icon the same file gets the moment it lands in DocManager. Key react-file-icon by extension through defaultStyles exactly as DocManager does, so an attachment looks the same in a draft as the document it becomes, and an unrecognised extension falls back to the same generic sheet.

  16. · DocsService

    File after Finish, repair from HR, and download BY imgId

    Filing is queued AFTER COMMIT and OFF the request thread. Finish is @Transactional in a service that also serves WOPI; holding a connection across N ESigService reads, a PDFBox merge and a MinIO write is how a pool of ten starves. Submitting from inside the transaction would also race the commit, which is the failure LegalAfterCommit was written for. A failure in the worker is not a failure of Finish: the attestation is written and the row falls to EXECUTION_INCOMPLETE. Downloads resolve by imgId through the legal endpoints, never through POST /api/imaging/list — that pages org-wide BEFORE filtering assocObjectId, so a member's own package silently vanishes past 100 rows at assoc 1, which the lead Files tab shares. And never through ImageService.downloadDocument either: that checks the CALLER's stale-able security level, and design §8 is explicit that the assignment record is the access path and DocManager the convenience copy. Three mappings, three authorizations: the member's is identity, HR's read is the HR read right, and HR's file action is the WRITE right because it uploads a document into someone's library. The file action is synchronous — an admin clicked it and "that member's security level is too low" is the answer they need, not a 200.

  17. · DocsService

    File the merged executed package into the member's own library

    uploadDocument takes the SUBJECT'S Member, so Img.organization and Img.createdByMember derive from them and HR ends up having filed a document the member owns. Assoc is SERVICE_APPLICATION — ORDINAL 1, what /settings/member/{id}?tab=docs renders — with assocObjectId = the member id, NOT My Docs. accessLevel is the int 2 and never null; description is the package title; both hashes are computed here because uploadDocument recomputes them and rejects a mismatch. The ImgDat is marked converted. uploadDocument does no conversion and no thumbnails: without the flag the inline-PDF endpoint answers 202 "Conversion in progress" forever. Thumbnails are deliberately absent — the DocManager grid shows a file icon instead of a page preview for this row, and rasterising pages is ConversionService's pipeline, not this one. Filing is REFUSED when the subject's security level is below Team Member. ImagingController skips a too-high document silently and answers a clean empty list, so filing at level 2 for a member at MEMBER(1) would hand them nothing over their own employment agreement. It is refused when a signable document's flatten never landed and when an acknowledge-only document's bytes drifted: a package missing a signature page, or carrying bytes the member never read, is worse than no package. All three leave EXECUTION_INCOMPLETE, which is exactly what that status was declared for. Idempotent by executedImgId, transactions explicit, and every network and MinIO call outside all of them.

  18. · kamo-internal

    Say what is too big, instead of "Failed to send email"

    A draft that outgrew the send endpoint's limits produced one flat line of red text after the whole payload had been uploaded — or, when the body itself was the problem, nothing until the send failed. Three things decide what a member does next: which piece is too big, how far over it is, and what to do about it, so the notice states the size, draws the overage, and names the fix. SendLimitNotice is a sibling of ComposerNotice and is built from the same tokens, but answers a different question: nothing was rejected and there is no file to point at — the message itself has grown too big. Its bar is the whole message and the dark tail is the part that does not fit, because byte counts alone don't say whether to drop one image or half the draft. Any real overage is drawn at a visible minimum; a tail thinner than a pixel would say the opposite of the sentence above it. The check runs before the upload starts. The server cannot answer usefully once it does: an oversized part is refused in the servlet container ahead of any handler, and on a large overage Tomcat drops the connection rather than swallow the rest of the body. Its 413 is still honoured as a backstop, with its numbers preferred over ours, for the case where the two sets of limits have drifted apart. The notice does not auto-dismiss — it is a blocking condition, not a status, and four seconds is not long enough to read it and act. Also carries the concurrent composer/attachment work in progress, at the user's request.

  19. · kamo-internal

    Gate each Human Resources tab on its own right

    Overview needs VIEW_OVERVIEW, Attendance MANAGE_TIMECARDS, Legal & Compliance MANAGE_LEGAL_AND_COMPLIANCE, Training MANAGE_TRAINING and Resources MANAGE_RESOURCES. Each tab's gate is derived from that one name, so the gate a tab is hidden by and the one it is refused by cannot drift apart. The HR nav option — and with it the home launchpad card and the command-palette entry, which both render from the registry — now appears only when the HRS app is on for the org AND the member holds at least one of the five. It used to admit any of the eleven HR record rights, which offered the module to members every tab of it would refuse.

  20. · DocsService

    Enforce MANAGE_LEGAL_AND_COMPLIANCE on the package endpoints

    LegalPackageAccess mirrors the Legal & Compliance tab gate, and that tab is now gated on one right of its own instead of VIEW_EMPLOYEES || EDIT_EMPLOYEES || MANAGE_HRS_SETTINGS. A server set narrower than the tab's leaves a 403 toast over an empty grid; wider, and the endpoint is reachable by someone the UI never offered it to. Read and write are the same right because the right says manage — which is why the SecurityService migration seeds it from the old write pair only, so nobody gained publish-and-void on upgrade. Viewing employees no longer dumps the roster through audience-preview.

  21. · SecurityService

    Carry existing HR holders onto the new per-tab rights

    A right that gates something already reachable is a removal of access: new rights insert as NOT_SPECIFIED, so the /hr tabs would have vanished on deploy for every HR administrator whose role is not full-access. NEW_RIGHT_SEEDS declares each new right's lineage — the OR-group its tab was gated on before — and it is applied wherever a grant can live: at insert time for org roles and security-model templates (mirroring FORCE_GRANTED as FORCE_GRANTED), and via seedOverrideRights() for job titles, departments and member profiles. Those three tables hold a row only where somebody set one, so insert-where-absent would resurrect a cleared right on every boot. The marker is org_role_rights: a right with no row there has never been seen by this database, which is true exactly once. MANAGE_LEGAL_AND_COMPLIANCE deliberately seeds from the old WRITE pair only, so no read-only viewer was handed publish-and-void by the upgrade.

  22. · kamo-shared-library

    One security right per Human Resources tab

    The /hr tabs were gated on whichever pre-existing HR right happened to be an OR-term in the front end: Attendance opened on VIEW_SCHEDULES, Training on VIEW_EMPLOYEES, Overview and Resources on any HR right at all. An org could not grant payroll without also handing over Legal & Compliance. VIEW_OVERVIEW(182), MANAGE_TIMECARDS(183), **************** MANAGE_TRAINING(185) and MANAGE_RESOURCES(186) gate a tab each. The record-level HR rights still govern what a tab may show and change.

  23. · kamo-internal

    Measure a draft against the send limits before uploading it

    A body carrying inline base64 — HTML pasted from a web page, a forward's quoted content, a signature's images — can pass the per-part ceiling on its own. The send endpoint refuses it in the servlet container ahead of any handler, and on a large overage Tomcat drops the connection rather than swallow the rest of the body, so the composer had a failed upload and nothing to say about it beyond "Failed to send email". checkSendPayload measures the exact JSON that goes on the wire, plus the attachments and the multipart framing, against the same limits EmailService enforces. sendError now reads the MESSAGE_TOO_LARGE reply and the limits that applied, so a server whose limits have moved is still described accurately rather than against a stale constant.

  24. · DocsService

    The executed package certificate and the ordered merge

    The certificate carries the package, the frozen version, the subject, the moment and address of execution, the attestation wording verbatim and both digests the WORM row carries — so the artifact and the ledger row can be checked against each other with no database in the room. Every string is sanitised to WinAnsi first. PDFBox's Standard-14 fonts encode WinAnsi only and showText THROWS on anything else; package titles, member names and authored wording are free text, so a Japanese title or a curly apostrophe pasted out of Word would otherwise be the reason a member's package could not be filed. Unencodable characters are substituted visibly, never dropped. Everything paginates: HR authors clauses of any length and a forty-document onboarding pack is ordinary. The merge refuses a gap. mergePdfs skips an unconvertible document and prepends a report of what it left out — right for an ad-hoc export, wrong here, because a package that quietly omits one document still looks like the executed instrument. Order is the order manifestSha256 hashed, so reordering would produce an artifact the attestation cannot vouch for.

  25. · kamo-shared-library

    Expose the content-addressed store so chat attachments can dedup

    Chat attachments never went through the hashing pipeline — they wrote a private "<imgId>/<fileName>" object per upload, so the same file attached three times occupied three objects. They could not simply call uploadDocument: that flow validates client-supplied hashes, and an attachment arrives as a plain multipart with none. storeContentAddressed is the storage half on its own — hash, reuse-or-create the ImgDat, upload only if the content is new. Callers attach the returned dat to their own Img row, so an attachment stays individually access-controlled and revocable (the row is the permission; the dat is only the bytes) while the bytes are shared with every other attachment — and every document — of the same content.

  26. · DocsService

    The two byte sources for the executed artifact

    LegalPinnedPdf extracts the frozen-digest rule out of the member read path so the wizard and the filed artifact read the SAME bytes through the SAME check — contentSha256 hashes the img_dats content-address triple, so the comparison costs no MinIO read and differs if and only if the bytes moved, and saveImgContent repoints img.setDat on every Docs save. Modifications are applied, because that is what the member was shown. **************** reads the FLATTENED PDF from the service that produced it. Nothing in ESigService changes: the internal endpoint already exists behind the same X-Internal-Auth + X-Org-Id pair. Re-flattening here would be a second set of signature-placement bugs over bytes ESigService has already committed to. 404 comes back as an empty Optional rather than an exception. EsignPublicService wraps flatten in try/catch { log.error }, so an envelope can sit SIGNED with a null signedBlobId forever — that is exactly what EXECUTION_INCOMPLETE names, and the caller refuses to merge an artifact missing a signature page.

  27. · kamo-internal

    Member legal-compliance page and the package execution wizard

    **************** is gated on identity (memberID === getMemberIdString(), compared as STRINGS because getMemberId() is lossy past MAX_SAFE_INTEGER) and includes the early `return null` the member settings page omits — that page renders its tabs for the frame before its redirect lands, which here would paint another member's package list. Never checkRight(): it returns true for everything under god mode. The wizard is the only genuinely new UI: an outer per-document step axis in orderIndex order plus a terminal attestation step. It resumes at the first incomplete step rather than at zero, refuses to skip a document (the manifest asserts the member was presented every one, in order), renders the counter from data because common.table.of is an empty string in pl, and shows a persistent awaiting-other-parties panel instead of re-running Consent -> Sign for a signer who already signed. Signable documents render the bytes ESigService flattens; acknowledge-only documents render DocsService's version-pinned bytes. A 422 from submit is surfaced verbatim — it means the server's FieldCompletion disagreed with the ported isFieldComplete, which is a port bug and must not hide behind a generic toast. Finish does NOT treat 409 as success, against the plan's sketch: LegalPackageAttestationService returns 200 with the existing attestation for an already-finished assignment, so a 409 from that endpoint means NOT_OPEN / NO_WORDING / NOT_DELIVERED / NOT_SIGNED / UNVERIFIABLE — VOIDED and SUPERSEDED among them. It re-reads the assignment and only reports success when the row is really attested.

  28. · kamo-internal

    Member execution proxy routes and the pure wizard model

    Eight 1:1 routes: /api/docs/legal/member/** through forwardToApi (PATH ONLY — forwardToApi owns the query string) and /api/esig/member/** direct to ESIG_SERVICE_URL with generateOTK, because APIService forwards /api/docs/** and nothing forwards /api/esig/**. One OTK, one upstream request per route: OTKAuthFilter does getAndDelete. The PDF routes stream bytes and preserve the upstream status, so a 409 (the frozen content digest no longer matches the stored bytes) reaches the wizard as a 409 rather than as a corrupt PDF. Consent is the one route that must not parse its success body: ESigService answers it with an empty 200. Every wizard rule lives in a pure module because vitest here collects only .test.ts in the node environment. A signable document renders from the esign route — those are the bytes ESigService flattens, and what you see must be what you sign; an acknowledge-only document renders from DocsService's version-pinned route. The step axis is orderIndex order and the attestation step exists even for a package with zero documents to sign.

  29. · kamo-internal

    Port the kamo-signer signing core into kamo-internal

    Every @kamo/* package is private:true with exports:"./src/index.ts" and no build step, so there is no publishable artifact — the files are copied and rewired onto relative imports. Behaviour is unchanged; only imports and i18n move. pdfjs-dist 4.10.38 with a SELF-HOSTED worker copied by a prebuild step. Not **************** that loads pdf.js 4.4.168 from cdnjs, which is a different engine and unacceptable for a legally binding surface, and the two GlobalWorkerOptions objects stay uncorrupted only because the module instances never meet. Not FieldElement.tsx either: its FieldControl renders pointerEvents:'none' previews with no value binding — a designer preview, not an input. useTranslations comes out of the four signing components: they call kamo-signer's own message keys, kamo-internal's dictionary does not have them, and new keys cannot be invented. Strings arrive as props. SIGNATURE_FONTS is a prop too rather than four next/font/google loads for one modal; the typed font id still rides through CapturedSignature.typedFont so flatten and the certificate are unaffected. Two marked deviations from a verbatim copy, both in signStore: - required-ness now skips readOnly fields. kamo-signer counted them; ESigService's FieldCompletion — the shipped server-side enforcement — does not ("if (f == null || f.readOnly() || !f.required()) continue;"). Keeping the signer copy stricter than the server would park a member on a gap they have no control to close while the server considers the envelope complete. - draft persistence guards localStorage instead of window. window is the wrong question: localStorage can be absent (server render) or throw on access (private mode, partitioned storage), and a draft must never take the wizard down with it.

  30. · DocsService

    Mount the member surface at /api/docs/legal/member/**

    A separate controller with a separate funnel, because authorization here is identity and not an HR right. Routing these through guarded() would mean either locking every employee out of their own paperwork or widening LegalPackageAccess until the templates grid and the audience preview's roster are open to the whole org; the class does not hold a reference to LegalPackageAccess so the shortcut is not available. No mapping takes a member id or an org id — both come from the session. Service ResponseStatusExceptions keep their status: 404 not-yours, 409 already-finished, 422 not-acknowledged and 503 esign-unavailable are four different instructions, and one of them means the member already succeeded. An unexpected exception is a fixed-string 500: echoing e.getMessage() is tolerable on the HR surface, but this one every employee can reach. KToken extraction moved to security/SessionKToken so this is not its third verbatim copy.

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