@openchambery/web 1.19.4 → 1.19.5-beta.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/{AgentAvatar-CVTgW_rx.js → AgentAvatar-BceI_qBC.js} +1 -1
- package/dist/assets/{AssistantView-D73E__f8.js → AssistantView-lSnZ7082.js} +1 -1
- package/dist/assets/{ChatContainer-pWolXKsP.js → ChatContainer-DJLqUdGE.js} +39 -39
- package/dist/assets/ContextPanel-CfO8NpOL.js +285 -0
- package/dist/assets/{DiagramView-BJBf7KGE.js → DiagramView-_LIlKVbO.js} +1 -1
- package/dist/assets/{FadeInOnReveal-Cy3sf2M5.js → FadeInOnReveal-Cj9ZC3Ab.js} +1 -1
- package/dist/assets/{Icon-BLWVqBee.js → Icon-Du1LX11Q.js} +1 -1
- package/dist/assets/MarkdownRendererImpl-DpapNgFo.js +7 -0
- package/dist/assets/{MobileDetailNavigation-CUq86QUr.js → MobileDetailNavigation-Ct_3mr5K.js} +1 -1
- package/dist/assets/{MobileShareBridge-BMolTtSL.js → MobileShareBridge-7LjBYxHH.js} +2 -2
- package/dist/assets/MobileSurface-CuEJc-ya.js +2 -0
- package/dist/assets/{MultiRunWindow-Bv3moZF8.js → MultiRunWindow-BEM2Ucj4.js} +1 -1
- package/dist/assets/{OnboardingScreen-CTNi2zuA.js → OnboardingScreen-Czusf7rR.js} +1 -1
- package/dist/assets/{OverlayScrollbar-GQgJusKL.js → OverlayScrollbar-oOLMqq2s.js} +1 -1
- package/dist/assets/SettingsView-DUeyhTBn.js +25 -0
- package/dist/assets/{SettingsWindow-BMtwRXID.js → SettingsWindow-Qa8HXF_v.js} +1 -1
- package/dist/assets/{TerminalView-DwzG7zBO.js → TerminalView-CBSreprw.js} +1 -1
- package/dist/assets/{ToolOutputDialog-iYAB6WiK.js → ToolOutputDialog-CRIU0XUQ.js} +1 -1
- package/dist/assets/{angular-html-BXZQHcPZ.js → angular-html-CtgxlIZT.js} +1 -1
- package/dist/assets/{angular-ts-D8x7dmpg.js → angular-ts-DrvebXRV.js} +1 -1
- package/dist/assets/{apl-ugbjmfzm.js → apl-ct3-l_ov.js} +1 -1
- package/dist/assets/appThemeRegistry-CvvscooO.js +2 -0
- package/dist/assets/{astro-BrfzL78y.js → astro-9KCX_vNx.js} +1 -1
- package/dist/assets/{blade-D4cYRUdW.js → blade-DHzjLhcB.js} +1 -1
- package/dist/assets/{button-CeCkEpvn.js → button-p6vbd3r6.js} +1 -1
- package/dist/assets/{c-CMzMVpWc.js → c-CEfQ88z4.js} +1 -1
- package/dist/assets/{clipboard-BXC1vZwE.js → clipboard-Ca2PSbr_.js} +1 -1
- package/dist/assets/{cobol-DthQPUuS.js → cobol-ffQsCDPB.js} +1 -1
- package/dist/assets/{coffee-8Pqo9WtM.js → coffee-DtRj345i.js} +1 -1
- package/dist/assets/{collapsible-B-aagADh.js → collapsible-Cimoz0kx.js} +1 -1
- package/dist/assets/{cpp-D18L7odM.js → cpp-NWnna9VM.js} +1 -1
- package/dist/assets/{crystal-9GeLf5Ic.js → crystal-CnnqEH_e.js} +1 -1
- package/dist/assets/{css-CCmJvSl6.js → css-BRwHOW3t.js} +1 -1
- package/dist/assets/{desktopHosts-BrrYPqKq.js → desktopHosts-DceeVBky.js} +1 -1
- package/dist/assets/{desktopRelayRestore-DR60DATT.js → desktopRelayRestore-B0MRdR5y.js} +1 -1
- package/dist/assets/{diagram-Cxma_IKI.js → diagram-DX_MPRs3.js} +1 -1
- package/dist/assets/{dialog-wraJHZQ2.js → dialog-B2ieeCSR.js} +1 -1
- package/dist/assets/{edge-BNU68miL.js → edge-BmL4YNvW.js} +1 -1
- package/dist/assets/{elixir-Clm-oycI.js → elixir-m9Mcv-AW.js} +1 -1
- package/dist/assets/{elm-DyuJMdF4.js → elm-Dazsuz2c.js} +1 -1
- package/dist/assets/{erb-BGQIoPeF.js → erb-CJ1X7LGG.js} +1 -1
- package/dist/assets/{es-DQqFAaWT.js → es-Bj9HwM-0.js} +3 -3
- package/dist/assets/{fileQueries-BJKLdYos.js → fileQueries-BDld6h3B.js} +1 -1
- package/dist/assets/{fr-CNfGRpc1.js → fr-BgEHZGug.js} +3 -3
- package/dist/assets/{git-rebase-ZHVbPCu8.js → git-rebase-DHqm8iFM.js} +1 -1
- package/dist/assets/{gitApi-C6XQwHcm.js → gitApi-BVB4hH6A.js} +3 -3
- package/dist/assets/{glimmer-js-Dx29eTyZ.js → glimmer-js-529w2Uvl.js} +1 -1
- package/dist/assets/{glimmer-ts-7AFohuRx.js → glimmer-ts-B38MdnnB.js} +1 -1
- package/dist/assets/{glsl-Nbih-1pG.js → glsl-IFfKD25X.js} +1 -1
- package/dist/assets/{graphql-IOG_4fSc.js → graphql-m-L9qWWB.js} +1 -1
- package/dist/assets/{hack-DyZU34CD.js → hack-S2S7pq0v.js} +1 -1
- package/dist/assets/{haml-mwEldSxy.js → haml-BE8HBwbH.js} +1 -1
- package/dist/assets/{handlebars-BRbCg-TR.js → handlebars-DPMWj4Da.js} +1 -1
- package/dist/assets/{html-derivative-DpusPW9p.js → html-derivative-D0yehinc.js} +1 -1
- package/dist/assets/{html-DH50wxss.js → html-gfIVmLgx.js} +1 -1
- package/dist/assets/{http-fM2Ft62a.js → http-Yj1uQSnx.js} +1 -1
- package/dist/assets/{hurl-C7ZRA11y.js → hurl-CiAC6gLf.js} +1 -1
- package/dist/assets/{input-CyG05w-V.js → input-gaFVNq1P.js} +1 -1
- package/dist/assets/insertionBoundaries-Cpj9r_o6.js +4 -0
- package/dist/assets/{ja-CQqehnVn.js → ja-clyxf6re.js} +3 -3
- package/dist/assets/{java-CuLv1JzP.js → java-lPyr66Qw.js} +1 -1
- package/dist/assets/{javascript-e9R9mij1.js → javascript-BlmKDQ-I.js} +1 -1
- package/dist/assets/{jinja-BqL4dpCe.js → jinja-DrvjNn4y.js} +1 -1
- package/dist/assets/{jison-BU1E246U.js → jison-BZ-k3PlS.js} +1 -1
- package/dist/assets/{json-CWUAz4dE.js → json-CaQi8cq5.js} +1 -1
- package/dist/assets/{jsx-Tb9QFw4P.js → jsx-kfzB3oTi.js} +1 -1
- package/dist/assets/{julia-C6q7nYBJ.js → julia-Bup0jpIM.js} +1 -1
- package/dist/assets/{just-CLHVCPQG.js → just-XMsX5iKL.js} +1 -1
- package/dist/assets/{ko-kmwi8UJD.js → ko-BZb3gnuq.js} +3 -3
- package/dist/assets/{latex-DzR96v9w.js → latex-CHhOHp9u.js} +1 -1
- package/dist/assets/{liquid-UjsTutWK.js → liquid-C90FVKxw.js} +1 -1
- package/dist/assets/{lua-laWCoC7H.js → lua-BXPwTBAD.js} +1 -1
- package/dist/assets/main-BsdsPsHT.js +29 -0
- package/dist/assets/{main-OM6EVqFo.js → main-CQLsl_QE.js} +2 -2
- package/dist/assets/markdown-shiki.worker-woF2hOAN.js +349 -0
- package/dist/assets/{markdown-worker-DqO-GnVj.js → markdown-worker-CoO82KYF.js} +4 -4
- package/dist/assets/{markdownTheme-H7_7lbuh.js → markdownTheme-St8zxDvJ.js} +1 -1
- package/dist/assets/{marko-Cw81uHQK.js → marko-oFkW-Ef7.js} +1 -1
- package/dist/assets/{mdc-5VRIrvpo.js → mdc-Dt17Fv3G.js} +1 -1
- package/dist/assets/{miniChat-CWJejsNH.js → miniChat-w-S3l9bR.js} +2 -2
- package/dist/assets/mobile-CHDToW-w.js +2 -0
- package/dist/assets/multirun-CMEZHI5g.js +4 -0
- package/dist/assets/{nginx-OjbRtDTh.js → nginx-LfNI-F2k.js} +1 -1
- package/dist/assets/{nim-BqzUQFfL.js → nim-so0j2_Ky.js} +1 -1
- package/dist/assets/{perl-Bba2cEgf.js → perl-DQcGQU9O.js} +1 -1
- package/dist/assets/{persistence-DE9RnVcA.js → persistence-CLUawIlm.js} +1 -1
- package/dist/assets/{php-CkVgF6HF.js → php-Dae1IcCM.js} +1 -1
- package/dist/assets/{pl-Blv5o2QA.js → pl-CwZzSvrm.js} +3 -3
- package/dist/assets/{platform-MjT8iqKB.js → platform-BrncW9ab.js} +1 -1
- package/dist/assets/{projectMeta-DCJQ6I3m.js → projectMeta-ZD-iXSOT.js} +2 -2
- package/dist/assets/{pt-BR-BK74t-Bd.js → pt-BR-6ali2BQX.js} +3 -3
- package/dist/assets/{pug-Dj44SzNY.js → pug-CaHd5xEa.js} +1 -1
- package/dist/assets/{qml-DETHcUPx.js → qml-lLqImktD.js} +1 -1
- package/dist/assets/{r-CrbPkwLJ.js → r-C2VuzPsf.js} +1 -1
- package/dist/assets/{radio-MAnQ7sDW.js → radio-B-nm8u03.js} +1 -1
- package/dist/assets/{razor-56IyEPA5.js → razor-DTpzsAil.js} +1 -1
- package/dist/assets/{regexp-BEMDgR1N.js → regexp-DJeMtopQ.js} +1 -1
- package/dist/assets/renderElectronMiniChatApp-65t0A6BD.js +2 -0
- package/dist/assets/renderMobileApp-a3Joz-yn.js +25 -0
- package/dist/assets/{rst-C8BEzC8U.js → rst-c6kvN9mK.js} +1 -1
- package/dist/assets/{ruby-DWyeL44t.js → ruby-B68kNTjT.js} +1 -1
- package/dist/assets/{runtimeConfig-C2Vygqku.js → runtimeConfig-BGuiq1aJ.js} +1 -1
- package/dist/assets/runtimeEndpointReset-HC9CW_hT.js +4 -0
- package/dist/assets/{sas-Dt-MhHv8.js → sas-Br3BRi6f.js} +1 -1
- package/dist/assets/{scss-jmTM-KWO.js → scss-ugvUHawi.js} +1 -1
- package/dist/assets/{shellscript-BgI_n_Bv.js → shellscript-Ci5rJl0t.js} +1 -1
- package/dist/assets/{shellsession-DhmUadx_.js → shellsession-Dxn1WKgk.js} +1 -1
- package/dist/assets/{shikiHighlight-Bpy8sf5r.js → shikiHighlight-DFiqYozo.js} +1 -1
- package/dist/assets/{sortable-tabs-strip-BdbKDDAP.js → sortable-tabs-strip-CxLE1X9x.js} +1 -1
- package/dist/assets/{soy-qfQbh7dQ.js → soy-kgnQSRVZ.js} +1 -1
- package/dist/assets/{sql-CwyNz0Gh.js → sql-Cx3XaVwp.js} +1 -1
- package/dist/assets/src-D-LxNwlu.css +2 -0
- package/dist/assets/{stata-D9MlkA-a.js → stata-B5YhL3cp.js} +1 -1
- package/dist/assets/{store-UweBeN2y.js → store-BvrCRq6U.js} +3 -3
- package/dist/assets/{surrealql-xR93a923.js → surrealql-B8ysTfdK.js} +1 -1
- package/dist/assets/{svelte-BA7ahdky.js → svelte-D5-K3GME.js} +1 -1
- package/dist/assets/{templ-CxpzFYl8.js → templ-n2DDBrat.js} +1 -1
- package/dist/assets/{terminalApi-Bnx0KvaV.js → terminalApi-D2OzR0I3.js} +1 -1
- package/dist/assets/{tex-CePKgOWg.js → tex-CpNhXIXy.js} +1 -1
- package/dist/assets/{tooltip-B__x-HDJ.js → tooltip-D1a6rKLA.js} +1 -1
- package/dist/assets/{ts-tags-DP2fsj53.js → ts-tags-CrtENrUj.js} +1 -1
- package/dist/assets/{tsx-CI52QFY8.js → tsx-DEnzboO-.js} +1 -1
- package/dist/assets/{twig-BjoebDgo.js → twig-B7PeheP0.js} +1 -1
- package/dist/assets/{typescript-cpHrN9Ya.js → typescript-CUlPIybL.js} +1 -1
- package/dist/assets/{uk-6I-KXy3R.js → uk-qRqJs4GS.js} +3 -3
- package/dist/assets/{url-CTuw7Rjf.js → url-zwuNq3GS.js} +1 -1
- package/dist/assets/{useAppFontEffects-BVVxQm3q.js → useAppFontEffects-pF6rKdKM.js} +4 -4
- package/dist/assets/{useAssistantUIStore-D2K7ksGj.js → useAssistantUIStore-D699Sbdb.js} +1 -1
- package/dist/assets/{useDesktopWindowControlsLayout-BR2bI_26.js → useDesktopWindowControlsLayout-DH7Om-wY.js} +1 -1
- package/dist/assets/{useEffectiveDirectory-DdS6rQm9.js → useEffectiveDirectory-BvZFK9wU.js} +1 -1
- package/dist/assets/{useFontPreferences-CWZnNRjz.js → useFontPreferences-F2QPNE2V.js} +1 -1
- package/dist/assets/{useMobileNavigationStore-DFSb2gzX.js → useMobileNavigationStore-CGOeJb9D.js} +10 -10
- package/dist/assets/{useSessionAutoCleanup-BNC1lVkC.js → useSessionAutoCleanup-lBn8oZ8f.js} +1 -1
- package/dist/assets/{useUIStore-DOxLVeGb.js → useUIStore-BXjMQrel.js} +1 -1
- package/dist/assets/{useWorkerHighlightedLines-B9mnTmvs.js → useWorkerHighlightedLines-Bc1NJlYO.js} +2 -2
- package/dist/assets/{utils-C2R0YoC5.js → utils-D94hxQYq.js} +1 -1
- package/dist/assets/{vue-DK5KPkZ5.js → vue-C6ZZy2AI.js} +1 -1
- package/dist/assets/{vue-html-BlUa8woS.js → vue-html-DsvGlIY2.js} +1 -1
- package/dist/assets/{vue-vine-D3hlJyLi.js → vue-vine-C8VLalyn.js} +1 -1
- package/dist/assets/{xml-CspvrcYx.js → xml-ChNx2zKg.js} +1 -1
- package/dist/assets/{xsl-D10hdnv5.js → xsl-yWKkpZli.js} +1 -1
- package/dist/assets/{yaml-C00-kDDU.js → yaml-CENNGtdf.js} +1 -1
- package/dist/assets/{zh-CN-DwlzS4yK.js → zh-CN-CwZ0adcY.js} +3 -3
- package/dist/assets/{zh-TW-p0VriyfT.js → zh-TW-DC0jiCzA.js} +3 -3
- package/dist/index.html +9 -9
- package/dist/mini-chat.html +9 -9
- package/dist/mobile.html +9 -9
- package/package.json +1 -1
- package/server/index.js +40 -3
- package/server/lib/notifications/DOCUMENTATION.md +2 -2
- package/server/lib/notifications/push-runtime.js +4 -14
- package/server/lib/notifications/runtime.js +11 -20
- package/server/lib/notifications/runtime.test.js +4 -6
- package/server/lib/opencode/core-routes.js +56 -23
- package/server/lib/opencode/core-routes.test.js +206 -6
- package/server/lib/relay/DOCUMENTATION.md +2 -1
- package/server/lib/relay/service.js +223 -71
- package/server/lib/relay/service.test.js +96 -13
- package/dist/assets/ContextPanel-GBq5lSES.js +0 -285
- package/dist/assets/MarkdownRendererImpl-BUXY9iuu.js +0 -7
- package/dist/assets/MobileSurface-DDRPrlAq.js +0 -2
- package/dist/assets/SettingsView-Ip_jVXoD.js +0 -25
- package/dist/assets/appThemeRegistry-rHYlrhb5.js +0 -2
- package/dist/assets/insertionBoundaries-DG_1_ENK.js +0 -4
- package/dist/assets/main-MkZ3xcbE.js +0 -29
- package/dist/assets/markdown-shiki.worker-CKKjvdhc.js +0 -12
- package/dist/assets/mobile-D-ubuubW.js +0 -2
- package/dist/assets/multirun-YB7NFEGX.js +0 -4
- package/dist/assets/renderElectronMiniChatApp-BPZ5LfbX.js +0 -2
- package/dist/assets/renderMobileApp-pqjbEzlN.js +0 -23
- package/dist/assets/runtimeEndpointReset-CuW2UDPM.js +0 -4
- package/dist/assets/src-eGuNWxHc.css +0 -2
|
@@ -915,13 +915,17 @@ describe('core-routes', () => {
|
|
|
915
915
|
]);
|
|
916
916
|
});
|
|
917
917
|
|
|
918
|
-
it('
|
|
919
|
-
const getRelayPairingCandidate = vi.fn()
|
|
918
|
+
it('lets paired desktop clients create pairing sessions like the local owner (relay-attached parity)', async () => {
|
|
919
|
+
const getRelayPairingCandidate = vi.fn(async () => ({
|
|
920
|
+
type: 'relay',
|
|
921
|
+
relayUrl: 'wss://relay.example/ws',
|
|
922
|
+
serverId: 'srv_desktop',
|
|
923
|
+
hostEncPubJwk: { kty: 'EC', crv: 'P-256', x: 'aaa', y: 'bbb' },
|
|
924
|
+
priority: 30,
|
|
925
|
+
}));
|
|
920
926
|
const { app, dependencies } = createPairingRouteApp({
|
|
921
927
|
getRelayPairingCandidate,
|
|
922
928
|
uiAuthController: {
|
|
923
|
-
// Force a remote desktop client past create-auth so the relayUrl gate is
|
|
924
|
-
// exercised independently of the desktop-local create-auth admission rule.
|
|
925
929
|
resolveAuthContext: vi.fn(async () => ({
|
|
926
930
|
type: 'client',
|
|
927
931
|
clientId: 'remote-desktop-1',
|
|
@@ -943,15 +947,152 @@ describe('core-routes', () => {
|
|
|
943
947
|
},
|
|
944
948
|
});
|
|
945
949
|
|
|
950
|
+
const response = await request(app)
|
|
951
|
+
.post('/api/client-auth/pairing/sessions')
|
|
952
|
+
.set('Host', 'runtime.example')
|
|
953
|
+
.send({ label: 'Pair phone', includeRelay: true })
|
|
954
|
+
.expect(201);
|
|
955
|
+
|
|
956
|
+
expect(dependencies.clientPairingRuntime.createPairingSession).toHaveBeenCalledWith({
|
|
957
|
+
label: 'Pair phone',
|
|
958
|
+
allowedClientKinds: undefined,
|
|
959
|
+
createdByClientId: 'remote-desktop-1',
|
|
960
|
+
usesRelay: true,
|
|
961
|
+
});
|
|
962
|
+
expect(response.body.server.candidates).toEqual([
|
|
963
|
+
{ type: 'lan', url: 'http://runtime.example', priority: 10 },
|
|
964
|
+
expect.objectContaining({ type: 'relay', relayUrl: 'wss://relay.example/ws' }),
|
|
965
|
+
]);
|
|
966
|
+
});
|
|
967
|
+
|
|
968
|
+
it('rejects pairing-session creation from mobile client tokens', async () => {
|
|
969
|
+
const { app, dependencies } = createPairingRouteApp({
|
|
970
|
+
uiAuthController: {
|
|
971
|
+
resolveAuthContext: vi.fn(async () => ({
|
|
972
|
+
type: 'client',
|
|
973
|
+
clientId: 'phone-1',
|
|
974
|
+
client: { id: 'phone-1', clientKind: 'mobile' },
|
|
975
|
+
})),
|
|
976
|
+
requireAuth: vi.fn((_req, _res, next) => next()),
|
|
977
|
+
requireSessionAuth: vi.fn((_req, _res, next) => next()),
|
|
978
|
+
handleSessionStatus: vi.fn(),
|
|
979
|
+
handleSessionCreate: vi.fn(),
|
|
980
|
+
handleUrlAuthToken: vi.fn(),
|
|
981
|
+
handlePasskeyStatus: vi.fn(),
|
|
982
|
+
handlePasskeyAuthenticationOptions: vi.fn(),
|
|
983
|
+
handlePasskeyAuthenticationVerify: vi.fn(),
|
|
984
|
+
handlePasskeyRegistrationOptions: vi.fn(),
|
|
985
|
+
handlePasskeyRegistrationVerify: vi.fn(),
|
|
986
|
+
handlePasskeyList: vi.fn(),
|
|
987
|
+
handlePasskeyRevoke: vi.fn(),
|
|
988
|
+
handleResetAuth: vi.fn(),
|
|
989
|
+
},
|
|
990
|
+
});
|
|
991
|
+
|
|
946
992
|
await request(app)
|
|
947
993
|
.post('/api/client-auth/pairing/sessions')
|
|
948
|
-
.send({
|
|
994
|
+
.send({ label: 'Pair phone' })
|
|
949
995
|
.expect(403, { error: 'Client tokens cannot create remote clients' });
|
|
950
996
|
|
|
997
|
+
expect(dependencies.clientPairingRuntime.createPairingSession).not.toHaveBeenCalled();
|
|
998
|
+
});
|
|
999
|
+
|
|
1000
|
+
it('forbids paired desktop clients from re-pointing the Relay endpoint when pairing', async () => {
|
|
1001
|
+
const getRelayPairingCandidate = vi.fn();
|
|
1002
|
+
const getEffectiveRelayUrls = vi.fn(async () => ['wss://relay.example/ws']);
|
|
1003
|
+
const { app, dependencies } = createPairingRouteApp({
|
|
1004
|
+
getRelayPairingCandidate,
|
|
1005
|
+
getEffectiveRelayUrls,
|
|
1006
|
+
uiAuthController: {
|
|
1007
|
+
resolveAuthContext: vi.fn(async () => ({
|
|
1008
|
+
type: 'client',
|
|
1009
|
+
clientId: 'remote-desktop-1',
|
|
1010
|
+
client: { id: 'remote-desktop-1', clientKind: 'desktop' },
|
|
1011
|
+
})),
|
|
1012
|
+
requireAuth: vi.fn((_req, _res, next) => next()),
|
|
1013
|
+
requireSessionAuth: vi.fn((_req, _res, next) => next()),
|
|
1014
|
+
handleSessionStatus: vi.fn(),
|
|
1015
|
+
handleSessionCreate: vi.fn(),
|
|
1016
|
+
handleUrlAuthToken: vi.fn(),
|
|
1017
|
+
handlePasskeyStatus: vi.fn(),
|
|
1018
|
+
handlePasskeyAuthenticationOptions: vi.fn(),
|
|
1019
|
+
handlePasskeyAuthenticationVerify: vi.fn(),
|
|
1020
|
+
handlePasskeyRegistrationOptions: vi.fn(),
|
|
1021
|
+
handlePasskeyRegistrationVerify: vi.fn(),
|
|
1022
|
+
handlePasskeyList: vi.fn(),
|
|
1023
|
+
handlePasskeyRevoke: vi.fn(),
|
|
1024
|
+
handleResetAuth: vi.fn(),
|
|
1025
|
+
},
|
|
1026
|
+
});
|
|
1027
|
+
|
|
1028
|
+
await request(app)
|
|
1029
|
+
.post('/api/client-auth/pairing/sessions')
|
|
1030
|
+
.send({ includeRelay: true, relayUrl: 'wss://attacker.example/ws' })
|
|
1031
|
+
.expect(403, { error: 'Only the owner UI session can set a custom Relay endpoint' });
|
|
1032
|
+
|
|
951
1033
|
expect(getRelayPairingCandidate).not.toHaveBeenCalled();
|
|
952
1034
|
expect(dependencies.clientPairingRuntime.createPairingSession).not.toHaveBeenCalled();
|
|
953
1035
|
});
|
|
954
1036
|
|
|
1037
|
+
it('lets paired desktop clients echo the current Relay endpoint when pairing', async () => {
|
|
1038
|
+
const getRelayPairingCandidate = vi.fn(async ({ relayUrl }) => ({
|
|
1039
|
+
type: 'relay',
|
|
1040
|
+
relayUrl,
|
|
1041
|
+
serverId: 'srv_desktop',
|
|
1042
|
+
hostEncPubJwk: { kty: 'EC', crv: 'P-256', x: 'aaa', y: 'bbb' },
|
|
1043
|
+
priority: 30,
|
|
1044
|
+
}));
|
|
1045
|
+
const getEffectiveRelayUrls = vi.fn(async () => ['wss://relay.example/ws', 'wss://relay2.example/ws']);
|
|
1046
|
+
const { app } = createPairingRouteApp({
|
|
1047
|
+
getRelayPairingCandidate,
|
|
1048
|
+
getEffectiveRelayUrls,
|
|
1049
|
+
uiAuthController: {
|
|
1050
|
+
resolveAuthContext: vi.fn(async () => ({
|
|
1051
|
+
type: 'client',
|
|
1052
|
+
clientId: 'remote-desktop-1',
|
|
1053
|
+
client: { id: 'remote-desktop-1', clientKind: 'desktop' },
|
|
1054
|
+
})),
|
|
1055
|
+
requireAuth: vi.fn((_req, _res, next) => next()),
|
|
1056
|
+
requireSessionAuth: vi.fn((_req, _res, next) => next()),
|
|
1057
|
+
handleSessionStatus: vi.fn(),
|
|
1058
|
+
handleSessionCreate: vi.fn(),
|
|
1059
|
+
handleUrlAuthToken: vi.fn(),
|
|
1060
|
+
handlePasskeyStatus: vi.fn(),
|
|
1061
|
+
handlePasskeyAuthenticationOptions: vi.fn(),
|
|
1062
|
+
handlePasskeyAuthenticationVerify: vi.fn(),
|
|
1063
|
+
handlePasskeyRegistrationOptions: vi.fn(),
|
|
1064
|
+
handlePasskeyRegistrationVerify: vi.fn(),
|
|
1065
|
+
handlePasskeyList: vi.fn(),
|
|
1066
|
+
handlePasskeyRevoke: vi.fn(),
|
|
1067
|
+
handleResetAuth: vi.fn(),
|
|
1068
|
+
},
|
|
1069
|
+
});
|
|
1070
|
+
|
|
1071
|
+
const response = await request(app)
|
|
1072
|
+
.post('/api/client-auth/pairing/sessions')
|
|
1073
|
+
.send({ includeRelay: true, includeDirect: false, relayUrl: 'wss://relay.example/ws' })
|
|
1074
|
+
.expect(201);
|
|
1075
|
+
|
|
1076
|
+
expect(getRelayPairingCandidate).toHaveBeenCalledWith({
|
|
1077
|
+
ensureEnabled: true,
|
|
1078
|
+
relayUrl: 'wss://relay.example/ws',
|
|
1079
|
+
});
|
|
1080
|
+
expect(response.body.server.candidates).toEqual([
|
|
1081
|
+
expect.objectContaining({ type: 'relay', relayUrl: 'wss://relay.example/ws' }),
|
|
1082
|
+
]);
|
|
1083
|
+
|
|
1084
|
+
// Multi-relay: any CONFIGURED endpoint is fair game for a paired desktop
|
|
1085
|
+
// (choosing between the host's endpoints is not a re-point).
|
|
1086
|
+
await request(app)
|
|
1087
|
+
.post('/api/client-auth/pairing/sessions')
|
|
1088
|
+
.send({ includeRelay: true, includeDirect: false, relayUrl: 'wss://relay2.example/ws' })
|
|
1089
|
+
.expect(201);
|
|
1090
|
+
expect(getRelayPairingCandidate).toHaveBeenLastCalledWith({
|
|
1091
|
+
ensureEnabled: true,
|
|
1092
|
+
relayUrl: 'wss://relay2.example/ws',
|
|
1093
|
+
});
|
|
1094
|
+
});
|
|
1095
|
+
|
|
955
1096
|
it('still lets desktop-local create pairing sessions without a custom Relay endpoint', async () => {
|
|
956
1097
|
const getRelayPairingCandidate = vi.fn(async () => null);
|
|
957
1098
|
const { app, dependencies } = createPairingRouteApp({
|
|
@@ -1287,6 +1428,25 @@ describe('client auth routes', () => {
|
|
|
1287
1428
|
expect(response.body.candidates).toEqual([]);
|
|
1288
1429
|
});
|
|
1289
1430
|
|
|
1431
|
+
it('returns every configured relay endpoint in the candidates refresh (multi-relay)', async () => {
|
|
1432
|
+
const app = express();
|
|
1433
|
+
const relayCandidates = [
|
|
1434
|
+
{ type: 'relay', relayUrl: 'wss://relay.example/ws', serverId: 'server-abc', hostEncPubJwk: { kty: 'EC', crv: 'P-256', x: 'x', y: 'y' }, priority: 30 },
|
|
1435
|
+
{ type: 'relay', relayUrl: 'wss://relay2.example/ws', serverId: 'server-abc', hostEncPubJwk: { kty: 'EC', crv: 'P-256', x: 'x', y: 'y' }, priority: 30 },
|
|
1436
|
+
];
|
|
1437
|
+
const dependencies = {
|
|
1438
|
+
...createDependencies({ resolveAuthContext: async () => ({ type: 'client', clientId: 'client-1' }) }),
|
|
1439
|
+
getDirectCandidateUrls: () => [],
|
|
1440
|
+
getRelayPairingCandidates: async () => relayCandidates,
|
|
1441
|
+
getServerId: async () => 'server-abc',
|
|
1442
|
+
};
|
|
1443
|
+
registerAuthAndAccessRoutes(app, dependencies);
|
|
1444
|
+
|
|
1445
|
+
const response = await request(app).get('/api/client-auth/connection/candidates');
|
|
1446
|
+
expect(response.status).toBe(200);
|
|
1447
|
+
expect(response.body.candidates).toEqual(relayCandidates);
|
|
1448
|
+
});
|
|
1449
|
+
|
|
1290
1450
|
it('scopes non-desktop client credentials to list and revoke only themselves', async () => {
|
|
1291
1451
|
const app = express();
|
|
1292
1452
|
let authContext = { type: 'session' };
|
|
@@ -1355,7 +1515,47 @@ describe('client auth routes', () => {
|
|
|
1355
1515
|
expect(purged.body.purged).toBe(1);
|
|
1356
1516
|
});
|
|
1357
1517
|
|
|
1358
|
-
it('
|
|
1518
|
+
it('lets paired desktop clients list and revoke every device (relay-attached parity)', async () => {
|
|
1519
|
+
const app = express();
|
|
1520
|
+
let authContext = { type: 'session' };
|
|
1521
|
+
const dependencies = createDependencies({
|
|
1522
|
+
resolveAuthContext: async () => authContext,
|
|
1523
|
+
});
|
|
1524
|
+
registerAuthAndAccessRoutes(app, dependencies);
|
|
1525
|
+
|
|
1526
|
+
const localDesktop = await request(app)
|
|
1527
|
+
.post('/api/client-auth/clients')
|
|
1528
|
+
.send({ label: 'OpenChamber Desktop', clientKind: 'desktop-local' });
|
|
1529
|
+
const pairedDesktop = await request(app)
|
|
1530
|
+
.post('/api/client-auth/clients')
|
|
1531
|
+
.send({ label: 'Study desktop', clientKind: 'desktop' });
|
|
1532
|
+
const phone = await request(app)
|
|
1533
|
+
.post('/api/client-auth/clients')
|
|
1534
|
+
.send({ label: 'Phone', clientKind: 'mobile' });
|
|
1535
|
+
|
|
1536
|
+
// A paired desktop (connected over relay) manages all devices like the
|
|
1537
|
+
// local desktop shell — SSH-attach parity for relay-attached hosts.
|
|
1538
|
+
authContext = { type: 'client', clientId: pairedDesktop.body.client.id, client: pairedDesktop.body.client };
|
|
1539
|
+
|
|
1540
|
+
const listed = await request(app).get('/api/client-auth/clients');
|
|
1541
|
+
expect(listed.status).toBe(200);
|
|
1542
|
+
const listedIds = listed.body.clients.map((client) => client.id).sort();
|
|
1543
|
+
expect(listedIds).toEqual([
|
|
1544
|
+
localDesktop.body.client.id,
|
|
1545
|
+
pairedDesktop.body.client.id,
|
|
1546
|
+
phone.body.client.id,
|
|
1547
|
+
].sort());
|
|
1548
|
+
|
|
1549
|
+
const revoked = await request(app).delete(`/api/client-auth/clients/${phone.body.client.id}`);
|
|
1550
|
+
expect(revoked.status).toBe(200);
|
|
1551
|
+
expect(revoked.body.revoked).toBe(true);
|
|
1552
|
+
|
|
1553
|
+
const purged = await request(app).delete('/api/client-auth/clients');
|
|
1554
|
+
expect(purged.status).toBe(200);
|
|
1555
|
+
expect(purged.body.purged).toBeGreaterThanOrEqual(1);
|
|
1556
|
+
});
|
|
1557
|
+
|
|
1558
|
+
it('restricts direct client-token creation to operator clients', async () => {
|
|
1359
1559
|
const app = express();
|
|
1360
1560
|
let authContext = { type: 'session' };
|
|
1361
1561
|
const dependencies = createDependencies({
|
|
@@ -19,7 +19,7 @@ Traffic is modeled as three stacked layers. The relay understands only Layer 1;
|
|
|
19
19
|
## Entrypoints and structure
|
|
20
20
|
|
|
21
21
|
Host side (`packages/web/server/lib/relay/`):
|
|
22
|
-
- `service.js` — thin entrypoint: relay config (enabled flag +
|
|
22
|
+
- `service.js` — thin entrypoint: relay config (enabled flag + endpoints), the management routes (`GET/POST /api/openchamber/relay/{status,enable,disable}`, `POST /api/openchamber/relay/endpoints`), a `getPairingCandidate()` accessor (the relay transport candidate folded into pairing-v2 links when enabled, consumed by the pairing-session route in `core-routes.js`), and lifecycle wiring. **Multi-relay:** `settings.privateRelay = { enabled, relayUrl, extraRelayUrls }` — the host dials EVERY configured endpoint simultaneously with one identity (same serverId); clients treat `serverId + relayUrl` as the instance identity, so the same machine exposed via two domains appears as two separately named instances. The primary (`relayUrl`) owns push registration and stays the default pairing candidate. A pairing link created for an endpoint the host does not have yet APPENDS it as an extra endpoint (owner request only) instead of replacing the primary, so devices paired over the old domain keep working. `POST /relay/endpoints` (owner session / `desktop-local` only) replaces the extra-endpoint list wholesale and starts/stops host connections incrementally. Paired desktops may pair over any CONFIGURED endpoint but can never add one. Host lifecycle is gated by `isRelayHostRuntime()` (`desktop` or `ssh-remote`): other runtimes (`bun run dev`, `dev:web:hmr`, ordinary CLI `serve`, VS Code, plain `node server`) report `state: 'unavailable'`, refuse `/relay/enable`, `/relay/disable`, `/relay/endpoints`, and relay pairing with 403, never mint a relay identity, never advertise `relayAvailable`, and never call `startRelayHost`. Direct `node server/index.js` forces `OPENCHAMBER_RUNTIME=web` unless it is already `ssh-remote`, so a leftover desktop env cannot open the host-control socket. On host runtimes, started from `packages/web/server/index.js` when demand/opt-in enables the relay. Desktop auto-clears `privateRelay.enabled` when demand drops; `ssh-remote` keeps an explicit opt-in (e.g. `openchamber serve --relay-host`) sticky until disabled. The relay endpoint defaults to the OpenChamber-hosted relay but can be pinned to a self-hosted relay via the `OPENCHAMBER_RELAY_URL` env var; when set it overrides the stored setting for the host connection, the pairing candidate, and status, so paired clients inherit the endpoint automatically. Endpoint identity is scheme/host/path only: accepted schemes are `ws://` and `wss://`; URLs with userinfo are rejected (no silent default fallback); query and fragment are stripped before persistence and pairing candidates. Custom endpoint persistence and Host control-connection switching are explicit authenticated management actions. Pairing creation with `relayUrl` requires an owner UI session or the local `desktop-local` shell client; `/relay/enable` follows its API auth gate. `derivePushSendUrlFromRelayUrl` maps a canonical Relay `wss://`/`ws://` URL to the same host's `https://`/`http://` `/v1/push/send` path (register is `/v1/push/register-token`); `OPENCHAMBER_PUSH_RELAY_URL` on the Host overrides that derivation. The isolated Push process lives in `packages/relay-server/src/push/` and does not share Layer 1 credentials or tunnel state.
|
|
23
23
|
- `identity.js` — the host's stable identity: the long-lived signing keypair (shared with the push relay, defines the routing id) plus a long-lived encryption keypair (the E2EE trust anchor). Reused across restarts; never rotated implicitly.
|
|
24
24
|
- `signing-key.js` — storage/derivation of the signing keypair and the routing id, shared with the notifications runtime. Canonical public JWK hashing matches `packages/relay-server/src/push/crypto.js`.
|
|
25
25
|
- `host-client.js` — the long-lived connection manager: one outbound control connection to the relay, a per-client data connection for each connected device, reconnect/backoff, and the E2EE responder handshake per connection.
|
|
@@ -59,6 +59,7 @@ See also `packages/web/server/lib/opencode/DOCUMENTATION.md` (“Catalog / OpenC
|
|
|
59
59
|
## Authentication model
|
|
60
60
|
|
|
61
61
|
- The tunnel is **transport only**. The OpenChamber server still authenticates every tunneled request exactly as it authenticates a direct remote client. The relay path grants reachability, not authorization.
|
|
62
|
+
- A paired **desktop** client is a device-fleet operator: through the tunnel it can create pairing links, list every paired device, and revoke other devices (parity with the SSH attach flow, where the connecting desktop authenticates as the operator). Mobile (and any other) paired clients stay scoped to their own record. Re-pointing the Host relay endpoint set from a remote bearer remains forbidden — a paired desktop may name any endpoint the host already has, but adding/removing endpoints (including `relayUrl` in pairing requests for a new domain) is an owner-UI / local-desktop-shell action.
|
|
62
63
|
- Clients carry their normal credential. HTTP and SSE requests authenticate with the client's bearer token (a header). **WebSocket upgrades cannot send headers**, so they authenticate with a short-lived URL-scoped token minted beforehand and passed as a query parameter. This asymmetry is important when adding new WebSocket features (see the skill).
|
|
63
64
|
- The host authenticates itself to the relay with a signed handshake using its long-lived signing key.
|
|
64
65
|
- Enabling the relay is explicit opt-in and disabled by default; disabling it severs all relay reachability immediately.
|