Repository navigation
Release/80 - #10540
Merged
Merged
Release/80#10540
Conversation
* fix(i18n): Dutch copy pass - 'betrokkene' for associates, informal 'je', wording fixes
* fix: chat message rendering, reactions on touch and grouped timestamps (#10428)
* Revert "fix: chat message rendering, reactions on touch and grouped timestamps" (#10441)
* feat(subspace): configurable subspace sidebar via Customize Sidebar (workspace#086) (#10444)
* chore(codegen): regenerate types with COMMUNITY and ACTIVITY sidebar widgets
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* feat(sidebar): per-level widget vocabularies, id-tiebreak first-state rule and positional reorder read-back
Adds the community and activity widgets to the CRD vocabulary with
per-level editor lists (subspaces drop Apply/Join, Contact Leads and
Guidelines; L2 also drops Create Subspace and Subspaces). The plan
resolver takes the level vocabulary, so out-of-level and unknown values
share one carry-back path. sortBySortOrder breaks ties by lowercase id
like the server, and selectFirstFlowState picks a flow's first state
over all states. The reorder mutation now reads back each state's
sidebar so the cache follows positional moves. i18n for the new labels,
Customize Sidebar copy and the replace-flow note in all six locales.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* feat(subspace): render the subspace sidebar from the first phase's widget list
The subspace page now resolves its sidebar from the first innovation-flow
phase (all phases, hidden ones included) with the level's vocabulary.
SubspaceSidebar renders the plan in order: consecutive quick actions
share one group, Collapse stays below the identity block, and the rail
shows only configured Add Post, quick-action icons and nested subspaces.
Every widget keeps its own gate: Invite needs the update right, Recent
Activity a signed-in viewer, Create Subspace the create right. Unconfigured
widgets fetch nothing (updates, nested subspaces, events, tags, search).
A configured search widget filters the active phase scoped to the
level-zero space, resets on phase change, and swaps the feed for results.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* feat(space-settings): Customize Sidebar for subspaces, per-phase save stops writing the sidebar
Settings > Layout of an L1/L2 gains a Customize Sidebar button that opens
the Layout dialog in a new sidebar-only mode, offering the level's
vocabulary and pre-filled from the persisted first phase (never the
column buffer). Saving writes only that phase's sidebar key, right away,
carrying out-of-level and unknown values back at their indices. The
per-phase Layout save now omits the sidebar key on subspaces, so a stale
list can never be written back after a positional reorder.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* feat(space): Community and Recent Activity sidebar widgets on top-level Spaces, replace-flow sidebar note
Top-level tabs can now configure the community and activity widgets:
CRD Community and Recent Activity buttons open the existing community
dialog and the Space activity dialog. Recent Activity renders, and its
dialog mounts, only for signed-in viewers; neither dialog mounts when
its widget is not configured. The subspace replace-flow confirmation
now notes that the template's sidebar replaces the current one.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* test(e2e): add subspace sidebar acceptance walks
Live-stack Playwright walks for configuring the subspace sidebar from the
first phase: the Customize Sidebar dialog, untouched-subspace defaults, and
member-facing widgets including search. They run only via pnpm test:e2e
(vitest excludes e2e/**); comments carry no spec references.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* test(e2e): make the stored-lists sidebar walk falsifiable
quality:client-web:qual-client-web-2 — the customized-L1 scenario now
requires the subspace page URL and asserts the rendered sidebar against
the resolved stored first list: every configured always-rendered widget
is visible, every unconfigured widget is absent, and present widgets
follow the stored order. It first checks the fixture can tell the first
list apart from phase 2's and the generic list. The API helper's
non-interactive login is now an assertion instead of a skip, with the
same default password as the browser login fixture, and the first-state
rank breaks sortOrder ties by id like the client and server do.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(innovation-flow): select every state setting on reorder so the cache keeps them
InnovationFlowStateSettings has no id, so Apollo replaces the cached settings
object with whatever the reorder mutation returned. Selecting only the sidebar
dropped visible, allowNewCallouts, descriptionDisplayMode and
showPublishDetails from every state after a reorder.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* refactor(space): share one sidebar search hook between the space tabs and subspaces
The subspace layout carried a line-for-line copy of the top-level tab page's
search wiring (debounce, tags query, term building, match count, clear).
Both now use useSidebarFlowStateSearch, parameterised by the root space id,
the searched flow state and the callouts set. The flow-state reset that
subspaces relied on lives in the hook; the top-level tab page is remounted
per tab, so it is unaffected.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* refactor(crd): drop the unused SubspaceSidebarWidgetId type
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* refactor(space-settings): one out-of-vocabulary path in the sidebar dialog, whole-subspace empty note
Both callers of PhaseLayoutDialog already resolve the stored sidebar against
the offered vocabulary and move every other value into sidebarUnknown, so the
dialog's own hidden-but-kept filter was a second, unreachable copy of that
mechanism. The title/helpText overrides were only ever passed by tests.
In sidebar-only mode the empty note now says the sidebar will be empty across
the whole subspace, in every phase, rather than for this tab.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(space-settings): Customize Sidebar never saves to a phase that is no longer first
The target phase was captured when the dialog opened, so a reorder, phase
delete or flow replacement that completed while it was open sent the list to
a stale phase. The connector now resolves the first phase from the latest
persisted states at save time, and the button is disabled while the Layout
tab's structural save, a phase delete or a flow replacement is in flight. It
stays enabled when the column buffer is merely dirty.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* test(e2e): locate the login password field by its label
The login form's password field is a native password input, which has no
textbox role, so the role-based locator never matched it. It is now found by
its label, anchored so the show/hide toggle is not matched too.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* test(e2e): one fixture helper for the subspace sidebar walks, credentials from env only
The three subspace sidebar walks each carried their own GraphQL and login
helpers, with different failure behaviour (assertion, thrown error) and three
different environment schemes for the same fixture. They now share
e2e/fixtures/subspaceSidebarFixture.ts: one environment scheme, documented in
its header, and one behaviour — a failed login or probe is an assertion
failure.
Every committed fallback password is gone. Credentials come only from
AUTH_TEST_HARNESS_PASSWORD; without it, or without the fixture or the member
persona a walk needs, the walk skips with the reason. The widgets walk signs
in through the shared login page instead of its own sign-in, which located the
password field by a textbox role it does not have.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* chore(innovation-flow): keep sidebar first in the reorder settings selection
The positional-reorder contract check reads the reorder document for a
settings selection that opens with sidebar.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(space-settings): a dragged phase column only lands on another column
Moving a column over a column that holds posts collided with a post card,
so the drop resolved to no column and the reorder was silently dropped.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* feat(subspace): an empty sidebar list hides the sidebar and widens the content
With no widgets configured the subspace page renders no sidebar column and
no collapse control, and the content takes the whole track. The mobile
drawer and its triggers remain only for admins, to reach the edit-flow
link.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* test(e2e): an empty subspace sidebar list renders no sidebar
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* feat(086-app-push-mvp): app-mode detection, native token binding, no webview collision (#10446)
* feat(086): app-mode detection, and no web push in app mode
Adds `src/core/appMode/isAppMode.ts` — the single owner of "are we inside the
Capacitor native shell?", read off the `window.Capacitor` global the shell
injects at document start into the remote origin. No Capacitor package is
pulled in, and the ambient `window.Capacitor` type has exactly one declaration
site (which is what makes a wrong access path a typecheck error). The predicate
is forgeable in a browser and documented as such: every effect reachable from
it is self-inflicted, and it is never an authorization or entry gate.
`usePushNotifications` gains one term — `!isAppMode()` — so in app mode the
page raises no browser subscription at all: no `pushManager.subscribe`, no
`subscribeToPushNotifications`, no service-worker notification, and the
provider's silent-refresh effect returns early. That is the second half of "no
webview collision": one server event cannot arrive twice.
The new test mounts the hook with `PushManager`, `serviceWorker` and
`Notification` all present, because asserting "a WebView has no Push API" would
be vacuous (sandbox web push is live). Measured: removing the term turns its two
app-mode cases red.
Refs workspace#086-app-push-mvp
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* feat(086): capture the native device token and bind it to the user
The bind half of the native push path. Three new modules plus one line of
wiring:
- `nativePushPlugin.ts` — a pure accessor for the plugin proxy the native shell
injects at `window.Capacitor.Plugins.PushNotifications`, guarded by the
bridge's own `isPluginAvailable`. Deliberately NOT `registerPlugin(...)`:
that function lives only in the `@capacitor/core` npm bundle, which this page
does not load (zero hits in either vendored native bridge), so written that
way it would return undefined on a real device too. `addListener` is typed
`unknown` because the injected implementation returns `{ remove }`
synchronously, not the promise the plugin's `.d.ts` declares — teardown goes
through `removeAllListeners`. No app-mode judgement and no log of its own.
- `devicePushTokenMutations.ts` — the two mutations as inline `gql` documents
shared by both consumers. Inline rather than generated hooks because
`pnpm codegen` fails on a clean develop over a pre-existing 027 rename
(BF-1, not fixed here); the file header records the deviation and its exit.
Both documents validate against the regenerated server SDL.
- `useNativePushRegistration.ts` — requests OS permission, and on
`receive === 'granted'` adds the listeners then calls `register()`. Note the
permission result is an OBJECT: the web-push leg next door compares a bare
string because `Notification.requestPermission()` resolves one, and copying
that shape here would make the comparison always true and `register()` never
run. On the `registration` event the token is persisted first, then bound.
One log owner for every failure arm — uncompiled plugin, registrationError,
rejected bind — because otherwise they are indistinguishable on the page.
No retry, no state, no exposed API.
`PushNotificationProviderInner` mounts the hook. That component mounts exactly
when the session becomes authenticated, so mounting IS the sign-in transition —
which is why there is no auth-flag effect anywhere in the slice.
Refs workspace#086-app-push-mvp
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* feat(086): unbind the device token on sign-out
The cross-user-leak control. An unbound device token keeps delivering the
signing-out user's notifications to a phone the next person signs in on, so
sign-out must unbind it.
One additional leg inside the existing `cleanupPushSubscription`, mirroring the
`PUSH_SUBSCRIPTION_ID_KEY` leg above it: read the token, send
`unregisterDevicePushToken`, swallow-and-continue, and clear the key in the
existing `finally`. The function already took its mutation fn as a parameter,
so this takes a second one and both call sites pass it — the parameter is
required on purpose: making it optional typechecks green while the unbind never
fires.
No `isAppMode()` check: in a browser the key is never written, so the leg is a
no-op by construction. The `removeItem` is unconditional for the same reason it
already is for the other two keys — a key kept after a failed unbind is a token
the page would keep re-sending for a user it can no longer prove.
The page already awaits cleanup before `window.location.replace` on both
outcome branches. The new test file asserts the whole shape (that directory held
only `LoginSuccessPage.test.ts`, so the gate's vitest leg over it passed
vacuously): the mutation carries the stored token, the redirect waits for it, the
key is cleared, a browser sends nothing, and a rejecting unbind still redirects.
Refs workspace#086-app-push-mvp
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* feat(086): keep the per-event push preference column reachable in the app
In app mode `pushAvailable` is false by construction (there is no browser
subscription), and `showPushColumn` was driven by it alone — so push would
arrive on the phone with the entire per-event push preference column hidden and
no way to see or change which events push.
`UserNotificationsTabView` gains one prop, `appMode`:
- `showPushColumn={pushAvailable || appMode}`, and
- `PushMasterBanner` renders `null` in app mode. That card is the *browser*
subscription switch; in the app the OS owns permission, so stating nothing is
correct where stating "push is unavailable" would be false. Surfacing the OS
permission state is deferred.
`CrdUserNotificationsTab` passes `appMode={isAppMode()}` and leaves the
`isPushAvailable` composition alone — that is the browser-push question and its
answer is correctly false in the app.
No new i18n key, no new component, no new copy. Three view cases cover app mode
on, and both pre-existing browser states unchanged, so the column rule cannot be
satisfied by making everything render.
Refs workspace#086-app-push-mvp
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* refactor(086): drop an unreachable window guard, a single-use type alias, and a restated rationale
Simplification pass over the 086 client-web diff, behaviour unchanged:
- isAppMode(): drop the `typeof window !== 'undefined'` term. client-web is a
client-rendered SPA with no SSR and vitest runs jsdom globally, so the
guarded state is unreachable on every real call path; the sibling
`'PushManager' in window` expression already assumes `window`.
- isAppMode(): the "no registerPlugin" rationale was stated here AND in
nativePushPlugin.ts, which owns it. Point at the owner instead of restating.
- nativePushPlugin.ts: inline the exported `NativePushPermissionStatus` alias
into the one signature that referenced it (no external consumer).
Gates before and after: pnpm install, pnpm run typecheck:native, and the scoped
vitest run (28 files / 285 passed) — identical. The repos.yaml contract greps
(`getPlatform()`, the two `(registrationData:` legs, `isAppMode`) still pass and
biome ci is rc 0 with the same 3 pre-existing warnings.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(086): stop native push registration from acting after effect cleanup
CodeRabbit review comment 4315389402 on client-web#10446. Verified against the
real code and measured: `requestPermissions()` resolves on the OS permission
dialog, which can outlive the effect — `PushNotificationProviderInner` unmounts
the instant `isAuthenticated` turns false, and on iOS that dialog can sit open
indefinitely. The `.then` callback then adds both listeners and calls
`register()` AFTER the cleanup has already run `removeAllListeners()`, so those
listeners are ones the cleanup can never remove: they survive to persist a token
and bind it outside the session that asked for the permission. The new test
case, run against the unfixed hook, records the ordering verbatim —
`removeAllListeners` first, then `addListener:registrationError`,
`addListener:registration`, `register`.
One local `let active = true`, flipped in cleanup. Checked at the `.then` entry
(which covers both listeners and `register()`), and again inside the
`registration` listener because `removeAllListeners()` is itself async, so an
event can land while removal is in flight. No new state, no new module, no
dependency. The larger coordination CodeRabbit asks for elsewhere is not added
here: this is the one window a local flag actually closes.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* feat(087): Download PDF button on memos (#10445)
* refactor(memo): generalise the signed-copy download into downloadPdf
Rename downloadMemoSignaturePdf to downloadPdf. The helper now throws a
typed PdfDownloadError carrying the HTTP status, and when no explicit
filename is passed it saves under the response's Content-Disposition
filename, falling back to memo.pdf. The signed-copy caller passes its
display name explicitly, so its saved name is unchanged.
workspace#087-memo-pdf-download
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* feat(memo): add Download PDF strings in all six locales
Button label plus three toasts (busy, too large / not exportable,
generic failure) under memo.downloadPdf in the crd-space namespace.
workspace#087-memo-pdf-download
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* feat(memo): add useMemoPdfDownload hook
Downloads the memo PDF from the fixed private REST path, leaving the
filename to the server's Content-Disposition, tracks the in-flight
state, and maps failures to three toasts: busy (429/503, warning), too
large / not exportable (422, error) and generic failure (anything else).
workspace#087-memo-pdf-download
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* feat(memo): Download PDF button in the memo dialog header
Every authenticated viewer with the memo loaded gets a Download PDF
action immediately before Share, on every screen size and independent
of the signing gating. While a download is in flight the button is
disabled, marked aria-busy and shows a spinner instead of its icon.
workspace#087-memo-pdf-download
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(memo): abort PDF download on dialog close and flush edits first
Abort on close (spec US3-AS6):
RCA: downloadPdf fetched with no AbortSignal and useMemoPdfDownload had
no unmount cleanup. Four of the five dialog call sites unmount the
dialog on close, so the orphaned request kept holding the server's
per-actor render slot (an immediate retry got 429) and still saved a
file after the user had dismissed the dialog.
Fix: downloadPdf takes an optional signal passed to fetch; the hook
creates an AbortController per download(), keeps it in a ref and aborts
it on unmount. An aborted download returns silently: no toast and no
setState after unmount.
Rejected: a server-side slot timeout alone (retry still blocked until it
expires, file still saved).
Flush unsynced edits first (FR-004 / US1-AS4):
RCA: the Download PDF button fetched immediately, while signing awaits
provider.requestDurability() first. Edits not yet acked by the server
were missing from a PDF that promises the content as of the request.
Fix: the hook takes an optional requestDurability and awaits it inside
the busy state before fetching; CrdMemoDialog passes the memo
provider's (a no-op when nothing is pending, so readers are unaffected).
A durability rejection shows the existing 'failed' toast and skips the
fetch.
Tests (all RED without the production change): unmount aborts the
in-flight download with no toast; durability awaited before fetch;
durability rejection -> failed toast, no fetch; signal reaches fetch;
the dialog wires the provider's requestDurability.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(memo): collapse Download PDF and Sign labels to icon-only below sm
The memo header action row is non-wrapping and shrink-0; with signed
copies + Sign + Download PDF (NL "PDF downloaden") + Share + Close it
overflows ~360px phones and pushes Share/Close off-screen. Wrap the
visible labels in `sr-only sm:not-sr-only` so they render icon-only on
phones while keeping the accessible name. Busy spinner unchanged.
Refs workspace#087-memo-pdf-download (FR-019 amendment pending).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Jeroen's feedback (#10451)
* Merge pull request #10456 from alkem-io/feat/088-contributor-picker-crd-parity
feat(088): toggle rows, checkboxes and stacked type labels in the Contributors picker
* fix: silence matrix-js-sdk debug logging (#10459)
* chore: stop tracking husky's generated pre-push hook (#10461)
* feat(088): open the notification's target on tap (#10454)
* feat(088): pure push target-URL normaliser (workspace#088-app-push-tap-routing)
The push payload carries its target at `notification.data.url` (spec 086
FR-007) either as an absolute URL rooted at the serving origin or as the bare
`'/'` home link. react-router's `navigate()` resolves an absolute `https://…`
string as a relative path, so the raw value is never router-usable.
`normalizePushTargetUrl(raw, origin)` reduces an origin-rooted absolute URL to
`pathname + search + hash`, passes a same-origin-relative path (and `'/'`)
through, and returns `undefined` — "stay on the start route" — for a different
origin, a protocol-relative `//host/path`, a non-http(s) scheme, an unparseable
URL, an empty string, or a non-string. `origin` is injected rather than read
off `window.location` so the function is pure.
Not `core/utils/links.ts`'s `resolveInternalReturnPath`: that helper also
admits the configured apex beside the current origin, which an auth-flow
`return_to` needs and a push target must not have.
* feat(088): route a tapped native push to its target (workspace#088-app-push-tap-routing)
The tap listener could not go in `useNativePushRegistration` as issue #2193
assumed: that hook mounts only while `isAuthenticated`, so it is absent exactly
when a cold-start or signed-out tap needs handling. New always-mounted consumer
`useNativePushTapRouting`, mounted from `root.tsx` inside `BrowserRouter` and
above `PushNotificationProvider`'s auth gate. It reads `notification.data.url`
by name, normalises it against the serving origin via `normalizePushTargetUrl`
(C1), and navigates on a defined result — no stash, no persisted shape, no
client-side identity check.
Resolves the `removeAllListeners()` collision by **narrowing the registration
hook's cleanup** (option b): the plugin-global call is deleted, and
`removeAllListeners` is removed from the hand-written `NativePushPlugin` type so
it cannot be called by accident. It was never a correct per-consumer teardown —
it removes every consumer's listener — and the `active` flag already provided
every guarantee it did, strictly better, since the global call is async and an
event could land while removal was in flight. Accepted consequence: the
registration hook's two listeners outlive their session, inert.
The `pushNotificationActionPerformed` overload carries no `definitions.d.ts`
line citation: that file is not present in this workspace, so the shape is
transcribed from the plugin's documented `ActionPerformed`/`PushNotificationSchema`
contract and the docblock says so rather than inventing line numbers.
Also records the `isAppMode` third-consumer ruling in its docblock: admitted,
and not on an authorization path.
Gate: `pnpm typecheck:native` + `pnpm vitest run src/main/pushNotifications
src/core/appMode` (32 tests). Both new cases verified red under mutation — a
`[navigate]` dependency, and the global `removeAllListeners()` restored.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* refactor(088): drop the navigate ref and compress restating docblocks (workspace#088-app-push-tap-routing)
- useNativePushTapRouting: remove the navigateRef + its sync effect. The
listener's empty dependency array is what prevents re-registration; the ref
only guarded a stale `navigate`, which cannot matter because
normalizePushTargetUrl always returns an absolute path and react-router's
resolveTo ignores the captured location for those (react-router 7.9.3,
chunk-NISHYRIK.mjs:840-888, 5039-5068).
- Compress the three docblock additions that restated the same two facts
(no per-consumer teardown; the tap listener must outlive sign-out).
Gates green before and after: pnpm typecheck:native, pnpm vitest run
src/main/pushNotifications src/core/appMode (32 tests).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(088): reject an origin-rooted push target whose path is protocol-relative
`new URL('<origin>//evil.example/x', origin)` is same-origin with pathname
`'//evil.example/x'`, so the origin check passed and the normaliser returned a
protocol-relative string. react-router keeps a leading-`/` target verbatim, and
`history.pushState` to a cross-origin-resolving URL throws SecurityError, whose
catch falls through to `window.location.assign()` — an off-origin navigation out
of the authenticated app with no interaction (SEC-088-01).
Guard the single declared boundary; the added case is RED without it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* docs(088): correct the isAppMode ruling on who can forge a tap event
The third-consumer ruling only considered browser forgery. On Android the tap
event itself is forgeable by any installed app: the feature's working path is a
relaunch of the exported MainActivity with the payload as intent extras, which
Capacitor fans out to every plugin, and an exported activity cannot distinguish
that from a local startActivity (SEC-088-02).
Records the real producer and the confinement (same-origin only, via
normalizePushTargetUrl), and names client-appstore as the owner of the residual.
No behaviour change — no provenance machinery is added to a signal this file
already declares non-authoritative.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* feat(089): Other Resources tab for visitors + guest sign-up notice on organisation profiles (#10458)
* feat(profile): query documents for the viewer-scoped hostedResources field
Adds UserHostedResources / OrganizationHostedResources with a shared
AccountHostedResourcesFields fragment selecting exactly what the profile
mapper reads, and regenerates the Apollo types and hooks.
Refs: workspace#089-profile-other-resources
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* feat(profile): read Other Resources from viewer-scoped hostedResources
The user and organization profile hooks replace the two-hop
account.id -> AccountResourcesInfo chain (null for every non-owner) with
one standalone hostedResources query, skipped for guests. The server
applies visibility; the client renders what it gets. The now unused
useAccountResources hook is deleted; its query document stays for the
settings Account tabs and the hub resource dialog.
Refs: workspace#089-profile-other-resources
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* feat(profile): sign-up notice for guests on the organization profile
OrganizationPublicProfileView gains an optional guestNotice slot rendered
above the resource tab strip, outside the loading branch. The page fills
it only once auth state is known and the viewer is signed out, never from
the resource data, with a sign-up link carrying the return URL. New key
orgProfile.guestResourcesNotice in all six languages.
Refs: workspace#089-profile-other-resources
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(profile): hosted-resources read hides the tab silently and waits for auth
F1: both hosted-resources queries opt out of the global error link, so a
failed read only hides the Other Resources tab instead of raising a toast.
F2: the auth-loading state is folded into the hosted-resources loading
flag, so the tab strip no longer settles without Other Resources while the
query is still skipped pending auth. Guests still resolve to not-loading.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(profile): render the organization name in the guest notice as text
F3: interpolation escaping is off globally, so markup in an
admin-controlled organization display name was parsed as Trans component
tags in the guest sign-up notice. The notice now escapes the value per call
and unescapes the resulting text nodes, so the name renders literally
(including "&") while the Sign up link keeps working.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* chore(codegen): regenerate types for the closed hosted Innovation Hub card
The server now lists hosted Innovation Hubs as AccountHostedInnovationHub
{id, nameID, profile}; the profile-page selection is unchanged.
Refs: workspace#089-profile-other-resources
* fix(profile): show the guest notice only once the organization is known
While the organization was still loading, a guest briefly saw the notice
with an empty name and a sign-up link without a return URL. The notice is
now withheld until the organization resolves; it stays driven by auth state
and the organization's identity only, never by its resource data. (C1)
Refs: workspace#089-profile-other-resources
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(profile): refetch hosted resources after a membership change
Joining the organization or accepting its invitation refetched only the
organization info, so the cached viewer-scoped hosted resources kept the
pre-membership list and newly visible resources stayed missing. Both refresh
paths now refetch the hosted resources too. The page's refresh moves into its
container hook, beside the query it refreshes, so it is unit-testable. (C2)
Refs: workspace#089-profile-other-resources
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix: do not start the Matrix session on the identity origin (#10462)
* fix: do not start the Matrix session on the identity origin
Chat has been entirely non-functional in the wrapped native app since
release 78 (#10365) — no reads, no sends — because the browser never
holds a Matrix session.
`MatrixSession` is mounted in `root.tsx`, above every `NonIdentity`
wrapper (those are per-route). So when the app signs in through the
identity origin, the provider fires while `window.location.origin` is
still `https://identity.<domain>`, before `NonIdentity` redirects to the
apex. `buildSsoUrl` then adds a `parent` parameter — it compares the
current origin against the platform origin — and `isAcceptedParent`
accepts it, since `identity.<domain>` ends with `.<domain>`. The callback
therefore takes the handoff branch and posts the token back to the
identity origin instead of exchanging it, so the credentials land in the
identity origin's IndexedDB. The page then redirects to the apex, where
that storage is empty. (Measured on a device: `indexedDB.databases()` is
`[]` on the apex while an `indexedDB.open` probe writes fine — the wrong
origin, not blocked storage.)
Fix: skip establishment while the page is on the identity origin. It is a
transient staging origin — `NonIdentity` always replaces the location
with the platform origin once authenticated, and that full reload mounts
the provider again where it can succeed.
Deliberately gated on the identity origin, not on
`window.location.origin !== platformOrigin`: an innovation hub
legitimately serves the app from a third origin and the callback's
`parent` handoff exists for exactly that case, so the broader gate would
leave hubs with no Matrix session at all. A comment and a test case guard
against that "simplification".
The identity-origin derivation moves out of `NonIdentity` into a
`useIdentityOrigin` hook beside `usePlatformOrigin`, so the two call
sites of this auth-adjacent invariant cannot drift.
`buildSsoUrl`, `isAcceptedParent` and the callback's handoff logic are
untouched — they are correct for the hub case.
Refs alkem-io/server#6706
* fix: guard the empty-provider case, and pin the gate to the CONFIGURED origin
Two findings from the review of df599e84b, both confirmed by mutation.
useIdentityOrigin read `authentication?.providers[0].config.issuer`. The
optional chain guarded only the missing `authentication` block, not a present
but empty `providers` array — `providers[0]` is then undefined and `.config`
throws. Faithfully moved from the inline expression it replaced, but the move
widened the blast radius: the hook is called from NonIdentity, which renders on
every route, so the symptom is a white screen rather than a missing Matrix
session. Now `providers[0]?.config.issuer`, with a case that fails (1 failed /
2 passed) when the chain is removed.
The provider suite did not pin that the guard uses the configured identity
origin. Replacing `window.location.origin === identityOrigin` with a hardcoded
`origin.startsWith('https://identity.')` left all nine cases green, because
every fixture happened to sit outside that prefix — so a deployment whose
identity origin does not follow the subdomain convention would break in exactly
the way this fix repairs, with nothing to catch it. A case with
`https://auth.partner.example` as both the configured origin and the serving
origin now fails under that mutation (1 failed / 9 passed).
Gates: pnpm lint exit 0; scoped 70 passed; full suite 5185 passed / 2 skipped.
Refs alkem-io/server#6706
* fix: normalise the identity issuer to an origin before comparing
CodeRabbit, Major, on PR #10462. Valid: `useIdentityOrigin` returned the raw
issuer, and both call sites compare it against `window.location.origin`, which
is always scheme+host+port with no path and no trailing slash. A raw comparison
against a path- or slash-carrying issuer never matches, which would leave the
Matrix guard inert AND `NonIdentity`'s redirect dead — the same defect this PR
exists to fix, reintroduced one layer up.
Reachable without misconfiguration: alkemio.yml's own default issuer is
`http://alkemio-server-dev/`, whose trailing slash alone defeats the compare.
Every deployed tier does configure a bare origin today (sandbox
`https://identity.sandbox-alkem.io`, production `https://identity.alkem.io`,
checked in dev-orchestration and infrastructure-operations), so the guard was
already working where it matters; this hardens the local default and any future
issuer that gains a path.
The suggested diff was NOT applied verbatim: it drops the `providers[0]?`
optional chain added in 5a5ae5a6c and lets `new URL` throw on a malformed
issuer — two crashes in a hook `NonIdentity` calls on every route, where the
symptom is a white screen. Measured: that diff fails 2 of the 7 cases. The
reported bug itself fails 4 of 7.
Four cases added: trailing slash, path, path+query, malformed.
Gates: pnpm lint exit 0; full suite 5189 passed / 2 skipped.
Refs alkem-io/server#6706
* feat: paste and add an image in comments and replies (#10464)
* fix: keep the delete-account entry point in every Security tab state (#10469)
The Security tab withheld the Delete-account card in its lapsed
identity-provider-session state. In the mobile app shell that state is the
only one the tab ever reaches — the WebView holds no identity-provider
session, so the Kratos settings flow 401s on every mount — which left no
in-app way to delete an account and made the app store's in-app account
deletion requirement unsatisfiable.
The stated reason for withholding it does not hold: the server gates
deletion on the freshness of the platform session, not on the identity
provider's, so a deletion started from the lapsed state succeeds with no
re-auth round trip. The one state that hid the entry point is a state in
which deletion would have worked.
Rather than add a fifth copy of the card to the fifth place, hoist the two
state-independent cards — Connected Accounts and Delete-account — out of
the per-state chain so each has exactly one render site and the chain only
decides the middle of the tab. Four copies is how the entry point was lost:
the lapsed-session state was added on a branch that never saw the fix which
had put the card in the other three.
The test asserting the card is withheld is deleted — it encoded the refuted
rationale — and replaced with a positive assertion over a fixture keyed on
the state union itself, so a sixth state cannot be added without extending
it and failing typecheck.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* feat(org-settings): Platform Support manages an organization's people on the Community tab (#10468)
* feat(org-settings): derive a community-only settings access mode for Platform Support
Select the organization role set's own privileges in the OrganizationInfo
fragment and regenerate codegen for the new
AuthorizationPrivilege.PlatformSupportOrgMembership value. useOrganization
exposes canManageCommunityAsSupport, and useCanEditOrganizationSettings
derives accessMode full | community-only | none, admitting a Support-only
viewer to the settings shell without Update.
Refs workspace#090-support-org-community-admin
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* feat(org-settings): confine a community-only viewer to the Community tab
In community-only mode the settings index, the catch-all and every tab
other than Community redirect to Community, and the tab strip shows the
Community tab alone; full access routes exactly as before. The profile
page offers the settings link to Platform Support too.
The catch-all now climbs back to the index instead of navigating to a
relative "profile": a splat route resolves relative targets against the
whole URL, so the old redirect appended to the unknown segment and
matched itself again without end.
Refs workspace#090-support-org-community-admin
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* feat(org-settings): enable the Community tab's management controls for Platform Support
canManage is GRANT or Platform Support's org-membership privilege,
evaluated as two separate permissions and ORed because useActionPermission
requires every listed privilege. A managing viewer also selects the
pending applications. Inviting keeps its own gate; no direct-add control
is introduced.
Refs workspace#090-support-org-community-admin
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* test(e2e): add platform-support community acceptance specs
Persist the acceptance walks for the three platform-support organisation
community user stories as live-stack Playwright specs under e2e/specs,
named *.e2e.spec.ts so only `pnpm test:e2e` picks them up; vitest and
the default lint gates exclude e2e/**.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* test(e2e): persist the platform-support community acceptance walks
Commit the Playwright specs emitted by the live acceptance walk for the
three platform-support organisation community stories, beside the
broader self-seeding specs. They drive the quickstart fixture
organisations (orphan, owners, managed) and optionally write evidence
screenshots. Named *.walk.e2e.spec.ts so only `pnpm test:e2e` picks
them up; vitest, biome and eslint exclude e2e/**, and they are covered
by `pnpm test:e2e:typecheck`.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* feat(org-settings): show one specific message when Platform Support targets its own account
A self-targeted Platform Support action now returns PLATFORM_SUPPORT_SELF_TARGET.
The invite dialog showed three toasts for it: the global link's translation,
its raw server detail, and the dialog's generic "Couldn't send the invitations".
The invite mutation now handles its own errors like the tab's other mutations,
and every Community-tab action shows the translated self-target message
instead of a generic failure. Adds the message in all six languages.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(org-settings): report the server's reason for a failed associate invite
The invite dialog skips the global error handler so a Platform Support
self-target refusal shows one specific message. Every other GraphQL
refusal was then reported as a network failure. It now shows the server's
translated reason (the same text the global handler shows), and the
connectivity message only when no server reply came back.
The e2e walks restore PLATFORM_SUPPORT in a finally block, fail when the
owners query errors, and match static copy exactly.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix: show new callout comments when reopening a callout (#10466)
* 0.170.0
* chore(deps): consume published Excalidraw-Yjs v0.5.5 (#10472)
* 0.170.1
* Settings: align Space, User, Organization and VC settings with the prototype (#10437)
* docs(045,097,098,100): space settings prototype parity — About, Layout, Community
Amend the owning specs (2026-10-16) for the space settings redesign that
aligns CRD with the client-web-prototype:
- 045: header identity tile; About as collapsible SettingsSection cards
with boxed inputs, Location before Context, Visuals in one row of true
shapes, all-values classification chips; per-section Save replaces the
never-shipped 2-second autosave; Layout restyle (count badge, Edit
Details pencils, soft active phase), loading skeleton, buffer lock while
saving; Community as section cards with pending-table avatars/colours,
Application Form + Community Guidelines previewed inline and edited in
a dialog (tasks T023a, T032a–e).
- 100: scoped exception (Hard rule 1a) — the L0 cardBanner is the
identity image in navigation chrome (breadcrumbs, settings header).
- 097: Profile tabs follow the About section-card redesign (T099a,
pending); Associates pending table shows avatars.
- 098: FR-038 guidelines editor now opens in a dialog on Community.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* feat(settings): align space settings About, Layout and Community with the prototype
Header
- SpaceSettingsHeader shows a 48px rounded-square identity tile: the L1/L2
avatar, the L0 cardBanner (spec 100 rule 1a), initials otherwise.
About
- New reusable CRD SettingsSection (coloured icon tile, title, optional
badge, chevron; Radix Collapsible; keeps the body mounted by default).
- About body becomes Identity · Visuals · Location · Context · Tags ·
Classifications · References & Links section cards; Name, Tagline and
City are boxed inputs; Visuals sit in one row of true shapes; the page
card and in-card heading are gone; content starts where the prototype's
does on every settings tab.
- ClassificationEntryCard shows every value as a chip (dashed "+" to
select, filled "×" to deselect; single-select replaces); the kebab
selector stays as an alternative.
- ReferencesEditor gains hideLabel.
Layout
- Prototype styling (style only): no page card or grid frame, compact
column header with post-count badge and Edit Details pencils, compact
rows with hover/focus-revealed menu, soft active-phase border.
- LayoutColumnsSkeleton while the innovation flow loads (Add disabled,
Save bar hidden, height reserved so the footer does not jump).
- Lock drag-and-drop and "Move to" while the layout is being written:
Save computes its mutations from the buffer as it was when clicked and
then marks that buffer clean, so a move landing mid-save was silently
treated as saved.
Community
- Section cards (Pending, Members, Application Form, Guidelines,
Organisations, VCs) with count badges; page heading and section
descriptions removed (keys deleted in all six locales); full-width
member search.
- PendingMembershipsTable: avatars (AdminCommunityCandidateMember now
selects profile.avatar; also shown on the org Associates tab) and an
amber/green/red/grey status palette; hideHeading for hosted use.
- Application Form and Community Guidelines are previewed inline and
edited in a shared SettingsEditorDialog (Cancel/Save, discard guard,
closes on successful save; guideline templates in the dialog header).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* feat(settings): align space settings Updates and Subspaces with the prototype
- SettingsSection gains `collapsible={false}` for fixed action sections
- Updates: fixed "Post an Update" section + collapsible "Published Updates"
- Subspaces: fixed "Default Subspace Template" (L0) + collapsible "Subspaces"
section holding the existing title, count and toolbar
- Drop the page card for both tabs; remove page headings, the composer title
and the default-template description (keys removed in all six languages)
- Spec 045: clarification + T032f
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* feat(settings): align space settings Templates, Storage, Settings and Account with the prototype
- Drop the shared page card for every settings tab; each tab lays out its own
full-width SettingsSection cards
- Storage: collapsible "Documents" section around the table / empty state
- Settings: Visibility / Membership / Allowed Actions as SettingsSection cards
(no centred column, page heading or section descriptions); subspace Save as
Template and Danger Zone restyled
- Account: Space URL, License, Visibility Status, Host Information, Danger Zone
(starts collapsed) and the support box; page heading and descriptions removed
- i18n: unused heading/description keys removed x6, storage.sections.documents
added, account.url.title -> "Space URL"
- Spec 045: clarification + T032g
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* spec(045): replace inline settings saves with a tab-wide unsaved-changes bar
Session 2026-10-22 (product owner): About, Layout and Settings save every
field through one bar fixed to the bottom of the viewport, with a count of
changed fields, a confirmed Discard that resets in place, and a Save that
saves everything on the tab. Classifications are staged too; Visuals still
commit on file pick. Adds FR-025a-d and FR-026a-c, dated amendments to
FR-005a/FR-008/FR-023/FR-025/FR-026, contracts, plan, data model and tasks
T032h-T032o.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* feat(045): replace inline settings saves with a tab-wide unsaved-changes bar
About, Layout and Settings now buffer every edit and show one bottom bar per
tab with a change counter, Discard (confirmed, in-place reset) and Save
changes (saves the whole tab, toasts on success). Switching tabs or View Post
with unsaved changes asks to save/discard; reload/close uses the native
browser confirm. Classifications are staged (add/remove/select/display) and
saved with the tab; the removal confirm is dropped. Layout structural actions
are disabled while dirty. Removes SpaceSettingsSaveBar, ClassificationRemoveConfirm
and the per-section inline saves, with their i18n keys in all 6 languages.
Spec: specs/045-crd-space-settings FR-025–FR-026c (amended 2026-10-22).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* spec(045,098): amend settings parity — Identity-only About, host type case, Updates buttons, Visuals tiles, empty-section library link
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* feat(045,098): settings prototype parity — collapsed About sections, sentence-case host type, right-aligned Updates actions, sized Visuals tiles, Browse General Library link
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* spec(097,045): Profile tabs save through the tab-wide unsaved-changes bar
User / Org / VC Profile tabs adopt the prototype's save bar and section layout
(only Identity open, References in its own section); the bar moves to a shared
CRD component.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* feat(097): Profile tabs save through the tab-wide unsaved-changes bar
User, Organization and VC Profile tabs buffer every edit and save through one
bottom bar (count, confirmed Discard, Save changes) shared with Space Settings;
tab switches with unsaved changes ask to save/discard and reload/close uses the
browser confirm. Sections follow the prototype: collapsible cards with boxed
inputs, only Identity open, References in its own section. Moves the bar to
@/crd/components/common/UnsavedChangesBar (strings to crd-common
unsavedChanges.* in all 6 languages), adds shared ProfileFormFields and
profileChanges save helpers, and removes the now-unused InlineEditText.
Spec: specs/097-crd-user-settings Session 2026-10-22.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* spec(097): Account, Membership, Organizations, Notifications, Settings and Security tabs follow the prototype
Session 2026-10-23 rulings: Account groups become collapsible sections with
the capacity badge, the dashed tile as the only create affordance and an
owner-only Danger Zone holding Delete Account (moved from Security, spec 054);
Membership and Organizations become row lists under a full-width search;
Notifications, Settings and Security are restyled into SettingsSections
with no behaviour change. Tasks T099c–T099h.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* feat(097,054): user settings tabs follow the prototype — Account sections + Danger Zone, Membership/Organizations row lists, restyled Notifications, Settings and Security
- Account (User + Org): groups are collapsible SettingsSections (Hosted
Spaces open) with the capacity badge beside the title; header Create
buttons and the help banner removed; an empty, non-creatable group shows
one muted line.
- Delete Account moves from Security into an owner-only, collapsed Danger
Zone at the end of User Account; useDeleteAccount moves to
settings/account and re-auth returns to the Account tab (spec 054).
- Membership (User + Org): row list with the space avatar (id-coloured
fallback), linked name, "Led by" avatars, role badge and a Leave button,
under a full-width search + filter/summary row; Home Space and Pending
Applications become sections.
- Organizations: "My Organisations" row list (avatar, verified, associates
· location, role, Disassociate) and a collapsed Create Organisation
section.
- Notifications, Settings and Security: same controls, restyled into
SettingsSections with coloured icon tiles.
- i18n parity across en/nl/es/bg/de/fr; unused keys removed.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(settings): leave-dialog Save honours validation; stale classification removals clear; failed Save offers a refresh
- The tab-switch dialog's Save (shared User/Org/VC hook and the Space page)
called saveAll() without checking canSave, so it could send edits the bar's
own Save refused. It is now disabled for invalid edits, and the handlers
refuse them too. On the Space page this also covers an invalid Application
Form.
- A staged classification removal of an entry that someone else had already
deleted failed, stayed counted after the refetch, and could only be
discarded. Only removals of entries the server still holds are counted or
sent now.
- Any failed Save, from the bar or the leave dialog, opens a dialog that shows
the error and offers Refresh page. Refresh discards the buffer first, so the
browser's own leave-site prompt does not ask a second time.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(settings): freeze forms and tab switches while a save runs; no retry of invalid edits
Addresses the Copilot review on #10437.
- Profile tabs (User, Organization, VC): the form is inert while the bar's
Save runs. saveAll() rebases the whole buffer on the refetched values, so
anything typed mid-save was silently lost. Same pattern as the About and
Settings tabs.
- SettingsEditorDialog (Community Guidelines, Application Form): the body is
inert while saving. Both hooks clear their buffer once the write resolves.
- Tab switches are ignored while the bar is saving: the shared User/Org/VC
hook, the Space settings guard (new setSaving) and Layout's View Post.
Before, the leave dialog's Save could start a second concurrent saveAll()
(duplicate reference creates) and its Discard could drop a buffer still
being written.
- The bar's error state carries canSave, so a retry after a failed Save is
disabled while an edit is invalid (it used to look enabled and do nothing).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(settings): deep links open their section; untitled references and failed refreshes no longer lose edits
- SettingsSection opens itself and scrolls to the target when the URL hash
names it or an anchor inside it (#description, #why, #who, #references,
#classifications, #guidelines). Replaces the Community-only hashIs and the
page-level scroll effect.
- About: a new reference with a URL or description but no title is flagged,
counted, and blocks Save instead of being skipped and then dropped.
- About and the User/Org/VC profile tabs: a failed refresh after the writes
fails the Save (so the save-failed dialog offers a reload) instead of
leaving created references staged to be created again on retry, or an
added classification hidden.
- Classification card: the "no values selected" hint sits below the chips so
they don't move on the first pick; drop the unused selectionPending prop.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* feat(091): acquire the Matrix session from the platform in the native app (#10475)
* feat(matrix): acquire a Matrix session in the native app shell
The app WebView holds no identity-origin cookie, so the silent-SSO
round-trip can never complete there — and on Android the attempt is
actively harmful: the shell intercepts the iframe's hop through
identity.<host>/login and navigates the main WebView to a "Sign in to
continue" landing.
Adds `mintAndStore`, which posts the argument-less `mintMatrixSession`
mutation to the platform's private GraphQL endpoint with `fetch` (no
Apollo: no React context here, the minted token must never reach the
normalized cache, and a failed acquisition must stay silent) and
persists the result through the existing credential store. It fails
closed on every path, including a `storeCredentials` write that
returns false — reporting an unstored record as persisted would make
the app mint a fresh Synapse device and audit row on every launch.
`loadOrAcquireRecord` gains the one branch that selects it, so the
48h renewal through `recover()` works without a second acquisition
path. `isAppMode`'s docblock records the fourth consumer's ruling and
corrects its census.
Refs workspace#091-app-chat-matrix-session
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* refactor(matrix): collapse the two mint call sites into one
`loadOrAcquireRecord` called `mintAndStore(abort.signal)` twice — once in
the app-mode branch and once as the browser's SSO-timeout fallback.
Inverting the predicate to guard only the SSO attempt leaves one call
site; the app still never attempts the harmful iframe hop, and the
browser still only mints after SSO fails.
`mintAndStore` also built the `CredentialRecord` and then re-validated
its own three fields through the object it had just built. It now
validates the response fields and builds the record once.
No behaviour change: all four client-web gates for this feature pass
identically before and after, 206/206 tests, none modified.
Refs workspace#091-app-chat-matrix-session
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(matrix): make the namespace listing work without indexedDB.databases()
SEC-091-CW-02. `listStoredUserIds` was the single owner of "find my
records" and depended entirely on `indexedDB.databases()`, which Firefox
has never implemented. There the listing returned [] forever, so a stored
credential could never be resumed -- a fresh Synapse device and a fresh 48h
token were issued on every page load -- and sign-out could never find one to
delete. storage.ts documented this in a comment rather than fixing it.
Both issuers hit it, because both write through this module: the shipped SSO
path via exchangeAndStore, and now the mint. So the remedy is one owner, not
a guard on the new caller: a guard would have disabled the only path that
makes chat work on that browser family while the baseline kept leaking at the
same rate.
The namespaces are now mirrored into localStorage and the listing is the union
of the two. The mirror is a second record of the same fact, not a second owner:
databases remain the only store, and a browser with `databases()` keeps
working from it alone when the mirror is unavailable or has not backfilled.
Index writes land only after the IndexedDB write commits, and are dropped
first and unconditionally on clear. loadActorCredentials now tries every
matching namespace, so a mirror entry that outlived its database cannot mask a
live record for the same actor.
Mutation-checked: removing the mirror from the listing fails exactly the three
new cases that bind it (listing, resume, stale-entry skip) and nothing else.
Refs alkem-io/server#6706
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(091): bound the mint request, and settle a late SSO exchange
CodeRabbit on client-web#10475, both findings confirmed against the code.
1. `mintAndStore` passed only the caller's signal to `fetch`. That signal
aborts on sign-out, which is not a time limit -- so a platform that accepts
the POST and never answers left `establishSession` pending for good:
`start()` never ran and the establishing flag never cleared. In app mode
this is the ONLY acquisition path, so chat would sit silently dead until
the page reloaded. A hung response is ordinary on a mobile network, and the
`catch` already claimed to fail "exactly like a timed-out SSO" while having
no deadline at all. Now 20s, matching `attemptSilentSso`'s own.
Composed by hand rather than with AbortSignal.any(): Safari shipped that in
17.4, under this repo's >90% support bar for a path every launch takes.
2. `attemptSilentSso` fired `exchangeAndStore` without awaiting it, so the
deadline path could return `timeout` with the exchange still in flight.
The caller then minted and started a client on the MINTED credential, and
the exchange's own `storeCredentials` overwrote the record that client was
built from -- live session and stored credential on two different Synapse
devices, and an orphaned device.
The exchange now gets a signal this function owns. The deadline path reads
storage once more (an exchange that finished after the final poll has
already persisted, and calling that a timeout would mint for nothing), then
cancels it. Every interleaving now leaves the stored record agreeing with
the running client: if the cancel lands after the write, `exchangeAndStore`
sees it and clears what it stored. The cost is an orphaned device inside
that narrow window -- the known device-churn residual, and the lesser harm.
`matrixCallback.ts` is untouched; FR-020's freeze holds.
Both assertions mutation-tested: reverting (1) hangs its test until the 15s
timeout, reverting (2) fails outright. One pre-existing assertion updated --
the exchange's third argument is no longer the caller's `undefined` signal.
86 matrix tests pass, `pnpm lint` exits 0.
Refs alkem-io/server#6706
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* 0.170.2
* fix(settings): accepted-invitation delete, Trusted applicants for org hosts, VC avatars, org Associates URL (#10476)
* fix(space-settings): hide delete for accepted invitations in Community pending memberships
Deleting an accepted invitation throws a server error. Hide the bin icon
for accepted invitations, matching approved applications (already hidden).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(space-settings): show Trusted applicants only when the space host is an organization
Trusting the host only applies to organizations; a user-hosted space no
longer shows the toggle. Selects provider.type in SpaceSettings and gates
the switch on ActorType.Organization.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(invite-vc): show VC avatars in the Invite Virtual Contributor dialog
The connector mapped account/library VCs to rows without avatarUrl, so
every row fell back to initials. Pass profile.avatar.uri through.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(org-settings): Associates tab lives at /settings/associates; /settings/community redirects
Every other org settings tab URL matches its name. Rename the tab id
community -> associates (route, tab list, index/guard/authorization
redirects, tab label key in all six locales) and keep the old
/settings/community URL as a redirect, since the tab was renamed as
part of #1897.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* 0.170.3
---------
Co-authored-by: Marlise Verwijs <199071362+MarliseVerwijs@users.noreply.github.com>
Co-authored-by: Polina Boneva <13227501+polibb@users.noreply.github.com>
Co-authored-by: Carlos Cano <carlos@alkem.io>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: Svetoslav Petkov <svetoslav@alkem.io>
Co-authored-by: Anton Starikov <ant.starikov@gmail.com>
Contributor
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.