[{"data":1,"prerenderedAt":1207},["ShallowReactive",2],{"work-\u002Fwork\u002Fwebfuse-amazon-connect-cobrowse":3},{"id":4,"title":5,"body":6,"date":1189,"description":1190,"extension":1191,"externalUrl":1192,"featured":1193,"kind":1194,"meta":1195,"navigation":464,"path":1196,"seo":1197,"stem":1198,"tags":1199,"__hash__":1206},"work\u002Fwork\u002Fwebfuse-amazon-connect-cobrowse.md","Co-browse without leaving the contact center: a Webfuse app inside Amazon Connect",{"type":7,"value":8,"toc":1183},"minimark",[9,13,30,35,38,55,58,83,87,94,104,260,283,362,372,693,715,983,1000,1088,1119,1123,1172,1176,1179],[10,11,5],"h1",{"id":12},"co-browse-without-leaving-the-contact-center-a-webfuse-app-inside-amazon-connect",[14,15,16,17,24,25,29],"p",{},"Built on ",[18,19,23],"a",{"href":20,"rel":21},"https:\u002F\u002Fwww.webfuse.com",[22],"nofollow","Webfuse",". It's a small app that lives inside the Amazon Connect agent workspace: the contact-center agent clicks one button and a Webfuse co-browsing session starts, right there in the panel, without leaving Connect. The customer joins by link or PIN. It's a reusable demo scaffold - one of several per-prospect Webfuse demos - built to prove a single claim: \"use Webfuse from inside Amazon Connect.\" Nuxt 4, Vue 3, Tailwind v4, Bun, and the official ",[26,27,28],"code",{},"@amazon-connect\u002Fapp"," SDK.",[31,32,34],"h2",{"id":33},"the-problem","The problem",[14,36,37],{},"Contact-center agents live inside Amazon Connect. The whole day happens there. So the naive ways to add co-browse both fail for the same reason - they pull the agent out of the tool they never leave:",[39,40,41,49],"ul",{},[42,43,44,48],"li",{},[45,46,47],"strong",{},"A separate tool or browser tab."," A context switch mid-call, and the agent loses the customer and contact context Connect already holds.",[42,50,51,54],{},[45,52,53],{},"A custom CCP"," via amazon-connect-streams, rebuilding the agent desktop. Large surface, and AWS explicitly disallows initializing Streams inside a workspace app.",[14,56,57],{},"Underneath those, three root causes shaped the build - and each one is a place where guessing would have produced a plausible-but-wrong integration:",[59,60,61,67,77],"ol",{},[42,62,63,66],{},[45,64,65],{},"The integration surface is an iframe inside the workspace."," It only renders if the app's host explicitly permits Connect to frame it, and only if secrets never touch that browser context.",[42,68,69,72,73,76],{},[45,70,71],{},"Webfuse session creation is not where you'd guess."," The obvious ",[26,74,75],{},"POST \u002Fapi\u002Fspaces\u002F{id}\u002Fsessions\u002F"," is GET-only. Sessions are activated at the space root instead. Assuming the RESTful-looking endpoint would have shipped a wrong integration that reads as right.",[42,78,79,82],{},[45,80,81],{},"The platform moved under my knowledge cutoff."," The service was renamed to \"Amazon Connect Customer\" in 2026 and the third-party-apps feature is region-gated. Setup guidance from memory would have been stale.",[31,84,86],{"id":85},"the-design","The design",[14,88,89,90,93],{},"An Agent Workspace ",[45,91,92],{},"third-party app"," - not a standalone tool, not a custom CCP. A 3p app is just a hosted web page the workspace frames and feeds context to. That framing decided almost everything.",[14,95,96,99,100,103],{},[45,97,98],{},"The Nuxt server is the backend."," Because a 3p app is a hosted web app, the Nuxt\u002FNitro server routes are a real backend - they hold the Webfuse keys and mint sessions server-side. I rejected a separate Lambda + API Gateway + Secrets Manager stack as overkill for a POC, and documented it as the production hardening path instead (Nitro deploys to Lambda natively, so it's a config change, not a rewrite). Secrets stay server-only via ",[26,101,102],{},"runtimeConfig","; only the app name is exposed to the browser.",[105,106,111],"pre",{"className":107,"code":108,"language":109,"meta":110,"style":110},"language-ts shiki shiki-themes material-theme-lighter github-light github-dark","runtimeConfig: {\n  \u002F\u002F Server-only (never shipped to the browser). Override via NUXT_WEBFUSE_* env.\n  webfuse: {\n    apiBase: 'https:\u002F\u002Fwebfuse.com\u002Fapi',\n    spaceRestKey: '',   \u002F\u002F ${env.NUXT_WEBFUSE_SPACE_REST_KEY} - scoped to one Space\n    magicLinkKey: '',   \u002F\u002F ${env.NUXT_WEBFUSE_MAGIC_LINK_KEY} - signs the agent's join\n    mock: 'true',       \u002F\u002F mint fake sessions so the flow works without creds\n  },\n  public: { appName: 'Webfuse Co-browse' },   \u002F\u002F only this reaches the browser\n},\n","ts","",[26,112,113,128,135,145,167,184,199,219,225,254],{"__ignoreMap":110},[114,115,118,121,125],"span",{"class":116,"line":117},"line",1,[114,119,102],{"class":120},"sbgvK",[114,122,124],{"class":123},"sP7_E",":",[114,126,127],{"class":123}," {\n",[114,129,131],{"class":116,"line":130},2,[114,132,134],{"class":133},"sutJx","  \u002F\u002F Server-only (never shipped to the browser). Override via NUXT_WEBFUSE_* env.\n",[114,136,138,141,143],{"class":116,"line":137},3,[114,139,140],{"class":120},"  webfuse",[114,142,124],{"class":123},[114,144,127],{"class":123},[114,146,148,151,153,157,161,164],{"class":116,"line":147},4,[114,149,150],{"class":120},"    apiBase",[114,152,124],{"class":123},[114,154,156],{"class":155},"sjJ54"," '",[114,158,160],{"class":159},"s_sjI","https:\u002F\u002Fwebfuse.com\u002Fapi",[114,162,163],{"class":155},"'",[114,165,166],{"class":123},",\n",[114,168,170,173,175,178,181],{"class":116,"line":169},5,[114,171,172],{"class":120},"    spaceRestKey",[114,174,124],{"class":123},[114,176,177],{"class":155}," ''",[114,179,180],{"class":123},",",[114,182,183],{"class":133},"   \u002F\u002F ${env.NUXT_WEBFUSE_SPACE_REST_KEY} - scoped to one Space\n",[114,185,187,190,192,194,196],{"class":116,"line":186},6,[114,188,189],{"class":120},"    magicLinkKey",[114,191,124],{"class":123},[114,193,177],{"class":155},[114,195,180],{"class":123},[114,197,198],{"class":133},"   \u002F\u002F ${env.NUXT_WEBFUSE_MAGIC_LINK_KEY} - signs the agent's join\n",[114,200,202,205,207,209,212,214,216],{"class":116,"line":201},7,[114,203,204],{"class":120},"    mock",[114,206,124],{"class":123},[114,208,156],{"class":155},[114,210,211],{"class":159},"true",[114,213,163],{"class":155},[114,215,180],{"class":123},[114,217,218],{"class":133},"       \u002F\u002F mint fake sessions so the flow works without creds\n",[114,220,222],{"class":116,"line":221},8,[114,223,224],{"class":123},"  },\n",[114,226,228,231,233,236,239,241,243,246,248,251],{"class":116,"line":227},9,[114,229,230],{"class":120},"  public",[114,232,124],{"class":123},[114,234,235],{"class":123}," {",[114,237,238],{"class":120}," appName",[114,240,124],{"class":123},[114,242,156],{"class":155},[114,244,245],{"class":159},"Webfuse Co-browse",[114,247,163],{"class":155},[114,249,250],{"class":123}," },",[114,252,253],{"class":133},"   \u002F\u002F only this reaches the browser\n",[114,255,257],{"class":116,"line":256},10,[114,258,259],{"class":123},"},\n",[14,261,262,265,266,269,270,274,275,278,279,282],{},[45,263,264],{},"Make the app framable by Connect - the whole thing hinges on this."," A browser only lets Connect frame the app if the app's own responses permit those parents via ",[26,267,268],{},"frame-ancestors",". Without it the panel renders blank in the workspace. Note the asymmetry: this restricts who is allowed to frame ",[271,272,273],"em",{},"us","; it does not restrict what ",[271,276,277],{},"we"," frame. And I deliberately send no ",[26,280,281],{},"X-Frame-Options",", because it would override the CSP and block framing outright.",[105,284,286],{"className":107,"code":285,"language":109,"meta":110,"style":110},"routeRules: {\n  '\u002F**': {\n    headers: {\n      'Content-Security-Policy':\n        'frame-ancestors https:\u002F\u002F*.my.connect.aws https:\u002F\u002F*.awsapps.com;',\n    },\n  },\n},\n",[26,287,288,297,314,323,337,349,354,358],{"__ignoreMap":110},[114,289,290,293,295],{"class":116,"line":117},[114,291,292],{"class":120},"routeRules",[114,294,124],{"class":123},[114,296,127],{"class":123},[114,298,299,302,305,307,311],{"class":116,"line":130},[114,300,301],{"class":155},"  '",[114,303,304],{"class":159},"\u002F**",[114,306,163],{"class":155},[114,308,310],{"class":309},"skxfh",": ",[114,312,313],{"class":123},"{\n",[114,315,316,319,321],{"class":116,"line":137},[114,317,318],{"class":309},"    headers",[114,320,124],{"class":123},[114,322,127],{"class":123},[114,324,325,328,332,334],{"class":116,"line":147},[114,326,327],{"class":155},"      '",[114,329,331],{"class":330},"sZUrc","Content-Security-Policy",[114,333,163],{"class":155},[114,335,336],{"class":123},":\n",[114,338,339,342,345,347],{"class":116,"line":169},[114,340,341],{"class":155},"        '",[114,343,344],{"class":159},"frame-ancestors https:\u002F\u002F*.my.connect.aws https:\u002F\u002F*.awsapps.com;",[114,346,163],{"class":155},[114,348,166],{"class":123},[114,350,351],{"class":116,"line":186},[114,352,353],{"class":123},"    },\n",[114,355,356],{"class":116,"line":201},[114,357,224],{"class":123},[114,359,360],{"class":116,"line":221},[114,361,259],{"class":123},[14,363,364,367,368,371],{},[45,365,366],{},"Degrade gracefully when the host isn't there."," The SDK only completes its handshake when the app is actually embedded in the workspace. In a plain browser tab - local dev, no live Connect instance - ",[26,369,370],{},"onCreate"," never fires. So a 4-second timer flips the app to a \"standalone\" mode that still runs and demos. This is what made local iteration possible without standing up a workspace for every change.",[105,373,375],{"className":107,"code":374,"language":109,"meta":110,"style":110},"const standaloneTimer = setTimeout(() => {\n  if (!settled) state.status = 'standalone'   \u002F\u002F plain tab - still works\n}, STANDALONE_AFTER_MS)                        \u002F\u002F 4000ms\n\nconst { provider } = AmazonConnectApp.init({\n  onCreate: async (event: any) => {\n    settled = true\n    clearTimeout(standaloneTimer)\n    state.status = 'connected'\n    const scope = event?.context?.scope        \u002F\u002F Per-Contact scope carries the id\n    if (scope && 'contactId' in scope) state.contactId = scope.contactId ?? null\n    \u002F* ... wire ContactClient.onConnected \u002F AgentClient.onStateChanged ... *\u002F\n  },\n  onDestroy: async () => { state.status = 'standalone' },\n})\n",[26,376,377,407,446,460,466,492,521,532,545,564,591,639,645,650,685],{"__ignoreMap":110},[114,378,379,383,387,391,395,399,402,405],{"class":116,"line":117},[114,380,382],{"class":381},"sbsja","const",[114,384,386],{"class":385},"s_hVV"," standaloneTimer",[114,388,390],{"class":389},"smGrS"," =",[114,392,394],{"class":393},"sGLFI"," setTimeout",[114,396,398],{"class":397},"su5hD","(",[114,400,401],{"class":123},"()",[114,403,404],{"class":381}," =>",[114,406,127],{"class":123},[114,408,409,413,416,419,422,425,428,431,434,436,438,441,443],{"class":116,"line":130},[114,410,412],{"class":411},"sVHd0","  if",[114,414,415],{"class":309}," (",[114,417,418],{"class":389},"!",[114,420,421],{"class":397},"settled",[114,423,424],{"class":309},") ",[114,426,427],{"class":397},"state",[114,429,430],{"class":123},".",[114,432,433],{"class":397},"status",[114,435,390],{"class":389},[114,437,156],{"class":155},[114,439,440],{"class":159},"standalone",[114,442,163],{"class":155},[114,444,445],{"class":133},"   \u002F\u002F plain tab - still works\n",[114,447,448,451,454,457],{"class":116,"line":137},[114,449,450],{"class":123},"},",[114,452,453],{"class":385}," STANDALONE_AFTER_MS",[114,455,456],{"class":397},")                        ",[114,458,459],{"class":133},"\u002F\u002F 4000ms\n",[114,461,462],{"class":116,"line":147},[114,463,465],{"emptyLinePlaceholder":464},true,"\n",[114,467,468,470,472,475,478,480,483,485,488,490],{"class":116,"line":169},[114,469,382],{"class":381},[114,471,235],{"class":123},[114,473,474],{"class":385}," provider",[114,476,477],{"class":123}," }",[114,479,390],{"class":389},[114,481,482],{"class":397}," AmazonConnectApp",[114,484,430],{"class":123},[114,486,487],{"class":393},"init",[114,489,398],{"class":397},[114,491,313],{"class":123},[114,493,494,497,499,502,504,508,510,514,517,519],{"class":116,"line":186},[114,495,496],{"class":393},"  onCreate",[114,498,124],{"class":123},[114,500,501],{"class":381}," async",[114,503,415],{"class":123},[114,505,507],{"class":506},"s99_P","event",[114,509,124],{"class":389},[114,511,513],{"class":512},"sZMiF"," any",[114,515,516],{"class":123},")",[114,518,404],{"class":381},[114,520,127],{"class":123},[114,522,523,526,528],{"class":116,"line":201},[114,524,525],{"class":397},"    settled",[114,527,390],{"class":389},[114,529,531],{"class":530},"syTEX"," true\n",[114,533,534,537,539,542],{"class":116,"line":221},[114,535,536],{"class":393},"    clearTimeout",[114,538,398],{"class":309},[114,540,541],{"class":397},"standaloneTimer",[114,543,544],{"class":309},")\n",[114,546,547,550,552,554,556,558,561],{"class":116,"line":227},[114,548,549],{"class":397},"    state",[114,551,430],{"class":123},[114,553,433],{"class":397},[114,555,390],{"class":389},[114,557,156],{"class":155},[114,559,560],{"class":159},"connected",[114,562,563],{"class":155},"'\n",[114,565,566,569,572,574,577,580,583,585,588],{"class":116,"line":256},[114,567,568],{"class":381},"    const",[114,570,571],{"class":385}," scope",[114,573,390],{"class":389},[114,575,576],{"class":397}," event",[114,578,579],{"class":123},"?.",[114,581,582],{"class":397},"context",[114,584,579],{"class":123},[114,586,587],{"class":397},"scope",[114,589,590],{"class":133},"        \u002F\u002F Per-Contact scope carries the id\n",[114,592,594,597,599,601,604,606,609,611,614,616,618,620,622,624,626,628,630,632,635],{"class":116,"line":593},11,[114,595,596],{"class":411},"    if",[114,598,415],{"class":309},[114,600,587],{"class":397},[114,602,603],{"class":389}," &&",[114,605,156],{"class":155},[114,607,608],{"class":159},"contactId",[114,610,163],{"class":155},[114,612,613],{"class":389}," in",[114,615,571],{"class":397},[114,617,424],{"class":309},[114,619,427],{"class":397},[114,621,430],{"class":123},[114,623,608],{"class":397},[114,625,390],{"class":389},[114,627,571],{"class":397},[114,629,430],{"class":123},[114,631,608],{"class":397},[114,633,634],{"class":389}," ??",[114,636,638],{"class":637},"s39Yj"," null\n",[114,640,642],{"class":116,"line":641},12,[114,643,644],{"class":133},"    \u002F* ... wire ContactClient.onConnected \u002F AgentClient.onStateChanged ... *\u002F\n",[114,646,648],{"class":116,"line":647},13,[114,649,224],{"class":123},[114,651,653,656,658,660,663,665,667,670,672,674,676,678,680,682],{"class":116,"line":652},14,[114,654,655],{"class":393},"  onDestroy",[114,657,124],{"class":123},[114,659,501],{"class":381},[114,661,662],{"class":123}," ()",[114,664,404],{"class":381},[114,666,235],{"class":123},[114,668,669],{"class":397}," state",[114,671,430],{"class":123},[114,673,433],{"class":397},[114,675,390],{"class":389},[114,677,156],{"class":155},[114,679,440],{"class":159},[114,681,163],{"class":155},[114,683,684],{"class":123}," },\n",[114,686,688,691],{"class":116,"line":687},15,[114,689,690],{"class":123},"}",[114,692,544],{"class":397},[14,694,695,698,699,702,703,706,707,710,711,714],{},[45,696,697],{},"Verify the real API surface instead of guessing it."," This is where the \"not where you'd guess\" root cause got paid off. Webfuse doesn't create a session under ",[26,700,701],{},"\u002Fapi\u002Fspaces\u002F{id}\u002Fsessions\u002F","; it activates one by POSTing to the space ",[271,704,705],{},"root",". Auth is ",[26,708,709],{},"Token",", not ",[26,712,713],{},"Bearer",". I confirmed both against the OpenAPI schema before writing a line.",[105,716,718],{"className":107,"code":717,"language":109,"meta":110,"style":110},"const space = await getSpace()\nconst spaceRoot = space.link.replace(\u002F\\\u002F+$\u002F, '')\n\u002F\u002F \"Creates a new session or retrieves the existing active one.\" (OpenAPI schema)\nconst activation = await $fetch(`${spaceRoot}\u002F`, {\n  method: 'POST',\n  headers: { Authorization: `Token ${apiKey}` },   \u002F\u002F Token, not Bearer\n  body: { metadata: { source: 'amazon-connect', \u002F* contactId, customerName *\u002F } },\n})\n\u002F\u002F Agent joins as host via a signed magic link targeting this session.\nconst agentLink = magicLinkKey\n  ? `${spaceRoot}\u002F?magic_link=${signMagicLink()}&session_id=${activation.session_id}`\n  : activation.link\n",[26,719,720,738,782,787,820,836,870,907,913,918,930,971],{"__ignoreMap":110},[114,721,722,724,727,729,732,735],{"class":116,"line":117},[114,723,382],{"class":381},[114,725,726],{"class":385}," space",[114,728,390],{"class":389},[114,730,731],{"class":411}," await",[114,733,734],{"class":393}," getSpace",[114,736,737],{"class":397},"()\n",[114,739,740,742,745,747,749,751,754,756,759,761,764,768,771,774,776,778,780],{"class":116,"line":130},[114,741,382],{"class":381},[114,743,744],{"class":385}," spaceRoot",[114,746,390],{"class":389},[114,748,726],{"class":397},[114,750,430],{"class":123},[114,752,753],{"class":397},"link",[114,755,430],{"class":123},[114,757,758],{"class":393},"replace",[114,760,398],{"class":397},[114,762,763],{"class":155},"\u002F",[114,765,767],{"class":766},"sjYin","\\\u002F",[114,769,770],{"class":389},"+",[114,772,773],{"class":411},"$",[114,775,763],{"class":155},[114,777,180],{"class":123},[114,779,177],{"class":155},[114,781,544],{"class":397},[114,783,784],{"class":116,"line":137},[114,785,786],{"class":133},"\u002F\u002F \"Creates a new session or retrieves the existing active one.\" (OpenAPI schema)\n",[114,788,789,791,794,796,798,801,803,806,809,811,813,816,818],{"class":116,"line":147},[114,790,382],{"class":381},[114,792,793],{"class":385}," activation",[114,795,390],{"class":389},[114,797,731],{"class":411},[114,799,800],{"class":393}," $fetch",[114,802,398],{"class":397},[114,804,805],{"class":155},"`${",[114,807,808],{"class":397},"spaceRoot",[114,810,690],{"class":155},[114,812,763],{"class":159},[114,814,815],{"class":155},"`",[114,817,180],{"class":123},[114,819,127],{"class":123},[114,821,822,825,827,829,832,834],{"class":116,"line":169},[114,823,824],{"class":309},"  method",[114,826,124],{"class":123},[114,828,156],{"class":155},[114,830,831],{"class":159},"POST",[114,833,163],{"class":155},[114,835,166],{"class":123},[114,837,838,841,843,845,848,850,853,856,859,862,865,867],{"class":116,"line":186},[114,839,840],{"class":309},"  headers",[114,842,124],{"class":123},[114,844,235],{"class":123},[114,846,847],{"class":309}," Authorization",[114,849,124],{"class":123},[114,851,852],{"class":155}," `",[114,854,855],{"class":159},"Token ",[114,857,858],{"class":155},"${",[114,860,861],{"class":397},"apiKey",[114,863,864],{"class":155},"}`",[114,866,250],{"class":123},[114,868,869],{"class":133},"   \u002F\u002F Token, not Bearer\n",[114,871,872,875,877,879,882,884,886,889,891,893,896,898,900,903,905],{"class":116,"line":201},[114,873,874],{"class":309},"  body",[114,876,124],{"class":123},[114,878,235],{"class":123},[114,880,881],{"class":309}," metadata",[114,883,124],{"class":123},[114,885,235],{"class":123},[114,887,888],{"class":309}," source",[114,890,124],{"class":123},[114,892,156],{"class":155},[114,894,895],{"class":159},"amazon-connect",[114,897,163],{"class":155},[114,899,180],{"class":123},[114,901,902],{"class":133}," \u002F* contactId, customerName *\u002F",[114,904,477],{"class":123},[114,906,684],{"class":123},[114,908,909,911],{"class":116,"line":221},[114,910,690],{"class":123},[114,912,544],{"class":397},[114,914,915],{"class":116,"line":227},[114,916,917],{"class":133},"\u002F\u002F Agent joins as host via a signed magic link targeting this session.\n",[114,919,920,922,925,927],{"class":116,"line":256},[114,921,382],{"class":381},[114,923,924],{"class":385}," agentLink",[114,926,390],{"class":389},[114,928,929],{"class":397}," magicLinkKey\n",[114,931,932,935,938,940,942,945,947,950,953,955,958,960,963,965,968],{"class":116,"line":593},[114,933,934],{"class":389},"  ?",[114,936,937],{"class":155}," `${",[114,939,808],{"class":397},[114,941,690],{"class":155},[114,943,944],{"class":159},"\u002F?magic_link=",[114,946,858],{"class":155},[114,948,949],{"class":393},"signMagicLink",[114,951,401],{"class":952},"sfo-9",[114,954,690],{"class":155},[114,956,957],{"class":159},"&session_id=",[114,959,858],{"class":155},[114,961,962],{"class":397},"activation",[114,964,430],{"class":155},[114,966,967],{"class":397},"session_id",[114,969,970],{"class":155},"}`\n",[114,972,973,976,978,980],{"class":116,"line":641},[114,974,975],{"class":389},"  :",[114,977,793],{"class":397},[114,979,430],{"class":123},[114,981,982],{"class":397},"link\n",[14,984,985,988,989,992,993,995,996,999],{},[45,986,987],{},"The co-browse view embeds inline - a nested iframe, not a new tab."," The host view renders in an iframe ",[271,990,991],{},"inside"," the panel, so the agent never leaves Connect: workspace > our app > Webfuse. It works because each layer controls the right contract - our app's ",[26,994,268],{}," says who can frame us, and Webfuse's Space ",[26,997,998],{},"frame_ancestors"," allows our origin to frame it. I kept an \"open in new tab\" escape hatch and a \"couldn't embed\" fallback behind the same 4-second pattern, because a nested embed is exactly the kind of thing a strict host policy can veto.",[105,1001,1005],{"className":1002,"code":1003,"language":1004,"meta":110,"style":110},"language-html shiki shiki-themes material-theme-lighter github-light github-dark","\u003Ciframe\n  :src=\"session.agentLink\"\n  class=\"h-[70vh] w-full rounded-lg border border-line bg-white\"\n  allow=\"camera; microphone; display-capture; clipboard-read; clipboard-write; fullscreen\"\n  @load=\"onLoad\" \u002F>\n\u003C!-- 4s timer -> \"couldn't embed\" fallback with an Open-in-new-tab button -->\n","html",[26,1006,1007,1016,1034,1048,1062,1083],{"__ignoreMap":110},[114,1008,1009,1012],{"class":116,"line":117},[114,1010,1011],{"class":123},"\u003C",[114,1013,1015],{"class":1014},"sQzsp","iframe\n",[114,1017,1018,1022,1025,1028,1031],{"class":116,"line":130},[114,1019,1021],{"class":1020},"s9AJx","  :src",[114,1023,1024],{"class":123},"=",[114,1026,1027],{"class":155},"\"",[114,1029,1030],{"class":159},"session.agentLink",[114,1032,1033],{"class":155},"\"\n",[114,1035,1036,1039,1041,1043,1046],{"class":116,"line":137},[114,1037,1038],{"class":1020},"  class",[114,1040,1024],{"class":123},[114,1042,1027],{"class":155},[114,1044,1045],{"class":159},"h-[70vh] w-full rounded-lg border border-line bg-white",[114,1047,1033],{"class":155},[114,1049,1050,1053,1055,1057,1060],{"class":116,"line":147},[114,1051,1052],{"class":1020},"  allow",[114,1054,1024],{"class":123},[114,1056,1027],{"class":155},[114,1058,1059],{"class":159},"camera; microphone; display-capture; clipboard-read; clipboard-write; fullscreen",[114,1061,1033],{"class":155},[114,1063,1064,1067,1069,1071,1074,1076,1080],{"class":116,"line":169},[114,1065,1066],{"class":1020},"  @load",[114,1068,1024],{"class":123},[114,1070,1027],{"class":155},[114,1072,1073],{"class":159},"onLoad",[114,1075,1027],{"class":155},[114,1077,1079],{"class":1078},"svtyB"," \u002F",[114,1081,1082],{"class":123},">\n",[114,1084,1085],{"class":116,"line":186},[114,1086,1087],{"class":133},"\u003C!-- 4s timer -> \"couldn't embed\" fallback with an Open-in-new-tab button -->\n",[14,1089,1090,1091,415,1094,1097,1098,310,1101,1104,1105,1104,1108,1111,1112,1115,1116,1118],{},"Two smaller calls worth recording. ",[45,1092,1093],{},"Mock mode is the default",[26,1095,1096],{},"NUXT_WEBFUSE_MOCK=true"," returns fake sessions) so the whole flow demos with zero credentials. And Connect permissions are ",[45,1099,1100],{},"least-privilege",[26,1102,1103],{},"Contact.Details.View",", ",[26,1106,1107],{},"Contact.Attributes.View",[26,1109,1110],{},"User.Status.View",", with contact scope set to ",[271,1113,1114],{},"Per contact"," so the app receives the live ",[26,1117,608],{}," and nothing more.",[31,1120,1122],{"id":1121},"results","Results",[39,1124,1125,1131,1137,1151,1166],{},[42,1126,1127,1130],{},[45,1128,1129],{},"Validated end-to-end in a real Connect Customer instance"," (EU Frankfurt \u002F eu-central-1): app registered, appeared in the Agent Workspace launcher, handshook to a \"Connected\" badge, and started a real Webfuse session inline.",[42,1132,1133,1136],{},[45,1134,1135],{},"~$0 running cost"," for the POC - usage-based, no idle-instance fee, 500 free chat messages a month. The cost risks (a claimed phone number, a customer-managed KMS key) were deliberately avoided.",[42,1138,1139,1142,1143,1146,1147,1150],{},[45,1140,1141],{},"~685 lines of app\u002Fserver code across 9 files",", with the integration logic concentrated in two: ",[26,1144,1145],{},"server\u002Futils\u002Fwebfuse.ts"," and ",[26,1148,1149],{},"app\u002Fcomponents\u002FCoBrowsePanel.vue",". Plus ~270 lines of setup docs.",[42,1152,1153,1154,1157,1158,1161,1162,1165],{},"The 3p-apps feature is live in ",[45,1155,1156],{},"10 AWS regions"," (confirmed for EU Frankfurt and London). ",[26,1159,1160],{},"bun run typecheck"," clean on pinned versions - Nuxt 4.4.8, Tailwind 4.3.2, ",[26,1163,1164],{},"@amazon-connect\u002F*"," 1.0.12, Bun 1.3.14.",[42,1167,1168,1171],{},[45,1169,1170],{},"Zero to a working in-workspace integration in about 30-45 minutes of build",", ~1.5-2 hours end-to-end including the console setup.",[31,1173,1175],{"id":1174},"the-principle","The principle",[14,1177,1178],{},"An embedded platform integration is just a hosted web app plus the host's context SDK. The work is not clever code - it's honoring three contracts: keep secrets on your own server, satisfy the host's framing and CSP rules, and degrade gracefully when the host isn't there. Then verify the platform's real API surface instead of guessing it, because the obvious endpoint is often the wrong one.",[1180,1181,1182],"style",{},"html pre.shiki code .sbgvK, html code.shiki .sbgvK{--shiki-light:#E2931D;--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sP7_E, html code.shiki .sP7_E{--shiki-light:#39ADB5;--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sutJx, html code.shiki .sutJx{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#6A737D;--shiki-default-font-style:inherit;--shiki-dark:#6A737D;--shiki-dark-font-style:inherit}html pre.shiki code .sjJ54, html code.shiki .sjJ54{--shiki-light:#39ADB5;--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .s_sjI, html code.shiki .s_sjI{--shiki-light:#91B859;--shiki-default:#032F62;--shiki-dark:#9ECBFF}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .skxfh, html code.shiki .skxfh{--shiki-light:#E53935;--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sZUrc, html code.shiki .sZUrc{--shiki-light:#E53935;--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sbsja, html code.shiki .sbsja{--shiki-light:#9C3EDA;--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .s_hVV, html code.shiki .s_hVV{--shiki-light:#90A4AE;--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .smGrS, html code.shiki .smGrS{--shiki-light:#39ADB5;--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sGLFI, html code.shiki .sGLFI{--shiki-light:#6182B8;--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .su5hD, html code.shiki .su5hD{--shiki-light:#90A4AE;--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sVHd0, html code.shiki .sVHd0{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#D73A49;--shiki-default-font-style:inherit;--shiki-dark:#F97583;--shiki-dark-font-style:inherit}html pre.shiki code .s99_P, html code.shiki .s99_P{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#E36209;--shiki-default-font-style:inherit;--shiki-dark:#FFAB70;--shiki-dark-font-style:inherit}html pre.shiki code .sZMiF, html code.shiki .sZMiF{--shiki-light:#E2931D;--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .syTEX, html code.shiki .syTEX{--shiki-light:#FF5370;--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .s39Yj, html code.shiki .s39Yj{--shiki-light:#39ADB5;--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sjYin, html code.shiki .sjYin{--shiki-light:#90A4AE;--shiki-light-font-weight:inherit;--shiki-default:#22863A;--shiki-default-font-weight:bold;--shiki-dark:#85E89D;--shiki-dark-font-weight:bold}html pre.shiki code .sfo-9, html code.shiki .sfo-9{--shiki-light:#90A4AE;--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sQzsp, html code.shiki .sQzsp{--shiki-light:#E53935;--shiki-default:#22863A;--shiki-dark:#85E89D}html pre.shiki code .s9AJx, html code.shiki .s9AJx{--shiki-light:#9C3EDA;--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .svtyB, html code.shiki .svtyB{--shiki-light:#39ADB5;--shiki-light-font-style:inherit;--shiki-default:#B31D28;--shiki-default-font-style:italic;--shiki-dark:#FDAEB7;--shiki-dark-font-style:italic}",{"title":110,"searchDepth":130,"depth":130,"links":1184},[1185,1186,1187,1188],{"id":33,"depth":130,"text":34},{"id":85,"depth":130,"text":86},{"id":1121,"depth":130,"text":1122},{"id":1174,"depth":130,"text":1175},"2026-07-14","A third-party app embedded in the Amazon Connect agent workspace that starts a Webfuse co-browsing session in one click - validated end-to-end in a live Connect instance, at roughly zero running cost.","md",null,false,"case-study",{},"\u002Fwork\u002Fwebfuse-amazon-connect-cobrowse",{"title":5,"description":1190},"work\u002Fwebfuse-amazon-connect-cobrowse",[1200,23,1201,1202,1203,1204,1205],"Amazon Connect","Co-browse","Nuxt","iframe-embedding","Bun","TypeScript","xuGij7hLo9Kaxd-R31uSrW4uaGcbOlcNApZt-shZn3k",1785421333422]