detached b3d3030

feat: thread requesting_domain/current_domain to permission dialog

commit
b3d303062506e14da58ca484ae9244e0e8f174aa
author
brice · · 2026-03-25 12:43 UTC
committer
brice · · 2026-03-25 12:47 UTC
parent
784ca95
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
10 files · +98 -52
M internal/application/port/mocks/mocks.go +14 -8
@@ -8211,8 +8211,8 @@ func (_m *MockPermissionDialogPresenter) EXPECT() *MockPermissionDialogPresenter
82118211 }
82128212
82138213 // ShowPermissionDialog provides a mock function for the type MockPermissionDialogPresenter
8214-func (_mock *MockPermissionDialogPresenter) ShowPermissionDialog(ctx context.Context, origin string, permTypes []entity.PermissionType, callback func(result port.PermissionDialogResult)) {
8215- _mock.Called(ctx, origin, permTypes, callback)
8214+func (_mock *MockPermissionDialogPresenter) ShowPermissionDialog(ctx context.Context, origin string, permTypes []entity.PermissionType, metadata entity.PermissionMetadata, callback func(result port.PermissionDialogResult)) {
8215+ _mock.Called(ctx, origin, permTypes, metadata, callback)
82168216 return
82178217 }
82188218
@@ -8225,12 +8225,13 @@ type MockPermissionDialogPresenter_ShowPermissionDialog_Call struct {
82258225 // - ctx context.Context
82268226 // - origin string
82278227 // - permTypes []entity.PermissionType
8228+// - metadata entity.PermissionMetadata
82288229 // - callback func(result port.PermissionDialogResult)
8229-func (_e *MockPermissionDialogPresenter_Expecter) ShowPermissionDialog(ctx interface{}, origin interface{}, permTypes interface{}, callback interface{}) *MockPermissionDialogPresenter_ShowPermissionDialog_Call {
8230- return &MockPermissionDialogPresenter_ShowPermissionDialog_Call{Call: _e.mock.On("ShowPermissionDialog", ctx, origin, permTypes, callback)}
8230+func (_e *MockPermissionDialogPresenter_Expecter) ShowPermissionDialog(ctx interface{}, origin interface{}, permTypes interface{}, metadata interface{}, callback interface{}) *MockPermissionDialogPresenter_ShowPermissionDialog_Call {
8231+ return &MockPermissionDialogPresenter_ShowPermissionDialog_Call{Call: _e.mock.On("ShowPermissionDialog", ctx, origin, permTypes, metadata, callback)}
82318232 }
82328233
8233-func (_c *MockPermissionDialogPresenter_ShowPermissionDialog_Call) Run(run func(ctx context.Context, origin string, permTypes []entity.PermissionType, callback func(result port.PermissionDialogResult))) *MockPermissionDialogPresenter_ShowPermissionDialog_Call {
8234+func (_c *MockPermissionDialogPresenter_ShowPermissionDialog_Call) Run(run func(ctx context.Context, origin string, permTypes []entity.PermissionType, metadata entity.PermissionMetadata, callback func(result port.PermissionDialogResult))) *MockPermissionDialogPresenter_ShowPermissionDialog_Call {
82348235 _c.Call.Run(func(args mock.Arguments) {
82358236 var arg0 context.Context
82368237 if args[0] != nil {
@@ -8244,15 +8245,20 @@ func (_c *MockPermissionDialogPresenter_ShowPermissionDialog_Call) Run(run func(
82448245 if args[2] != nil {
82458246 arg2 = args[2].([]entity.PermissionType)
82468247 }
8247- var arg3 func(result port.PermissionDialogResult)
8248+ var arg3 entity.PermissionMetadata
82488249 if args[3] != nil {
8249- arg3 = args[3].(func(result port.PermissionDialogResult))
8250+ arg3 = args[3].(entity.PermissionMetadata)
8251+ }
8252+ var arg4 func(result port.PermissionDialogResult)
8253+ if args[4] != nil {
8254+ arg4 = args[4].(func(result port.PermissionDialogResult))
82508255 }
82518256 run(
82528257 arg0,
82538258 arg1,
82548259 arg2,
82558260 arg3,
8261+ arg4,
82568262 )
82578263 })
82588264 return _c
@@ -8263,7 +8269,7 @@ func (_c *MockPermissionDialogPresenter_ShowPermissionDialog_Call) Return() *Moc
82638269 return _c
82648270 }
82658271
8266-func (_c *MockPermissionDialogPresenter_ShowPermissionDialog_Call) RunAndReturn(run func(ctx context.Context, origin string, permTypes []entity.PermissionType, callback func(result port.PermissionDialogResult))) *MockPermissionDialogPresenter_ShowPermissionDialog_Call {
8272+func (_c *MockPermissionDialogPresenter_ShowPermissionDialog_Call) RunAndReturn(run func(ctx context.Context, origin string, permTypes []entity.PermissionType, metadata entity.PermissionMetadata, callback func(result port.PermissionDialogResult))) *MockPermissionDialogPresenter_ShowPermissionDialog_Call {
82678273 _c.Run(run)
82688274 return _c
82698275 }
@@ -8211,8 +8211,8 @@ func (_m *MockPermissionDialogPresenter) EXPECT() *MockPermissionDialogPresenter
8211 }8211 }
8212 8212
8213 // ShowPermissionDialog provides a mock function for the type MockPermissionDialogPresenter8213 // ShowPermissionDialog provides a mock function for the type MockPermissionDialogPresenter
8214-func (_mock *MockPermissionDialogPresenter) ShowPermissionDialog(ctx context.Context, origin string, permTypes []entity.PermissionType, callback func(result port.PermissionDialogResult)) {8214+func (_mock *MockPermissionDialogPresenter) ShowPermissionDialog(ctx context.Context, origin string, permTypes []entity.PermissionType, metadata entity.PermissionMetadata, callback func(result port.PermissionDialogResult)) {
8215- _mock.Called(ctx, origin, permTypes, callback)8215+ _mock.Called(ctx, origin, permTypes, metadata, callback)
8216 return8216 return
8217 }8217 }
8218 8218
@@ -8225,12 +8225,13 @@ type MockPermissionDialogPresenter_ShowPermissionDialog_Call struct {
8225 // - ctx context.Context8225 // - ctx context.Context
8226 // - origin string8226 // - origin string
8227 // - permTypes []entity.PermissionType8227 // - permTypes []entity.PermissionType
8228+// - metadata entity.PermissionMetadata
8228 // - callback func(result port.PermissionDialogResult)8229 // - callback func(result port.PermissionDialogResult)
8229-func (_e *MockPermissionDialogPresenter_Expecter) ShowPermissionDialog(ctx interface{}, origin interface{}, permTypes interface{}, callback interface{}) *MockPermissionDialogPresenter_ShowPermissionDialog_Call {8230+func (_e *MockPermissionDialogPresenter_Expecter) ShowPermissionDialog(ctx interface{}, origin interface{}, permTypes interface{}, metadata interface{}, callback interface{}) *MockPermissionDialogPresenter_ShowPermissionDialog_Call {
8230- return &MockPermissionDialogPresenter_ShowPermissionDialog_Call{Call: _e.mock.On("ShowPermissionDialog", ctx, origin, permTypes, callback)}8231+ return &MockPermissionDialogPresenter_ShowPermissionDialog_Call{Call: _e.mock.On("ShowPermissionDialog", ctx, origin, permTypes, metadata, callback)}
8231 }8232 }
8232 8233
8233-func (_c *MockPermissionDialogPresenter_ShowPermissionDialog_Call) Run(run func(ctx context.Context, origin string, permTypes []entity.PermissionType, callback func(result port.PermissionDialogResult))) *MockPermissionDialogPresenter_ShowPermissionDialog_Call {8234+func (_c *MockPermissionDialogPresenter_ShowPermissionDialog_Call) Run(run func(ctx context.Context, origin string, permTypes []entity.PermissionType, metadata entity.PermissionMetadata, callback func(result port.PermissionDialogResult))) *MockPermissionDialogPresenter_ShowPermissionDialog_Call {
8234 _c.Call.Run(func(args mock.Arguments) {8235 _c.Call.Run(func(args mock.Arguments) {
8235 var arg0 context.Context8236 var arg0 context.Context
8236 if args[0] != nil {8237 if args[0] != nil {
@@ -8244,15 +8245,20 @@ func (_c *MockPermissionDialogPresenter_ShowPermissionDialog_Call) Run(run func(
8244 if args[2] != nil {8245 if args[2] != nil {
8245 arg2 = args[2].([]entity.PermissionType)8246 arg2 = args[2].([]entity.PermissionType)
8246 }8247 }
8247- var arg3 func(result port.PermissionDialogResult)8248+ var arg3 entity.PermissionMetadata
8248 if args[3] != nil {8249 if args[3] != nil {
8249- arg3 = args[3].(func(result port.PermissionDialogResult))8250+ arg3 = args[3].(entity.PermissionMetadata)
8251+ }
8252+ var arg4 func(result port.PermissionDialogResult)
8253+ if args[4] != nil {
8254+ arg4 = args[4].(func(result port.PermissionDialogResult))
8250 }8255 }
8251 run(8256 run(
8252 arg0,8257 arg0,
8253 arg1,8258 arg1,
8254 arg2,8259 arg2,
8255 arg3,8260 arg3,
8261+ arg4,
8256 )8262 )
8257 })8263 })
8258 return _c8264 return _c
@@ -8263,7 +8269,7 @@ func (_c *MockPermissionDialogPresenter_ShowPermissionDialog_Call) Return() *Moc
8263 return _c8269 return _c
8264 }8270 }
8265 8271
8266-func (_c *MockPermissionDialogPresenter_ShowPermissionDialog_Call) RunAndReturn(run func(ctx context.Context, origin string, permTypes []entity.PermissionType, callback func(result port.PermissionDialogResult))) *MockPermissionDialogPresenter_ShowPermissionDialog_Call {8272+func (_c *MockPermissionDialogPresenter_ShowPermissionDialog_Call) RunAndReturn(run func(ctx context.Context, origin string, permTypes []entity.PermissionType, metadata entity.PermissionMetadata, callback func(result port.PermissionDialogResult))) *MockPermissionDialogPresenter_ShowPermissionDialog_Call {
8267 _c.Run(run)8273 _c.Run(run)
8268 return _c8274 return _c
8269 }8275 }
M internal/application/port/permission.go +2 -0
@@ -26,11 +26,13 @@ type PermissionDialogPresenter interface {
2626 // - ctx: context for cancellation and logging
2727 // - origin: the website origin requesting permission (e.g., "https://meet.example.com")
2828 // - permTypes: the types of permissions being requested
29+ // - metadata: permission-type-specific context (e.g., "requesting_domain" for website_data_access)
2930 // - callback: invoked when the user makes a decision or dialog is dismissed
3031 ShowPermissionDialog(
3132 ctx context.Context,
3233 origin string,
3334 permTypes []entity.PermissionType,
35+ metadata entity.PermissionMetadata,
3436 callback func(result PermissionDialogResult),
3537 )
3638 }
@@ -26,11 +26,13 @@ type PermissionDialogPresenter interface {
26 // - ctx: context for cancellation and logging26 // - ctx: context for cancellation and logging
27 // - origin: the website origin requesting permission (e.g., "https://meet.example.com")27 // - origin: the website origin requesting permission (e.g., "https://meet.example.com")
28 // - permTypes: the types of permissions being requested28 // - permTypes: the types of permissions being requested
29+ // - metadata: permission-type-specific context (e.g., "requesting_domain" for website_data_access)
29 // - callback: invoked when the user makes a decision or dialog is dismissed30 // - callback: invoked when the user makes a decision or dialog is dismissed
30 ShowPermissionDialog(31 ShowPermissionDialog(
31 ctx context.Context,32 ctx context.Context,
32 origin string,33 origin string,
33 permTypes []entity.PermissionType,34 permTypes []entity.PermissionType,
35+ metadata entity.PermissionMetadata,
34 callback func(result PermissionDialogResult),36 callback func(result PermissionDialogResult),
35 )37 )
36 }38 }
M internal/application/port/webview.go +2 -1
@@ -107,7 +107,8 @@ type WebViewCallbacks struct {
107107 // OnPermissionRequest is called when a site requests permission (mic, camera, screen sharing).
108108 // Return true to indicate the request was handled. Call allow()/deny() to respond.
109109 // The permission types are passed as strings ("microphone", "camera", "display", etc.).
110- OnPermissionRequest func(origin string, permTypes []string, allow, deny func()) bool
110+ // The metadata map carries permission-type-specific context (e.g., "requesting_domain" for website_data_access).
111+ OnPermissionRequest func(origin string, permTypes []string, metadata map[string]string, allow, deny func()) bool
111112
112113 // OnLinkMiddleClick is called when a link is middle-clicked.
113114 // Return true if handled (blocks default navigation).
@@ -107,7 +107,8 @@ type WebViewCallbacks struct {
107 // OnPermissionRequest is called when a site requests permission (mic, camera, screen sharing).107 // OnPermissionRequest is called when a site requests permission (mic, camera, screen sharing).
108 // Return true to indicate the request was handled. Call allow()/deny() to respond.108 // Return true to indicate the request was handled. Call allow()/deny() to respond.
109 // The permission types are passed as strings ("microphone", "camera", "display", etc.).109 // The permission types are passed as strings ("microphone", "camera", "display", etc.).
110- OnPermissionRequest func(origin string, permTypes []string, allow, deny func()) bool110+ // The metadata map carries permission-type-specific context (e.g., "requesting_domain" for website_data_access).
111+ OnPermissionRequest func(origin string, permTypes []string, metadata map[string]string, allow, deny func()) bool
111 112
112 // OnLinkMiddleClick is called when a link is middle-clicked.113 // OnLinkMiddleClick is called when a link is middle-clicked.
113 // Return true if handled (blocks default navigation).114 // Return true if handled (blocks default navigation).
M internal/application/usecase/handle_permission.go +5 -2
@@ -79,11 +79,13 @@ func (uc *HandlePermissionUseCase) getDialog() port.PermissionDialogPresenter {
7979 // - ctx: context for cancellation and logging
8080 // - origin: the website origin (URI) requesting permission
8181 // - permTypes: the types of permissions being requested
82+// - metadata: permission-type-specific context (e.g., "requesting_domain" for website_data_access)
8283 // - callback: callbacks to allow or deny the request (must call one)
8384 func (uc *HandlePermissionUseCase) HandlePermissionRequest(
8485 ctx context.Context,
8586 origin string,
8687 permTypes []entity.PermissionType,
88+ metadata entity.PermissionMetadata,
8789 callback PermissionCallback,
8890 ) {
8991 log := uc.logger(ctx).With().
@@ -135,7 +137,7 @@ func (uc *HandlePermissionUseCase) HandlePermissionRequest(
135137 }
136138
137139 // Show dialog for undecided permissions
138- uc.showPermissionDialog(ctx, origin, permTypes, callback)
140+ uc.showPermissionDialog(ctx, origin, permTypes, metadata, callback)
139141 }
140142
141143 // QueryPermissionState returns the current permission state for the W3C Permissions API.
@@ -249,6 +251,7 @@ func (uc *HandlePermissionUseCase) showPermissionDialog(
249251 ctx context.Context,
250252 origin string,
251253 permTypes []entity.PermissionType,
254+ metadata entity.PermissionMetadata,
252255 callback PermissionCallback,
253256 ) {
254257 log := uc.logger(ctx)
@@ -262,7 +265,7 @@ func (uc *HandlePermissionUseCase) showPermissionDialog(
262265 return
263266 }
264267
265- dialog.ShowPermissionDialog(ctx, origin, permTypes, func(result port.PermissionDialogResult) {
268+ dialog.ShowPermissionDialog(ctx, origin, permTypes, metadata, func(result port.PermissionDialogResult) {
266269 if result.Allowed {
267270 log.Debug().Bool("persistent", result.Persistent).Msg("user allowed permission")
268271 callback.Allow()
@@ -79,11 +79,13 @@ func (uc *HandlePermissionUseCase) getDialog() port.PermissionDialogPresenter {
79 // - ctx: context for cancellation and logging79 // - ctx: context for cancellation and logging
80 // - origin: the website origin (URI) requesting permission80 // - origin: the website origin (URI) requesting permission
81 // - permTypes: the types of permissions being requested81 // - permTypes: the types of permissions being requested
82+// - metadata: permission-type-specific context (e.g., "requesting_domain" for website_data_access)
82 // - callback: callbacks to allow or deny the request (must call one)83 // - callback: callbacks to allow or deny the request (must call one)
83 func (uc *HandlePermissionUseCase) HandlePermissionRequest(84 func (uc *HandlePermissionUseCase) HandlePermissionRequest(
84 ctx context.Context,85 ctx context.Context,
85 origin string,86 origin string,
86 permTypes []entity.PermissionType,87 permTypes []entity.PermissionType,
88+ metadata entity.PermissionMetadata,
87 callback PermissionCallback,89 callback PermissionCallback,
88 ) {90 ) {
89 log := uc.logger(ctx).With().91 log := uc.logger(ctx).With().
@@ -135,7 +137,7 @@ func (uc *HandlePermissionUseCase) HandlePermissionRequest(
135 }137 }
136 138
137 // Show dialog for undecided permissions139 // Show dialog for undecided permissions
138- uc.showPermissionDialog(ctx, origin, permTypes, callback)140+ uc.showPermissionDialog(ctx, origin, permTypes, metadata, callback)
139 }141 }
140 142
141 // QueryPermissionState returns the current permission state for the W3C Permissions API.143 // QueryPermissionState returns the current permission state for the W3C Permissions API.
@@ -249,6 +251,7 @@ func (uc *HandlePermissionUseCase) showPermissionDialog(
249 ctx context.Context,251 ctx context.Context,
250 origin string,252 origin string,
251 permTypes []entity.PermissionType,253 permTypes []entity.PermissionType,
254+ metadata entity.PermissionMetadata,
252 callback PermissionCallback,255 callback PermissionCallback,
253 ) {256 ) {
254 log := uc.logger(ctx)257 log := uc.logger(ctx)
@@ -262,7 +265,7 @@ func (uc *HandlePermissionUseCase) showPermissionDialog(
262 return265 return
263 }266 }
264 267
265- dialog.ShowPermissionDialog(ctx, origin, permTypes, func(result port.PermissionDialogResult) {268+ dialog.ShowPermissionDialog(ctx, origin, permTypes, metadata, func(result port.PermissionDialogResult) {
266 if result.Allowed {269 if result.Allowed {
267 log.Debug().Bool("persistent", result.Persistent).Msg("user allowed permission")270 log.Debug().Bool("persistent", result.Persistent).Msg("user allowed permission")
268 callback.Allow()271 callback.Allow()
M internal/application/usecase/handle_permission_test.go +25 -19
@@ -39,7 +39,7 @@ func TestHandlePermissionUseCase_AutoAllowDisplayCapture(t *testing.T) {
3939 // Display capture should be auto-allowed (portal handles UI)
4040 uc.HandlePermissionRequest(ctx, "https://meet.example.com", []entity.PermissionType{
4141 entity.PermissionTypeDisplay,
42- }, callback)
42+ }, nil, callback)
4343
4444 assert.True(t, allowed, "display capture should be auto-allowed")
4545 assert.False(t, denied)
@@ -65,7 +65,7 @@ func TestHandlePermissionUseCase_AutoAllowDeviceInfo(t *testing.T) {
6565 // Device info should be auto-allowed
6666 uc.HandlePermissionRequest(ctx, "https://example.com", []entity.PermissionType{
6767 entity.PermissionTypeDeviceInfo,
68- }, callback)
68+ }, nil, callback)
6969
7070 assert.True(t, allowed, "device info should be auto-allowed")
7171 }
@@ -94,7 +94,7 @@ func TestHandlePermissionUseCase_StoredPermissionGranted(t *testing.T) {
9494
9595 uc.HandlePermissionRequest(ctx, "https://meet.example.com", []entity.PermissionType{
9696 entity.PermissionTypeMicrophone,
97- }, callback)
97+ }, nil, callback)
9898
9999 assert.True(t, allowed, "should use stored granted permission")
100100 dialog.AssertNotCalled(t, "ShowPermissionDialog")
@@ -124,7 +124,7 @@ func TestHandlePermissionUseCase_StoredPermissionDenied(t *testing.T) {
124124
125125 uc.HandlePermissionRequest(ctx, "https://meet.example.com", []entity.PermissionType{
126126 entity.PermissionTypeCamera,
127- }, callback)
127+ }, nil, callback)
128128
129129 assert.True(t, denied, "should use stored denied permission")
130130 dialog.AssertNotCalled(t, "ShowPermissionDialog")
@@ -150,13 +150,13 @@ func TestHandlePermissionUseCase_ShowDialogForPrompt(t *testing.T) {
150150 // Dialog shows and user clicks Allow (not persistent)
151151 dialog.EXPECT().ShowPermissionDialog(mock.Anything, "https://meet.example.com", []entity.PermissionType{
152152 entity.PermissionTypeMicrophone,
153- }, mock.Anything).Run(func(_ context.Context, _ string, _ []entity.PermissionType, callback func(port.PermissionDialogResult)) {
153+ }, mock.Anything, mock.Anything).Run(func(_ context.Context, _ string, _ []entity.PermissionType, _ entity.PermissionMetadata, callback func(port.PermissionDialogResult)) {
154154 callback(port.PermissionDialogResult{Allowed: true, Persistent: false})
155155 })
156156
157157 uc.HandlePermissionRequest(ctx, "https://meet.example.com", []entity.PermissionType{
158158 entity.PermissionTypeMicrophone,
159- }, callback)
159+ }, nil, callback)
160160
161161 assert.True(t, allowed, "user allowed permission")
162162 // Should not persist since Persistent=false
@@ -183,7 +183,7 @@ func TestHandlePermissionUseCase_PersistAllowed(t *testing.T) {
183183 // Dialog shows and user clicks "Always Allow"
184184 dialog.EXPECT().ShowPermissionDialog(mock.Anything, "https://meet.example.com", []entity.PermissionType{
185185 entity.PermissionTypeMicrophone,
186- }, mock.Anything).Run(func(_ context.Context, _ string, _ []entity.PermissionType, cb func(port.PermissionDialogResult)) {
186+ }, mock.Anything, mock.Anything).Run(func(_ context.Context, _ string, _ []entity.PermissionType, _ entity.PermissionMetadata, cb func(port.PermissionDialogResult)) {
187187 cb(port.PermissionDialogResult{Allowed: true, Persistent: true})
188188 })
189189
@@ -197,7 +197,7 @@ func TestHandlePermissionUseCase_PersistAllowed(t *testing.T) {
197197
198198 uc.HandlePermissionRequest(ctx, "https://meet.example.com", []entity.PermissionType{
199199 entity.PermissionTypeMicrophone,
200- }, callback)
200+ }, nil, callback)
201201
202202 assert.True(t, allowed)
203203 }
@@ -222,7 +222,7 @@ func TestHandlePermissionUseCase_PersistDenied(t *testing.T) {
222222 // Dialog shows and user clicks "Always Deny"
223223 dialog.EXPECT().ShowPermissionDialog(mock.Anything, "https://meet.example.com", []entity.PermissionType{
224224 entity.PermissionTypeCamera,
225- }, mock.Anything).Run(func(_ context.Context, _ string, _ []entity.PermissionType, cb func(port.PermissionDialogResult)) {
225+ }, mock.Anything, mock.Anything).Run(func(_ context.Context, _ string, _ []entity.PermissionType, _ entity.PermissionMetadata, cb func(port.PermissionDialogResult)) {
226226 // User denied but wants to persist this decision
227227 cb(port.PermissionDialogResult{Allowed: false, Persistent: true})
228228 })
@@ -235,7 +235,7 @@ func TestHandlePermissionUseCase_PersistDenied(t *testing.T) {
235235
236236 uc.HandlePermissionRequest(ctx, "https://meet.example.com", []entity.PermissionType{
237237 entity.PermissionTypeCamera,
238- }, callback)
238+ }, nil, callback)
239239
240240 assert.True(t, denied)
241241 }
@@ -256,7 +256,7 @@ func TestHandlePermissionUseCase_EmptyOrigin(t *testing.T) {
256256 // Empty origin should be denied
257257 uc.HandlePermissionRequest(ctx, "", []entity.PermissionType{
258258 entity.PermissionTypeMicrophone,
259- }, callback)
259+ }, nil, callback)
260260
261261 assert.True(t, denied, "empty origin should be denied")
262262 }
@@ -292,7 +292,7 @@ func TestHandlePermissionUseCase_CombinedPermissions(t *testing.T) {
292292 uc.HandlePermissionRequest(ctx, "https://meet.example.com", []entity.PermissionType{
293293 entity.PermissionTypeMicrophone,
294294 entity.PermissionTypeCamera,
295- }, callback)
295+ }, nil, callback)
296296
297297 assert.True(t, denied, "if any permission is denied, whole request is denied")
298298 }
@@ -398,7 +398,7 @@ func TestHandlePermissionUseCase_NonPersistableNotSaved(t *testing.T) {
398398
399399 uc.HandlePermissionRequest(ctx, "https://meet.example.com", []entity.PermissionType{
400400 entity.PermissionTypeDisplay,
401- }, callback)
401+ }, nil, callback)
402402
403403 assert.True(t, allowed)
404404 permRepo.AssertNotCalled(t, "Set")
@@ -423,7 +423,7 @@ func TestHandlePermissionUseCase_NoDialogPresenter(t *testing.T) {
423423
424424 uc.HandlePermissionRequest(ctx, "https://meet.example.com", []entity.PermissionType{
425425 entity.PermissionTypeMicrophone,
426- }, callback)
426+ }, nil, callback)
427427
428428 assert.True(t, denied, "should deny when no dialog presenter is available")
429429 }
@@ -450,7 +450,7 @@ func TestHandlePermissionUseCase_DisplayOverrideDenied(t *testing.T) {
450450
451451 uc.HandlePermissionRequest(ctx, "https://meet.example.com", []entity.PermissionType{
452452 entity.PermissionTypeDisplay,
453- }, callback)
453+ }, nil, callback)
454454
455455 assert.True(t, denied, "display should be denied when manual override is denied")
456456 dialog.AssertNotCalled(t, "ShowPermissionDialog")
@@ -526,7 +526,7 @@ func TestHandlePermissionUseCase_UsesInjectedLoggerFactory(t *testing.T) {
526526 }
527527
528528 uc := usecase.NewHandlePermissionUseCase(permRepo, dialog, loggerFactory)
529- uc.HandlePermissionRequest(ctx, "", []entity.PermissionType{entity.PermissionTypeMicrophone}, usecase.PermissionCallback{
529+ uc.HandlePermissionRequest(ctx, "", []entity.PermissionType{entity.PermissionTypeMicrophone}, nil, usecase.PermissionCallback{
530530 Allow: func() {},
531531 Deny: func() {},
532532 })
@@ -551,13 +551,19 @@ func TestHandlePermissionUseCase_WebsiteDataAccess_ShowsDialogAndPersists(t *tes
551551 Deny: func() {},
552552 }
553553
554+ meta := entity.PermissionMetadata{
555+ "requesting_domain": "accounts.google.com",
556+ "current_domain": "shop.example.com",
557+ }
558+
554559 // Dialog shows and user clicks "Always Allow"
555560 dialog.EXPECT().ShowPermissionDialog(
556561 mock.Anything,
557562 "https://shop.example.com",
558563 []entity.PermissionType{entity.PermissionTypeWebsiteDataAccess},
564+ meta,
559565 mock.Anything,
560- ).Run(func(_ context.Context, _ string, _ []entity.PermissionType, cb func(port.PermissionDialogResult)) {
566+ ).Run(func(_ context.Context, _ string, _ []entity.PermissionType, _ entity.PermissionMetadata, cb func(port.PermissionDialogResult)) {
561567 cb(port.PermissionDialogResult{Allowed: true, Persistent: true})
562568 })
563569
@@ -571,7 +577,7 @@ func TestHandlePermissionUseCase_WebsiteDataAccess_ShowsDialogAndPersists(t *tes
571577
572578 uc.HandlePermissionRequest(ctx, "https://shop.example.com", []entity.PermissionType{
573579 entity.PermissionTypeWebsiteDataAccess,
574- }, callback)
580+ }, meta, callback)
575581
576582 assert.True(t, allowed)
577583 }
@@ -599,7 +605,7 @@ func TestHandlePermissionUseCase_WebsiteDataAccess_UsesStoredGrant(t *testing.T)
599605
600606 uc.HandlePermissionRequest(ctx, "https://shop.example.com", []entity.PermissionType{
601607 entity.PermissionTypeWebsiteDataAccess,
602- }, callback)
608+ }, entity.PermissionMetadata{"requesting_domain": "accounts.google.com", "current_domain": "shop.example.com"}, callback)
603609
604610 assert.True(t, allowed, "should use stored granted permission without dialog")
605611 dialog.AssertNotCalled(t, "ShowPermissionDialog")
@@ -39,7 +39,7 @@ func TestHandlePermissionUseCase_AutoAllowDisplayCapture(t *testing.T) {
39 // Display capture should be auto-allowed (portal handles UI)39 // Display capture should be auto-allowed (portal handles UI)
40 uc.HandlePermissionRequest(ctx, "https://meet.example.com", []entity.PermissionType{40 uc.HandlePermissionRequest(ctx, "https://meet.example.com", []entity.PermissionType{
41 entity.PermissionTypeDisplay,41 entity.PermissionTypeDisplay,
42- }, callback)42+ }, nil, callback)
43 43
44 assert.True(t, allowed, "display capture should be auto-allowed")44 assert.True(t, allowed, "display capture should be auto-allowed")
45 assert.False(t, denied)45 assert.False(t, denied)
@@ -65,7 +65,7 @@ func TestHandlePermissionUseCase_AutoAllowDeviceInfo(t *testing.T) {
65 // Device info should be auto-allowed65 // Device info should be auto-allowed
66 uc.HandlePermissionRequest(ctx, "https://example.com", []entity.PermissionType{66 uc.HandlePermissionRequest(ctx, "https://example.com", []entity.PermissionType{
67 entity.PermissionTypeDeviceInfo,67 entity.PermissionTypeDeviceInfo,
68- }, callback)68+ }, nil, callback)
69 69
70 assert.True(t, allowed, "device info should be auto-allowed")70 assert.True(t, allowed, "device info should be auto-allowed")
71 }71 }
@@ -94,7 +94,7 @@ func TestHandlePermissionUseCase_StoredPermissionGranted(t *testing.T) {
94 94
95 uc.HandlePermissionRequest(ctx, "https://meet.example.com", []entity.PermissionType{95 uc.HandlePermissionRequest(ctx, "https://meet.example.com", []entity.PermissionType{
96 entity.PermissionTypeMicrophone,96 entity.PermissionTypeMicrophone,
97- }, callback)97+ }, nil, callback)
98 98
99 assert.True(t, allowed, "should use stored granted permission")99 assert.True(t, allowed, "should use stored granted permission")
100 dialog.AssertNotCalled(t, "ShowPermissionDialog")100 dialog.AssertNotCalled(t, "ShowPermissionDialog")
@@ -124,7 +124,7 @@ func TestHandlePermissionUseCase_StoredPermissionDenied(t *testing.T) {
124 124
125 uc.HandlePermissionRequest(ctx, "https://meet.example.com", []entity.PermissionType{125 uc.HandlePermissionRequest(ctx, "https://meet.example.com", []entity.PermissionType{
126 entity.PermissionTypeCamera,126 entity.PermissionTypeCamera,
127- }, callback)127+ }, nil, callback)
128 128
129 assert.True(t, denied, "should use stored denied permission")129 assert.True(t, denied, "should use stored denied permission")
130 dialog.AssertNotCalled(t, "ShowPermissionDialog")130 dialog.AssertNotCalled(t, "ShowPermissionDialog")
@@ -150,13 +150,13 @@ func TestHandlePermissionUseCase_ShowDialogForPrompt(t *testing.T) {
150 // Dialog shows and user clicks Allow (not persistent)150 // Dialog shows and user clicks Allow (not persistent)
151 dialog.EXPECT().ShowPermissionDialog(mock.Anything, "https://meet.example.com", []entity.PermissionType{151 dialog.EXPECT().ShowPermissionDialog(mock.Anything, "https://meet.example.com", []entity.PermissionType{
152 entity.PermissionTypeMicrophone,152 entity.PermissionTypeMicrophone,
153- }, mock.Anything).Run(func(_ context.Context, _ string, _ []entity.PermissionType, callback func(port.PermissionDialogResult)) {153+ }, mock.Anything, mock.Anything).Run(func(_ context.Context, _ string, _ []entity.PermissionType, _ entity.PermissionMetadata, callback func(port.PermissionDialogResult)) {
154 callback(port.PermissionDialogResult{Allowed: true, Persistent: false})154 callback(port.PermissionDialogResult{Allowed: true, Persistent: false})
155 })155 })
156 156
157 uc.HandlePermissionRequest(ctx, "https://meet.example.com", []entity.PermissionType{157 uc.HandlePermissionRequest(ctx, "https://meet.example.com", []entity.PermissionType{
158 entity.PermissionTypeMicrophone,158 entity.PermissionTypeMicrophone,
159- }, callback)159+ }, nil, callback)
160 160
161 assert.True(t, allowed, "user allowed permission")161 assert.True(t, allowed, "user allowed permission")
162 // Should not persist since Persistent=false162 // Should not persist since Persistent=false
@@ -183,7 +183,7 @@ func TestHandlePermissionUseCase_PersistAllowed(t *testing.T) {
183 // Dialog shows and user clicks "Always Allow"183 // Dialog shows and user clicks "Always Allow"
184 dialog.EXPECT().ShowPermissionDialog(mock.Anything, "https://meet.example.com", []entity.PermissionType{184 dialog.EXPECT().ShowPermissionDialog(mock.Anything, "https://meet.example.com", []entity.PermissionType{
185 entity.PermissionTypeMicrophone,185 entity.PermissionTypeMicrophone,
186- }, mock.Anything).Run(func(_ context.Context, _ string, _ []entity.PermissionType, cb func(port.PermissionDialogResult)) {186+ }, mock.Anything, mock.Anything).Run(func(_ context.Context, _ string, _ []entity.PermissionType, _ entity.PermissionMetadata, cb func(port.PermissionDialogResult)) {
187 cb(port.PermissionDialogResult{Allowed: true, Persistent: true})187 cb(port.PermissionDialogResult{Allowed: true, Persistent: true})
188 })188 })
189 189
@@ -197,7 +197,7 @@ func TestHandlePermissionUseCase_PersistAllowed(t *testing.T) {
197 197
198 uc.HandlePermissionRequest(ctx, "https://meet.example.com", []entity.PermissionType{198 uc.HandlePermissionRequest(ctx, "https://meet.example.com", []entity.PermissionType{
199 entity.PermissionTypeMicrophone,199 entity.PermissionTypeMicrophone,
200- }, callback)200+ }, nil, callback)
201 201
202 assert.True(t, allowed)202 assert.True(t, allowed)
203 }203 }
@@ -222,7 +222,7 @@ func TestHandlePermissionUseCase_PersistDenied(t *testing.T) {
222 // Dialog shows and user clicks "Always Deny"222 // Dialog shows and user clicks "Always Deny"
223 dialog.EXPECT().ShowPermissionDialog(mock.Anything, "https://meet.example.com", []entity.PermissionType{223 dialog.EXPECT().ShowPermissionDialog(mock.Anything, "https://meet.example.com", []entity.PermissionType{
224 entity.PermissionTypeCamera,224 entity.PermissionTypeCamera,
225- }, mock.Anything).Run(func(_ context.Context, _ string, _ []entity.PermissionType, cb func(port.PermissionDialogResult)) {225+ }, mock.Anything, mock.Anything).Run(func(_ context.Context, _ string, _ []entity.PermissionType, _ entity.PermissionMetadata, cb func(port.PermissionDialogResult)) {
226 // User denied but wants to persist this decision226 // User denied but wants to persist this decision
227 cb(port.PermissionDialogResult{Allowed: false, Persistent: true})227 cb(port.PermissionDialogResult{Allowed: false, Persistent: true})
228 })228 })
@@ -235,7 +235,7 @@ func TestHandlePermissionUseCase_PersistDenied(t *testing.T) {
235 235
236 uc.HandlePermissionRequest(ctx, "https://meet.example.com", []entity.PermissionType{236 uc.HandlePermissionRequest(ctx, "https://meet.example.com", []entity.PermissionType{
237 entity.PermissionTypeCamera,237 entity.PermissionTypeCamera,
238- }, callback)238+ }, nil, callback)
239 239
240 assert.True(t, denied)240 assert.True(t, denied)
241 }241 }
@@ -256,7 +256,7 @@ func TestHandlePermissionUseCase_EmptyOrigin(t *testing.T) {
256 // Empty origin should be denied256 // Empty origin should be denied
257 uc.HandlePermissionRequest(ctx, "", []entity.PermissionType{257 uc.HandlePermissionRequest(ctx, "", []entity.PermissionType{
258 entity.PermissionTypeMicrophone,258 entity.PermissionTypeMicrophone,
259- }, callback)259+ }, nil, callback)
260 260
261 assert.True(t, denied, "empty origin should be denied")261 assert.True(t, denied, "empty origin should be denied")
262 }262 }
@@ -292,7 +292,7 @@ func TestHandlePermissionUseCase_CombinedPermissions(t *testing.T) {
292 uc.HandlePermissionRequest(ctx, "https://meet.example.com", []entity.PermissionType{292 uc.HandlePermissionRequest(ctx, "https://meet.example.com", []entity.PermissionType{
293 entity.PermissionTypeMicrophone,293 entity.PermissionTypeMicrophone,
294 entity.PermissionTypeCamera,294 entity.PermissionTypeCamera,
295- }, callback)295+ }, nil, callback)
296 296
297 assert.True(t, denied, "if any permission is denied, whole request is denied")297 assert.True(t, denied, "if any permission is denied, whole request is denied")
298 }298 }
@@ -398,7 +398,7 @@ func TestHandlePermissionUseCase_NonPersistableNotSaved(t *testing.T) {
398 398
399 uc.HandlePermissionRequest(ctx, "https://meet.example.com", []entity.PermissionType{399 uc.HandlePermissionRequest(ctx, "https://meet.example.com", []entity.PermissionType{
400 entity.PermissionTypeDisplay,400 entity.PermissionTypeDisplay,
401- }, callback)401+ }, nil, callback)
402 402
403 assert.True(t, allowed)403 assert.True(t, allowed)
404 permRepo.AssertNotCalled(t, "Set")404 permRepo.AssertNotCalled(t, "Set")
@@ -423,7 +423,7 @@ func TestHandlePermissionUseCase_NoDialogPresenter(t *testing.T) {
423 423
424 uc.HandlePermissionRequest(ctx, "https://meet.example.com", []entity.PermissionType{424 uc.HandlePermissionRequest(ctx, "https://meet.example.com", []entity.PermissionType{
425 entity.PermissionTypeMicrophone,425 entity.PermissionTypeMicrophone,
426- }, callback)426+ }, nil, callback)
427 427
428 assert.True(t, denied, "should deny when no dialog presenter is available")428 assert.True(t, denied, "should deny when no dialog presenter is available")
429 }429 }
@@ -450,7 +450,7 @@ func TestHandlePermissionUseCase_DisplayOverrideDenied(t *testing.T) {
450 450
451 uc.HandlePermissionRequest(ctx, "https://meet.example.com", []entity.PermissionType{451 uc.HandlePermissionRequest(ctx, "https://meet.example.com", []entity.PermissionType{
452 entity.PermissionTypeDisplay,452 entity.PermissionTypeDisplay,
453- }, callback)453+ }, nil, callback)
454 454
455 assert.True(t, denied, "display should be denied when manual override is denied")455 assert.True(t, denied, "display should be denied when manual override is denied")
456 dialog.AssertNotCalled(t, "ShowPermissionDialog")456 dialog.AssertNotCalled(t, "ShowPermissionDialog")
@@ -526,7 +526,7 @@ func TestHandlePermissionUseCase_UsesInjectedLoggerFactory(t *testing.T) {
526 }526 }
527 527
528 uc := usecase.NewHandlePermissionUseCase(permRepo, dialog, loggerFactory)528 uc := usecase.NewHandlePermissionUseCase(permRepo, dialog, loggerFactory)
529- uc.HandlePermissionRequest(ctx, "", []entity.PermissionType{entity.PermissionTypeMicrophone}, usecase.PermissionCallback{529+ uc.HandlePermissionRequest(ctx, "", []entity.PermissionType{entity.PermissionTypeMicrophone}, nil, usecase.PermissionCallback{
530 Allow: func() {},530 Allow: func() {},
531 Deny: func() {},531 Deny: func() {},
532 })532 })
@@ -551,13 +551,19 @@ func TestHandlePermissionUseCase_WebsiteDataAccess_ShowsDialogAndPersists(t *tes
551 Deny: func() {},551 Deny: func() {},
552 }552 }
553 553
554+ meta := entity.PermissionMetadata{
555+ "requesting_domain": "accounts.google.com",
556+ "current_domain": "shop.example.com",
557+ }
558+
554 // Dialog shows and user clicks "Always Allow"559 // Dialog shows and user clicks "Always Allow"
555 dialog.EXPECT().ShowPermissionDialog(560 dialog.EXPECT().ShowPermissionDialog(
556 mock.Anything,561 mock.Anything,
557 "https://shop.example.com",562 "https://shop.example.com",
558 []entity.PermissionType{entity.PermissionTypeWebsiteDataAccess},563 []entity.PermissionType{entity.PermissionTypeWebsiteDataAccess},
564+ meta,
559 mock.Anything,565 mock.Anything,
560- ).Run(func(_ context.Context, _ string, _ []entity.PermissionType, cb func(port.PermissionDialogResult)) {566+ ).Run(func(_ context.Context, _ string, _ []entity.PermissionType, _ entity.PermissionMetadata, cb func(port.PermissionDialogResult)) {
561 cb(port.PermissionDialogResult{Allowed: true, Persistent: true})567 cb(port.PermissionDialogResult{Allowed: true, Persistent: true})
562 })568 })
563 569
@@ -571,7 +577,7 @@ func TestHandlePermissionUseCase_WebsiteDataAccess_ShowsDialogAndPersists(t *tes
571 577
572 uc.HandlePermissionRequest(ctx, "https://shop.example.com", []entity.PermissionType{578 uc.HandlePermissionRequest(ctx, "https://shop.example.com", []entity.PermissionType{
573 entity.PermissionTypeWebsiteDataAccess,579 entity.PermissionTypeWebsiteDataAccess,
574- }, callback)580+ }, meta, callback)
575 581
576 assert.True(t, allowed)582 assert.True(t, allowed)
577 }583 }
@@ -599,7 +605,7 @@ func TestHandlePermissionUseCase_WebsiteDataAccess_UsesStoredGrant(t *testing.T)
599 605
600 uc.HandlePermissionRequest(ctx, "https://shop.example.com", []entity.PermissionType{606 uc.HandlePermissionRequest(ctx, "https://shop.example.com", []entity.PermissionType{
601 entity.PermissionTypeWebsiteDataAccess,607 entity.PermissionTypeWebsiteDataAccess,
602- }, callback)608+ }, entity.PermissionMetadata{"requesting_domain": "accounts.google.com", "current_domain": "shop.example.com"}, callback)
603 609
604 assert.True(t, allowed, "should use stored granted permission without dialog")610 assert.True(t, allowed, "should use stored granted permission without dialog")
605 dialog.AssertNotCalled(t, "ShowPermissionDialog")611 dialog.AssertNotCalled(t, "ShowPermissionDialog")
M internal/domain/entity/permission.go +4 -0
@@ -38,6 +38,10 @@ const (
3838 PermissionTypeWebsiteDataAccess PermissionType = "website_data_access"
3939 )
4040
41+// PermissionMetadata carries extra context for specific permission types.
42+// Keys are permission-type-specific (e.g., "requesting_domain" for website_data_access).
43+type PermissionMetadata map[string]string
44+
4145 // PermissionDecision represents the user's decision for a permission.
4246 type PermissionDecision string
4347
@@ -38,6 +38,10 @@ const (
38 PermissionTypeWebsiteDataAccess PermissionType = "website_data_access"38 PermissionTypeWebsiteDataAccess PermissionType = "website_data_access"
39 )39 )
40 40
41+// PermissionMetadata carries extra context for specific permission types.
42+// Keys are permission-type-specific (e.g., "requesting_domain" for website_data_access).
43+type PermissionMetadata map[string]string
44+
41 // PermissionDecision represents the user's decision for a permission.45 // PermissionDecision represents the user's decision for a permission.
42 type PermissionDecision string46 type PermissionDecision string
43 47
M internal/infrastructure/webkit/webview.go +15 -13
@@ -162,7 +162,8 @@ type WebView struct {
162162 // PermissionRequest is called when a site requests permission (mic, camera, screen sharing).
163163 // Return true to indicate the request was handled. Call allow()/deny() to respond.
164164 // The permission types are determined from the request object.
165- OnPermissionRequest func(origin string, permTypes []string, allow, deny func()) bool
165+ // The metadata map carries permission-type-specific context (e.g., "requesting_domain" for website_data_access).
166+ OnPermissionRequest func(origin string, permTypes []string, metadata map[string]string, allow, deny func()) bool
166167
167168 logger zerolog.Logger
168169 mu sync.RWMutex
@@ -998,14 +999,11 @@ func (wv *WebView) connectPermissionRequestSignal() {
998999 }
9991000
10001001 // Determine permission types from the request
1001- permTypes, originOverride := wv.determinePermissionTypes(ctx, requestPtr)
1002+ permTypes, metadata := wv.determinePermissionTypes(ctx, requestPtr)
10021003 if len(permTypes) == 0 {
10031004 wv.logger.Warn().Msg("permission request with unknown type, denying")
10041005 return false
10051006 }
1006- if originOverride != "" {
1007- origin = originOverride
1008- }
10091007
10101008 // Ref the request object to prevent use-after-free
10111009 // The request may outlive the signal handler if we show a dialog
@@ -1039,7 +1037,7 @@ func (wv *WebView) connectPermissionRequestSignal() {
10391037 }
10401038
10411039 // Call the handler
1042- return wv.OnPermissionRequest(origin, permTypes, allow, deny)
1040+ return wv.OnPermissionRequest(origin, permTypes, metadata, allow, deny)
10431041 }
10441042
10451043 sigID := wv.inner.ConnectPermissionRequest(&permissionCb)
@@ -1051,13 +1049,13 @@ func (wv *WebView) connectPermissionRequestSignal() {
10511049 // We use GObject property accessors for audio/video request flags. Display detection
10521050 // uses the dedicated WebKit API because this WebKit build does not expose an
10531051 // "is-for-display-device" GObject property.
1054-func (wv *WebView) determinePermissionTypes(ctx context.Context, requestPtr uintptr) (types []string, originOverride string) {
1052+func (wv *WebView) determinePermissionTypes(ctx context.Context, requestPtr uintptr) (types []string, metadata map[string]string) {
10551053 requestKind := detectPermissionRequestKind(ctx, requestPtr)
10561054 switch requestKind {
10571055 case permissionRequestKindUserMedia:
10581056 userMediaReq := webkit.UserMediaPermissionRequestNewFromInternalPtr(requestPtr)
10591057 if userMediaReq == nil {
1060- return nil, ""
1058+ return nil, nil
10611059 }
10621060
10631061 // Use GObject property accessors — more reliable than the C function wrappers
@@ -1072,13 +1070,13 @@ func (wv *WebView) determinePermissionTypes(ctx context.Context, requestPtr uint
10721070 Bool("is_display", isDisplay).
10731071 Msg("permission request type detection")
10741072
1075- return classifyPermissionRequestTypes(ctx, requestKind, isAudio, isVideo, isDisplay), ""
1073+ return classifyPermissionRequestTypes(ctx, requestKind, isAudio, isVideo, isDisplay), nil
10761074 case permissionRequestKindDeviceInfo:
1077- return classifyPermissionRequestTypes(ctx, requestKind, false, false, false), ""
1075+ return classifyPermissionRequestTypes(ctx, requestKind, false, false, false), nil
10781076 case permissionRequestKindWebsiteDataAccess:
10791077 wdaReq := webkit.WebsiteDataAccessPermissionRequestNewFromInternalPtr(requestPtr)
10801078 if wdaReq == nil {
1081- return nil, ""
1079+ return nil, nil
10821080 }
10831081 currentDomain := wdaReq.GetCurrentDomain()
10841082 requestingDomain := wdaReq.GetRequestingDomain()
@@ -1086,7 +1084,11 @@ func (wv *WebView) determinePermissionTypes(ctx context.Context, requestPtr uint
10861084 Str("current_domain", currentDomain).
10871085 Str("requesting_domain", requestingDomain).
10881086 Msg("website data access permission request")
1089- return classifyPermissionRequestTypes(ctx, requestKind, false, false, false), ""
1087+ meta := map[string]string{
1088+ "requesting_domain": requestingDomain,
1089+ "current_domain": currentDomain,
1090+ }
1091+ return classifyPermissionRequestTypes(ctx, requestKind, false, false, false), meta
10901092 default:
10911093 if requestPtr != 0 {
10921094 typeName := permissionRequestTypeName(ctx, requestPtr)
@@ -1098,7 +1100,7 @@ func (wv *WebView) determinePermissionTypes(ctx context.Context, requestPtr uint
10981100 }
10991101 // Unknown permission type - could be clipboard, notifications, geolocation, etc.
11001102 // For now, return empty to trigger denial. Future phases will add these types.
1101- return nil, ""
1103+ return nil, nil
11021104 }
11031105 }
11041106
@@ -162,7 +162,8 @@ type WebView struct {
162 // PermissionRequest is called when a site requests permission (mic, camera, screen sharing).162 // PermissionRequest is called when a site requests permission (mic, camera, screen sharing).
163 // Return true to indicate the request was handled. Call allow()/deny() to respond.163 // Return true to indicate the request was handled. Call allow()/deny() to respond.
164 // The permission types are determined from the request object.164 // The permission types are determined from the request object.
165- OnPermissionRequest func(origin string, permTypes []string, allow, deny func()) bool165+ // The metadata map carries permission-type-specific context (e.g., "requesting_domain" for website_data_access).
166+ OnPermissionRequest func(origin string, permTypes []string, metadata map[string]string, allow, deny func()) bool
166 167
167 logger zerolog.Logger168 logger zerolog.Logger
168 mu sync.RWMutex169 mu sync.RWMutex
@@ -998,14 +999,11 @@ func (wv *WebView) connectPermissionRequestSignal() {
998 }999 }
999 1000
1000 // Determine permission types from the request1001 // Determine permission types from the request
1001- permTypes, originOverride := wv.determinePermissionTypes(ctx, requestPtr)1002+ permTypes, metadata := wv.determinePermissionTypes(ctx, requestPtr)
1002 if len(permTypes) == 0 {1003 if len(permTypes) == 0 {
1003 wv.logger.Warn().Msg("permission request with unknown type, denying")1004 wv.logger.Warn().Msg("permission request with unknown type, denying")
1004 return false1005 return false
1005 }1006 }
1006- if originOverride != "" {
1007- origin = originOverride
1008- }
1009 1007
1010 // Ref the request object to prevent use-after-free1008 // Ref the request object to prevent use-after-free
1011 // The request may outlive the signal handler if we show a dialog1009 // The request may outlive the signal handler if we show a dialog
@@ -1039,7 +1037,7 @@ func (wv *WebView) connectPermissionRequestSignal() {
1039 }1037 }
1040 1038
1041 // Call the handler1039 // Call the handler
1042- return wv.OnPermissionRequest(origin, permTypes, allow, deny)1040+ return wv.OnPermissionRequest(origin, permTypes, metadata, allow, deny)
1043 }1041 }
1044 1042
1045 sigID := wv.inner.ConnectPermissionRequest(&permissionCb)1043 sigID := wv.inner.ConnectPermissionRequest(&permissionCb)
@@ -1051,13 +1049,13 @@ func (wv *WebView) connectPermissionRequestSignal() {
1051 // We use GObject property accessors for audio/video request flags. Display detection1049 // We use GObject property accessors for audio/video request flags. Display detection
1052 // uses the dedicated WebKit API because this WebKit build does not expose an1050 // uses the dedicated WebKit API because this WebKit build does not expose an
1053 // "is-for-display-device" GObject property.1051 // "is-for-display-device" GObject property.
1054-func (wv *WebView) determinePermissionTypes(ctx context.Context, requestPtr uintptr) (types []string, originOverride string) {1052+func (wv *WebView) determinePermissionTypes(ctx context.Context, requestPtr uintptr) (types []string, metadata map[string]string) {
1055 requestKind := detectPermissionRequestKind(ctx, requestPtr)1053 requestKind := detectPermissionRequestKind(ctx, requestPtr)
1056 switch requestKind {1054 switch requestKind {
1057 case permissionRequestKindUserMedia:1055 case permissionRequestKindUserMedia:
1058 userMediaReq := webkit.UserMediaPermissionRequestNewFromInternalPtr(requestPtr)1056 userMediaReq := webkit.UserMediaPermissionRequestNewFromInternalPtr(requestPtr)
1059 if userMediaReq == nil {1057 if userMediaReq == nil {
1060- return nil, ""1058+ return nil, nil
1061 }1059 }
1062 1060
1063 // Use GObject property accessors — more reliable than the C function wrappers1061 // Use GObject property accessors — more reliable than the C function wrappers
@@ -1072,13 +1070,13 @@ func (wv *WebView) determinePermissionTypes(ctx context.Context, requestPtr uint
1072 Bool("is_display", isDisplay).1070 Bool("is_display", isDisplay).
1073 Msg("permission request type detection")1071 Msg("permission request type detection")
1074 1072
1075- return classifyPermissionRequestTypes(ctx, requestKind, isAudio, isVideo, isDisplay), ""1073+ return classifyPermissionRequestTypes(ctx, requestKind, isAudio, isVideo, isDisplay), nil
1076 case permissionRequestKindDeviceInfo:1074 case permissionRequestKindDeviceInfo:
1077- return classifyPermissionRequestTypes(ctx, requestKind, false, false, false), ""1075+ return classifyPermissionRequestTypes(ctx, requestKind, false, false, false), nil
1078 case permissionRequestKindWebsiteDataAccess:1076 case permissionRequestKindWebsiteDataAccess:
1079 wdaReq := webkit.WebsiteDataAccessPermissionRequestNewFromInternalPtr(requestPtr)1077 wdaReq := webkit.WebsiteDataAccessPermissionRequestNewFromInternalPtr(requestPtr)
1080 if wdaReq == nil {1078 if wdaReq == nil {
1081- return nil, ""1079+ return nil, nil
1082 }1080 }
1083 currentDomain := wdaReq.GetCurrentDomain()1081 currentDomain := wdaReq.GetCurrentDomain()
1084 requestingDomain := wdaReq.GetRequestingDomain()1082 requestingDomain := wdaReq.GetRequestingDomain()
@@ -1086,7 +1084,11 @@ func (wv *WebView) determinePermissionTypes(ctx context.Context, requestPtr uint
1086 Str("current_domain", currentDomain).1084 Str("current_domain", currentDomain).
1087 Str("requesting_domain", requestingDomain).1085 Str("requesting_domain", requestingDomain).
1088 Msg("website data access permission request")1086 Msg("website data access permission request")
1089- return classifyPermissionRequestTypes(ctx, requestKind, false, false, false), ""1087+ meta := map[string]string{
1088+ "requesting_domain": requestingDomain,
1089+ "current_domain": currentDomain,
1090+ }
1091+ return classifyPermissionRequestTypes(ctx, requestKind, false, false, false), meta
1090 default:1092 default:
1091 if requestPtr != 0 {1093 if requestPtr != 0 {
1092 typeName := permissionRequestTypeName(ctx, requestPtr)1094 typeName := permissionRequestTypeName(ctx, requestPtr)
@@ -1098,7 +1100,7 @@ func (wv *WebView) determinePermissionTypes(ctx context.Context, requestPtr uint
1098 }1100 }
1099 // Unknown permission type - could be clipboard, notifications, geolocation, etc.1101 // Unknown permission type - could be clipboard, notifications, geolocation, etc.
1100 // For now, return empty to trigger denial. Future phases will add these types.1102 // For now, return empty to trigger denial. Future phases will add these types.
1101- return nil, ""1103+ return nil, nil
1102 }1104 }
1103 }1105 }
1104 1106
M internal/ui/coordinator/content/callbacks.go +4 -3
@@ -117,8 +117,8 @@ func (c *Coordinator) setupWebViewCallbacks(ctx context.Context, paneID entity.P
117117 Msg("failed to load crash page after web process termination")
118118 }
119119 },
120- OnPermissionRequest: func(origin string, permTypes []string, allow, deny func()) bool {
121- return c.handlePermissionRequest(ctx, origin, permTypes, allow, deny)
120+ OnPermissionRequest: func(origin string, permTypes []string, metadata map[string]string, allow, deny func()) bool {
121+ return c.handlePermissionRequest(ctx, origin, permTypes, metadata, allow, deny)
122122 },
123123 }
124124
@@ -191,6 +191,7 @@ func (c *Coordinator) handlePermissionRequest(
191191 ctx context.Context,
192192 origin string,
193193 permTypes []string,
194+ metadata map[string]string,
194195 allow, deny func(),
195196 ) bool {
196197 log := logging.FromContext(ctx)
@@ -264,7 +265,7 @@ func (c *Coordinator) handlePermissionRequest(
264265 Deny: wrappedDeny,
265266 }
266267
267- c.permissionUC.HandlePermissionRequest(ctx, origin, entityTypes, callback)
268+ c.permissionUC.HandlePermissionRequest(ctx, origin, entityTypes, entity.PermissionMetadata(metadata), callback)
268269 return true
269270 }
270271
@@ -117,8 +117,8 @@ func (c *Coordinator) setupWebViewCallbacks(ctx context.Context, paneID entity.P
117 Msg("failed to load crash page after web process termination")117 Msg("failed to load crash page after web process termination")
118 }118 }
119 },119 },
120- OnPermissionRequest: func(origin string, permTypes []string, allow, deny func()) bool {120+ OnPermissionRequest: func(origin string, permTypes []string, metadata map[string]string, allow, deny func()) bool {
121- return c.handlePermissionRequest(ctx, origin, permTypes, allow, deny)121+ return c.handlePermissionRequest(ctx, origin, permTypes, metadata, allow, deny)
122 },122 },
123 }123 }
124 124
@@ -191,6 +191,7 @@ func (c *Coordinator) handlePermissionRequest(
191 ctx context.Context,191 ctx context.Context,
192 origin string,192 origin string,
193 permTypes []string,193 permTypes []string,
194+ metadata map[string]string,
194 allow, deny func(),195 allow, deny func(),
195 ) bool {196 ) bool {
196 log := logging.FromContext(ctx)197 log := logging.FromContext(ctx)
@@ -264,7 +265,7 @@ func (c *Coordinator) handlePermissionRequest(
264 Deny: wrappedDeny,265 Deny: wrappedDeny,
265 }266 }
266 267
267- c.permissionUC.HandlePermissionRequest(ctx, origin, entityTypes, callback)268+ c.permissionUC.HandlePermissionRequest(ctx, origin, entityTypes, entity.PermissionMetadata(metadata), callback)
268 return true269 return true
269 }270 }
270 271
M internal/ui/dialog/permission.go +13 -5
@@ -21,6 +21,7 @@ type permissionDialogRequest struct {
2121 ctx context.Context
2222 origin string
2323 permTypes []entity.PermissionType
24+ metadata entity.PermissionMetadata
2425 callback func(result port.PermissionDialogResult)
2526 }
2627
@@ -48,12 +49,14 @@ func (d *PermissionDialog) ShowPermissionDialog(
4849 ctx context.Context,
4950 origin string,
5051 permTypes []entity.PermissionType,
52+ metadata entity.PermissionMetadata,
5153 callback func(result port.PermissionDialogResult),
5254 ) {
5355 req := permissionDialogRequest{
5456 ctx: ctx,
5557 origin: origin,
5658 permTypes: permTypes,
59+ metadata: metadata,
5760 callback: callback,
5861 }
5962
@@ -94,7 +97,7 @@ func (d *PermissionDialog) showRequest(req permissionDialogRequest) {
9497
9598 // Build dialog text
9699 heading := d.buildHeading(permTypes)
97- body := d.buildBody(origin, permTypes)
100+ body := d.buildBody(origin, permTypes, req.metadata)
98101
99102 d.popup.Show(ctx, heading, body, func(allowed, persistent bool) {
100103 log.Debug().
@@ -198,7 +201,7 @@ func (d *PermissionDialog) buildHeading(
198201
199202 // buildBody creates the dialog body text.
200203 func (d *PermissionDialog) buildBody(
201- origin string, permTypes []entity.PermissionType,
204+ origin string, permTypes []entity.PermissionType, metadata entity.PermissionMetadata,
202205 ) string {
203206 f := parsePermFlags(permTypes)
204207 var parts []string
@@ -213,9 +216,14 @@ func (d *PermissionDialog) buildBody(
213216 parts = append(parts, "share your screen")
214217 }
215218 if f.dataAccess {
216- if len(parts) == 0 {
217- parts = append(parts,
218- "access its stored data while you browse this site")
219+ reqDomain := metadata["requesting_domain"]
220+ curDomain := metadata["current_domain"]
221+ if reqDomain != "" && curDomain != "" {
222+ parts = append(parts, fmt.Sprintf(
223+ "allow %s to access its data (including cookies) while you browse %s",
224+ reqDomain, curDomain))
225+ } else if len(parts) == 0 {
226+ parts = append(parts, "access its stored data while you browse this site")
219227 } else {
220228 parts = append(parts, "access its stored data")
221229 }
@@ -21,6 +21,7 @@ type permissionDialogRequest struct {
21 ctx context.Context21 ctx context.Context
22 origin string22 origin string
23 permTypes []entity.PermissionType23 permTypes []entity.PermissionType
24+ metadata entity.PermissionMetadata
24 callback func(result port.PermissionDialogResult)25 callback func(result port.PermissionDialogResult)
25 }26 }
26 27
@@ -48,12 +49,14 @@ func (d *PermissionDialog) ShowPermissionDialog(
48 ctx context.Context,49 ctx context.Context,
49 origin string,50 origin string,
50 permTypes []entity.PermissionType,51 permTypes []entity.PermissionType,
52+ metadata entity.PermissionMetadata,
51 callback func(result port.PermissionDialogResult),53 callback func(result port.PermissionDialogResult),
52 ) {54 ) {
53 req := permissionDialogRequest{55 req := permissionDialogRequest{
54 ctx: ctx,56 ctx: ctx,
55 origin: origin,57 origin: origin,
56 permTypes: permTypes,58 permTypes: permTypes,
59+ metadata: metadata,
57 callback: callback,60 callback: callback,
58 }61 }
59 62
@@ -94,7 +97,7 @@ func (d *PermissionDialog) showRequest(req permissionDialogRequest) {
94 97
95 // Build dialog text98 // Build dialog text
96 heading := d.buildHeading(permTypes)99 heading := d.buildHeading(permTypes)
97- body := d.buildBody(origin, permTypes)100+ body := d.buildBody(origin, permTypes, req.metadata)
98 101
99 d.popup.Show(ctx, heading, body, func(allowed, persistent bool) {102 d.popup.Show(ctx, heading, body, func(allowed, persistent bool) {
100 log.Debug().103 log.Debug().
@@ -198,7 +201,7 @@ func (d *PermissionDialog) buildHeading(
198 201
199 // buildBody creates the dialog body text.202 // buildBody creates the dialog body text.
200 func (d *PermissionDialog) buildBody(203 func (d *PermissionDialog) buildBody(
201- origin string, permTypes []entity.PermissionType,204+ origin string, permTypes []entity.PermissionType, metadata entity.PermissionMetadata,
202 ) string {205 ) string {
203 f := parsePermFlags(permTypes)206 f := parsePermFlags(permTypes)
204 var parts []string207 var parts []string
@@ -213,9 +216,14 @@ func (d *PermissionDialog) buildBody(
213 parts = append(parts, "share your screen")216 parts = append(parts, "share your screen")
214 }217 }
215 if f.dataAccess {218 if f.dataAccess {
216- if len(parts) == 0 {219+ reqDomain := metadata["requesting_domain"]
217- parts = append(parts,220+ curDomain := metadata["current_domain"]
218- "access its stored data while you browse this site")221+ if reqDomain != "" && curDomain != "" {
222+ parts = append(parts, fmt.Sprintf(
223+ "allow %s to access its data (including cookies) while you browse %s",
224+ reqDomain, curDomain))
225+ } else if len(parts) == 0 {
226+ parts = append(parts, "access its stored data while you browse this site")
219 } else {227 } else {
220 parts = append(parts, "access its stored data")228 parts = append(parts, "access its stored data")
221 }229 }
M internal/ui/dialog/permission_test.go +14 -1
@@ -51,6 +51,7 @@ func TestPermissionDialog_QueuesRequestsWhilePopupVisible(t *testing.T) {
5151 context.Background(),
5252 "https://example.com",
5353 []entity.PermissionType{entity.PermissionTypeMicrophone},
54+ nil,
5455 func(result port.PermissionDialogResult) {
5556 firstCalled = true
5657 firstResult = result
@@ -63,6 +64,7 @@ func TestPermissionDialog_QueuesRequestsWhilePopupVisible(t *testing.T) {
6364 context.Background(),
6465 "https://example.com",
6566 []entity.PermissionType{entity.PermissionTypeCamera},
67+ nil,
6668 func(result port.PermissionDialogResult) {
6769 secondCalled = true
6870 secondResult = result
@@ -98,6 +100,7 @@ func TestPermissionDialog_NoPopup_DeniesRequest(t *testing.T) {
98100 context.Background(),
99101 "https://example.com",
100102 []entity.PermissionType{entity.PermissionTypeMicrophone},
103+ nil,
101104 func(r port.PermissionDialogResult) {
102105 called = true
103106 result = r
@@ -153,7 +156,17 @@ func TestPermissionDialog_BuildHeadingAndBody_DisplayCombinations(t *testing.T)
153156 for _, tt := range tests {
154157 t.Run(tt.name, func(t *testing.T) {
155158 assert.Equal(t, tt.expectHeading, d.buildHeading(tt.permTypes))
156- assert.Equal(t, origin+" wants to "+tt.expectedAction+".", d.buildBody(origin, tt.permTypes))
159+ assert.Equal(t, origin+" wants to "+tt.expectedAction+".", d.buildBody(origin, tt.permTypes, nil))
157160 })
158161 }
162+
163+ // Test website_data_access with populated metadata (Epiphany-style rich message)
164+ t.Run("website data access with both domains", func(t *testing.T) {
165+ meta := entity.PermissionMetadata{
166+ "requesting_domain": "accounts.google.com",
167+ "current_domain": "shop.example.com",
168+ }
169+ body := d.buildBody(origin, []entity.PermissionType{entity.PermissionTypeWebsiteDataAccess}, meta)
170+ assert.Equal(t, origin+" wants to allow accounts.google.com to access its data (including cookies) while you browse shop.example.com.", body)
171+ })
159172 }
@@ -51,6 +51,7 @@ func TestPermissionDialog_QueuesRequestsWhilePopupVisible(t *testing.T) {
51 context.Background(),51 context.Background(),
52 "https://example.com",52 "https://example.com",
53 []entity.PermissionType{entity.PermissionTypeMicrophone},53 []entity.PermissionType{entity.PermissionTypeMicrophone},
54+ nil,
54 func(result port.PermissionDialogResult) {55 func(result port.PermissionDialogResult) {
55 firstCalled = true56 firstCalled = true
56 firstResult = result57 firstResult = result
@@ -63,6 +64,7 @@ func TestPermissionDialog_QueuesRequestsWhilePopupVisible(t *testing.T) {
63 context.Background(),64 context.Background(),
64 "https://example.com",65 "https://example.com",
65 []entity.PermissionType{entity.PermissionTypeCamera},66 []entity.PermissionType{entity.PermissionTypeCamera},
67+ nil,
66 func(result port.PermissionDialogResult) {68 func(result port.PermissionDialogResult) {
67 secondCalled = true69 secondCalled = true
68 secondResult = result70 secondResult = result
@@ -98,6 +100,7 @@ func TestPermissionDialog_NoPopup_DeniesRequest(t *testing.T) {
98 context.Background(),100 context.Background(),
99 "https://example.com",101 "https://example.com",
100 []entity.PermissionType{entity.PermissionTypeMicrophone},102 []entity.PermissionType{entity.PermissionTypeMicrophone},
103+ nil,
101 func(r port.PermissionDialogResult) {104 func(r port.PermissionDialogResult) {
102 called = true105 called = true
103 result = r106 result = r
@@ -153,7 +156,17 @@ func TestPermissionDialog_BuildHeadingAndBody_DisplayCombinations(t *testing.T)
153 for _, tt := range tests {156 for _, tt := range tests {
154 t.Run(tt.name, func(t *testing.T) {157 t.Run(tt.name, func(t *testing.T) {
155 assert.Equal(t, tt.expectHeading, d.buildHeading(tt.permTypes))158 assert.Equal(t, tt.expectHeading, d.buildHeading(tt.permTypes))
156- assert.Equal(t, origin+" wants to "+tt.expectedAction+".", d.buildBody(origin, tt.permTypes))159+ assert.Equal(t, origin+" wants to "+tt.expectedAction+".", d.buildBody(origin, tt.permTypes, nil))
157 })160 })
158 }161 }
162+
163+ // Test website_data_access with populated metadata (Epiphany-style rich message)
164+ t.Run("website data access with both domains", func(t *testing.T) {
165+ meta := entity.PermissionMetadata{
166+ "requesting_domain": "accounts.google.com",
167+ "current_domain": "shop.example.com",
168+ }
169+ body := d.buildBody(origin, []entity.PermissionType{entity.PermissionTypeWebsiteDataAccess}, meta)
170+ assert.Equal(t, origin+" wants to allow accounts.google.com to access its data (including cookies) while you browse shop.example.com.", body)
171+ })
159 }172 }

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 42

↑↓ moveenter openesc close