Auto-indexing service and GraphQL API for AT Protocol Records quickslice.slices.network/
atproto gleam graphql

feat(notifications): use authenticated user DID instead of viewerDid argument

Remove viewerDid argument from notifications query. Now uses viewer_did
from auth token context, matching the pattern used by viewer state fields.
Unauthenticated requests return 'notifications query requires authentication'.

- Remove viewerDid from build_notification_query_args
- Update resolver to use get_viewer_did_from_context
- Update tests to use OAuth authentication with test tokens

+285 -16
+254
dev-docs/plans/2025-12-27-notifications-auth.md
··· 1 + # Notifications Query Authentication Implementation Plan 2 + 3 + > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. 4 + 5 + **Goal:** Remove `viewerDid` argument from notifications query and use authenticated user's DID from auth token instead. 6 + 7 + **Architecture:** The notifications query currently requires a `viewerDid` argument passed by the client. We'll change it to use the same pattern as viewer state fields: read `viewer_did` from context variables (injected by server from auth token). Unauthenticated requests will return an error. 8 + 9 + **Tech Stack:** Gleam, lexicon_graphql schema, GraphQL 10 + 11 + --- 12 + 13 + ### Task 1: Update Schema - Remove viewerDid Argument 14 + 15 + **Files:** 16 + - Modify: `lexicon_graphql/src/lexicon_graphql/schema/database.gleam:2296-2302` 17 + 18 + **Step 1: Remove viewerDid from build_notification_query_args** 19 + 20 + Find and remove the `viewerDid` argument from `build_notification_query_args`: 21 + 22 + ```gleam 23 + // BEFORE (lines 2296-2302): 24 + let base_args = [ 25 + schema.argument( 26 + "viewerDid", 27 + schema.non_null(schema.string_type()), 28 + "DID of the viewer to get notifications for", 29 + option.None, 30 + ), 31 + schema.argument( 32 + 33 + // AFTER: 34 + let base_args = [ 35 + schema.argument( 36 + ``` 37 + 38 + Remove the entire `schema.argument("viewerDid", ...)` block (lines 2297-2302). 39 + 40 + **Step 2: Verify removal compiles** 41 + 42 + Run: `cd /Users/chadmiller/code/quickslice && gleam build` 43 + Expected: Compilation succeeds (resolver still has the old code but we'll fix that next) 44 + 45 + --- 46 + 47 + ### Task 2: Update Schema - Use get_viewer_did_from_context 48 + 49 + **Files:** 50 + - Modify: `lexicon_graphql/src/lexicon_graphql/schema/database.gleam:2205-2272` 51 + 52 + **Step 1: Update resolver to use get_viewer_did_from_context** 53 + 54 + Replace the resolver's argument extraction with context variable lookup: 55 + 56 + ```gleam 57 + // BEFORE (lines 2205-2272): 58 + fn(ctx: schema.Context) { 59 + // Get viewer DID from context (viewer field should be resolved first) 60 + case schema.get_argument(ctx, "viewerDid") { 61 + option.Some(value.String(viewer_did)) -> { 62 + // ... rest of resolver ... 63 + } 64 + _ -> Error("notifications query requires viewerDid argument") 65 + } 66 + }, 67 + 68 + // AFTER: 69 + fn(ctx: schema.Context) { 70 + // Get viewer DID from auth token (injected by server into variables) 71 + case get_viewer_did_from_context(ctx) { 72 + Ok(viewer_did) -> { 73 + // ... rest of resolver (unchanged) ... 74 + } 75 + Error(Nil) -> Error("notifications query requires authentication") 76 + } 77 + }, 78 + ``` 79 + 80 + The inner resolver logic (lines 2209-2269) remains exactly the same. 81 + 82 + **Step 2: Verify schema compiles** 83 + 84 + Run: `cd /Users/chadmiller/code/quickslice && gleam build` 85 + Expected: Compilation succeeds 86 + 87 + **Step 3: Commit schema changes** 88 + 89 + ```bash 90 + git add lexicon_graphql/src/lexicon_graphql/schema/database.gleam 91 + git commit -m "feat(notifications): use authenticated user DID instead of viewerDid argument 92 + 93 + Remove viewerDid argument from notifications query. Now uses viewer_did 94 + from auth token context, matching the pattern used by viewer state fields. 95 + Unauthenticated requests return 'notifications query requires authentication'." 96 + ``` 97 + 98 + --- 99 + 100 + ### Task 3: Update Tests - Add OAuth Setup 101 + 102 + **Files:** 103 + - Modify: `server/test/graphql/notifications_e2e_test.gleam` 104 + 105 + **Step 1: Add test_helpers import for OAuth setup** 106 + 107 + The file already imports `test_helpers`. Verify it's present at line 17. 108 + 109 + **Step 2: Update notifications_returns_mentioning_records_test** 110 + 111 + Add OAuth table setup and test token after existing table creation (after line 172): 112 + 113 + ```gleam 114 + // After line 172: let assert Ok(_) = test_helpers.create_actor_table(exec) 115 + // Add: 116 + let assert Ok(_) = test_helpers.create_oauth_tables(exec) 117 + let assert Ok(_) = 118 + test_helpers.insert_test_token(exec, "test-notification-token", "did:plc:target") 119 + ``` 120 + 121 + Update the query string (line 235) to remove viewerDid: 122 + 123 + ```gleam 124 + // BEFORE: 125 + notifications(viewerDid: \"did:plc:target\", first: 10) { 126 + 127 + // AFTER: 128 + notifications(first: 10) { 129 + ``` 130 + 131 + Update the execute_query_with_db call to pass auth token (line 258-267): 132 + 133 + ```gleam 134 + // BEFORE: 135 + lexicon_schema.execute_query_with_db( 136 + exec, 137 + query, 138 + "{}", 139 + Error(Nil), 140 + cache, 141 + 142 + // AFTER: 143 + lexicon_schema.execute_query_with_db( 144 + exec, 145 + query, 146 + "{}", 147 + Ok("test-notification-token"), 148 + cache, 149 + ``` 150 + 151 + **Step 3: Update notifications_filters_by_collection_test** 152 + 153 + Add OAuth setup after line 294: 154 + 155 + ```gleam 156 + let assert Ok(_) = test_helpers.create_oauth_tables(exec) 157 + let assert Ok(_) = 158 + test_helpers.insert_test_token(exec, "test-notification-token", "did:plc:target") 159 + ``` 160 + 161 + Update query string (line 335): 162 + 163 + ```gleam 164 + // BEFORE: 165 + notifications(viewerDid: \"did:plc:target\", collections: [APP_BSKY_FEED_LIKE], first: 10) { 166 + 167 + // AFTER: 168 + notifications(collections: [APP_BSKY_FEED_LIKE], first: 10) { 169 + ``` 170 + 171 + Update execute_query_with_db call (line 355): 172 + 173 + ```gleam 174 + Error(Nil), 175 + // change to: 176 + Ok("test-notification-token"), 177 + ``` 178 + 179 + **Step 4: Update notifications_excludes_self_authored_test** 180 + 181 + Add OAuth setup after line 383: 182 + 183 + ```gleam 184 + let assert Ok(_) = test_helpers.create_oauth_tables(exec) 185 + let assert Ok(_) = 186 + test_helpers.insert_test_token(exec, "test-notification-token", "did:plc:target") 187 + ``` 188 + 189 + Update query string (line 406): 190 + 191 + ```gleam 192 + // BEFORE: 193 + notifications(viewerDid: \"did:plc:target\", first: 10) { 194 + 195 + // AFTER: 196 + notifications(first: 10) { 197 + ``` 198 + 199 + Update execute_query_with_db call (line 423): 200 + 201 + ```gleam 202 + Error(Nil), 203 + // change to: 204 + Ok("test-notification-token"), 205 + ``` 206 + 207 + --- 208 + 209 + ### Task 4: Run Tests and Verify 210 + 211 + **Step 1: Run notification tests** 212 + 213 + Run: `cd /Users/chadmiller/code/quickslice/server && gleam test -- --filter notifications` 214 + Expected: All 3 notification tests pass 215 + 216 + **Step 2: Run all tests to check for regressions** 217 + 218 + Run: `cd /Users/chadmiller/code/quickslice && gleam test` 219 + Expected: All tests pass 220 + 221 + **Step 3: Commit test changes** 222 + 223 + ```bash 224 + git add server/test/graphql/notifications_e2e_test.gleam 225 + git commit -m "test(notifications): update tests to use OAuth authentication 226 + 227 + Tests now use test tokens instead of viewerDid argument, matching 228 + the new authentication-based approach for the notifications query." 229 + ``` 230 + 231 + --- 232 + 233 + ### Task 5: Final Verification 234 + 235 + **Step 1: Run full test suite** 236 + 237 + Run: `cd /Users/chadmiller/code/quickslice && gleam test` 238 + Expected: All tests pass 239 + 240 + **Step 2: Verify schema introspection (optional)** 241 + 242 + The notifications query should no longer show viewerDid as a required argument in the schema. 243 + 244 + --- 245 + 246 + ## Summary of Changes 247 + 248 + | File | Change | 249 + |------|--------| 250 + | `lexicon_graphql/src/lexicon_graphql/schema/database.gleam` | Remove `viewerDid` argument, use `get_viewer_did_from_context()` | 251 + | `server/test/graphql/notifications_e2e_test.gleam` | Add OAuth setup, remove `viewerDid` from queries, pass auth token | 252 + 253 + **Error behavior:** 254 + - Unauthenticated requests: `"notifications query requires authentication"`
+4 -10
lexicon_graphql/src/lexicon_graphql/schema/database.gleam
··· 2203 2203 "Query notifications for the authenticated user (records mentioning them)", 2204 2204 notification_args, 2205 2205 fn(ctx: schema.Context) { 2206 - // Get viewer DID from context (viewer field should be resolved first) 2207 - case schema.get_argument(ctx, "viewerDid") { 2208 - option.Some(value.String(viewer_did)) -> { 2206 + // Get viewer DID from auth token (injected by server into variables) 2207 + case get_viewer_did_from_context(ctx) { 2208 + Ok(viewer_did) -> { 2209 2209 // Extract collection filter 2210 2210 let collections = case schema.get_argument(ctx, "collections") { 2211 2211 option.Some(value.List(items)) -> { ··· 2268 2268 ) 2269 2269 Ok(connection.connection_to_value(conn)) 2270 2270 } 2271 - _ -> Error("notifications query requires viewerDid argument") 2271 + Error(Nil) -> Error("notifications query requires authentication") 2272 2272 } 2273 2273 }, 2274 2274 ), ··· 2294 2294 collection_enum: option.Option(schema.Type), 2295 2295 ) -> List(schema.Argument) { 2296 2296 let base_args = [ 2297 - schema.argument( 2298 - "viewerDid", 2299 - schema.non_null(schema.string_type()), 2300 - "DID of the viewer to get notifications for", 2301 - option.None, 2302 - ), 2303 2297 schema.argument( 2304 2298 "first", 2305 2299 schema.int_type(),
+27 -6
server/test/graphql/notifications_e2e_test.gleam
··· 170 170 let assert Ok(_) = test_helpers.create_lexicon_table(exec) 171 171 let assert Ok(_) = test_helpers.create_record_table(exec) 172 172 let assert Ok(_) = test_helpers.create_actor_table(exec) 173 + let assert Ok(_) = test_helpers.create_oauth_tables(exec) 174 + let assert Ok(_) = 175 + test_helpers.insert_test_token( 176 + exec, 177 + "test-notification-token", 178 + "did:plc:target", 179 + ) 173 180 174 181 // Insert lexicons 175 182 let assert Ok(_) = ··· 232 239 let query = 233 240 " 234 241 query { 235 - notifications(viewerDid: \"did:plc:target\", first: 10) { 242 + notifications(first: 10) { 236 243 edges { 237 244 cursor 238 245 node { ··· 259 266 exec, 260 267 query, 261 268 "{}", 262 - Error(Nil), 269 + Ok("test-notification-token"), 263 270 cache, 264 271 option.None, 265 272 "", ··· 292 299 let assert Ok(_) = test_helpers.create_lexicon_table(exec) 293 300 let assert Ok(_) = test_helpers.create_record_table(exec) 294 301 let assert Ok(_) = test_helpers.create_actor_table(exec) 302 + let assert Ok(_) = test_helpers.create_oauth_tables(exec) 303 + let assert Ok(_) = 304 + test_helpers.insert_test_token( 305 + exec, 306 + "test-notification-token", 307 + "did:plc:target", 308 + ) 295 309 296 310 // Insert lexicons 297 311 let assert Ok(_) = ··· 332 346 let query = 333 347 " 334 348 query { 335 - notifications(viewerDid: \"did:plc:target\", collections: [APP_BSKY_FEED_LIKE], first: 10) { 349 + notifications(collections: [APP_BSKY_FEED_LIKE], first: 10) { 336 350 edges { 337 351 cursor 338 352 node { ··· 352 366 exec, 353 367 query, 354 368 "{}", 355 - Error(Nil), 369 + Ok("test-notification-token"), 356 370 cache, 357 371 option.None, 358 372 "", ··· 381 395 let assert Ok(_) = test_helpers.create_lexicon_table(exec) 382 396 let assert Ok(_) = test_helpers.create_record_table(exec) 383 397 let assert Ok(_) = test_helpers.create_actor_table(exec) 398 + let assert Ok(_) = test_helpers.create_oauth_tables(exec) 399 + let assert Ok(_) = 400 + test_helpers.insert_test_token( 401 + exec, 402 + "test-notification-token", 403 + "did:plc:target", 404 + ) 384 405 385 406 // Insert lexicons 386 407 let assert Ok(_) = ··· 403 424 let query = 404 425 " 405 426 query { 406 - notifications(viewerDid: \"did:plc:target\", first: 10) { 427 + notifications(first: 10) { 407 428 edges { 408 429 cursor 409 430 node { ··· 420 441 exec, 421 442 query, 422 443 "{}", 423 - Error(Nil), 444 + Ok("test-notification-token"), 424 445 cache, 425 446 option.None, 426 447 "",