@yuriai/web 0.1.2 → 0.1.3
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/{AssistantView-aO1htuC0.js → AssistantView-BJyGm9Js.js} +3 -3
- package/dist/assets/{DiagramView-DbtzXUOC.js → DiagramView-BRK8rzVX.js} +1 -1
- package/dist/assets/{ImageLoadingPlaceholder-BPp8BN0a.js → ImageLoadingPlaceholder-CGHGcrEo.js} +1 -1
- package/dist/assets/{LoadingImage-DvtPqFRw.js → LoadingImage-BG9q4Tm_.js} +1 -1
- package/dist/assets/{MarkdownRenderer-BlDd-zTg.js → MarkdownRenderer-D4Ba9SUF.js} +2 -2
- package/dist/assets/{MarkdownRendererImpl-O4nzQKpI.js → MarkdownRendererImpl-Dt0t56Yx.js} +1 -1
- package/dist/assets/{MarkstreamRendererImpl-B8USZU8R.js → MarkstreamRendererImpl-rGcpWsHj.js} +1 -1
- package/dist/assets/{MobileDetailNavigation-D8f__4GE.js → MobileDetailNavigation-Cyp_3Hha.js} +1 -1
- package/dist/assets/{MobileShareBridge-z70r79hc.js → MobileShareBridge-BQnwoSB3.js} +1 -1
- package/dist/assets/MobileTabPageHeader-CDFE4Ws4.js +2 -0
- package/dist/assets/{MultiRunWindow-F9XbZPCH.js → MultiRunWindow-rggd5jvJ.js} +1 -1
- package/dist/assets/{SettingsView-B-AWwURS.js → SettingsView-DWeiBwOG.js} +10 -10
- package/dist/assets/{SettingsWindow-UFwLu7_8.js → SettingsWindow-1csE2lF6.js} +1 -1
- package/dist/assets/{TerminalView-Cj9xXSXJ.js → TerminalView-mF-mbAzo.js} +1 -1
- package/dist/assets/{ToolOutputDialog-DIr7gmhc.js → ToolOutputDialog-BvgdBVst.js} +1 -1
- package/dist/assets/{appThemeRegistry-BttGHPXa.js → appThemeRegistry-CiQ61T5d.js} +1 -1
- package/dist/assets/{capgoAdapter-KNLHDj7G.js → capgoAdapter-DLi2tcmI.js} +1 -1
- package/dist/assets/{fileMentionTouchSelection-BFSbKBHy.js → fileMentionTouchSelection-C9jpvVUn.js} +1 -1
- package/dist/assets/{gitApi-CT2gHl-y.js → gitApi-nArpJO6q.js} +1 -1
- package/dist/assets/{image-loading-CVq5GLEY.js → image-loading-23c-W-bq.js} +1 -1
- package/dist/assets/{insertionBoundaries-D3e6uOkH.js → insertionBoundaries-BgHgsc1p.js} +1 -1
- package/dist/assets/{main-BujKFYwj.js → main-D5JkjH8k.js} +22 -22
- package/dist/assets/{main-BXcvTyKT.js → main-R2wbcT-3.js} +2 -2
- package/dist/assets/{mcpQueries-DpgzW70K.js → mcpQueries-W-LFzCOw.js} +1 -1
- package/dist/assets/{miniChat-5qOrHHEA.js → miniChat-ChGISVuo.js} +2 -2
- package/dist/assets/{mobile-BefkgomZ.js → mobile-Dv3wbcd9.js} +2 -2
- package/dist/assets/{mobileBackNavigation-DOPWc25L.js → mobileBackNavigation-BnipJtFo.js} +1 -1
- package/dist/assets/{multirun-Dzh_Fsid.js → multirun-DvSn-icg.js} +1 -1
- package/dist/assets/{projectMeta-O4wg5jiy.js → projectMeta-Cm0n6vtB.js} +1 -1
- package/dist/assets/renderElectronMiniChatApp-BjQyp-u8.js +2 -0
- package/dist/assets/{renderMobileApp-D4M59yMR.js → renderMobileApp-BEkWDH7L.js} +6 -6
- package/dist/assets/runtimeConfig-D3moXtPz.js +2 -0
- package/dist/assets/{runtimeEndpointReset-DCy2sHAD.js → runtimeEndpointReset-BYn28E2A.js} +5 -5
- package/dist/assets/{sessionChangeSummary-D3G985mf.js → sessionChangeSummary-DXpoQiGE.js} +1 -1
- package/dist/assets/{sessionLookup-BhaDgtSq.js → sessionLookup-BYycaYEQ.js} +1 -1
- package/dist/assets/{speechSettingsQueries-C3wBvjjw.js → speechSettingsQueries-Bv08BFlI.js} +1 -1
- package/dist/assets/src-CmermOKP.css +2 -0
- package/dist/assets/types-Q_utERET.js +2 -0
- package/dist/assets/{useAppFontEffects-Dp_Rk9nw.js → useAppFontEffects-HYIeI-xf.js} +6 -6
- package/dist/assets/{useDesktopWindowControlsLayout-Jt77zqdp.js → useDesktopWindowControlsLayout-Cc-2uuN5.js} +1 -1
- package/dist/assets/{useEffectiveDirectory-aQIIxV4x.js → useEffectiveDirectory-BBb97raM.js} +1 -1
- package/dist/assets/{useMobileNavigationStore-CO6CAp4i.js → useMobileNavigationStore-CEVJSNnR.js} +3 -3
- package/dist/assets/{useWorkerHighlightedLines-DDXiS_7t.js → useWorkerHighlightedLines-D8JkeSuf.js} +1 -1
- package/dist/index.html +12 -12
- package/dist/mini-chat.html +6 -6
- package/dist/mobile.html +8 -8
- package/package.json +1 -1
- package/server/lib/notifications/APNS.md +13 -0
- package/server/lib/notifications/DOCUMENTATION.md +4 -0
- package/server/lib/notifications/apns-runtime.js +45 -24
- package/server/lib/notifications/apns-runtime.test.js +57 -1
- package/server/lib/notifications/routes.js +6 -2
- package/server/lib/opencode/DOCUMENTATION.md +2 -0
- package/server/lib/opencode/pwa-manifest-routes.js +2 -2
- package/server/lib/opencode/settings-runtime.js +1 -1
- package/server/lib/opencode/settings-runtime.test.js +15 -0
- package/server/lib/opencode/static-routes-runtime.js +6 -6
- package/dist/assets/MobileTabPageHeader-yWc9DWuc.js +0 -2
- package/dist/assets/renderElectronMiniChatApp-C5KLkJSM.js +0 -2
- package/dist/assets/runtimeConfig-BDh3gBH4.js +0 -2
- package/dist/assets/src-BEVRKYT8.css +0 -2
- package/dist/assets/types-BuiqMmh_.js +0 -1
package/dist/assets/{useWorkerHighlightedLines-DDXiS_7t.js → useWorkerHighlightedLines-D8JkeSuf.js}
RENAMED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{r as e}from"./rolldown-runtime-hePW80VL.js";import{it as t,rt as n}from"./vendor-base-ui-CdzJ6My_.js";import{v as r}from"./useUIStore-NMQx_cnG.js";import{n as i}from"./vendor-react-BP2cRVqX.js";import{f as a,r as o,t as s}from"./button-C3292Ee6.js";import{n as c}from"./useThemeSystem-8jhE4Zyk.js";import{n as l}from"./Icon-jsQQXouE.js";import{i as u,n as d,t as f}from"./tooltip-DGUZtHif.js";import{n as p}from"./MarkdownRenderer-
|
|
1
|
+
import{r as e}from"./rolldown-runtime-hePW80VL.js";import{it as t,rt as n}from"./vendor-base-ui-CdzJ6My_.js";import{v as r}from"./useUIStore-NMQx_cnG.js";import{n as i}from"./vendor-react-BP2cRVqX.js";import{f as a,r as o,t as s}from"./button-C3292Ee6.js";import{n as c}from"./useThemeSystem-8jhE4Zyk.js";import{n as l}from"./Icon-jsQQXouE.js";import{i as u,n as d,t as f}from"./tooltip-DGUZtHif.js";import{n as p}from"./MarkdownRenderer-D4Ba9SUF.js";import{n as m,o as h,s as g,t as _}from"./markdown-worker-CkO3sT4Y.js";import{t as v}from"./vendor-tanstack-react-virtual-B_EO--9t.js";import{n as y}from"./markdownTheme-DHoKvD6O.js";var b=e(t(),1),x=i(),S=n(),C=`w-[min(30rem,calc(100cqw-1rem))] min-w-[280px] max-w-full overflow-hidden rounded-xl shadow-[inset_0_1px_0_0_rgba(255,255,255,0.8),inset_0_0_0_1px_rgba(0,0,0,0.04),0_0_0_1px_rgba(0,0,0,0.10),0_1px_2px_-0.5px_rgba(0,0,0,0.08),0_4px_8px_-2px_rgba(0,0,0,0.08),0_12px_20px_-4px_rgba(0,0,0,0.08)] dark:shadow-[inset_0_1px_0_0_rgba(255,255,255,0.12),inset_0_0_0_1px_rgba(255,255,255,0.08),0_0_0_1px_rgba(0,0,0,0.36),0_1px_1px_-0.5px_rgba(0,0,0,0.22),0_3px_3px_-1.5px_rgba(0,0,0,0.20),0_6px_6px_-3px_rgba(0,0,0,0.16)]`,w={maxWidth:`min(28rem, calc(100cqw - 4ch))`,backgroundColor:`var(--surface-elevated)`,color:`var(--surface-elevated-foreground)`},T=`flex items-center justify-between gap-3 border-b border-[var(--surface-subtle)] px-3 py-2.5`,E=`text-xs font-medium leading-4 text-foreground md:text-[0.8125rem] md:leading-5`,D=`text-xs tabular-nums leading-4 text-muted-foreground md:text-[0.8125rem] md:leading-5`,O=`m-0 list-none divide-y divide-[var(--surface-subtle)] p-0`,k=`${O} max-h-[min(22rem,50vh)] overflow-y-auto`,A=`${O} w-full`,ee={className:`p-0 rounded-none`,maxHeightClass:`max-h-[46vh]`,disableHorizontal:!0},j=`px-2.5 py-1.5 text-xs leading-4 md:py-2 md:text-[0.8125rem] md:leading-5`,M=`flex min-w-0 items-center gap-1 text-xs leading-4 text-muted-foreground md:gap-1.5 md:text-[0.8125rem] md:leading-5`,te=`min-w-0 truncate text-xs leading-4 text-foreground md:text-[0.8125rem] md:leading-5`,ne=`inline-flex shrink-0 items-center gap-1 text-xs leading-4 tabular-nums text-muted-foreground md:text-[0.8125rem] md:leading-5`,re=`h-3 w-3 shrink-0 md:h-3.5 md:w-3.5`,ie={in_progress:`text-foreground`,pending:`text-foreground`,completed:`text-muted-foreground line-through`,cancelled:`text-muted-foreground line-through`},ae={in_progress:`chat.statusRow.todo.status.inProgress`,pending:`chat.statusRow.todo.status.pending`,completed:`chat.statusRow.todo.status.completed`,cancelled:`chat.statusRow.todo.status.cancelled`},N=e=>{let t=(0,x.c)(26),{todo:n,isActive:r,activeRef:i}=e,{t:s}=a(),c=oe.has(n.status)?n.status:`pending`,p=ae[c],m;t[0]!==s||t[1]!==p?(m=s(p),t[0]=s,t[1]=p,t[2]=m):m=t[2];let h=m,g;t[3]===c?g=t[4]:(g=c===`in_progress`?(0,S.jsx)(l,{name:`record-circle`,className:`h-3.5 w-3.5 text-[var(--status-info)]`,"aria-hidden":`true`}):c===`completed`?(0,S.jsx)(l,{name:`checkbox-circle`,className:`h-3.5 w-3.5 text-[var(--status-success)]`,"aria-hidden":`true`}):(0,S.jsx)(l,{name:`time`,className:`h-3.5 w-3.5 text-muted-foreground`,"aria-hidden":`true`}),t[3]=c,t[4]=g);let _=g,v=r?`step`:void 0,y=r&&`bg-[var(--surface-muted)] before:absolute before:inset-y-2 before:left-0 before:w-0.5 before:rounded-full before:bg-[var(--status-info)]`,b;t[5]===y?b=t[6]:(b=o(`group relative grid min-w-0 grid-cols-[1rem_minmax(0,1fr)] items-start gap-2.5`,j,y),t[5]=y,t[6]=b);let C;t[7]!==_||t[8]!==h?(C=(0,S.jsx)(u,{asChild:!0,children:(0,S.jsx)(`span`,{className:`flex size-4 items-center justify-center`,"aria-label":h,children:_})}),t[7]=_,t[8]=h,t[9]=C):C=t[9];let w;t[10]===h?w=t[11]:(w=(0,S.jsx)(d,{side:`left`,sideOffset:6,children:h}),t[10]=h,t[11]=w);let T;t[12]!==C||t[13]!==w?(T=(0,S.jsxs)(f,{children:[C,w]}),t[12]=C,t[13]=w,t[14]=T):T=t[14];let E=ie[c],D;t[15]===E?D=t[16]:(D=o(`block whitespace-normal break-words`,E),t[15]=E,t[16]=D);let O;t[17]!==D||t[18]!==n.content?(O=(0,S.jsx)(`span`,{className:`min-w-0 flex-1`,children:(0,S.jsx)(`span`,{className:D,children:n.content})}),t[17]=D,t[18]=n.content,t[19]=O):O=t[19];let k;return t[20]!==i||t[21]!==O||t[22]!==v||t[23]!==b||t[24]!==T?(k=(0,S.jsxs)(`li`,{ref:i,"aria-current":v,className:b,children:[T,O]}),t[20]=i,t[21]=O,t[22]=v,t[23]=b,t[24]=T,t[25]=k):k=t[25],k},oe=new Set([`in_progress`,`pending`,`completed`,`cancelled`]),P=e=>e.replace(/&/g,`&`).replace(/</g,`<`).replace(/>/g,`>`).replace(/"/g,`"`).replace(/'/g,`'`),F=e=>e?Object.entries(e).map(([e,t])=>t==null?``:`${e.startsWith(`--`)?e:e.replace(/[A-Z]/g,e=>`-${e.toLowerCase()}`)}:${typeof t==`number`?`${t}px`:t};`).join(``):``,I=(e,t,n)=>{let r=e.querySelector(`pre`);r&&(r.style.margin=`0`,r.style.background=`transparent`,r.style.whiteSpace=t?`pre-wrap`:`pre`,t&&(r.style.wordBreak=`break-word`,r.style.overflowWrap=`break-word`));let i=e.querySelector(`code`);if(i){let e=F(n);e&&i.setAttribute(`style`,`${i.getAttribute(`style`)??``}${e}`)}},L=e=>`<pre><code>${P(e)}</code></pre>`,R=e=>{let t=(0,x.c)(21),{code:n,language:r,className:i,style:a,codeStyle:s,wrap:l}=e,u=l!==void 0&&l,{currentTheme:d}=c(),f=b.useRef(null),p;t[0]===d?p=t[1]:(p=y(d),t[0]=d,t[1]=p);let m=p,h,g;t[2]!==n||t[3]!==s||t[4]!==u?(h=()=>{let e=f.current;e&&(e.innerHTML=L(n),I(e,u,s))},g=[n,u,s],t[2]=n,t[3]=s,t[4]=u,t[5]=h,t[6]=g):(h=t[5],g=t[6]),b.useLayoutEffect(h,g);let v,C;t[7]!==n||t[8]!==s||t[9]!==r||t[10]!==u?(v=()=>{let e=f.current;if(!e)return;let t=!0,i=new AbortController;return _(n,(r||`text`).toLowerCase(),{signal:i.signal,priority:`visible`}).then(n=>{!t||!e||!n||(e.innerHTML=n,I(e,u,s))}),()=>{t=!1,i.abort()}},C=[n,r,u,s],t[7]=n,t[8]=s,t[9]=r,t[10]=u,t[11]=v,t[12]=C):(v=t[11],C=t[12]),b.useEffect(v,C);let w;t[13]===i?w=t[14]:(w=o(`typography-code`,i),t[13]=i,t[14]=w);let T=m,E;t[15]!==a||t[16]!==T?(E={...T,...a},t[15]=a,t[16]=T,t[17]=E):E=t[17];let D;return t[18]!==w||t[19]!==E?(D=(0,S.jsx)(`div`,{ref:f,className:w,style:E}),t[18]=w,t[19]=E,t[20]=D):D=t[20],D},z=e=>{let t=e.replace(/^<file>\s*\n?/,``).replace(/\n?<\/file>\s*$/,``);return t=t.replace(/^\s*\d{5}\|\s?/gm,``),t.trim()},B=(e,t=``)=>{if(typeof e==`string`)return e;if(e==null)return t;if(typeof e==`number`||typeof e==`boolean`||typeof e==`bigint`)return String(e);try{return JSON.stringify(e)}catch{return t}},V=e=>e?e.includes(`<diagnostics`)||e.includes(`<file_diagnostics>`)||e.includes(`LSP errors detected`)||e.includes(`This file has errors`):!1,H=e=>e?e.replace(/\n{0,2}LSP errors detected[\s\S]*?<diagnostics[^>]*>[\s\S]*?<\/diagnostics>/g,``).replace(/\n{0,2}This file has errors[\s\S]*?<\/file_diagnostics>/g,``).replace(/<diagnostics[^>]*>[\s\S]*?<\/diagnostics>/g,``).replace(/<file_diagnostics>[\s\S]*?<\/file_diagnostics>/g,``).trim():``,U=(e,t)=>!e||typeof e!=`object`?String(e):g(e,t||``),W=e=>{if(typeof e==`string`){let t=e.trim();return t.length>0?t:void 0}if(e&&typeof e==`object`){let t=e.patch;if(typeof t==`string`){let e=t.trim();return e.length>0?e:void 0}}},G=e=>{if(!e||typeof e!=`object`)return;let t=W(e.patch)??W(e.diff);if(t)return t;let n=Array.isArray(e.files)?e.files:[];for(let e of n){if(!e||typeof e!=`object`)continue;let t=W(e.patch)??W(e.diff);if(t)return t}},K=e=>{if(!e||typeof e!=`string`)return{data:null,isJson:!1};let t=e.trim();if(!t.startsWith(`{`)&&!t.startsWith(`[`)||!t.endsWith(`}`)&&!t.endsWith(`]`)||t.length<2)return{data:null,isJson:!1};try{let e=JSON.parse(t);return typeof e==`object`&&e?{data:e,isJson:!0}:{data:null,isJson:!1}}catch{return{data:null,isJson:!1}}},q=(e,t,n)=>{let r=z(e);if((t===`edit`||t===`multiedit`||t===`write`||t===`apply_patch`)&&V(r)&&(r=H(r)),(t===`edit`||t===`multiedit`||t===`apply_patch`)&&r.trim().length===0){let e=G(n);if(e)return e}return r},se=e=>{let t=e.match(/<type>(file|directory)<\/type>/i)?.[1]?.toLowerCase()??`unknown`,n=(e.match(/<content>([\s\S]*?)<\/content>/i)?.[1]??e).replace(/\r\n/g,`
|
|
2
2
|
`).split(`
|
|
3
3
|
`),r=e=>/\(\s*File has more lines\..*offset.*\)/i.test(e.trim());return{type:t,lines:n.map(e=>{let n=e.trim(),i=n.startsWith(`(`)&&n.endsWith(`)`)||r(n);if(t!==`directory`){let t=e.match(/^(\d+):\s?(.*)$/);if(t){let e=t[2],n=e.trim(),i=n.startsWith(`(`)&&n.endsWith(`)`)||r(n);return{lineNumber:i?null:Number(t[1]),text:e,isInfo:i}}}return{lineNumber:null,text:e,isInfo:i}}).filter((e,t,n)=>{if(e.text.trim().length>0)return!0;let r=n[t-1],i=n[t+1],a=!!(r?.isInfo||i?.isInfo),o=e.lineNumber!==null;return!(a&&o)})}},ce=(e,t)=>{try{let n=e.trim().split(`
|
|
4
4
|
`).filter(Boolean);if(n.length===0)return null;let i=[];return n.forEach(e=>{let t=e.match(/^(\s*)(.+)$/);if(t){let[,e,n]=t,r=Math.floor(e.length/2),a=!n.endsWith(`/`);i.push({name:n.replace(/\/$/,``),depth:r,isFile:a})}}),(0,S.jsx)(`div`,{className:o(`w-full min-w-0 font-mono space-y-0.5`,t?.unstyled?null:`p-3 bg-muted/20 rounded-xl border border-border/30`),style:r.tool.popup,children:i.map((e,t)=>(0,S.jsx)(`div`,{className:`min-w-0`,style:{paddingLeft:`${e.depth*20}px`},children:e.isFile?(0,S.jsx)(`span`,{className:`text-foreground/90 block truncate`,children:e.name}):(0,S.jsxs)(`span`,{className:`font-semibold text-foreground block truncate`,children:[e.name,`/`]})},t))})}catch{return null}},le={backgroundColor:`var(--status-info)`,opacity:.6},ue=(e,t,n)=>{try{let i=e.trim().split(`
|
package/dist/index.html
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
|
9
9
|
<link rel="icon" type="image/png" href="/favicon-32.png" sizes="32x32" />
|
|
10
10
|
<link rel="icon" type="image/png" href="/favicon-16.png" sizes="16x16" />
|
|
11
|
-
<link rel="mask-icon" href="/favicon.svg" color="#
|
|
11
|
+
<link rel="mask-icon" href="/favicon.svg" color="#4c9df3" />
|
|
12
12
|
|
|
13
13
|
<!-- Apple touch icon - PNG format required for iOS PWA support -->
|
|
14
14
|
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon-180x180.png" />
|
|
@@ -238,8 +238,8 @@
|
|
|
238
238
|
display: 'standalone',
|
|
239
239
|
display_override: ['window-controls-overlay'],
|
|
240
240
|
...(manifestOrientation ? { orientation: manifestOrientation } : {}),
|
|
241
|
-
background_color: '#
|
|
242
|
-
theme_color: '#
|
|
241
|
+
background_color: '#1a1a1a',
|
|
242
|
+
theme_color: '#4c9df3',
|
|
243
243
|
icons: [
|
|
244
244
|
{ src: `${baseUrl}/pwa-192.png`, sizes: '192x192', type: 'image/png', purpose: 'any' },
|
|
245
245
|
{ src: `${baseUrl}/pwa-512.png`, sizes: '512x512', type: 'image/png', purpose: 'any' },
|
|
@@ -422,8 +422,8 @@
|
|
|
422
422
|
</script>
|
|
423
423
|
|
|
424
424
|
<!-- Theme color - Safari iOS 26+ prioritizes CSS background-color over this, but keep as fallback -->
|
|
425
|
-
<meta name="theme-color" content="#
|
|
426
|
-
<meta name="theme-color" content="#
|
|
425
|
+
<meta name="theme-color" content="#1a1a1a" />
|
|
426
|
+
<meta name="theme-color" content="#1a1a1a" media="(prefers-color-scheme: dark)" />
|
|
427
427
|
|
|
428
428
|
<!-- iOS Safari / installable PWA styling -->
|
|
429
429
|
<meta name="mobile-web-app-capable" content="yes" />
|
|
@@ -464,8 +464,8 @@
|
|
|
464
464
|
}
|
|
465
465
|
|
|
466
466
|
:root {
|
|
467
|
-
--splash-background-dark: #
|
|
468
|
-
--splash-stroke-dark: #
|
|
467
|
+
--splash-background-dark: #1a1a1a;
|
|
468
|
+
--splash-stroke-dark: #CCCCCCdd;
|
|
469
469
|
--splash-background-light: #FFFCF0;
|
|
470
470
|
--splash-stroke-light: #100F0F;
|
|
471
471
|
|
|
@@ -552,7 +552,7 @@
|
|
|
552
552
|
opacity: 0.72;
|
|
553
553
|
}
|
|
554
554
|
</style>
|
|
555
|
-
<script type="module" crossorigin src="/assets/main-
|
|
555
|
+
<script type="module" crossorigin src="/assets/main-R2wbcT-3.js"></script>
|
|
556
556
|
<link rel="modulepreload" crossorigin href="/assets/rolldown-runtime-hePW80VL.js">
|
|
557
557
|
<link rel="modulepreload" crossorigin href="/assets/runtime-auth-ef9W7A43.js">
|
|
558
558
|
<link rel="modulepreload" crossorigin href="/assets/runtime-switch-CAZiOV2G.js">
|
|
@@ -575,10 +575,10 @@
|
|
|
575
575
|
<link rel="modulepreload" crossorigin href="/assets/platform-7bX6qtBV.js">
|
|
576
576
|
<link rel="modulepreload" crossorigin href="/assets/terminalApi-BHXvha-U.js">
|
|
577
577
|
<link rel="modulepreload" crossorigin href="/assets/runtime-file-download-DAOam_oV.js">
|
|
578
|
-
<link rel="modulepreload" crossorigin href="/assets/types-
|
|
579
|
-
<link rel="modulepreload" crossorigin href="/assets/capgoAdapter-
|
|
580
|
-
<link rel="modulepreload" crossorigin href="/assets/runtimeConfig-
|
|
581
|
-
<link rel="stylesheet" crossorigin href="/assets/src-
|
|
578
|
+
<link rel="modulepreload" crossorigin href="/assets/types-Q_utERET.js">
|
|
579
|
+
<link rel="modulepreload" crossorigin href="/assets/capgoAdapter-DLi2tcmI.js">
|
|
580
|
+
<link rel="modulepreload" crossorigin href="/assets/runtimeConfig-D3moXtPz.js">
|
|
581
|
+
<link rel="stylesheet" crossorigin href="/assets/src-CmermOKP.css">
|
|
582
582
|
</head>
|
|
583
583
|
<body class="h-full bg-background text-foreground">
|
|
584
584
|
<!-- Keep the startup layer outside #root. React replaces every child of
|
package/dist/mini-chat.html
CHANGED
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
briefly show a second, differently-sized one before handing off. The
|
|
53
53
|
backdrop matches bg-background, so fading it out is seamless. */
|
|
54
54
|
:root {
|
|
55
|
-
--splash-background-dark: #
|
|
55
|
+
--splash-background-dark: #1a1a1a;
|
|
56
56
|
--splash-background-light: #FFFCF0;
|
|
57
57
|
--splash-background: var(--splash-background-dark);
|
|
58
58
|
}
|
|
@@ -74,7 +74,7 @@
|
|
|
74
74
|
}
|
|
75
75
|
</style>
|
|
76
76
|
|
|
77
|
-
<script type="module" crossorigin src="/assets/miniChat-
|
|
77
|
+
<script type="module" crossorigin src="/assets/miniChat-ChGISVuo.js"></script>
|
|
78
78
|
<link rel="modulepreload" crossorigin href="/assets/rolldown-runtime-hePW80VL.js">
|
|
79
79
|
<link rel="modulepreload" crossorigin href="/assets/runtime-auth-ef9W7A43.js">
|
|
80
80
|
<link rel="modulepreload" crossorigin href="/assets/runtime-switch-CAZiOV2G.js">
|
|
@@ -97,10 +97,10 @@
|
|
|
97
97
|
<link rel="modulepreload" crossorigin href="/assets/platform-7bX6qtBV.js">
|
|
98
98
|
<link rel="modulepreload" crossorigin href="/assets/terminalApi-BHXvha-U.js">
|
|
99
99
|
<link rel="modulepreload" crossorigin href="/assets/runtime-file-download-DAOam_oV.js">
|
|
100
|
-
<link rel="modulepreload" crossorigin href="/assets/types-
|
|
101
|
-
<link rel="modulepreload" crossorigin href="/assets/capgoAdapter-
|
|
102
|
-
<link rel="modulepreload" crossorigin href="/assets/runtimeConfig-
|
|
103
|
-
<link rel="stylesheet" crossorigin href="/assets/src-
|
|
100
|
+
<link rel="modulepreload" crossorigin href="/assets/types-Q_utERET.js">
|
|
101
|
+
<link rel="modulepreload" crossorigin href="/assets/capgoAdapter-DLi2tcmI.js">
|
|
102
|
+
<link rel="modulepreload" crossorigin href="/assets/runtimeConfig-D3moXtPz.js">
|
|
103
|
+
<link rel="stylesheet" crossorigin href="/assets/src-CmermOKP.css">
|
|
104
104
|
</head>
|
|
105
105
|
<body class="h-full bg-background text-foreground">
|
|
106
106
|
<div id="root" class="h-full">
|
package/dist/mobile.html
CHANGED
|
@@ -41,16 +41,16 @@
|
|
|
41
41
|
// splashBg* are the resolved theme surface.background colors persisted by the app
|
|
42
42
|
// theme system (see ThemeSystemContext). When absent (e.g. a fresh install before
|
|
43
43
|
// the theme has been persisted once), fall back to the default theme's
|
|
44
|
-
// surface.background — carbonfox-light /
|
|
44
|
+
// surface.background — carbonfox-light / cursor-dark (see lib/theme/themes),
|
|
45
45
|
// which is what the app renders on first launch — so the pre-paint matches instead
|
|
46
46
|
// of flashing a different colour.
|
|
47
|
-
var bgDark = localStorage.getItem('splashBgDark') || '#
|
|
47
|
+
var bgDark = localStorage.getItem('splashBgDark') || '#1a1a1a';
|
|
48
48
|
var bgLight = localStorage.getItem('splashBgLight') || '#FFFFFF';
|
|
49
49
|
var bg = isDark ? bgDark : bgLight;
|
|
50
50
|
|
|
51
51
|
// Set the actual --background CSS variable, not just the element background.
|
|
52
52
|
// `<body class="bg-background">` paints `var(--background)`, and design-system.css
|
|
53
|
-
// ships a baked default (.dark { --background:
|
|
53
|
+
// ships a baked default (.dark { --background: #1a1a1a }) that otherwise
|
|
54
54
|
// overpaints our html background the moment the stylesheet loads — before React's
|
|
55
55
|
// theme system injects the real (default/custom) vars with !important. Setting the
|
|
56
56
|
// var inline on the root wins over that non-!important default, so body paints the
|
|
@@ -63,7 +63,7 @@
|
|
|
63
63
|
})();
|
|
64
64
|
</script>
|
|
65
65
|
|
|
66
|
-
<script type="module" crossorigin src="/assets/mobile-
|
|
66
|
+
<script type="module" crossorigin src="/assets/mobile-Dv3wbcd9.js"></script>
|
|
67
67
|
<link rel="modulepreload" crossorigin href="/assets/rolldown-runtime-hePW80VL.js">
|
|
68
68
|
<link rel="modulepreload" crossorigin href="/assets/runtime-auth-ef9W7A43.js">
|
|
69
69
|
<link rel="modulepreload" crossorigin href="/assets/runtime-switch-CAZiOV2G.js">
|
|
@@ -86,10 +86,10 @@
|
|
|
86
86
|
<link rel="modulepreload" crossorigin href="/assets/platform-7bX6qtBV.js">
|
|
87
87
|
<link rel="modulepreload" crossorigin href="/assets/terminalApi-BHXvha-U.js">
|
|
88
88
|
<link rel="modulepreload" crossorigin href="/assets/runtime-file-download-DAOam_oV.js">
|
|
89
|
-
<link rel="modulepreload" crossorigin href="/assets/types-
|
|
90
|
-
<link rel="modulepreload" crossorigin href="/assets/capgoAdapter-
|
|
91
|
-
<link rel="modulepreload" crossorigin href="/assets/runtimeConfig-
|
|
92
|
-
<link rel="stylesheet" crossorigin href="/assets/src-
|
|
89
|
+
<link rel="modulepreload" crossorigin href="/assets/types-Q_utERET.js">
|
|
90
|
+
<link rel="modulepreload" crossorigin href="/assets/capgoAdapter-DLi2tcmI.js">
|
|
91
|
+
<link rel="modulepreload" crossorigin href="/assets/runtimeConfig-D3moXtPz.js">
|
|
92
|
+
<link rel="stylesheet" crossorigin href="/assets/src-CmermOKP.css">
|
|
93
93
|
</head>
|
|
94
94
|
<body class="h-full bg-background text-foreground">
|
|
95
95
|
<div id="root" class="h-full"></div>
|
package/package.json
CHANGED
|
@@ -7,6 +7,19 @@ registered them — so a leaked device token alone can't be used to push.
|
|
|
7
7
|
|
|
8
8
|
## How it works
|
|
9
9
|
|
|
10
|
+
Native registration now includes the installed shell's `bundleId`, obtained via
|
|
11
|
+
Capacitor App metadata, not the connected Host or web bundle version. The Host
|
|
12
|
+
validates and persists this optional field with alert and Live Activity tokens.
|
|
13
|
+
It appends `.${bundleId}` to the existing Relay registration signature when present,
|
|
14
|
+
and checks app identity in registration caches. Re-registration after restart
|
|
15
|
+
or Relay switching preserves it. The Relay owns the allowlist and app-to-APNs-key
|
|
16
|
+
mapping; the mobile client never supplies credentials or an arbitrary APNs topic.
|
|
17
|
+
Existing tokens without identity retain the default-app behavior; missing fields
|
|
18
|
+
must not overwrite an identity already stored. Upgrade Relay before Host/client.
|
|
19
|
+
Direct APNs remains single-app and skips explicit tokens for other apps without
|
|
20
|
+
deleting them. Browser/desktop/VS Code do not acquire a native app identity; Android
|
|
21
|
+
notification-stream behavior is unchanged.
|
|
22
|
+
|
|
10
23
|
1. The app registers its APNs device token with **its own server** (`POST /api/push/apns-token`,
|
|
11
24
|
`useNativePushRegistration`). PWA/desktop never register — only the native Capacitor app.
|
|
12
25
|
2. The server **binds the token on the relay**: it POSTs `{ token, publicKeyJwk, ts, sig }` to
|
|
@@ -5,6 +5,10 @@ This module provides notification message preparation utilities for the web serv
|
|
|
5
5
|
|
|
6
6
|
## Entrypoints and structure
|
|
7
7
|
|
|
8
|
+
Native push registration accepts optional `bundleId` and carries it through Host
|
|
9
|
+
persistence and signed Relay binding for alert and Live Activity delivery. See
|
|
10
|
+
`APNS.md` for the multi-app compatibility contract and deployment ordering.
|
|
11
|
+
|
|
8
12
|
Android notification-only subscriptions send the App locale in `Accept-Language`. `native-stream.js` localizes built-in English scenario titles per response using `apns-titles.js`, for both live delivery and replay, without mutating shared history. Custom titles, contact names and bodies are preserved. Missing/unsupported locales use the existing English fallback. The UI reconnects with its current locale when language changes; direct and encrypted relay requests carry the same header.
|
|
9
13
|
- `native-stream.js`: bounded in-memory Android notification-only SSE/replay runtime, instantiated by the emitter and published by trigger fanout. `GET /api/notifications/native-stream` uses normal API/client authentication. It emits only `{type: 'notification', title, body, kind?, tag?, sessionId?, assistantID?, directory?, requireHidden}` and `{type: 'ready', gap}` with opaque SSE IDs; no session-status/transcript traffic. Initial connections receive only future events, reconnects use `Last-Event-ID`. Retention is 128 events / 15 minutes, no disk writes. Unknown/expired cursors explicitly signal a gap; backpressured clients disconnect and recover using the last processed ID. Heartbeat is 60 seconds and timers are removed on response close/error. Android owns foreground-service lifetime, native visibility and local system notification display; this channel is independent of APNs/FCM and the WebView.
|
|
10
14
|
- `packages/web/server/lib/notifications/index.js`: public entrypoint imported by `packages/web/server/index.js`.
|
|
@@ -28,6 +28,11 @@ const APNS_HOST_SANDBOX = 'https://api.sandbox.push.apple.com';
|
|
|
28
28
|
// APNs rejects auth tokens older than 1h; refresh well inside that window.
|
|
29
29
|
const JWT_TTL_MS = 50 * 60 * 1000;
|
|
30
30
|
const DEFAULT_BUNDLE_ID = 'com.yee94.yuri';
|
|
31
|
+
const parseBundleId = (value) => {
|
|
32
|
+
if (value === undefined) return undefined;
|
|
33
|
+
if (typeof value !== 'string' || value.length > 255 || !/^[A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)+$/.test(value)) throw new Error('Invalid push bundleId');
|
|
34
|
+
return value;
|
|
35
|
+
};
|
|
31
36
|
const MAX_TOKENS_PER_SESSION = 10;
|
|
32
37
|
const MAX_LIVE_ACTIVITY_TOKENS_PER_SESSION = 8;
|
|
33
38
|
const MAX_LIVE_ACTIVITY_TOKEN_CHARS = 512;
|
|
@@ -187,8 +192,8 @@ export const createApnsRuntime = (deps) => {
|
|
|
187
192
|
y: publicJwk.y,
|
|
188
193
|
});
|
|
189
194
|
|
|
190
|
-
const registrationCacheKey = (registerUrl, token, platform) =>
|
|
191
|
-
`${registerUrl}\0${token}\0${platform}`;
|
|
195
|
+
const registrationCacheKey = (registerUrl, token, platform, bundleId) =>
|
|
196
|
+
`${registerUrl}\0${token}\0${platform}\0${bundleId ?? ''}`;
|
|
192
197
|
|
|
193
198
|
const rememberRelayRegisterUrl = (registerUrl) => {
|
|
194
199
|
if (registrationCacheRegisterUrl === registerUrl) return;
|
|
@@ -211,28 +216,28 @@ export const createApnsRuntime = (deps) => {
|
|
|
211
216
|
const boundRegistrationCache = (registerUrl, entries) => {
|
|
212
217
|
rememberRelayRegisterUrl(registerUrl);
|
|
213
218
|
const allowed = new Set(
|
|
214
|
-
entries.map((entry) => registrationCacheKey(registerUrl, entry.deviceToken, entry.platform)),
|
|
219
|
+
entries.map((entry) => registrationCacheKey(registerUrl, entry.deviceToken, entry.platform, entry.bundleId)),
|
|
215
220
|
);
|
|
216
221
|
for (const key of registrationSuccessCache.keys()) {
|
|
217
222
|
if (!allowed.has(key)) registrationSuccessCache.delete(key);
|
|
218
223
|
}
|
|
219
224
|
};
|
|
220
225
|
|
|
221
|
-
const registerTokenWithRelay = async (token, platform = 'ios', relayOverride) => {
|
|
226
|
+
const registerTokenWithRelay = async (token, platform = 'ios', relayOverride, bundleId) => {
|
|
222
227
|
const relay = relayOverride ?? (await resolveRelayConfig());
|
|
223
228
|
if (!relay) return true; // direct mode — no relay binding needed
|
|
224
229
|
rememberRelayRegisterUrl(relay.registerUrl);
|
|
225
|
-
const cacheKey = registrationCacheKey(relay.registerUrl, token, platform);
|
|
230
|
+
const cacheKey = registrationCacheKey(relay.registerUrl, token, platform, bundleId);
|
|
226
231
|
if (registrationSuccessCache.has(cacheKey)) return true;
|
|
227
232
|
try {
|
|
228
233
|
const { privateKey, publicJwk } = await getOrCreateRelayKeypair();
|
|
229
234
|
const ts = Date.now();
|
|
230
235
|
// platform is part of the signed message so it can't be tampered en route.
|
|
231
|
-
const sig = signRelayMessage(privateKey, `${ts}.${token}.${platform}`);
|
|
236
|
+
const sig = signRelayMessage(privateKey, `${ts}.${token}.${platform}${bundleId ? `.${bundleId}` : ''}`);
|
|
232
237
|
const res = await fetch(relay.registerUrl, {
|
|
233
238
|
method: 'POST',
|
|
234
239
|
headers: { 'content-type': 'application/json' },
|
|
235
|
-
body: JSON.stringify({ token, platform, publicKeyJwk: relayPublicJwk(publicJwk), ts, sig }),
|
|
240
|
+
body: JSON.stringify({ token, platform, bundleId, publicKeyJwk: relayPublicJwk(publicJwk), ts, sig }),
|
|
236
241
|
});
|
|
237
242
|
if (!res.ok) {
|
|
238
243
|
console.warn(`[Push relay] register-token failed status=${res.status}`);
|
|
@@ -246,18 +251,18 @@ export const createApnsRuntime = (deps) => {
|
|
|
246
251
|
}
|
|
247
252
|
};
|
|
248
253
|
|
|
249
|
-
const registerLiveActivityTokenWithRelay = async (token, relayOverride) => {
|
|
254
|
+
const registerLiveActivityTokenWithRelay = async (token, relayOverride, bundleId) => {
|
|
250
255
|
const relay = relayOverride ?? (await resolveRelayConfig());
|
|
251
256
|
if (!relay) return true;
|
|
252
257
|
rememberLiveActivityRelayRegisterUrl(relay.liveActivityRegisterUrl);
|
|
253
258
|
const cacheKey = liveActivityRegistrationCacheKey(relay.liveActivityRegisterUrl, token);
|
|
254
|
-
if (liveActivityRegistrationSuccessCache.
|
|
259
|
+
if (liveActivityRegistrationSuccessCache.get(cacheKey) === (bundleId ?? '')) return true;
|
|
255
260
|
try {
|
|
256
261
|
const { privateKey, publicJwk } = await getOrCreateRelayKeypair();
|
|
257
262
|
const ts = Date.now();
|
|
258
263
|
const sig = signRelayMessage(
|
|
259
264
|
privateKey,
|
|
260
|
-
`${ts}.${token}.${LIVE_ACTIVITY_PLATFORM}.${LIVE_ACTIVITY_KIND}`,
|
|
265
|
+
`${ts}.${token}.${LIVE_ACTIVITY_PLATFORM}.${LIVE_ACTIVITY_KIND}${bundleId ? `.${bundleId}` : ''}`,
|
|
261
266
|
);
|
|
262
267
|
const res = await fetch(relay.liveActivityRegisterUrl, {
|
|
263
268
|
method: 'POST',
|
|
@@ -266,6 +271,7 @@ export const createApnsRuntime = (deps) => {
|
|
|
266
271
|
token,
|
|
267
272
|
platform: LIVE_ACTIVITY_PLATFORM,
|
|
268
273
|
kind: LIVE_ACTIVITY_KIND,
|
|
274
|
+
bundleId,
|
|
269
275
|
publicKeyJwk: relayPublicJwk(publicJwk),
|
|
270
276
|
ts,
|
|
271
277
|
sig,
|
|
@@ -275,7 +281,7 @@ export const createApnsRuntime = (deps) => {
|
|
|
275
281
|
console.warn(`[Push relay] register-live-activity-token failed status=${res.status}`);
|
|
276
282
|
return false;
|
|
277
283
|
}
|
|
278
|
-
liveActivityRegistrationSuccessCache.set(cacheKey,
|
|
284
|
+
liveActivityRegistrationSuccessCache.set(cacheKey, bundleId ?? '');
|
|
279
285
|
return true;
|
|
280
286
|
} catch (error) {
|
|
281
287
|
console.warn('[Push relay] register-live-activity-token request failed:', error?.message ?? error);
|
|
@@ -394,6 +400,7 @@ export const createApnsRuntime = (deps) => {
|
|
|
394
400
|
if (!entry || typeof entry !== 'object') return null;
|
|
395
401
|
const deviceToken = entry.deviceToken;
|
|
396
402
|
if (typeof deviceToken !== 'string' || deviceToken.trim().length === 0) return null;
|
|
403
|
+
try { parseBundleId(entry.bundleId); } catch { return null; }
|
|
397
404
|
return {
|
|
398
405
|
deviceToken: deviceToken.trim(),
|
|
399
406
|
createdAt: typeof entry.createdAt === 'number' ? entry.createdAt : null,
|
|
@@ -403,6 +410,7 @@ export const createApnsRuntime = (deps) => {
|
|
|
403
410
|
platform: entry.platform === 'android' ? 'android' : 'ios',
|
|
404
411
|
// App UI locale at last registration; missing → English titles (legacy tokens).
|
|
405
412
|
locale: normalizeApnsLocale(entry.locale),
|
|
413
|
+
bundleId: entry.bundleId,
|
|
406
414
|
};
|
|
407
415
|
})
|
|
408
416
|
.filter(Boolean);
|
|
@@ -419,11 +427,12 @@ export const createApnsRuntime = (deps) => {
|
|
|
419
427
|
if (!isLimitedId(token, MAX_LIVE_ACTIVITY_TOKEN_CHARS)) return null;
|
|
420
428
|
if (!isLimitedId(activityId, MAX_LIVE_ACTIVITY_ID_CHARS)) return null;
|
|
421
429
|
if (!isLimitedId(sessionId, MAX_LIVE_ACTIVITY_ID_CHARS)) return null;
|
|
430
|
+
try { parseBundleId(entry.bundleId); } catch { return null; }
|
|
422
431
|
const createdAt = typeof entry.createdAt === 'number' ? entry.createdAt : null;
|
|
423
432
|
const lastSeenAt = typeof entry.lastSeenAt === 'number' ? entry.lastSeenAt : createdAt;
|
|
424
433
|
if (typeof lastSeenAt === 'number' && now - lastSeenAt > LIVE_ACTIVITY_TOKEN_TTL_MS) return null;
|
|
425
434
|
const snapshot = parseLiveActivitySnapshot(entry.snapshot);
|
|
426
|
-
return { token, activityId, sessionId, snapshot, createdAt, lastSeenAt };
|
|
435
|
+
return { token, activityId, sessionId, snapshot, createdAt, lastSeenAt, bundleId: entry.bundleId };
|
|
427
436
|
})
|
|
428
437
|
.filter(Boolean);
|
|
429
438
|
};
|
|
@@ -473,17 +482,20 @@ export const createApnsRuntime = (deps) => {
|
|
|
473
482
|
return unique;
|
|
474
483
|
};
|
|
475
484
|
|
|
476
|
-
const addOrUpdateApnsToken = async (uiSessionToken, deviceToken, userAgent, platform, locale) => {
|
|
485
|
+
const addOrUpdateApnsToken = async (uiSessionToken, deviceToken, userAgent, platform, locale, bundleId) => {
|
|
486
|
+
parseBundleId(bundleId);
|
|
477
487
|
if (!uiSessionToken || typeof deviceToken !== 'string' || deviceToken.trim().length === 0) return;
|
|
478
488
|
const token = deviceToken.trim();
|
|
479
489
|
const tokenPlatform = normalizePlatform(platform);
|
|
480
490
|
const tokenLocale = normalizeApnsLocale(locale);
|
|
491
|
+
let tokenBundleId = bundleId;
|
|
481
492
|
const now = Date.now();
|
|
482
493
|
|
|
483
494
|
await persistTokenUpdate((current) => {
|
|
484
495
|
const tokensBySession = { ...(current.tokensBySession || {}) };
|
|
485
496
|
const existing = normalizeTokens(tokensBySession[uiSessionToken]);
|
|
486
497
|
const prior = existing.find((entry) => entry.deviceToken === token);
|
|
498
|
+
tokenBundleId ??= prior?.bundleId;
|
|
487
499
|
const filtered = existing.filter((entry) => entry.deviceToken !== token);
|
|
488
500
|
filtered.unshift({
|
|
489
501
|
deviceToken: token,
|
|
@@ -492,6 +504,7 @@ export const createApnsRuntime = (deps) => {
|
|
|
492
504
|
userAgent: typeof userAgent === 'string' && userAgent.length > 0 ? userAgent : undefined,
|
|
493
505
|
platform: tokenPlatform,
|
|
494
506
|
locale: tokenLocale,
|
|
507
|
+
bundleId: tokenBundleId,
|
|
495
508
|
});
|
|
496
509
|
tokensBySession[uiSessionToken] = filtered.slice(0, MAX_TOKENS_PER_SESSION);
|
|
497
510
|
return { version: APNS_TOKENS_VERSION, tokensBySession };
|
|
@@ -502,7 +515,7 @@ export const createApnsRuntime = (deps) => {
|
|
|
502
515
|
// every time (not just for new tokens) keeps existing tokens bound after a relay/server
|
|
503
516
|
// upgrade rather than silently going unbound. Platform is bound too so the relay routes
|
|
504
517
|
// it to APNs vs FCM.
|
|
505
|
-
await registerTokenWithRelay(token, tokenPlatform);
|
|
518
|
+
await registerTokenWithRelay(token, tokenPlatform, undefined, tokenBundleId);
|
|
506
519
|
};
|
|
507
520
|
|
|
508
521
|
const removeApnsToken = async (uiSessionToken, deviceToken) => {
|
|
@@ -531,7 +544,9 @@ export const createApnsRuntime = (deps) => {
|
|
|
531
544
|
});
|
|
532
545
|
};
|
|
533
546
|
|
|
534
|
-
const addOrUpdateLiveActivityToken = async (uiSessionToken, token, activityId, sessionId, snapshot) => {
|
|
547
|
+
const addOrUpdateLiveActivityToken = async (uiSessionToken, token, activityId, sessionId, snapshot, bundleId) => {
|
|
548
|
+
parseBundleId(bundleId);
|
|
549
|
+
let tokenBundleId = bundleId;
|
|
535
550
|
if (!uiSessionToken) return;
|
|
536
551
|
const trimmedToken = typeof token === 'string' ? token.trim() : '';
|
|
537
552
|
const trimmedActivityId = typeof activityId === 'string' ? activityId.trim() : '';
|
|
@@ -549,6 +564,7 @@ export const createApnsRuntime = (deps) => {
|
|
|
549
564
|
);
|
|
550
565
|
const existing = liveActivityTokensBySession[uiSessionToken] || [];
|
|
551
566
|
const replaced = existing.find((entry) => entry.activityId === trimmedActivityId);
|
|
567
|
+
tokenBundleId ??= replaced?.bundleId;
|
|
552
568
|
if (replaced && replaced.token !== trimmedToken) replacedToken = replaced.token;
|
|
553
569
|
const filtered = existing.filter(
|
|
554
570
|
(entry) => entry.activityId !== trimmedActivityId && entry.token !== trimmedToken,
|
|
@@ -559,6 +575,7 @@ export const createApnsRuntime = (deps) => {
|
|
|
559
575
|
activityId: trimmedActivityId,
|
|
560
576
|
sessionId: trimmedSessionId,
|
|
561
577
|
snapshot: nextSnapshot,
|
|
578
|
+
bundleId: tokenBundleId,
|
|
562
579
|
createdAt: typeof replaced?.createdAt === 'number' ? replaced.createdAt : now,
|
|
563
580
|
lastSeenAt: now,
|
|
564
581
|
});
|
|
@@ -566,7 +583,7 @@ export const createApnsRuntime = (deps) => {
|
|
|
566
583
|
return { ...current, liveActivityTokensBySession };
|
|
567
584
|
});
|
|
568
585
|
|
|
569
|
-
await registerLiveActivityTokenWithRelay(trimmedToken);
|
|
586
|
+
await registerLiveActivityTokenWithRelay(trimmedToken, undefined, tokenBundleId);
|
|
570
587
|
if (replacedToken) {
|
|
571
588
|
const store = await readTokensFromDisk();
|
|
572
589
|
if (countLiveActivityTokenCopies(store.liveActivityTokensBySession, replacedToken) === 0) {
|
|
@@ -1122,7 +1139,7 @@ export const createApnsRuntime = (deps) => {
|
|
|
1122
1139
|
|
|
1123
1140
|
// Shared Live Activity delivery path (relay register-then-send, or direct APNs).
|
|
1124
1141
|
// Returns the tokens APNs accepted; callers decide what an accepted `end` means.
|
|
1125
|
-
const deliverLiveActivityPayload = async (tokens, payload) => {
|
|
1142
|
+
const deliverLiveActivityPayload = async (tokens, payload, bundleIds) => {
|
|
1126
1143
|
const accepted = [];
|
|
1127
1144
|
const relay = await resolveRelayConfig();
|
|
1128
1145
|
if (relay) {
|
|
@@ -1131,7 +1148,7 @@ export const createApnsRuntime = (deps) => {
|
|
|
1131
1148
|
tokens,
|
|
1132
1149
|
RELAY_REGISTER_CONCURRENCY,
|
|
1133
1150
|
async (token) => {
|
|
1134
|
-
const registered = await registerLiveActivityTokenWithRelay(token, relay);
|
|
1151
|
+
const registered = await registerLiveActivityTokenWithRelay(token, relay, bundleIds.get(token));
|
|
1135
1152
|
return registered ? token : null;
|
|
1136
1153
|
},
|
|
1137
1154
|
);
|
|
@@ -1140,7 +1157,9 @@ export const createApnsRuntime = (deps) => {
|
|
|
1140
1157
|
accepted.push(...await sendLiveActivityViaRelay(readyTokens, payload, relay));
|
|
1141
1158
|
}
|
|
1142
1159
|
} else {
|
|
1143
|
-
|
|
1160
|
+
const config = await resolveApnsConfig();
|
|
1161
|
+
const compatible = tokens.filter((token) => !bundleIds.get(token) || bundleIds.get(token) === config?.bundleId);
|
|
1162
|
+
accepted.push(...await sendLiveActivityViaDirectApns(compatible, payload));
|
|
1144
1163
|
}
|
|
1145
1164
|
return accepted;
|
|
1146
1165
|
};
|
|
@@ -1224,6 +1243,7 @@ export const createApnsRuntime = (deps) => {
|
|
|
1224
1243
|
}
|
|
1225
1244
|
|
|
1226
1245
|
const updatedAtSeconds = now / 1000;
|
|
1246
|
+
const bundleIds = new Map(entries.map((entry) => [entry.token, entry.bundleId]));
|
|
1227
1247
|
const tokensToEnd = [];
|
|
1228
1248
|
for (const group of groups.values()) {
|
|
1229
1249
|
const working = group.items.filter((item) => liveActivityItemIsWorking(item.status));
|
|
@@ -1244,7 +1264,7 @@ export const createApnsRuntime = (deps) => {
|
|
|
1244
1264
|
};
|
|
1245
1265
|
let accepted = [];
|
|
1246
1266
|
try {
|
|
1247
|
-
accepted = await deliverLiveActivityPayload(group.tokens, payload);
|
|
1267
|
+
accepted = await deliverLiveActivityPayload(group.tokens, payload, bundleIds);
|
|
1248
1268
|
} catch (error) {
|
|
1249
1269
|
console.warn('[Live Activity] refresh failed:', error?.message ?? error);
|
|
1250
1270
|
continue;
|
|
@@ -1351,7 +1371,7 @@ export const createApnsRuntime = (deps) => {
|
|
|
1351
1371
|
const tokens = pendingEntries.map((entry) => entry.token);
|
|
1352
1372
|
let accepted = [];
|
|
1353
1373
|
try {
|
|
1354
|
-
accepted = await deliverLiveActivityPayload(tokens, payload);
|
|
1374
|
+
accepted = await deliverLiveActivityPayload(tokens, payload, new Map(pendingEntries.map((entry) => [entry.token, entry.bundleId])));
|
|
1355
1375
|
} catch (error) {
|
|
1356
1376
|
console.warn('[Live Activity] end failed:', error?.message ?? error);
|
|
1357
1377
|
return;
|
|
@@ -1378,7 +1398,7 @@ export const createApnsRuntime = (deps) => {
|
|
|
1378
1398
|
const outcomes = await mapWithBoundedConcurrency(
|
|
1379
1399
|
entries,
|
|
1380
1400
|
RELAY_REGISTER_CONCURRENCY,
|
|
1381
|
-
(entry) => registerTokenWithRelay(entry.deviceToken, entry.platform, relay),
|
|
1401
|
+
(entry) => registerTokenWithRelay(entry.deviceToken, entry.platform, relay, entry.bundleId),
|
|
1382
1402
|
);
|
|
1383
1403
|
let succeeded = 0;
|
|
1384
1404
|
let failed = 0;
|
|
@@ -1419,7 +1439,7 @@ export const createApnsRuntime = (deps) => {
|
|
|
1419
1439
|
uniqueEntries,
|
|
1420
1440
|
RELAY_REGISTER_CONCURRENCY,
|
|
1421
1441
|
async (entry) => {
|
|
1422
|
-
const registered = await registerTokenWithRelay(entry.deviceToken, entry.platform, relay);
|
|
1442
|
+
const registered = await registerTokenWithRelay(entry.deviceToken, entry.platform, relay, entry.bundleId);
|
|
1423
1443
|
return registered ? entry : null;
|
|
1424
1444
|
},
|
|
1425
1445
|
);
|
|
@@ -1436,7 +1456,8 @@ export const createApnsRuntime = (deps) => {
|
|
|
1436
1456
|
}
|
|
1437
1457
|
return;
|
|
1438
1458
|
}
|
|
1439
|
-
const
|
|
1459
|
+
const config = await resolveApnsConfig();
|
|
1460
|
+
const byLocale = groupTokensByLocale(uniqueEntries.filter((entry) => !entry.bundleId || entry.bundleId === config?.bundleId));
|
|
1440
1461
|
for (const [locale, deviceTokens] of byLocale) {
|
|
1441
1462
|
await sendViaDirectApns(deviceTokens, localizeApnsPayload(payload, locale));
|
|
1442
1463
|
}
|
|
@@ -83,6 +83,47 @@ afterEach(() => {
|
|
|
83
83
|
});
|
|
84
84
|
|
|
85
85
|
describe('apns runtime bundle ID', () => {
|
|
86
|
+
it('persists installed app identities and rebinds both alerts and activities after Host restart', async () => {
|
|
87
|
+
const sent = [];
|
|
88
|
+
const server = createPushRelayServer({
|
|
89
|
+
host: '127.0.0.1', port: 0, databasePath: ':memory:',
|
|
90
|
+
apns: { bundleId: 'com.example.legacy', apps: [{ bundleId: 'com.yee94.yuri' }] },
|
|
91
|
+
apnsProvider: { async send(input) { sent.push(input); return { ok: true }; }, close() {} },
|
|
92
|
+
});
|
|
93
|
+
await server.start();
|
|
94
|
+
process.env.YURI_PUSH_RELAY_URL = `${server.url}/v1/push/send`;
|
|
95
|
+
const deps = makeDeps();
|
|
96
|
+
const token = (label) => crypto.createHash('sha256').update(label).digest('hex');
|
|
97
|
+
try {
|
|
98
|
+
const first = createApnsRuntime(deps);
|
|
99
|
+
await first.addOrUpdateApnsToken('legacy', token('old'));
|
|
100
|
+
await first.addOrUpdateApnsToken('current', token('new'), undefined, 'ios', 'en', 'com.yee94.yuri');
|
|
101
|
+
await first.addOrUpdateLiveActivityToken('current', token('live'), 'activity', 'session', undefined, 'com.yee94.yuri');
|
|
102
|
+
const restored = createApnsRuntime(deps);
|
|
103
|
+
await restored.sendApnsToAllUiSessions({ title: 'Ready', body: 'Done' });
|
|
104
|
+
await restored.sendLiveActivityEnd({ sessionId: 'session', status: 'complete' });
|
|
105
|
+
expect(sent).toHaveLength(3);
|
|
106
|
+
expect(sent.find((input) => input.token === token('old')).bundleId).toBe('com.example.legacy');
|
|
107
|
+
expect(sent.find((input) => input.token === token('new')).bundleId).toBe('com.yee94.yuri');
|
|
108
|
+
expect(sent.find((input) => input.token === token('live'))).toMatchObject({ bundleId: 'com.yee94.yuri', pushType: 'liveactivity' });
|
|
109
|
+
expect(JSON.parse(await deps.fsPromises.readFile()).liveActivityTokensBySession).toEqual({});
|
|
110
|
+
} finally { await server.stop(); }
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('does not send or delete a different app token through a single-app direct credential', async () => {
|
|
114
|
+
process.env.YURI_PUSH_RELAY_DISABLED = 'true';
|
|
115
|
+
const deps = makeDeps({ readSettingsFromDiskMigrated: vi.fn(async () => ({ apnsConfig: APNS_CONFIG })) });
|
|
116
|
+
const runtime = createApnsRuntime(deps);
|
|
117
|
+
await runtime.addOrUpdateApnsToken('s', 'other-alert', undefined, 'ios', 'en', 'com.example.other');
|
|
118
|
+
await runtime.addOrUpdateLiveActivityToken('s', 'other-live', 'a', 'session', undefined, 'com.example.other');
|
|
119
|
+
await runtime.sendApnsToAllUiSessions({ title: 'Ready' });
|
|
120
|
+
await runtime.sendLiveActivityEnd({ sessionId: 'session', status: 'complete' });
|
|
121
|
+
expect(deps.http2.connect).not.toHaveBeenCalled();
|
|
122
|
+
const stored = JSON.parse(await deps.fsPromises.readFile());
|
|
123
|
+
expect(stored.tokensBySession.s).toHaveLength(1);
|
|
124
|
+
expect(stored.liveActivityTokensBySession.s).toHaveLength(1);
|
|
125
|
+
});
|
|
126
|
+
|
|
86
127
|
it('defaults to the current product bundle ID when env and settings omit it', async () => {
|
|
87
128
|
process.env.YURI_APNS_KEY_ID = 'KEY123';
|
|
88
129
|
process.env.YURI_APNS_TEAM_ID = 'TEAM123';
|
|
@@ -859,13 +900,28 @@ describe('apns live activity routes', () => {
|
|
|
859
900
|
body: { token: ' la-token ', activityId: ' act-1 ', sessionId: ' ses_1 ' },
|
|
860
901
|
}, postRes);
|
|
861
902
|
expect(postRes.body).toEqual({ ok: true });
|
|
862
|
-
expect(addOrUpdateLiveActivityToken).toHaveBeenCalledWith('ui-token', 'la-token', 'act-1', 'ses_1', undefined);
|
|
903
|
+
expect(addOrUpdateLiveActivityToken).toHaveBeenCalledWith('ui-token', 'la-token', 'act-1', 'ses_1', undefined, undefined);
|
|
863
904
|
|
|
864
905
|
const delRes = createMockRes();
|
|
865
906
|
await getRoute('DELETE', '/api/push/live-activity-token')({ body: { token: 'la-token' } }, delRes);
|
|
866
907
|
expect(delRes.body).toEqual({ ok: true });
|
|
867
908
|
expect(removeLiveActivityToken).toHaveBeenCalledWith('ui-token', 'la-token');
|
|
868
909
|
});
|
|
910
|
+
|
|
911
|
+
it('passes app identity through both registration routes and rejects malformed identities', async () => {
|
|
912
|
+
const addOrUpdateApnsToken = vi.fn(async () => {});
|
|
913
|
+
const { getRoute, addOrUpdateLiveActivityToken } = register({ addOrUpdateApnsToken });
|
|
914
|
+
for (const endpoint of ['/api/push/apns-token', '/api/push/live-activity-token']) {
|
|
915
|
+
const response = createMockRes();
|
|
916
|
+
await getRoute('POST', endpoint)({ headers: {}, body: { token: 'token', activityId: 'activity', sessionId: 'session', bundleId: 'com.yee94.yuri' } }, response);
|
|
917
|
+
expect(response.body).toEqual({ ok: true });
|
|
918
|
+
const invalid = createMockRes();
|
|
919
|
+
await getRoute('POST', endpoint)({ headers: {}, body: { token: 'token', activityId: 'activity', sessionId: 'session', bundleId: '../../other' } }, invalid);
|
|
920
|
+
expect(invalid.statusCode).toBe(400);
|
|
921
|
+
}
|
|
922
|
+
expect(addOrUpdateApnsToken).toHaveBeenCalledExactlyOnceWith('ui-token', 'token', undefined, 'ios', undefined, 'com.yee94.yuri');
|
|
923
|
+
expect(addOrUpdateLiveActivityToken).toHaveBeenCalledExactlyOnceWith('ui-token', 'token', 'activity', 'session', undefined, 'com.yee94.yuri');
|
|
924
|
+
});
|
|
869
925
|
});
|
|
870
926
|
|
|
871
927
|
describe('apns live activity relay and direct delivery', () => {
|
|
@@ -177,8 +177,10 @@ export const registerNotificationRoutes = (app, dependencies) => {
|
|
|
177
177
|
|
|
178
178
|
const platform = req.body?.platform === 'android' ? 'android' : 'ios';
|
|
179
179
|
const locale = typeof req.body?.locale === 'string' ? req.body.locale.trim() : undefined;
|
|
180
|
+
const bundleId = req.body?.bundleId;
|
|
181
|
+
if (bundleId !== undefined && (typeof bundleId !== 'string' || bundleId.length > 255 || !/^[A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)+$/.test(bundleId))) return res.status(400).json({ error: 'Invalid bundleId' });
|
|
180
182
|
if (typeof addOrUpdateApnsToken === 'function') {
|
|
181
|
-
await addOrUpdateApnsToken(uiToken, deviceToken, req.headers['user-agent'], platform, locale);
|
|
183
|
+
await addOrUpdateApnsToken(uiToken, deviceToken, req.headers['user-agent'], platform, locale, bundleId);
|
|
182
184
|
}
|
|
183
185
|
return res.json({ ok: true });
|
|
184
186
|
});
|
|
@@ -220,7 +222,9 @@ export const registerNotificationRoutes = (app, dependencies) => {
|
|
|
220
222
|
}
|
|
221
223
|
|
|
222
224
|
if (typeof addOrUpdateLiveActivityToken === 'function') {
|
|
223
|
-
|
|
225
|
+
const bundleId = req.body?.bundleId;
|
|
226
|
+
if (bundleId !== undefined && (typeof bundleId !== 'string' || bundleId.length > 255 || !/^[A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)+$/.test(bundleId))) return res.status(400).json({ error: 'Invalid bundleId' });
|
|
227
|
+
await addOrUpdateLiveActivityToken(uiToken, token, activityId, sessionId, req.body?.items, bundleId);
|
|
224
228
|
}
|
|
225
229
|
return res.json({ ok: true });
|
|
226
230
|
});
|
|
@@ -47,6 +47,7 @@ This module provides OpenCode server integration utilities for the web server ru
|
|
|
47
47
|
- `packages/web/server/lib/opencode/shutdown-runtime.js`: graceful shutdown orchestration runtime for watcher/session/terminal/process/server teardown.
|
|
48
48
|
- `packages/web/server/lib/opencode/server-startup-runtime.js`: server listen/startup flow and process/signal handler orchestration runtime.
|
|
49
49
|
- `packages/web/server/lib/opencode/static-routes-runtime.js`: static asset/SPA fallback route registration and manifest route wiring. The SPA fallback sends `index.html` relative to `root`, so deep links keep working when the install lives under a dot directory.
|
|
50
|
+
- The API-only dark fallback and both server/client PWA manifest defaults use the Cursor surface (`#1a1a1a`) and primary (`#4c9df3`).
|
|
50
51
|
- `packages/web/server/lib/opencode/feature-routes-runtime.js`: feature route composition runtime for dynamic import-backed config/skill/provider route registration.
|
|
51
52
|
- `packages/web/server/lib/conversations/`: combined create-session-and-prompt orchestration (Yuri-owned, registered before generic proxy).
|
|
52
53
|
- `packages/web/server/lib/browser-provider/`: host for an installed extension that declares `browser`. Lists reachable providers, sends one agent browser action to the selected provider, and refuses when none is selected. Does not drive the in-app browser pane. See `browser-provider/DOCUMENTATION.md`.
|
|
@@ -61,6 +62,7 @@ This module provides OpenCode server integration utilities for the web server ru
|
|
|
61
62
|
- `packages/web/server/lib/opencode/project-icon-routes.js`: project icon upload/read/discovery route registration and icon storage orchestration.
|
|
62
63
|
- `packages/web/server/lib/opencode/skill-routes.js`: route registration for skill config CRUD, supporting files, and skills catalog scan/install flows.
|
|
63
64
|
- `packages/web/server/lib/opencode/settings-runtime.js`: Settings persistence runtime (disk IO, migrations, normalization, project validation, and persisted update serialization).
|
|
65
|
+
- Missing theme preferences use `carbonfox-light` / `cursor-dark`, matching the shared UI defaults. Existing per-variant IDs and legacy explicitly selected theme IDs remain authoritative; changing the default does not rewrite a user's saved theme.
|
|
64
66
|
- Project registry order and `projectSortOrder` are shared Host authority. After a successful changed write, `onProjectOrderChanged` emits `yuri:project-order-changed` on the existing Yuri SSE channel. Clients GET settings on initial mount, tip, and reconnect. Unchanged and failed writes emit no tip; older settings without a sort mode use `manual`.
|
|
65
67
|
- `packages/web/server/lib/opencode/settings-helpers.js`: Settings payload sanitization/format helpers runtime for response shaping and persisted merge prep.
|
|
66
68
|
- `packages/web/server/lib/opencode/settings-normalization-runtime.js`: path/settings normalization and sanitization helpers runtime used by settings/routes/config wiring.
|