Implement SupabaseStorageAdapter signed URL generation
epic-document-attachments-foundation-task-007 — Add the createSignedUrl(storagePath, expiresInSeconds) method to SupabaseStorageAdapter. The method requests a time-limited signed URL from Supabase Storage for the given object path and returns the URL string. Default expiry should be 3600 seconds. Handle storage errors and return a typed Result. Write unit tests verifying the correct bucket name and path are passed to the Supabase client.
Acceptance Criteria
Technical Requirements
Execution Context
Tier 2 - 518 tasks
Can start after Tier 1 completes
Implementation Notes
Add `createSignedUrl` as a new method on `SupabaseStorageAdapter` (extends the existing class from task-006). Signature: `Future
Validate the expiry range at the top of the method before making the SDK call. Keep `StorageSignedUrlError` in the same sealed error file as `StorageUploadError` and `StorageDeleteError` from task-006 for cohesion. If `SignedUrlResult` (URL + expiry timestamp) is needed by `AttachmentSignedUrlService` (task-002), return it from this method; otherwise a plain `String` is sufficient at the adapter layer.
Testing Requirements
Unit tests using mockito/mocktail to mock `SupabaseStorageFileApi`. Test cases: (1) successful signed URL — verify bucket 'activity-attachments', correct storagePath, and expiresInSeconds are forwarded to SDK, assert returned string equals mock URL; (2) default expiry — call without second argument and verify 3600 is passed to SDK; (3) storage error — SDK throws StorageException, adapter returns/throws StorageSignedUrlError containing the original exception; (4) invalid expiresInSeconds (0 or >86400) — ArgumentError thrown before SDK call. All tests are pure unit tests with no network I/O.
Supabase RLS policies may not cover all query paths (e.g., service-role key usage in edge functions), potentially exposing attachment metadata or objects from another organisation to an unauthorised actor, breaching GDPR requirements.
Mitigation & Contingency
Mitigation: Add org_id scoping as an explicit WHERE clause at the Dart repository level as a second line of defence. Document which queries use the anon key versus service-role key, and audit all edge function calls that touch the storage bucket.
Contingency: If a bypass is discovered post-deployment, immediately revoke the affected signed URLs, rotate the service-role key, add the missing org_id filter, and deploy a patch. Notify affected organisations per GDPR breach protocol.
Supabase free/pro tier storage quotas may be exceeded earlier than expected if organisations upload large PDFs frequently, causing upload failures with no graceful degradation for users.
Mitigation & Contingency
Mitigation: Configure a 10 MB per-file cap enforced in the upload service (Epic 2), and add a storage usage monitoring alert at 80% of the allocated quota. Document the upgrade path in runbooks.
Contingency: If the quota is hit, temporarily disable new uploads via the org-level feature flag (attachments_enabled) and upgrade the Supabase plan. Communicate clearly to affected coordinators with an estimated restoration time.
The feature documentation specifies a migration order dependency: the activity_attachments table must be created after the activities table and before the Bufdir export join query is updated. Running migrations out of order will cause foreign-key or join failures.
Mitigation & Contingency
Mitigation: Add the migration to the numbered Supabase migration sequence immediately after the activities table migration. Add a CI check that runs migrations in order against a clean schema.
Contingency: If a deployment runs migrations out of order, roll back via the Supabase migration rollback script, reorder, and redeploy. No data loss occurs as attachments do not exist yet at that point.